Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/smogon/pokemon-showdown-client/llms.txt

Use this file to discover all available pages before exploring further.

The Pokémon Showdown battle engine is split into two clearly separated concerns: state and presentation. Battle (in battle.ts) is a pure state machine that ingests pipe-delimited protocol messages and maintains a model of the current game. BattleScene (in battle-animations.ts) reads that model and renders animations, sprites, and effects to the DOM. This split means Battle can run headlessly in the replay viewer, automated tests, or any context without a DOM, simply by substituting BattleSceneStub for the real renderer.
Licensing note: The battle-*.ts files are individually licensed MIT, while the full Pokémon Showdown client is AGPLv3. The MIT boundary means you can embed just the battle replay engine in other projects without the copyleft requirement applying to your whole codebase.

Battle: the state machine

export class Battle {
  scene: BattleSceneStub;   // BattleScene in the browser, BattleSceneStub elsewhere

  // Queue management
  stepQueue: string[];
  preemptStepQueue: string[];
  currentStep: number;
  seeking: number | null;   // null = not seeking, 0 = start, Infinity = end

  // Game state
  turn: number;             // -1 before |start|, 0 after Team Preview, ≥1 in-battle
  weather: ID;
  pseudoWeather: WeatherState[];
  sides: Side[];
  p1: Side;
  p2: Side;
  gen: number;
  tier: string;
  gameType: 'singles' | 'doubles' | 'triples' | 'multi' | 'freeforall' | 'rotation';

  // Status
  ended: boolean;
  paused: boolean;
  isReplay: boolean;
  atQueueEnd: boolean;

  // Player perspective
  mySide: Side;
  nearSide: Side;
  farSide: Side;
  myPokemon: ServerPokemon[] | null;
}

Constructing a Battle

const battle = new Battle({
  $frame: $('#battle-frame'),
  $logFrame: $('#battle-log'),
  id: 'battle-gen9randombattle-12345' as ID,
  paused: false,
  isReplay: false,
});
Pass undefined for $frame and $logFrame to get a headless battle with BattleSceneStub — useful for unit testing or server-side rendering.

BattleScene: the animation renderer

BattleScene implements the BattleSceneStub interface and renders everything visible in a battle: sprites, HP bars, weather effects, move animations, and the message bar.
export class BattleScene implements BattleSceneStub {
  battle: Battle;
  animating: boolean;
  acceleration: number;

  /** Sprite generation (not necessarily the battle's gen) */
  gen: number;
  mod: string;

  /** 1 = singles, 2 = doubles, 3 = triples */
  activeCount: number;

  // jQuery-wrapped DOM layers
  $frame: JQuery;
  $battle: JQuery;
  $bg: JQuery;
  $sprite: JQuery;
  $sprites: [JQuery, JQuery];
  $fx: JQuery;
  $messagebar: JQuery;

  log: BattleLog;
  bgm: BattleBGM | null;
  tooltips: BattleTooltips;
}

animating

When false, the scene skips all animation timers and renders frames instantly. Used during seeking/scrubbing.

acceleration

Multiplier applied to animation durations. Set higher when fast-forwarding through a long replay.

gen

Controls which sprite sheet and background art is used. Independently of the actual battle’s generation.

mod

Identifies the mod (e.g. 'digimon') for mod-specific sprite overrides.

How a battle message flows end-to-end

1

Server sends a protocol message

The game server sends a newline-delimited block prefixed with the room ID:
>battle-gen9randombattle-12345
|move|p1a: Pikachu|Thunderbolt|p2a: Eevee
|-damage|p2a: Eevee|60/100
|turn|2
2

PSConnection routes it to PS.receive()

PS.receive() strips the room prefix and dispatches each line to the appropriate PSRoom via room.receiveLine(args).
// In PS.receive() — client-main.ts
const args = BattleTextParser.parseLine(line);
room?.receiveLine(args);
3

BattleRoom passes lines to Battle

BattleRoom (a subclass of ChatRoom) holds a Battle instance. Protocol lines are added to battle.stepQueue.
// In BattleRoom.receiveLine (panel-battle.tsx)
this.battle.add(line);
4

Battle updates its state model

As the queue plays, Battle updates turn, weather, sides, pokemon, etc. It calls methods on this.scene to trigger animations at the right moment.
5

BattleScene renders the frame

BattleScene animates sprite movement, HP bar changes, weather overlays, and queues the BGM fade-in/out via BattleBGM.

BattleTextParser

BattleTextParser (in battle-text-parser.ts) converts raw pipe-delimited protocol lines into typed Args tuples that the rest of the engine operates on.
export class BattleTextParser {
  // Parse a single "|cmd|arg1|arg2|..." line into an Args tuple
  static parseLine(line: string): Args;

  // Parse name, group, and status from a formatted player string
  static parseNameParts(name: string): { group: string; name: string; away: boolean; status: string };
}

Protocol line examples

// "|move|p1a: Pikachu|Thunderbolt|p2a: Eevee"
// → ['move', 'p1a: Pikachu', 'Thunderbolt', 'p2a: Eevee']

// "|turn|3"
// → ['turn', '3']

// "|-damage|p2a: Eevee|60/100 par"
// → ['-damage', 'p2a: Eevee', '60/100 par']
KWArgs are keyword arguments appended after the positional args in the form [from] move name, [of] pokemon, etc. BattleTextParser separates these for you.

BattleChoiceBuilder

When a player receives a request from the server, BattleChoiceBuilder helps build the response string incrementally — one Pokémon’s choice at a time.
export class BattleChoiceBuilder {
  request: BattleRequest;
  noCancel: boolean;
  choices: string[];          // completed choices in string form
  current: BattleMoveChoice;  // in-progress move choice

  constructor(request: BattleRequest);

  toString(): string;   // "move 1, switch 3" etc.
  isDone(): boolean;
  isEmpty(): boolean;
  index(): number;      // which Pokémon's choice we're building
  requestLength(): number;
}

Request types

export type BattleRequest =
  | BattleMoveRequest    // requestType: 'move'
  | BattleSwitchRequest  // requestType: 'switch'
  | BattleTeamRequest    // requestType: 'team'
  | BattleWaitRequest;   // requestType: 'wait'

export interface BattleRequestActivePokemon {
  moves: {
    name: string; id: ID; pp: number; maxpp: number;
    target: Dex.MoveTarget; disabled?: boolean;
  }[];
  maxMoves?: { name: string; id: ID; target: Dex.MoveTarget; disabled?: boolean; }[];
  zMoves?: ({ name: string; id: ID; target: Dex.MoveTarget; } | null)[];
  canDynamax?: boolean;
  gigantamax?: string;
  canMegaEvo?: boolean;
  canTerastallize?: string;
  trapped?: boolean;
}

Building a move choice

const builder = new BattleChoiceBuilder(moveRequest);

// Choose move slot 1 (1-based), no target needed in singles
builder.choose('move 1');

if (builder.isDone()) {
  // Send "move 1" to the server
  room.send(`/choose ${builder.toString()}`);
}

BattleSound and BattleBGM

Audio in battles is managed by two collaborating objects: BattleSound (a singleton that owns the audio cache and the active BGM list) and BattleBGM (a per-battle object representing one music track).
export class BattleBGM {
  url: string;
  loopstart: number;   // ms
  loopend: number;     // ms
  isPlaying: boolean;
  isActuallyPlaying: boolean;
  willRewind: boolean;

  play(): void;      // rewind and resume
  resume(): void;    // resume from current position
  pause(): void;
  stop(): void;      // pause + mark to rewind on next play
  destroy(): void;   // remove from BattleSound.bgm and stop
}

export const BattleSound = new class {
  bgm: BattleBGM[];
  effectVolume: number;
  bgmVolume: number;
  muted: boolean;

  loadBgm(url: string, loopstart: number, loopend: number,
          replaceBGM?: BattleBGM | null): BattleBGM;
  deleteBgm(bgm: BattleBGM): void;
  currentBgm(): BattleBGM | false | null;
  playEffect(url: string): void;
  setMute(muted: boolean): void;
  setBgmVolume(bgmVolume: number): void;
  setEffectVolume(effectVolume: number): void;
};
When multiple battle tabs are open, only the first BattleBGM in BattleSound.bgm that isPlaying is isActuallyPlaying. The others are silenced but keep their play state, so they resume correctly when the first battle is closed or paused.

BGM lifecycle

// Load BGM for a new battle (loopstart/end in ms)
const bgm = BattleSound.loadBgm(
  'audio/xy-rival.mp3', 14_020, 123_548
);

bgm.play();    // start from the beginning
bgm.pause();   // pause mid-track
bgm.stop();    // pause and mark to rewind on next play
bgm.destroy(); // remove from the global list entirely
BattleSound.currentBgm() returns false if BGM is muted or volume is zero, null if nothing is playing, or the active BattleBGM instance.

BattleTooltips

BattleTooltips (in battle-tooltips.ts) generates the HTML content for the hover tooltips shown over Pokémon sprites and move buttons during a live battle or replay.
export class BattleTooltips {
  battle: Battle;

  constructor(battle: Battle);

  // Returns an HTML string for the given Pokémon
  showPokemonTooltip(pokemon: Pokemon, isActive?: boolean, isFlipped?: boolean): string;

  // Returns an HTML string for a move
  showMoveTooltip(move: Dex.Move, isZ?: boolean, pokemon?: Pokemon,
                  serverPokemon?: ServerPokemon): string;
}
Tooltips are populated with live HP, status conditions, stat stage modifiers, and ability/item descriptions pulled from battle-dex.ts.

Build docs developers (and LLMs) love