-- OT-MMO: avisos de jefe (Boss Warning System), estilo DBM. Solo pinta: el servidor -- (server/scripts/combate/avisos.lua) decide qué, a quién y cuándo, y resuelve el golpe con su reloj. -- Opcode extendido 106: -- «A|id|tipo|habilidad|ms[|icono]» aviso principal: banda grande, cuenta atrás y bordes de pantalla -- «T|id|habilidad|ms[|tipo[|icono]]» barra de «próximas habilidades», con el color de su tipo -- «S|id|texto|ms» alerta secundaria (quien no es el afectado) -- «Z|id|tipo|ms|z|x,y;x,y…» telegrafiado en el suelo: casillas que se llenan hasta el impacto -- «Z|id|tipo|ms|@idCriatura|radio» telegrafiado que sigue a una criatura -- «C|id» quitar (cancelado o resuelto) -- Llegan milisegundos RESTANTES: el fin se calcula con el reloj local al recibir. local OPCODE = 106 local PASO = 40 local GRACIA = 1500 -- ms que el aviso sigue en pantalla tras llegar a 0 si el servidor no lo quitó local DESTELLO = 220 -- ms del destello blanco del suelo al impactar -- Por tipo: título, color (tokens del sistema; INTERRUMPE conserva el violeta de interrupción, sin token), símbolo. local TIPOS = { ESCONDETE = { '¡ESCÓNDETE!', OtmmoTheme.color.accent, '!' }, DEFIENDETE = { '¡DEFIÉNDETE!', OtmmoTheme.color.warn, '§' }, INTERRUMPE = { '¡INTERRUMPE!', '#b56ae0', '×' }, MUEVETE = { '¡SAL DEL ÁREA!', OtmmoTheme.color.hp, '»' }, SEPARENSE = { '¡ALÉJATE DE TUS COMPAÑEROS!', OtmmoTheme.color.mana, '«' }, AGRUPENSE = { '¡AGRÚPENSE!', OtmmoTheme.color.ok, '+' }, -- Marismas de Cenagal (server/scripts/abadia/abadia.lua, monstruos/cenagal.lua): el servidor los usa cuando -- Avisos.TIPOS los conoce; hasta entonces manda MUEVETE / AGRUPENSE / SEPARENSE LUZ = { '¡LUZ!', OtmmoTheme.color.warn, '*' }, A_LA_LUZ = { '¡A LA LUZ!', OtmmoTheme.color.warn, '*' }, ECO = { 'ECO', '#9a9a9a', '~' }, LIBERALO = { '¡LIBÉRALO!', OtmmoTheme.color.hp, '!' }, } -- Dibujo de cada habilidad (arte/iconos-jefe/build.py): posición en la tira jefe-32 / jefe-64. local ICONOS = { grito = 0, mazazo = 1, maldicion = 2, golpe = 3, llamado = 4, marca = 5, estallido = 6 } -- Color del suelo por tipo de zona (PELIGRO y SEGURO son solo de suelo). -- Tokens (hp, ok, warn, mana) sin «#»; INTERRUMPE conserva su violeta (no hay token). local SUELO = { PELIGRO = 'C94242', SEGURO = '52A674', DEFIENDETE = 'D99A45', INTERRUMPE = 'a855e0', MUEVETE = 'C94242', SEPARENSE = '347CCD', AGRUPENSE = '52A674', LUZ = 'E8C35A', A_LA_LUZ = 'E8C35A', ECO = '8C8C8C' } avisosController = Controller:new() local panel = nil local marco = nil -- bordes de pantalla del aviso principal local proximas = nil -- barras de próximas habilidades (esquina del mapa) local evento = nil local principal = nil -- { id, tipo, habilidad, fin, total } local secundario = nil -- { id, texto, fin } local barras = {} -- [id] = { widget, fin, total, color } local zonas = {} -- [id] = { tipo, fin, total, casillas = { {x,y,z} }, sigue, radio, widgets = {}, destello } local function hex2(v) return string.format('%02x', math.max(0, math.min(255, math.floor(v)))) end local function segundos(ms) -- decimal con coma, como el resto del juego («2,4») return (string.format('%.1f', math.max(0, ms) / 1000):gsub('%.', ',')) end local function mapa() return modules.game_interface.getMapPanel() end local function verPrincipal(si) panel.principal:setVisible(si) panel.principal:setHeight(si and 56 or 0) if marco then marco:setVisible(si) end end local function ordenarBarras() local lista = {} for _, b in pairs(barras) do lista[#lista + 1] = b end table.sort(lista, function(a, b) return a.fin < b.fin end) for i, b in ipairs(lista) do proximas:moveChildToIndex(b.widget, i) end end -- Suelo ------------------------------------------------------------------------------------------- local function borrarZona(id) local z = zonas[id] if not z then return end for _, w in ipairs(z.widgets) do w:destroy() end zonas[id] = nil end -- Al impactar: destello blanco breve y fuera. local function impacto(id) local z = zonas[id] if z and not z.destello then z.destello = g_clock.millis() end end local function casillasDe(z) if not z.sigue then return z.casillas end local c = g_map.getCreatureById(z.sigue) if not c then return {} end local p, lista = c:getPosition(), {} for dy = -z.radio, z.radio do for dx = -z.radio, z.radio do lista[#lista + 1] = { x = p.x + dx, y = p.y + dy, z = p.z } end end return lista end local function pintarZona(id, z, t) local m = mapa() local color = SUELO[z.tipo] or SUELO.PELIGRO local casillas = casillasDe(z) -- un widget por casilla, reutilizados entre pasos while #z.widgets < #casillas do local w = g_ui.createWidget('UIWidget', m) w:setPhantom(true) local fill = g_ui.createWidget('UIWidget', w) fill:setPhantom(true) w.fill = fill z.widgets[#z.widgets + 1] = w end local avance = math.max(0, math.min(1, 1 - (z.fin - t) / z.total)) -- 0 al anunciar, 1 al impactar local fondo, borde, relleno if z.destello then local f = 1 - math.min(1, (t - z.destello) / DESTELLO) fondo = '#ffffff' .. hex2(120 * f) borde, relleno = fondo, '#ffffff00' else -- late suave y se intensifica al acercarse el golpe local latido = 0.5 + 0.5 * math.sin(t / 110) fondo = '#' .. color .. hex2(z.suave and (10 + 18 * latido) or (24 + 46 * avance + 14 * latido)) borde = '#' .. color .. hex2(z.suave and (80 + 80 * avance) or (130 + 110 * avance)) relleno = '#' .. color .. hex2(z.suave and 0 or (45 + 55 * avance)) end for i, w in ipairs(z.widgets) do local c = casillas[i] local r = c and m:getTileRect(c) if r and r.width > 0 then w:setVisible(true) w:setRect(r) w:setBackgroundColor(fondo) w:setBorderWidth(1) w:setBorderColor(borde) -- el relleno crece desde el centro: cuando llena la casilla, llega el golpe local lado = math.floor(r.width * avance) w.fill:setRect({ x = r.x + math.floor((r.width - lado) / 2), y = r.y + math.floor((r.height - lado) / 2), width = lado, height = lado }) w.fill:setBackgroundColor(relleno) else w:setVisible(false) end end end -- Pantalla ---------------------------------------------------------------------------------------- local function pintar() if not panel then return end local t = g_clock.millis() if principal then local resta = principal.fin - t if resta < -GRACIA then principal = nil verPrincipal(false) else local color = principal.color local p = panel.principal p.cuenta:setText(resta > 0 and segundos(resta) or '¡YA!') local frac = math.max(0, math.min(1, resta / principal.total)) p.barra.fill:setWidth(math.max(1, math.floor(p.barra:getWidth() * frac))) -- latido: más rápido y fuerte en el último segundo y medio local rapido = resta < 1500 local latido = 0.5 + 0.5 * math.sin(t / (rapido and 70 or 160)) p:setBorderColor(color .. hex2(140 + 115 * latido)) p.icono:setBackgroundColor(color .. hex2(rapido and (90 + 120 * latido) or 90)) p.icono.dibujo:setOpacity(rapido and (0.75 + 0.25 * latido) or 1) p.cuenta:setColor(rapido and color or OtmmoTheme.color.text) if marco then marco:setBorderColor(color .. hex2((rapido and 70 or 25) + (rapido and 90 or 35) * latido)) end end end if secundario then local resta = secundario.fin - t if resta < -GRACIA then secundario = nil panel.secundario:hide() else panel.secundario:setText(secundario.texto .. ' · ' .. segundos(resta) .. ' s') end end for id, b in pairs(barras) do local resta = b.fin - t if resta < -GRACIA then b.widget:destroy() barras[id] = nil else local w = b.widget w.tiempo:setText(segundos(resta) .. ' s') local frac = math.max(0, math.min(1, resta / b.total)) w.fill:setWidth(math.max(1, math.floor((w:getWidth() - 6) * frac))) local cerca = resta < 5000 -- las inminentes se encienden w.fill:setBackgroundColor(b.color .. (cerca and '70' or '38')) w.franja:setBackgroundColor(b.color .. (cerca and hex2(170 + 85 * (0.5 + 0.5 * math.sin(t / 120))) or 'a0')) w.nombre:setColor(cerca and OtmmoTheme.color.text or OtmmoTheme.color.text2) end end for id, z in pairs(zonas) do if z.destello and t - z.destello > DESTELLO then borrarZona(id) elseif not z.destello and z.fin - t < -GRACIA then borrarZona(id) else if not z.destello and t >= z.fin then impacto(id) end pintarZona(id, z, t) end end if not principal and not secundario and next(barras) == nil and next(zonas) == nil then removeEvent(evento) evento = nil end end local function arrancar() if not evento then evento = cycleEvent(pintar, PASO) end pintar() end local function onOpcode(protocol, opcode, buffer) if not panel then return end local p = buffer:split('|') local id = tonumber(p[2]) if not id then return end local ahora = g_clock.millis() if p[1] == 'A' then local tipo = TIPOS[p[3]] or { p[3], OtmmoTheme.color.text, '!' } local ms = tonumber(p[5]) or 0 principal = { id = id, color = tipo[2], fin = ahora + ms, total = math.max(1, ms) } local w = panel.principal w.titulo:setText(tipo[1]) w.titulo:setColor(tipo[2]) w.habilidad:setText(p[4] or '') local ic = ICONOS[p[6] or ''] w.icono.dibujo:setVisible(ic ~= nil) w.icono.simbolo:setVisible(ic == nil) if ic then w.icono.dibujo:setImageClip({ x = ic * 64, y = 0, width = 64, height = 64 }) end w.icono.simbolo:setText(tipo[3]) w.icono:setBorderColor(tipo[2]) w.barra.fill:setBackgroundColor(tipo[2]) verPrincipal(true) arrancar() elseif p[1] == 'S' then secundario = { id = id, texto = p[3] or '', fin = ahora + (tonumber(p[4]) or 0) } panel.secundario:show() arrancar() elseif p[1] == 'T' then if barras[id] then barras[id].widget:destroy() end local ms = tonumber(p[4]) or 0 local tipo = TIPOS[p[5] or ''] or { '', OtmmoTheme.color.accent, '·' } local w = g_ui.createWidget('BarraAviso', proximas) w.nombre:setText(p[3] or '') local ic = ICONOS[p[6] or ''] if ic then w.dibujo:setImageClip({ x = ic * 32, y = 0, width = 32, height = 32 }) w.icono:setText('') else w.dibujo:hide() w.icono:setText(tipo[3]) w.icono:setColor(tipo[2]) end barras[id] = { widget = w, fin = ahora + ms, total = math.max(1, ms), color = tipo[2] } ordenarBarras() arrancar() elseif p[1] == 'Z' then borrarZona(id) local ms = tonumber(p[4]) or 0 local z = { tipo = p[3], fin = ahora + ms, total = math.max(1, ms), widgets = {} } if (p[5] or ''):sub(1, 1) == '@' then z.sigue, z.radio = tonumber(p[5]:sub(2)), tonumber(p[6]) or 1 else local nivel = tonumber(p[5]) or 7 z.casillas = {} for _, par in ipairs((p[6] or ''):split(';')) do local x, y = par:match('(%d+),(%d+)') if x then z.casillas[#z.casillas + 1] = { x = tonumber(x), y = tonumber(y), z = nivel } end end end z.suave = z.casillas ~= nil and #z.casillas > 60 -- áreas enormes: tenues, que no tapen el mapa zonas[id] = z arrancar() elseif p[1] == 'C' then if principal and principal.id == id then principal = nil verPrincipal(false) end if secundario and secundario.id == id then secundario = nil panel.secundario:hide() end if barras[id] then barras[id].widget:destroy() barras[id] = nil end if zonas[id] then impacto(id) -- resuelto o cancelado: destello y fuera arrancar() end end end local function limpiar() principal, secundario = nil, nil for id, b in pairs(barras) do b.widget:destroy() barras[id] = nil end for id in pairs(zonas) do borrarZona(id) end if panel then verPrincipal(false) panel.secundario:hide() end removeEvent(evento) evento = nil end function avisosController:onInit() ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) end function avisosController:onTerminate() ProtocolGame.unregisterExtendedOpcode(OPCODE) limpiar() if panel then panel:destroy() panel = nil end if marco then marco:destroy() marco = nil end if proximas then proximas:destroy() proximas = nil end end function avisosController:onGameStart() if not panel then -- debajo del seguimiento del contrato (game_tablon: margen 40, alto 40) panel = g_ui.loadUI('avisos', modules.game_interface.getRootPanel()) panel:addAnchor(AnchorTop, 'parent', AnchorTop) panel:addAnchor(AnchorHorizontalCenter, 'parent', AnchorHorizontalCenter) panel:setMarginTop(48) -- bordes de la pantalla: un marco del tamaño del mapa que late con el color del aviso marco = g_ui.createWidget('UIWidget', mapa()) marco:fill('parent') marco:setPhantom(true) marco:setBorderWidth(4) marco:setBackgroundColor('#00000000') -- compactas debajo del aviso (a la derecha el mapa sigue por debajo del panel lateral y se taparían) proximas = g_ui.createWidget('ProximasAvisos', panel) proximas:addAnchor(AnchorTop, 'secundario', AnchorBottom) proximas:addAnchor(AnchorHorizontalCenter, 'parent', AnchorHorizontalCenter) proximas:setMarginTop(4) verPrincipal(false) end end function avisosController:onGameEnd() limpiar() end