-- OT-MMO: bloque central de vitales (etapa 3 del rediseño): vida, clase, nivel y maná encima de la -- barra de habilidades. Se actualiza por eventos del jugador, no por ciclo (en WebAssembly no hay LuaJIT). -- El servidor envía el «clientid» de server/xml/vocations.xml, no el id interno: -- Guardian (id 4) -> 1, Cazador (id 3) -> 2, Hechicero (id 1) -> 3, Druida (id 2) -> 4. local CLASES = { [0] = 'Sin clase', [1] = 'Guardián', [2] = 'Cazador', [3] = 'Hechicero', [4] = 'Druida', [6] = 'Brujo', [7] = 'Bardo', [8] = 'Gunslinger' } vitalsController = Controller:new() vitalsController:setUI('vitals', modules.game_interface.getRootPanel()) local function setBar(bar, value, max) local frac = (max and max > 0) and math.max(0, math.min(1, value / max)) or 0 bar.fill:setWidth(math.max(0, math.floor((bar:getWidth() - 2) * frac))) bar.text:setText(string.format('%d / %d', value, max or 0)) end local function refresh() local player = g_game.getLocalPlayer() local ui = vitalsController.ui if not player or not ui then return end setBar(ui.health, player:getHealth(), player:getMaxHealth()) setBar(ui.mana, player:getMana(), player:getMaxMana()) ui.nivel:setText('Nv ' .. player:getLevel()) ui.nivel:setTooltip(CLASES[player:getVocation()] or 'Aventurero') end -- Experiencia (curva de Tibia; tope de nivel 100: server/canary/data/events/scripts/player.lua). local NIVEL_MAX = 100 local function expNivel(n) return math.floor((50 * (n - 1) ^ 3 - 150 * (n - 1) ^ 2 + 400 * (n - 1)) / 3) end local function miles(n) local s = tostring(math.floor(n)) while true do local r, k = s:gsub('^(-?%d+)(%d%d%d)', '%1.%2') s = r if k == 0 then break end end return s end local function refreshExp() local player = g_game.getLocalPlayer() local bar = vitalsController.ui and vitalsController.ui.exp if not player or not bar then return end local nivel, exp = player:getLevel(), player:getExperience() if nivel >= NIVEL_MAX then bar.fill:setWidth(bar:getWidth() - 2) bar.text:setText('Nivel máximo') bar:setTooltip(string.format('Experiencia total: %s', miles(exp))) return end local desde, hasta = expNivel(nivel), expNivel(nivel + 1) local frac = math.max(0, math.min(1, (exp - desde) / math.max(1, hasta - desde))) bar.fill:setWidth(math.max(0, math.floor((bar:getWidth() - 2) * frac))) bar.text:setText(string.format('%s / %s (%d %%)', miles(exp - desde), miles(hasta - desde), math.floor(frac * 100))) bar:setTooltip(string.format('Faltan %s para el nivel %d\nExperiencia total: %s', miles(hasta - exp), nivel + 1, miles(exp))) end -- Colores y cifra de las tres barras (GameProgressBar, data/styles/50-otmmo.otui): tokens de OtmmoTheme. local function pintarBarras(ui) ui.health.fill:setBackgroundColor(OtmmoTheme.color.hp) ui.mana.fill:setBackgroundColor(OtmmoTheme.color.mana) ui.exp.fill:setBackgroundColor(OtmmoTheme.color.xp) -- la de experiencia es fina: cifra de 11 (el mínimo de la guía) con el mismo contorno bg pcall(function() ui.exp.text:setTTFFont(OtmmoTheme.font.num, OtmmoTheme.size.xs, 1, OtmmoTheme.color.bg) end) end -- Barra de lanzamiento sobre la cabeza: el servidor (server/scripts/lanzamiento.lua) avisa por el opcode 101 -- a todos los que ven al lanzador: «C|idCriatura|nombre|ms» al empezar y «X|idCriatura|ok» o «|x» al terminar. -- La barra va en el panel del mapa y se recoloca en cada paso sobre la casilla del lanzador (moverse interrumpe -- la carga, así que no hace falta seguir el paso). attachWidget daba coordenadas inválidas en la versión web. local OPCODE_LANZAMIENTO = 101 local NOMBRES = { Regeneracion = 'Regeneración' } -- el servidor envía los nombres sin acentos -- Estilo WoW con la paleta del juego: nombre a la izquierda y tiempo a la derecha DENTRO de la barra, que crece con -- el nombre (con 100 px fijos «Piedra de Hogar» salía cortado). Dorado al cargar, destello al terminar, rojo si se corta. local ANCHO_MIN, ALTO = 112, 16 local ORO, ORO_CLARO, ROJO = OtmmoTheme.color.accent, OtmmoTheme.color.text, OtmmoTheme.color.hp -- tokens local cargas = {} -- [idCriatura] = { barra, inicio, ms, ancho, evento } local function texto(padre, id, alinear) local l = g_ui.createWidget('Label', padre) l:setId(id) l:addAnchor(AnchorTop, 'parent', AnchorTop) l:addAnchor(AnchorBottom, 'parent', AnchorBottom) l:setTextAlign(alinear) l:setFont('verdana-11px-rounded') l:setColor(OtmmoTheme.color.text) l:setPhantom(true) return l end local function crearBarra(nombreTexto) local barra = g_ui.createWidget('UIWidget', modules.game_interface.getMapPanel()) barra:setPhantom(true) barra:setBackgroundColor(OtmmoTheme.color.bg .. 'D9') barra:setBorderWidth(1) barra:setBorderColor(OtmmoTheme.color.border) local fill = g_ui.createWidget('UIWidget', barra) fill:addAnchor(AnchorTop, 'parent', AnchorTop) fill:addAnchor(AnchorBottom, 'parent', AnchorBottom) fill:addAnchor(AnchorLeft, 'parent', AnchorLeft) fill:setMargin(1) fill:setWidth(1) fill:setBackgroundColor(ORO) fill:setPhantom(true) -- punta clara del relleno: se ve avanzar aunque la barra sea corta local punta = g_ui.createWidget('UIWidget', barra) punta:addAnchor(AnchorTop, 'parent', AnchorTop) punta:addAnchor(AnchorBottom, 'parent', AnchorBottom) punta:setMarginTop(1) punta:setMarginBottom(1) punta:setWidth(2) punta:addAnchor(AnchorLeft, 'parent', AnchorLeft) punta:setBackgroundColor(ORO_CLARO) punta:setPhantom(true) local nombre = texto(barra, 'nombre', AlignLeft) nombre:addAnchor(AnchorLeft, 'parent', AnchorLeft) nombre:setMarginLeft(5) nombre:setText(nombreTexto) local anchoNombre = nombre:getTextSize().width nombre:setWidth(anchoNombre + 2) local tiempo = texto(barra, 'tiempo', AlignRight) tiempo:addAnchor(AnchorRight, 'parent', AnchorRight) tiempo:setMarginRight(5) tiempo:setWidth(34) barra.fill, barra.punta, barra.nombre, barra.tiempo = fill, punta, nombre, tiempo local ancho = math.max(ANCHO_MIN, anchoNombre + 34 + 18) barra:setSize({ width = ancho, height = ALTO }) return barra, ancho end local function pintar(c, frac) local w = math.max(1, math.floor((c.ancho - 2) * frac)) c.barra.fill:setWidth(w) c.barra.punta:setMarginLeft(math.max(1, w - 1)) end local function cerrarCarga(id, ok) local c = cargas[id] if not c then return end cargas[id] = nil removeEvent(c.evento) c.barra.tiempo:setText('') if ok then -- destello: llena y aclara un instante antes de irse pintar(c, 1) c.barra.fill:setBackgroundColor(ORO_CLARO) c.barra.punta:hide() scheduleEvent(function() c.barra:destroy() end, 140) return end c.barra.nombre:setText('Interrumpido') -- la barra se calculó para el nombre de la habilidad («Montar» es más corto): que quepa el aviso local necesita = c.barra.nombre:getTextSize().width + 34 + 18 if necesita > c.ancho then c.barra:setX(c.barra:getX() - math.floor((necesita - c.ancho) / 2)) c.ancho = necesita c.barra:setWidth(necesita) end c.barra.nombre:setWidth(c.barra.nombre:getTextSize().width + 2) pintar(c, 1) c.barra.punta:hide() c.barra.fill:setBackgroundColor(ROJO) scheduleEvent(function() c.barra:destroy() end, 650) end local function onLanzamiento(protocol, opcode, buffer) local partes = buffer:split('|') local id = tonumber(partes[2]) if not id then return end if partes[1] == 'C' then cerrarCarga(id, true) local criatura = g_map.getCreatureById(id) if not criatura then return end local barra, ancho = crearBarra(NOMBRES[partes[3]] or partes[3]) local c = { barra = barra, ancho = ancho, inicio = g_clock.millis(), ms = tonumber(partes[4]) or 1000 } local mapa = modules.game_interface.getMapPanel() local function paso() local r = mapa:getTileRect(criatura:getPosition()) if r then -- el personaje se dibuja desplazado 1/4 de casilla arriba a la izquierda; el nombre va encima local d = r.width / 4 c.barra:setPosition({ x = r.x + r.width / 2 - d - c.ancho / 2, y = r.y - d - ALTO - 16 }) end local pasado = g_clock.millis() - c.inicio pintar(c, math.min(1, pasado / c.ms)) c.barra.tiempo:setText((string.format('%.1f s', math.max(0, (c.ms - pasado) / 1000)):gsub('%.', ','))) end paso() c.evento = cycleEvent(paso, 30) cargas[id] = c elseif partes[1] == 'X' then cerrarCarga(id, partes[3] == 'ok') end end -- Para probar sin servidor: modules.game_vitals.probarCarga('Piedra de Hogar', 4000[, true = interrumpir a la mitad]) function probarCarga(nombre, ms, cortar) local yo = g_game.getLocalPlayer() if not yo then return end local id = yo:getId() onLanzamiento(nil, OPCODE_LANZAMIENTO, ('C|%d|%s|%d'):format(id, nombre, ms)) scheduleEvent(function() onLanzamiento(nil, OPCODE_LANZAMIENTO, ('X|%d|%s'):format(id, cortar and 'x' or 'ok')) end, cortar and ms / 2 or ms) end -- Reserva de Ceniza del Brujo: «R|actual|máximo» (opcode 120). Solo la recibe el Brujo; el resto no ve el hilo. local function onCeniza(protocol, opcode, buffer) local cur, max = buffer:match('^R|(%d+)|(%d+)$') local ui = vitalsController.ui if not cur or not ui or not ui.ceniza then return end cur, max = tonumber(cur), math.max(1, tonumber(max)) ui.ceniza:show() ui.ceniza.fill:setWidth(math.floor((ui.ceniza:getWidth() - 2) * math.min(1, cur / max))) ui.ceniza:setTooltip(('Reserva de Ceniza: %d / %d'):format(cur, max)) end function vitalsController:onInit() ProtocolGame.registerExtendedOpcode(OPCODE_LANZAMIENTO, onLanzamiento) ProtocolGame.registerExtendedOpcode(120, onCeniza) end function vitalsController:onTerminate() ProtocolGame.unregisterExtendedOpcode(OPCODE_LANZAMIENTO) ProtocolGame.unregisterExtendedOpcode(120) for id in pairs(cargas) do cerrarCarga(id, true) end end function vitalsController:onGameStart() local ui = self.ui ui:breakAnchors() -- Las habilidades flotantes ya no viven en gameBottomActionPanel. -- Mantener vitales independientes evita que el panel vacio los oculte. ui:setWidth(470) ui:addAnchor(AnchorBottom, 'parent', AnchorBottom) ui:addAnchor(AnchorHorizontalCenter, 'parent', AnchorHorizontalCenter) ui:setMarginBottom(174) ui.nivel:setPhantom(false) ui.nivel:setTooltip('Arrastra el bloque para moverlo') -- Todo el bloque (insignia, experiencia, vida y maná) agarra y arrastra, no solo la insignia «Nv»: antes nadie -- encontraba el agarre. La posición se recuerda entre sesiones (OtmmoVentanas, clave "vitales"). for _, id in ipairs({ 'nivel', 'exp', 'health', 'mana' }) do OtmmoVentanas.moverCon(ui, ui[id], 'vitales') end ui:show() ui:raise() OtmmoVentanas.aplicar(ui) -- posición guardada (si la movió); al reconectar el anclaje por defecto la pisaba pintarBarras(ui) self:registerEvents(LocalPlayer, { onHealthChange = refresh, onManaChange = refresh, onLevelChange = function() refresh() refreshExp() end, onVocationChange = refresh, onExperienceChange = refreshExp }):execute() -- El ancho de las barras se conoce tras el primer acomodo de la interfaz. ui.onGeometryChange = function() refresh() refreshExp() end scheduleEvent(function() refresh() refreshExp() end, 100) -- La vida y el maná ya están aquí: se oculta la copia del panel derecho. scheduleEvent(function() local hp = modules.game_healthinfo and modules.game_healthinfo.healthManaController if hp and hp.ui then hp.ui:hide() end end, 200) end