-- OT-MMO: marco de objetivo (estilo WoW). Arriba, a la izquierda del centro: retrato, nombre, qué es (enemigo, -- jefe, jugador y su clase, invocación), vida en % y la barra de carga cuando el objetivo canaliza. Debajo, la fila -- «Ataca a» (objetivo del objetivo, opcode 155 «T|id|nombre» de server/scripts/objetivo.lua, que recibe game_apuntar) -- y, aparte, el marco de FOCO (Alt+clic, game_apuntar): más pequeño, con su vida y su carga. -- Solo escucha. Las cargas de jugadores llegan por el opcode 101 (server/scripts/lanzamiento.lua, que pinta -- game_vitals sobre la cabeza) y las habilidades de jefe por el 106 (server/scripts/combate/avisos.lua, que pinta -- game_avisos). Para leerlas sin quitarles el opcode se envuelve ProtocolGame.onExtendedOpcode. local CLASES = { [1] = 'Guardián', [11] = 'Guardián', [2] = 'Cazador', [12] = 'Cazador', [3] = 'Hechicero', [13] = 'Hechicero', [4] = 'Druida', [14] = 'Druida', [6] = 'Brujo', [7] = 'Bardo', [8] = 'Gunslinger' } -- clientid de vocations.xml -- ponytail: el servidor no marca a los jefes; lista a mano hasta que lo haga (p. ej. con un icono de criatura). -- En minúsculas: el cliente recibe «Centinela De Prueba». local RANGOS = { ['varg el tuerto'] = 'Jefe', ['rey hueco'] = 'Jefe', ['centinela de prueba'] = 'Jefe', ['coloso de ceniza'] = 'Élite' } local NOMBRES = { Regeneracion = 'Regeneración' } -- el servidor envía los nombres de carga sin acentos local C = OtmmoTheme.color -- tokens (data/styles/0-vars.otui) local ORO, BRASA, SANGRE, APAGADO = C.accent, C.warn, C.hp, C.text2 objetivoController = Controller:new() -- Dos marcos con la misma lógica: el de objetivo y el de foco. m = { panel, criatura, carga, avisoId } local obj = { clave = 'objetivo' } local foco = { clave = 'foco' } local tot = nil -- objetivo del objetivo: { id, nombre } local original = nil local function pintarVida(m) if not m.criatura or not m.panel then return end local pct = m.criatura:getHealthPercent() local barra = m.panel.marco.vida barra.fill:setWidth(math.max(1, math.floor((barra:getWidth() - 2) * pct / 100))) barra.pct:setText(pct .. ' %') end local function quitarCarga(m) if m.carga then removeEvent(m.carga.evento) m.carga = nil end if m.panel then m.panel.carga:hide() end end local function mostrarCarga(m, nombre, ms, interrumpible) quitarCarga(m) local b = m.panel.carga local c = { inicio = g_clock.millis(), ms = math.max(1, ms) } b.nombre:setText(nombre) b.fill:setBackgroundColor(interrumpible and BRASA or ORO) b.fill:setOpacity(0.35) b:setBorderColor(interrumpible and BRASA or C.border) b.aviso:setVisible(interrumpible) b:show() local function paso() local hecho = g_clock.millis() - c.inicio local frac = math.min(1, hecho / c.ms) b.fill:setWidth(math.max(1, math.floor((b:getWidth() - 2) * frac))) b.tiempo:setText((string.format('%.1f s', math.max(0, c.ms - hecho) / 1000):gsub('%.', ','))) if frac >= 1 then quitarCarga(m) end end c.evento = cycleEvent(paso, 30) m.carga = c paso() end -- cortada antes de terminar: un momento en rojo, como la barra sobre la cabeza local function cortarCarga(m) if not m.carga then return end removeEvent(m.carga.evento) m.carga = nil local b = m.panel.carga b.fill:setBackgroundColor(SANGRE) b.fill:setOpacity(0.6) b.fill:setWidth(b:getWidth() - 2) b.nombre:setText('Interrumpido') b.aviso:hide() b.tiempo:setText('') scheduleEvent(function() if m.panel and not m.carga then m.panel.carga:hide() end end, 600) end -- Jefe con nombre de la lista de arriba (la usa también game_apuntar para el aro dorado) function esJefe(c) return c ~= nil and c:isMonster() and RANGOS[c:getName():lower()] ~= nil end local function queEs(c) if c:isPlayer() then local clase = CLASES[c:getVocation()] return clase and ('Jugador · ' .. clase) or 'Jugador', APAGADO elseif c:isNpc() then return 'Habitante', APAGADO end local t = c:getType() if t == CreatureTypeSummonOwn then return 'Tu invocación', APAGADO elseif t == CreatureTypeSummonOther then return 'Invocación', APAGADO end local rango = RANGOS[c:getName():lower()] if rango then return rango, ORO end -- monstruo de pack de una dungeon (server/scripts/dungeon_elite.lua, opcode 139 en game_elite) if modules.game_elite and modules.game_elite.esElite and modules.game_elite.esElite(c) then return 'Élite', ORO, true end return 'Enemigo', APAGADO end -- Fila «Ataca a»: nombre y vida pequeña del objetivo del objetivo. La carga baja debajo de la fila si se ve. local function pintarTot() local p = obj.panel if not p then return end local fila = p.tot local ver = obj.criatura ~= nil and tot ~= nil fila:setVisible(ver) p.carga:breakAnchors() p.carga:addAnchor(AnchorTop, ver and 'tot' or 'marco', AnchorBottom) p.carga:addAnchor(AnchorLeft, 'parent', AnchorLeft) p.carga:addAnchor(AnchorRight, 'parent', AnchorRight) if not ver then return end local yo = g_game.getLocalPlayer() local c = g_map.getCreatureById(tot.id) local esYo = yo and tot.id == yo:getId() fila.nombre:setText(esYo and 'ti' or (c and c:getName() or tot.nombre)) -- «Ataca a ti» fila.nombre:setColor(esYo and C.errorText or C.text) fila:setBorderColor(esYo and '#C9424299' or C.border) fila.vida:setVisible(c ~= nil) if c then local pct = c:getHealthPercent() fila.vida.fill:setWidth(math.max(1, math.floor((fila.vida:getWidth() - 2) * pct / 100))) fila.vida.fill:setBackgroundColor(pct <= 30 and C.hp or (c:isPlayer() and C.ok or C.hp)) end end -- game_apuntar lo llama con lo que manda el servidor (opcode 155 «T|...»): id nil = tu objetivo no ataca a nadie function alObjetivoDelObjetivo(id, nombre) tot = id and { id = id, nombre = nombre or '' } or nil pintarTot() end local function llenar(m, c) m.criatura, m.avisoId = c, nil quitarCarga(m) if not m.panel then return end if not c then return m.panel:hide() end local marco = m.panel.marco marco.retratoMarco.retrato:setCreature(c) marco.nombre:setText(c:getName()) local texto, color, elite = queEs(c) if marco.tipo then marco.tipo:setText(texto) marco.tipo:setColor(color) end if m.panel.dragon then m.panel.dragon:setVisible(elite == true) -- el dragon dorado del retrato (images/elite-retrato.png) end -- jefe/élite: borde de acento; el resto, borde normal marco:setBorderColor(color == ORO and ORO or C.border) marco.retratoMarco:setBorderColor(color == ORO and ORO or C.border) pintarVida(m) m.panel:show() end local function actualizar() if not obj.panel then return end local c = g_game.getAttackingCreature() or g_game.getFollowingCreature() if c == obj.criatura then return end tot = nil -- el del objetivo anterior ya no vale; el servidor manda el nuevo en <= 0,5 s llenar(obj, c) pintarTot() end -- game_elite lo llama cuando llega la lista de elite: el objetivo actual puede haber pasado a serlo. function refrescarObjetivo() obj.criatura = nil actualizar() if foco.criatura then llenar(foco, foco.criatura) end if modules.game_apuntar and modules.game_apuntar.refrescar then modules.game_apuntar.refrescar() end end -- game_apuntar: marco de foco (nil lo quita) function ponerFoco(c) if c == foco.criatura then return end llenar(foco, c) end -- Amenaza (server/scripts/amenaza.lua, «A|pct|lleva»): ok lejos, xp (amarillo) desde el 80 %, warn desde el 100 % -- (a punto de quitárselo al tanque), rojo cuando el monstruo va a por ti. local function pintarAmenaza(buffer) local w = obj.panel and obj.panel.amenaza if not w then return end local pct, lleva = buffer:match('^A|(%d+)|(%d)$') if not pct then return w:hide() end pct = tonumber(pct) w:setText((lleva == '1' and 'Te ataca ' or 'Amenaza ') .. pct .. ' %') w:setColor(lleva == '1' and C.errorText or pct >= 100 and C.warn or pct >= 80 and C.xp or C.ok) w:show() end local function escuchar(opcode, buffer) if opcode == 119 then return pintarAmenaza(buffer) end if opcode ~= 101 and opcode ~= 106 then return end local p = buffer:split('|') for _, m in ipairs({ obj, foco }) do local c = m.criatura if c and m.panel then if opcode == 101 then if tonumber(p[2]) == c:getId() then if p[1] == 'C' then mostrarCarga(m, NOMBRES[p[3]] or p[3], tonumber(p[4]) or 1000, false) elseif p[1] == 'X' then if p[3] == 'ok' then quitarCarga(m) else cortarCarga(m) end end end elseif c:isMonster() then if p[1] == 'A' then -- con «lanza» (7.º campo) se sabe quién lo lanza; sin él se atribuye al monstruo que tienes de -- objetivo (ponytail: los avisos de jefe no lo dicen), nunca al foco local lanza = tonumber(p[7]) if (lanza and lanza == c:getId()) or (not lanza and m == obj) then m.avisoId = p[2] mostrarCarga(m, p[4], tonumber(p[5]) or 1000, p[3] == 'INTERRUMPE') end elseif p[1] == 'C' and p[2] == m.avisoId then m.avisoId = nil cortarCarga(m) -- solo hay carga si se quitó antes de acabar end end end end end local function onSalud(c) if c == obj.criatura then pintarVida(obj) end if c == foco.criatura then pintarVida(foco) end if tot and c:getId() == tot.id then pintarTot() end end function objetivoController:onInit() original = ProtocolGame.onExtendedOpcode ProtocolGame.onExtendedOpcode = function(self, opcode, buffer) original(self, opcode, buffer) local ok, err = pcall(escuchar, opcode, buffer) if not ok then g_logger.error('[objetivo] ' .. tostring(err)) end end connect(g_game, { onAttackingCreatureChange = actualizar, onFollowingCreatureChange = actualizar }) connect(Creature, { onHealthPercentChange = onSalud }) if g_game.isOnline() then -- recargado a mitad de partida self:onGameStart() end end function objetivoController:onGameStart() if obj.panel then return end local raiz = modules.game_interface.getRootPanel() -- arriba, a la izquierda de la columna central (contrato y avisos de jefe van en el centro) obj.panel = g_ui.loadUI('objetivo', raiz) obj.panel:addAnchor(AnchorTop, 'parent', AnchorTop) obj.panel:addAnchor(AnchorRight, 'parent', AnchorHorizontalCenter) obj.panel:setMarginTop(0) -- el contenedor sube 12 px (y crece 36 a la izquierda) por el dragon de los elite obj.panel:setMarginRight(216) obj.panel:hide() -- foco: debajo del bloque del objetivo, alineado con el borde izquierdo de su marco (268 de ancho) foco.panel = g_ui.loadUI('foco', raiz) foco.panel:addAnchor(AnchorTop, 'parent', AnchorTop) foco.panel:addAnchor(AnchorRight, 'parent', AnchorHorizontalCenter) foco.panel:setMarginTop(136) foco.panel:setMarginRight(216 + 268 - foco.panel:getWidth()) foco.panel:hide() actualizar() pintarTot() end function objetivoController:onGameEnd() tot = nil for _, m in ipairs({ obj, foco }) do m.criatura = nil if m.panel then quitarCarga(m) m.panel:hide() end end end function objetivoController:onTerminate() if original then ProtocolGame.onExtendedOpcode = original original = nil end disconnect(g_game, { onAttackingCreatureChange = actualizar, onFollowingCreatureChange = actualizar }) disconnect(Creature, { onHealthPercentChange = onSalud }) for _, m in ipairs({ obj, foco }) do if m.panel then quitarCarga(m) m.panel:destroy() m.panel = nil end m.criatura = nil end tot = nil end