RA

[PokéTibia / TFS 0.3.6] Nurse Joy com janela — Curar, Registrar e Fechar

Por raulcdj, há 10h em Mods, funções e outros

1
14
raulcdjR
01 de outubro de 2026 às 17:07

Nurse Joy com janela: Curar, Registrar e Fechar — PokéTibia / TFS 0.3.6

Compartilhando a Nurse Joy da minha base com dialogo no OTClient. A janela mostra o nome e outfit da Nurse, mensagem e botoes Curar/Registrar/Fechar. Cura o treinador, Pokemon solto quando permitido, Ball do slot 8 e Balls da mochila; restaura HP, cooldowns move1..move15, felicidade e fome conforme a configuracao da base. O registro libera uma cidade no sistema de Teleport existente. Nao implementa o Teleport completo.

E uma aplicacao do sistema de dialogo por clique esquerdo que compartilhei antes. O pacote abaixo inclui os arquivos das duas pontas, para nao depender de baixar outro topico. Edicao publica adaptada: JSON no transporte, registro explicitamente configurado no XML e verificacao de mochila valida. A base em uso nao foi alterada. A imagem de demonstracao corresponde a base original; na edicao publica a fonte usa verdana-11px-rounded e pode mudar ligeiramente a aparencia. O fundo window.png do modulo foi mantido.

DEPENDENCIAS: OTClient com extended opcodes, json.decode do corelib, UICreature, estilos Button/Label/ScrollablePanel e fonte verdana-11px-rounded. TFS 0.3.6 / PokexGeneration com NpcSystem, FocusModule, doSendPlayerExtendedOpcode e os helpers/tabelas da sua base. Nao funciona somente copiando heal.lua num TFS puro.

Servidor: copie os arquivos server/data para as mesmas pastas de data. A biblioteca npcdialog_lib.lua deve carregar tambem no ambiente dos NPCs. Nurse.xml aponta para heal.lua; se sua Nurse tem outro XML, preserve seu look/nome e altere apenas script e parametros. Configure/coloque a Nurse no mapa/spawn como de costume. O lookType 532 pertence a esta base; use um ID valido no seu DAT. Faça backup antes de substituir heal.lua: todas as Nurses que o usam receberao a mudanca.

Registro de cidade: ajuste no XML <parameter key="teleportStorage" value="0"/>. Zero deixa o registro desativado. Use a storage que seu menu de Teleport realmente consulta para aquela cidade; exemplo: 897530 SOMENTE se seu Teleport usa esse numero. Registrar grava 1 nessa storage; nao teleporta, nao muda automaticamente townId e nao adiciona destinos ao menu. Nurses de cidades diferentes devem usar storages diferentes. Na edicao publica, Curar nao cadastra cidade automaticamente. Coordenadas particulares e a busca automatica do templo mais proximo foram retiradas para evitar registrar cidade errada em outro mapa.

Cliente: copie os arquivos client/modules/game_npcdialog para modules/game_npcdialog e carregue o modulo depois de corelib/game_interface. Se ja instalou a edicao JSON do meu topico anterior, atualize somente o OTUI/fundo para esta aparencia e use os scripts Nurse do servidor; nao registre o opcode duas vezes. Nao misture o servidor JSON com cliente antigo que usa loadstring/table.serialize.

Clique esquerdo: o bloco client/clique_esquerdo.lua vai DENTRO de processMouseAction em game_interface/gameinterface.lua, logo depois de local keyboardModifiers = g_keyboard.getModifiers(). Preserve o restante do arquivo. Ele manda hi quando o clique esquerdo sem modificadores identifica um NPC. NpcSystem cuida do foco/distancia; com dois NPCs juntos o hi pode escolher outro NPC proximo. Use classificacao isNpc correta no protocolo. O jogador tambem pode cumprimentar pela fala.

Opcode 80 reservado ao dialogo servidor->cliente: se ja usado nessa direcao, altere nas duas pontas. O modulo fecha ao andar, sair do jogo ou pressionar Escape. Fechar libera o foco no servidor. Botoes usam hifen como separador; nao use hifen dentro do nome de uma opcao.

Configuracao de cura: a base precisa definir nurseHealsOnlyInPZ, nurseHealsPokemonOut, baseNurseryHappiness, baseNurseryHunger e pokeballs (entradas all/on). Tambem usa exhaustion.get/set, isPokeball, getPokeballsInContainer, setCD, doCureStatus e opcionalmente doUpdateMoves quando useKpdoDlls=true. Flags false/nil desativam suas respectivas exigencias, mas fome/felicidade precisam de valores numericos. Nao sobrescreva configuration.lua inteira. Exaustao: storage 9211 por 5 segundos; duelo: storage 52480; Fly/Ride/Surf: 17000/17001/63215. Confira conflitos. O HP da Ball e fracao: hp=1 significa cheia. IDs on das Balls precisam ser os seus. Outros atributos como boost, level, held e nick nao sao zerados por este script.

Validacao: Lua 5.1/XML e testes simulados de foco, abrir/voltar/fechar, registrar/repetir/desativado, bloqueio em duelo/PZ/exaustao, Pokemon solto proibido, cura de treinador/slot/mochila e mochila ausente. O transporte JSON e clique foram verificados no pacote anterior. Nao foi feito novo teste visual/end-to-end desta edicao no cliente, nem teste de todas as combinacoes de Balls. Teste em copia da base antes de producao.

Creditos: modulo original game_npcdialog identificado como Gengo, modificado por Biel; OTClient team e autores do fork; autores do NpcSystem e do sistema de cura/PokéTibia preexistente. Raul compartilha sua integracao, organizada com auxilio do Codex. Nao reivindico autoria de todos os componentes. Nao acompanha executavel, cliente completo, sprites, banco ou mapa.

Arquivos completos abaixo. Salve cada bloco no caminho indicado.

client/clique_esquerdo.lua

  -- OTPokemon-style NPC interaction: a plain left click greets the NPC and
-- lets the server open the dialog window through the NPC dialog opcode.
local clickedNpc = nil
if creatureThing and creatureThing:isNpc() then
clickedNpc = creatureThing
elseif attackCreature and attackCreature:isNpc() then
clickedNpc = attackCreature
elseif lookThing and lookThing:isCreature() and
lookThing:isNpc() then
clickedNpc = lookThing
end
if mouseButton == MouseLeftButton and keyboardModifiers == KeyboardNoModifier and clickedNpc then
local player = g_game.getLocalPlayer()
if player then player:stopAutoWalk() end
g_game.cancelAttack()
g_game.talkChannel(MessageModes.NpcTo, 0, 'hi')
return true
end

client/modules/game_npcdialog/npcdialog.lua

-- ui widgets
local windowDialog
local buttonHolder
local labelTitle
local outfitBox
local panelMessage
local scrollPanel
local labelMessage

-- variables
local OpcodeDialog = 80
local Actions = {
open = 1,
closed = 2
}

function init()
connect(g_game, { onGameEnd = closeDialog })
connect(LocalPlayer, { onPositionChange = onPositionChange })

ProtocolGame.registerExtendedOpcode(OpcodeDialog, function(protocol, opcode, buffer)
if type(buffer) ~= 'string' or #buffer > 60000 then return end
local ok, result = pcall(json.decode, buffer)
if not ok or type(result) ~= 'table' then return end
if result.action == Actions.closed then
closeDialog()
elseif result.action == Actions.open and type(result.data) == 'table' then
local data = result.data
if type(data.npcId) == 'number' and type(data.message) == 'string'
and (data.options == nil or type(data.options) == 'string') then
createDialog(data)
end
end
end)

windowDialog = g_ui.displayUI('npcdialog')

buttonHolder = windowDialog:getChildById('buttonHolder')
labelTitle = windowDialog:getChildById('labelTitle')
scrollPanel = windowDialog:getChildById('scrollPanel')
panelMessage = windowDialog:getChildById('panelMessage')
outfitBox = windowDialog:getChildById('outfitBox')

labelMessage = g_ui.createWidget('LabelText', panelMessage)
end

function terminate()
disconnect(g_game, { onGameEnd = closeDialog })
disconnect(LocalPlayer, { onPositionChange = onPositionChange })
ProtocolGame.unregisterExtendedOpcode(OpcodeDialog)
windowDialog:destroy()
end

function closeDialog()
windowDialog:hide()
end

function onPositionChange(creature, newPos, oldPos)
if creature:isLocalPlayer() then
windowDialog:hide()
end
end

function openDialog()
windowDialog:raise()
windowDialog:show()
end

function createDialog(value)
local Npc = g_map.getCreatureById(value.npcId)
if not Npc then
return error("game_npcdialog/npcdialog.lua: NPC nao encontrado no mapa visivel.")
end

labelTitle:setText(Npc:getName())
outfitBox:setOutfit(Npc:getOutfit())

labelMessage:clearText()
labelMessage:setText(value.message)

-- Evita erro se o limitText não estiver configurado no .otui
local limit = panelMessage.limitText or 100
scrollPanel:setVisible(labelMessage:getTextSize().height > limit)

buttonHolder:destroyChildren()
if value.options and value.options ~= '' then
-- Divide os botões pelo caractere '-' vindo do script do NPC
local options = {}
for option in value.options:gmatch('[^-]+') do table.insert(options, option) end

for i = 1, #options do
local button = g_ui.createWidget('OptionButton', buttonHolder)
button:setText(tr(options[i]))
button.onClick = function()
g_game.talkChannel(MessageModes.NpcTo, 0, options[i])
end
end

buttonHolder:setHeight(#options > 6 and 48 or 25)
end

openDialog()
end

client/modules/game_npcdialog/npcdialog.otmod

Module
name: game_npcdialog
description: Game NpcDialog
author: Gengo Modificado por Biel
scripts: [ npcdialog ]
sandboxed: true
autoload: true
autoload-priority: 1001
@onLoad: init()
@onUnload: terminate()

client/modules/game_npcdialog/npcdialog.otui

LabelText < Label
padding-bottom: 5
font: verdana-11px-rounded
color: #FFFFFF
text-wrap: true
text-auto-resize: true

OptionButton < Button
margin: 3 0 0 2
width: 80
color: #FFFFFF

UIWindow
size: 620 231
visible: false
image-source: window
image-border: 12
anchors.centerIn: parent
@onEscape: modules.game_npcdialog.closeDialog()

Label
id: labelTitle
font: verdana-11px-rounded
anchors.top: parent.top
anchors.left: parent.left
!text: tr('Title')
margin: 18 0 5 25
color: #FFFFFF
text-auto-resize: true

UICreature
id: outfitBox
size: 75 75
anchors.top: prev.bottom
anchors.left: parent.left
margin: 20 0 5 34

VerticalScrollBar
id: scrollPanel
height: 65
anchors.top: panelMessage.top
anchors.left: panelMessage.right
anchors.bottom: panelMessage.bottom
margin-left: 5
visible: false
pixels-scroll: true
step: 14

ScrollablePanel
id: panelMessage
size: 0 120
anchors.top: labelTitle.bottom
anchors.left: outfitBox.right
anchors.right: parent.right
margin: 8 30 10 25
vertical-scrollbar: scrollPanel
layout: verticalBox
&limitText: 80

ScrollablePanel
id: buttonHolder
size: 0 24
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
margin: 15 10 8 10
layout:
type: grid
cell-size: 99 22
flow: true

server/data/lib/npcdialog_lib.lua

-- Public JSON variant. Use the matching client module; old Lua-table payloads differ.
local OpcodeDialog = 80
local function quote(value)
return '"' .. value:gsub('[%z\1-\31\\"]', function(c)
if c == '"' then return '\\"' end
if c == '\\' then return '\\\\' end
return string.format('\\u%04x', string.byte(c))
end) .. '"'
end
function doSendDialogNpc(cid, npc, msg, opc)
if type(npc) ~= 'number' or type(msg) ~= 'string' then return false end
local buffer = '{"action":1,"data":{"npcId":' .. tostring(npc)
.. ',"message":' .. quote(msg) .. ',"options":' .. quote(opc or '') .. '}}'
return doSendPlayerExtendedOpcode(cid, OpcodeDialog, buffer)
end
function doSendDialogNpcClose(cid)
return doSendPlayerExtendedOpcode(cid, OpcodeDialog, '{"action":2}')
end

server/data/npc/Nurse.xml

<?xml version="1.0" encoding="UTF-8"?>
<npc name="Nurse Joy" script="heal.lua" walkinterval="350000" floorchange="0" speed="0">
<health now="150" max="150"/>
<look type="532" head="91" body="102" legs="83" feet="0"/>
<parameters>
<!-- 0 disables registration. Set your Teleport destination storage. -->
<parameter key="teleportStorage" value="0"/>
<!-- As mensagens foram movidas para o script em Lua para o sistema de dialogo funcionar corretamente -->
<parameter key="message_idletimeout" value="Ate mais treinador!"/>
<parameter key="message_walkaway" value="Ate mais treinador!"/>
</parameters>
</npc>

server/data/npc/scripts/heal.lua

-- Public adaptation: explicit teleportStorage per NPC; no private map coordinates.
local keywordHandler = KeywordHandler:new()
local npcHandler = NpcHandler:new(keywordHandler)
NpcSystem.parseParameters(npcHandler)
local talkState = {}

function onThingMove(creature, thing, oldpos, oldstackpos) end
function onCreatureAppear(creature) npcHandler:onCreatureAppear(creature) end

function onCreatureDisappear(cid)
if isPlayer(cid) then doSendDialogNpcClose(cid) end
npcHandler:onCreatureDisappear(cid)
end

function onCreatureTurn(creature) end

function msgcontains(txt, str)
return (string.find(txt, str) and not string.find(txt, '(%w+)' .. str) and not string.find(txt, str .. '(%w+)'))
end

function onCreatureSay(cid, type, msg) npcHandler:onCreatureSay(cid, type, msg:lower()) end
function onThink() npcHandler:onThink() end

function greetCallback(cid)
doSendDialogNpc(cid, getNpcId(), "Ola treinador! Bem-vindo ao Centro Pokemon.\nComo posso ajudar voce e seus Pokemons hoje?", "Curar-Registrar-Fechar")
return true
end

function creatureSayCallback(cid, type, msg)
if(not npcHandler:isFocused(cid)) then return false end
local msg = string.lower(msg)
local talkUser = NPCHANDLER_CONVBEHAVIOR == CONVERSATION_DEFAULT and 0 or cid

for a, b in pairs(gobackmsgs or {}) do
local gm = string.gsub(b.go, "doka!", "")
local bm = string.gsub(b.back, "doka!", "")
if string.find(string.lower(msg), string.lower(gm)) or string.find(string.lower(msg), string.lower(bm)) then
return true
end
end

-- REGISTRAR CIDADE PARA O SISTEMA DE TELEPORT
if msgcontains(msg, 'registrar') or msgcontains(msg, 'registra') then
local registeredStorage = tonumber(getNpcParameter("teleportStorage"))
if registeredStorage and (registeredStorage <= 0 or registeredStorage ~= math.floor(registeredStorage)) then
registeredStorage = nil
end

if not registeredStorage then
doSendDialogNpc(cid, getNpcId(), "Esta Nurse ainda nao possui uma cidade configurada para o Teleport.", "Voltar-Fechar")
return true
end

if getPlayerStorageValue(cid, registeredStorage) >= 1 then
doSendDialogNpc(cid, getNpcId(), "Esta cidade ja esta registrada no seu Teleport!", "Curar-Fechar")
return true
end

setPlayerStorageValue(cid, registeredStorage, 1)
doSendMagicEffect(getThingPos(cid), CONST_ME_MAGIC_GREEN)
doSendDialogNpc(cid, getNpcId(), "Cidade registrada com sucesso! Agora voce pode voltar para ca usando um Pokemon com Teleport.", "Curar-Fechar")
return true
end
-- 1. GATILHO: CLICOU EM CURAR OU FALOU "HEAL/HELP/CURAR"
if msgcontains(msg, 'curar') or msgcontains(msg, 'heal') or msgcontains(msg, 'help') then
if exhaustion.get(cid, 9211) then
selfSay('Por favor, aguarde alguns instantes antes de pedir para curar seus pokemons novamente!', cid)
return true
end
if not getTileInfo(getThingPos(cid)).protection and nurseHealsOnlyInPZ then
selfSay("Por favor, entre no centro pokemon para curar seus pokemons!", cid)
return true
end
if getPlayerStorageValue(cid, 52480) >= 1 then
selfSay("Voce nao pode fazer isso enquanto estiver em um Duelo!", cid)
return true
end

-- Reject before applying healing/exhaustion when summons must be recalled.
if #getCreatureSummons(cid) >= 1 and not nurseHealsPokemonOut then
selfSay("Por favor, coloque seu pokemon de volta na bola!", cid)
return true
end

-- Aplica a exaustao e cura o treinador
exhaustion.set(cid, 9211, 5)
doCreatureAddHealth(cid, getCreatureMaxHealth(cid)-getCreatureHealth(cid))
doCureStatus(cid, "all", true)
doSendMagicEffect(getThingPos(cid), 14)
local mypb = getPlayerSlotItem(cid, 8)

-- Cura Pokemon para fora da bola
if #getCreatureSummons(cid) >= 1 then
if not nurseHealsPokemonOut then
selfSay("Por favor, coloque seu pokemon de volta na bola!", cid)
return true
end
local s = getCreatureSummons(cid)[1]
doCreatureAddHealth(s, getCreatureMaxHealth(s))
doSendMagicEffect(getThingPos(s), 14)
doCureStatus(s, "all", false)
if getPlayerStorageValue(s, 1008) < baseNurseryHappiness then setPlayerStorageValue(s, 1008, baseNurseryHappiness) end
if getPlayerStorageValue(s, 1009) > baseNurseryHunger then setPlayerStorageValue(s, 1009, baseNurseryHunger) end
else
-- Cura o slot principal (Slot 8)
if mypb and mypb.uid > 0 and mypb.itemid ~= 0 and isPokeball(mypb.itemid) then
doItemSetAttribute(mypb.uid, "hp", 1)
if getItemAttribute(mypb.uid, "hunger") and getItemAttribute(mypb.uid, "hunger") > baseNurseryHunger then
doItemSetAttribute(mypb.uid, "hunger", baseNurseryHunger)
end
for c = 1, 15 do
local str = "move"..c
setCD(mypb.uid, str, 0)
end
if getItemAttribute(mypb.uid, "happy") and getItemAttribute(mypb.uid, "happy") < baseNurseryHappiness then
doItemSetAttribute(mypb.uid, "happy", baseNurseryHappiness)
end
if getPlayerStorageValue(cid, 17000) <= 0 and getPlayerStorageValue(cid, 17001) <= 0 and getPlayerStorageValue(cid, 63215) <= 0 then
for a, b in pairs (pokeballs) do
if isInArray(b.all, mypb.itemid) then doTransformItem(mypb.uid, b.on) end
end
end
end
end

-- Varre a mochila e cura todas as outras pokebolas guardadas
local bp = getPlayerSlotItem(cid, CONST_SLOT_BACKPACK)
local balls = {}
if bp and bp.uid > 0 and isContainer(bp.uid) then
balls = getPokeballsInContainer(bp.uid) or {}
end
if #balls >= 1 then
for _, uid in ipairs(balls) do
doItemSetAttribute(uid, "hp", 1)
for c = 1, 15 do
local str = "move"..c
setCD(uid, str, 0)
end
if getItemAttribute(uid, "hunger") and getItemAttribute(uid, "hunger") > baseNurseryHunger then
doItemSetAttribute(uid, "hunger", baseNurseryHunger)
end
if getItemAttribute(uid, "happy") and getItemAttribute(uid, "happy") < baseNurseryHappiness then
doItemSetAttribute(uid, "happy", baseNurseryHappiness)
end
local this = getThing(uid)
for a, b in pairs (pokeballs) do
if isInArray(b.all, this.itemid) then doTransformItem(uid, b.on) end
end
end
end

-- Fecha a janela de dialogo apos a cura concluida com sucesso
doSendDialogNpcClose(cid)
selfSay('Prontinho! Voce e seus pokemons estao saudaveis novamente.', cid)
if useKpdoDlls then doUpdateMoves(cid) end
if npcHandler.unGreet then npcHandler:unGreet(cid) else npcHandler:resetNpc(cid) end
npcHandler:releaseFocus(cid)
return true

elseif msgcontains(msg, "voltar") then
doSendDialogNpc(cid, getNpcId(), "Ola treinador! Bem-vindo ao Centro Pokemon.\nComo posso ajudar voce e seus Pokemons hoje?", "Curar-Registrar-Fechar")
return true
-- 2. GATILHO: CLICOU EM FECHAR OU FALOU "SAIR/BYE/FECHAR"
elseif msgcontains(msg, "fechar") or msgcontains(msg, 'sair') or msgcontains(msg, 'bye') then
doSendDialogNpcClose(cid)
selfSay('Ate mais treinador!', cid)
if npcHandler.unGreet then npcHandler:unGreet(cid) else npcHandler:resetNpc(cid) end
npcHandler:releaseFocus(cid)
return true
end
return true
end

npcHandler:setCallback(CALLBACK_GREET, greetCallback)
npcHandler:setCallback(CALLBACK_MESSAGE_DEFAULT, creatureSayCallback)
npcHandler:addModule(FocusModule:new())

Fundo window.png — recriar sem anexo

Na pasta modules/game_npcdialog do cliente, abra PowerShell e execute o bloco abaixo. Ele grava somente window.png nessa pasta. Se preferir outro fundo, ajuste image-source no OTUI.

[System.IO.File]::WriteAllBytes((Join-Path (Get-Location) 'window.png'), [Convert]::FromBase64String('iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAACVUlEQVR4nO3dv07yUBiA8bcNXICzW3cTEyMX4O4d6aR35N5dCMTEWTZnZ0MEB77iaVMK8WvNk/D8NnOwad6H0z8TWfwzKfJNJKbLdRYazL5559Xi89tX5z+oP13zzg4NvipnoP9z7BxHhw5UHeDq+qafMztZ5VFf6FFExMPFbevi/etTRGxjzGdlX2d2ktIZds17FBHxuVi0H2X8c6Dlx/hlmFM9EbPysppl17x317W71Xlt7XH8boyeFWerXZS2eU+X62z3aNu82RhjGGmUVHXTr71rpDdwYwynGSV958ubHzbG8JYf45f5rGx9cq1dsozxt9KdUntTF4dBYAwCYxAYg8AYBMYgMAaBMQiMQWAMAmMQGIPAGATGIDAGgTEIjEFgDAJjEBiDwBgExiAwBoExCIxBYAwCYxAYg8AYBMYgMAaBMQiMQWAMAmMQGIPAGATGIDAGgTEIjEFgDAJjEBiDwBgExiAwBoExCIxBYAwCYxAYg8AYBMYgMAaBMQiMQWAMAmMQGIPAGATGIDAGgTEIjEFgDAJjEBiDwBgExiAwBoExCIxBYAwCYxAYg8AYBMYgMFn6h7+p/nfafks9omWHzGdlXF3fRHG2uvzbUzwdaYymXZlJkW/SBXfKMPbFqHZJHrGN8fz2VfuAO6V/aYzmvKsNkTV3RpM7pR9dl6nUwSARP1H0e8fOMJsU+ebh4rZ18f71qXZA/V4ao2veo4iIz8Wi/SjjSB7JyoM7Sd2my3U2KfJN17yziO0N5W51Xlt7HL/Xno/Vn655733sNcaw9s37G4DX+20+pJNdAAAAAElFTkSuQmCC'))