Saltar al contenido principal

Scripting Lua: Hooks y Ciclo de Vida

webapp_banner.jpg

Esta página cubre cada hook que un poder Lua puede definir, el orden en que se ejecutan los hooks, cómo cada jefe obtiene su propio entorno de ejecución (runtime) aislado, y qué funciones de la biblioteca estándar están disponibles dentro del sandbox.

Si aún no has escrito un poder Lua, comienza primero con Primeros pasos.

Hooks de poderes de jefe

Esta página documenta los hooks para los poderes Lua de jefe en plugins/EliteMobs/powers/. Los scripts Lua de NPC usan su propia carpeta plugins/EliteMobs/npc_scripts/ y hooks específicos de NPC como on_npc_interact y on_npc_proximity_enter; consulta Scripts de NPC.


Referencia de hooks​

Cada archivo de poder Lua devuelve una tabla. Cada clave de esa tabla (aparte de api_version y priority) debe ser uno de los hooks listados a continuación. El runtime llama a la función correspondiente cada vez que se activa el evento de juego asociado.

HookSe activa cuando¿context.player disponible?
on_spawnLa elite mob apareceNo
on_game_tickUna vez por tick mientras el reloj del runtime está activo y sin pausar; normalmente cada 50 ms a 20 TPSNo
on_boss_damagedEl jefe recibe daño de cualquier fuenteNo
on_boss_damaged_by_playerEl jefe recibe daño de un jugadorSí
on_reinforcement_damaged_by_playerUn refuerzo invocado por este jefe recibe daño de un jugador; se ejecuta en los poderes del invocadorSí
on_boss_damaged_by_eliteEl jefe recibe daño de otra elite mobNo
on_player_damaged_by_bossUn jugador recibe daño de este jefeSí
on_enter_combatEl jefe entra en combateSí
on_exit_combatEl jefe sale del combateNo
on_healEl jefe se curaNo
on_boss_target_changedEliteMobs notifica un evento de objetivo jugador, incluida la adquisición inicial; no cubre cada cambio o pérdida de objetivoSí
on_deathEl jefe muereNo
on_phase_switchUn jefe por fases cambia a una nueva faseNo
on_mind_actionUn Mind nativo solicita una acción a sus poderes; consulta el contexto de acciónNo
on_zone_enterUna entidad entra en una zona vigiladaSí (si la entidad es un jugador)
on_zone_leaveUna entidad sale de una zona vigiladaSí (si la entidad es un jugador)

Cuando context.player aparece como "No", acceder a él devuelve nil. Comprueba siempre que no sea nil antes de usarlo.

on_boss_damaged y los hooks específicos de daño son rutas de eventos separadas. Un golpe puede activar ambas; no son mutuamente excluyentes. El hook de objetivo no permite cancelar el evento mediante esta tabla Lua ni informa del objetivo anterior.

Las zonas Lua no activan on_zone_enter / on_zone_leave

El cargador acepta y valida estos dos hooks, pero nada dentro de un poder Lua puede hacer que se disparen. Solo los despacha el escáner de zona de EliteScript, que se inicia cuando un eliteScript: en YAML del mismo jefe lista ZoneEnterEvent o ZoneLeaveEvent en sus Events:. Un jefe que solo tenga poderes Lua cargará sin quejarse un archivo que contenga estos hooks y después no los llamará nunca.

Los vigilantes (watchers) de Lua son un mecanismo aparte: context.zones:watch_zone(...) y context.script:zone(...):watch(...) invocan directamente los callbacks on_enter / on_leave que les pasas, y nunca pasan por estos hooks de nivel superior. Usa los vigilantes salvo que estés emparejando deliberadamente un poder Lua con un script de zona en YAML.

Los eventos de zona YAML llegan a todos los poderes correspondientes del jefe. Incluyen la entidad que entra o sale, pero no identifican la zona ni el script de origen. Varios escuchadores YAML pueden entregar varios eventos para la misma entidad.

(Los scripts Lua de NPC son distintos: su context.zones es el de MagmaCore, cuyo watch sí despacha on_zone_enter / on_zone_leave. Consulta Scripts de NPC.)

Poder típico con múltiples hooks​

Un solo poder Lua puede definir tantos hooks como necesite. A continuación se muestra un esqueleto que usa tres hooks juntos:

return {
api_version = 1,

on_enter_combat = function(context)
-- Initialize per-fight state when combat begins
context.state.hit_count = 0
context.log:info("Combat started!")
end,

on_boss_damaged_by_player = function(context)
-- Track hits and trigger an ability every 5th hit
context.state.hit_count = (context.state.hit_count or 0) + 1
if context.state.hit_count % 5 ~= 0 then
return
end
if not context.cooldowns:check_local("counter_attack", 100) then
return
end
-- Fire a projectile back at the player
local origin = context.boss:get_location()
origin:add(0, 1, 0)
context.boss:summon_projectile(
"SMALL_FIREBALL", origin, context.player:get_location(), 1.5
)
end,

on_death = function(context)
-- Spawn an explosion particle on death
context.world:spawn_particle_at_location(
context.boss:get_location(), "EXPLOSION_EMITTER", 1
)
end
}

Datos del evento (context.event)​

Algunos hooks reciben una tabla context.event que expone datos sobre el evento de juego que activó el hook. Los campos disponibles dependen de qué hook se está ejecutando.

Hooks de daño​

Se aplica a on_boss_damaged, on_boss_damaged_by_player, on_reinforcement_damaged_by_player, on_boss_damaged_by_elite y on_player_damaged_by_boss.

Campo / MétodoTipoDescripción
event.damage_amountdoubleInstantánea del daño al crear esta tabla de evento
event.get_damage_amount()doubleLee el daño actual después de modificaciones anteriores
event.damage_causestringNombre de DamageCause de Spigot (p. ej. "ENTITY_ATTACK", "PROJECTILE")
event.damagerentity tableAtacante Bukkit original si el evento subyacente es daño por entidad, también en on_boss_damaged. En un impacto de proyectil es el proyectil, no quien lo disparó.
event.projectileentity tableLa entidad proyectil, si el atacante fue un proyectil.
event.set_damage_amount(n)—Sobrescribe el daño con un valor fijo
event.multiply_damage_amount(n)—Multiplica el daño actual por n
event.cancel_event()—Cancela por completo el evento de daño
on_boss_damaged_by_player = function(context)
-- Halve all projectile damage
if context.event.damage_cause == "PROJECTILE" then
context.event.multiply_damage_amount(0.5)
end
end

Llama a event.set_damage_amount(n) y event.multiply_damage_amount(n) con punto. Estos setters leen directamente el primer argumento y no descartan el self de una llamada con dos puntos. event:transfer_damage(target, amount) sí admite dos puntos.

En los hooks de daño de jugador a élite, event.entity identifica a la élite dañada. En on_reinforcement_damaged_by_player, context.boss sigue siendo el invocador y context.player es el jugador atacante. El hook recibe el evento de daño original y modificable del refuerzo.

Estos hooks también exponen event.is_damage_transfer y event:transfer_damage(target, amount). El objetivo debe ser otra élite viva a la que el jefe propietario tenga permitido afectar. La cantidad debe ser finita y no negativa. Un evento cancelado, una cantidad cero o una transferencia anidada devuelve false. Una llamada válida envía un golpe independiente atribuido al jugador y devuelve true; no resta daño al golpe original ni garantiza que otro escuchador permita el nuevo golpe. Usa event.set_damage_amount(...) si quieres repartir el daño en vez de duplicarlo. Las transferencias no pueden transferirse recursivamente.

Hook de aparición​

Se aplica a on_spawn.

Campo / MétodoTipoDescripción
event.spawn_reasonstringNombre de SpawnReason de Spigot
event.cancel_event()—Cancela la aparición

Hook de muerte​

Se aplica a on_death.

Campo / MétodoTipoDescripción
event.entityentity tableLa entidad que está muriendo

Hooks de zona​

Se aplica a on_zone_enter y on_zone_leave.

Campo / MétodoTipoDescripción
event.entityentity tableLa entidad que entra o sale de la zona

Los vigilantes de zona creados en Lua no rellenan context.event; pasan la entidad que entra/sale directamente a su callback.

Eventos cancelables (general)​

Cualquier hook cuyo evento subyacente sea cancelable expone event.cancel_event(). context.event es nil en on_game_tick, on_mind_action y los callbacks ordinarios del programador. Hooks como on_heal tienen un evento subyacente, pero esta tabla no ofrece campos específicos de curación. Comprueba los miembros documentados antes de llamarlos.

La tabla de evento de jefe no ofrece cancel(), uncancel() ni is_cancelled del envoltorio compartido. Cancelar un evento detiene su envío a los poderes posteriores del mismo jefe, pero no sale de tu función Lua actual. Añade return si el resto de la función no debe ejecutarse.

Para conocer todos los campos de la tabla de entidad, consulta Jefe y Entidades. Para los valores de causa de daño y razón de aparición, consulta Enums y Valores.


Orden de ejecución de los hooks​

El despachador de eventos del jefe recorre los poderes de script adjuntos en orden de ejecución. El campo priority de un poder Lua determina su posición:

  • Los valores más bajos se ejecutan primero (el predeterminado es 0).
  • Las prioridades iguales conservan el orden de inserción de los poderes del jefe. No dependas de un orden concreto entre distintas rutas de carga.
return {
api_version = 1,
priority = -10, -- runs before most other powers

on_boss_damaged_by_player = function(context)
-- Record this power's last attacker before its later hooks run
context.state.last_attacker = context.player.uuid
end
}

Los poderes YAML EliteScript participan en el mismo despachador con prioridad 0. Una prioridad Lua negativa se ejecuta antes; una positiva, después. Esto ordena un evento de un jefe, no eventos Bukkit independientes ni tareas programadas. Un evento ya cancelado detiene el despachador, por lo que no todos los poderes tienen por qué recibirlo. Tras ejecutar los poderes propios de un refuerzo, su invocador directo recibe el hook de daño al refuerzo solo si el evento sigue sin cancelar.


Modelo de runtime​

Un runtime por poder y jefe​

Cada poder Lua adjunto tiene su propio runtime para cada jefe. Al iniciarlo, EliteMobs evalúa el código cargado en un sandbox nuevo y guarda la tabla devuelta. Al desaparecer o eliminarse el jefe, el runtime se cierra. Un jefe con dos poderes Lua tiene dos tablas de estado y dos entornos independientes.

Esto significa:

  • Las variables y funciones locales del archivo, así como los globales Lua que definas, son privados del runtime de ese poder en ese jefe.
  • Las funciones de hook de la tabla devuelta nunca se comparten entre jefes.

El cargador también evalúa el archivo una vez para validar la tabla devuelta. La inicialización a nivel de archivo debe ser independiente del contexto de los hooks. Un poder que solo usa ticks no necesita on_spawn para iniciar su reloj. La inicialización del runtime y el callback de aparición son pasos separados.

Aislamiento de estado​

Cada runtime tiene su propia tabla context.state, separada de otros poderes del mismo jefe y del mismo poder en otros jefes. Úsala para guardar contadores, indicadores, temporizadores u otros datos del poder entre hooks. El estado dura mientras el runtime está activo; no se guarda en disco entre desapariciones o reinicios.

return {
api_version = 1,

on_boss_damaged_by_player = function(context)
-- Each boss tracks its own enrage counter independently
context.state.enrage_hits = (context.state.enrage_hits or 0) + 1
if context.state.enrage_hits >= 20 then
context.boss:apply_potion_effect("SPEED", 200, 2)
end
end
}

Propiedad de las tareas programadas​

Todas las tareas creadas a través de context.scheduler son propiedad del runtime que las creó. Cuando un jefe desaparece:

  1. El runtime llama a shutdown().
  2. Cada tarea de su propiedad -- tanto las de un solo disparo (run_after) como las repetitivas (run_every) -- se cancela automáticamente.
  3. Todas las vigilancias de zona se limpian.
  4. Se restauran los cambios de IA, consciencia, escala e invulnerabilidad que este poder aún mantiene, se borra la curación que estableció y se detiene su controlador de bolas de fuego rastreadoras.

El runtime limpia sus tareas programadas al eliminar el jefe. Algunas utilidades Java de juego inician tareas independientes con su propia duración; consulta su documentación. Cancela las tareas repetitivas cuando ya no las necesites durante el juego normal:

return {
api_version = 1,

on_enter_combat = function(context)
local pulse_count = 0
local task_id
task_id = context.scheduler:run_every(20, function(tick_context)
pulse_count = pulse_count + 1
if pulse_count > 10 or not tick_context.boss.exists then
tick_context.scheduler:cancel_task(task_id)
return
end
tick_context.world:spawn_particle_at_location(
tick_context.boss:get_location(),
{ particle = "FLAME", amount = 20, speed = 0.1 }
)
end)
end
}

Comportamiento del reloj por tick​

El reloj de ticks interno de una instancia de poder Lua solo se ejecuta cuando el poder define un hook on_game_tick. Las vigilancias de zona creadas a través de context.zones:watch_zone(...) o context.script:zone(...):watch(...) crean sus propias tareas repetitivas propias en lugar de habilitar el hook de nivel superior on_game_tick del poder.

Sin on_game_tick ni vigilantes de zona, el runtime no registra ese reloj de ticks. Las tareas explícitas del programador y las iniciadas por utilidades de juego mantienen sus propios horarios. El trabajo por tick y las tareas de vigilancia propiedad del runtime se cancelan cuando desaparece el jefe o se cierra el runtime.

Pausar un runtime conserva su estado Lua y sus tareas, pero omite su trabajo. La ruta de eventos del jefe también comprueba la supresión de poderes. Eliminar el jefe o cerrar el runtime termina el trabajo que le pertenece en vez de pausarlo.


Comportamiento de errores y rendimiento​

EliteMobs impone límites estrictos de error y rendimiento a los poderes Lua:

Excepciones​

Si una función de hook o un callback programado lanza un error de Lua (o una excepción de Java surge de una llamada a la API), el poder se desactiva inmediatamente para esa instancia de jefe. El runtime se cierra y todas las tareas de su propiedad se cancelan.

La instancia fallida permanece cerrada; los hooks de daño posteriores no la recrean silenciosamente. Corrige y recarga la definición y crea un runtime nuevo. Un cierre deliberado por el propietario puede borrar la referencia antigua para reconstruir el cuerpo más adelante.

El error se registra en la consola con el archivo del poder y el hook que se ejecutaba. Se incluye un número de línea cuando está disponible:

[Lua] Error in 'frost_cone.lua' at line 35 during 'on_boss_damaged_by_player':
[Lua] -> ...explanation of what went wrong...
[Lua] -> Script has been disabled for this entity to prevent further errors.

Presupuesto de ejecución​

Los poderes Lua se ejecutan bajo un presupuesto de ejecución estricto que se aplica dentro de la propia VM de Lua, no solo se mide a posteriori. Cada hook, cada callback programado e incluso la evaluación inicial del archivo se ejecutan dentro de ese presupuesto:

LímiteValor
Tiempo de CPU del hilo por punto de entrada50 ms
Tiempo transcurrido alternativo si no se puede medir la CPU250 ms
Instrucciones de bytecode por punto de entrada250,000

La VM cuenta las instrucciones y comprueba el tiempo cada 1024 instrucciones. El error identifica el límite superado:

Lua instruction budget exceeded (250000 instruction limit)
Lua CPU-time budget exceeded (50ms current-thread CPU limit)
Lua elapsed-time fallback budget exceeded (250ms fallback; current-thread CPU time unavailable)

Un bucle Lua infinito se interrumpe durante su ejecución. Las llamadas anidadas comparten el presupuesto del punto de entrada. La VM no puede interrumpir una llamada Java en curso, y no existe un segundo límite de tiempo real de 50 ms tras el retorno. El presupuesto no hace seguras las operaciones costosas sobre el mundo.

Para mantenerte dentro del presupuesto:

  • Evita los bucles sin límite dentro de los hooks. Usa context.scheduler:run_every(...) para repartir el trabajo entre los ticks.
  • Recuerda que los bucles de partículas sobre zonas grandes son la forma habitual de consumir 250,000 instrucciones. Usa coverage para muestrear menos ubicaciones.
  • Mantén los manejadores de on_game_tick ligeros -- se ejecutan en cada tick.
  • Mueve la inicialización pesada a on_spawn o on_enter_combat en lugar de repetirla en cada tick.

Sandbox de Lua​

Los poderes Lua se ejecutan dentro de un entorno LuaJ con sandbox. Se eliminan varios globales que podrían acceder al sistema de archivos o al runtime de Java.

Globales eliminados​

Los siguientes globales estándar de Lua están establecidos en nil y no se pueden usar:

EliminadoPor qué
debugExpone el estado interno de la VM
dofileAcceso al sistema de archivos
ioAcceso al sistema de archivos
loadCarga de código arbitrario
loadfileAcceso al sistema de archivos
luajavaAcceso directo a clases de Java
moduleSistema de módulos (no necesario)
osAcceso al sistema operativo
packageSistema de módulos (no necesario)
requireSistema de módulos / acceso al sistema de archivos

Los hooks de depuración de la VM siguen instalados internamente para contar instrucciones del presupuesto de ejecución. La tabla debug se establece en nil inmediatamente después para impedir que los scripts accedan a ella.

Biblioteca estándar disponible​

El sandbox conserva las bibliotecas estándar de LuaJ, incluidas estas funciones habituales:

CategoríaFunciones
Mathmath.abs, math.ceil, math.floor, math.max, math.min, math.random, math.sin, math.cos, math.sqrt, math.pi, y todas las demás funciones math.*
Stringstring.byte, string.char, string.find, string.format, string.gsub, string.len, string.lower, string.match, string.rep, string.sub, string.upper, y todas las demás funciones string.*
Tabletable.insert, table.remove, table.sort, table.concat, y todas las demás funciones table.*
Iteradorespairs, ipairs, next
Tipotype, tostring, tonumber, select, unpack
Manejo de errorespcall, xpcall, error, assert
Otrosprint, rawget, rawset, rawequal, rawlen, setmetatable, getmetatable

Las cadenas comparten una única metatabla bloqueada. getmetatable("") devuelve false, y las llamadas a métodos sobre cadenas, como ("text"):upper(), usan siempre las funciones estándar de cadenas, incluso después de que un script añada o reemplace funciones en su propia tabla string.

consejo

print escribe en la consola, pero es preferible usar context.log:info(msg) o context.log:warn(msg). El registro de los jefes no incluye automáticamente el archivo del poder. Añádelo al mensaje si varios poderes pueden escribir a la vez.


Espacio de nombres auxiliar em​

La tabla em está disponible en el momento de carga del archivo (antes de que se ejecute cualquier hook). Proporciona constructores auxiliares para construir tablas de ubicación, tablas de vector y definiciones de zona usadas en toda la API.

FunciónPropósito
em.create_location(x, y, z [, world, yaw, pitch])Crea una tabla de ubicación con nombre de mundo, yaw y pitch opcionales
em.create_vector(x, y, z)Crea una tabla de vector
em.zone.create_sphere_zone(radius)Crea una definición de zona esférica
em.zone.create_dome_zone(radius)Crea una definición de zona de cúpula
em.zone.create_cylinder_zone(radius, height)Crea una definición de zona cilíndrica
em.zone.create_cuboid_zone(x, y, z)Crea una definición de zona cuboide
em.zone.create_cone_zone(length, radius)Crea una definición de zona cónica
em.zone.create_static_ray_zone(length, thickness)Crea una definición de zona de rayo estático
em.zone.create_rotating_ray_zone(length, point_radius, animation_duration)Crea una definición de zona de rayo rotatorio
em.zone.create_translating_ray_zone(length, point_radius, animation_duration)Crea una definición de zona de rayo trasladante

em también incluye em.location, con consultas de mundo compartidas de solo lectura: is_in_dungeon, is_protected, owned_by, owners, kinds_at y has_kind. Reciben una tabla de ubicación como primer argumento. Si falta la ubicación, no es una tabla, falta el mundo o este no está cargado, devuelven false o una tabla vacía. Las coordenadas ausentes valen cero; un tipo de coordenada inválido puede provocar un error. Indica el mundo explícitamente al construir ubicaciones para estas consultas. Consulta las firmas y los resultados en la Referencia de la API de Lua.

-- Skip a destructive mechanic inside claims or protected regions
if em.location.is_protected(context.player:get_location()) then return end

No hay ninguna vinculación de Lua para las comprobaciones de permiso de construcción. em.location.is_protected indica si algún plugin de protección reclama la ubicación; no resuelve los derechos de construcción por jugador.

Los constructores de zona devuelven tablas encadenables con :set_center(loc) o :set_origin(loc) / :set_destination(loc), según el tipo. Puedes usarlas a nivel de archivo o dentro de los hooks:

-- At file scope: create a reusable zone shape
local blast_zone = em.zone.create_sphere_zone(5)

return {
api_version = 1,

on_boss_damaged_by_player = function(context)
-- Anchor the zone to the boss's current location at call time
blast_zone:set_center(context.boss:get_location())

local entities = context.zones:get_entities_in_zone(blast_zone)
for i = 1, #entities do
if entities[i].is_player then
entities[i]:apply_potion_effect("SLOWNESS", 60, 1)
end
end
end
}

Para un desglose completo de las formas de zona, filtros, vigilantes y patrones de selección de objetivos, consulta Zonas y Selección de Objetivos.


Próximos pasos​