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.

BattleTextParser turns raw Pokémon Showdown protocol lines into structured Args tuples and human-readable battle log HTML. It is used by both the full client (inside BattleLog) and the standalone replay viewer — the parser itself has no dependencies on battle state or the DOM, only an optional dependency on the BattleText locale table for message templates. Protocol lines are first split into Args by parseLine or parseBattleLine, then dispatched through parseArgs/parseArgsInner to produce localised text with player and Pokémon names filled in.
BattleTextParser is MIT-licensed and carries no Node.js runtime dependencies. It can be used in environments without a DOM as long as BattleText locale data is available.

Type Exports

export type Args = [string, ...string[]];
export type KWArgs = { [kw: string]: string };
export type SideID = 'p1' | 'p2' | 'p3' | 'p4';
Args
[string, ...string[]]
A non-empty tuple representing a parsed protocol line. The first element is always the command string (e.g. 'move', 'switch', '-damage'). Subsequent elements are the positional arguments extracted from the line.
export type Args = [string, ...string[]];
// e.g. ['move', 'p1a: Pikachu', 'Thunderbolt', 'p2a: Charizard']
KWArgs
{ [kw: string]: string }
Keyword arguments extracted from the trailing [keyword: value] suffix syntax on battle lines. The bracket-enclosed key maps to the trimmed value (or '.' when the value is empty, so the key evaluates as truthy).
export type KWArgs = { [kw: string]: string };
// e.g. { from: 'ability:Intimidate', of: 'p2a: Landorus-Therian' }
SideID
'p1' | 'p2' | 'p3' | 'p4'
Identifies one of the four possible battle sides. p1/p2 are the primary players; p3/p4 appear in multi-battles and are allies of p1/p2 respectively.

BattleTextParser Class

export class BattleTextParser {
  p1: string;      // Player 1 display name (default 'Player 1')
  p2: string;      // Player 2 display name
  p3: string;      // Player 3 display name
  p4: string;      // Player 4 display name
  perspective: SideID;
  gen: number;     // current generation (default 9)
  turn: number;    // current turn counter (default 0)
  curLineSection: 'break' | 'preMajor' | 'major' | 'postMajor';
  lowercaseRegExp: RegExp | null | undefined;
}

Constructor

new BattleTextParser(perspective?: SideID)
perspective
SideID
The side from whose perspective narrative text is generated. Defaults to 'p1'. Affects pronoun choices in templates — p1’s Pokémon are referred to as “your Pokémon” and p2’s as “the opposing Pokémon” when the perspective is 'p1'.

Properties

p1 / p2 / p3 / p4
string
Player display names. Set automatically when a |player| protocol line is parsed. These strings are pre-escaped for use as the second argument to String.prototype.replace (dollar signs are doubled).
perspective
SideID
The viewing side. Determines which player’s Pokémon are called “your Pokémon” vs “the opposing Pokémon” in log templates.
gen
number
Current generation, updated when a |gen| protocol line is parsed. Defaults to 9. Affects certain message templates that differ between generations.
turn
number
Current turn number, updated when a |turn| protocol line is parsed.
curLineSection
'break' | 'preMajor' | 'major' | 'postMajor'
Tracks where in a turn’s message sequence the parser currently is. Used by sectionBreak to insert blank-line separators at the right points in the formatted log.
ValueMeaning
'break'Between turns or at the start of the log
'preMajor'Before the main action of a turn (e.g. weather messages before a move)
'major'A primary action line (move, switch)
'postMajor'Consequential lines after the main action (damage, status, etc.)
lowercaseRegExp
RegExp | null | undefined
Lazily compiled regular expression used by fixLowercase to capitalise the first letter of Pokémon/team template prefixes when they appear at the start of a log line. undefined means not yet initialised; null means no lowercasing is needed for the current locale.

Static Methods

BattleTextParser.parseLine(line)

Parses a single raw protocol line and returns an Args tuple. This is the primary entry point for splitting protocol data.
static parseLine(line: string, noDefault: true): Args | null;
static parseLine(line: string): Args;
line
string
required
A raw protocol line. Lines starting with | are battle/room messages; lines without a leading | are treated as plain text with command ''.
noDefault
true
When true, returns null for lines whose command is not in the known special-cased set (instead of falling back to a generic split). Use this to test whether a line needs custom handling.

How parseLine Splits Lines

The method inspects the command token (the text between the first and second |) and applies one of three splitting strategies:
These commands have all their content in one trailing string — no further splitting is done.
|chatmsg|...         → ['chatmsg', '...']
|chatmsg-raw|...     → ['chatmsg-raw', '...']
|raw|...             → ['raw', '...']
|error|...           → ['error', '...']
|html|...            → ['html', '...']
|inactive|...        → ['inactive', '...']
|inactiveoff|...     → ['inactiveoff', '...']
|warning|...         → ['warning', '...']
|fieldhtml|...       → ['fieldhtml', '...']
|controlshtml|...    → ['controlshtml', '...']
|pagehtml|...        → ['pagehtml', '...']
|bigerror|...        → ['bigerror', '...']
|debug|...           → ['debug', '...']
|tier|...            → ['tier', '...']
|challstr|...        → ['challstr', '...']
|popup|...           → ['popup', '...']
||...                → ['', '...']
Split at the first two | separators so the third segment can contain embedded | characters.
|c|USER|MESSAGE           → ['c', 'USER', 'MESSAGE']
|chat|USER|MESSAGE        → ['chat', 'USER', 'MESSAGE']
|uhtml|NAME|HTML          → ['uhtml', 'NAME', 'HTML']
|uhtmlchange|NAME|HTML    → ['uhtmlchange', 'NAME', 'HTML']
|queryresponse|TYPE|JSON  → ['queryresponse', 'TYPE', 'JSON']
|showteam|SIDE|PACKED     → ['showteam', 'SIDE', 'PACKED']
Split at the first three | separators.
|c:|TIMESTAMP|USER|MESSAGE  → ['c:', 'TIMESTAMP', 'USER', 'MESSAGE']
|pm|FROM|TO|MESSAGE         → ['pm', 'FROM', 'TO', 'MESSAGE']
For all other commands (and when noDefault is falsy), the line is split on every |:
|move|p1a: Pikachu|Thunderbolt|p2a: Charizard
  → ['move', 'p1a: Pikachu', 'Thunderbolt', 'p2a: Charizard']

|switch|p2a: Garchomp|Garchomp, L50, M|341/341
  → ['switch', 'p2a: Garchomp', 'Garchomp, L50, M', '341/341']

|turn|1
  → ['turn', '1']

|-damage|p1a: Pikachu|150/250
  → ['-damage', 'p1a: Pikachu', '150/250']

|faint|p2a: Charizard
  → ['faint', 'p2a: Charizard']

| (single pipe)
  → ['done']

BattleTextParser.parseBattleLine(line)

Like parseLine but also extracts keyword arguments from the trailing [key: value] suffix syntax used by battle messages. Calls upgradeArgs to normalise old protocol syntax.
static parseBattleLine(line: string): { args: Args; kwArgs: KWArgs }
// Line: '|-damage|p1a: Pikachu|150/250|[from] item: Life Orb|[of] p1a: Pikachu'
const { args, kwArgs } = BattleTextParser.parseBattleLine(
  '|-damage|p1a: Pikachu|150/250|[from] item: Life Orb|[of] p1a: Pikachu'
);
// args:   ['-damage', 'p1a: Pikachu', '150/250']
// kwArgs: { from: 'item: Life Orb', of: 'p1a: Pikachu' }

BattleTextParser.upgradeArgs({ args, kwArgs })

Normalises legacy protocol syntax to the current format. Called by parseBattleLine. Handles deprecated commands such as -nothing (→ -activate||move:Splash), old -activate formats, Snow weather rename (SnowSnowscape), and ability/move prefix corrections.
static upgradeArgs(
  { args, kwArgs }: { args: Args; kwArgs: KWArgs }
): { args: Args; kwArgs: KWArgs }

BattleTextParser.effectId(effect?)

Strips the item:, move:, or ability: prefix from an effect string, then converts to a bare ID. Used internally to identify effects in parseArgsInner.
static effectId(effect?: string): string
BattleTextParser.effectId('ability:Intimidate');  // 'intimidate'
BattleTextParser.effectId('item: Life Orb');       // 'lifeorb'
BattleTextParser.effectId('move:Moonblast');       // 'moonblast'
BattleTextParser.effectId('Rough Skin');           // 'roughskin'

BattleTextParser.allyID(sideid)

Returns the ally side ID in a multi-battle (p1p3, p2p4). Returns '' for unknown inputs.
static allyID(sideid: SideID): SideID | ''

BattleTextParser.escapeRegExp(input)

Escapes special regular-expression characters in a string so it can be embedded safely in a RegExp pattern.
static escapeRegExp(input: string): string

BattleTextParser.escapeReplace(input)

Escapes $ characters in a string so it is safe to pass as the second argument to String.prototype.replace. Player names are pre-escaped with this method before being stored in this.p1this.p4.
static escapeReplace(input: string): string

BattleTextParser.parseNameParts(text)

Splits a raw display name into its constituent parts: group symbol (e.g. +, @), base name, away flag, and status string (the part after @).
static parseNameParts(text: string): { group: string; name: string; away: boolean; status: string }

BattleTextParser.stat(stat)

Returns the human-readable display name for a stat ID (e.g. 'atk''Attack'), sourced from the BattleText locale table.
static stat(stat: string): string

Instance Methods

parseArgs(args, kwArgs, noSectionBreak?)

The main dispatch method. Inserts a section-break newline if sectionBreak returns true for this args/kwArgs combination, then calls parseArgsInner and passes the result through fixLowercase.
parseArgs(args: Args, kwArgs: KWArgs, noSectionBreak?: boolean): string

parseArgsInner(args, kwArgs)

The core switch statement that handles every recognised battle protocol command and returns a formatted string (or '' for commands that produce no log output such as |player| and |gen|).
parseArgsInner(args: Args, kwArgs: KWArgs): string | null | undefined
Key cases handled:
CommandLog output
playerSets this.p1this.p4; returns ''
genSets this.gen; returns ''
turnSets this.turn; returns formatted “Turn N” heading
startReturns “X sent out Y” opening line
win / tieReturns victory/tie message
switch / dragReturns switch-in description
faintReturns fainting message
moveReturns “X used Y!” line
cantReturns move-prevention message
-damage / -healReturns HP change description
-status / -curestatusReturns status-infliction/cure message
-boost / -unboostReturns stat change message
-weatherReturns weather description
c / c:Returns chat message

extractMessage(buf)

Parses a multi-line protocol buffer and returns the concatenated log output. Calls parseBattleLine and parseArgs for each line.
extractMessage(buf: string): string

pokemon(pokemon) / pokemonName

Helper that resolve a Pokémon identifier string (e.g. "p1a: Pikachu") to a display string.
  • pokemonName is an arrow-function property (not a regular method) that returns the bare nickname string, pre-escaped for String.prototype.replace.
  • pokemon is a regular method that wraps the nickname in the appropriate near/far template from BattleText.
pokemonName: (pokemon: string) => string  // arrow property, escaped for String.replace
pokemon(pokemon: string): string          // formatted with BattleText template

pokemonFull(pokemon, details)

Returns a [side, displayName] tuple where side is 'p1'/'p2'/etc. and displayName combines the nickname and bold species name (e.g. "Sparky (**Pikachu**)"). Used to format switch-in log lines.
pokemonFull(pokemon: string, details: string): [string, string]

trainer(side)

Resolves a side string like "p1" to the player’s display name.
trainer(side: string): string

Protocol Line Format Reference

The Pokémon Showdown protocol uses |-delimited lines. Each line starts with |CMD where CMD is the command. Fields that contain | characters use the multi-part splitting strategies described under parseLine.
|player|p1|Alice|dawn|1200
  → ['player', 'p1', 'Alice', 'dawn', '1200']

|gen|9
  → ['gen', '9']

|tier|[Gen 9] OU
  → ['tier', '[Gen 9] OU']

|gametype|doubles
  → ['gametype', 'doubles']

|start
  → ['start']

|turn|1
  → ['turn', '1']
|switch|p1a: Gardevoir|Gardevoir, L50, F|250/250
  → ['switch', 'p1a: Gardevoir', 'Gardevoir, L50, F', '250/250']

|move|p1a: Gardevoir|Moonblast|p2a: Tyranitar
  → ['move', 'p1a: Gardevoir', 'Moonblast', 'p2a: Tyranitar']

|move|p1a: Gardevoir|Moonblast|p2a: Tyranitar|[from] ability:Serene Grace
  → args:   ['move', 'p1a: Gardevoir', 'Moonblast', 'p2a: Tyranitar']
  → kwArgs: { from: 'ability:Serene Grace' }

|faint|p2a: Tyranitar
  → ['faint', 'p2a: Tyranitar']

|drag|p2a: Garchomp|Garchomp, L50, M|341/341
  → ['drag', 'p2a: Garchomp', 'Garchomp, L50, M', '341/341']

|swap|p1a: Gardevoir|p1b: Alakazam
  → ['swap', 'p1a: Gardevoir', 'p1b: Alakazam']
|-damage|p2a: Tyranitar|241/341
  → ['-damage', 'p2a: Tyranitar', '241/341']

|-damage|p2a: Tyranitar|241/341|[from] item: Life Orb
  → args:   ['-damage', 'p2a: Tyranitar', '241/341']
  → kwArgs: { from: 'item: Life Orb' }

|-heal|p1a: Gardevoir|220/250|[from] item: Leftovers
  → args:   ['-heal', 'p1a: Gardevoir', '220/250']
  → kwArgs: { from: 'item: Leftovers' }

|-status|p1a: Pikachu|brn
  → ['-status', 'p1a: Pikachu', 'brn']

|-curestatus|p1a: Pikachu|brn
  → ['-curestatus', 'p1a: Pikachu', 'brn']
|-boost|p1a: Garchomp|atk|2
  → ['-boost', 'p1a: Garchomp', 'atk', '2']

|-unboost|p2a: Garchomp|spe|1
  → ['-unboost', 'p2a: Garchomp', 'spe', '1']

|-start|p1a: Gardevoir|confusion
  → ['-start', 'p1a: Gardevoir', 'confusion']

|-end|p1a: Gardevoir|confusion
  → ['-end', 'p1a: Gardevoir', 'confusion']
|-weather|Sandstorm|[from] ability:Sand Stream|[of] p2a: Tyranitar
  → args:   ['-weather', 'Sandstorm']
  → kwArgs: { from: 'ability:Sand Stream', of: 'p2a: Tyranitar' }

|-fieldstart|move:Electric Terrain
  → ['-fieldstart', 'move:Electric Terrain']

|-sidestart|p2|Spikes
  → ['-sidestart', 'p2', 'Spikes']
|c|+Alice|Hello everyone!
  → ['c', '+Alice', 'Hello everyone!']

|c:|1700000000|+Alice|Hello everyone!
  → ['c:', '1700000000', '+Alice', 'Hello everyone!']

|pm|Alice|Bob|Private message text
  → ['pm', 'Alice', 'Bob', 'Private message text']
|win|Alice
  → ['win', 'Alice']

|tie
  → ['tie']

|done
  → ['done']

| (single pipe — end of update block)
  → ['done']

Code Examples

import { BattleTextParser, type Args, type KWArgs } from './battle-text-parser';

const parser = new BattleTextParser('p1');

// Set player names
parser.p1 = 'Alice';
parser.p2 = 'Bob';

const lines = [
  '|player|p1|Alice|dawn|1400',
  '|player|p2|Bob|ethan|1300',
  '|gen|9',
  '|tier|[Gen 9] OU',
  '|start',
  '|switch|p1a: Gardevoir|Gardevoir, L50, F|250/250',
  '|switch|p2a: Tyranitar|Tyranitar, L50, M|341/341',
  '|turn|1',
  '|move|p1a: Gardevoir|Moonblast|p2a: Tyranitar',
  '|-damage|p2a: Tyranitar|241/341',
];

for (const line of lines) {
  const { args, kwArgs } = BattleTextParser.parseBattleLine(line);
  const text = parser.parseArgs(args, kwArgs);
  if (text) process.stdout.write(text);
}
// Output (approximate):
// Battle between Alice and Bob started!
// Alice sent out Gardevoir!
// Bob sent out Tyranitar!
// Turn 1
// Gardevoir used Moonblast!
// (100/341 HP damage)
When building a headless log processor, prefer BattleTextParser.parseBattleLine over parseLine for battle messages. It automatically strips keyword arguments from the args array and populates kwArgs, giving you a cleaner split for switch statements on args[0].

Build docs developers (and LLMs) love