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.
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
-
Creación de zona --
context.script:zone(...)crea una forma de cono usando los mismos nombres de campo que las Zonas de EliteScript.Targetestablece el origen del cono (el propio boss, desplazado 1 bloque hacia arriba), yTarget2establece el destino (los jugadores más cercanos dentro de 20 bloques).radiuscontrola qué tan ancho se abre el cono. -
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,amountyspeed. -
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.
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
-
Inicialización de estado --
on_spawnestablececontext.state.startedenfalseycontext.state.loop_task_idennil. La tablastatepersiste durante toda la vida de esta instancia del boss, por lo que los valores establecidos aquí sobreviven entre hooks. -
Guardia de combate --
on_enter_combatverificacontext.state.startedantes de iniciar el bucle. Esto previene múltiples bucles superpuestos si el evento se dispara más de una vez. -
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 queloop_context.bosste da la instantánea más reciente del boss. El scheduler devuelve un ID de tarea numérico que almacenas en el estado. -
Limpieza al salir --
on_exit_combatcancela 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.
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
-
Guardia nil --
context.playeres 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 sernil. Siempre verifica antes de usarlo. -
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, devuelvefalsey 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. -
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. -
Partículas --
context.world:spawn_particle_at_location(location, spec)genera partículas en una ubicación específica. La tabla de especificación aceptaparticle(nombre de enum de partícula de Bukkit),amountyspeed. -
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. -
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
-
Configuración de estado --
on_spawninicializaaoe_task_idennil. Este ID de tarea mantendrá la referencia de la tarea del scheduler repetitiva. -
Ataque repetitivo --
on_enter_combatinicia una tarea repetitiva cada 60 ticks (3 segundos). La guardia al inicio previene iniciar un segundo bucle si se reingresa al combate. -
Definición de zona -- La tabla
zone_defusa la sintaxis de zona nativa Lua. El campokindespecifica la forma ("sphere"),radiusestablece el tamaño, yoriginse 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. -
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ónfilteracepta"players","elites","mobs"o"living"(predeterminado). -
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. -
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.
-
Limpieza --
on_exit_combatcancela la tarea repetitiva y limpia el estado, siguiendo el mismo patrón que el ejemplo anterior.
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
-
Inicialización de estado --
on_spawnestablecephaseen1,attack_task_idennilyphase_switchedenfalse. Estos valores persisten a través de todos los hooks para esta instancia del boss. -
Bucle de ataque fase 1 --
on_enter_combatinicia una tarea repetitiva que ejecutaphase_one_attackcada 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. -
Verificación de fase en on_game_tick --
on_game_tickse 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. -
Umbral de salud --
context.boss.health / context.boss.maximum_healthda la salud actual como fracción. Cuando cae al 50% o menos, comienza la transición. -
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).
-
Ataques de fase 2 --
phase_two_attackusa 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. -
Registro --
context.log:info(...)escribe en la consola del servidor, lo cual es invaluable para depurar transiciones de fase durante el desarrollo. -
Limpieza --
on_exit_combatcancela cualquier bucle de ataque actualmente activo, independientemente de la fase en la que se encuentre el boss.
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_combatpara 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 enon_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:
- El archivo devuelve exactamente una tabla con
api_version = 1. - Cada nombre de hook coincide exactamente con una entrada en la lista de hooks.
context.playerse protege con== nilantes de usarse en hooks donde puede ser nil.- Los campos de
context.statese inicializan enon_spawn. - Cada
context.scheduler:run_every(...)tiene uncontext.scheduler:cancel_task(...)correspondiente enon_exit_combat. - Los callbacks del scheduler usan el parámetro de contexto propio del callback, no el
contextexterno. - Las claves de cooldown son strings descriptivos y las duraciones están en ticks.
- Los hooks
on_game_tickprotegen el trabajo costoso detrás de una verificación de cooldown. - Todos los nombres de métodos existen en la Referencia de API.
- Las tablas de utilidades de script usan nombres de campo de EliteScript (
targetType,shape,Target, etc.). - Las definiciones de zona nativas usan
kind,radius,origin,destination, etc. - El poder no llama operaciones bloqueantes o de larga duración dentro de un hook o callback.
- 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_spawnque 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.statepara estado en tiempo de ejecucion. No uses variables globales de Lua.context.stateesta 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_tickligero. 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_everydebe tener uncancel_taskcorrespondiente enon_exit_combat(y posiblementeon_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 elcontextexterno -- 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 acontext.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
contextexterno dentro de un callback del scheduler. El contexto externo captura un snapshot en el momento en que se ejecuto el hook. Dentro de un callbackrun_everyorun_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_everyenon_enter_combatpero 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 leescontext.state.phaseenon_game_tickpero nunca lo estableces enon_spawn, seranily tus comparaciones se comportaran inesperadamente. -
Verificar
context.playersin guardia nil. En hooks comoon_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-- notarget_type,target,target2,zone_shape. Los nombres no coincidentes silenciosamente no producen resultados. -
Ejecutar logica pesada en
on_game_ticksin 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 deentity:teleport_to_location(loc), oplayer:set_velocity(vec)en lugar deplayer:set_velocity_vector(vec). -
Usar
context.boss.healthpara establecer salud.context.boss.healthes un snapshot de solo lectura. Para curar al boss, usacontext.boss:restore_health(amount). -
Olvidar
api_version = 1. La tabla devuelta debe incluir este campo, o EliteMobs no cargara el poder.
Próximos pasos
- Primeros pasos -- estructura de archivos, hooks, primera explicación de poder
- Hooks y ciclo de vida -- referencia completa de hooks y contexto
- Boss y entidades -- métodos de boss, jugador y entidad
- Mundo y entorno -- partículas, sonidos, rayos, bloques
- Zonas y objetivos -- zonas nativas y zonas de utilidades de script
- Enums -- valores válidos para Particle, Sound, Material y otras constantes de cadena
- Solución de problemas -- problemas comunes, consejos de depuración y consejos de migración
