Scripting Lua: Scripts de NPC
Los scripts Lua de NPC de EliteMobs son archivos .lua independientes que se adjuntan a las configuraciones de NPC. Son distintos de los poderes Lua de jefe: los poderes de jefe viven en plugins/EliteMobs/powers/, mientras que los scripts de NPC viven en plugins/EliteMobs/npc_scripts/.
Los scripts de NPC usan el runtime compartido de MagmaCore, también utilizado por los poderes de jefe y los props de FreeMinecraftModels. Reciben servicios como context.world, context.zones, context.scheduler, context.cooldowns y context.log, además de una tabla context.npc específica. Los datos de jugador y evento dependen del hook. Los poderes de jefe ofrecen versiones propias de varias tablas; usa las firmas de la API compartida al escribir scripts de NPC.
Los scripts Lua de NPC siguen siendo experimentales. Los hooks específicos de NPC y los helpers de context.npc pueden cambiar. Las tablas compartidas (context.world, context.zones, context.scheduler, context.cooldowns, context.log, context.event, context.player) son las mismas que se documentan en el Motor de Scripting y en la Referencia de la API de Lua.
Ubicación de los archivos
Crea los archivos de script de NPC en:
plugins/
EliteMobs/
npc_scripts/
wave.lua
Las subcarpetas sí se escanean, de forma recursiva. Sin embargo, los scripts se registran solo por nombre de archivo, así que npc_scripts/wave.lua y npc_scripts/town/wave.lua colisionan -- mantén los nombres base únicos en todo el árbol.
El descubrimiento selecciona un archivo por nombre base, sin distinguir mayúsculas, en un orden estable por ruta absoluta. Registra las rutas seleccionadas y omitidas cuando las copias son distintas. Un archivo seleccionado que falla no se sustituye por un duplicado omitido. Las referencias de configuración deben conservar las mayúsculas exactas del archivo seleccionado.
La extensión .lua es opcional en una configuración de NPC: tanto - wave como - wave.lua resuelven a wave.lua. Si una configuración de NPC referencia un script que no existe, EliteMobs registra una advertencia y el NPC aparece igualmente.
Adjuntar scripts a los NPC
Añade una lista scripts: a la configuración del NPC:
scripts:
- wave.lua
Se pueden adjuntar varios scripts a un mismo NPC:
scripts:
- wave.lua
- greeting_particles.lua
Los scripts se ejecutan por prioridad, empezando por los valores de priority más bajos. El valor predeterminado es 0. Las prioridades iguales conservan el orden de la lista scripts:. Esto ordena el envío de eventos del NPC, no las tareas programadas independientes.
Forma del script
Todo script de NPC debe devolver una única tabla:
return {
api_version = 1,
priority = 0,
on_spawn = function(context)
context.state.spawned = true
context.npc:play_model_animation("idle")
end
}
Solo se aceptan estos campos de nivel superior:
| Campo | Tipo | Notas |
|---|---|---|
api_version | número | Obligatorio. Debe ser 1. |
priority | número | Opcional. Los valores más bajos se ejecutan primero. |
on_spawn | función | Se ejecuta después de que el NPC aparece. |
on_remove | función | Se ejecuta durante la eliminación normal del NPC, antes del cierre de los scripts y de eliminar el cuerpo. |
on_game_tick | función | Se ejecuta cada tick una vez inicializado el runtime del script y activado su reloj. Mantén el trabajo ligero. |
on_npc_interact | función | Se ejecuta cuando el manejador normal de interacción llega al paso de scripts; consulta las limitaciones más abajo. |
on_npc_proximity_enter | función | Se ejecuta una vez cuando un jugador entra en el radio de activación de este NPC. |
on_npc_proximity_leave | función | Se ejecuta una vez cuando un jugador sale del radio de activación de este NPC. |
on_zone_enter | función | Se ejecuta cuando un jugador entra en una zona que este script está vigilando (consulta context.zones). |
on_zone_leave | función | Se ejecuta cuando un jugador sale de una zona vigilada. |
La vigilancia de zonas solo rastrea jugadores -- los mobs y otras entidades nunca disparan on_zone_enter / on_zone_leave.
Las claves de nivel superior desconocidas se rechazan durante la carga del script. Las funciones auxiliares deben declararse como funciones local por encima de la tabla devuelta.
El cargador ejecuta el código a nivel de archivo para validarlo; después, cada instancia de script del NPC lo ejecuta de nuevo en su propio sandbox. No uses el contexto de los hooks durante la inicialización del archivo. Cada script tiene globales y context.state independientes, incluso en el mismo NPC.
La inicialización del runtime de NPC es diferida. Actualmente, el NPC construye cada instancia y envía on_spawn, pero no llama al método de inicio inmediato start(). Un archivo que solo contiene on_game_tick no empieza a ejecutarse por sí solo. Define on_spawn = function(context) end para inicializarlo al aparecer, o deja que otro hook declarado lo inicialice más adelante.
Un error Lua cierra la instancia afectada y cancela el trabajo que le pertenece. Los eventos posteriores descartan esa instancia cerrada sin recrearla. Recarga la definición corregida y crea una instancia nueva. El estado no persiste entre eliminaciones o reinicios; on_remove corresponde a la eliminación del NPC, no es un callback general de error o cierre.
Hooks de proximidad
Los hooks de proximidad de NPC usan el valor de configuración activationRadius del NPC.
| Hook | Se dispara cuando |
|---|---|
on_npc_proximity_enter | Un jugador pasa de estar fuera del radio de activación del NPC a estar dentro. |
on_npc_proximity_leave | Un jugador pasa de estar dentro del radio de activación del NPC a estar fuera. |
El escáner del servidor comprueba cada par NPC/jugador cada 100 ticks, normalmente cinco segundos a 20 TPS, mediante distancia esférica desde el cuerpo actual del NPC. Si el jugador ya está dentro en el primer escaneo, cuenta como entrada. Una visita breve entre escaneos puede pasar inadvertida. Estar cerca de un NPC no bloquea la entrada en otro, y permanecer detectado dentro no repite el evento.
Se omiten los NPC que están patrullando activamente o en edición. Un par detectado anteriormente puede generar una salida cuando comienza el movimiento y una entrada al reanudarse la detección. La eliminación y la desconexión del jugador no garantizan un hook de salida: se necesitan tanto el NPC registrado como un jugador conectado. Si no quedan NPC o jugadores conectados, se limpia el seguimiento sin eventos de salida. Un activationRadius menor o igual a cero desactiva la proximidad; el radio independiente de 30 bloques del indicador de misiones no amplía la activación Lua.
El comportamiento normal de saludo, diálogo e indicador de misiones sigue ejecutándose. El hook de Lua añade comportamiento por encima de eso.
API de scripting compartida
Como los scripts de NPC se ejecutan sobre el runtime unificado, cada hook de NPC también recibe las tablas de context compartidas de MagmaCore que usan los scripts de FreeMinecraftModels. Los poderes de jefe de EliteMobs se ejecutan sobre el mismo runtime pero usan variantes específicas de jefe para varias tablas. Las listas completas de métodos están en la Referencia de la API de Lua y en el Motor de Scripting:
| Tabla | Qué hace |
|---|---|
context.world | Efectos y consultas del mundo: strike_lightning, spawn_particle, play_sound, set_block_at, place_temporary_block, spawn_entity, spawn_firework, get_nearby_entities, get_nearby_players, raycast y más. Se aceptan tanto la forma con coordenadas (strike_lightning(x, y, z)) como la de tabla de ubicación (strike_lightning_at_location(loc)). |
context.zones | Crea zonas con create_sphere(x, y, z, radius), create_cylinder(x, y, z, radius, height) o create_cuboid(x, y, z, xSize, ySize, zSize). Devuelven un identificador numérico o nil si el propietario no tiene mundo. watch(handle, on_enter, on_leave) habilita los hooks superiores correspondientes cuando se proporciona una función; esa función no se llama directamente. unwatch(handle) elimina la zona y detiene el seguimiento. |
context.scheduler | run_later(ticks, fn), run_repeating(delay, interval, fn), cancel(task_id). |
context.cooldowns | Tiempos de reutilización compartidos de MagmaCore: local_ready, local_remaining, check_local, set_local, global_ready, set_global. |
context.log | info(msg), warn(msg), error(msg) — escribe en la consola del servidor. |
context.event | Datos del evento o del actor cuando el hook proporciona alguno. Consulta más abajo. |
context.player | El jugador que interactúa o dispara el hook, cuando está presente. Consulta más abajo. |
context.state | Una tabla Lua simple que persiste para esta instancia de script del NPC hasta que el NPC es eliminado. |
Las zonas compartidas usan coordenadas absolutas en el mundo del NPC al crearse; no siguen al NPC en movimiento. Crearlas registra inmediatamente un escaneo por tick de los jugadores del mundo actual del propietario, incluso antes de que watch instale los manejadores. Pasa una función para cada dirección que quieras habilitar e implementa el hook on_zone_enter o on_zone_leave correspondiente en la tabla devuelta. Esos hooks reciben context.player y una tabla de evento del actor, pero ningún identificador de zona. unwatch elimina el identificador por completo; crea otra zona para volver a vigilar. Los jugadores que ya están dentro en el primer escaneo cuentan como entradas; un jugador desconectado no puede recibir un callback de salida.
Ejemplo: fulminar al interactuar
return {
api_version = 1,
on_npc_interact = function(context)
-- NPC scripts can now reach the full world API.
context.world:strike_lightning_at_location(context.npc:get_location())
end
}
context.npc
context.npc está disponible en todos los hooks de NPC.
Campos
| Campo | Tipo | Notas |
|---|---|---|
name | cadena | Nombre visible del NPC según la configuración. |
filename | cadena | Nombre del archivo de configuración del NPC. |
uuid | cadena | UUID del NPC en tiempo de ejecución. |
activation_radius | número | Radio de activación configurado. |
current_location | tabla de ubicación | Instantánea de la ubicación cuando la entidad de respaldo existe. |
entity_type | cadena | Tipo de entidad de Bukkit cuando la entidad de respaldo existe. |
Métodos
| Método | Argumentos | Devuelve | Notas |
|---|---|---|---|
is_valid() | - | booleano | Si el NPC todavía tiene una entidad de respaldo válida. |
get_location() | - | tabla de ubicación | Ubicación actual del NPC, o la ubicación de aparición si la entidad no está disponible. |
get_eye_location() | - | tabla de ubicación | Ubicación actual de los ojos, o respaldo a la ubicación de aparición. |
get_activation_radius() | - | número | Radio de activación configurado actualmente. |
get_nearby_players(radius) | número | tabla | Envoltorios de jugador a distancia esférica en el mundo del NPC. Sin filtro de modo de juego; usa un radio no negativo. |
face_direction_or_location(target) | vector, ubicación o tabla de entidad | nil | Mira en una dirección o hacia una posición del mismo mundo. Las tablas con world o current_location se interpretan como posiciones; las coordenadas solas, como dirección. No hace nada mientras la patrulla se mueve activamente. |
say_greeting(player?) | jugador, UUID, nombre o nil | nil | Envía un saludo configurado. Por defecto usa el jugador que dispara el hook cuando está disponible. |
say_dialog(player?) | jugador, UUID, nombre o nil | nil | Envía el diálogo configurado. Por defecto usa el jugador que dispara el hook cuando está disponible. |
say_farewell(player?) | jugador, UUID, nombre o nil | nil | Envía el texto de despedida configurado. Por defecto usa el jugador que dispara el hook cuando está disponible. |
play_model_animation(name) | cadena | nil | Reproduce una animación de modelo personalizado si existe. En caso contrario no hace nada de forma segura. |
patrol_pause() | - | booleano | Pausa la patrulla configurada. |
patrol_resume() | - | booleano | Cancela una espera o marcha temporal y reanuda la patrulla. |
walk_to(x, y, z) | tres números | booleano | Camina hasta un desplazamiento desde la ubicación de aparición configurada y reanuda la patrulla. Las rutas largas se resuelven automáticamente. |
hold(x, y, z) | tres números | booleano | Camina hasta un desplazamiento desde el origen configurado y espera allí. |
teleport(x, y, z) | tres números | booleano | Se teletransporta a un desplazamiento desde el origen configurado si el destino procesa entidades. |
remove() | - | nil | Elimina el cuerpo actual mediante el proceso normal con motivo OTHER; no borra su ubicación configurada. No lo llames desde on_remove, pues volvería a entrar en la eliminación. |
Los métodos de movimiento devuelven false si el NPC no tiene una patrulla configurada o no se puede aceptar la solicitud. Consulta Patrullas de NPCs y jefes.
context.player
context.player está disponible en on_npc_interact, on_npc_proximity_enter, on_npc_proximity_leave y los hooks de entrada/salida de zonas vigiladas. Es nil en los hooks de ciclo de vida sin jugador.
Es el envoltorio compartido de jugador de MagmaCore, también usado por los scripts de FMM. Ofrece salud, efectos de poción, send_message, show_title, show_action_bar, get_held_item, raycasting y más. Los poderes de jefe tienen su propio envoltorio, por lo que sus métodos no son intercambiables. Consulta la Referencia de la API de Lua para la lista completa. Los miembros habituales son:
| Campo / Método | Notas |
|---|---|
name | Nombre del jugador. |
uuid | UUID del jugador. |
current_location | Ubicación leída en vivo en cada acceso al campo. Una tabla de ubicación guardada previamente sigue siendo una instantánea. |
get_eye_location() | Ubicación actual de los ojos del jugador. |
send_message(text) | Envía un mensaje de chat. Admite códigos de color. |
Comprueba siempre si context.player es nil antes de usarlo en funciones auxiliares compartidas.
entity_type va en minúsculasEn las tablas de entidad compartidas de MagmaCore, entity_type es el nombre de Bukkit en minúsculas ("player", "zombie"). Solo context.npc.entity_type y las tablas de entidad de los poderes de jefe de EliteMobs usan la forma en mayúsculas. Compara sin distinguir mayúsculas si un script tiene que funcionar con ambas.
Campos que EliteMobs añade a todas las tablas de entidad compartidas
Mientras EliteMobs está activo, añade campos a las tablas de entidad compartidas de MagmaCore, incluidos context.player, los envoltorios de context.npc:get_nearby_players(...) y los scripts de props de FreeMinecraftModels:
| Campo / Método | Tipo | Notas |
|---|---|---|
is_elite | booleano | true si EliteMobs rastrea la entidad como un elite |
is_custom_boss | booleano | true si es un jefe personalizado (siempre false cuando is_elite es false) |
is_significant_boss | booleano | true para un jefe personalizado cuyo multiplicador de salud supera 1. No comprueba si fue invocado como refuerzo. |
elite | tabla o nil | Presente solo en los elites. Ver más abajo |
elite_player | tabla o nil | En envoltorios compartidos de jugador creados por EliteMobs, ofrece tier cuando el inventario élite está cargado. Cada acceso crea una tabla nueva; una tabla guardada conserva el nivel anterior. |
can_receive_hostile_effect(actor_uuid) | booleano | Comprueba un actor jugador según las reglas compartidas de objetivos hostiles y la autorización de armas mágicas de EliteMobs. Pasa una cadena UUID válida. |
La subtabla elite:
| Campo / Método | Tipo | Notas |
|---|---|---|
elite.level | número | Nivel del elite |
elite.name | cadena o nil | Nombre visible del elite |
elite.health | número | Salud actual del elite (se lee en vivo) |
elite.max_health | número | Salud máxima del elite (se lee en vivo) |
elite.is_custom_boss | booleano | El mismo valor que el campo de nivel superior |
elite.health_multiplier | número | Multiplicador de salud configurado |
elite.damage_multiplier | número | Multiplicador de daño configurado |
elite:remove() | — | Elimina al elite |
-- Warn the approaching player if a real boss is loose near this NPC
on_npc_proximity_enter = function(context)
if context.player == nil then return end
local here = context.npc:get_location()
local nearby = context.world:get_nearby_entities(here.x, here.y, here.z, 40)
for i = 1, #nearby do
if nearby[i].is_significant_boss then
context.player:send_message("&cA boss is nearby: " .. tostring(nearby[i].elite.name))
return
end
end
end
Los envoltorios de poderes de jefe usan un constructor y un conjunto de campos distintos. Algunos nombres coinciden, como is_elite, pero la tabla compartida elite no es intercambiable con los métodos del poder de jefe. Consulta Jefes y entidades. FMM ofrece su propio adaptador de EliteMobs; no supongas que todos los campos adicionales de un envoltorio creado por EliteMobs existen también en FMM.
context.event
context.event es nil solo cuando el hook no tiene ni evento ni actor. Es la tabla de evento compartida de MagmaCore:
| Campo / Método | Notas |
|---|---|
is_cancelled | Instantánea de la cancelación al crear la tabla; false en eventos no cancelables. No cambia tras cancel() o uncancel(). |
cancel() | Solo existe si el evento es cancelable. Cancela ese evento. |
uncancel() | Solo existe si el evento es cancelable. Deshace su cancelación. |
player | El actor del evento (p. ej. el jugador que interactúa), como envoltorio de jugador, cuando está presente. |
Para el jugador que interactúa o entra en proximidad, prefiere context.player (está definido para esos hooks).
Actualmente, on_npc_interact proporciona el jugador sin un evento Bukkit. Por ello, su tabla contiene player e is_cancelled: false, pero no cancel ni uncancel. El script no puede cancelar el menú normal del NPC mediante esta tabla.
La ruta de interacción ignora los eventos Bukkit ya cancelados, bloquea etiquetas de nombre y NPC en edición, y limita las interacciones a una por tick por jugador. Si un objetivo de diálogo de misión consume la interacción, retorna antes del hook Lua. En los demás casos, el hook precede al menú o acción normal. Los eventos de proximidad tampoco son cancelables; los de zona proporcionan un actor sin evento Bukkit. Estas rutas tampoco ofrecen cancel() ni uncancel().
Estado, planificador y tiempos de reutilización
context.state pertenece a esta instancia individual y dura hasta que termina. Es independiente del estado de otros scripts del mismo NPC.
context.scheduler es el planificador compartido de MagmaCore. Funcionan tanto los nombres de MagmaCore como los nombres run_after / run_every de EliteMobs — son alias del mismo comportamiento:
| Método | Argumentos | Notas |
|---|---|---|
run_later(ticks, callback) / run_after(ticks, callback) | número, función | Se ejecuta una vez tras un retraso. Devuelve un ID de tarea. |
run_repeating(delay, interval, callback) | número, número, función | Se ejecuta repetidamente tras un retraso inicial. Devuelve un ID de tarea. |
run_every(interval, callback) | número, función | Se ejecuta cada interval ticks (retraso inicial 0). Devuelve un ID de tarea. |
cancel(task_id) / cancel_task(task_id) | número | Cancela una tarea propia. |
Los callbacks del planificador reciben un context nuevo. No reciben el context.player ni el context.event originales. Todas las tareas propias se cancelan automáticamente cuando el NPC es eliminado.
context.cooldowns es la tabla de tiempos de reutilización compartida de MagmaCore:
| Método | Argumentos | Devuelve | Notas |
|---|---|---|---|
local_ready(key?) | cadena | booleano | Verdadero cuando el tiempo de reutilización local ha expirado. |
local_remaining(key?) | cadena | número | Ticks restantes, o 0 cuando está listo. |
check_local(key?, duration) | cadena, número | booleano | Si está listo, inicia el tiempo de reutilización y devuelve verdadero. |
set_local(duration, key?) | número, cadena | nil | Establece o reinicia el tiempo de reutilización. |
global_ready() | - | booleano | Verdadero cuando el tiempo de reutilización global compartido está listo. |
set_global(duration) | número | nil | Inicia el tiempo de reutilización global. |
En check_local, la duración siempre es el segundo argumento: usa check_local(nil, 60) para la clave predeterminada. set_local recibe primero la duración. La clave predeterminada incluye el archivo del script. Las claves locales explícitas y el cooldown global se comparten entre los scripts del mismo adaptador de NPC; usa claves distintas salvo que quieras coordinarlos. Las duraciones usan tiempo monotónico transcurrido, a 50 ms por tick nominal, no ticks contados del servidor. Las duraciones no positivas borran el cooldown. Establecer uno global no bloquea automáticamente los hooks ni check_local; comprueba global_ready() tú mismo.
Los scripts de NPC ahora usan el orden de tiempos de reutilización compartido de MagmaCore (check_local(key?, duration)), el mismo que los poderes de jefe y los scripts de FreeMinecraftModels. Las compilaciones experimentales anteriores de NPC usaban check_local(duration, key?) — actualiza cualquier script antiguo al orden compartido.
Ejemplo: saludar al acercarse
Este script hace que el NPC mire al jugador cuya entrada se detectó y reproduzca wave. La entrada depende del escaneo de cinco segundos, no de cada paquete de movimiento. El cooldown con clave limita las repeticiones cuando se reciben varias entradas; no aumenta la frecuencia del escáner.
return {
api_version = 1,
priority = 0,
on_npc_proximity_enter = function(context)
if context.player == nil then return end
if context.cooldowns:check_local("wave:" .. context.player.uuid, 60) then
context.npc:face_direction_or_location(context.player.current_location)
context.npc:play_model_animation("wave")
end
end
}
play_model_animation(name) no hace nada de forma segura cuando el NPC no tiene modelo personalizado o el modelo no tiene esa animación.
Pautas de rendimiento
- Mantén ligeros los hooks
on_game_tick. Tras inicializarse, su reloj corre cada tick, normalmente 20 veces por segundo. Un script sin este hook puede seguir realizando escaneos de zona por tick o tareas programadas explícitas. - Prefiere
on_npc_proximity_enteryon_npc_proximity_leavepara el comportamiento de proximidad en lugar de consultar los jugadores cercanos en cada tick. - Usa
context.cooldowns:check_local(...)para limitar animaciones, sonidos y ráfagas de partículas. - Usa
context.scheduler:run_repeating(...)con un intervalo razonable cuando un comportamiento no necesite ejecutarse en cada tick. - Evita las búsquedas grandes en Lua.
context.npc:get_nearby_players(radius)está bien para comprobaciones locales pequeñas, pero los escaneos amplios deben quedarse en el runtime del plugin.
Páginas relacionadas
- Creando NPCs -- campos de configuración de NPC, incluido
activationRadius - Primeros Pasos con Lua -- poderes Lua de jefe
- Motor de Scripting -- conceptos compartidos de Lua y el runtime unificado
- Referencia de la API de Lua -- lista completa de métodos para
context.world,context.player,context.zonesy más
