Aller au contenu principal

Scripting Lua : Scripts de PNJ

webapp_banner.jpg

Les scripts Lua de PNJ d'EliteMobs sont des fichiers .lua autonomes qui s'attachent aux configurations de PNJ. Ils sont distincts des pouvoirs Lua de boss : les pouvoirs de boss se trouvent dans plugins/EliteMobs/powers/, tandis que les scripts de PNJ se trouvent dans plugins/EliteMobs/npc_scripts/.

Les scripts de PNJ utilisent le runtime partagé MagmaCore, également utilisé par les pouvoirs de boss et les props FreeMinecraftModels. Ils reçoivent context.world, context.zones, context.scheduler, context.cooldowns et context.log, ainsi qu'une table context.npc propre aux PNJ. Les données du joueur et de l'événement dépendent du hook. Les pouvoirs de boss fournissent leurs propres versions de plusieurs tables ; utilisez les signatures de l'API partagée pour les scripts de PNJ.

Fonctionnalité expérimentale

Les scripts Lua de PNJ sont encore expérimentaux. Les hooks spécifiques aux PNJ et les utilitaires context.npc peuvent changer. Les tables partagées (context.world, context.zones, context.scheduler, context.cooldowns, context.log, context.event, context.player) sont les mêmes que celles documentées dans le Moteur de Scripting et la Référence de l'API Lua.


Emplacement des fichiers​

Créez les fichiers de scripts de PNJ dans :

plugins/
EliteMobs/
npc_scripts/
wave.lua

Les sous-dossiers sont analysés, de manière récursive. Cependant, les scripts sont enregistrés par nom de fichier seul, donc npc_scripts/wave.lua et npc_scripts/town/wave.lua entrent en collision -- gardez des noms de base uniques dans toute l'arborescence.

La découverte sélectionne un fichier par nom de base, sans tenir compte de la casse, dans un ordre stable de chemins absolus. Elle consigne les chemins retenus et ignorés lorsque les copies diffèrent. Un fichier sélectionné défectueux n'est pas remplacé par un doublon ignoré. Les références de configuration doivent conserver la casse exacte du fichier retenu.

L'extension .lua est optionnelle dans une configuration de PNJ : - wave et - wave.lua se résolvent tous deux en wave.lua. Si une configuration de PNJ référence un script inexistant, EliteMobs enregistre un avertissement et le PNJ apparaît quand même.


Attacher des scripts aux PNJ​

Ajoutez une liste scripts: à la configuration du PNJ :

scripts:
- wave.lua

Plusieurs scripts peuvent être attachés à un même PNJ :

scripts:
- wave.lua
- greeting_particles.lua

Les scripts s'exécutent par priorité croissante ; priority vaut 0 par défaut. À priorité égale, l'ordre de la liste scripts: est conservé. Cela ordonne la transmission des événements du PNJ, pas les tâches planifiées indépendantes.


Forme d'un script​

Chaque script de PNJ doit retourner une seule table :

return {
api_version = 1,
priority = 0,

on_spawn = function(context)
context.state.spawned = true
context.npc:play_model_animation("idle")
end
}

Seuls ces champs de premier niveau sont acceptés :

ChampTypeNotes
api_versionnumberRequis. Doit valoir 1.
prioritynumberOptionnel. Les valeurs les plus basses s'exécutent en premier.
on_spawnfunctionS'exécute après l'apparition du PNJ.
on_removefunctionS'exécute pendant le retrait normal du PNJ, avant l'arrêt des scripts et le retrait du corps.
on_game_tickfunctionS'exécute chaque tick une fois le runtime du script initialisé et son horloge active. Gardez ce traitement léger.
on_npc_interactfunctionS'exécute quand le gestionnaire d'interaction atteint l'étape des scripts ; voir les limites ci-dessous.
on_npc_proximity_enterfunctionS'exécute une fois lorsqu'un joueur entre dans le rayon d'activation de ce PNJ.
on_npc_proximity_leavefunctionS'exécute une fois lorsqu'un joueur quitte le rayon d'activation de ce PNJ.
on_zone_enterfunctionS'exécute lorsqu'un joueur entre dans une zone surveillée par ce script (voir context.zones).
on_zone_leavefunctionS'exécute lorsqu'un joueur quitte une zone surveillée.

La surveillance de zone ne suit que les joueurs -- les mobs et autres entités ne déclenchent jamais on_zone_enter / on_zone_leave.

Les clés de premier niveau inconnues sont rejetées lors du chargement du script. Les fonctions auxiliaires doivent être déclarées comme fonctions local au-dessus de la table retournée.

Le chargeur exécute le code au niveau du fichier pour le valider, puis chaque instance de script du PNJ l'exécute de nouveau dans son propre sandbox. N'utilisez pas le contexte des hooks pendant cette initialisation. Chaque script possède ses propres globales et context.state, même sur un PNJ commun.

L'initialisation du runtime des PNJ est différée. Le PNJ construit actuellement chaque instance et transmet on_spawn, sans appeler la méthode de démarrage immédiat start(). Un fichier contenant uniquement on_game_tick ne commence donc pas à fonctionner seul. Définissez on_spawn = function(context) end pour l'initialiser à l'apparition, ou laissez un autre hook déclaré l'initialiser plus tard.

Une erreur Lua ferme l'instance concernée et annule le travail qu'elle possède. Les événements suivants écartent cette instance fermée sans la recréer. Rechargez la définition corrigée et créez une nouvelle instance. L'état n'est pas conservé entre les retraits ou redémarrages ; on_remove est un hook de retrait du PNJ, pas un callback général d'erreur ou d'arrêt.


Hooks de proximité​

Les hooks de proximité des PNJ utilisent la valeur de configuration activationRadius du PNJ.

HookSe déclenche quand
on_npc_proximity_enterUn joueur passe de l'extérieur du rayon d'activation du PNJ à l'intérieur.
on_npc_proximity_leaveUn joueur passe de l'intérieur du rayon d'activation du PNJ à l'extérieur.

Le scanner du serveur vérifie chaque paire PNJ/joueur toutes les 100 ticks, normalement cinq secondes à 20 TPS, selon la distance sphérique depuis le corps actuel du PNJ. Un joueur déjà présent au premier passage compte comme une entrée. Une visite brève entre deux passages peut être manquée. Se tenir près d'un PNJ ne bloque pas l'entrée auprès d'un autre, et rester détecté à l'intérieur ne répète pas l'événement.

Les PNJ en déplacement actif de patrouille ou en édition sont ignorés. Une paire précédemment détectée peut donc produire une sortie au début du mouvement, puis une entrée à la reprise de la détection. Retrait et déconnexion ne garantissent pas un hook de sortie : il faut encore le PNJ suivi et un joueur connecté. L'absence de PNJ ou de joueurs connectés efface le suivi sans événements de sortie. Un activationRadius inférieur ou égal à zéro désactive la proximité ; la portée distincte de 30 blocs des indicateurs de quête n'étend pas l'activation Lua.

Le comportement normal de salutation, de dialogue et d'indicateur de quête continue de s'exécuter. Le hook Lua ajoute du comportement par-dessus.


API de scripting partagée​

Comme les scripts de PNJ s'exécutent sur le runtime unifié, chaque hook de PNJ reçoit également les tables de context MagmaCore partagées utilisées par les scripts FreeMinecraftModels. Les pouvoirs de boss d'EliteMobs s'exécutent sur le même runtime mais utilisent des variantes spécifiques aux boss pour plusieurs tables. Les listes complètes de méthodes se trouvent dans la Référence de l'API Lua et le Moteur de Scripting :

TableCe qu'elle fait
context.worldEffets et requêtes sur le monde : strike_lightning, spawn_particle, play_sound, set_block_at, place_temporary_block, spawn_entity, spawn_firework, get_nearby_entities, get_nearby_players, raycast, et plus encore. Les deux formes sont acceptées : par coordonnées (strike_lightning(x, y, z)) et par table de localisation (strike_lightning_at_location(loc)).
context.zonesCrée des zones avec create_sphere(x, y, z, radius), create_cylinder(x, y, z, radius, height) ou create_cuboid(x, y, z, xSize, ySize, zSize). Renvoie un identifiant numérique, ou nil sans monde du propriétaire. watch(handle, on_enter, on_leave) active les hooks correspondants si une fonction est fournie ; cette fonction n'est pas appelée directement. unwatch(handle) supprime la zone et arrête le suivi.
context.schedulerrun_later(ticks, fn), run_repeating(delay, interval, fn), cancel(task_id).
context.cooldownsTemps de recharge partagés MagmaCore : local_ready, local_remaining, check_local, set_local, global_ready, set_global.
context.loginfo(msg), warn(msg), error(msg) — écrit dans la console du serveur.
context.eventDonnées de l'événement ou de l'acteur quand le hook en fournit. Voir ci-dessous.
context.playerLe joueur qui interagit ou déclenche, lorsqu'il est présent. Voir ci-dessous.
context.stateUne table Lua simple qui persiste pour cette instance de script de PNJ jusqu'à la suppression du PNJ.

Les zones partagées utilisent des coordonnées absolues dans le monde du PNJ à leur création ; elles ne suivent pas ses déplacements. Leur création enregistre immédiatement un scan par tick des joueurs du monde actuel du propriétaire, avant même que watch installe les traitements. Passez une fonction pour chaque direction à activer et implémentez le hook on_zone_enter ou on_zone_leave correspondant dans la table retournée. Ces hooks reçoivent context.player et une table d'événement d'acteur, mais aucun identifiant de zone. unwatch supprime entièrement l'identifiant ; créez une autre zone pour reprendre la surveillance. Les joueurs déjà présents au premier scan comptent comme des entrées ; un joueur déconnecté ne reçoit pas de callback de sortie.

Exemple : foudroyer lors d'une interaction​

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 dans chaque hook de PNJ.

Champs​

ChampTypeNotes
namestringNom d'affichage du PNJ défini dans la configuration.
filenamestringNom du fichier de configuration du PNJ.
uuidstringUUID du PNJ à l'exécution.
activation_radiusnumberRayon d'activation configuré.
current_locationtable de localisationInstantané de la localisation lorsque l'entité sous-jacente existe.
entity_typestringType d'entité Bukkit lorsque l'entité sous-jacente existe.

Méthodes​

MéthodeArgumentsRetourneNotes
is_valid()-booleanIndique si le PNJ possède encore une entité sous-jacente valide.
get_location()-table de localisationLocalisation actuelle du PNJ, ou localisation d'apparition si l'entité n'est pas disponible.
get_eye_location()-table de localisationLocalisation actuelle des yeux, ou repli sur la localisation d'apparition.
get_activation_radius()-numberRayon d'activation actuellement configuré.
get_nearby_players(radius)numbertableWrappers de joueurs à distance sphérique dans le monde du PNJ. Aucun filtre de mode de jeu ; utilisez un rayon positif ou nul.
face_direction_or_location(target)vecteur, position ou table d'entiténilOriente vers une direction ou une position du même monde. Une table avec world ou current_location est une position ; des coordonnées seules sont une direction. Sans effet pendant un déplacement actif de patrouille.
say_greeting(player?)joueur, UUID, nom ou nilnilEnvoie une salutation configurée. Par défaut, le joueur déclencheur lorsqu'il est disponible.
say_dialog(player?)joueur, UUID, nom ou nilnilEnvoie le dialogue configuré. Par défaut, le joueur déclencheur lorsqu'il est disponible.
say_farewell(player?)joueur, UUID, nom ou nilnilEnvoie le texte d'adieu configuré. Par défaut, le joueur déclencheur lorsqu'il est disponible.
play_model_animation(name)stringnilJoue une animation de modèle personnalisé si elle existe. Sans effet et sans erreur sinon.
patrol_pause()-booléenMet en pause la patrouille configurée.
patrol_resume()-booléenAnnule une attente ou un déplacement temporaire et reprend la patrouille.
walk_to(x, y, z)trois nombresbooléenMarche jusqu'à un décalage depuis l'apparition configurée puis reprend la patrouille. Les longs trajets sont résolus automatiquement.
hold(x, y, z)trois nombresbooléenMarche jusqu'à un décalage depuis l'origine configurée et y reste.
teleport(x, y, z)trois nombresbooléenSe téléporte à un décalage depuis l'origine configurée si la destination traite les entités.
remove()-nilRetire le corps actuel par le cycle normal avec le motif OTHER ; ne supprime pas son placement configuré. Ne pas appeler depuis on_remove, ce qui relancerait le retrait.

Les méthodes de déplacement renvoient false si le PNJ n'a pas de patrouille configurée ou si la demande ne peut pas être acceptée. Consultez Patrouilles des PNJ et boss.


context.player​

context.player est disponible dans on_npc_interact, on_npc_proximity_enter, on_npc_proximity_leave et les hooks d'entrée/sortie des zones surveillées. Il vaut nil dans les hooks de cycle de vie sans joueur.

C'est le wrapper de joueur partagé MagmaCore, également utilisé par FMM. Il fournit vie, effets de potion, send_message, show_title, show_action_bar, get_held_item, raycasting et d'autres méthodes. Les pouvoirs de boss ont leur propre wrapper ; leurs méthodes ne sont pas interchangeables. Voir la Référence de l'API Lua pour la liste complète. Les membres courants sont :

Champ / MéthodeNotes
nameNom du joueur.
uuidUUID du joueur.
current_locationPosition du joueur lue en direct à chaque accès. Une table de position enregistrée auparavant reste un instantané.
get_eye_location()Localisation actuelle des yeux du joueur.
send_message(text)Envoie un message dans le chat. Prend en charge les codes couleur.

Vérifiez toujours que context.player n'est pas nil avant de l'utiliser dans des fonctions auxiliaires partagées.

Ici, entity_type est en minuscules

Sur les tables d'entité partagées MagmaCore, entity_type est le nom Bukkit en minuscules ("player", "zombie"). Seuls context.npc.entity_type et les tables d'entité des pouvoirs de boss EliteMobs utilisent la forme en majuscules. Comparez sans tenir compte de la casse si un script doit gérer les deux.

Champs EliteMobs ajoutés à chaque table d'entité partagée​

Pendant son fonctionnement, EliteMobs ajoute des champs aux tables d'entité partagées MagmaCore, notamment context.player, les wrappers de context.npc:get_nearby_players(...) et les scripts de props FreeMinecraftModels :

Champ / MéthodeTypeNotes
is_elitebooléentrue si EliteMobs suit l'entité comme un élite
is_custom_bossbooléentrue s'il s'agit d'un Custom Boss (toujours false lorsque is_elite vaut false)
is_significant_bossbooléentrue pour un Custom Boss dont le multiplicateur de santé dépasse 1. Ne vérifie pas s'il a été invoqué comme renfort.
elitetable ou nilPrésent uniquement sur les élites. Voir ci-dessous
elite_playertable ou nilSur les wrappers partagés de joueur construits par EliteMobs, expose tier si l'inventaire élite est chargé. Chaque accès crée une table neuve ; une table enregistrée garde son ancienne valeur.
can_receive_hostile_effect(actor_uuid)booléenVérifie un acteur joueur selon les règles partagées de cible hostile et l'autorisation des armes magiques EliteMobs. Fournissez une chaîne UUID valide.

La sous-table elite :

Champ / MéthodeTypeNotes
elite.levelnombreNiveau de l'élite
elite.namechaîne ou nilNom d'affichage de l'élite
elite.healthnombreSanté actuelle de l'élite (lue en direct)
elite.max_healthnombreSanté maximale de l'élite (lue en direct)
elite.is_custom_bossbooléenMême valeur que le champ de premier niveau
elite.health_multipliernombreMultiplicateur de santé configuré
elite.damage_multipliernombreMultiplicateur de dégâts configuré
elite:remove()—Fait disparaître l'élite
-- 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

Les wrappers des pouvoirs de boss ont leur propre constructeur et un autre ensemble de champs. Certains noms se recoupent, comme is_elite, mais la table partagée elite n'est pas interchangeable avec les méthodes des pouvoirs de boss. Voir Boss et entités. FMM fournit son propre adaptateur EliteMobs ; ne supposez pas que tous les champs supplémentaires d'un wrapper construit par EliteMobs existent dans FMM.


context.event​

context.event vaut nil uniquement quand le hook n'a ni événement ni acteur. C'est la table d'événement partagée MagmaCore :

Champ / MéthodeNotes
is_cancelledInstantané de l'annulation à la création de la table ; false pour les événements non annulables. Ne change pas après cancel() ou uncancel().
cancel()Présent uniquement si l'événement est annulable. Annule cet événement.
uncancel()Présent uniquement si l'événement est annulable. Efface son annulation.
playerL'acteur de l'événement (par exemple le joueur qui interagit), sous forme de wrapper de joueur, lorsqu'il est présent.

Pour le joueur qui interagit ou déclenche la proximité, préférez context.player (il est défini pour ces hooks).

on_npc_interact fournit actuellement le joueur sans événement Bukkit. Sa table contient donc player et is_cancelled: false, mais pas cancel ni uncancel. Le script ne peut pas annuler le menu normal du PNJ via cette table.

Le chemin d'interaction ignore les événements Bukkit déjà annulés, bloque les étiquettes de nom et les PNJ en édition, et limite les interactions à une par tick et par joueur. Un objectif de dialogue de quête qui consomme l'interaction retourne avant le hook Lua. Sinon, le hook précède le menu ou l'action normale. Les événements de proximité ne sont pas annulables non plus ; les hooks de zone fournissent un acteur sans événement Bukkit. Ces chemins n'offrent donc pas cancel() ou uncancel().


État, planificateur et temps de recharge​

context.state appartient à cette instance individuelle et dure jusqu'à sa fin. Il est séparé de l'état des autres scripts du même PNJ.

context.scheduler est le planificateur partagé MagmaCore. Les noms MagmaCore et les noms EliteMobs run_after / run_every fonctionnent tous les deux — ce sont des alias du même comportement :

MéthodeArgumentsNotes
run_later(ticks, callback) / run_after(ticks, callback)number, functionS'exécute une fois après un délai. Retourne un ID de tâche.
run_repeating(delay, interval, callback)number, number, functionS'exécute de façon répétée après un délai initial. Retourne un ID de tâche.
run_every(interval, callback)number, functionS'exécute toutes les interval ticks (délai initial de 0). Retourne un ID de tâche.
cancel(task_id) / cancel_task(task_id)numberAnnule une tâche dont le script est propriétaire.

Les callbacks du planificateur reçoivent un context neuf. Ils ne reçoivent pas le context.player ni le context.event d'origine. Toutes les tâches détenues sont annulées automatiquement lorsque le PNJ est supprimé.

context.cooldowns est la table de temps de recharge partagée MagmaCore :

MéthodeArgumentsRetourneNotes
local_ready(key?)stringbooleanVrai lorsque le temps de recharge local a expiré.
local_remaining(key?)stringnumberTicks restants, ou 0 lorsque prêt.
check_local(key?, duration)string, numberbooleanSi prêt, démarre le temps de recharge et retourne vrai.
set_local(duration, key?)number, stringnilDéfinit ou réinitialise le temps de recharge.
global_ready()-booleanVrai lorsque le temps de recharge global partagé est prêt.
set_global(duration)numbernilDémarre le temps de recharge global.

Dans check_local, la durée est toujours le deuxième argument : utilisez check_local(nil, 60) pour la clé par défaut. set_local prend la durée en premier. La clé par défaut comprend le nom du fichier. Les clés locales explicites et le délai global sont partagés par les scripts du même adaptateur de PNJ ; choisissez des clés distinctes sauf si cette coordination est voulue. Les durées utilisent le temps monotone écoulé, à 50 ms par tick nominal, pas les ticks réellement comptés du serveur. Une durée nulle ou négative efface le délai. Définir un délai global ne bloque automatiquement ni les hooks ni check_local ; vérifiez global_ready() vous-même.

API de temps de recharge unifiée

Les scripts de PNJ utilisent désormais l'ordre d'arguments partagé de MagmaCore (check_local(key?, duration)), le même que les pouvoirs de boss et les scripts FreeMinecraftModels. Les anciens builds expérimentaux de PNJ utilisaient check_local(duration, key?) — mettez à jour vos anciens scripts vers l'ordre partagé.


Exemple : saluer à l'approche​

Ce script fait regarder au PNJ le joueur dont l'entrée a été détectée et joue wave. L'entrée dépend du scan toutes les cinq secondes, pas de chaque paquet de déplacement. Le délai associé à une clé limite les répétitions en cas de notifications d'entrée successives ; il n'accélère pas le scanner.

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) ne fait rien, sans erreur, lorsque le PNJ n'a pas de modèle personnalisé ou que le modèle ne possède pas cette animation.


Recommandations de performance​

  • Gardez les hooks on_game_tick légers. Après initialisation, leur horloge fonctionne chaque tick serveur, normalement 20 fois par seconde. Un script sans ce hook peut encore effectuer des scans de zone par tick ou des tâches planifiées explicites.
  • Préférez on_npc_proximity_enter et on_npc_proximity_leave pour les comportements de proximité plutôt que d'interroger les joueurs proches à chaque tick.
  • Utilisez context.cooldowns:check_local(...) pour limiter les animations, les sons et les rafales de particules.
  • Utilisez context.scheduler:run_repeating(...) avec un intervalle raisonnable lorsqu'un comportement n'a pas besoin de s'exécuter à chaque tick.
  • Évitez les recherches volumineuses en Lua. context.npc:get_nearby_players(radius) convient pour de petites vérifications locales, mais les balayages larges doivent rester dans le runtime du plugin.