The Pokémon Showdown Client is structured around three interlocking ideas: a phased loading strategy that keeps first-paint fast, a reactive model system built on a lightweight Observable pattern, and a panel/room framework that maps every piece of UI — from the main menu to a live battle — onto a uniform abstraction. Understanding how these three ideas fit together makes it much easier to navigate the codebase and contribute changes without breaking things unexpectedly.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 Four Loading Phases
The client splits its JavaScript into four distinct phases. Each phase depends on the previous one but is otherwise loaded independently. This structure keeps the initial page load lightweight and defers heavyweight assets (jQuery, battle data, sprite sheets) until the user actually needs them.client.cssclient-core.tsPSModel / PSStreamModel base classes, background model and viewclient-core.ts is intentionally dependency-free. It sets up the model infrastructure and renders the background image as quickly as possible, so users see a styled page rather than a blank white screen before the heavier bundles arrive.The main client shell. By the end of this phase the page is interactive: the user can see the main menu, the header, and an initial connection to the game server is established.
font-awesome.cssclient-main.tsPSPrefs, PSTeams, PSUser, PSRoom, and the top-level PS singletonclient-connection.tspanel-mainmenu.tsxpanel-rooms.tsxpanels.tsxPSRouter, global event listeners, and the root PSView componentpanel-chat.tsxChatRoom)panel-ladder.tsxPhase 4 does not load automatically. It loads only when the user opens a battle room or the team builder. This is where all the large data files and jQuery live.
battle.css, sim-types.css, utilichart.csspanel-battle.tsxpanel-teambuilder.tsxbattle-dex.tsDex)battle.tsjQuery is only loaded in Phase 4. All code in Phases 1–3 interacts with the DOM directly, without jQuery. When writing code for earlier phases, make sure not to use jQuery APIs and avoid constructs that crash in IE 9 (the minimum target for non-replay code).
The Observable Model System
State management in the Pokémon Showdown Client is built around three classes defined inclient-core.ts. They implement a lightweight Observable pattern similar to RxJS, but without the library overhead.
PSSubscription — handle to a single listener
PSSubscription — handle to a single listener
PSSubscription is returned whenever you subscribe to a model. Hold onto the reference if you need to unsubscribe later (e.g., when a panel unmounts).PSModel — state container (notify on change)
PSModel — state container (notify on change)
PSModel is the base for any piece of state that panels want to react to. Calling update() fans out to all current subscribers. Unlike React’s usual paradigm, PS models are mutable — you mutate the model then call update().PSPrefs, PSTeams, PSUser, and the top-level PS singleton all extend PSModel.PSStreamModel — event stream with backlog
PSStreamModel — event stream with backlog
PSStreamModel is used for things that emit a sequence of events (not just state snapshots). The key difference from PSModel is the backlog: events emitted before the first subscriber is attached are buffered and replayed when a subscriber arrives. Once drained, the backlog is set to null.PSRoom extends PSStreamModel<Args | null>, which is how server messages flow from the connection layer into each room’s panel.The Room & Panel System
Every piece of UI in the client — main menu, chat rooms, battles, the team builder, popups — is modelled as a room with an associated panel.PSRoom (model)
PSRoom is the model class for a single room. It extends PSStreamModel<Args | null>, meaning it can stream parsed server message arguments to its panel. Defined in client-main.ts:
left, right, popup, mini-window, modal-popup) that controls how they are positioned in the layout. The top-level PS singleton tracks all open rooms in PS.rooms (a roomid → PSRoom map) and the currently focused room in PS.room.
PSRoomPanel (view)
PSRoomPanel is the Preact component base class for every room’s view. It lives in panels.tsx and handles subscriptions, visibility optimization, and focus management:
ChatRoom) that extends PSRoom, and a panel class (e.g. ChatPanel) that extends PSRoomPanel and holds a reference to the model via its room prop. This separation keeps state management and rendering concerns distinct.
Registering Room Types
Room types are registered withPS.addRoomType(). Each registration associates a string type identifier with a PSRoomPanel subclass (the panel class, not the room model):
PS singleton then uses these registrations to instantiate the correct panel when the server tells the client to join a room.
The URL Router (PSRouter)
PSRouter (defined in panels.tsx) keeps the browser address bar in sync with which room is currently focused. It is instantiated as a singleton (PS.router = new PSRouter()) at the end of panels.tsx.
- History API (play.pokemonshowdown.com)
- Hash routing (testclient files)
On the live site,
PSRouter uses history.pushState and history.replaceState so room navigation produces real URL paths (/battle-gen9ou-12345, /teambuilder, etc.) and back/forward buttons work as expected.replay.pokemonshowdown.com/gen9ou-12345, psim.us/r/gen9ou-12345, and teams.pokemonshowdown.com/view/12345 are all mapped to the appropriate internal room ID (battle-gen9ou-12345, viewteam-12345, etc.) by extractRoomID().
Multi-Site Structure
The same client codebase serves multiple subdomains via different entry-point HTML files and config objects.| Site | Purpose |
|---|---|
play.pokemonshowdown.com | Main client — battles, chat, ladder, team builder |
replay.pokemonshowdown.com | Replay viewer (battle-*.ts files, MIT-licensed) |
teams.pokemonshowdown.com | Team sharing and import |
Config.routes object (typed as PSConfig['routes'] in client-main.ts) provides the correct hostname for each sub-site at runtime, allowing a single build to be used across all of them without hard-coded URLs scattered through the source.
Phased Loading Rationale
Why defer jQuery and battle data to Phase 4?
Why defer jQuery and battle data to Phase 4?
The combined weight of jQuery, the battle animation engine (
battle.ts), and the Pokémon data files (Pokédex, moves, items, abilities, sprite mappings) is substantial. The vast majority of sessions never open a battle room during that visit — players may just browse chat, check the ladder, or build a team without fighting. Loading all of that data eagerly would make every page load slower for every user, even those who never trigger a battle.By gating Phase 4 behind user intent (clicking “Battle!” or joining a battle room), the client keeps its critical-path bundle small. Phases 1 through 3 give you a fully functional chat client and team builder with a fraction of the total asset weight.Why no Maps or Sets? Why no async/await?
Why no Maps or Sets? Why no async/await?
The client targets ES3 for maximum compatibility — IE 9 for the main client, and IE 7 for battle replays. Babel 7 compiles TypeScript and modern JavaScript syntax down to ES3, but some runtime features cannot be polyfilled at ES3 without unacceptable overhead:
MapandSet— polyfillable, but plainObject(andObject.create(null)for null-prototype objects) is faster and simpler.async/await— not compilable to ES3.Promisechains are fine.- Generators and non-Array iterables — too much overhead;
for-ofonArraycompiles to zero overhead.
Array#includes, Array.isArray, String#startsWith, String#endsWith, String#includes, String#trim, Object.assign, and Object.create. These are optimized for speed over strict spec compliance.Next Steps
Quickstart
Clone the repo, run the build, and open the test client.
Introduction
Features, browser support, and licensing overview.
