Aller au contenu principal

Scripting Lua : Hooks et Cycle de Vie

webapp_banner.jpg

Cette page couvre chaque hook qu'un pouvoir Lua peut définir, l'ordre d'exécution des hooks, comment chaque boss obtient son propre environnement d'exécution isolé, et quelles fonctions de la bibliothèque standard sont disponibles dans le sandbox.

Si vous n'avez pas encore écrit de pouvoir Lua, commencez par Pour Commencer.

Hooks des pouvoirs de boss

Cette page documente les hooks des pouvoirs Lua de boss dans plugins/EliteMobs/powers/. Les scripts Lua de PNJ utilisent leur propre dossier plugins/EliteMobs/npc_scripts/ et des hooks spécifiques aux PNJ comme on_npc_interact et on_npc_proximity_enter ; voir Scripts de PNJ.


Référence des Hooks​

Chaque fichier de pouvoir Lua renvoie une table. Chaque clé de cette table (en dehors de api_version et priority) doit être l'un des hooks listés ci-dessous. L'environnement d'exécution appelle la fonction correspondante chaque fois que l'événement de jeu correspondant se déclenche.

HookSe déclenche quandcontext.player disponible ?
on_spawnL'elite mob apparaîtNon
on_game_tickUne fois par tick lorsque l'horloge du runtime est active et non suspendue ; normalement toutes les 50 ms à 20 TPSNon
on_boss_damagedLe boss subit des dégâts de n'importe quelle sourceNon
on_boss_damaged_by_playerLe boss subit des dégâts d'un joueurOui
on_reinforcement_damaged_by_playerUn renfort invoqué par ce boss subit les dégâts d'un joueur ; le hook s'exécute sur les pouvoirs de l'invocateurOui
on_boss_damaged_by_eliteLe boss subit des dégâts d'un autre elite mobNon
on_player_damaged_by_bossUn joueur subit des dégâts de ce bossOui
on_enter_combatLe boss entre en combatOui
on_exit_combatLe boss quitte le combatNon
on_healLe boss se soigneNon
on_boss_target_changedEliteMobs signale un événement ciblant un joueur, y compris l'acquisition initiale ; pas chaque changement ou perte de cibleOui
on_deathLe boss meurtNon
on_phase_switchUn boss à phases passe à une nouvelle phaseNon
on_mind_actionUn Mind natif demande une action à ses pouvoirs ; voir le contexte d'actionNon
on_zone_enterUne entité entre dans une zone surveilléeOui (si l'entité est un joueur)
on_zone_leaveUne entité quitte une zone surveilléeOui (si l'entité est un joueur)

Lorsque context.player est indiqué « Non », y accéder renvoie nil. Vérifiez toujours qu'il n'est pas nil avant de l'utiliser.

on_boss_damaged et les hooks de dégâts plus spécifiques suivent des chemins distincts. Un coup peut déclencher les deux ; ils ne s'excluent pas. Le hook de cible ne permet pas d'annuler l'événement via cette table Lua et n'indique pas la cible précédente.

on_zone_enter / on_zone_leave ne sont pas déclenchés par les zones Lua

Ces deux hooks sont acceptés et validés par le chargeur, mais rien dans un pouvoir Lua ne peut les faire se déclencher. Ils ne sont dispatchés que depuis l'écouteur de zone d'EliteScript, lequel est démarré lorsqu'un eliteScript: YAML sur le même boss liste ZoneEnterEvent ou ZoneLeaveEvent dans ses Events:. Un boss ne disposant que de pouvoirs Lua chargera sans se plaindre un fichier contenant ces hooks, puis ne les appellera jamais.

Les surveillants Lua sont un mécanisme distinct : context.zones:watch_zone(...) et context.script:zone(...):watch(...) invoquent directement les callbacks on_enter / on_leave que vous leur passez, et ne passent jamais par ces hooks de premier niveau. Utilisez les surveillants, sauf si vous associez délibérément un pouvoir Lua à un script de zone YAML.

Les événements de zone YAML atteignent tous les pouvoirs correspondants du boss. Ils contiennent l'entité entrante ou sortante, sans identifier la zone ni le script d'origine. Plusieurs écouteurs YAML peuvent donc transmettre plusieurs événements pour la même entité.

Les scripts Lua de PNJ fonctionnent différemment. Leur context.zones vient de MagmaCore, dont watch déclenche bien on_zone_enter / on_zone_leave. Voir Scripts de PNJ.

Pouvoir typique multi-hooks​

Un seul pouvoir Lua peut définir autant de hooks que nécessaire. Voici un squelette qui utilise trois hooks ensemble :

return {
api_version = 1,

on_enter_combat = function(context)
-- Initialize per-fight state when combat begins
context.state.hit_count = 0
context.log:info("Combat started!")
end,

on_boss_damaged_by_player = function(context)
-- Track hits and trigger an ability every 5th hit
context.state.hit_count = (context.state.hit_count or 0) + 1
if context.state.hit_count % 5 ~= 0 then
return
end
if not context.cooldowns:check_local("counter_attack", 100) then
return
end
-- Fire a projectile back at the player
local origin = context.boss:get_location()
origin:add(0, 1, 0)
context.boss:summon_projectile(
"SMALL_FIREBALL", origin, context.player:get_location(), 1.5
)
end,

on_death = function(context)
-- Spawn an explosion particle on death
context.world:spawn_particle_at_location(
context.boss:get_location(), "EXPLOSION_EMITTER", 1
)
end
}

Données d'événement (context.event)​

Certains hooks reçoivent une table context.event qui expose des données sur l'événement de jeu ayant déclenché le hook. Les champs disponibles dépendent du hook en cours d'exécution.

Hooks de dégâts​

S'applique à on_boss_damaged, on_boss_damaged_by_player, on_reinforcement_damaged_by_player, on_boss_damaged_by_elite et on_player_damaged_by_boss.

Champ / MéthodeTypeDescription
event.damage_amountdoubleValeur des dégâts au moment de créer cette table d'événement
event.get_damage_amount()doubleLit les dégâts actuels après les modifications précédentes
event.damage_causestringNom Spigot DamageCause (ex. "ENTITY_ATTACK", "PROJECTILE")
event.damagerentity tableAttaquant Bukkit brut si l'événement sous-jacent est un dégât par entité, y compris dans on_boss_damaged. Pour un projectile, il s'agit du projectile et non du tireur.
event.projectileentity tableL'entité projectile, si l'attaquant était un projectile.
event.set_damage_amount(n)—Remplace les dégâts par une valeur fixe
event.multiply_damage_amount(n)—Multiplie les dégâts actuels par n
event.cancel_event()—Annule entièrement l'événement de dégâts
on_boss_damaged_by_player = function(context)
-- Halve all projectile damage
if context.event.damage_cause == "PROJECTILE" then
context.event.multiply_damage_amount(0.5)
end
end

Appelez event.set_damage_amount(n) et event.multiply_damage_amount(n) avec un point. Ces setters lisent directement le premier argument sans retirer le self d'un appel avec deux-points. event:transfer_damage(target, amount) accepte les deux-points.

Dans les hooks de dégâts d'un joueur vers une élite, event.entity désigne l'élite blessée. Dans on_reinforcement_damaged_by_player, context.boss reste l'invocateur et context.player est le joueur attaquant. Le hook reçoit l'événement de dégâts original et modifiable du renfort.

Ces hooks exposent aussi event.is_damage_transfer et event:transfer_damage(target, amount). La cible doit être une autre élite vivante que le boss propriétaire est autorisé à affecter. La quantité doit être finie et positive ou nulle. Un événement annulé, une quantité nulle ou un transfert imbriqué renvoie false. Un appel valide soumet un coup distinct attribué au joueur et renvoie true ; il ne soustrait rien au coup initial et ne garantit pas qu'un autre écouteur autorisera le nouveau coup. Utilisez event.set_damage_amount(...) pour répartir les dégâts au lieu de les dupliquer. Les transferts ne peuvent pas se transférer récursivement.

Hook d'apparition​

S'applique à on_spawn.

Champ / MéthodeTypeDescription
event.spawn_reasonstringNom Spigot SpawnReason
event.cancel_event()—Annule l'apparition

Hook de mort​

S'applique à on_death.

Champ / MéthodeTypeDescription
event.entityentity tableL'entité mourante

Hooks de zone​

S'applique à on_zone_enter et on_zone_leave.

Champ / MéthodeTypeDescription
event.entityentity tableL'entité entrant dans la zone ou la quittant

Les surveillants de zone créés en Lua ne remplissent pas context.event ; ils transmettent directement l'entité qui entre/sort à leur callback.

Événements annulables (généralités)​

Tout hook dont l'événement sous-jacent est annulable expose event.cancel_event(). context.event vaut nil dans on_game_tick, on_mind_action et les callbacks ordinaires du planificateur. Des hooks comme on_heal ont un événement sous-jacent, mais aucun champ de soin dédié dans cette table. Vérifiez les membres documentés avant de les appeler.

La table d'événement propre au boss n'offre pas les membres cancel(), uncancel() et is_cancelled du wrapper partagé. Annuler un événement arrête sa transmission aux pouvoirs suivants du même boss, sans quitter la fonction Lua actuelle. Ajoutez return si la suite de cette fonction ne doit pas s'exécuter.

Pour les champs complets des tables d'entité, voir Boss et Entités. Pour les valeurs de cause de dégâts et de raison d'apparition, voir Enums et Valeurs.


Ordre d'exécution des hooks​

Le répartiteur d'événements du boss parcourt les pouvoirs de script attachés dans l'ordre d'exécution. Le champ priority d'un pouvoir Lua détermine sa position :

  • Les valeurs plus basses s'exécutent en premier (la valeur par défaut est 0).
  • À priorité égale, l'ordre d'insertion des pouvoirs du boss est conservé. Ne supposez pas un ordre précis entre différents chemins de chargement.
return {
api_version = 1,
priority = -10, -- runs before most other powers

on_boss_damaged_by_player = function(context)
-- Record this power's last attacker before its later hooks run
context.state.last_attacker = context.player.uuid
end
}

Les pouvoirs YAML EliteScript participent au même répartiteur avec la priorité 0. Une priorité Lua négative passe avant eux ; une priorité positive passe après. Cela ordonne un événement sur un boss, pas des événements Bukkit distincts ni des tâches planifiées. Un événement déjà annulé arrête le répartiteur ; tous les pouvoirs ne le reçoivent donc pas forcément. Après les pouvoirs du renfort, son invocateur direct reçoit le hook de dégâts au renfort uniquement si l'événement n'est toujours pas annulé.


Modèle d'exécution​

Un runtime par pouvoir et par boss​

Chaque pouvoir Lua attaché possède son propre runtime pour chaque boss. Au démarrage, EliteMobs évalue le code chargé dans un nouvel environnement isolé et stocke la table renvoyée. À la disparition ou au retrait du boss, le runtime s'arrête. Un boss avec deux pouvoirs Lua a donc deux tables d'état et deux environnements indépendants.

Cela signifie :

  • Les variables et fonctions locales au fichier, ainsi que les globales Lua que vous définissez, sont privées au runtime de ce pouvoir sur ce boss.
  • Les fonctions de hook de la table renvoyée ne sont jamais partagées entre les boss.

Le chargeur évalue aussi le fichier une fois pour valider la table renvoyée. L'initialisation au niveau du fichier doit rester indépendante du contexte des hooks. Un pouvoir utilisant seulement les ticks n'a pas besoin de on_spawn pour démarrer son horloge. L'initialisation du runtime et le callback d'apparition sont deux étapes distinctes.

Isolation de l'état​

Chaque runtime possède sa propre table context.state, distincte des autres pouvoirs du même boss et du même pouvoir sur d'autres boss. Utilisez-la pour conserver compteurs, indicateurs, minuteurs et autres données du pouvoir entre hooks. L'état dure le temps du runtime actif ; il n'est pas sauvegardé sur disque entre les disparitions ou redémarrages.

return {
api_version = 1,

on_boss_damaged_by_player = function(context)
-- Each boss tracks its own enrage counter independently
context.state.enrage_hits = (context.state.enrage_hits or 0) + 1
if context.state.enrage_hits >= 20 then
context.boss:apply_potion_effect("SPEED", 200, 2)
end
end
}

Propriété des tâches planifiées​

Toutes les tâches créées via context.scheduler appartiennent au runtime qui les a créées. Lorsqu'un boss disparaît :

  1. Le runtime appelle shutdown().
  2. Chaque tâche possédée -- qu'elle soit unique (run_after) ou répétée (run_every) -- est automatiquement annulée.
  3. Toutes les surveillances de zone sont effacées.
  4. Les changements d'IA, de conscience, d'échelle et d'invulnérabilité que ce pouvoir détient encore sont restaurés, le soin qu'il a défini est effacé et son contrôleur de boules de feu guidées s'arrête.

Le runtime nettoie ses tâches planifiées au retrait du boss. Certains utilitaires de jeu Java lancent des tâches distinctes avec leur propre durée de vie ; consultez leur documentation. Annulez les tâches répétées devenues inutiles pendant le jeu :

return {
api_version = 1,

on_enter_combat = function(context)
local pulse_count = 0
local task_id
task_id = context.scheduler:run_every(20, function(tick_context)
pulse_count = pulse_count + 1
if pulse_count > 10 or not tick_context.boss.exists then
tick_context.scheduler:cancel_task(task_id)
return
end
tick_context.world:spawn_particle_at_location(
tick_context.boss:get_location(),
{ particle = "FLAME", amount = 20, speed = 0.1 }
)
end)
end
}

Comportement de l'horloge par tick​

L'horloge interne de tick d'une instance de pouvoir Lua ne tourne que lorsque le pouvoir définit un hook on_game_tick. Les surveillances de zone créées via context.zones:watch_zone(...) ou context.script:zone(...):watch(...) créent leurs propres tâches répétées possédées au lieu d'activer le hook on_game_tick de premier niveau du pouvoir.

Sans on_game_tick ni surveillants de zone, le runtime n'enregistre pas cette horloge. Les tâches explicites du planificateur et celles lancées par les utilitaires de jeu continuent selon leurs propres horaires. Le travail par tick et les surveillances appartenant au runtime sont annulés à la disparition du boss ou à l'arrêt du runtime.

Suspendre un runtime conserve son état Lua et ses tâches tout en sautant leur travail. Le chemin d'événement du boss vérifie aussi la suppression des pouvoirs. Retirer le boss ou fermer le runtime termine le travail dont il est propriétaire, au lieu de simplement le suspendre.


Comportement en matière d'erreurs et de performances​

EliteMobs applique des limites strictes d'erreur et de performance sur les pouvoirs Lua :

Exceptions​

Si une fonction de hook ou un callback planifié déclenche une erreur Lua (ou si une exception Java remonte d'un appel d'API), le pouvoir est immédiatement désactivé pour cette instance de boss. Le runtime est arrêté et toutes les tâches possédées sont annulées.

L'instance en échec reste fermée ; les hooks de dégâts suivants ne la recréent pas silencieusement. Corrigez et rechargez la définition, puis créez un nouveau runtime. Un démontage délibéré par le propriétaire peut effacer l'ancienne référence pour une reconstitution ultérieure du corps.

L'erreur est consignée avec le fichier du pouvoir et le hook en cours. Un numéro de ligne est inclus lorsqu'il est disponible :

[Lua] Error in 'frost_cone.lua' at line 35 during 'on_boss_damaged_by_player':
[Lua] -> ...explanation of what went wrong...
[Lua] -> Script has been disabled for this entity to prevent further errors.

Budget d'exécution​

Les pouvoirs Lua s'exécutent sous un budget d'exécution strict appliqué à l'intérieur de la VM Lua, et pas seulement mesuré après coup. Chaque hook, chaque callback planifié et même l'évaluation initiale du fichier s'exécutent dans ce budget :

LimiteValeur
Temps CPU du thread par point d'entrée50 ms
Temps écoulé de secours si le temps CPU est indisponible250 ms
Instructions de bytecode par point d'entrée250 000

La VM compte les instructions et vérifie le temps toutes les 1024 instructions. L'erreur indique la limite dépassée :

Lua instruction budget exceeded (250000 instruction limit)
Lua CPU-time budget exceeded (50ms current-thread CPU limit)
Lua elapsed-time fallback budget exceeded (250ms fallback; current-thread CPU time unavailable)

Une boucle Lua infinie est interrompue pendant son exécution. Les appels imbriqués partagent le budget du point d'entrée. La VM ne peut pas interrompre un appel Java en cours, et aucun second seuil de 50 ms de temps réel ne s'applique après son retour. Le budget ne rend donc pas les opérations coûteuses sur le monde sans risque.

Pour rester dans le budget :

  • Évitez les boucles non bornées à l'intérieur des hooks. Utilisez context.scheduler:run_every(...) pour répartir le travail sur plusieurs ticks.
  • Gardez les gestionnaires on_game_tick légers -- ils s'exécutent à chaque tick.
  • Déplacez l'initialisation lourde dans on_spawn ou on_enter_combat plutôt que de la répéter à chaque tick.
  • Rappelez-vous que les boucles de particules sur de grandes zones sont la manière habituelle de brûler 250 000 instructions. Utilisez coverage pour échantillonner moins d'emplacements.

Sandbox Lua​

Les pouvoirs Lua s'exécutent dans un environnement LuaJ sandboxé. Plusieurs variables globales qui pourraient accéder au système de fichiers ou au runtime Java sont supprimées.

Variables globales supprimées​

Les variables globales Lua standard suivantes sont mises à nil et ne peuvent pas être utilisées :

SuppriméPourquoi
debugExpose l'état interne de la VM
dofileAccès au système de fichiers
ioAccès au système de fichiers
loadChargement de code arbitraire
loadfileAccès au système de fichiers
luajavaAccès direct aux classes Java
moduleSystème de modules (non nécessaire)
osAccès au système d'exploitation
packageSystème de modules (non nécessaire)
requireSystème de modules / accès au système de fichiers

Les hooks de débogage de la VM restent installés en interne pour compter les instructions du budget d'exécution. La table debug est ensuite immédiatement mise à nil, empêchant les scripts d'y accéder.

Bibliothèque standard disponible​

Le sandbox conserve les bibliothèques standard de LuaJ, dont ces fonctions courantes :

CatégorieFonctions
Mathmath.abs, math.ceil, math.floor, math.max, math.min, math.random, math.sin, math.cos, math.sqrt, math.pi, et toutes les autres fonctions math.*
Stringstring.byte, string.char, string.find, string.format, string.gsub, string.len, string.lower, string.match, string.rep, string.sub, string.upper, et toutes les autres fonctions string.*
Tabletable.insert, table.remove, table.sort, table.concat, et toutes les autres fonctions table.*
Itérateurspairs, ipairs, next
Typetype, tostring, tonumber, select, unpack
Gestion d'erreurspcall, xpcall, error, assert
Autreprint, rawget, rawset, rawequal, rawlen, setmetatable, getmetatable

Les chaînes partagent une seule métatable verrouillée. getmetatable("") retourne false, et les appels de méthode sur les chaînes, comme ("text"):upper(), utilisent toujours les fonctions standard de string, même après qu'un script a ajouté ou remplacé des fonctions dans sa propre table string.

astuce

print écrit dans la console, mais préférez context.log:info(msg) ou context.log:warn(msg). Les journaux des boss n'incluent pas automatiquement le fichier du pouvoir. Ajoutez-le au message si plusieurs pouvoirs peuvent écrire simultanément.


Espace de noms d'aide em​

La table em est disponible au moment du chargement du fichier (avant l'exécution de tout hook). Elle fournit des constructeurs d'aide pour construire des location tables, des vector tables et des définitions de zone utilisées dans toute l'API.

FonctionObjectif
em.create_location(x, y, z [, world, yaw, pitch])Crée une location table avec un nom de monde, un yaw et un pitch optionnels
em.create_vector(x, y, z)Crée une vector table
em.zone.create_sphere_zone(radius)Crée une définition de zone sphère
em.zone.create_dome_zone(radius)Crée une définition de zone dôme
em.zone.create_cylinder_zone(radius, height)Crée une définition de zone cylindre
em.zone.create_cuboid_zone(x, y, z)Crée une définition de zone cuboïde
em.zone.create_cone_zone(length, radius)Crée une définition de zone cône
em.zone.create_static_ray_zone(length, thickness)Crée une définition de zone rayon statique
em.zone.create_rotating_ray_zone(length, point_radius, animation_duration)Crée une définition de zone rayon rotatif
em.zone.create_translating_ray_zone(length, point_radius, animation_duration)Crée une définition de zone rayon translatant

em contient aussi em.location, avec des requêtes de monde partagées en lecture seule : is_in_dungeon, is_protected, owned_by, owners, kinds_at et has_kind. Elles prennent une table de position en premier argument. Une position absente ou qui n'est pas une table, un monde absent ou non chargé renvoie false ou une table vide. Les coordonnées absentes valent zéro ; des types invalides peuvent toutefois provoquer une erreur. Indiquez explicitement le monde en construisant ces positions. Voir les signatures et résultats dans la Référence de l'API Lua.

-- Skip a destructive mechanic inside claims or protected regions
if em.location.is_protected(context.player:get_location()) then return end

Il n'existe aucune liaison Lua pour les vérifications de permission de construction. em.location.is_protected vous indique si un plugin de protection revendique l'emplacement ; il ne résout pas les droits de construction par joueur.

Les constructeurs de zone renvoient des tables chaînables avec :set_center(loc) (ou :set_origin(loc) / :set_destination(loc) selon le type de zone). Ils sont conçus pour être utilisés en haut d'un fichier ou à l'intérieur des hooks :

-- At file scope: create a reusable zone shape
local blast_zone = em.zone.create_sphere_zone(5)

return {
api_version = 1,

on_boss_damaged_by_player = function(context)
-- Anchor the zone to the boss's current location at call time
blast_zone:set_center(context.boss:get_location())

local entities = context.zones:get_entities_in_zone(blast_zone)
for i = 1, #entities do
if entities[i].is_player then
entities[i]:apply_potion_effect("SLOWNESS", 60, 1)
end
end
end
}

Pour une présentation complète des formes de zone, des filtres, des surveillants et des modèles de ciblage, voir Zones et Ciblage.


Étapes suivantes​