Scripting Lua: Hooks y Ciclo de Vida
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.
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.
| Hook | Se activa cuando | ¿context.player disponible? |
|---|---|---|
on_spawn | La elite mob aparece | No |
on_game_tick | Una vez por tick mientras el reloj del runtime está activo y sin pausar; normalmente cada 50 ms a 20 TPS | No |
on_boss_damaged | El jefe recibe daño de cualquier fuente | No |
on_boss_damaged_by_player | El jefe recibe daño de un jugador | Sí |
on_reinforcement_damaged_by_player | Un refuerzo invocado por este jefe recibe daño de un jugador; se ejecuta en los poderes del invocador | Sí |
on_boss_damaged_by_elite | El jefe recibe daño de otra elite mob | No |
on_player_damaged_by_boss | Un jugador recibe daño de este jefe | Sí |
on_enter_combat | El jefe entra en combate | Sí |
on_exit_combat | El jefe sale del combate | No |
on_heal | El jefe se cura | No |
on_boss_target_changed | EliteMobs notifica un evento de objetivo jugador, incluida la adquisición inicial; no cubre cada cambio o pérdida de objetivo | Sí |
on_death | El jefe muere | No |
on_phase_switch | Un jefe por fases cambia a una nueva fase | No |
on_mind_action | Un Mind nativo solicita una acción a sus poderes; consulta el contexto de acción | No |
on_zone_enter | Una entidad entra en una zona vigilada | Sí (si la entidad es un jugador) |
on_zone_leave | Una entidad sale de una zona vigilada | Sí (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.
on_zone_enter / on_zone_leaveEl 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étodo | Tipo | Descripción |
|---|---|---|
event.damage_amount | double | Instantánea del daño al crear esta tabla de evento |
event.get_damage_amount() | double | Lee el daño actual después de modificaciones anteriores |
event.damage_cause | string | Nombre de DamageCause de Spigot (p. ej. "ENTITY_ATTACK", "PROJECTILE") |
event.damager | entity table | Atacante 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.projectile | entity table | La 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étodo | Tipo | Descripción |
|---|---|---|
event.spawn_reason | string | Nombre de SpawnReason de Spigot |
event.cancel_event() | — | Cancela la aparición |
Hook de muerte
Se aplica a on_death.
| Campo / Método | Tipo | Descripción |
|---|---|---|
event.entity | entity table | La entidad que está muriendo |
Hooks de zona
Se aplica a on_zone_enter y on_zone_leave.
| Campo / Método | Tipo | Descripción |
|---|---|---|
event.entity | entity table | La 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:
- El runtime llama a
shutdown(). - Cada tarea de su propiedad -- tanto las de un solo disparo (
run_after) como las repetitivas (run_every) -- se cancela automáticamente. - Todas las vigilancias de zona se limpian.
- 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ímite | Valor |
|---|---|
| Tiempo de CPU del hilo por punto de entrada | 50 ms |
| Tiempo transcurrido alternativo si no se puede medir la CPU | 250 ms |
| Instrucciones de bytecode por punto de entrada | 250,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
coveragepara muestrear menos ubicaciones. - Mantén los manejadores de
on_game_tickligeros -- se ejecutan en cada tick. - Mueve la inicialización pesada a
on_spawnoon_enter_combaten 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:
| Eliminado | Por qué |
|---|---|
debug | Expone el estado interno de la VM |
dofile | Acceso al sistema de archivos |
io | Acceso al sistema de archivos |
load | Carga de código arbitrario |
loadfile | Acceso al sistema de archivos |
luajava | Acceso directo a clases de Java |
module | Sistema de módulos (no necesario) |
os | Acceso al sistema operativo |
package | Sistema de módulos (no necesario) |
require | Sistema 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ía | Funciones |
|---|---|
| Math | math.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.* |
| String | string.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.* |
| Table | table.insert, table.remove, table.sort, table.concat, y todas las demás funciones table.* |
| Iteradores | pairs, ipairs, next |
| Tipo | type, tostring, tonumber, select, unpack |
| Manejo de errores | pcall, xpcall, error, assert |
| Otros | print, 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.
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ón | Propó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
- Jefe y Entidades --
context.boss,context.player, envoltorios de entidad - Mundo y Entorno -- partículas, sonidos, aparición,
context.world - Zonas y Selección de Objetivos -- zonas nativas, utilidades de script,
context.zones/context.script - Ejemplos y Patrones -- poderes funcionales completos que puedes estudiar y adaptar
- Enums y Valores -- enlaces a la Javadoc de Spigot para todas las constantes de cadena
