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 client builds its entire reactive architecture on a lightweight observable system defined in client-core.ts. Rather than adopting React’s immutable data paradigm or a third-party state library, PS uses mutable model objects that notify subscribers whenever they change — a deliberate choice that keeps the bundle small and lets the same models power both the Preact-based UI and the IE7-compatible replay viewer.

PSModel and PSSubscription

PSModel<T> is the observable base class. It holds a list of PSSubscription<T> objects and fans out values to them on every update() call.
export class PSModel<T = null> {
  subscriptions: PSSubscription<T>[] = [];

  subscribe(listener: (value: T) => void): PSSubscription<T>;
  subscribeAndRun(listener: (value: T) => void, value?: T): PSSubscription<T>;
  update(this: PSModel): void;
  update(value: T): void;
}
PSSubscription<T> is returned by subscribe(). Hold onto it so you can call unsubscribe() when the component unmounts:
export class PSSubscription<T = any> {
  observable: PSModel<T> | PSStreamModel<T>;
  listener: (value: T) => void;

  unsubscribe(): void;
}
subscribeAndRun immediately invokes the listener with the optional value argument before returning the subscription. This is useful for initialising a view to the current state without writing the update logic twice.

Basic subscription example

import { PSModel } from './client-core';

// Create a simple counter model
const counter = new PSModel<number>();

// Subscribe and receive updates
const sub = counter.subscribe(value => {
  console.log('Counter changed to', value);
});

// Push a value to all subscribers
counter.update(42); // logs "Counter changed to 42"

// Clean up when no longer needed
sub.unsubscribe();

PSStreamModel

PSStreamModel<T> extends the observable pattern with a backlog: events emitted before any subscriber has attached are queued and replayed when the first subscriber arrives. Nullish values are never added to the backlog.
export class PSStreamModel<T = string> {
  subscriptions: PSSubscription<T>[] = [];
  backlog: NonNullable<T>[] | null = [];

  subscribe(listener: (value: T) => void): PSSubscription<T>;
  subscribeAndRun(listener: (value: T) => void, value?: T): PSSubscription<T>;
  update(value: T): void;
}
backlog starts as an empty array [] and accumulates events before any subscriber attaches. When the first subscriber calls subscribe(), the backlog is drained to that subscriber and then set to null. Subsequent subscribers do not receive the backlog — they only see future updates.
import { PSStreamModel } from './client-core';

const stream = new PSStreamModel<string>();

// These two updates happen before any subscriber is attached
stream.update('hello');
stream.update('world');

// The subscriber receives both backlogged values immediately on subscribe
stream.subscribe(msg => console.log(msg));
// → "hello"
// → "world"

PSPrefs

PSPrefs extends PSStreamModel<string | null> and manages all user preferences stored in localStorage. When a preference changes, update() is called with the key name so listeners can react selectively.
class PSPrefs extends PSStreamModel<string | null> {
  // Visual
  theme: 'light' | 'dark' | 'system';
  nogif: boolean | null;
  noanim: boolean | null;
  bwgfx: boolean | null;
  nopastgens: boolean | null;

  // Chat
  inchatpm: boolean | null;

  // Audio
  mute: boolean;
  effectvolume: number;   // 0–100
  musicvolume: number;    // 0–100

  set<T extends keyof PSPrefs>(key: T, value: PSPrefs[T] | null): void;
}

Theme

'light' | 'dark' | 'system' — controls the CSS theme. 'system' follows the OS preference.

GIF / Animation flags

nogif disables animated GIF sprites (workaround for a Chrome 64 bug). noanim disables all battle animations. bwgfx forces Gen 5 sprite style.

Past-gen graphics

nopastgens forces all sprites to use modern artwork regardless of the battle’s generation.

In-chat PMs

inchatpm controls whether private messages appear inline in the chat panel.

Reacting to preference changes

Because PSPrefs streams the key that changed, you can skip re-rendering when an unrelated preference is updated:
PS.prefs.subscribeAndRun(key => {
  // key is null on initial run, or the name of the changed pref
  if (!key || key === 'theme') {
    applyTheme(PS.prefs.theme);
  }
});

Changing a preference

// Set a specific preference
PS.prefs.set('theme', 'dark');

// Reset to default (pass null)
PS.prefs.set('nogif', null);
PSPrefs.set() persists the new value to localStorage automatically. Avoid mutating pref properties directly — always use set() so the save and subscriber notification both fire.

The PS Global Singleton

PS is the top-level application model, exported from client-main.ts. It extends PSModel (no type parameter — it notifies on any layout or state change) and owns every major sub-model.
export const PS = new class extends PSModel {
  // Sub-models
  prefs: PSPrefs;
  teams: PSTeams;
  user: PSUser;
  server: PSServer;
  connection: PSConnection | null;

  // Room management
  rooms: { [roomid: string]: PSRoom | undefined };
  roomTypes: { [type: string]: PSRoomPanelSubclass | undefined };
  room: PSRoom;       // currently focused room
  panel: PSRoom;      // currently active panel
  leftPanel: PSRoom;
  rightPanel: PSRoom | null;
  mainmenu: MainMenuRoom;

  // State flags
  isOffline: boolean;
  readonly startTime: number;   // Date.now() at load
  lastMessageTime: string;

  // Router
  router: PSRouter;

  send(msg: string, roomid?: RoomID): void;
  receive(msg: string): void;
};

ServerInfo and PSConfig

The ServerInfo interface describes the server PS is currently connected to, and PSConfig is the compile-time configuration object:
export interface ServerInfo {
  id: ID;
  protocol: string;   // 'https' | 'http'
  host: string;
  port: number;
  httpport?: number;
  altport?: number;
  prefix: string;
  afd?: boolean;
  registered?: boolean;
}

export interface PSConfig {
  server: ServerInfo;
  defaultserver: ServerInfo;
  routes: {
    root: string;
    client: string;
    dex: string;
    replays: string;
    users: string;
    teams: string;
  };
  customcolors: Record<string, string>;
  whitelist?: string[];
  testclient?: boolean;
}

Putting It All Together

1

Subscribe to a model

Call model.subscribe(listener) or model.subscribeAndRun(listener) to receive updates. Store the returned PSSubscription for cleanup.
const sub = PS.prefs.subscribe(key => {
  if (key === 'mute') updateVolume();
});
2

Update the model

Mutate the model’s properties and call update() (with or without a value) to fan out to all subscribers.
PS.prefs.set('mute', true); // internally calls this.update('mute')
3

Unsubscribe when done

Call subscription.unsubscribe() in component teardown to prevent memory leaks and stale callbacks.
// In a Preact component:
override componentWillUnmount() {
  sub.unsubscribe();
}
PS’s model system predates the broad adoption of hooks and flux stores, and it has to work in the IE7-compatible replay viewer as well as the full Preact client. PSStreamModel’s backlog feature also solves a specific problem: the connection may emit messages before the UI is ready, and those messages must not be lost. A plain Redux store would require middleware to replay actions — here it’s built into the base class.

Build docs developers (and LLMs) love