Saltar al contenido principal

Scripting Lua: Ejemplos y patrones

Esta página contiene ejemplos completos y funcionales de poderes Lua de EliteMobs, además de patrones prácticos, mejores prácticas y consejos. Cada ejemplo incluye una explicación de qué hace y por qué.

Si eres nuevo en los poderes Lua, comienza con Primeros pasos. Para detalles completos de la API, 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(...) crea una forma de cono usando los mismos nombres de campo que las Zonas de EliteScript. Target establece el origen del cono (el propio boss, desplazado 1 bloque hacia arriba), y Target2 establece el destino (los jugadores más cercanos dentro de 20 bloques). radius controla qué tan ancho se abre el cono.

  2. Generación de partículas -- cone:full_target(0.4) devuelve un handle de objetivo que resuelve a todas las ubicaciones dentro del cono con 40% de cobertura (muestrea aleatoriamente el 40% de los puntos de zona en cada llamada). La especificación de partículas usa los mismos nombres de campo que las partículas de EliteScript: particle, amount y speed.

  3. Daño -- context.script:damage(cone:full_target(), 1.0, 1.5) golpea a todas las entidades vivas dentro del cono completo. El primer número (1.0) es la cantidad de daño base, y el segundo (1.5) es el multiplicador de daño aplicado a los jugadores.

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 durante toda la vida de esta instancia del boss, por lo que los valores establecidos aquí sobreviven entre hooks.

  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) ejecuta el callback cada 100 ticks (5 segundos). El callback recibe un contexto fresco, por lo que loop_context.boss te da la instantánea más reciente del boss. El scheduler devuelve un ID de tarea numérico que almacenas en el estado.

  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

Si inicias una tarea repetitiva en on_enter_combat, siempre cancélala en on_exit_combat. Olvidar cancelar deja una tarea en segundo plano ejecutándose hasta que el boss desaparezca, lo que desperdicia rendimiento y puede causar comportamiento inesperado.


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

-- 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

-- 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!")

-- Set the global power cooldown so other powers on this boss
-- don't all fire at the same instant
context.cooldowns:set_global(40)
end
}

Explicación

  1. Guardia nil -- context.player es una clave perezosa que resuelve al jugador involucrado en el evento. En casos extremos raros (ej., el jugador se desconectó entre el disparo del evento y la ejecución del hook), puede ser nil. Siempre verifica antes de usarlo.

  2. Cooldown local -- context.cooldowns:check_local("fire_touch", 60) hace dos cosas atómicamente: verifica si la clave de cooldown "fire_touch" está lista y, si lo está, establece inmediatamente el cooldown a 60 ticks. Si el cooldown no está listo, devuelve false y la función sale temprano. La clave "fire_touch" está limitada a esta instancia del boss -- otros bosses con el mismo poder tienen cooldowns independientes.

  3. Ticks de fuego -- context.player:set_fire_ticks(60) prende fuego al jugador durante 60 ticks de juego (3 segundos). Esto llama directamente al método subyacente de Bukkit.

  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) pone todos los poderes de este boss en un cooldown de 40 ticks (2 segundos). Esto previene que múltiples poderes se activen simultáneamente.


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)
if context.state.aoe_task_id ~= nil then
return
end

context.state.aoe_task_id = context.scheduler:run_every(60, function(tick_context)
if not tick_context.boss.exists then
return
end

if not tick_context.cooldowns:check_local("pulse_aoe", 60) then
return
end

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

local victims = tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" })

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

tick_context.world:spawn_particle_at_location(
tick_context.boss:get_location(),
{ particle = "SPELL_MOB", amount = 40, speed = 0.1 }
)

tick_context.cooldowns:set_global(60)
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 repetitiva cada 60 ticks (3 segundos). La guardia al inicio previene iniciar un segundo bucle si se reingresa al combate.

  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 de todas las tablas de jugadores dentro de la esfera. La opción filter acepta "players", "elites", "mobs" o "living" (predeterminado).

  5. Causar daño -- victim:deal_custom_damage(4.0) causa 4 puntos de daño atribuidos al boss. Esto usa el sistema de daño personalizado de EliteMobs, que respeta la armadura y otros modificadores de combate.

  6. Feedback de partículas -- Partículas de polvo morado aparecen en cada víctima, y partículas ambientales aparecen en la posición del boss para señalar visualmente el pulso.

  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.

Archivo de poder completo (clic para expandir)
local function phase_one_attack(context)
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)
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

return {
api_version = 1,

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

on_enter_combat = function(context)
if context.state.attack_task_id ~= nil then return end
context.state.attack_task_id = context.scheduler:run_every(100, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("phase_attack", 100) then return end
phase_one_attack(tick_context)
end)
end,

on_game_tick = function(context)
if not context.cooldowns:check_local("phase_check", 20) then return end
if context.state.phase ~= 1 then return end
local health_ratio = context.boss.health / context.boss.maximum_health
if health_ratio <= 0.5 then
context.state.phase = 2
context.state.phase_switched = true
context.log:info("Boss entering phase 2 at " .. tostring(math.floor(health_ratio * 100)) .. "% health")
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end
context.boss:play_model_animation("transform")
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
context.state.attack_task_id = context.scheduler:run_every(40, function(tick_context)
if not tick_context.boss.exists then return end
if not tick_context.cooldowns:check_local("phase_attack", 40) then return end
phase_two_attack(tick_context)
end)
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, attack_task_id en nil y phase_switched en false. Estos valores persisten a través de todos los hooks para esta instancia del boss.

  2. Bucle de ataque fase 1 -- on_enter_combat inicia una tarea repetitiva que ejecuta phase_one_attack cada 100 ticks. El ataque crea una esfera de 5 bloques, daña jugadores dentro y reproduce una animación de golpe con partículas de explosión.

  3. Verificación de fase en on_game_tick -- on_game_tick se dispara en cada tick del servidor, por lo que lo primero que hace es verificar un cooldown de 20 ticks ("phase_check") para evitar ejecutar lógica costosa cada tick. Si el boss ya está en fase 2, sale temprano.

  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 más grande (8 bloques), causa ligeramente menos daño por golpe pero dispara mucho más frecuentemente, aplica un efecto de poción de lentitud y usa partículas de polvo rojo y un sonido de wither para una sensación diferente.

  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

on_game_tick se ejecuta cada tick del servidor (20 veces por segundo). Siempre protege el trabajo costoso detrás de una verificación de cooldown, como se muestra con check_local("phase_check", 20). Si tu hook excede 50ms, EliteMobs desactivará automáticamente el poder.


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.
  • Cooldowns locales y globales -- especifica el nombre de la clave y la duración en ticks.
  • Nombres de animación de modelo personalizado -- la IA no puede adivinarlos; proporciónelos.
  • 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.
  • Solo usa nombres de métodos documentados -- si no está en la Referencia de API, no existe.
  • Las especificaciones de utilidades de script usan nombres de campo de EliteScript -- targetType, shape, Target, Target2, range, offset, coverage.
  • Los callbacks del scheduler aceptan contexto fresco -- siempre usa el parámetro del callback dentro de callbacks.

Buen ejemplo de prompt

Escribe un poder Lua que use on_enter_combat para iniciar una tarea repetitiva cada 80 ticks. En cada tick, crea una zona de esfera nativa Lua (radio 6) centrada en el boss, consulta los jugadores dentro y causa 2.0 de daño personalizado a cada uno. Usa una clave de cooldown local "pulse" con duración 80. Cancela la tarea en on_exit_combat. Genera partículas DUST (red=0, green=255, blue=100) en cada víctima.

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 en la lista de hooks.
  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. Cada context.scheduler:run_every(...) tiene un context.scheduler:cancel_task(...) correspondiente en on_exit_combat.
  6. Los callbacks del scheduler usan el parámetro de contexto propio del callback, no el context externo.
  7. Las claves de cooldown son strings descriptivos y las duraciones están en ticks.
  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.
  10. Las tablas de utilidades de script usan nombres de campo de EliteScript (targetType, shape, Target, etc.).
  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 de enum de partículas válidos de Bukkit en UPPER_CASE.

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.

  • Manten las funciones auxiliares locales. Declara helpers como local function pick_action(context) encima de la tabla de retorno. Esto las mantiene fuera del scope global y evita colisiones con otros poderes Lua cargados en el mismo runtime.

  • Pon la geometria en utilidades de script. Si necesitas conos, rayos rotatorios, rayos en traslacion o zonas animadas, usa context.script:zone(...) con nombres de campo de EliteScript. Las utilidades de script reutilizan el motor de zonas probado de EliteScript.

  • Usa context.state para estado en tiempo de ejecucion. No uses variables globales de Lua. context.state esta limitado a una sola instancia de boss y persiste entre hooks durante la vida de ese boss.

  • Usa claves de cooldown locales con nombre. En lugar de numeros simples, usa claves descriptivas como "fire_touch" o "aoe_pulse". Esto facilita la depuracion y previene colisiones accidentales entre diferentes cooldowns en el mismo poder.

  • Manten on_game_tick ligero. Siempre protegelo detras de una verificacion de cooldown. Si tu logica se ejecuta cada tick, debe completarse en mucho menos de 50ms o el poder sera desactivado.

  • Cancela las tareas repetitivas cuando termines. Cada run_every debe tener un cancel_task correspondiente en on_exit_combat (y posiblemente on_death). Las tareas no canceladas desperdician CPU y pueden causar errores de referencia nula despues de que el boss desaparezca.

  • Usa contextos frescos en callbacks del scheduler. Los callbacks del scheduler (run_every, run_after) reciben un contexto fresco como parametro. Siempre usa ese parametro -- no el context externo -- porque el contexto externo puede contener snapshots obsoletos.

  • Registra transiciones de estado con context.log:info(). Durante el desarrollo, agrega logging para cambios de fase, inicios de cooldown e inicios/paradas del scheduler. Elimina o cambia a context.log:debug() antes de desplegar.

  • Reutiliza la documentacion existente de EliteScript. Las paginas de Zonas, Objetivos, Vectores relativos y Condiciones documentan los mismos nombres de campo que las utilidades de script aceptan. No dupliques esa informacion en tu poder Lua -- simplemente haz referencia a ella.


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.

  • Verificar context.player sin guardia nil. En hooks como on_player_damaged_by_boss, el jugador esta casi siempre disponible -- pero "casi siempre" no es "siempre". Una sola guardia nil faltante puede crashear el poder.

  • Usar nombres de campo estilo Lua en llamadas de utilidades de script. Las utilidades de script esperan targetType, Target, Target2, shape -- no target_type, target, target2, zone_shape. Los nombres no coincidentes silenciosamente no producen resultados.

  • Ejecutar logica pesada en on_game_tick sin gate de cooldown. Este hook se dispara cada tick del servidor. Incluso aritmetica simple repetida 20 veces por segundo en muchos bosses se acumula.

  • Inventar nombres de metodos. Si un metodo no esta listado en la Referencia de API, no existe. Errores comunes incluyen escribir entity:teleport(loc) en lugar de entity:teleport_to_location(loc), o player:set_velocity(vec) en lugar de player:set_velocity_vector(vec).

  • 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