Saltar al contenido principal

Scripting Lua: Scripts de NPC

webapp_banner.jpg

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.

Función experimental

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:

CampoTipoNotas
api_versionnúmeroObligatorio. Debe ser 1.
prioritynúmeroOpcional. Los valores más bajos se ejecutan primero.
on_spawnfunciónSe ejecuta después de que el NPC aparece.
on_removefunciónSe ejecuta durante la eliminación normal del NPC, antes del cierre de los scripts y de eliminar el cuerpo.
on_game_tickfunciónSe ejecuta cada tick una vez inicializado el runtime del script y activado su reloj. Mantén el trabajo ligero.
on_npc_interactfunciónSe ejecuta cuando el manejador normal de interacción llega al paso de scripts; consulta las limitaciones más abajo.
on_npc_proximity_enterfunciónSe ejecuta una vez cuando un jugador entra en el radio de activación de este NPC.
on_npc_proximity_leavefunciónSe ejecuta una vez cuando un jugador sale del radio de activación de este NPC.
on_zone_enterfunciónSe ejecuta cuando un jugador entra en una zona que este script está vigilando (consulta context.zones).
on_zone_leavefunciónSe 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.

HookSe dispara cuando
on_npc_proximity_enterUn jugador pasa de estar fuera del radio de activación del NPC a estar dentro.
on_npc_proximity_leaveUn 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:

TablaQué hace
context.worldEfectos 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.zonesCrea 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.schedulerrun_later(ticks, fn), run_repeating(delay, interval, fn), cancel(task_id).
context.cooldownsTiempos de reutilización compartidos de MagmaCore: local_ready, local_remaining, check_local, set_local, global_ready, set_global.
context.loginfo(msg), warn(msg), error(msg) — escribe en la consola del servidor.
context.eventDatos del evento o del actor cuando el hook proporciona alguno. Consulta más abajo.
context.playerEl jugador que interactúa o dispara el hook, cuando está presente. Consulta más abajo.
context.stateUna 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​

CampoTipoNotas
namecadenaNombre visible del NPC según la configuración.
filenamecadenaNombre del archivo de configuración del NPC.
uuidcadenaUUID del NPC en tiempo de ejecución.
activation_radiusnúmeroRadio de activación configurado.
current_locationtabla de ubicaciónInstantánea de la ubicación cuando la entidad de respaldo existe.
entity_typecadenaTipo de entidad de Bukkit cuando la entidad de respaldo existe.

Métodos​

MétodoArgumentosDevuelveNotas
is_valid()-booleanoSi el NPC todavía tiene una entidad de respaldo válida.
get_location()-tabla de ubicaciónUbicación actual del NPC, o la ubicación de aparición si la entidad no está disponible.
get_eye_location()-tabla de ubicaciónUbicación actual de los ojos, o respaldo a la ubicación de aparición.
get_activation_radius()-númeroRadio de activación configurado actualmente.
get_nearby_players(radius)númerotablaEnvoltorios 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 entidadnilMira 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 nilnilEnvía un saludo configurado. Por defecto usa el jugador que dispara el hook cuando está disponible.
say_dialog(player?)jugador, UUID, nombre o nilnilEnvía el diálogo configurado. Por defecto usa el jugador que dispara el hook cuando está disponible.
say_farewell(player?)jugador, UUID, nombre o nilnilEnvía el texto de despedida configurado. Por defecto usa el jugador que dispara el hook cuando está disponible.
play_model_animation(name)cadenanilReproduce una animación de modelo personalizado si existe. En caso contrario no hace nada de forma segura.
patrol_pause()-booleanoPausa la patrulla configurada.
patrol_resume()-booleanoCancela una espera o marcha temporal y reanuda la patrulla.
walk_to(x, y, z)tres númerosbooleanoCamina 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úmerosbooleanoCamina hasta un desplazamiento desde el origen configurado y espera allí.
teleport(x, y, z)tres númerosbooleanoSe teletransporta a un desplazamiento desde el origen configurado si el destino procesa entidades.
remove()-nilElimina 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étodoNotas
nameNombre del jugador.
uuidUUID del jugador.
current_locationUbicació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.

Aquí entity_type va en minúsculas

En 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étodoTipoNotas
is_elitebooleanotrue si EliteMobs rastrea la entidad como un elite
is_custom_bossbooleanotrue si es un jefe personalizado (siempre false cuando is_elite es false)
is_significant_bossbooleanotrue para un jefe personalizado cuyo multiplicador de salud supera 1. No comprueba si fue invocado como refuerzo.
elitetabla o nilPresente solo en los elites. Ver más abajo
elite_playertabla o nilEn 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)booleanoComprueba 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étodoTipoNotas
elite.levelnúmeroNivel del elite
elite.namecadena o nilNombre visible del elite
elite.healthnúmeroSalud actual del elite (se lee en vivo)
elite.max_healthnúmeroSalud máxima del elite (se lee en vivo)
elite.is_custom_bossbooleanoEl mismo valor que el campo de nivel superior
elite.health_multipliernúmeroMultiplicador de salud configurado
elite.damage_multipliernúmeroMultiplicador 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étodoNotas
is_cancelledInstantá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.
playerEl 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étodoArgumentosNotas
run_later(ticks, callback) / run_after(ticks, callback)número, funciónSe ejecuta una vez tras un retraso. Devuelve un ID de tarea.
run_repeating(delay, interval, callback)número, número, funciónSe ejecuta repetidamente tras un retraso inicial. Devuelve un ID de tarea.
run_every(interval, callback)número, funciónSe ejecuta cada interval ticks (retraso inicial 0). Devuelve un ID de tarea.
cancel(task_id) / cancel_task(task_id)númeroCancela 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étodoArgumentosDevuelveNotas
local_ready(key?)cadenabooleanoVerdadero cuando el tiempo de reutilización local ha expirado.
local_remaining(key?)cadenanúmeroTicks restantes, o 0 cuando está listo.
check_local(key?, duration)cadena, númerobooleanoSi está listo, inicia el tiempo de reutilización y devuelve verdadero.
set_local(duration, key?)número, cadenanilEstablece o reinicia el tiempo de reutilización.
global_ready()-booleanoVerdadero cuando el tiempo de reutilización global compartido está listo.
set_global(duration)númeronilInicia 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.

API de tiempos de reutilización unificada

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_enter y on_npc_proximity_leave para 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.