Skip to main content
View

Scenarize a battle

This guide explains how to use battle events: Ruby hooks into the battle scene that show mid-battle dialogue, set up initial effects, react to moves and force the AI, selected per battle by its battle id.

What battle events do​

A battle event is a Ruby file that hooks into the battle scene at specific moments, to make a fight feel scripted rather than mechanical. Three things stand out:

  • Show messages: mid-battle dialogue, typically the enemy trainer talking.
  • Set up the logic once it is loaded, for example starting the battle with an effect already active.
  • Force the AI to take a specific action, a lighter alternative to writing a full custom AI for a gym leader or an elite.

Creating a battle event file​

Battle events live in Data/Events/Battle/ as Ruby files. The filename must start with five digits, followed by anything, then .rb:

Data/Events/Battle/00234 Brock battle.rb

The five digits are what matters: they are the battle_id the scene looks for.

Selecting which events load​

Battle::Logic::BattleInfo#battle_id decides which file loads. A battle_id of 5 loads Data/Events/Battle/00005*.rb. There are two ways to set it:

  • From a script, on the BattleInfo: bi.battle_id = 5 (see the battle scripting guide).
  • In Pokémon Studio, by setting the trainer's Battle Group ID to a non-zero value.

A few things to keep in mind:

  • battle_id defaults to -1, which loads nothing. Events only run once you set it.
  • A wild battle uses battle_id = 1 by default (so 00001*.rb), unless you call $wild_battle.setup(battle_id) yourself.
warning

Avoid two files sharing the same five digits (00005 Brock.rb and 00005 Misty.rb): only one of them loads, and which one is undefined.

Registering an event​

An event file reopens Battle::Scene and registers a block for each moment you want to hook into. Reopening the class groups every event of the file together and avoids repeating the Battle::Scene. prefix on each call:

module Battle
class Scene
register_event(:battle_begin) do |scene|
# ...
end
end
end

The block always receives the battle scene as its first argument; some events pass more. Only one block can be registered per event name: registering the same event twice keeps the last block.

The snippets below show the register_event calls on their own; each one goes inside that module Battle / class Scene block. The fully qualified form Battle::Scene.register_event(...) works too at the top level of the file, it is just more verbose.

Showing mid-battle dialogue​

The most common use is making the enemy trainer talk. PSDK does not ship a ready-made method for it, so add this helper once. It slides the trainer sprite in, shows the messages, then slides it back out:

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

Place it in a custom script, not in the battle event file, so every battle can use it. The examples below assume it exists.

note

show_event_message calls visual.lock itself, so never wrap a call to it inside another visual.lock: locking the visuals twice raises a race-condition error.

The available events​

PSDK fires eight events over the course of a battle. They are all registered the same way; they differ in when they fire and in the extra arguments the block receives.

EventBlock argumentsWhen it fires
:logic_init|scene|Right after the scene is built, before any animation. Set up the logic here.
:pre_battle_begin|scene|During the intro, after "Trainer wants to fight!", before any Pokémon is sent out.
:battle_begin|scene|After both sides sent their first Pokémon, before the first player choice.
:trainer_dialog|scene|Each turn, after the player has chosen, before the AI chooses.
:AI_force_action|scene, ai, index|Each turn, once per AI. Return actions to force them, or nil for default AI.
:after_attack|scene, launcher, move|After each move resolves.
:after_action_dialog|scene|After the turn's actions, before fainted Pokémon are replaced.
:battle_turn_end|scene|At the end of the turn, after fainted Pokémon are replaced.

Setting up the logic​

:logic_init runs before any animation, while the logic exists but nothing is on screen yet. It is the place to add starting effects:

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

The battle now starts with Light Screen and Reflect already up on the enemy side for an unlimited number of turns. bank_effects[bank] holds a bank's effects, and the effect constructor takes (logic, bank, position, turn_count).

Making the trainer talk​

Five events pass only scene, so any of them can call show_event_message; they differ only in timing (see the table). A line before the player's first move uses :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

A first-turn-only line uses :trainer_dialog with a turn guard:

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

A "down to the last Pokémon" boast uses :battle_turn_end (which fires after fainted Pokémon are replaced), with a flag so it runs only once:

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

The @ace_dialog_done flag, stored on the long-lived scene, stops the event from firing every later turn. :after_action_dialog is the same idea one step earlier, before the replacement.

Reacting to a move​

:after_attack fires after every move, with the attacker (launcher) and the move. Use the move's public predicates to react, no need to reach into its internals:

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? and move.effectiveness are all public.

Forcing the AI​

:AI_force_action fires once per AI (index is its position, ai is the AI). Return an array of actions to override its turn, or nil to let the default AI decide:

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

This forces the first AI to switch its active Pokémon for a random benched ally. Battle::Actions::Switch.new(scene, out, in) builds a switch action (the first Pokémon leaves, the second comes in), and returning the array makes the AI perform it.

Defining your own events​

The eight events above are the ones the battle scene fires, but register_event accepts any symbol, so a plugin can expose its own hooks. Triggering an event goes through call_event, which is private on the scene, so mirror the engine's own on_after_attack / on_pre_battle_begin wrappers and add a public method that calls it:

module Battle
class Scene
def on_low_hp_taunt(launcher)
call_event(:low_hp_taunt, launcher)
end
end
end

Call scene.on_low_hp_taunt(...) wherever your logic needs the hook (typically from a prepended battle method), and event files react to it like any built-in event:

register_event(:low_hp_taunt) do |scene, launcher|
# ...
end

This is exactly how the engine fires :after_attack, :pre_battle_begin and :battle_turn_end internally.

Putting it together​

A battle event file is several register_event calls inside one reopened Battle::Scene:

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

Set the battle's battle_id to the file's digits (from a script with bi.battle_id = 2, or via the trainer's Battle Group ID in Studio), add the show_event_message helper to a custom script, and the scene runs each block at the right moment.

Conclusion​

  • Battle events are Ruby files in Data/Events/Battle/NNNNN*.rb, selected by battle_id (default -1, which loads nothing; 1 for wild battles).
  • Reopen Battle::Scene and register one block per moment with register_event(:event); the block always receives scene first.
  • Use the dialogue events with the show_event_message helper for mid-battle lines, :logic_init for starting effects, :after_attack to react to moves, and :AI_force_action to script the AI.
  • Set battle_id from a BattleInfo (bi.battle_id = N) or via the trainer's Battle Group ID in Studio.
  • Beyond the eight built-in events, register your own symbol and trigger it with a public on_* wrapper around call_event to expose battle hooks from a plugin.