-- OT-MMO: HUD de la esquina derecha. -- · Minimapa redondo (UIMinimap:setCircular, cambio en C++) en un contenedor pizarra con N/S/E/O y dos columnas de -- botones (fase 3, concepto A.6b): zoom, centrar y mapa a la izquierda; buscar grupo, logros, misiones y opciones a -- la derecha. -- · Ventana de Personaje (I): reutiliza el panel de equipo de game_inventory (se reubica dentro), con nivel, -- clase, vida, maná y capacidad. La C ya es la diagonal sureste al caminar con WASD, por eso es la I. -- · Bolsa unificada (B): una cuadrícula con todas las casillas de la mochila de la espalda y de las bolsas -- que lleva dentro. Es solo presentación sobre los contenedores abiertos del protocolo de Tibia (cada -- casilla tiene su posición real, así arrastrar, usar y equipar van al servidor como siempre); las -- ventanas normales de esas mochilas se ocultan. -- · Micromenú: grupo aparte abajo a la derecha, 6 botones con íconos de línea propios y contadores; -- lo que no cabe va en el menú «Más» o en la columna derecha del minimapa (y todo sigue con su tecla). -- · 23-oct (pedido del usuario): todo lo de la derecha va pegado al borde derecho de la pantalla, sin hueco: el marco del -- minimapa, el micromenú, Personaje, Bolsas y la columna de Tibia (Enemigos, Amigos). acomodarDerecha() los apila sin -- encimarlos: Personaje bajo el minimapa (a la izquierda de Enemigos si esa columna tiene algo abierto), Bolsas sobre -- el micromenú (a la izquierda de Enemigos si chocarían). Lo que el jugador arrastra a mano se queda donde lo dejó. hudController = Controller:new() local CLASES = { [1] = 'Guardián', [2] = 'Cazador', [3] = 'Hechicero', [4] = 'Druida', [6] = 'Brujo', [7] = 'Bardo', [8] = 'Gunslinger' } -- vocación del cliente local ORO = { [3031] = 1, [3035] = 100, [3043] = 10000 } local MAX_BOLSAS = 6 local personaje, bolsas, micromenu, marcoMapa = nil, nil, nil, nil local contadoresEvento = nil local nuestros = {} -- [containerId] = Container: contenedores que muestra la bolsa unificada local pendientes = {} -- ids de objeto (bolsas) que pedimos abrir y aún no llegan local abiertasPorBolsa = {} -- [x:y:z de la casilla] = true: sub-bolsas ya pedidas local refrescoProgramado = nil local todos = {} -- [containerId] = Container: todo lo abierto (para adoptar la mochila abierta al entrar) local botones = {} -- Micromenú ------------------------------------------------------------------------------------- local function aviso(texto) modules.game_textmessage.displayStatusMessage(texto) end local ICONOS = '/images/game/hud/iconos/' local function contrato() aviso('Los contratos se aceptan en el tablón del Campamento; el seguimiento sale arriba al centro.') end -- Fila del micromenú (A.6b: unos 6). id, texto del tooltip (con su tecla), ícono, acción. local BOTONES = { { 'personaje', 'Personaje (I)', 'personaje', function() togglePersonaje() end }, { 'bolsas', 'Bolsas (B)', 'bolsas', function() toggleBolsas() end }, { 'hechizos', 'Hechizos (P)', 'hechizos', function() modules.game_libro.toggle() end }, { 'talentos', 'Talentos (N)', 'talentos', function() modules.game_talentos.toggle() end }, { 'enemigos', 'Enemigos (Ctrl+B)', 'enemigos', function() modules.game_battle.toggle() end }, { 'bloqueo', 'Bloquear / desbloquear UI', 'ajustes', function() OtmmoVentanas.alternarBloqueo() end }, { 'pvp', 'Activar / desactivar ataques PvP', 'enemigos', function() local activar = g_game.isSafeFight() g_game.setSafeFight(not activar) if g_game.getFeature(GamePVPMode) then g_game.setPVPMode(activar and PVPRedFist or PVPWhiteDove) end aviso(activar and 'PvP activo fuera de zonas protegidas.' or 'Ataques a jugadores desactivados.') end }, { 'autobotin', 'Botín automático: apagado (clic para activar)', 'botin', function() modules.game_autobotin.alternar() end }, { 'mas', 'Más', 'mas', function(boton) abrirMas(boton) end }, } -- Columnas del minimapa: izquierda = el mapa; derecha = accesos. local BOTONES_MAPA_IZQ = { { 'zoomMas', 'Acercar', 'zoom-mas', function() modules.game_minimap.zoomIn() refrescarZoom() end }, { 'zoomMenos', 'Alejar', 'zoom-menos', function() modules.game_minimap.zoomOut() refrescarZoom() end }, { 'centrar', 'Centrar en mí', 'centrar', function() modules.game_minimap.resetMap() end }, { 'mapa', 'Mapa (M)', 'mapa', function() modules.game_mapa.toggle() end }, } local BOTONES_MAPA_DER = { { 'buscador', 'Buscar grupo (O)', 'grupo', function() modules.game_buscador.toggle() end }, { 'logros', 'Logros (Y)', 'logros', function() modules.game_logros.toggle() end }, { 'misiones', 'Misiones (L)', 'misiones', function() modules.game_misiones.toggleDiario() end }, { 'opciones', 'Opciones', 'ajustes', function() modules.client_options.toggle() end }, } -- Bloquear la barra de acción (antes, un candado de Tibia pegado a la barra; ahora en «Más»). local function barraBloqueada() return modules.client_options and modules.client_options.getOption('actionBarBottomLocked') end local function alternarBloqueo() local candado = modules.game_interface.getBottomLockPanel and modules.game_interface.getBottomLockPanel() if candado and modules.game_actionbar then modules.game_actionbar.changeLockStatus(candado, 'Bottom') end end -- Menú «Más»: lo que salió de la fila. Se abre encima del botón. function abrirMas(boton) local menu = g_ui.createWidget('PopupMenu') menu:setGameMenu(true) menu:addOption('Amigos', function() modules.game_viplist.toggle() scheduleEvent(refrescarEstados, 100) end, 'Ctrl+P') menu:addOption('Grupo', function() toggleGrupo() end) menu:addOption('Jugador contra jugador', function() if modules.game_pvp then modules.game_pvp.toggle() end end, 'H') menu:addOption('Oficios', function() g_game.talk('!oficios') end) menu:addOption('Contratos', contrato) menu:addSeparator() menu:addOption(barraBloqueada() and 'Desbloquear las barras' or 'Bloquear las barras', alternarBloqueo) local ab = modules.game_actionbar if ab and ab.segundaBarraVisible then menu:addOption(ab.segundaBarraVisible() and 'Ocultar la segunda barra' or 'Mostrar la segunda barra', function() ab.alternarSegundaBarra() end) end menu:addOption('Opciones', function() modules.client_options.toggle() end) menu:addSeparator() menu:addOption('Salir', function() modules.game_interface.tryLogout(true) end) local x = boton and boton:getX() or 0 menu:display({ x = x, y = boton and boton:getY() or 0 }) addEvent(function() -- el alto se conoce tras el primer acomodo if not menu:isDestroyed() and boton then menu:setPosition({ x = math.min(x, rootWidget:getWidth() - menu:getWidth() - 8), y = boton:getY() - menu:getHeight() - 6 }) end end) end -- Contadores pequeños: misiones listas para entregar y puntos de talento sin gastar. local function contador(boton, n) if not boton or not boton.contador then return end boton.contador:setVisible(n ~= nil and n > 0) boton.contador:setText(n and n > 9 and '9+' or tostring(n or '')) end -- + y - del minimapa: apagados en el límite del zoom (game_minimap). function refrescarZoom() local mm = modules.game_minimap if not mm or not mm.getZoomEstado then return end local z, zmin, zmax = mm.getZoomEstado() if botones.zoomMas then botones.zoomMas:setEnabled(z < zmax) end if botones.zoomMenos then botones.zoomMenos:setEnabled(z > zmin) end end -- Columna derecha ------------------------------------------------------------------------------------------------------ -- Borde inferior de lo que hay abierto en la columna de Tibia (Enemigos, Amigos...) y su ancho; nil si está vacía. local function columnaTibia() local panel = modules.game_interface.getRightPanel() if not panel or not panel:isVisible() then return nil end local principal = modules.game_interface.getMainRightPanel() local abajo = nil for _, w in ipairs(panel:getChildren()) do if w ~= principal and w:isVisible() and w:getHeight() > 0 then local b = w:getY() + w:getHeight() if not abajo or b > abajo then abajo = b end end end return abajo, panel:getWidth() end local ultimoAcomodo = nil function acomodarDerecha() if not marcoMapa or not micromenu then return end local abajoTibia, anchoTibia = columnaTibia() local bajoMapa = marcoMapa:getMarginTop() + marcoMapa:getHeight() + 8 local arriba = micromenu:getY() - 8 - (bolsas and bolsas:getHeight() or 0) local choca = abajoTibia ~= nil and abajoTibia > arriba local seg = modules.game_interface.getRootPanel():getChildById('seguimientoMisiones') local personajeVisible = personaje and personaje:isVisible() and not personaje.movida local clave = table.concat({ tostring(abajoTibia ~= nil), tostring(anchoTibia), tostring(choca), bajoMapa, tostring(personaje and personaje.movida), tostring(bolsas and bolsas.movida), tostring(personajeVisible), tostring(seg and seg:isVisible()) }, '|') if clave == ultimoAcomodo then return end -- cada segundo: solo se reancla si algo cambió ultimoAcomodo = clave if personaje and not personaje.movida then personaje:breakAnchors() personaje:addAnchor(AnchorTop, 'parent', AnchorTop) personaje:addAnchor(AnchorRight, 'parent', AnchorRight) personaje:setMarginTop(bajoMapa) personaje:setMarginRight(abajoTibia and anchoTibia or 0) end -- el seguimiento de misiones va a la izquierda del minimapa; si Personaje está abierto a la izquierda de la columna -- de Tibia (le taparía), el seguimiento se corre a la izquierda de Personaje if seg and not seg.movida then seg:breakAnchors() seg:addAnchor(AnchorTop, 'marcoMinimapa', AnchorTop) if personajeVisible and abajoTibia then seg:addAnchor(AnchorRight, 'parent', AnchorRight) seg:setMarginRight(anchoTibia + personaje:getWidth() + 8) else seg:addAnchor(AnchorRight, 'marcoMinimapa', AnchorLeft) seg:setMarginRight(8) end seg:setMarginTop(0) end if bolsas and not bolsas.movida then bolsas:breakAnchors() bolsas:addAnchor(AnchorBottom, 'micromenu', AnchorTop) bolsas:addAnchor(AnchorRight, 'parent', AnchorRight) bolsas:setMarginBottom(8) bolsas:setMarginRight(choca and anchoTibia or 0) end end -- Lo llama game_autobotin cuando el servidor dice si el botín automático está activo (también al entrar). function marcarAutobotin(on) local b = botones.autobotin if b then b:setOn(on and true or false) b:setTooltip(on and 'Botín automático: activo (clic para apagar)' or 'Botín automático: apagado (clic para activar)') end end local function refrescarContadores() if botones.bloqueo then botones.bloqueo:setOn(OtmmoVentanas.bloqueada()) botones.bloqueo:setTooltip(OtmmoVentanas.bloqueada() and 'UI bloqueada: clic para editar' or 'UI editable: clic para bloquear') end if botones.pvp then botones.pvp:setOn(not g_game.isSafeFight()) end marcarAutobotin(modules.game_autobotin and modules.game_autobotin.activo()) local m = modules.game_misiones contador(botones.misiones, m and m.getListas and m.getListas() or nil) local t = modules.game_talentos contador(botones.talentos, t and t.getLibres and t.getLibres() or nil) refrescarZoom() acomodarDerecha() end local function ventanaEnemigos() return rootWidget:recursiveGetChildById('battleWindow') end local function marcar(id, on) if botones[id] then botones[id]:setOn(on) end end function toggleGrupo() modules.game_grupo.toggle() end -- El borde oro de un botón = su ventana está abierta (estado real, mirando la ventana). function refrescarEstados() local vip = modules.game_viplist and modules.game_viplist.vipWindow marcar('amigos', vip and vip:isVisible() or false) local bw = ventanaEnemigos() marcar('enemigos', bw and bw:isVisible() or false) -- se recuerda si Enemigos y Amigos quedaron abiertas g_settings.setNode('game_hud', { enemigos = bw and bw:isVisible() or false, amigos = vip and vip:isVisible() or false }) local sp = modules.game_spelllist and modules.game_spelllist.spelllistWindow marcar('hechizos', sp and sp:isVisible() or false) marcar('personaje', personaje and personaje:isVisible() or false) marcar('bolsas', bolsas and bolsas:isVisible() or false) end -- Personaje ------------------------------------------------------------------------------------- local function refrescarPersonaje() local p = g_game.getLocalPlayer() if not personaje or not p or not personaje:isVisible() then return end personaje.nombre:setText(p:getName()) personaje.clase:setText(string.format('Nivel %d · %s', p:getLevel(), CLASES[p:getVocation()] or 'Aventurero')) personaje.vida.etiqueta:setText('Vida') personaje.vida.valor:setText(string.format('%d / %d', p:getHealth(), p:getMaxHealth())) personaje.mana.etiqueta:setText('Maná') personaje.mana.valor:setText(string.format('%d / %d', p:getMana(), p:getMaxMana())) end -- Decisión del usuario (guía E): el muñeco de Personaje queda solo con el equipo. Fuera posturas, modos de -- combate, PvP, «Stop», capacidad, alma, bolsa «purse», bendiciones y la fila de condiciones de Tibia. -- Se ocultan (no se borran): game_inventory sigue teniéndolos y su lógica no cambia. local TIBIA_INVENTARIO = { 'changeSize', 'blessings', 'purseButton', 'standPosture', 'followPosture', 'expert', 'pvp', 'whiteHandBox', 'redFistBox', 'whiteDoveBox', 'yellowHandBox', 'attack', 'balanced', 'defense', 'stop', 'icons', 'soulPanel', 'capacityPanel' } function ocultarTibia(inv) local panel = inv and inv.onPanel if not panel then return end for _, id in ipairs(TIBIA_INVENTARIO) do local w = panel:getChildById(id) if w then w:hide() end end end local function montarEquipo() local ic = modules.game_inventory and modules.game_inventory.inventoryController local inv = ic and ic.ui ocultarTibia(inv) -- cada vez: game_inventory vuelve a mostrar algunos (purse, modos) al refrescar if not inv or not personaje or inv:getParent() == personaje.equipo then return end inv:setParent(personaje.equipo) inv:breakAnchors() inv:addAnchor(AnchorTop, 'parent', AnchorTop) inv:addAnchor(AnchorHorizontalCenter, 'parent', AnchorHorizontalCenter) inv:setWidth(124) -- tres columnas de casillas (8 + 3 x 34 + 2 x 3 + 8), centradas en el panel inv:setMarginTop(2) inv:setHeight(154) inv:setDraggable(false) inv:setImageSource('') inv:setBackgroundColor('alpha') inv:setBorderWidth(0) inv:show() end function togglePersonaje() if not personaje then return end if personaje:isVisible() then personaje:hide() acomodarDerecha() else montarEquipo() personaje:show() personaje:raise() acomodarDerecha() refrescarPersonaje() end marcar('personaje', personaje:isVisible()) end -- Bolsa unificada ------------------------------------------------------------------------------- local function contenedores() local lista = {} for _, c in pairs(nuestros) do -- guardamos los objetos: g_game.getContainers() no llega bien a Lua lista[#lista + 1] = c end table.sort(lista, function(a, b) return a:getId() < b:getId() end) return lista end local function claveDe(pos) return pos and string.format('%d:%d:%d', pos.x, pos.y, pos.z) or '' end -- Abre las bolsas que hay dentro de las nuestras (una vez cada una). local function abrirSubBolsas() local total = #contenedores() for _, c in ipairs(contenedores()) do for _, it in ipairs(c:getItems()) do if total >= MAX_BOLSAS then return end if it:isContainer() then local k = claveDe(it:getPosition()) if not abiertasPorBolsa[k] then abiertasPorBolsa[k] = true pendientes[#pendientes + 1] = it:getId() g_game.open(it, nil) total = total + 1 end end end end end local mochilaEspalda, asegurarMochila, programarPintado -- definidas más abajo local function pintarBolsas() refrescoProgramado = nil if not bolsas or not bolsas:isVisible() or bolsas.otmmoPlegados then return end local panel = bolsas.casillas -- 23-oct: no se rehace la cuadrícula con una casilla arrastrándose (se destruiría a medio arrastre y el ícono podía -- quedarse pegado al ratón): se repinta al soltar. local arrastrada = g_ui.getDraggingWidget() if arrastrada and not arrastrada:isDestroyed() and arrastrada:getParent() == panel then refrescoProgramado = scheduleEvent(pintarBolsas, 200) return end panel:destroyChildren() local usadas, total, oro = 0, 0, 0 local lista = contenedores() for _, c in ipairs(lista) do for slot = 0, c:getCapacity() - 1 do local it = c:getItem(slot) local w = g_ui.createWidget('CasillaBolsa', panel) w:setItem(it) ItemsDatabase.setCalidad(w, it) -- marco de color de la calidad (gamelib/items.lua) w.position = c:getSlotPosition(slot) total = total + 1 if it then usadas = usadas + 1 oro = oro + (ORO[it:getId()] or 0) * it:getCount() if it:isContainer() then -- ya se muestra su contenido aquí: el clic derecho no la abre encima de la mochila w.onMouseRelease = function(self, pos, boton) if boton == MouseRightButton then return true end return UIItem.onMouseRelease(self, pos, boton) end w:setBorderWidth(1) w:setBorderColor(OtmmoTheme.color.borderHover) end else w:setOpacity(0.55) end end end local columnas = math.max(3, math.floor(panel:getWidth() / 36)) panel:getLayout():setNumColumns(columnas) local filas = math.max(1, math.ceil(total / columnas)) bolsas.vacio:setVisible(#lista == 0) if #lista == 0 and mochilaEspalda() then bolsas.vacio:setText('Abriendo tu mochila...') scheduleEvent(function() -- si se cerró (p. ej. al usarla desde Personaje), se vuelve a pedir if bolsas and bolsas:isVisible() and #contenedores() == 0 then asegurarMochila() programarPintado() end end, 2100) else bolsas.vacio:setText('No llevas mochila: equipa una en la espalda (ventana Personaje, I).') end bolsas.oro:setText(tostring(oro)) bolsas.cuenta:setText(string.format('%d / %d', usadas, total)) if not bolsas.otmmoResized and not bolsas.otmmoPlegados then bolsas:setHeight(math.min(480, 36 + (#lista == 0 and 44 or filas * 36) + 8 + 18 + 12)) end acomodarDerecha() -- pegada encima del micromenú mientras el jugador no la arrastre end function programarPintado() if not refrescoProgramado then refrescoProgramado = scheduleEvent(pintarBolsas, 30) end end -- La mochila de la espalda, si es un contenedor. function mochilaEspalda() local p = g_game.getLocalPlayer() local espalda = p and p:getInventoryItem(InventorySlotBack) return espalda end -- ¿Este contenedor abierto es la mochila de la espalda? (raíz, mismo objeto y, si se sabe, en la ranura del personaje) local function esLaMochila(c, espalda) local it = c:getContainerItem() if not espalda or not it or c:hasParent() or it:getId() ~= espalda:getId() then return false end local pos = it:getPosition() return not pos or pos.x == 65535 end -- 23-oct: «si abro Bolsas directamente me dice que no tengo bolsa». Antes solo se pedía abrir la mochila si no había -- NINGÚN contenedor abierto (por miedo a cerrarla pidiéndola dos veces): con el buzón de la tienda, un cadáver o un -- cofre abierto, no se pedía y la ventana decía «No llevas mochila» aunque la hubiera. Ahora: si la mochila ya está -- abierta se adopta; si no, se pide (una vez cada 2 s como mucho, por si el servidor no contesta). local ultimoPedido = 0 function asegurarMochila() if #contenedores() > 0 then return end local espalda = mochilaEspalda() if not espalda then return end -- el servidor puede haberla abierto ya al entrar (antes de que el equipo estuviera cargado). Se mira también la -- lista del motor: pedir abrir una mochila ya abierta la CERRARÍA (el servidor alterna). pcall(function() for id, c in pairs(g_game.getContainers() or {}) do if type(c) == 'userdata' and not todos[id] then todos[id] = c end end end) for id, c in pairs(todos) do if esLaMochila(c, espalda) then nuestros[id] = c if c.window then c.window:hide() end end end if #contenedores() > 0 then abrirSubBolsas() return end if g_clock.millis() - ultimoPedido < 2000 then return end ultimoPedido = g_clock.millis() pendientes[#pendientes + 1] = espalda:getId() g_game.open(espalda, nil) end function toggleBolsas() if not bolsas then return end if bolsas:isVisible() then bolsas:hide() marcar('bolsas', false) return end bolsas:show() bolsas:raise() marcar('bolsas', true) if #contenedores() == 0 then asegurarMochila() else abrirSubBolsas() end pintarBolsas() end local function esperado(container) local it = container:getContainerItem() local id = it and it:getId() for i, pid in ipairs(pendientes) do if pid == id then table.remove(pendientes, i) return true end end -- la mochila de la espalda abierta a mano (clic derecho en el equipo) también es nuestra local p = g_game.getLocalPlayer() local espalda = p and p:getInventoryItem(InventorySlotBack) return not container:hasParent() and espalda and id == espalda:getId() and next(nuestros) == nil end local function onOpen(container, previous) todos[container:getId()] = container -- el servidor reabre la misma mochila al cambiar su contenido (open con previous = la vieja): sigue siendo nuestra local eraNuestra = previous and nuestros[previous:getId()] ~= nil if eraNuestra then nuestros[previous:getId()] = nil end if not eraNuestra and not esperado(container) then return end nuestros[container:getId()] = container scheduleEvent(function() if container.window then container.window:hide() end end, 1) if bolsas and not bolsas:isVisible() then bolsas:show() marcar('bolsas', true) end abrirSubBolsas() programarPintado() end local function onClose(container) if todos[container:getId()] == container then todos[container:getId()] = nil end -- el cierre de la versión vieja llega DESPUÉS de abrir la nueva con el mismo id: no tocar la nueva if nuestros[container:getId()] == container then nuestros[container:getId()] = nil abiertasPorBolsa = {} programarPintado() end end local function onCambio(container) if nuestros[container:getId()] then if container.window and container.window:isVisible() then container.window:hide() end programarPintado() end end -- Minimapa redondo ------------------------------------------------------------------------------ local function crearBoton(estilo, padre, b) local w = g_ui.createWidget(estilo, padre) w:setId('hud_' .. b[1]) w:setIcon(ICONOS .. b[3]) w:setTooltip(b[2]) w.onClick = function(self) b[4](self) scheduleEvent(refrescarEstados, 100) end botones[b[1]] = w return w end -- El minimapa (game_minimap) sale de su panel de Tibia y entra en el marco pizarra. game_minimap sigue usando -- mapController.ui.minimapBorder: se deja como campo Lua para que lo encuentre aunque ya no sea hijo de ese panel -- (DESPUÉS de setParent: al sacar un hijo, OTClient borra el campo con su id en el padre viejo). local function montarMinimapa() local mc = modules.game_minimap and modules.game_minimap.mapController local ui = mc and mc.ui if not ui or not marcoMapa then return end local borde = ui.minimapBorder or (marcoMapa and marcoMapa.minimapBorder) if not borde then return end if borde:getParent() ~= marcoMapa then borde:setParent(marcoMapa) ui.minimapBorder = borde borde:breakAnchors() borde:addAnchor(AnchorHorizontalCenter, 'hueco', AnchorHorizontalCenter) borde:addAnchor(AnchorVerticalCenter, 'hueco', AnchorVerticalCenter) borde:setImageSource('') borde:setMargin(0) borde:setSize({ width = 176, height = 176 }) borde.minimap:setMargin(12) borde.minimap:setCircular(true) marcoMapa.rosa:raise() end -- el panel de Tibia queda vacío: sin alto, para que no robe clics en la esquina ui:setHeight(0) ui.panelHeight = 0 ui:hide() -- las mini ventanas de Tibia (Enemigos, Amigos...) empiezan debajo del marco, no detrás local panel = modules.game_interface.getMainRightPanel() if panel then panel:setMarginTop(marcoMapa:getMarginTop() + marcoMapa:getHeight() + 8) end -- la columna de Tibia, pegada al borde derecho (sin hueco) local columna = modules.game_interface.getRightPanel() if columna then columna:setMarginRight(0) end end -- Arranque -------------------------------------------------------------------------------------- -- Las ventanas solo se arrastran desde el título: así arrastrar una casilla mueve el objeto, no la ventana. local function soloTitulo(w) -- la ventana no es arrastrable: el arrastre de una casilla es del objeto. Se mueve solo desde el título. w:setDraggable(false) local agarre = nil w.onMousePress = function(self, pos, boton) if not OtmmoVentanas.bloqueada() and boton == MouseLeftButton and pos.y < self:getY() + 28 then local x, y = self:getX(), self:getY() self:breakAnchors() self:setPosition({ x = x, y = y }) agarre = { x = pos.x - self:getX(), y = pos.y - self:getY() } return true end return false end w.onMouseMove = function(self, pos) -- 23-oct: si la suelta del botón no llegó (p. ej. se soltó fuera de la ventana o del navegador), la ventana se -- quedaba pegada al ratón. Sin el botón pulsado, el agarre se termina aquí. if agarre and not g_window.isMouseButtonPressed(MouseLeftButton) then agarre = nil if self.movida then OtmmoVentanas.guardar(self) end return false end if OtmmoVentanas.bloqueada() then agarre = nil end if agarre then self:setPosition({ x = pos.x - agarre.x, y = pos.y - agarre.y }) self:bindRectToParent() self.movida = true return true end return false end w.onMouseRelease = function(self) local habia = agarre ~= nil agarre = nil if habia and self.movida then OtmmoVentanas.guardar(self) end -- posición entre sesiones (fase 4) return habia end end local function montar() -- el panel de botones de Tibia (VIP, habilidades...) lo sustituye el micromenú local oc = modules.game_mainpanel and modules.game_mainpanel.optionsController if oc and oc.ui then oc.ui:hide() end montarMinimapa() montarEquipo() if modules.game_mainpanel then modules.game_mainpanel.reloadMainPanelSizes() end -- La barra superior de Tibia (iconos repetidos con el micromenú) se oculta: el mapa gana esa franja. if modules.client_topmenu then modules.client_topmenu.hide() end -- Barra de acciones: una sola fila de 10; las flechas para pasar de página de Tibia sobran. local raiz = modules.game_interface.getRootPanel() local function ocultarFlechas(w) for _, h in ipairs(w:getChildren()) do local id = h:getId() if id == 'prevPanel' or id == 'nextPanel' then h:setWidth(0) h:hide() else ocultarFlechas(h) end end end ocultarFlechas(raiz) modules.game_actionbar.instalarFlotantes() -- el candado de Tibia pegado a la barra sobra: «Bloquear la barra» está en el menú «Más» local candado = raiz:recursiveGetChildById('lockPanel') if candado then candado:setWidth(0) candado:hide() end -- Seguimiento de misiones: arriba a la derecha, a la izquierda del marco del minimapa (antes iba pegado al panel -- derecho de Tibia, que ahora queda debajo del marco) local seg = raiz:recursiveGetChildById('seguimientoMisiones') if seg and marcoMapa and not seg.movida then seg:breakAnchors() seg:addAnchor(AnchorTop, 'marcoMinimapa', AnchorTop) seg:addAnchor(AnchorRight, 'marcoMinimapa', AnchorLeft) seg:setMarginTop(0) seg:setMarginRight(8) end -- Enemigos y Amigos ya no están fijas en el panel derecho: solo se abren desde el micromenú local guardado = g_settings.getNode('game_hud') or {} local bw = ventanaEnemigos() if bw and bw:isVisible() ~= (guardado.enemigos == true) then modules.game_battle.toggle() end local vip = modules.game_viplist and modules.game_viplist.vipWindow if vip and vip:isVisible() ~= (guardado.amigos == true) then modules.game_viplist.toggle() end refrescarEstados() refrescarZoom() ultimoAcomodo = nil acomodarDerecha() end function hudController:onInit() g_ui.importStyle('hud') connect(Container, { onOpen = onOpen, onClose = onClose, onSizeChange = onCambio, onUpdateItem = onCambio, onAddItem = onCambio, onRemoveItem = onCambio }) Keybind.new('Ventanas', 'Personaje', { [CHAT_MODE.ON] = '', [CHAT_MODE.OFF] = 'I' }, '') -- I/B no deben abrir ventanas mientras se escribe en otra caja (p. ej. la ventana de NPC, que no activa el chat) local function escribiendo() local w = rootWidget while w:getFocusedChild() do w = w:getFocusedChild() end return w:getClassName() == 'UITextEdit' and w:isVisible() end Keybind.bind('Ventanas', 'Personaje', { { type = KEY_DOWN, callback = function() if not escribiendo() then togglePersonaje() end end } }) Keybind.new('Ventanas', 'Bolsas', { [CHAT_MODE.ON] = '', [CHAT_MODE.OFF] = 'B' }, '') Keybind.bind('Ventanas', 'Bolsas', { { type = KEY_DOWN, callback = function() if not escribiendo() then toggleBolsas() end end } }) end function hudController:onTerminate() disconnect(Container, { onOpen = onOpen, onClose = onClose, onSizeChange = onCambio, onUpdateItem = onCambio, onAddItem = onCambio, onRemoveItem = onCambio }) Keybind.delete('Ventanas', 'Personaje') Keybind.delete('Ventanas', 'Bolsas') removeEvent(contadoresEvento) for _, w in ipairs({ personaje, bolsas, micromenu, marcoMapa }) do if w then w:destroy() end end end function hudController:onGameStart() local raiz = modules.game_interface.getRootPanel() if not personaje then personaje = g_ui.createWidget('VentanaPersonaje', raiz) personaje.cerrar.onClick = togglePersonaje personaje:hide() bolsas = g_ui.createWidget('VentanaBolsas', raiz) bolsas.cerrar.onClick = toggleBolsas bolsas:hide() soloTitulo(personaje) soloTitulo(bolsas) OtmmoVentanas.recordar(personaje, 'personaje') OtmmoVentanas.recordar(bolsas, 'bolsas', { tamano = true }) OtmmoVentanas.redimensionar(bolsas, 180, 150, programarPintado) bolsas.onUnfold = programarPintado personaje.otmmoClose:hide() bolsas.otmmoClose:hide() micromenu = g_ui.createWidget('Micromenu', raiz) -- grupo aparte abajo a la derecha, pegado al borde (23-oct) y a la altura de la barra principal micromenu:addAnchor(AnchorRight, 'parent', AnchorRight) micromenu:addAnchor(AnchorBottom, 'gameBottomActionPanel', AnchorBottom) micromenu:setMarginRight(0) micromenu:setMarginBottom(2) for _, b in ipairs(BOTONES) do crearBoton('BotonMicro', micromenu, b) end marcoMapa = g_ui.createWidget('MarcoMinimapa', raiz) marcoMapa:addAnchor(AnchorTop, 'parent', AnchorTop) marcoMapa:addAnchor(AnchorRight, 'parent', AnchorRight) marcoMapa:setMarginTop(8) local agarre = g_ui.createWidget('UIButton', marcoMapa) agarre:setId('moverMapa') agarre:setText('Mapa') agarre:setSize({ width = 70, height = 18 }) agarre:addAnchor(AnchorTop, 'parent', AnchorTop) agarre:addAnchor(AnchorHorizontalCenter, 'parent', AnchorHorizontalCenter) agarre:setTooltip('Arrastra para mover el minimapa') OtmmoVentanas.moverCon(marcoMapa, agarre, 'minimapa') marcoMapa:setMarginRight(0) -- pegado al borde derecho, como el resto de la columna (23-oct) for _, b in ipairs(BOTONES_MAPA_IZQ) do crearBoton('BotonMapa', marcoMapa.columnaIzq, b) end for _, b in ipairs(BOTONES_MAPA_DER) do crearBoton('BotonMapa', marcoMapa.columnaDer, b) end end removeEvent(contadoresEvento) contadoresEvento = cycleEvent(refrescarContadores, 1000) -- 23-oct: una vez, las posiciones guardadas de Personaje y Bolsas se olvidan para que entren en la columna nueva if not g_settings.getBoolean('otmmoColumnaDerecha', false) then OtmmoVentanas.olvidar('personaje') OtmmoVentanas.olvidar('bolsas') personaje.movida, bolsas.movida = nil, nil g_settings.set('otmmoColumnaDerecha', true) end -- colocar las ventanas: personaje bajo el minimapa, bolsas sobre el micromenú, pegadas al borde derecho ultimoAcomodo = nil acomodarDerecha() -- (no se vacía «todos»: la mochila puede abrirse antes de onGameStart) scheduleEvent(montar, 300) scheduleEvent(montar, 1500) -- algunos módulos rehacen su panel al arrancar hudController:registerEvents(LocalPlayer, { onLevelChange = refrescarPersonaje, onHealthChange = refrescarPersonaje, onManaChange = refrescarPersonaje, onInventoryChange = function(_, slot) if slot == InventorySlotBack and bolsas and bolsas:isVisible() then asegurarMochila() programarPintado() end end, }) end function hudController:onGameEnd() removeEvent(contadoresEvento) if modules.client_topmenu then modules.client_topmenu.show() end nuestros, pendientes, abiertasPorBolsa, todos = {}, {}, {}, {} if personaje then personaje:hide() end if bolsas then bolsas:hide() end for id in pairs(botones) do marcar(id, false) end end