Scénariser un combat
Ce guide explique comment utiliser les battle events : des hooks Ruby dans la scène de combat qui affichent des dialogues en plein combat, mettent en place des effets initiaux, réagissent aux attaques et forcent l'IA, sélectionnés par combat via son battle id.
Ce que font les battle events
Un battle event est un fichier Ruby qui s'accroche à la scène de combat à des moments précis, pour rendre un combat scénarisé plutôt que mécanique. Trois usages se détachent :
- Afficher des messages : un dialogue en plein combat, typiquement le dresseur ennemi qui parle.
- Mettre en place la logique une fois chargée, par exemple démarrer le combat avec un effet déjà actif.
- Forcer l'IA à exécuter une action précise, alternative légère à l'écriture d'une IA personnalisée complète pour un champion ou un membre de l'élite.
Créer un fichier de battle event
Les battle events vivent dans Data/Events/Battle/ sous forme de fichiers Ruby. Le nom de fichier doit commencer par cinq chiffres, suivis de n'importe quoi, puis de .rb :
Data/Events/Battle/00234 Brock battle.rb
Ce sont les cinq chiffres qui comptent : ils correspondent au battle_id que la scène recherche.
Choisir quels events charger
Battle::Logic::BattleInfo#battle_id décide quel fichier charger. Un battle_id de 5 charge Data/Events/Battle/00005*.rb. Il y a deux façons de le définir :
- Depuis un script, sur le
BattleInfo:bi.battle_id = 5(voir le guide de scripting de combat). - Dans Pokémon Studio, en réglant le Battle Group ID du dresseur sur une valeur non nulle.
Quelques points à garder en tête :
battle_idvaut-1par défaut, ce qui ne charge rien. Les events ne tournent qu'une fois la valeur définie.- Un combat sauvage utilise
battle_id = 1par défaut (donc00001*.rb), sauf si on appelle soi-même$wild_battle.setup(battle_id).
On évite deux fichiers partageant les mêmes cinq chiffres (00005 Brock.rb et 00005 Misty.rb) : un seul est chargé, et lequel n'est pas déterminé.
Enregistrer un event
Un fichier de battle event rouvre Battle::Scene et enregistre un bloc pour chaque moment auquel on veut s'accrocher. Rouvrir la classe regroupe tous les events du fichier et évite de répéter le préfixe Battle::Scene. à chaque appel :
module Battle
class Scene
register_event(:battle_begin) do |scene|
# ...
end
end
end
Le bloc reçoit toujours la scene de combat comme premier argument ; certains events en passent davantage. On ne peut enregistrer qu'un seul bloc par nom d'event : enregistrer le même event deux fois conserve le dernier bloc.
Les exemples ci-dessous montrent les appels register_event seuls ; chacun va à l'intérieur de ce bloc module Battle / class Scene. La forme qualifiée Battle::Scene.register_event(...) fonctionne aussi au niveau racine du fichier, elle est juste plus verbeuse.
Afficher un dialogue en plein combat
L'usage le plus courant est de faire parler le dresseur ennemi. PSDK ne fournit pas de méthode toute prête pour ça, donc on ajoute ce helper une fois. Il fait glisser le sprite du dresseur, affiche les messages, puis le fait ressortir :
module Battle
class Scene
# Show messages from the enemy trainer during battle.
# @param messages [Array<String>]
# @note wraps everything in visual.lock
def show_event_message(*messages)
visual.lock do
sprite = visual.battler_sprite(1, -1) # negative position = trainer sprite, -1 = first trainer
slide_in = Yuki::Animation.move(0.4, sprite, 320 + sprite.width, sprite.y, 290, sprite.y)
slide_in.start
visual.animations << slide_in
visual.hide_team_info
visual.wait_for_animation
messages.each do |message|
message_window.blocking = true
message_window.wait_input = true
display_message_and_wait(message)
end
slide_out = Yuki::Animation.move(0.4, sprite, 290, sprite.y, 320 + sprite.width, sprite.y)
slide_out.start
visual.animations << slide_out
visual.show_team_info
visual.wait_for_animation
end
end
end
end
On le place dans un script personnalisé, pas dans le fichier de battle event, pour que tous les combats puissent l'utiliser. Les exemples ci-dessous supposent qu'il existe.
show_event_message appelle lui-même visual.lock : il ne faut donc jamais envelopper un appel à cette méthode dans un autre visual.lock, verrouiller deux fois les visuels lève une erreur de race condition.
Les events disponibles
PSDK déclenche huit events au cours d'un combat. Ils s'enregistrent tous de la même façon ; ils diffèrent par le moment où ils se déclenchent et par les arguments supplémentaires que reçoit le bloc.
| Event | Arguments du bloc | Moment de déclenchement |
|---|---|---|
:logic_init | |scene| | Juste après la construction de la scène, avant toute animation. On y met en place la logique. |
:pre_battle_begin | |scene| | Pendant l'intro, après « Le dresseur veut se battre ! », avant l'envoi du premier Pokémon. |
:battle_begin | |scene| | Après que les deux camps ont envoyé leur premier Pokémon, avant le premier choix du joueur. |
:trainer_dialog | |scene| | Chaque tour, après le choix du joueur, avant celui de l'IA. |
:AI_force_action | |scene, ai, index| | Chaque tour, une fois par IA. On retourne des actions pour les forcer, ou nil pour l'IA par défaut. |
:after_attack | |scene, launcher, move| | Après la résolution de chaque attaque. |
:after_action_dialog | |scene| | Après les actions du tour, avant le remplacement des Pokémon K.O. |
:battle_turn_end | |scene| | À la fin du tour, après le remplacement des Pokémon K.O. |
Mettre en place la logique
:logic_init tourne avant toute animation, alors que la logique existe mais que rien n'est encore à l'écran. C'est l'endroit pour ajouter des effets de départ :
register_event(:logic_init) do |scene|
scene.logic.bank_effects[1].add(Battle::Effects::LightScreen.new(scene.logic, 1, 0, Float::INFINITY))
scene.logic.bank_effects[1].add(Battle::Effects::Reflect.new(scene.logic, 1, 0, Float::INFINITY))
end
Le combat démarre alors avec Mur Lumière et Protection déjà actifs côté ennemi pour un nombre de tours illimité. bank_effects[bank] contient les effets d'un banc, et le constructeur d'effet prend (logic, bank, position, turn_count).
Faire parler le dresseur
Cinq events ne passent que scene, donc n'importe lequel peut appeler show_event_message ; ils ne diffèrent que par le moment (voir le tableau). Une réplique avant la première action du joueur utilise :battle_begin :
register_event(:battle_begin) do |scene|
scene.show_event_message("You actually made it this far? Let me show you a real battle!")
end
Une réplique au premier tour seulement utilise :trainer_dialog avec un garde sur le tour :
register_event(:trainer_dialog) do |scene|
next if $game_temp.battle_turn != 1 # only on the first turn
scene.show_event_message("Oh, I forgot to mention, I never intended to play fair.")
end
Une vantardise « plus qu'un dernier Pokémon » utilise :battle_turn_end (qui se déclenche après le remplacement des Pokémon K.O.), avec un drapeau pour ne tourner qu'une fois :
register_event(:battle_turn_end) do |scene|
next if scene.logic.alive_battlers_without_check(1).size > 1
next if scene.instance_variable_get(:@ace_dialog_done)
scene.instance_variable_set(:@ace_dialog_done, true)
scene.show_event_message("You are pushing me to my limits! Here is my best Pokémon!")
end
Le drapeau @ace_dialog_done, stocké sur la scène (objet à longue durée de vie), empêche l'event de se déclencher à chaque tour suivant. :after_action_dialog est la même idée une étape plus tôt, avant le remplacement.
Réagir à une attaque
:after_attack se déclenche après chaque attaque, avec l'attaquant (launcher) et l'attaque (move). On utilise les prédicats publics de l'attaque pour réagir, sans avoir à fouiller dans ses internes :
register_event(:after_attack) do |scene, launcher, move|
next if launcher.bank != 0 # only react to the player's moves
next if scene.instance_variable_get(:@praised_player)
if move.super_effective?
scene.instance_variable_set(:@praised_player, true)
next scene.show_event_message("Nicely done! You really know how to hit a weakness.")
end
if move.critical_hit?
scene.instance_variable_set(:@praised_player, true)
scene.show_event_message("Ouch, that critical hit must have hurt!")
end
end
move.super_effective?, move.critical_hit? et move.effectiveness sont tous publics.
Forcer l'IA
:AI_force_action se déclenche une fois par IA (index est sa position, ai est l'IA). On retourne un tableau d'actions pour remplacer son tour, ou nil pour laisser l'IA par défaut décider :
register_event(:AI_force_action) do |scene, ai, index|
next if index != 0 # only the first AI
controlled = ai.controlled_pokemon
next if controlled.empty?
next unless scene.logic.can_battler_be_replaced?(active = controlled.first)
bench = ai.party.select { |pokemon| pokemon.alive? && !controlled.include?(pokemon) }
next if bench.empty?
next [Battle::Actions::Switch.new(scene, active, bench.sample)]
end
Cela force la première IA à remplacer son Pokémon actif par un allié du banc tiré au hasard. Battle::Actions::Switch.new(scene, out, in) construit une action de changement (le premier Pokémon sort, le second entre), et retourner le tableau fait exécuter l'action à l'IA.
Définir ses propres events
Les huit events ci-dessus sont ceux que déclenche la scène de combat, mais register_event accepte n'importe quel symbole : un plugin peut donc exposer ses propres hooks. Déclencher un event passe par call_event, privé sur la scène, donc on imite les wrappers on_after_attack / on_pre_battle_begin du moteur et on ajoute une méthode publique qui l'appelle :
module Battle
class Scene
def on_low_hp_taunt(launcher)
call_event(:low_hp_taunt, launcher)
end
end
end
On appelle scene.on_low_hp_taunt(...) là où la logique en a besoin (typiquement depuis une méthode de combat surchargée par prepend), et les fichiers d'event y réagissent comme à n'importe quel event natif :
register_event(:low_hp_taunt) do |scene, launcher|
# ...
end
C'est exactement ainsi que le moteur déclenche :after_attack, :pre_battle_begin et :battle_turn_end en interne.
Mettre le tout ensemble
Un fichier de battle event est une suite d'appels register_event à l'intérieur d'un même Battle::Scene rouvert :
module Battle
class Scene
register_event(:logic_init) do |scene|
scene.logic.bank_effects[1].add(Battle::Effects::LightScreen.new(scene.logic, 1, 0, Float::INFINITY))
end
register_event(:battle_begin) do |scene|
scene.show_event_message("So you finally made it. Do not expect me to go easy on you!")
end
end
end
On règle le battle_id du combat sur les chiffres du fichier (depuis un script avec bi.battle_id = 2, ou via le Battle Group ID du dresseur dans Studio), on ajoute le helper show_event_message à un script personnalisé, et la scène exécute chaque bloc au bon moment.
Conclusion
- Les battle events sont des fichiers Ruby dans
Data/Events/Battle/NNNNN*.rb, sélectionnés parbattle_id(par défaut-1, qui ne charge rien ;1pour les combats sauvages). - On rouvre
Battle::Sceneet on enregistre un bloc par moment avecregister_event(:event); le bloc reçoit toujourssceneen premier. - On utilise les events de dialogue avec le helper
show_event_messagepour les répliques en plein combat,:logic_initpour les effets de départ,:after_attackpour réagir aux attaques, et:AI_force_actionpour scénariser l'IA. - On définit
battle_iddepuis unBattleInfo(bi.battle_id = N) ou via le Battle Group ID du dresseur dans Studio. - Au-delà des huit events natifs, on enregistre son propre symbole et on le déclenche avec un wrapper public
on_*autour decall_eventpour exposer des hooks de combat depuis un plugin.