Saltar al contenido principal

Scripting Lua: Ejemplos y patrones

Esta página contiene ejemplos completos de poderes Lua de EliteMobs y sus explicaciones. Guarda cada ejemplo en su propio archivo .lua, asígnalo a un boss de prueba, ejecuta /em reload y genera un boss nuevo después de editarlo. Los nombres de animación como slam, roar, frenzy y transform deben existir en el modelo personalizado del boss; no son animaciones incorporadas. Un boss sin modelo personalizado omite estas llamadas de animación. Los tiempos indicados suponen 20 TPS.

Si eres nuevo en los poderes Lua, comienza con Primeros pasos. Para scripts de NPC, consulta Scripts de NPC. Para detalles completos de la API de poderes de boss, consulta la Referencia de API, Boss y entidades, Mundo y entorno, Zonas y objetivos y Enums.

webapp_banner.jpg


Ejemplo: Selección de objetivos basada en zonas con utilidades de script​

Lo que enseña este ejemplo: Cómo usar context.script para crear geometría de zona estilo EliteScript desde Lua, generar partículas y dañar entidades en la zona.

Archivo de poder completo (clic para expandir)
return {
api_version = 1,

on_boss_damaged_by_player = function(context)
local cone = context.script:zone({
shape = "CONE",
Target = {
targetType = "SELF",
offset = "0,1,0"
},
Target2 = {
targetType = "NEARBY_PLAYERS",
range = 20
},
radius = 5
})

context.script:spawn_particles(
cone:full_target(0.4),
{
particle = "FLAME",
amount = 1,
speed = 0.05
}
)

context.script:damage(cone:full_target(), 1.0, 1.5)
end
}

Explicación​

  1. Creación de zona -- context.script:zone(...) usa los campos de las Zonas de EliteScript. Target sitúa la punta del cono un bloque por encima del boss. La primera posición resuelta por Target2 determina el centro de la base y, por tanto, la longitud; radius = 5 fija el radio de la base. NEARBY_PLAYERS busca en una caja que se extiende 20 bloques en cada eje, sin garantizar el jugador más cercano. Usa DIRECT_TARGET en este hook de daño para apuntar al atacante. Sin jugador resuelto no hay cono; los extremos deben ser distintos y pertenecer al mismo mundo.

  2. Generación de partículas -- cone:full_target(0.4) devuelve un objeto de objetivo que selecciona puntos de la zona con una probabilidad del 40% en cada llamada. No garantiza una cantidad exacta ni filtra los destinatarios del daño. La especificación usa los campos de EliteScript particle, amount y speed. El método desplaza las posiciones de partículas de zona medio bloque en X y Z; las partículas no representan los límites exactos de impacto contra entidades.

  3. Daño -- context.script:damage(cone:full_target(), 1.0, 1.5) alcanza a los jugadores resueltos dentro de los conos porque el filtro predeterminado de zona es PLAYER. El primer número (1.0) es el daño base enviado y el segundo (1.5) es el multiplicador de daño de EliteMobs aplicado a jugadores. El procesamiento de combate determina el daño final.

Este ejemplo se ejecuta con cada golpe despachado: no tiene cooldown local ni global, ni espera de aviso entre las partículas y el daño. Añade comprobaciones explícitas si lo combinas con otros ataques. Cada consulta de una zona estática vuelve a resolver los objetivos; un objeto de zona Lua no aplica automáticamente condiciones ni caché de objetivos de acciones YAML.

Nombres de campo de EliteScript

Las tablas de zona y objetivo pasadas a context.script usan nombres de campo de EliteScript (targetType, shape, Target, Target2, range, offset, coverage). Para la lista completa, consulta Zonas de EliteScript y Objetivos de EliteScript.


Ejemplo: Bucle de ataque con estado y scheduler​

Lo que enseña este ejemplo: Uso de context.state para rastrear el estado en tiempo de ejecución, context.scheduler para tareas repetitivas y el ciclo de vida adecuado de entrada/salida de combate.

Archivo de poder completo (clic para expandir)
local function pick_action(context)
local roll = math.random(1, 2)

if roll == 1 then
context.boss:play_model_animation("slam")
else
context.boss:play_model_animation("roar")
end
end

return {
api_version = 1,

on_spawn = function(context)
context.state.started = false
context.state.loop_task_id = nil
end,

on_enter_combat = function(context)
if context.state.started then
return
end

context.state.started = true

context.state.loop_task_id = context.scheduler:run_every(100, function(loop_context)
if loop_context.boss.exists then
pick_action(loop_context)
end
end)
end,

on_exit_combat = function(context)
if context.state.loop_task_id ~= nil then
context.scheduler:cancel_task(context.state.loop_task_id)
context.state.loop_task_id = nil
end
context.state.started = false
end
}

Explicación​

  1. Inicialización de estado -- on_spawn establece context.state.started en false y context.state.loop_task_id en nil. La tabla state persiste entre los hooks de esta instancia de ejecución del poder en este boss. Otros poderes tienen sus propias tablas de estado.

  2. Guardia de combate -- on_enter_combat verifica context.state.started antes de iniciar el bucle. Esto previene múltiples bucles superpuestos si el evento se dispara más de una vez.

  3. Patrón de scheduler -- context.scheduler:run_every(100, callback) programa la primera llamada sin retraso y luego repite cada 100 ticks (5 segundos). No espera cinco segundos antes de empezar. Usa run_repeating(100, 100, callback) si quieres esa espera inicial. El callback recibe un contexto nuevo sin el evento ni el jugador desencadenante, con una instantánea nueva del boss en loop_context.boss. Guarda el ID numérico devuelto en el estado. Este ejemplo solo elige una animación; no causa daño por sí mismo.

  4. Limpieza al salir -- on_exit_combat cancela la tarea repetitiva usando el ID de tarea almacenado y reinicia el estado. Esto es crítico: sin limpieza, el scheduler sigue ejecutándose incluso después de que termine el combate.

Siempre cancela las tareas del scheduler

Cancela las repeticiones exclusivas de combate en on_exit_combat. Sin esa limpieza siguen programadas después del combate, hasta su cancelación o el cierre de la instancia del poder. Las instancias pausadas omiten callbacks sin perder la tarea. Un error no capturado en el callback cierra la instancia y cancela sus tareas.


Ejemplo: Efecto de fuego al golpear​

Lo que enseña este ejemplo: Un simple poder de "al golpear aplicar efecto" -- el patrón más común para poderes de combate.

Archivo de poder completo (clic para expandir)
return {
api_version = 1,

on_player_damaged_by_boss = function(context)
-- Guard: the player may be nil in edge cases
if context.player == nil then
return
end

if not context.cooldowns:global_ready() then
return
end

-- Check and set a 60-tick (3 second) local cooldown in one call
if not context.cooldowns:check_local("fire_touch", 60) then
return
end

-- Coordinate with other powers that check the shared cooldown
context.cooldowns:set_global(40)

-- Set the player on fire for 60 ticks (3 seconds)
context.player:set_fire_ticks(60)

-- Visual feedback: spawn flame particles at the player's location
context.world:spawn_particle_at_location(
context.player.current_location,
{ particle = "FLAME", amount = 20, speed = 0.1 }
)

-- Tell the player what happened
context.player:send_message("&cThe boss's touch burns!")
end
}

Explicación​

  1. Comprobación de nil -- context.player se resuelve a partir del jugador actor o del objetivo directo del evento actual. Compruébalo antes de usarlo, especialmente al adaptar el código a otro hook. Los callbacks programados no heredan este jugador.

  2. Cooldown local -- Después de comprobar global_ready(), context.cooldowns:check_local("fire_touch", 60) comprueba su clave y establece inmediatamente 60 ticks si está lista. Si no, devuelve false y termina el hook. Los poderes del mismo boss comparten la clave; otros bosses son independientes. Añade un prefijo con el nombre del poder al combinar ejemplos.

  3. Ticks de fuego -- context.player:set_fire_ticks(60) fija la duración de fuego de Bukkit en 60 ticks (3 segundos), tras comprobar que el jugador es válido, está vivo y conectado, pertenece al mundo de origen y, dentro de una instancia de mazmorra, es un participante actual. Sustituye la duración en vez de añadir 60 ticks. Otras mecánicas pueden apagar al jugador; el mensaje y las partículas no prueban que se aceptó el efecto.

  4. Partículas -- context.world:spawn_particle_at_location(location, spec) genera partículas en una ubicación específica. La tabla de especificación acepta particle (nombre de enum de partícula de Bukkit), amount y speed.

  5. Mensaje -- context.player:send_message(text) envía un mensaje de chat con códigos de color. Los códigos de color estándar de Minecraft como &c (rojo) funcionan automáticamente.

  6. Cooldown global -- context.cooldowns:set_global(40) fija el cooldown compartido del boss en 40 ticks (2 segundos), antes de aplicar efectos. Este ejemplo lo comprueba antes de consumir el local. Otros hooks y callbacks Lua también deben llamar a global_ready() si deben esperar; establecer el cooldown global no suspende todos los callbacks Lua.


Ejemplo: Poder AoE basado en zonas usando zonas nativas Lua​

Lo que enseña este ejemplo: Crear y consultar zonas nativas Lua para dañar jugadores en un área.

Archivo de poder completo (clic para expandir)
return {
api_version = 1,

on_spawn = function(context)
context.state.aoe_task_id = nil
end,

on_enter_combat = function(context)
-- Prevent duplicate loops
if context.state.aoe_task_id ~= nil then
return
end

context.state.aoe_task_id = context.scheduler:run_every(60, function(tick_context)
-- Make sure the boss is still alive
if not tick_context.boss.exists then
return
end

if not tick_context.cooldowns:global_ready() then
return
end

-- Check and set this boss's named cooldown before applying effects
if not tick_context.cooldowns:check_local("pulse_aoe", 60) then
return
end
tick_context.cooldowns:set_global(60)

-- Build a sphere zone centered on the boss's current position
local zone_def = {
kind = "sphere",
radius = 8,
origin = tick_context.boss:get_location()
}

-- Find all players inside the sphere
local victims = tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" })

-- Damage and show particles on each player found
for i = 1, #victims do
local victim = victims[i]
victim:deal_custom_damage(4.0)

tick_context.world:spawn_particle_at_location(
victim.current_location,
{ particle = "DUST", amount = 15, speed = 0, red = 128, green = 0, blue = 255 }
)
end

-- Spawn a particle burst at the boss; this does not draw the sphere
tick_context.world:spawn_particle_at_location(
tick_context.boss:get_location(),
{ particle = "WITCH", amount = 40, speed = 0.1 }
)
end)
end,

on_exit_combat = function(context)
if context.state.aoe_task_id ~= nil then
context.scheduler:cancel_task(context.state.aoe_task_id)
context.state.aoe_task_id = nil
end
end
}

Explicación​

  1. Configuración de estado -- on_spawn inicializa aoe_task_id en nil. Este ID de tarea mantendrá la referencia de la tarea del scheduler repetitiva.

  2. Ataque repetitivo -- on_enter_combat inicia una tarea sin retraso inicial y con intervalos de 60 ticks (3 segundos). La comprobación evita bucles duplicados. Cada callback comprueba el cooldown global, después comprueba y establece pulse_aoe, y fija un cooldown global de 60 ticks antes de los efectos. Solo coordina poderes que comprueban esas claves; no bloquea automáticamente todos los scripts.

  3. Definición de zona -- La tabla zone_def usa la sintaxis de zona nativa Lua. El campo kind especifica la forma ("sphere"), radius establece el tamaño, y origin se establece a la ubicación actual del boss en el momento en que se ejecuta el callback. Esto significa que la zona sigue al boss mientras se mueve.

  4. Consulta de entidades -- tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" }) devuelve un array Lua con índices desde uno según la prueba de intersección de entidades de la forma, no una distancia exacta hasta los pies. Busca en el mundo actual del boss y no filtra jugadores por modo de juego ni participación en el encuentro. Los filtros aceptan "players", "elites", "mobs" o "living" (predeterminado); el propio boss queda excluido.

  5. Causar daño -- victim:deal_custom_damage(4.0) envía 4 puntos de daño base atribuidos al boss mediante BossCustomAttackDamage. Omite objetivos invulnerables o muertos y jugadores fuera de Supervivencia o Aventura. En la ruta de elite a jugador evita la fórmula normal y la reducción por bloqueo, y elimina las reducciones de armadura de vanilla. Los listeners, la cancelación y la absorción aún pueden modificar la salud perdida. El método reinicia el cooldown de daño nativo del objetivo tras enviar el daño; no usa la misma ruta que context.script:damage.

  6. Partículas -- El polvo morado aparece en la posición guardada de cada jugador encontrado, incluso si se rechazó su daño. Aparece una ráfaga WITCH en el boss; no dibuja un anillo ni el límite de la esfera. Estas tablas compactas usan dispersión de posición cero por defecto, y el tamaño del polvo es fijo: 1.

  7. Limpieza -- on_exit_combat cancela la tarea repetitiva y limpia el estado, siguiendo el mismo patrón que el ejemplo anterior.

Zonas nativas vs. zonas de utilidades de script

Este ejemplo usa zonas nativas Lua (context.zones:get_entities_in_zone()), que toman una tabla simple con kind, radius, origin, etc. Las utilidades de script (context.script:zone(...)) usan nombres de campo de EliteScript como shape, Target y Target2. Ambas funcionan -- usa zonas nativas para formas simples y context.script cuando necesites la resolución avanzada de objetivos de EliteScript.


Ejemplo: Mecánica de boss multifase​

Lo que enseña este ejemplo: Uso de estado para rastrear fases del boss y cambiar comportamiento en umbrales de salud.

Son fases de estado Lua, no phases: de YAML que sustituyen la definición del boss. Aquí no se sustituye ni cura al boss. Curarlo después no lo devuelve a la fase 1; salir del combate conserva la fase 2 hasta que se recrea la instancia del poder.

Archivo de poder completo (clic para expandir)
local function phase_one_attack(context)
-- One damage pulse per callback; the animation does not delay it
context.boss:play_model_animation("slam")

local zone_def = {
kind = "sphere",
radius = 5,
origin = context.boss:get_location()
}

local targets = context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #targets do
targets[i]:deal_custom_damage(3.0)
end

context.world:spawn_particle_at_location(
context.boss:get_location(),
{ particle = "EXPLOSION", amount = 3, speed = 0 }
)
end

local function phase_two_attack(context)
-- One pulse per callback, repeated more frequently in phase 2
context.boss:play_model_animation("frenzy")

local zone_def = {
kind = "sphere",
radius = 8,
origin = context.boss:get_location()
}

local targets = context.zones:get_entities_in_zone(zone_def, { filter = "players" })
for i = 1, #targets do
targets[i]:deal_custom_damage(2.0)
targets[i]:apply_potion_effect("SLOWNESS", 40, 1)
end

context.world:spawn_particle_at_location(
context.boss:get_location(),
{ particle = "DUST", amount = 30, speed = 0.2, red = 255, green = 0, blue = 0 }
)

context.world:play_sound_at_location(
context.boss:get_location(),
"entity.wither.ambient",
1.0, 1.5
)
end

local function start_attack_loop(context)
if context.state.attack_task_id ~= nil then return end
local phase = context.state.phase or 1
local interval = phase == 2 and 40 or 100
local attack = phase == 2 and phase_two_attack or phase_one_attack
context.state.attack_task_id = context.scheduler:run_every(interval, function(tick_context)
if not tick_context.boss.exists or not tick_context.boss.is_in_combat then return end
if tick_context.cooldowns:check_local("phase_attack", interval) then
attack(tick_context)
end
end)
end

return {
api_version = 1,

on_spawn = function(context)
context.state.phase = 1
context.state.attack_task_id = nil
end,

on_enter_combat = function(context)
start_attack_loop(context)
end,

on_game_tick = function(context)
-- Only check phase transition every 20 ticks (1 second) to stay lightweight
if not context.boss.is_in_combat then return end
if not context.cooldowns:check_local("phase_check", 20) then
return
end

-- Skip if already in phase 2
if context.state.phase ~= 1 then
return
end

-- Check health ratio
local health_ratio = context.boss.health / context.boss.maximum_health

if health_ratio <= 0.5 then
-- Transition to phase 2
context.state.phase = 2

context.log:info("Boss entering phase 2 at " .. tostring(math.floor(health_ratio * 100)) .. "% health")

-- Cancel the old attack loop
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end

-- Play transition effects
context.boss:play_model_animation("transform")

-- Announce the phase change to nearby players
local nearby = context.players.nearby_players(40)
for i = 1, #nearby do
nearby[i]:send_message("&4&lThe boss enters a frenzy!")
nearby[i]:show_title("&4Phase 2", "&cThe boss is enraged!", 10, 40, 10)
end

start_attack_loop(context)
end
end,

on_exit_combat = function(context)
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end
end
}

Explicación​

  1. Inicialización de estado -- on_spawn establece phase en 1 y attack_task_id en nil. Los valores persisten entre los hooks de esta instancia del poder, no tras una recarga ni en otros poderes.

  2. Bucle de ataque -- on_enter_combat reanuda la fase actual mediante start_attack_loop: la fase 1 usa un intervalo de 100 ticks y la fase 2 de 40. Ambas programan el primer callback sin retraso. Cada ataque necesita que phase_attack esté listo; cambiar de fase no borra el cooldown anterior, así que un callback puede omitir el ataque. Este ejemplo no comprueba el cooldown global. La fase 1 envía 3 puntos de daño personalizado a jugadores en una esfera de 5 bloques, con animación y partículas, sin esperar a que termine la animación.

  3. Comprobación de fase en on_game_tick -- on_game_tick termina de inmediato fuera del combate y después comprueba un cooldown de 20 ticks ("phase_check"). Si el jefe ya está en la fase 2, también termina antes.

  4. Umbral de salud -- context.boss.health / context.boss.maximum_health da la salud actual como fracción. Cuando cae al 50% o menos, comienza la transición.

  5. Transición de fase -- Se cancela el viejo bucle de ataque, se reproduce una animación de transformación, los jugadores cercanos reciben un mensaje y título, y comienza un nuevo bucle de ataque más rápido (cada 40 ticks en lugar de 100).

  6. Ataques de fase 2 -- phase_two_attack usa una esfera de 8 bloques y envía un golpe de 2 puntos por jugador encontrado y por callback que supera las comprobaciones. Intenta aplicar Lentitud II durante 40 ticks (amplifier = 1), respetando las comprobaciones de participación de efectos descritas arriba, y después muestra polvo rojo y reproduce un sonido de wither. No programa varios golpes dentro de un mismo callback.

  7. Registro -- context.log:info(...) escribe en la consola del servidor, lo cual es invaluable para depurar transiciones de fase durante el desarrollo.

  8. Limpieza -- on_exit_combat cancela cualquier bucle de ataque actualmente activo, independientemente de la fase en la que se encuentre el boss.

Mantén on_game_tick ligero

Una instancia activa y no pausada recibe on_game_tick cada tick del servidor (20 veces por segundo a 20 TPS). Limita el trabajo costoso con un cooldown, como check_local("phase_check", 20). La salud se consulta periódicamente, así que un golpe letal puede eliminar al boss antes de comprobar la transición. Superar el presupuesto de ejecución cierra la instancia del poder y cancela sus tareas.


Consejos para generación con IA​

Si quieres que la IA genere poderes Lua de forma confiable, asegúrate de que el prompt incluya:

  • Nombre exacto del hook -- ej., on_player_damaged_by_boss, no "cuando el boss golpea a un jugador".
  • Zonas nativas Lua o utilidades de script -- especifica cuál. Las nativas usan context.zones:get_entities_in_zone(zone_def, opts) con kind, radius y origin. Las utilidades usan context.script:zone(...) con campos de EliteScript como shape, Target y Target2.
  • Cooldowns locales y globales -- especifica una clave con prefijo y la duración en ticks. context.cooldowns:check_local(key, ticks) comprueba y establece un cooldown con nombre por boss. Para coordinarlo con el global, comprueba global_ready() antes de consumir la clave local y establece la duración global antes de los efectos.
  • Nombres de animación de modelo personalizado -- por ejemplo, context.boss:play_model_animation("slam"). La IA no puede adivinarlos; proporciónalos.
  • Selección de objetivo -- especifica si apunta a context.player, context.players.nearby_players(range) o una consulta de zona.
  • Tipo de efecto -- daño (deal_custom_damage), poción (apply_potion_effect), fuego (set_fire_ticks), velocidad (set_velocity_vector, apply_push_vector), etc.
  • Usa los métodos del wrapper correcto -- proporciona la referencia de boss o de API compartida que corresponda y no inventes alias.
  • Las especificaciones de utilidades de script usan nombres de campo de EliteScript -- targetType, shape, Target, Target2, range, offset, coverage, no nombres de estilo Lua.
  • Tiempo y contexto del scheduler -- indica si la primera ejecución es inmediata o retrasada. run_every no tiene retraso inicial; run_repeating recibe uno separado. Usa el contexto nuevo del callback, que no contiene el evento ni el jugador desencadenante. Guarda explícitamente un UUID de jugador o una posición si lo necesitarás después.

Buen ejemplo de prompt​

Escribe un poder Lua que use on_enter_combat para iniciar una tarea con 80 ticks de retraso inicial e intervalo de 80 ticks. En cada callback, crea una esfera nativa Lua de radio 6 centrada en el boss, consulta los jugadores y envía 2.0 de daño personalizado a cada uno. Comprueba el cooldown global antes de comprobar y establecer la clave local "my_power:pulse" durante 80 ticks; después fija el global en 80 antes de los efectos. Cancela la tarea en on_exit_combat. Genera partículas DUST (red=0, green=255, blue=100) en cada jugador encontrado.

Restricciones adicionales a incluir​

  • "Devuelve una sola tabla con api_version = 1."
  • "Inicializa todos los campos de estado en on_spawn."
  • "Protege los callbacks del scheduler con if not tick_context.boss.exists then return end."
  • "Cancela todas las tareas repetitivas en on_exit_combat."
  • "No inventes nombres de métodos -- solo usa métodos de la Referencia de API."
  • "Usa context.cooldowns:check_local(key, ticks) para verificar-y-establecer combinado."

Lista de verificación QC para revisión humana o de IA​

Usa esta lista para verificar un poder Lua antes de desplegarlo:

  1. El archivo devuelve exactamente una tabla con api_version = 1.
  2. Cada nombre de hook coincide exactamente con una entrada de la lista de hooks, por ejemplo on_player_damaged_by_boss, no on_player_hit.
  3. context.player se protege con == nil antes de usarse en hooks donde puede ser nil.
  4. Los campos de context.state se inicializan en on_spawn.
  5. Toda tarea repetitiva exclusiva de combate, creada con run_every o run_repeating, se cancela en on_exit_combat; su retraso inicial es intencionado.
  6. Los callbacks usan su propio contexto y no suponen que exista un evento o jugador desencadenante.
  7. Las claves de cooldown incluyen un prefijo con el nombre del poder y las duraciones se expresan en ticks. La coordinación global incluye una comprobación explícita de disponibilidad.
  8. Los hooks on_game_tick protegen el trabajo costoso detrás de una verificación de cooldown.
  9. Todos los nombres de métodos existen en la Referencia de API; no hay alias inventados.
  10. Las tablas de utilidades usan campos de EliteScript (targetType, shape, Target, etc.), no nombres de estilo Lua.
  11. Las definiciones de zona nativas usan kind, radius, origin, destination, etc.
  12. El poder no llama operaciones bloqueantes o de larga duración dentro de un hook o callback.
  13. Las especificaciones de partículas usan nombres válidos del enum de Bukkit en UPPER_CASE, por ejemplo "FLAME", "DUST" y "EXPLOSION".
  14. Recarga los archivos modificados, genera un boss nuevo y comprueba el comportamiento visible y la consola en un servidor de pruebas. Que el archivo se analice correctamente no demuestra que el ataque funcione.

Mejores prácticas​

  • Comienza con un hook pequeno y verifica. Escribe un solo on_spawn que envie un mensaje de log. Confirma que se dispara. Luego construye desde ahi.

  • Mantén las funciones auxiliares locales. Declara funciones como local function pick_action(context) antes de la tabla de retorno para mantener explícitas las dependencias. Cada instancia de poder ya dispone de su propio entorno Lua.

  • Pon la geometría en utilidades de script. Para conos, rayos rotatorios, rayos en traslación o zonas animadas, usa context.script:zone(...) con campos de EliteScript. Las utilidades reutilizan el motor de zonas de EliteScript.

  • Usa context.state para el estado de ejecución. Pertenece a una instancia de poder en un boss y persiste entre sus hooks. No se comparte con otros poderes ni se guarda entre recargas.

  • Usa claves de cooldown locales con nombre. Usa claves descriptivas como "fire_touch" o "aoe_pulse". Las claves explícitas se comparten entre todos los poderes del mismo boss. Añade el nombre del poder como prefijo, por ejemplo "my_power:fire_touch", para evitar colisiones.

  • Mantén on_game_tick ligero. Limita las operaciones costosas con un cooldown y mantente muy por debajo del presupuesto de ejecución.

  • Cancela las repeticiones al terminar. Los bucles exclusivos de combate deben cancelarse en on_exit_combat. Las tareas de la instancia se cancelan automáticamente cuando esta se cierra, pero continúan tras el combate salvo cancelación explícita.

  • Usa contextos nuevos en callbacks del scheduler. Los callbacks de run_every, run_repeating y run_after reciben un contexto nuevo sin el evento ni el jugador que los programó. Usa ese argumento y conserva explícitamente el UUID o la posición que necesites; el contexto externo puede contener instantáneas obsoletas.

  • Registra transiciones con context.log:info(). Durante el desarrollo registra cambios de fase, inicios de cooldown e inicios y paradas del scheduler. Elimina registros ruidosos antes de desplegar: context.log:debug() del boss también escribe a nivel info, con otro prefijo.

  • Reutiliza las referencias pertinentes de EliteScript. Zonas, Objetivos y Vectores relativos describen los analizadores compartidos. Los auxiliares Lua independientes no evalúan automáticamente las Condiciones, esperas ni cachés de acciones YAML; expresa las comprobaciones en Lua. Los objetivos heredados y las posiciones de aterrizaje necesitan un contexto de acción YAML que estos objetos no tienen.


Errores comunes de principiante​

  • Usar el context externo dentro de un callback del scheduler. El contexto externo captura un snapshot en el momento en que se ejecuto el hook. Dentro de un callback run_every o run_after, siempre usa el parametro propio del callback (ej., tick_context), que te da un snapshot fresco.

  • Olvidar cancelar tareas repetitivas. Si inicias un run_every en on_enter_combat pero nunca lo cancelas, la tarea se ejecuta hasta que el boss es removido del servidor, incluso despues de que termine el combate.

  • No inicializar estado en on_spawn. Si lees context.state.phase en on_game_tick pero nunca lo estableces en on_spawn, sera nil y tus comparaciones se comportaran inesperadamente.

  • Usar context.player sin comprobar nil. Revisa qué hooks proporcionan jugador. Los callbacks programados no lo tienen por defecto; omitir la comprobación puede cerrar la instancia del poder.

  • Usar campos de estilo Lua en utilidades de script. Usa los campos documentados targetType, Target, Target2 y shape. No se admiten alternativas como target_type y zone_shape. El analizador de zona acepta target y target2 en minúsculas, pero eso no permite alias arbitrarios de estilo Lua.

  • Ejecutar trabajo costoso en on_game_tick sin cooldown. Las consultas de entidades de todo el mundo y las geometrías densas de partículas para muchos bosses son costosas. Limita ese trabajo y mantén cada callback dentro de su presupuesto de ejecución.

  • Mezclar APIs de wrappers. Los wrappers específicos de boss de EliteMobs usan entity:teleport_to_location(loc) y player:set_velocity_vector(vec). Los wrappers compartidos de MagmaCore tienen alias adicionales. Consulta la referencia de tu contexto antes de copiar un método del ejemplo de otro plugin.

  • Usar context.boss.health para establecer salud. context.boss.health es un snapshot de solo lectura. Para curar al boss, usa context.boss:restore_health(amount).

  • Olvidar api_version = 1. La tabla devuelta debe incluir este campo, o EliteMobs no cargara el poder.


Próximos pasos​