Aller au contenu principal

Script Lua : Exemples et modèles

Cette page présente des fichiers complets de pouvoirs Lua EliteMobs avec leurs explications. Enregistrez chaque exemple dans son propre fichier .lua, attachez-le à un boss de test, exécutez /em reload puis faites apparaître un nouveau boss après les modifications. Les animations slam, roar, frenzy et transform doivent exister sur le modèle personnalisé du boss ; elles ne sont pas intégrées au plugin. Sans modèle personnalisé, le boss ignore ces appels d'animation. Les durées supposent 20 TPS.

Si vous débutez avec les pouvoirs Lua, commencez par Premiers pas. Pour les scripts de PNJ, consultez Scripts de PNJ. Pour l'API complète des pouvoirs de boss, consultez la Référence API, Boss et entités, Monde et environnement, Zones et ciblage et Enums.

webapp_banner.jpg


Exemple : Ciblage par zone avec les utilitaires de script​

Ce que cet exemple enseigne : Comment utiliser context.script pour créer une géométrie de zone de style EliteScript depuis Lua, générer des particules et infliger des dégâts aux entités dans la zone.

Fichier de pouvoir complet (cliquez pour développer)
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
}

Explication​

  1. Création de zone -- context.script:zone(...) utilise les champs des Zones EliteScript. Target place la pointe du cône un bloc au-dessus du boss. La première position résolue par Target2 fixe le centre de la base, donc la longueur du cône ; radius = 5 définit le rayon de sa base. NEARBY_PLAYERS recherche dans une boîte s'étendant de 20 blocs sur chaque axe, sans garantir le joueur le plus proche. Utilisez DIRECT_TARGET dans ce hook de dégâts pour viser l'attaquant. Sans joueur résolu, aucun cône n'est créé ; les extrémités doivent être distinctes et dans le même monde.

  2. Génération de particules -- cone:full_target(0.4) retourne un objet de cible qui sélectionne les points de zone avec une probabilité de 40 % à chaque appel. Le nombre exact n'est pas garanti et cette couverture ne filtre pas les victimes des dégâts. La spécification utilise les champs EliteScript particle, amount et speed. L'aide décale les positions des particules de zone d'un demi-bloc sur X et Z ; les particules ne représentent pas les limites exactes des impacts sur les entités.

  3. Dégâts -- context.script:damage(cone:full_target(), 1.0, 1.5) touche les joueurs résolus à l'intérieur des cônes, car le filtre de zone par défaut est PLAYER. Le premier nombre (1.0) est le montant de dégâts de base soumis et le second (1.5) le multiplicateur EliteMobs appliqué aux joueurs. Le traitement du combat détermine les dégâts finaux.

Cet exemple s'exécute à chaque coup transmis : aucun cooldown local ou global, ni délai d'avertissement entre particules et dégâts. Ajoutez des vérifications explicites si vous le combinez avec d'autres attaques. Chaque requête de zone statique résout à nouveau ses cibles ; un objet de zone Lua n'applique pas automatiquement les conditions ou caches de cibles des actions YAML.

Noms de champs EliteScript

Les tables de zone et de cible passées à context.script utilisent les noms de champs EliteScript (targetType, shape, Target, Target2, range, offset, coverage). Pour la liste complète, consultez Zones EliteScript et Cibles EliteScript.


Exemple : Boucle d'attaque avec état et planificateur​

Ce que cet exemple enseigne : Utilisation de context.state pour suivre l'état d'exécution, context.scheduler pour les tâches répétitives et le cycle de vie approprié d'entrée/sortie de combat.

Fichier de pouvoir complet (cliquez pour développer)
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
}

Explication​

  1. Initialisation de l'état -- on_spawn initialise context.state.started à false et context.state.loop_task_id à nil. La table state persiste entre les hooks de cette instance d'exécution du pouvoir sur ce boss. Les autres pouvoirs ont des tables d'état distinctes.

  2. Garde de combat -- on_enter_combat vérifie context.state.started avant de démarrer la boucle. Cela empêche les boucles multiples superposées si l'événement se déclenche plus d'une fois.

  3. Modèle de planificateur -- context.scheduler:run_every(100, callback) programme son premier appel sans délai, puis répète tous les 100 ticks (5 secondes). Il n'attend pas cinq secondes avant de commencer. Utilisez run_repeating(100, 100, callback) si cette attente initiale est souhaitée. Le callback reçoit un nouveau contexte sans l'événement ni le joueur déclencheur ; loop_context.boss est donc un nouvel instantané. Stockez l'identifiant numérique retourné dans l'état. Cet exemple choisit uniquement une animation ; il n'inflige pas lui-même de dégâts.

  4. Nettoyage à la sortie -- on_exit_combat annule la tâche répétitive en utilisant l'ID de tâche stocké et réinitialise l'état. C'est essentiel : sans nettoyage, le planificateur continue de s'exécuter même après la fin du combat.

Toujours annuler les tâches du planificateur

Annulez les répétitions réservées au combat dans on_exit_combat. Sans cela, elles restent programmées après le combat jusqu'à leur annulation ou à la fermeture de l'instance du pouvoir. Une instance en pause ignore les callbacks sans perdre la tâche. Une erreur non interceptée dans un callback ferme l'instance et annule ses tâches.


Exemple : Effet de feu au toucher​

Ce que cet exemple enseigne : Un simple pouvoir « appliquer un effet au toucher » -- le modèle le plus courant pour les pouvoirs de combat.

Fichier de pouvoir complet (cliquez pour développer)
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

if not context.cooldowns:global_ready() 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

-- Coordinate with other powers that check the shared cooldown
context.cooldowns:set_global(40)

-- 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!")
end
}

Explication​

  1. Vérification de nil -- context.player se résout depuis le joueur acteur ou la cible directe de l'événement actuel. Vérifiez sa présence avant utilisation, surtout en adaptant ce code à un autre hook. Les callbacks programmés n'héritent pas de ce joueur.

  2. Cooldown local -- Après global_ready(), context.cooldowns:check_local("fire_touch", 60) vérifie sa clé et définit immédiatement 60 ticks si elle est prête. Sinon, il retourne false et le hook s'arrête. Les pouvoirs de ce boss partagent la clé ; les autres boss sont indépendants. Préfixez les clés avec le nom du pouvoir lorsque vous combinez les exemples.

  3. Ticks de feu -- context.player:set_fire_ticks(60) fixe la durée de feu Bukkit à 60 ticks (3 secondes), après avoir vérifié que le joueur est valide, vivant, connecté, dans le monde source et, dans une instance de donjon, un participant actuel. Il remplace la durée au lieu d'ajouter 60 ticks. D'autres mécaniques peuvent éteindre le joueur ; le message et les particules ne prouvent pas que l'effet a été accepté.

  4. Particules -- context.world:spawn_particle_at_location(location, spec) génère des particules à un emplacement spécifique. La table spec accepte particle (nom de l'enum de particules Bukkit), amount et speed.

  5. Message -- context.player:send_message(text) envoie un message avec codes couleur dans le chat. Les codes couleur Minecraft standard comme &c (rouge) fonctionnent automatiquement.

  6. Cooldown global -- context.cooldowns:set_global(40) fixe le cooldown partagé du boss à 40 ticks (2 secondes), avant les effets. Cet exemple le vérifie avant de consommer son cooldown local. Les autres hooks et callbacks Lua doivent aussi appeler global_ready() pour attendre ; définir le cooldown global ne suspend pas tous les callbacks Lua.


Exemple : Pouvoir AoE par zone avec zones natives Lua​

Ce que cet exemple enseigne : Créer et interroger des zones Lua natives pour infliger des dégâts aux joueurs dans une zone.

Fichier de pouvoir complet (cliquez pour développer)
return {
api_version = 1,

on_spawn = function(context)
context.state.aoe_task_id = nil
end,

on_enter_combat = function(context)
-- Prevent duplicate loops
if context.state.aoe_task_id ~= nil then
return
end

context.state.aoe_task_id = context.scheduler:run_every(60, function(tick_context)
-- Make sure the boss is still alive
if not tick_context.boss.exists then
return
end

if not tick_context.cooldowns:global_ready() then
return
end

-- Check and set this boss's named cooldown before applying effects
if not tick_context.cooldowns:check_local("pulse_aoe", 60) then
return
end
tick_context.cooldowns:set_global(60)

-- Build a sphere zone centered on the boss's current position
local zone_def = {
kind = "sphere",
radius = 8,
origin = tick_context.boss:get_location()
}

-- Find all players inside the sphere
local victims = tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" })

-- Damage and show particles on each player found
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

-- Spawn a particle burst at the boss; this does not draw the sphere
tick_context.world:spawn_particle_at_location(
tick_context.boss:get_location(),
{ particle = "WITCH", amount = 40, speed = 0.1 }
)
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
}

Explication​

  1. Configuration de l'état -- on_spawn initialise aoe_task_id à nil. Cet ID de tâche conservera la référence du planificateur répétitif.

  2. Attaque répétitive -- on_enter_combat démarre une tâche sans délai initial, répétée tous les 60 ticks (3 secondes). La garde évite les boucles en double. Chaque callback vérifie le cooldown global, vérifie et définit pulse_aoe, puis fixe le global à 60 ticks avant les effets. Cela coordonne uniquement les pouvoirs qui vérifient ces clés, sans bloquer automatiquement tous les scripts.

  3. Définition de zone -- La table zone_def utilise la syntaxe de zone Lua native. Le champ kind spécifie la forme ("sphere"), radius définit la taille, et origin est défini sur la position actuelle du boss au moment où le callback s'exécute. Cela signifie que la zone suit le boss lorsqu'il se déplace.

  4. Requête d'entités -- tick_context.zones:get_entities_in_zone(zone_def, { filter = "players" }) retourne un tableau Lua indexé à partir de un selon le test d'intersection des entités de la forme, pas une distance exacte jusqu'aux pieds. Les requêtes cherchent dans le monde actuel du boss et ne filtrent ni le mode de jeu ni la participation au combat. Les filtres acceptent "players", "elites", "mobs" ou "living" (par défaut) ; le boss lui-même est exclu.

  5. Infliger des dégâts -- victim:deal_custom_damage(4.0) soumet 4 points de dégâts de base attribués au boss via BossCustomAttackDamage. Il ignore les cibles invulnérables ou mortes et les joueurs hors Survie ou Aventure. Sur le parcours élite vers joueur, il contourne la formule normale et la réduction du blocage, tandis que les réductions d'armure vanilla sont annulées. Les listeners, l'annulation et l'absorption peuvent encore modifier la santé perdue. L'aide réinitialise le cooldown de dégâts natif de la cible après soumission ; ce n'est pas le même parcours que context.script:damage.

  6. Particules -- La poussière violette apparaît à la position enregistrée de chaque joueur trouvé, même si ses dégâts ont été refusés. Une gerbe WITCH apparaît sur le boss ; elle ne dessine ni anneau ni frontière de la sphère. Ces tables compactes utilisent par défaut une dispersion de position nulle, avec une taille de poussière fixée à 1.

  7. Nettoyage -- on_exit_combat annule la tâche répétitive et efface l'état, suivant le même modèle que l'exemple précédent.

Zones natives vs. zones d'utilitaires de script

Cet exemple utilise les zones Lua natives (context.zones:get_entities_in_zone()), qui prennent une table simple avec kind, radius, origin, etc. Les utilitaires de script (context.script:zone(...)) utilisent les noms de champs EliteScript comme shape, Target et Target2. Les deux fonctionnent -- utilisez les zones natives pour les formes simples et context.script lorsque vous avez besoin de la résolution avancée de cibles d'EliteScript.


Exemple : Mécanique de boss multi-phases​

Ce que cet exemple enseigne : Utilisation de l'état pour suivre les phases du boss et changer de comportement aux seuils de santé.

Il s'agit de phases dans l'état Lua, pas des phases: YAML d'EliteMobs qui remplacent une définition de boss. Aucun boss n'est remplacé ni soigné ici. Le soigner ensuite ne le ramène pas en phase 1 ; quitter le combat conserve la phase 2 jusqu'à la recréation de l'instance du pouvoir.

Fichier de pouvoir complet (cliquez pour développer)
local function phase_one_attack(context)
-- One damage pulse per callback; the animation does not delay it
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)
-- One pulse per callback, repeated more frequently in phase 2
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

local function start_attack_loop(context)
if context.state.attack_task_id ~= nil then return end
local phase = context.state.phase or 1
local interval = phase == 2 and 40 or 100
local attack = phase == 2 and phase_two_attack or phase_one_attack
context.state.attack_task_id = context.scheduler:run_every(interval, function(tick_context)
if not tick_context.boss.exists or not tick_context.boss.is_in_combat then return end
if tick_context.cooldowns:check_local("phase_attack", interval) then
attack(tick_context)
end
end)
end

return {
api_version = 1,

on_spawn = function(context)
context.state.phase = 1
context.state.attack_task_id = nil
end,

on_enter_combat = function(context)
start_attack_loop(context)
end,

on_game_tick = function(context)
-- Only check phase transition every 20 ticks (1 second) to stay lightweight
if not context.boss.is_in_combat then return end
if not context.cooldowns:check_local("phase_check", 20) then
return
end

-- Skip if already in phase 2
if context.state.phase ~= 1 then
return
end

-- Check health ratio
local health_ratio = context.boss.health / context.boss.maximum_health

if health_ratio <= 0.5 then
-- Transition to phase 2
context.state.phase = 2

context.log:info("Boss entering phase 2 at " .. tostring(math.floor(health_ratio * 100)) .. "% health")

-- Cancel the old attack loop
if context.state.attack_task_id ~= nil then
context.scheduler:cancel_task(context.state.attack_task_id)
context.state.attack_task_id = nil
end

-- Play transition effects
context.boss:play_model_animation("transform")

-- Announce the phase change to nearby players
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

start_attack_loop(context)
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
}

Explication​

  1. Initialisation de l'état -- on_spawn définit phase à 1 et attack_task_id à nil. Ces valeurs persistent entre les hooks de cette instance du pouvoir, pas après rechargement ni dans d'autres pouvoirs.

  2. Boucle d'attaque -- on_enter_combat reprend la phase actuelle avec start_attack_loop : intervalle de 100 ticks en phase 1, de 40 en phase 2. Les deux programment le premier callback sans délai. Chaque attaque exige encore que phase_attack soit prêt ; changer de phase n'efface pas l'ancien cooldown, donc un callback peut ignorer l'attaque. Cet exemple ne vérifie pas le cooldown global. La phase 1 soumet 3 points de dégâts personnalisés aux joueurs dans une sphère de 5 blocs, avec animation et particules, sans attendre la durée de l'animation.

  3. Vérification de phase dans on_game_tick -- on_game_tick retourne immédiatement hors combat, puis vérifie un cooldown de 20 ticks ("phase_check"). Si le boss est déjà en phase 2, il retourne également sans poursuivre.

  4. Seuil de santé -- context.boss.health / context.boss.maximum_health donne la santé actuelle sous forme de fraction. Lorsqu'elle descend à 50% ou en dessous, la transition commence.

  5. Transition de phase -- L'ancienne boucle d'attaque est annulée, une animation de transformation est jouée, les joueurs proches reçoivent un message et un titre, et une nouvelle boucle d'attaque plus rapide démarre (toutes les 40 ticks au lieu de 100).

  6. Attaques de phase 2 -- phase_two_attack utilise une sphère de 8 blocs et soumet un coup de 2 points par joueur trouvé et par callback ayant passé les vérifications. Il tente d'appliquer Lenteur II pendant 40 ticks (amplifier = 1), sous réserve des vérifications de participation aux effets décrites plus haut, puis affiche de la poussière rouge et joue un son de wither. Il ne programme pas plusieurs coups dans un même callback.

  7. Journalisation -- context.log:info(...) écrit dans la console du serveur, ce qui est inestimable pour déboguer les transitions de phase pendant le développement.

  8. Nettoyage -- on_exit_combat annule la boucle d'attaque active, quelle que soit la phase du boss.

Gardez on_game_tick léger

Une instance active et non suspendue reçoit on_game_tick à chaque tick serveur (20 fois par seconde à 20 TPS). Limitez les opérations coûteuses avec un cooldown, comme check_local("phase_check", 20). La santé est interrogée périodiquement : un coup fatal peut supprimer le boss avant la vérification de transition. Dépasser le budget d'exécution ferme cette instance du pouvoir et annule ses tâches.


Conseils pour la génération par IA​

Si vous souhaitez que l'IA génère des pouvoirs Lua de manière fiable, assurez-vous que le prompt inclut :

  • Nom de hook exact -- par ex., on_player_damaged_by_boss, pas « quand le boss frappe un joueur ».
  • Zones Lua natives ou utilitaires de script -- précisez lequel. Les zones natives utilisent context.zones:get_entities_in_zone(zone_def, opts) avec kind, radius, origin. Les utilitaires de script utilisent context.script:zone(...) avec les noms de champs EliteScript comme shape, Target, Target2.
  • Cooldowns locaux et globaux -- précisez une clé préfixée et la durée en ticks. context.cooldowns:check_local(key, ticks) vérifie et définit un cooldown nommé par boss. Pour coordonner avec le global, vérifiez explicitement global_ready() avant de consommer la clé locale et définissez la durée globale avant les effets.
  • Noms d'animation de modèle personnalisé -- par ex., context.boss:play_model_animation("slam"). L'IA ne peut pas deviner les noms d'animation ; fournissez-les.
  • Sélection de cible -- précisez si le pouvoir cible context.player (le joueur de l'événement), context.players.nearby_players(range) ou une requête de zone.
  • Type d'effet -- dégâts (deal_custom_damage), potion (apply_potion_effect), feu (set_fire_ticks), vélocité (set_velocity_vector, apply_push_vector), etc.
  • Utilisez les méthodes du bon wrapper -- fournissez la référence adaptée au boss ou à l'API partagée, sans inventer d'alias.
  • Les spécifications d'utilitaires de script utilisent les noms de champs EliteScript -- targetType, shape, Target, Target2, range, offset, coverage, pas des noms de style Lua.
  • Délais et contexte du planificateur -- précisez si le premier callback doit être immédiat ou différé. run_every n'a aucun délai initial ; run_repeating en reçoit un séparément. Utilisez le nouveau contexte du callback : il ne contient ni joueur ni événement déclencheur. Conservez explicitement l'UUID d'un joueur ou une position si la mécanique en a besoin plus tard.

Bon exemple de prompt​

Écrivez un pouvoir Lua utilisant on_enter_combat pour démarrer une tâche avec 80 ticks de délai initial et 80 ticks d'intervalle. À chaque callback, créez une sphère Lua native de rayon 6 centrée sur le boss, trouvez les joueurs et soumettez 2.0 points de dégâts personnalisés à chacun. Vérifiez le cooldown global avant de vérifier et définir la clé locale "my_power:pulse" pour 80 ticks, puis fixez le global à 80 avant les effets. Annulez la tâche dans on_exit_combat. Générez des particules DUST (red=0, green=255, blue=100) sur chaque joueur trouvé.

Contraintes supplémentaires à inclure​

  • « Retournez une seule table avec api_version = 1. »
  • « Initialisez tous les champs d'état dans on_spawn. »
  • « Protégez les callbacks du planificateur avec if not tick_context.boss.exists then return end. »
  • « Annulez toutes les tâches répétitives dans on_exit_combat. »
  • « N'inventez pas de noms de méthodes -- n'utilisez que les méthodes de la page de Référence API. »
  • « Utilisez context.cooldowns:check_local(key, ticks) pour une vérification-et-définition combinée. »

Liste de vérification QC pour révision humaine ou IA​

Utilisez cette liste de vérification pour valider un pouvoir Lua avant de le déployer :

  1. Le fichier retourne exactement une table avec api_version = 1.
  2. Chaque nom de hook correspond exactement à une entrée dans la liste des hooks (par ex., on_player_damaged_by_boss, pas on_player_hit).
  3. context.player est protégé avec == nil avant utilisation dans les hooks où il peut être nil.
  4. Les champs de context.state sont initialisés dans on_spawn.
  5. Toute répétition réservée au combat, créée avec run_every ou run_repeating, possède une annulation dans on_exit_combat ; son délai initial est intentionnel.
  6. Les callbacks utilisent leur propre contexte et ne supposent pas la présence d'un joueur ou événement déclencheur.
  7. Les clés de cooldown portent un préfixe avec le nom du pouvoir et les durées sont en ticks. La coordination globale vérifie explicitement la disponibilité.
  8. Les hooks on_game_tick protègent le travail coûteux derrière une vérification de cooldown.
  9. Tous les noms de méthodes existent dans la Référence API -- pas d'alias inventés.
  10. Les tables d'utilitaires de script utilisent les noms de champs EliteScript (targetType, shape, Target, etc.), pas des noms de style Lua.
  11. Les définitions de zone natives utilisent kind, radius, origin, destination, etc.
  12. Le pouvoir n'appelle aucune opération bloquante ou de longue durée à l'intérieur d'un hook ou callback.
  13. Les spécifications de particules utilisent des noms d'enum de particules Bukkit valides en MAJUSCULES (par ex., "FLAME", "DUST", "EXPLOSION").
  14. Rechargez les fichiers modifiés, faites apparaître un nouveau boss et vérifiez le comportement visible et la console sur un serveur de test. Un fichier analysé sans erreur ne prouve pas que l'attaque fonctionne.

Bonnes pratiques​

  • Commencez petit et vérifiez. Écrivez un seul on_spawn qui envoie un message de log. Confirmez qu'il se déclenche. Puis construisez à partir de là.

  • Gardez les fonctions auxiliaires locales. Déclarez les fonctions comme local function pick_action(context) avant la table de retour pour garder les dépendances explicites. Chaque instance de pouvoir dispose déjà de son propre environnement Lua.

  • Placez la géométrie dans les utilitaires de script. Pour les cônes, rayons rotatifs ou en translation et zones animées, utilisez context.script:zone(...) avec les champs EliteScript. Les utilitaires réutilisent le moteur de zones d'EliteScript.

  • Utilisez context.state pour l'état d'exécution. Il appartient à une instance de pouvoir sur un boss et persiste entre ses hooks. Il n'est ni partagé avec les autres pouvoirs ni sauvegardé entre rechargements.

  • Utilisez des clés de cooldown locales nommées. Choisissez des clés descriptives comme "fire_touch" ou "aoe_pulse". Les clés explicites sont partagées entre tous les pouvoirs du même boss. Préfixez-les avec le nom du pouvoir, par exemple "my_power:fire_touch", pour éviter les collisions.

  • Gardez on_game_tick léger. Limitez les opérations coûteuses avec un cooldown et restez bien en dessous du budget d’exécution.

  • Annulez les répétitions terminées. Les boucles réservées au combat doivent être annulées dans on_exit_combat. Les tâches de l'instance sont automatiquement annulées à sa fermeture, mais continuent après le combat sans annulation explicite.

  • Utilisez le nouveau contexte des callbacks du planificateur. Les callbacks de run_every, run_repeating et run_after reçoivent un contexte sans l'événement ni le joueur qui les a programmés. Utilisez cet argument et conservez explicitement l'UUID ou la position nécessaire ; un contexte externe peut contenir des instantanés périmés.

  • Journalisez les transitions avec context.log:info(). Pendant le développement, consignez les phases, débuts de cooldown et démarrages ou arrêts du planificateur. Retirez les messages bruyants avant déploiement : context.log:debug() du boss écrit aussi au niveau info, avec un autre préfixe.

  • Réutilisez les références EliteScript pertinentes. Zones, Cibles et Vecteurs relatifs décrivent les parseurs partagés. Les aides Lua autonomes n'évaluent pas automatiquement les Conditions, attentes ou caches des actions YAML : exprimez vos vérifications en Lua. Les cibles héritées et positions d'atterrissage exigent un contexte d'action YAML absent de ces objets.


Erreurs courantes de débutant​

  • Utiliser le context externe dans un callback du planificateur. Le contexte externe capture un snapshot au moment où le hook s'est exécuté. Dans un callback run_every ou run_after, utilisez toujours le paramètre propre du callback (par ex., tick_context), qui vous donne un snapshot frais.

  • Oublier d'annuler les tâches répétitives. Si vous démarrez un run_every dans on_enter_combat mais ne l'annulez jamais, la tâche continue de s'exécuter jusqu'à ce que le boss soit retiré du serveur, même après la fin du combat.

  • Ne pas initialiser l'état dans on_spawn. Si vous lisez context.state.phase dans on_game_tick mais ne le définissez jamais dans on_spawn, il sera nil et vos comparaisons se comporteront de manière inattendue.

  • Utiliser context.player sans vérifier nil. Vérifiez quels hooks fournissent un joueur. Les callbacks programmés n'en ont aucun par défaut ; oublier la vérification peut fermer l'instance du pouvoir.

  • Utiliser des champs de style Lua dans les utilitaires de script. Utilisez targetType, Target, Target2 et shape. Les variantes avec soulignement comme target_type et zone_shape ne sont pas reconnues. Le parseur de zone accepte target et target2 en minuscules, sans pour autant accepter n'importe quel alias de style Lua.

  • Exécuter du travail coûteux dans on_game_tick sans cooldown. Les requêtes sur toutes les entités du monde et les géométries denses de particules pour de nombreux boss sont coûteuses. Limitez ce travail et gardez chaque callback dans son budget d'exécution.

  • Mélanger les API des wrappers. Les wrappers spécifiques aux boss EliteMobs utilisent entity:teleport_to_location(loc) et player:set_velocity_vector(vec). Les wrappers partagés de MagmaCore ont des alias supplémentaires. Consultez la référence de votre contexte avant d'emprunter une méthode à l'exemple d'un autre plugin.

  • Utiliser context.boss.health pour définir la santé. context.boss.health est un snapshot en lecture seule. Pour guérir le boss, utilisez context.boss:restore_health(amount).

  • Oublier api_version = 1. La table retournée doit inclure ce champ, sinon EliteMobs ne chargera pas le pouvoir.


Étapes suivantes​