local showHighlightedUnderline = false -- OT-MMO (fase 4): tokens del Design System (OtmmoTheme, corelib/ui/theme.lua). Palabras clave en acento, no el azul -- de Tibia; lo que dice el NPC en text, lo que dice el jugador y la cabecera en text-2. local NPC_DIALOG_HEADER_COLOR = OtmmoTheme.color.text2 local NPC_TEXT_COLOR = OtmmoTheme.color.text local PLAYER_TEXT_COLOR = OtmmoTheme.color.text2 local KEYWORD_COLOR = OtmmoTheme.color.accent local function getHighlightedText(text, color, highlightColor) color = color or NPC_TEXT_COLOR highlightColor = highlightColor or KEYWORD_COLOR local firstBrace = text:find("{", 1, true) if not firstBrace then return string.format("{%s, %s}", text, color) end local parts = {} local lastPos = 1 for startPos, content, endPos in text:gmatch("()%{([^}]*)%}()") do if startPos > lastPos then parts[#parts + 1] = string.format("{%s, %s}", text:sub(lastPos, startPos - 1), color) end local textPart = content:match("([^,]+)") or content local trimmed = textPart local highlighted = trimmed if showHighlightedUnderline then highlighted = string.format("[text-event]%s[/text-event]", trimmed) else highlighted = string.format("[text-event]%s%s[/text-event]", string.char(1), trimmed) end parts[#parts + 1] = string.format("{%s, %s}", highlighted, highlightColor) lastPos = endPos end if lastPos <= #text then parts[#parts + 1] = string.format("{%s, %s}", text:sub(lastPos), color) end return table.concat(parts) end local function createDialogLabel(consoleBuffer, entry) local label = g_ui.createWidget('ConsoleLabel', consoleBuffer) label:setId("consoleLabel" .. consoleBuffer:getChildCount()) -- OT-MMO (fase 4): texto del diálogo en Geist 12 como el resto de ventanas (ConsoleLabel trae la bitmap de Tibia) pcall(function() label:setTTFFont(OtmmoTheme.font.text, OtmmoTheme.size.sm, 0, OtmmoTheme.color.bg) end) if entry.coloredData then label:setColoredText(entry.coloredData) label.coloredData = entry.coloredData else label:setText(entry.text or "") end if entry.color then label:setColor(entry.color) end if entry.name then label.name = entry.name end if entry.clickable and not label:hasEventListener(EVENT_TEXT_CLICK) then label:setEventListener(EVENT_TEXT_CLICK) connect(label, { onTextClick = function(w, t) controllerNpcTrader:onConsoleTextClicked(w, t) end }) end return label end local function buildTalkingToEntry(npcName, timestamp) local prefix = timestamp and (timestamp .. " ") or "" return { text = prefix .. "hablando con " .. npcName, color = NPC_DIALOG_HEADER_COLOR } end function controllerNpcTrader:ensureDialogHeader(consoleBuffer) if not consoleBuffer or consoleBuffer:getChildCount() > 0 or not self.creatureName or self.creatureName == "" then return end createDialogLabel(consoleBuffer, buildTalkingToEntry(self.creatureName, os.date('%H:%M'))) end function controllerNpcTrader:onConsoleTextClicked(widget, text) if type(widget) == "string" and not text then text = widget widget = nil end if not text or text == "" then return end modules.game_misiones.ocultarNpcMisiones() local npcTab = modules.game_console.consoleTabBar:getTab("NPCs") if npcTab then modules.game_console.sendMessage(text, npcTab) onNpcTalk(g_game.getCharacterName(), 0, MessageModes.NpcTo, text) end if text == "bye" then controllerNpcTrader:onCloseNpcTrade() end end function controllerNpcTrader:cloneConsoleMessages() local consoleBuffer = self:findWidget("#consoleBuffer") if consoleBuffer then consoleBuffer:destroyChildren() self:ensureDialogHeader(consoleBuffer) end end -- temp fix. can't drag the left panel to move the window. function controllerNpcTrader:setupWindowDragBehavior() if not self.ui then return end local dragHandle = self:findWidget("#dragHandle") if not dragHandle then return end dragHandle:setDraggable(true) dragHandle.onDragEnter = function(widget, mousePos) return self.ui:onDragEnter(mousePos) end dragHandle.onDragMove = function(widget, mousePos, mouseMoved) self.ui:onDragMove(mousePos, mouseMoved) return true end dragHandle.onDragLeave = function(widget, droppedWidget, mousePos) -- VALDREN: signalcall y no self.ui:onDragLeave(): OtmmoVentanas.recordar (connect) lo convierte en una lista -- de manejadores, y así también corre el que guarda la posición. signalcall(self.ui.onDragLeave, self.ui, droppedWidget, mousePos) return true end end -- OT-MMO: el cursor va a la caja de la ventana del NPC al abrirla y tras cada respuesta. Si no, lo que se escribia -- sin hacer clic iba al juego y las letras abrian ventanas por sus atajos (B bolsas, M mapa, O buscador; 22-oct). -- focus() solo no basta: hay que enfocar toda la cadena de padres hasta la raiz. function controllerNpcTrader:enfocarEntrada() scheduleEvent(function() local input = self.ui and self.ui:isVisible() and self:findWidget(".inputConsole") if not input or not input:isVisible() or not input:isEnabled() then return end local hijo, padre = input, input:getParent() while padre do padre:focusChild(hijo, ActiveFocusReason) hijo, padre = padre, padre:getParent() end end, 50) end -- VALDREN (23-oct, 2): cabecera en el Design System (templates/npc_dialogo.otui). Respuestas rápidas en botones de -- texto (fuera los íconos de Tibia de palomita, X y mano) y pestañas Conversación | Misiones con GameTab. El panel de -- misiones lo pinta game_misiones dentro de la misma caja que la consola; la entrada de texto queda siempre a la vista. local ETIQUETA_RESPUESTA = { [KeywordButtonIcon.KEYWORDBUTTONICON_YES] = 'S\237', [KeywordButtonIcon.KEYWORDBUTTONICON_NO] = 'No', [KeywordButtonIcon.KEYWORDBUTTONICON_BYE] = 'Adi\243s', [KeywordButtonIcon.KEYWORDBUTTONICON_GENERALTRADE] = 'Comerciar', [KeywordButtonIcon.KEYWORDBUTTONICON_POTIONTRADE] = 'Pociones', [KeywordButtonIcon.KEYWORDBUTTONICON_EQUIPMENTTRADE] = 'Equipo', [KeywordButtonIcon.KEYWORDBUTTONICON_SAIL] = 'Viajar', [KeywordButtonIcon.KEYWORDBUTTONICON_DEPOSITALL] = 'Depositar todo', [KeywordButtonIcon.KEYWORDBUTTONICON_WITHDRAW] = 'Retirar', [KeywordButtonIcon.KEYWORDBUTTONICON_BALANCE] = 'Saldo', } -- Íconos 32x32 (scripts/generar-iconos-npc.py) y tooltip de las cuatro respuestas de siempre. local ICONO_RESPUESTA = { [KeywordButtonIcon.KEYWORDBUTTONICON_YES] = { '/images/game/npc/si', 'S\237' }, [KeywordButtonIcon.KEYWORDBUTTONICON_NO] = { '/images/game/npc/no', 'No' }, [KeywordButtonIcon.KEYWORDBUTTONICON_GENERALTRADE] = { '/images/game/npc/comerciar', 'Comerciar' }, [KeywordButtonIcon.KEYWORDBUTTONICON_BYE] = { '/images/game/npc/adios', 'Adi\243s' }, } local RESPUESTAS_ANCHO, RESPUESTAS_ALTO, RESPUESTA_ALTO, HUECO = 287, 64, 32, 8 local estilosCargados = false local function vivo(w) return w and not w:isDestroyed() end -- Botones de texto en filas (con salto si no caben), centrados en vertical con el retrato. «Adiós» siempre al final. -- El botón manda la palabra del servidor (yes, no, bye, trade...): solo cambia lo que se lee. function controllerNpcTrader:montarRespuestas() local panel = self.respuestas if not vivo(panel) then return end panel:destroyChildren() local lista, adios = {}, nil for _, b in ipairs(self.buttons or {}) do if b.id == KeywordButtonIcon.KEYWORDBUTTONICON_BYE then adios = b else lista[#lista + 1] = b end end if adios then lista[#lista + 1] = adios end local filas, x, y = 1, 0, 0 local botones = {} for _, b in ipairs(lista) do local icono = ICONO_RESPUESTA[b.id] local w, ancho if icono then w = g_ui.createWidget('NpcRespuestaIcono', panel) w:setIcon(icono[1]) w.tooltip = icono[2] -- lo muestra g_tooltip (corelib/ui/tooltip.lua) al pasar el ratón ancho = 32 else w = g_ui.createWidget('NpcRespuesta', panel) local texto = ETIQUETA_RESPUESTA[b.id] or (b.text:sub(1, 1):upper() .. b.text:sub(2)) w:setText(texto) ancho = math.max(56, w:getTextSize().width + 24) w:setWidth(ancho) end if x > 0 and x + ancho > RESPUESTAS_ANCHO then x, y, filas = 0, y + RESPUESTA_ALTO + HUECO, filas + 1 end botones[#botones + 1] = { w = w, x = x, y = y } x = x + ancho + HUECO local palabra = b.text w.onClick = function() self:onConsoleTextClicked(nil, palabra) end end local alto = filas * RESPUESTA_ALTO + (filas - 1) * HUECO local arriba = math.max(0, math.floor((RESPUESTAS_ALTO - alto) / 2)) for _, e in ipairs(botones) do e.w:addAnchor(AnchorLeft, 'parent', AnchorLeft) e.w:addAnchor(AnchorTop, 'parent', AnchorTop) e.w:setMarginLeft(e.x) e.w:setMarginTop(arriba + e.y) end end function controllerNpcTrader:montarCabecera() if not self.ui then return end if not estilosCargados then g_ui.importStyle('/modules/game_npctrade/templates/npc_dialogo') estilosCargados = true end if not vivo(self.respuestas) or self.respuestas:getParent() ~= self.ui then self.respuestas = g_ui.createWidget('NpcRespuestas', self.ui) end if not vivo(self.pestanas) or self.pestanas:getParent() ~= self.ui then self.pestanas = g_ui.createWidget('NpcPestanas', self.ui) self.pestanas.conversacion.onClick = function() self:elegirPestana('conversacion') end self.pestanas.misiones.onClick = function() self:elegirPestana('misiones') end self:marcarPestana('conversacion') self:contarMisiones(0) end self:montarRespuestas() end -- Clic en una pestaña. Misiones pide al servidor la lista de ESTE NPC (N|id, validado allí por cercanía). function controllerNpcTrader:elegirPestana(cual) if cual == 'misiones' then modules.game_misiones.solicitarNpcMisiones() else modules.game_misiones.ocultarNpcMisiones() end self:enfocarEntrada() end -- Solo dibuja el estado; lo llama game_misiones al mostrar u ocultar su panel. function controllerNpcTrader:marcarPestana(cual) local p = self.pestanas if not vivo(p) then return end p.conversacion:setChecked(cual ~= 'misiones') p.misiones:setChecked(cual == 'misiones') end function controllerNpcTrader:contarMisiones(n) local p = self.pestanas if not vivo(p) then return end p.contador:setText(tostring(n)) p.contador:setVisible(n > 0) end function controllerNpcTrader:initNpcWindow(creature, buttons) if self:isLegacyMode() then return end self:connectNpcTalkEvent() self.widthConsole = self.DEFAULT_CONSOLE_WIDTH self.isTradeOpen = false if creature then self.creatureId = creature:getId() self.creatureName = creature:getName() or "Unknown" self.outfit = creature:getOutfit() else self.creatureName = "Unknown" self.outfit = "/game_npctrade/assets/images/icon-npcdialog-multiplenpcs" end self.buttons = buttons or self.buttons or self.buttonsDefault self:updateChatButton() if not self.ui or not self.ui:isVisible() then self:loadHtml('templates/game_npctrader.html') end self:setupWindowDragBehavior() self:montarCabecera() -- VALDREN: recuerda dónde la dejó el jugador (corelib/ui/ventanas.lua; se guarda al soltar la cabecera) if OtmmoVentanas and self.ui then OtmmoVentanas.recordar(self.ui, 'npc', { controles = false }) -- el motor HTML centra la ventana (placement="center") después de crearla: se recoloca un instante después local ui = self.ui scheduleEvent(function() if ui and not ui:isDestroyed() and ui:isVisible() then OtmmoVentanas.aplicar(ui) end end, 50) end local creatureOutfit = self:findWidget("#creatureOutfit") if creatureOutfit then creatureOutfit:setCreatureSize(64) -- dentro de la casilla de 64 del retrato, centrado (fase 4) creatureOutfit:setCenter(true) if type(self.outfit) == "string" then creatureOutfit:setImageSource(self.outfit) else creatureOutfit:setOutfit(self.outfit) end end modules.game_misiones.onNpcDialog(self) self:cloneConsoleMessages() self:enfocarEntrada() end function onNpcChatWindow(data) if controllerNpcTrader:isLegacyMode() then controllerNpcTrader:legacy_show() return end if type(data) ~= "table" or type(data.npcIds) ~= "table" or #data.npcIds == 0 then return end -- The server appends every greeting NPC to the list and builds the buttons from the -- LAST id (the active conversation); npcIds[1] is the oldest, e.g. a bystander that -- happened to be in talk range. Showing npcIds[1] paints the wrong name and outfit. local creature = g_map.getCreatureById(data.npcIds[#data.npcIds]) if creature then controllerNpcTrader:initNpcWindow(creature, data.buttons) end end function controllerNpcTrader:onConsoleKeyPress(event) if event.value == KeyEnter then local input = controllerNpcTrader:findWidget(".inputConsole") if input then local text = input:getText() if text and #text > 0 then controllerNpcTrader:onConsoleTextClicked(nil, text) input:clearText() end end end end function onNpcTalk(name, level, mode, text, channelId, creaturePos) if not controllerNpcTrader.ui or not controllerNpcTrader.ui:isVisible() then return end if mode == MessageModes.NpcTo or mode == MessageModes.NpcFrom or mode == MessageModes.NpcFromStartBlock then local consoleBuffer = controllerNpcTrader:findWidget("#consoleBuffer") if consoleBuffer then controllerNpcTrader:ensureDialogHeader(consoleBuffer) local color = mode == MessageModes.NpcTo and PLAYER_TEXT_COLOR or NPC_TEXT_COLOR local fullText = text if mode == MessageModes.NpcFrom or mode == MessageModes.NpcFromStartBlock then fullText = name .. " dice: " .. text elseif mode == MessageModes.NpcTo then fullText = name .. ": " .. text end local entry = { text = fullText, color = color, name = mode == MessageModes.NpcTo and g_game.getCharacterName() or name, clickable = true } if getHighlightedText then entry.coloredData = getHighlightedText(fullText, color, KEYWORD_COLOR) end createDialogLabel(consoleBuffer, entry) controllerNpcTrader:enfocarEntrada() end end end function controllerNpcTrader:updateChatButton() local isChatEnabled = modules.game_console.isChatEnabled() self.chatMode = isChatEnabled and tr('Chat On') or tr('Chat Off') local inputConsole = self:findWidget(".inputConsole") if inputConsole then inputConsole:setEnabled(isChatEnabled) end end function controllerNpcTrader:toggleChatMode() modules.game_console.toggleChat() self:updateChatButton() end