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.

BattleLog (in battle-log.ts) is responsible for turning raw protocol Args tuples into human-readable, styled HTML lines displayed in the chat/log panel of every battle room and replay. It is intentionally written without jQuery, without Preact, and without ES5+ array methods that break in Internet Explorer 7 — because the exact same class powers both the modern Preact client and the legacy replay viewer that must work in IE7+.

Class overview

export class BattleLog {
  elem: HTMLDivElement;
  innerElem: HTMLDivElement;
  scene: BattleScene | null;
  preemptElem: HTMLDivElement;

  // Perspective controls pronoun choice
  perspective: -1 | 0 | 1;

  // Callback for highlighting specific lines
  getHighlight: ((line: Args) => boolean) | null;

  battleParser: BattleTextParser | null;

  atBottom: boolean;
  skippedLines: boolean;
}

Constructor

constructor(
  elem: HTMLDivElement,
  scene?: BattleScene | null,
  innerElem?: HTMLDivElement
)
When scene is provided (i.e. the log is attached to a live battle), BattleLog creates a preemptElem div for messages that should appear before animations finish, and instantiates a BattleTextParser for converting protocol lines to English. Without a scene, the log acts as a plain message renderer — suitable for the standalone replay viewer.
// In a live battle panel
const log = new BattleLog(
  document.querySelector('.battle-log') as HTMLDivElement,
  battleScene,
);

// In the standalone replay viewer (no scene)
const log = new BattleLog(
  document.querySelector('.battle-log') as HTMLDivElement
);

The perspective property

/**
 * * -1 = spectator: "Red sent out Pikachu!" / "Blue's Eevee used Tackle!"
 * *  0 = player 1:  "Go! Pikachu!"          / "The opposing Eevee used Tackle!"
 * *  1 = player 2:  "Red sent out Pikachu!" / "Eevee used Tackle!"
 */
perspective: -1 | 0 | 1 = -1;
Setting perspective to 0 or 1 enables first/second-person phrasing. BattleTextParser reads this value when generating English text, so the same protocol line produces different output depending on who is watching.
Red sent out Pikachu!
Blue's Eevee used Tackle!

Core methods

add

add(
  args: Args,
  kwArgs?: KWArgs,
  preempt?: boolean,
  showTimestamps?: 'minutes' | 'seconds'
): void
The main entry point. Accepts a parsed Args tuple (e.g. ['move', 'p1a: Pikachu', 'Thunderbolt', 'p2a: Eevee']) and appends the appropriate HTML to the log. When preempt is true, the message goes into preemptElem so it appears before the current animation finishes. Messages tagged with kwArgs.silent are silently discarded.

addDiv, addNode, message

// Create a <div> with the given className and innerHTML, then append it
addDiv(className: string, innerHTML: string, preempt?: boolean): void;

// Append a raw DOM node (more control than addDiv)
addNode(node: HTMLElement, preempt?: boolean): void;

// Append a battle-history message (also forwards to the scene's message bar)
message(message: string, sceneMessage?: string): void;
These are the internal building blocks used by add(). External callers should generally prefer add() for protocol lines, or addDiv()/message() for custom messages that are already sanitised.

Static helpers on BattleLog

// Render a timestamp span
static renderTimestamp(
  timestamp: number | null,
  showTimestamps?: 'minutes' | 'seconds' | null
): string;

BattleLog.formatName

// Format a raw format ID into a human-readable label
// e.g. 'gen9randombattle' → 'Gen 9 Random Battle'
static formatName(formatid?: string, fixGen6?: boolean): string;

Message routing inside add

add() switches on args[0] to determine how to render each line. A simplified view:
switch (args[0]) {
  case 'chat': case 'c': case 'c:':
    // parse username, message, optional timestamp
    // run formatText() on the message body
    // check highlight list for notification
    break;

  case 'join': case 'j':
  case 'leave': case 'l':
    // batch consecutive join/leave events into one line
    break;

  case 'raw':
    // addDiv('', args[1]) — trusted HTML from the server
    break;

  case 'error':
    // render in red
    break;

  // battle protocol lines are routed through BattleTextParser
  // which converts them to English sentences
}

BattleTextParser integration

When BattleLog is constructed with a scene, it creates a BattleTextParser instance (this.battleParser). Protocol lines that describe in-battle events — |move|, |switch|, |turn|, |-damage|, etc. — are parsed through battleParser.parseArgs(args, kwArgs) which returns a human-readable string that BattleLog then wraps in a <div class="battle-history"> element.
// Under the hood in BattleLog.add():
const [result] = this.battleParser!.parseArgs(args, kwArgs);
if (result) {
  this.addDiv('battle-history', result);
}

HTML sanitisation with Caja

Chat messages from other users are sanitised through Caja (the html4 and html globals loaded via battle-log-misc.js) before being inserted into the DOM. This prevents XSS while still allowing trusted formatting like bold, italics, and spoiler tags.
// The html global exposes Caja's sanitiseWithPolicy function
declare const html4: any; // Caja's html4 policy namespace
declare const html: any;  // sanitiseWithPolicy

// formatText (from battle-log-misc) does Showdown's own markdown-like
// formatting, then passes the result through Caja
declare function formatText(input: string, isTrusted?: boolean): string;
Only messages from trusted sources (the server’s |raw| protocol line, or messages from the ~ rank) bypass Caja sanitisation. All user-generated chat content must go through formatText before being inserted with innerHTML.

MD5 and formatText utilities

Two functions are declared in battle-log.ts but defined in battle-log-misc.js at runtime:
FunctionSignaturePurpose
MD5(input: string) => stringComputes an MD5 hash — used for generating Gravatar avatar URLs
formatText(input: string, isTrusted?: boolean) => stringApplies Showdown’s chat formatting (bold, italics, spoilers, links) and sanitises with Caja
export { MD5, formatText };
These are re-exported from battle-log.ts so other modules can import them from a single stable location.

Why no jQuery or Preact?

The replay viewer at replay.pokemonshowdown.com must render correctly in Internet Explorer 7. jQuery 2.x dropped IE6/7/8 support, and Preact is an ES6 module that won’t parse in IE7 at all. BattleLog therefore uses only DOM Level 1 APIs (document.createElement, appendChild, innerHTML) and for loops — no Array.prototype.forEach, no template literals in hot paths, no optional chaining. This constraint is explicitly documented in the file header.
Using direct DOM manipulation rather than a VDOM diff is significantly faster when appending thousands of log lines during replay fast-forward. The addSeekEarlierButton() optimisation caps rendered lines at 2 000 steps back from the current position to keep frame times below ~2 seconds even in 1 000-turn battles.

Auto-scroll behaviour

BattleLog tracks whether the user has scrolled up (reading old messages) via an onscroll handler:
onScroll = () => {
  const distanceFromBottom =
    this.elem.scrollHeight - this.elem.scrollTop - this.elem.clientHeight;
  this.atBottom = (distanceFromBottom < 30);
};
New messages only auto-scroll the log to the bottom when atBottom is true, preserving the user’s reading position when they scroll up to review earlier moves.

Build docs developers (and LLMs) love