-- OT-MMO: beneficios y perjuicios arriba a la izquierda, estilo WoW, con el tiempo que les queda. -- El servidor (server/scripts/estados.lua) manda por el opcode 121 la lista de estados del jugador: -- «E|clave^c^ms^total^extra~...» c = b (beneficio) | p (perjuicio) | n (neutro); ms = -1 si es permanente. -- Entre mensaje y mensaje la cuenta atrás es local. Fila de beneficios y debajo la de perjuicios (con borde del color -- de su tipo). Sustituye a la columna de íconos de estado de Tibia (game_healthcircle/statusiconbar.lua), que ya no -- se muestra. local OPCODE = 121 local MARGEN = 12 -- px desde la esquina del mapa (escala de 4 de la guía) local PASO = 36 -- celda de 32 (casilla de 30) + 4 de hueco: «18 s» cabe debajo sin pisar al vecino local ALTO = 45 -- casilla + tiempo debajo local AVISO = 5000 -- ms: en los últimos 5 s el ícono parpadea suave -- Colores de la guía (data/styles/0-vars.otui) local C = OtmmoTheme.color local BORDE, VERDE, NARANJA, ROJO, AZUL, ACENTO, GRIS = C.border, C.ok, C.warn, C.hp, C.mana, C.accent, C.text2 local CONDICION = '/game_healthcircle/images/conditions/player-state-flags-%s.png' local function cond(n) return { imagen = CONDICION:format(n) } end local function hechizo(clientId) return { hechizo = clientId } end -- índice en la tira de íconos de habilidad -- clave del servidor -> nombre, descripción corta, ícono y color del borde (solo perjuicios) local ESTADOS = { -- perjuicios veneno = { 'Envenenado', 'Pierdes vida poco a poco por daño de tierra.', cond('00'), VERDE }, fuego = { 'Ardiendo', 'Pierdes vida poco a poco por daño de fuego.', cond('01'), NARANJA }, energia = { 'Electrizado', 'Pierdes vida poco a poco por daño de energía.', cond('02'), AZUL }, sangrado = { 'Sangrando', 'Pierdes vida poco a poco por una herida abierta.', cond('15'), ROJO }, ahogo = { 'Ahogándote', 'Te falta el aire: pierdes vida mientras sigas así.', cond('08'), AZUL }, congelacion = { 'Congelado', 'Pierdes vida poco a poco por daño de hielo.', cond('09'), AZUL }, deslumbrado = { 'Deslumbrado', 'Pierdes vida poco a poco por daño sagrado.', cond('10'), ACENTO }, maldicion = { 'Maldito', 'Pierdes cada vez más vida por daño de muerte.', cond('11'), GRIS }, lentitud = { 'Ralentizado', 'Te mueves más despacio (o no te mueves si te aturdieron).', cond('05'), ACENTO }, inmovil = { 'Inmovilizado', 'No puedes moverte.', cond('19'), ACENTO }, miedo = { 'Aterrado', 'Huyes sin control.', cond('20'), ACENTO }, borracho = { 'Borracho', 'Te cuesta caminar en línea recta.', cond('03'), ACENTO }, pz_bloqueado = { 'Marcado', 'Atacaste a otro jugador: no puedes entrar en zona segura ni salir del juego.', cond('13'), ROJO }, en_combate = { 'En combate', 'Sin regenerar vida ni usar la piedra de hogar hasta 10 s sin pelear.', cond('07'), GRIS }, -- beneficios prisa = { 'Prisa', 'Te mueves más rápido.', cond('06') }, invisible = { 'Invisible', 'Los monstruos no te ven.', cond('12') }, zona_segura = { 'Zona segura', 'Aquí nadie puede atacarte.', cond('14') }, montado = { 'Montado', 'Vas un 20 % más rápido. No puedes atacar ni lanzar hechizos.', hechizo(23) }, escudo_arcano = { 'Escudo Arcano', 'El daño se descuenta de tu maná en vez de tu vida.', hechizo(8) }, muro_escudos = { 'Muro de Escudos', 'Recibes la mitad del daño.', hechizo(2) }, regeneracion = { 'Regeneración', 'Recuperas vida cada 2 segundos.', hechizo(4) }, floracion = { 'Floración', 'Recuperas vida cada 1,5 segundos.', hechizo(16) }, corteza = { 'Corteza Protectora', 'Una corteza absorbe el daño que recibes.', hechizo(17) }, corteza_ligera = { 'Corteza Ligera', 'Te mueves más rápido mientras dura la corteza.', hechizo(17) }, golpe_escudo = { 'Golpe de Escudo', 'Tu escudo absorbe el daño que recibes.', hechizo(12) }, cargas_arcanas = { 'Cargas Arcanas', 'Cada carga: +20 % de daño y +50 % de coste a Descarga Arcana.', hechizo(21) }, comida_regen = { 'Caldo de huesos', 'Recuperas 8 de vida cada 2 segundos.', { objeto = 9079 } }, comida_vigor = { 'Asado del cazador', '+10 % de vida máxima.', { objeto = 22187 } }, } local DESCONOCIDO = { 'Efecto', '', cond('12'), GRIS } estadosController = Controller:new() local panel = nil local lista = {} -- { clave, c, fin (g_clock.millis), total, extra } local reloj = nil -- «12 s», «3 m», «2 h»: redondeando hacia arriba, como WoW. local function texto(ms) if ms < 0 then return '' end local s = math.ceil(ms / 1000) if s < 60 then return s .. ' s' elseif s < 3600 then return math.ceil(s / 60) .. ' m' end return math.ceil(s / 3600) .. ' h' end local function tooltip(e, resta) local d = ESTADOS[e.clave] or DESCONOCIDO local t = d[1] if d[2] ~= '' then t = t .. '\n' .. d[2] end if e.extra and (e.clave == 'corteza' or e.clave == 'golpe_escudo') then t = t .. '\nAbsorbe ' .. e.extra .. ' de daño más.' elseif e.extra and e.clave == 'cargas_arcanas' then t = t .. '\nCargas: ' .. e.extra .. ' de 4.' end if resta >= 0 then local s = math.ceil(resta / 1000) t = t .. '\n' .. (s < 60 and ('Quedan ' .. s .. ' s') or ('Quedan ' .. math.floor(s / 60) .. ' min ' .. (s % 60) .. ' s')) end return t end local function pintarIcono(w, e) local d = ESTADOS[e.clave] or DESCONOCIDO local ic = d[3] local c = w.casilla if ic.objeto then c.imagen:hide() c.objeto:setItemId(ic.objeto) c.objeto:show() else c.objeto:hide() c.imagen:show() if ic.hechizo then c.imagen:setImageSource(SpelllistSettings.Default.iconFile) c.imagen:setImageClip(Spells.getImageClip(ic.hechizo, 'Default')) else c.imagen:setImageSource(ic.imagen) end end -- perjuicios: borde del color de su tipo; beneficios, el borde neutro de la guía c:setBorderColor(e.c ~= 'b' and (d[4] or ROJO) or BORDE) c.cuenta:setText(e.clave == 'cargas_arcanas' and e.extra and tostring(e.extra) or '') end -- Tiempo, barrido y parpadeo. Lo llama el reloj cada 100 ms. local function avanzar() if not panel then return end local ahora = g_clock.millis() for _, fila in ipairs({ panel.beneficios, panel.perjuicios }) do for _, w in ipairs(fila:getChildren()) do local e = w.estado if e then local resta = e.fin and math.max(0, e.fin - ahora) or -1 w.tiempo:setText(texto(resta)) local alto = 0 if resta >= 0 and e.total and e.total > 0 then alto = math.floor(28 * math.min(1, 1 - resta / e.total)) end w.casilla.barrido:setHeight(alto) if resta >= 0 and resta < AVISO then -- parpadeo suave: la opacidad va y viene entre 1 y 0,45 una vez por segundo w:setOpacity(0.725 + 0.275 * math.cos(ahora / 1000 * 2 * math.pi)) else w:setOpacity(1) end if w:isHovered() then w:setTooltip(tooltip(e, resta)) end end end end end -- Hasta dónde puede crecer la fila sin meterse bajo el marco de objetivo (game_objetivo: borde derecho en el centro -- menos 216 y 268 de ancho). local function ancho() local raiz = modules.game_interface.getRootPanel() local libre = math.floor(raiz:getWidth() / 2) - 216 - 268 - MARGEN - 8 return math.max(PASO * 4, libre) end -- Los marcos de grupo están debajo (game_grupo, arriba a la izquierda): si el jugador no los movió, se bajan lo -- justo para no quedar debajo de los estados. local GRUPO_TOP = 120 local function ajustarGrupo(bajo) local g = modules.game_grupo and modules.game_grupo.grupoController and modules.game_grupo.grupoController.ui if not g then return end local ajustes = g_settings.getNode('game_grupo') or {} if ajustes.x and ajustes.y then return -- lo colocó el jugador end g:setMarginTop(math.max(GRUPO_TOP, bajo + 8)) end local function colocarFila(fila, estados, max) fila:destroyChildren() local porFila = math.max(1, math.floor((max + 4) / PASO)) for i, e in ipairs(estados) do local w = g_ui.createWidget('EstadoIcono', fila) w.estado = e w:addAnchor(AnchorTop, 'parent', AnchorTop) w:addAnchor(AnchorLeft, 'parent', AnchorLeft) w:setMarginLeft(((i - 1) % porFila) * PASO) w:setMarginTop(math.floor((i - 1) / porFila) * (ALTO + 4)) pintarIcono(w, e) w:setTooltip(tooltip(e, e.fin and math.max(0, e.fin - g_clock.millis()) or -1)) end local filas = math.ceil(#estados / porFila) fila:setWidth(math.max(1, math.min(#estados, porFila) * PASO - 4)) fila:setHeight(filas > 0 and (filas * (ALTO + 4) - 4) or 0) return filas end local function dibujar() if not panel then return end local buenos, malos = {}, {} for _, e in ipairs(lista) do table.insert(e.c == 'b' and buenos or malos, e) end -- primero los que tienen tiempo (de menos a más), después los permanentes; WoW los ordena parecido local function orden(a, b) local ta, tb = a.fin or math.huge, b.fin or math.huge if ta ~= tb then return ta < tb end return a.clave < b.clave end table.sort(buenos, orden) table.sort(malos, orden) local max = ancho() colocarFila(panel.beneficios, buenos, max) colocarFila(panel.perjuicios, malos, max) panel.perjuicios:setMarginTop(#buenos > 0 and 4 or 0) local alto = panel.beneficios:getHeight() + panel.perjuicios:getHeight() + (#buenos > 0 and #malos > 0 and 4 or 0) panel:setSize({ width = math.max(panel.beneficios:getWidth(), panel.perjuicios:getWidth()), height = math.max(1, alto) }) panel:setVisible(#lista > 0) ajustarGrupo(MARGEN + (#lista > 0 and alto or 0)) avanzar() end local function onOpcode(protocol, opcode, buffer) if buffer:sub(1, 2) ~= 'E|' then return end local ahora = g_clock.millis() lista = {} for _, item in ipairs(buffer:sub(3):split('~')) do local f = item:split('^') local ms = tonumber(f[3]) if f[1] and f[1] ~= '' and ms then local total = tonumber(f[4]) or -1 lista[#lista + 1] = { clave = f[1], c = f[2], fin = ms >= 0 and (ahora + ms) or nil, total = total > 0 and total or nil, extra = tonumber(f[5]) } end end dibujar() end function estadosController:onInit() ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) if g_game.isOnline() then -- recargado a mitad de partida self:onGameStart() end end function estadosController:onGameStart() if not panel then panel = g_ui.loadUI('estados', modules.game_interface.getRootPanel()) panel:addAnchor(AnchorTop, 'parent', AnchorTop) panel:addAnchor(AnchorLeft, 'parent', AnchorLeft) panel:setMarginTop(MARGEN) panel:setMarginLeft(MARGEN) panel:hide() connect(modules.game_interface.getRootPanel(), { onGeometryChange = dibujar }) end if not reloj then reloj = cycleEvent(avanzar, 100) end lista = {} dibujar() -- Al entrar se pide la lista: lo que el servidor manda en el primer instante del login puede perderse. scheduleEvent(function() local protocol = g_game.getProtocolGame() if protocol then protocol:sendExtendedOpcode(OPCODE, 'P') end end, 1000) end function estadosController:onGameEnd() lista = {} dibujar() if reloj then removeEvent(reloj) reloj = nil end end function estadosController:onTerminate() ProtocolGame.unregisterExtendedOpcode(OPCODE) if reloj then removeEvent(reloj) reloj = nil end if panel then disconnect(modules.game_interface.getRootPanel(), { onGeometryChange = dibujar }) panel:destroy() panel = nil end lista = {} end