There are a wide variety of concepts and details involved in working with the battle engine. This page is intended to serve as a glossary for miscellaneous information. It's split into two parts: "glossary" for defining terms, and "details" for explaining more specific or niche concepts.
Glossary
Action
A decision to be made by a battler, such as "use a move" or "run." Battle controllers submit actions (and, when necessary, the details of those actions) to the battle engine, and the battle engine then executes those actions as appropriate. Each battler can take one action per turn.
Some actions require parameters; for example, the action to use an item requires an item type to use. HandleTurnActionSelectionState, the function which manages the execution flow for performing actions, refers to these as "action cases:" using an item is an action, and Full Heal is an action case; using a move is an action, and using Hyper Beam is an action case.
Attack canceller
Broadly, a group of situations that can cancel an attack, override its effect, or display supplemental messages prior to the effect of a move. Cancellers are defined and processed in AtkCanceller_UnableToUseMove, which is called by the attackcanceler script command. (This command is invoked at the start of basically every move script.) Each canceller is latent code and may run for an arbitrary number of frames (i.e. to display messages to the player).
| Name | Description |
|---|---|
CANCELLER_ASLEEP |
Checks if the user is asleep, and whether they should spontaneously wake up. If they should wake up, then the canceller queues a script to wake them up, and then waits to re-run; otherwise, the canceller cancels the user's action. |
CANCELLER_FROZEN |
Checks if the user is frozen. |
CANCELLER_TRUANT |
Checks if the user is blocked by Truant. |
CANCELLER_RECHARGE |
Checks if the user needs to recharge (STATUS2_RECHARGE) after using moves like Hyper Beam. |
CANCELLER_FLINCH |
Checks if the user is flinching (STATUS2_FLINCHED). |
CANCELLER_DISABLED |
Checks if the user's chosen move is blocked by Disable. This case would arise if the Pokemon is hit with Disable after it chose the disabled move. |
CANCELLER_TAUNTED |
Checks if the user is affected by Taunt. |
CANCELLER_IMPRISONED |
Checks if the user is affected by Imprison. |
CANCELLER_CONFUSED |
Handles confusion, either snapping the user out of confusion, or cancelling their action by having them injure themselves. |
CANCELLER_PARALYSED |
Checks if the user is unable to move due to paralysis. |
CANCELLER_IN_LOVE |
Checks if the user is immobilized by love (i.e. Attract). |
CANCELLER_BIDE |
If the user has used Bide and is still storing energy, then their attack is cancelled. If the user has used Bide and is now due to attack, then their chosen action is overridden with the Bide attack (or with a failure message, if they never took any damage while storing energy). |
CANCELLER_THAW |
Handles the user being thawed out, if they're frozen and use a move that thaws. |
Battle message
A predefined UI string intended for use within the battle engine. These use special placeholder codes that are only meaningful within battles. Each such string has a unique ID, which can be referenced within battle scripts and sent to battle controllers.
Battle script
Scripts, which run during battle. These use a completely separate bytecode from the "event scripts" used on the overworld, and have different functionality.
Generally, the battle engine can execute one script at a time, and script execution will pause all other tasks that the battle engine might perform internally. The engine will run at most one script command per frame, though on frames in which any battle controllers are active or pending synchronization, no script commands will run.
End-of-battle script
At the end of a turn, the battle engine will check if the battle's outcome has been set. If no outcome is set, then the engine will run HandleEndTurn_ContinueBattle; but if an outcome has been set, then the engine will pick one of the other functions in sEndTurnFuncsTable, and run one of those.
Most of these functions set gBattlescriptCurrInstr to a battle script, and then fall through to HandleEndTurn_FinishBattle, which will manually execute the current script (whatever that may be) until gCurrentActionFuncId is set to either B_ACTION_TRY_FINISH or B_ACTION_FINISHED. (That will happen when the script runs the end, end2, or finishaction commands.) Scripts executed through these means, then, are end-of-battle scripts.
Several battle outcomes, however, list HandleEndTurn_FinishBattle as their handlers: B_OUTCOME_PLAYER_TELEPORTED, B_OUTCOME_CAUGHT, B_OUTCOME_NO_SAFARI_BALLS, B_OUTCOME_FORFEITED, and B_OUTCOME_MON_TELEPORTED. How do these outcomes set up the script to run? Well, they don't. Rather, it's assumed that a battle script will have run at some point, and that that script will have concluded with end, end2, finishaction, or finishturn, any of which would set gCurrentActionFuncId to a value that will end script execution. (Indeed, these battle outcomes can only be set by a script in the first place.) Therefore, the script instruction pointer will still be at that command, and so when HandleEndTurn_FinishBattle goes to execute a script, that last command is what it'll execute.
Selection script
Typically, the battle engine will ask a battler's controller what action the battler should perform. The engine will gather this information from all controllers, and then carry out these actions as part of the current turn. Some action selections, however, will instead lead to the battle engine executing a script for the given battler. These special scripts are called selection scripts.
Selection scripts and their opcodes are executed directly by HandleTurnActionSelectionState, the function that coordinates gathering all controllers' action choices. These scripts are an exception to some of the usual rules for battle script execution:
- If multiple battlers are running a selection script, their scripts will run side-by-side, still one opcode per frame.
- A battler's current selection script can run opcodes even if other battlers' controllers are currently active, though still not if any controllers are pending synchronization. This means that within a selection script, any given battle script command is only safe to run if it does not modify
gActiveBattlerand if it meets any of the following criteria:- The command never emits messages to battle controllers.
- The command only emits messages to
gActiveBattlerorgBattlerAttacker, which, during a selection script, will be the same battler. - The command emits messages to the controller of a battler you specify, and you only specify
BS_ATTACKER. - The command waits for all controllers to become idle before it actually does anything. (Many, but not all, commands that emit messages do this.)
- The
callscript command, and any script commands that implicitlycallscripts, is unsafe to use. The script engine maintains only one call stack, so if multiple selection scripts end up executing concurrently, any whichcallorreturnwill mangle that global call stack.
Battle script commands
Each battle script is a sequence of battle script commands, which, generally, are run one after another. Battle script commands will read arguments (if they accept any) from just ahead of the script instruction pointer, and then advance the instruction pointer when they're done.
Some battle script commands are latent: they may need to run over the course of multiple frames, so they'll only advance the instruction pointer when they're done. Since latent functions have to store their state elsewhere, these script commands will generally store their state in places like gBattleStruct and gBattleScripting.
Some battle script commands send messages to a battle controller.
Battle TV
Some in-game TV programs will go on the air depending on the player's actions during a match. These programs all rely on three hooks:
BattleTv_SetDataBasedOnString(u16 stringId)is called whenever a battle message is displayed. It's called by the battle controllers when they respond to theCONTROLLER_PRINTSTRINGmessage.BattleTv_SetDataBasedOnMove(u16 move, u16 weatherFlags, struct DisableStruct*)is called when any move animation plays. It's called by the battle controllers when they respond to theCONTROLLER_MOVEANIMATIONmessage.BattleTv_SetDataBasedOnAnimation(u8 animationId)is called when any battle animation plays. It's called by the battle controllers when they respond to theCONTROLLER_BATTLEANIMATIONmessage.
These hooks do not, on their own, provide all of the information that the Battle TV needs. For example, the Battle Seminar show may go on the air if the player uses a not-very-effective move; this relies on the CONTROLLER_PRINTSTRING hook, but has no clean way to know what move the player used. To that end, it relies on global state: gBattleMons[gBattlerAttacker].moves[gMoveSelectionCursor[gBattlerAttacker]].
Battler
A participant in the battle; a Pokemon currently on the battlefield. There are up to four battlers. Each battler has a battle controller that directs its actions on each turn.
Battlers have a large amount of state, scattered across several globals. The more notable pieces of state include:
| Global | Purpose |
|---|---|
gBattleMons[n] |
The battler's stats and most of their status conditions. |
gStatuses3[n] |
The status-3 flags and state for this battler. Status-1 and status-2 are stored in gBattleMons. |
gBattlerPartyIndexes[n] |
Tracks the battler's index within its containing party. |
gBattlerPositions[n] |
Tracks the battler's position on the battlefield. During singleplayer battles, the position will always be the same value as the battler's index number, but during multiplayer battles, the two will differ, since battlefield sides are defined relative to the local player. |
gDisableStructs[n] |
States per battler, scoped to the battler's presence on the field. (Its contents are similar to those of gWishFutureKnock, but gWishFutureKnock is used for state that persists even if the battler faints or switches out.) |
gProtectStructs[n] |
States per battler, scoped to a single turn. |
gSpecialStatuses[n] |
States per battler, scoped to a single action. See here for details. |
Hitmarker
Hitmarkers are stored in gHitMarker, and are a set of flags that can be used to queue something to happen or be suppressed.
Most hitmarkers influence the behavior of one queued event, one time. For example, the HITMARKER_NO_ATTACKSTRING flag suppresses the "NAME used MOVE!" messages produced by the attackstring script command, but when a message is suppressed, the flag is also cleared. The "scope" column below lists how long the hitmarker remains active, in order from the soonest possible clear to the latest possible clear.
| Name | Scope | Purpose |
|---|---|---|
HITMARKER_WAKE_UP_CLEAR |
action | Cleared at the end of the current action, or by BattleScript_MoveUsedWokeUp when waking up. Never set or checked. |
HITMARKER_IGNORE_BIDE |
damage | Prevents the next to-be-inflicted damage from being stored by Bide (if the attacker happens to be using Bide). |
HITMARKER_DESTINYBOND |
next use move action battler presence |
Indicates that a Pokemon is pending being fainted by Destiny Bond. Checked and handled by tryfaintmon. Set in a few different places, in response to Destiny Bond's user having fainted. |
HITMARKER_NO_ANIMATIONS |
whole battle | Set at the start of a battle based on whether the player has disabled battle animations, whether this is a recorded battle, and whether this is a Link Battle. Alters the behavior of native code related to Pokemon animations, as well as various battle script commands that can potentially trigger animations. |
HITMARKER_IGNORE_SUBSTITUTE |
damage action |
Causes the next to-be-inflicted damage to bypass any protection afforded by Substitute. |
HITMARKER_NO_ATTACKSTRING |
turn | Suppresses the next attackstring script command. |
HITMARKER_ATTACKSTRING_PRINTED |
action | Suppresses the next attackstring script command. Gets set when the command successfully displays a message, and by battle scripts and commands that wish to suppress any such message. |
HITMARKER_NO_PPDEDUCT |
next use | Suppresses the next ppreduce script command. Used for a variety of purposes where PP should not be reduced, e.g. when Magic Coat redirects a move, and by multi-turn moves after the initial turn on which they are used. |
HITMARKER_SWAP_ATTACKER_TARGET |
move | Toggled by swapattackerwithtarget. Cleared as a moveend effect. |
HITMARKER_STATUS_ABILITY_EFFECT |
move effect action |
Indicates that the next primary move effect processed by SetMoveEffect was triggered by or blocked by an ability. In the latter case, this influences the message displayed to the user. |
HITMARKER_SYNCHRONISE_EFFECT |
move action |
Indicates that gBattlerTarget has just received a status condition (stored in gBattleStruct->synchronizeMoveEffect), and should Synchronize it to their attacker. Set by SetMoveEffect; checked and cleared when the Synchronize ability effect is processed, or otherwise cleared by moveend or at the end of the current action. |
HITMARKER_RUN |
whole battle | Set if a battler chooses to flee from battle. Suppresses the Focus Punch setup animations and messages ("Mankey is tightening its focus") that get displayed before all other actions in the current turn. The intention seems to have been to ensure that if a battler successfully flees, ending the fight, the "tightening focus" message (or messages, if multiple battlers used the move) won't display before the flee message. However, the hitmarker isn't cleared if the attempt to flee fails; in fact, it isn't cleared at all, so it persists until all hitmarkers are reset at the start of the next battle. |
HITMARKER_IGNORE_ON_AIR |
next use | The next accuracy calculation will bypass any semi-invulnerability afforded to the target by having used Fly. |
HITMARKER_IGNORE_UNDERGROUND |
next use | The next accuracy calculation will bypass any semi-invulnerability afforded to the target by having used Dig. |
HITMARKER_IGNORE_UNDERWATER |
next use | The next accuracy calculation will bypass any semi-invulnerability afforded to the target by having used Dive. |
HITMARKER_UNABLE_TO_USE_MOVE |
turn | Indicates that a Pokemon failed to use its chosen move, usually due to an attack canceller. This is checked within moveend and used to prevent a multi-target move from hitting any further targets. |
HITMARKER_PASSIVE_DAMAGE |
damage action turn |
Indicates that the damage being inflicted should not trigger HP regen from Shell Bell, and should not influence moves like Counter or Mirror Move. |
HITMARKER_DISOBEDIENT_MOVE |
whole battle | Set if a traded Pokemon disobeys the player's orders specifically by using a move other than the one they were instructed to use. |
HITMARKER_PLAYER_FAINTED |
turn | Provided for the benefit of Potentially set by |
HITMARKER_ALLOW_NO_PP |
next use | The next time attack cancellers are run, the canceller for being out of PP will not be checked or executed. This hitmarker exists so that when Magic Coat or Snatch are used to redirect an enemy's move, the redirected move doesn't fail if this was the last PP that Magic Coat or Snatch had. The battle scripts for Magic Coat and Snatch set the hitmarker before executing the redirected move. |
HITMARKER_GRUDGE |
end-turn effect | Suppresses the effect of Grudge, so that Pokemon that faint from things like weather effects don't drain anyone's PP. Cleared after each end-turn effect is checked and processed. |
HITMARKER_OBEYS |
action | Set if the Pokemon obeys the command it's been given during the current action (as opposed to a traded Pokemon randomly disobeying). This is used to prevent the attack canceller for disobedience from running multiple times, should the Pokemon run multiple move scripts during a single action (e.g. because they used a multi-target move, or a move like Metronome that triggers a second move). |
HITMARKER_NEVER_SET |
action | Cleared as part of a large group. Never set or checked. |
HITMARKER_CHARGING |
action | Used for multi-turn moves that only hit on the next turn, like Fly and Solar Beam, to indicate that the Pokemon isn't ready to strike yet. This is used to defer type calculations and similar behavior until the move actually hits. |
HITMARKER_FAINTED(battler) |
battler presence | Tracks whether a given battler is currently fainted (or has lost during the Battle Arena) and has yet to be switched out. If the owning trainer has no Pokemon they can switch in, then this is cleared by Broadly speaking, this is used by any battle script functionality which cares whether a battler has fainted. This includes but is not limited to |
HITMARKER_FAINTED2(battler) |
battler presence | Identical to HITMARKER_FAINTED; checks the same bits, but with a different expression. |
Hitmarkers that are scoped to their "next use" are cleared by whatever code checks and reacts to their having been set. The code in question will be described in the table.
Hitmarkers that are scoped to the current damage are cleared by datahpupdate, but only if the hitmarker is responsible for preventing damage. (This is a subset of "next use." I just don't want to have to repeatedly mention datahpupdate.)
Hitmarkers that are scoped to the current move effect are generally cleared if they influence how the effect is applied (e.g. the display or suppression of a message).
Hitmarkers that are scoped to the current move are generally cleared by anything that cleans up move effects.
Hitmarkers that are scoped to the current action are cleared by HandleAction_ActionFinished and HandleAction_NothingIsFainted. (The latter function runs if a battler does nothing or if the battler is fainted, not "if nothing is fainted.")
Hitmarkers that are scoped to the current turn are cleared by BattleTurnPassed and, potentially, RunTurnActionsFunctions. It depends on whether they may be influenced by events occurring at the very end of a turn, such as Perish Song and Wish.
Hitmarkers that are scoped to a battler's presence are cleared when any battler switches in or out.
Move effect
"Move effects" are non-damaging effects that a move can have on the user or the target, such as altering stats, inflicting a status condition, curing a status condition, stealing an item, or more. They're defined in the MOVE_EFFECT_... macros, and are functionally an enum with the high bits acting as flags.
The typical flow for move effects is as follows: the move's battle script sets gBattleCommunication[MOVE_EFFECT_BYTE] to the desired effect (using the convenience setmoveeffect macro). Then, the battle scripts apply the effect via the (misleadingly named) seteffectprimary command, which applies and resets the current move effect. Some abilities (e.g. Effect Spore) work by setting the desired effect in native code and then invoking a script that runs seteffectsecondary, which applies (not sets) the current effect. Both "primary" and "secondary" effects are applied via an (also misleadingly named) internal helper function named SetMoveEffect.
The sole difference between "primary" and "secondary" effects is that if a primary effect is blocked, a battle script is executed to display an appropriate message, whereas "secondary" effects are blocked silently.
Primary/secondary difference example
Here's part of the implementation for move effects that apply the Poison status to their target:
// The "Immunity" ability was checked just before this excerpt.
if ((IS_BATTLER_OF_TYPE(gEffectBattler, TYPE_POISON) || IS_BATTLER_OF_TYPE(gEffectBattler, TYPE_STEEL))
&& (gHitMarker & HITMARKER_STATUS_ABILITY_EFFECT)
&& (primary == TRUE || certain == MOVE_EFFECT_CERTAIN))
{
BattleScriptPush(gBattlescriptCurrInstr + 1);
gBattlescriptCurrInstr = BattleScript_PSNPrevention;
gBattleCommunication[MULTISTRING_CHOOSER] = B_MSG_STATUS_HAD_NO_EFFECT;
RESET_RETURN
}
if (IS_BATTLER_OF_TYPE(gEffectBattler, TYPE_POISON))
break;
if (IS_BATTLER_OF_TYPE(gEffectBattler, TYPE_STEEL))
break;
if (gBattleMons[gEffectBattler].status1)
break;
if (gBattleMons[gEffectBattler].ability == ABILITY_IMMUNITY)
break;
statusChanged = TRUE;
break;
Poison- and Steel-type Pokemon can't be poisoned. If a Poison- or Steel-type Pokemon avoids being poisoned by a Pokemon's ability, and the poisoning is a primary effect, then we want to display a message. Otherwise, we just want the poisoning to be blocked silently. Abilities like Poison Point act as secondary effects, so they wouldn't show a message; but abilities like Synchronize may potentially try to poison as a primary effect, so they might show a message. We only display a message for abilities because in the vanilla game, all moves that apply poisoning as a primary effect are Poison-type, so "It doesn't affect STEELIX" would already display for that reason.
You can see that some of the checks are duplicated in the code above. We can refactor the code to avoid duplicated checks:
if ((IS_BATTLER_OF_TYPE(gEffectBattler, TYPE_POISON) || IS_BATTLER_OF_TYPE(gEffectBattler, TYPE_STEEL))
{
// Subject is immune to poisoning. Should we bother to show a message?
if (primary != TRUE && certain != MOVE_EFFECT_CERTAIN)
break;
// In vanilla, moves that inflict poisoning as a primary effect are Poison-type, so type effectiveness
// messages are sufficient; we don't need an additional message.
if (!(gHitMarker & HITMARKER_STATUS_ABILITY_EFFECT))
break;
BattleScriptPush(gBattlescriptCurrInstr + 1);
gBattlescriptCurrInstr = BattleScript_PSNPrevention;
gBattleCommunication[MULTISTRING_CHOOSER] = B_MSG_STATUS_HAD_NO_EFFECT;
RESET_RETURN
}
// Do nothing if the subject already has a status condition.
if (gBattleMons[gEffectBattler].status1)
break;
// Imagine we refactored "Immunity" the same way, so we don't check that down here for the "no message"
// case anymore.
statusChanged = TRUE;
break;
Multi Battle
Similar to a Double Battle, with two teams of two Pokemon battling against each other on the battlefield, except that each Pokemon is commanded by a separate Trainer: the battle is between four Trainers, each bringing a party of up to three Pokemon.
Outcome
The result of a battle. This is stored in gBattleOutcome, and its value is one of the B_OUTCOME_... constants. The outcome can be queried by code that runs after a battle, and is also checked by the battle cleanup code. For example, the player's Pokemon can only evolve from level-ups if the outcome is B_OUTCOME_WON.
Position
A position on the battlefield, described as the combination of a side and a flank. This is a two-bit value, where the least-significant bit (1) is the side and the most-significant bit (2) is the flank.
-
Side: Player/Opponent
Battlefield sides are relative to the local player, so in multiplayer, players will regard the same battler as being on different sides.
B_POSITION_PLAYER_LEFTis always the position of the local player's left-flank Pokemon, no matter what ID that battler would have.Battlefield sides can have "side statuses," tracked in
gSideStatuses. This tracks things like Mist, Reflect, and Spikes. -
Flank: Left/Right
Flanks are relative to each side. The local player's left-flank Pokemon is displayed on the left half of the screen. The opponent's left-flank Pokemon will be displayed on the right half of that player's screen, because the player is viewing them from the front; were they viewing that opponent from behind, it'd be on the left half of the screen. (This, of course, makes flanks absolute for each linked GBA: each regards the same Pokemon as being on the left flank versus the right flank.)
Status
Broadly, a condition which affects a Pokemon or a side of the battlefield. There are multiple kinds of conditions, with different characteristics.
Status1
These are primary status conditions — mutually exclusive conditions that persist past the end of a battle, and are displayed on a Pokemon's healthbox and in the party menu. When the term "status" is used in-game and by typical players, it refers to these conditions alone. They include:
- Burn
- Freeze
- Paralysis
- Poisoning (including being badly poisoned by Toxic)
- Sleep
Status2
These are potentially overlapping status conditions that only last while a Pokemon is out on the field. They include:
- Bide (applied to the user)
- Bound
- Confusion
- Curse
- Defense Curl
- Destiny Bond
- Flinching
- Focus Energy
- Foresight
- Infatuation
- Rage
- Substitute
- Torment
- Transform
- Uproar (applied to the user)
- Being locked into a multi-turn move
- Being prevented from fleeing or switching out
- Recharging after moves like Hyper Beam
Status3
These are potentially overlapping status conditions that only last while a Pokemon is out on the field. Status1 and Status2 conditions can have animations, but Status3 conditions cannot. Beyond that, there's no clear pattern to what makes something Status2 versus Status3.
Status3 conditions include:
- Charge
- Grudge
- Leech Seed
- Imprisoning others
- Minimize
- Perish Song
- Rooted
- Yawn
- Semi-invulnerable
- Mid-air (Fly)
- Underground (Dig)
- Underwater (Dive)
- Type diminishing (applied to user)
- Mud Sport
- Water Sport
- Sure Hit
Turn
The period of time in which all battlers perform their actions.
Details
Multi Battle parties
The battle engine keeps track of two parties, each containing up to six (PARTY_SIZE) Pokemon. How, then, is the engine capable of managing the four half-parties that participate in a Multi Battle?
During a Multi Battle, the game mashes each pair of allied half-parties together into a single full party. Parties are glued together end-to-end, with party slots 0, 1, and 2 belonging to the left-flank player, and slots 3, 4, and 5 belonging to the right-flank player. During a singleplayer Multi Battle, such as the one at Mossdeep Space Center where Steven acts as an ally, the first three Pokemon in gPlayerParty belong to the player, and the last three belong to their ally. If we visualize these slots the way they're displayed in the Party Menu, then they'll look like this: the player on the top, the ally on the bottom, and the currently-fielded Pokemon on the left.
| 0 | 1 |
|---|---|
| 2 | |
| 3 | 4 |
| 5 |
During a Link Multi Battle, the player may end up on the right flank instead of the left flank. In that case, they own the last three Pokemon.
The Party Menu, however, prefers a different ordering: it wants the Pokemon that are deployed to the field to be at the start of the party, in slots 0 and 1; and it wants the player's party to always be at the top of the screen. This makes it easier for the Party Menu to handle Double Battles and Multi Battles uniformly, since both battle types use essentially the same visual layout and in a Double Battle, party indices 0 and 1 would be on the battlefield.
| 0 | 2 |
|---|---|
| 3 | |
| 1 | 4 |
| 5 |
To help normalize interactions between the Party Menu, the battle engine, and the battle controllers, each battler stores a subjective view of the coalesced party in gBattleStruct->battlerPartyOrders[battler], an array of three (PARTY_SIZE / 2) bytes. Each byte is split in half, with each half storing a party slot number. These mappings are initialized in BufferBattlePartyOrder in party_menu.c, and can be used to translate party slot indices so that they're handled consistently between the Party Menu and the battle engine.
For the left-flank player, the mapping is as follows:
battlerPartyOrders value |
Map from | Map to | Description | ||
|---|---|---|---|---|---|
(0 << 4) | 3 |
0 | 3 | 0 | 1 | Map player's first Pokemon (gPlayerParty[0]) and ally's first Pokemon (gPlayerParty[3]) to the front of the party. |
(1 << 4) | 2 |
1 | 2 | 2 | 3 | Map player's second and third Pokemon to the top half of the party. |
(4 << 4) | 5 |
4 | 5 | 4 | 5 | Map ally's second and third Pokemon to the bottom half of the party. |
For the right-flank player, the mapping is flipped horizontally, to ensure that the player's fielded Pokemon is in the top-left box despite the player occupying the right flank of the battlefield:
battlerPartyOrders value |
Map from | Map to | Description | ||
|---|---|---|---|---|---|
(3 << 4) | 0 |
3 | 0 | 0 | 1 | Map player's first Pokemon (gPlayerParty[3]) and ally's first Pokemon (gPlayerParty[0]) to the front of the party. |
(2 << 4) | 1 |
2 | 1 | 2 | 3 | Map player's second and third Pokemon to the top half of the party. |
(5 << 4) | 4 |
5 | 4 | 4 | 5 | Map ally's second and third Pokemon to the bottom half of the party. |
multi-target moves
During Double Battles, some moves can hit multiple targets. For example, Surf hits the user's ally and both foes, and Growl hits both foes. How does this work?
If you pay close attention, you can see that the game seems to treat these moves as if they're being used by the battler against each individual target — as if a Zigzagoon who uses Growl is using it twice during a single turn, each use on a different target. Well, that's pretty much exactly what happens! The moveend script command can process one, several, or all move-end effects. The last of these is MOVEEND_NEXT_TARGET, which handles multi-target moves. If the current battle is a Double Battle, and the move targets both enemies, then moveend will clean up state for the current move (e.g. queued move effects), queue the move's battle script to run a second time, and set HITMARKER_NO_ATTACKSTRING so that "Zigzagoon used Growl!" doesn't display a second time.
This implementation seems hacky, and it makes some battle enhancements impossible. For example, because Growl is, in essence, being used separately on each target, it's not possible to combine the stat loss animations and messages for both targets and play them at the same time. You can't display, "Poochyena and Aron's Attack fell;" each target is handled separately, so Double Battles feel slow by the standards of modern gaming. However, running the move script twice, with different targets, is the safest way to guarantee that multi-target moves will affect each target uniformly, while allowing variations as appropriate within the implementations for each individual battle script command. (For example, attackanimation counts the number of times it's run and how many targets were hit, and it only plays the move animation for the first target that the move hits. If the move hits both targets, you see one animation. If the move misses the first target and hits the second, you see the move animation after being told about the miss.) It also means that Double Battles can be bolted onto the engine, and moves can quickly be changed to single-target or multi-target, without needing to rewrite the entire move script. Theoretically, you could add Triple Battles to the engine without needing to change very many move scripts. It just works.
scoped to a single action
Some state is only tracked for the duration of a single action.
Here's an example of how this may be useful. Some move effects, like Memento, are capable of lowering multiple stats. However, if a battle script command attempts to lower both Attack and Special Attack, there are five possible outcomes:
- Both stats are successfully lowered
- Attack is already at its lowest, so only Special Attack is lowered
- Special Attack is already at its lowest, so only Attack is lowered
- Both stats are already at their lowest, so neither is lowered
- The target is protected from stat-lowering effects by Mist or by their ability
If we look at the battle script for Memento, we first see this:
setbyte sSTAT_ANIM_PLAYED, FALSE
playstatchangeanimation BS_TARGET, BIT_ATK | BIT_SPATK, STAT_CHANGE_NEGATIVE | STAT_CHANGE_BY_TWO | STAT_CHANGE_MULTIPLE_STATS
playstatchangeanimation BS_TARGET, BIT_ATK, STAT_CHANGE_NEGATIVE | STAT_CHANGE_BY_TWO
setstatchanger STAT_ATK, 2, TRUE
statbuffchange STAT_CHANGE_ALLOW_PTR, BattleScript_EffectMementoTrySpAtk
@ Greater than B_MSG_DEFENDER_STAT_FELL is checking if the stat cannot decrease
jumpifbyte CMP_GREATER_THAN, cMULTISTRING_CHOOSER, B_MSG_DEFENDER_STAT_FELL, BattleScript_EffectMementoTrySpAtk
printfromtable gStatDownStringIds
waitmessage B_WAIT_TIME_LONG
BattleScript_EffectMementoTrySpAtk:
playstatchangeanimation BS_TARGET, BIT_SPATK, STAT_CHANGE_NEGATIVE | STAT_CHANGE_BY_TWO
setstatchanger STAT_SPATK, 2, TRUE
statbuffchange STAT_CHANGE_ALLOW_PTR, BattleScript_EffectMementoTryFaint
@ Greater than B_MSG_DEFENDER_STAT_FELL is checking if the stat cannot decrease
jumpifbyte CMP_GREATER_THAN, cMULTISTRING_CHOOSER, B_MSG_DEFENDER_STAT_FELL, BattleScript_EffectMementoTryFaint
printfromtable gStatDownStringIds
waitmessage B_WAIT_TIME_LONG
The sSTAT_ANIM_PLAYED value is a run-once flag for the playstatchangeanimation command. We first try playing an animation for lowering both stats; if this animation succeeds, then that run-once flag will be set, and the next playstatchangeanimation command won't do anything. However, if we can't play an animation for both stats, then we try playing an animation for Attack only. Next, we queue (setstatchanger) and apply (statbuffchange) a stat change for Attack only. If the change fails, we jump ahead to code that tries to lower Special Attack, whereas if it succeeds, we display a message and then fall through to the code that lowers Special Attack.
This code structure handles our first four cases: lowering both stats, lowering only one stat, or failing to lower either stat because both are at their lowest. But how do we handle the case of the target being protected from stat-lowering effects?
If we look at the source code for the statbuffchange command, we'll see that it relies on a helper function: ChangeStatBuffs. That function uses gSpecialStatuses[gActiveBattler].statLowered as a run-once flag, to call the appropriate battle script that displays a message saying that the target's stats are protected. The run-once flag here ensures that if this battle script runs statbuffchange twice (because both of the target's stats can be lowered), we don't display "GASTLY is protected by MIST!" twice. This flag will be cleared at the end of the current action, i.e. when Memento has fully run its course. If, hypothetically, both sides of the battlefield are protected by Mist, and both combatants use stat-lowering effects, then each will see exactly one error message. If this is a Double Battle, and all four battlers use stat-lowering effects, then each battler will see exactly one error message.