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 cada tick del servidor (50 ms) mientras el reloj del runtime está activo | 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_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 | El jefe cambia su 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_zone_enter / on_zone_leave are not fired by Lua zonesEl 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 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 a firework 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_boss_damaged_by_elite y on_player_damaged_by_boss.
| Campo / Método | Tipo | Descripción |
|---|---|---|
event.damage_amount | double | Valor de daño en bruto |
event.damage_cause | string | Nombre de DamageCause de Spigot (p. ej. "ENTITY_ATTACK", "PROJECTILE") |
event.damager | entity table | Entidad que infligió el daño. Solo presente en los hooks de daño por entidad. |
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
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 de juego subyacente sea cancelable expone event.cancel_event(). Si context.event es nil para un hook dado (p. ej. on_game_tick, on_heal), no hay un evento subyacente con el que interactuar.
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
Cuando un jefe tiene varios poderes Lua adjuntos, el hook de cada poder se llama para el mismo evento. El orden se determina por el campo priority:
- Los valores más bajos se ejecutan primero (el predeterminado es
0). - Los poderes con la misma prioridad se ejecutan en orden de carga (efectivamente no especificado).
return {
api_version = 1,
priority = -10, -- runs before most other powers
on_boss_damaged_by_player = function(context)
-- This runs early, so other powers see any state changes we make
context.state.last_attacker = context.player.uuid
end
}
La prioridad solo afecta al orden entre los poderes Lua del mismo jefe. No interactúa con el orden de ejecución de EliteScript.
Modelo de runtime
Un runtime por jefe
Cada entidad jefe obtiene su propia instancia de runtime Lua independiente. Cuando el jefe aparece, EliteMobs carga el código fuente Lua, lo evalúa en un entorno sandbox nuevo y almacena la tabla devuelta. Cuando el jefe desaparece o es eliminado, el runtime se cierra.
Esto significa:
- Las variables globales de Lua establecidas durante la evaluación del archivo (como funciones auxiliares con
local function) son privadas de ese jefe. - Las funciones de hook de la tabla devuelta nunca se comparten entre jefes.
Aislamiento de estado
Cada runtime tiene su propia tabla context.state. El estado de un jefe es completamente invisible para todos los demás jefes, incluso si comparten el mismo archivo de poder Lua. Usa context.state para almacenar contadores, banderas, temporizadores o cualquier dato por jefe que necesites a través de los hooks.
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.
Nunca necesitas limpiar manualmente las tareas programadas al eliminar un jefe. Sin embargo, deberías cancelar las tareas repetitivas cuando ya no sean necesarias durante el juego normal para evitar trabajo innecesario:
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.
Si un poder no tiene ni on_game_tick ni vigilantes de zona, no se incurre en ningún trabajo por tick. El trabajo de ticks y las tareas de los vigilantes de zona se cancelan automáticamente cuando el jefe desaparece o el runtime se cierra.
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.
El error se registra en la consola del servidor junto con el nombre del archivo del poder, el número de línea y el hook que se estaba ejecutando:
[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 |
Biblioteca estándar disponible
Todo lo demás de la biblioteca estándar de Lua funciona normalmente:
| 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 |
print escribe en la consola del servidor, pero prefiere context.log:info(msg) o context.log:warn(msg) para la salida. Estos llevan como prefijo el nombre del poder, lo que facilita rastrear qué poder produjo el mensaje.
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 |
Los constructores de zona devuelven tablas encadenables con :set_center(loc) (o :set_origin(loc) / :set_destination(loc) dependiendo del tipo de zona). Están diseñados para usarse en la parte superior de un archivo o dentro de los hooks:
-- Skip a destructive mechanic inside claims or protected regions
if em.location.is_protected(context.player:get_location()) then return end
em incluye también una sub-tabla em.location con consultas de mundo de solo lectura compartidas por todos los plugins de MagmaGuy: is_in_dungeon, is_protected, owned_by, owners, kinds_at y has_kind. Toman una tabla de ubicación como primer argumento y son seguras frente a nil (una ubicación ausente o malformada devuelve false o una tabla vacía en lugar de dar error). Las firmas y las formas de retorno están documentadas una sola vez, en la Referencia de la API de Lua.
-- 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
}
No hay ninguna vinculación de Lua para las comprobaciones de permiso de construcción. em.location.is_protected te dice si algún plugin de protección reclama la ubicación; no resuelve los derechos de construcción por jugador.
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
