-- OT-MMO: impacto del combate. Capas de respuesta breves que vuelven al reposo y no tapan el juego: -- · destello claro ~90 ms sobre la criatura que pierde vida (como mucho uno cada 160 ms por criatura) -- · golpe fuerte recibido (>= 12 % de tu vida de una vez): borde rojo que se apaga en ~0,4 s; -- con >= 20 %, además un temblor leve del mapa (180 ms). Con poca vida (< 25 %) queda un borde rojo quieto. -- · opcode extendido 110, lo manda el servidor: -- «K|idCriatura|valor» golpe crítico: rótulo grande donde cayó + destello más largo -- «T|fuerza» temblor pedido por el servidor (golpe de jefe), fuerza 1-3 -- Opciones (Opciones > Gráficos > Efectos): impactoTemblor e impactoDestellos (g_settings, por defecto sí). -- Los números de daño siguen siendo los del cliente (AnimatedText en C++, ya coloreados por tipo de daño): -- su fuente y tamaño no se pueden cambiar desde Lua. local OPCODE = 110 local SHADER = 'Impacto - Destello' local PASO = 30 local POCA_VIDA, GOLPE_FUERTE, GOLPE_TEMBLOR = 0.25, 0.12, 0.20 impactoController = Controller:new() local capa = nil local anim = {} -- [clave] = evento local ultimoDestello = {} -- [idCriatura] = ms local baseVineta = 0 -- opacidad en reposo (poca vida) local margenes = nil -- márgenes originales del mapa durante un temblor local criticos = 0 -- contador: cada rótulo anima por su cuenta local lento = 1 -- solo pruebas: probar('L|20') alarga todo para poder capturarlo local function mapa() return modules.game_interface.getMapPanel() end local function activo(clave) return g_settings.getBoolean(clave, true) end local function parar(clave) removeEvent(anim[clave]) anim[clave] = nil end -- Interpola de 0 a 1 en `ms` y llama a f(t) en cada paso. local function animar(clave, ms, f, alFinal) parar(clave) local inicio = g_clock.millis() local function paso() local t = math.min(1, (g_clock.millis() - inicio) / math.max(1, ms * lento)) f(t) if t < 1 then anim[clave] = scheduleEvent(paso, PASO) else anim[clave] = nil if alFinal then alFinal() end end end paso() end local function salida(t) -- ease-out quart: rápido al principio, se posa sin rebote return 1 - (1 - t) ^ 4 end -- Destello ---------------------------------------------------------------------------------------- local function destello(creature, ms) if not activo('impactoDestellos') or not creature or creature:isLocalPlayer() then return end local id, ahora = creature:getId(), g_clock.millis() if ultimoDestello[id] and ahora - ultimoDestello[id] < 160 then return end ultimoDestello[id] = ahora creature:setShader(SHADER) scheduleEvent(function() creature:setShader('') end, (ms or 90) * lento) end local function onSalud(creature, porcentaje, anterior) if anterior and anterior <= 100 and porcentaje < anterior then destello(creature) end end -- Borde rojo y temblor (solo el jugador local) -------------------------------------------------------- local function pulso(fuerza) if not capa then return end local v = capa.vineta local pico = math.min(0.75, baseVineta + 0.35 + 0.3 * fuerza) animar('vineta', 420, function(t) v:setOpacity(pico + (baseVineta - pico) * salida(t)) end) end local function restaurarMapa() local m = mapa() if m and margenes then m:setMarginLeft(margenes[1]); m:setMarginTop(margenes[2]) m:setMarginRight(margenes[3]); m:setMarginBottom(margenes[4]) end margenes = nil end local function temblor(px) local m = mapa() if not activo('impactoTemblor') or not m then return end parar('temblor') restaurarMapa() margenes = { m:getMarginLeft(), m:getMarginTop(), m:getMarginRight(), m:getMarginBottom() } local pasos = { { 1, -0.6 }, { -0.9, 0.7 }, { 0.6, 0.4 }, { -0.4, -0.5 }, { 0.2, 0.2 }, { 0, 0 } } animar('temblor', 180, function(t) local p = pasos[math.min(#pasos, 1 + math.floor(t * (#pasos - 1)))] local dx, dy = math.floor(p[1] * px + 0.5), math.floor(p[2] * px + 0.5) m:setMarginLeft(margenes[1] + dx); m:setMarginRight(margenes[3] - dx) m:setMarginTop(margenes[2] + dy); m:setMarginBottom(margenes[4] - dy) end, restaurarMapa) end local function onVida(player, vida, maxima, vidaAntes, maximaAntes) if not capa or not maxima or maxima <= 0 then return end local frac = vida / maxima baseVineta = (vida > 0 and frac < POCA_VIDA) and 0.34 or 0 if not anim.vineta then capa.vineta:setOpacity(baseVineta) end local perdida = (vidaAntes or vida) - vida if perdida <= 0 or maximaAntes ~= maxima then -- curas, login, subir de nivel: nada return end local golpe = perdida / maxima if golpe >= GOLPE_FUERTE then pulso(math.min(1, golpe / 0.4)) end if golpe >= GOLPE_TEMBLOR then temblor(golpe >= 0.35 and 6 or 4) end end -- Crítico ----------------------------------------------------------------------------------------- local function critico(creature, valor) local m = mapa() if not m or not creature then return end local pos = creature:getPosition() destello(creature, 160) local w = g_ui.createWidget('ImCritico', m) w.valor:setText(tostring(valor)) local function colocar(subida) local r = m:getTileRect(pos) if not r or r.width == 0 then return false end w:setPosition({ x = r.x + math.floor(r.width / 2) - math.floor(w:getWidth() / 2) - 8, y = r.y - 60 - math.floor(subida) }) -- encima del nombre y la barra de vida return true end if not colocar(0) then return w:destroy() end criticos = criticos + 1 local clave = 'critico' .. criticos animar(clave, 850, function(t) colocar(22 * salida(t)) w:setOpacity(t < 0.65 and 1 or (1 - (t - 0.65) / 0.35)) end, function() w:destroy() end) end local function onOpcode(protocol, opcode, buffer) local f = buffer:split('|') if f[1] == 'K' then critico(g_map.getCreatureById(tonumber(f[2]) or 0), tonumber(f[3]) or 0) elseif f[1] == 'T' then temblor(({ 3, 4, 6 })[math.max(1, math.min(3, tonumber(f[2]) or 1))]) end end -- Para probar sin servidor: probar('K||148'), probar('T|2') o probar('G|0.3') (golpe recibido del 30 %) function probar(texto) if texto:match('^L|') then lento = tonumber(texto:sub(3)) or 1 return end local golpe = texto:match('^G|([%d%.]+)') local j = g_game.getLocalPlayer() if golpe and j then local m = j:getMaxHealth() return onVida(j, math.floor(m * (1 - golpe)), m, m, m) end onOpcode(nil, OPCODE, texto) end function impactoController:onInit() g_ui.importStyle('impacto') g_shaders.createFragmentShader(SHADER, resolvepath('destello.frag'), false) g_shaders.setupOutfitShader(SHADER) ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) connect(Creature, { onHealthPercentChange = onSalud }) connect(LocalPlayer, { onHealthChange = onVida }) if g_game.isOnline() then self:onGameStart() end end function impactoController:onTerminate() ProtocolGame.unregisterExtendedOpcode(OPCODE) disconnect(Creature, { onHealthPercentChange = onSalud }) disconnect(LocalPlayer, { onHealthChange = onVida }) self:onGameEnd() if capa then capa:destroy() capa = nil end end function impactoController:onGameStart() if not capa then capa = g_ui.createWidget('ImCapa', mapa()) capa:fill('parent') end local j = g_game.getLocalPlayer() if j then onVida(j, j:getHealth(), j:getMaxHealth(), j:getHealth(), j:getMaxHealth()) end end function impactoController:onGameEnd() for clave in pairs(anim) do parar(clave) end restaurarMapa() ultimoDestello = {} baseVineta = 0 if capa then capa.vineta:setOpacity(0) end end