Showdown Research
Showdown Research
Section titled “Showdown Research”Tier-3 ShowdownActions — the showdown { hooks { ... } } block — runs your code inside Showdown’s own JavaScript engine, with full access to its battle, pokemon, side, and field APIs. To use that effectively you need a working mental model of how Showdown works under the hood, and you need to know where to look when the docs don’t cover what you need.
This page is the long-form orientation. It’s deliberately not a copy-paste reference — the API surface is too large for that and the engine evolves between Cobblemon versions. What this page gives you is the structure: which parts of Showdown matter, how Cobblemon glues into them, and how to find the source-of-truth answer when sugar can’t help.
If you only need to write tier-1 sugar actions, you can skip this page. The bundled apply fields handle the common cases without any of this.
What Pokémon Showdown Actually Is
Section titled “What Pokémon Showdown Actually Is”Showdown is the open-source competitive Pokémon battle simulator that Smogon and the broader competitive community use. The full source lives at github.com/smogon/pokemon-showdown. For our purposes, three pieces matter:
- The simulator (
sim/) — the engine that actually runs battles. This is what Cobblemon embeds. - The data files (
data/) — everything that exists in the game:moves.ts,abilities.ts,items.ts,conditions.ts, plus per-generation overlays underdata/mods/. These are static catalogs of behavior, expressed as JS-style objects whose function-valued properties (onStart,onModifyAtk, etc.) are the engine’s hook points. - The Dex (
sim/dex.tsand friends) — the indexed loader that reads the data files into in-memoryDex.data.*tables and providesDex.forFormat(...)to get a per-format view. Cobblemon’sModdedDexinstances are this exact mechanism.
Cobblemon ships its own fork of the simulator, lightly modified, running inside the JVM via a JS engine (Rhino-style). The JS your hook bodies contain runs in the same context Showdown itself runs in, with Dex, Battles, etc. all in scope.
Why this matters for hook authors
Section titled “Why this matters for hook authors”Every hook you write in showdown { hooks { onStart = "..." } } is a function that gets attached to a condition object inside Showdown’s data tables. When the simulator evaluates a battle event — a switch-in, a stat read, a damage calc — it walks every active condition on the relevant scope (volatile, side, field, etc.) and calls each matching on... method it finds. Your hook is just one more entry in that walk.
The implication: anything you can do inside an on... function, ShowdownActions can do. The flip side: you have to know what on... functions Showdown calls and what arguments it passes. That information lives in the simulator source, not in a separate manual.
Where to Read the Source
Section titled “Where to Read the Source”The Showdown repository is the canonical reference. The four files you’ll consult most often:
| File | What’s in it |
|---|---|
sim/HANDBOOK.md | The closest thing to official docs. Walks through the lifecycle, the event system, and the on... event names. Read this first. |
sim/battle.ts | The Battle class. Methods like boost, addVolatile, setWeather, setTerrain, add (for protocol messages). Inside hooks, this is a Battle instance, so this is the API. |
sim/pokemon.ts | The Pokemon class. pokemon.species, pokemon.types, pokemon.ability, pokemon.item, pokemon.hp, pokemon.maxhp, pokemon.heal, pokemon.trySetStatus, pokemon.cureStatus, pokemon.addVolatile, pokemon.side, etc. |
data/conditions.ts | The vanilla volatile/status/weather/terrain conditions. This is the goldmine. Every entry in here is a worked example of every hook in production use. Reading the existing confusion volatile teaches you how onBeforeMove works; reading safeguard teaches you how side conditions handle status blocking; reading raindance teaches you how onWeatherModifyDamage works. |
A handful of secondary files come up:
data/abilities.ts— the abilities catalog. Every ability is a condition withon...hooks. Studyingintimidateteaches you howonStartinteracts with foes;swiftswimteaches youonModifySpesemantics;regeneratorteaches youonSwitchOut.data/moves.ts— moves are also conditions, with hooks likeonTryHit,onHit,onModifyMove. Useful when your effect needs to interact with a specific move.sim/side.ts— theSideclass.side.foe,side.active,side.addSideCondition,side.removeSideCondition. Inside hooks,pokemon.sideis aSide.sim/field.ts— theFieldclass for weather/terrain.this.fieldinside a hook reaches it.
The HANDBOOK is the only narrative document. Everything else is code. The skill to develop is “I want to do X; what existing condition does something analogous?” — find it in data/conditions.ts or data/abilities.ts, copy the structure, adapt the specifics.
How Conditions Work
Section titled “How Conditions Work”Every Showdown effect — statuses, weathers, terrains, side conditions, volatiles — is a Condition object in Dex.data.Conditions. Conditions have a stable id, a name, optional effectType and duration, and a set of function-valued event handlers.
// sketch of an existing Showdown condition (data/conditions.ts)confusion: { name: 'confusion', duration: 3, onStart(target, source, sourceEffect) { this.add('-start', target, 'confusion'); }, onBeforeMove(pokemon) { // 33% chance to hit yourself if (this.randomChance(33, 100)) { this.activate(pokemon, 'confusion'); return false; } }, onEnd(pokemon) { this.add('-end', pokemon, 'confusion'); },},When a Pokémon has confusion as a volatile, the simulator walks every event for that volatile in turn. On the volatile’s first attach, onStart runs. Before each of the host’s moves, onBeforeMove runs. When the volatile expires, onEnd runs.
A ShowdownActions tier-3 block builds the same kind of object:
showdown { conditionId = "showdownactionsmycondition" conditionName = "My Custom Condition" scope = "volatile" turnDuration = 3 hooks { onStart = """function(pokemon) { this.add('-start', pokemon, 'mycondition'); }""" onBeforeMove = """function(pokemon) { // ... your logic }""" }}ShowdownActions writes this into Showdown’s Dex.data.Conditions table and into every cached ModdedDex instance, so format-specific battles can resolve it. (Without that copy step, a singles battle’s Dex.forFormat('gen9singles') would have its own data.Conditions cache that didn’t see the registration.)
Event handler arguments
Section titled “Event handler arguments”Every on... function is called with positional arguments specific to the event. onStart(pokemon). onModifyAtk(atk, pokemon, target, move). onAnyDamage(damage, target, source, effect). The arguments are documented in the HANDBOOK and visible in the source by reading the call site of runEvent('Start', ...), runEvent('ModifyAtk', ...), etc. inside sim/battle.ts.
Inside the function body, this is the Battle instance. this.add(...) queues a protocol message. this.boost(...) issues a stat boost. this.field.setWeather(...) changes weather. this.randomChance(num, den) rolls a probability. The full set is whatever methods exist on Battle.
Return values
Section titled “Return values”A handful of events are modifier events — the value you return replaces the value being asked for. onModifyAtk(atk, pokemon) should return the new Atk; returning undefined leaves it alone. onTryHit can return false to cancel the move, null to silently fail, or undefined to let it continue. Mistakes here are subtle — a return of 0 from onModifyAtk will set the Pokémon’s Atk to zero, not “no change”.
Read the HANDBOOK section on each event family before relying on it. Modify* events especially have nuanced “additive vs. multiplicative” semantics that depend on which event family they’re in.
How Cobblemon Plugs In
Section titled “How Cobblemon Plugs In”Cobblemon’s Showdown integration is mostly transparent — a battle starts, the engine runs, the result comes back. Two patches are worth knowing about as a tier-3 author:
runSwitchre-firesonStarton every switch-in, not just the first. This is what lets sugar effects re-apply when the host comes back in. It’s also what makes the auto-applied species guard necessary — without the guard, the same effect would re-apply to a different Pokémon on swap.- A custom
|-ceremonyvolatile|pokemon|conditionIdprotocol line is emitted whenever an applied volatile has a registered Cobblemon-side handler. That’s how stat-up animations, particles, and the action’sapply.messageget translated back to clients.
Practically: any hook you write runs in the same JS engine as the rest of Showdown, but the side effects you produce — stat-up animations, battle text, particles — get reflected on Cobblemon clients through that protocol bridge.
The Hook Catalogue
Section titled “The Hook Catalogue”The hooks ShowdownActions exposes by name in the hooks { } block:
| Hook | When it fires | Common return |
|---|---|---|
onStart | When the volatile attaches (and on every switch-in for the patched runSwitch in this environment). | — |
onEnd | When the volatile is removed. | — |
onResidual | Every turn during the residual phase, after moves resolve. | — |
onRestart | When the volatile is reapplied to a host that already has it. | — |
onModifyAtk | When something asks for the host’s Atk. | New Atk value |
onModifyDef | Same, for Def. | New Def |
onModifySpA | Special Atk. | New SpA |
onModifySpD | Special Def. | New SpD |
onModifySpe | Speed. | New Spe |
The above are the convenience-named slots ShowdownActions exposes by name. Anything outside that list — onAnyDamage, onTryHit, onAfterMoveSecondary, onSwitchOut, onBeforeMove, onModifyTypePriority, you name it — still works: drop it under hooks { } with the exact name Showdown uses, and the key is forwarded verbatim into the registered condition. The full catalogue lives in sim/HANDBOOK.md and (more comprehensively) by grepping for runEvent( inside sim/battle.ts.
Every hook you provide is auto-wrapped so its fires and errors land in the Showdown log:
[ShowdownActions][<conditionId>] onStart fired[ShowdownActions][<conditionId>] onModifyAtk fired[ShowdownActions][<conditionId>] onResidual ERROR: TypeError: pokemon.foo is not a function stack=...These are visible in the Showdown log when debug = true. The print() call writes to the same channel Cobblemon uses for Cobblemon[print] lines.
Debugging and the Showdown Log
Section titled “Debugging and the Showdown Log”When debug = true, ShowdownActions emits trace lines at every stage of the pipeline. Reading these top-to-bottom is the fastest way to figure out why a hook isn’t doing what you expect.
A typical debug session, annotated:
[hook] BATTLE_STARTED_PRE id=… actors=2 candidates=8The battle-start subscriber fired with 2 actors and 8 registered actions to evaluate.
[matcher] action='rainy_swift_swim' species=pikachu molang='q.world.is_raining && q.pokemon.level >= 30' → missAction rainy_swift_swim was tested against Pikachu, the compiled Molang predicate evaluated to 0.0 (“miss”). Probably because it isn’t raining, or Pikachu is below level 30.
[matcher] action='lycanroc_dusk' rejected by property check on pikachuAction lycanroc_dusk was rejected before Molang even ran — the species property gate didn’t match. (This action’s match clause requires species = "lycanroc".)
[hook] queued 'friendship_aura' (matched on pikachu) → condition='showdownactionsfriendshipaura' side=p1 slot=0Action friendship_aura matched on Pikachu, the auto-generated condition id is showdownactionsfriendshipaura, the volatile is queued for side p1 slot 0.
[hook] registered 1 volatile(s) across 1 side(s) for battle …The volatile is now queued on the Showdown side for application at switch-in.
[ShowdownActions] propagated showdownactionsfriendshipaura to 4 ModdedDex instance(s)The condition was copied from base Dex.data.Conditions into 4 cached format-specific dexes. (This step is critical — without it, addVolatile(conditionId) at switch-in would fail to resolve.)
[ShowdownActions][showdownactionsfriendshipaura] onStart firedThe hook actually ran inside Showdown. If you don’t see this line, the volatile never attached — check the propagation log and the side+slot routing.
[applier] cobblemon-side 'friendship_aura' on pikachu/Trainer[applier] forced aspect 'aura' on pikachuCobblemon-side effects ran. (Forced aspects, console commands.)
The log lines fall into a few categories:
[hook]lines mark battle-start lifecycle steps (candidates loaded, volatiles queued, final volatile count).[matcher]lines log per-(action, pokemon)match evaluations (compiled Molang, match/miss).[applier]lines log Cobblemon-side effects (forced aspects, console commands).[ShowdownActions][<conditionId>]lines come from the auto-wrapped Showdown JS hooks (one per fire, one per error).[ShowdownActions] propagated …lines log how many format-specific Showdown dex instances each newly-registered condition was copied to.
If your hook never fires, work backwards: is the matcher rejecting it? Is the propagation step seeing a missing condition in Dex.data.Conditions? Is the side+slot mapping landing where the host actually is? The trace tells you which step failed.
A Worked Example — “Permanent +25% Atk while in”
Section titled “A Worked Example — “Permanent +25% Atk while in””Sugar can’t do this — it’s a stat-modifier hook, not a one-shot boost. The minimum tier-3 action:
id = "iron_fist_buff"target = "SELF"
match { species = "machamp" ability = "ironfist"}
showdown { conditionId = "showdownactionsmachampironfist" conditionName = "Iron Fist Reinforcement" scope = "volatile" hooks { onModifyAtk = """function(atk, pokemon) { return Math.floor(atk * 1.25); }""" }}What’s happening:
- ShowdownActions matches the action against Machamp at battle start.
- The condition is registered into
Dex.data.Conditionsand propagated to everyModdedDex. - The volatile is queued for the trainer’s side, slot 0.
- When Machamp switches in, the patched
runSwitchadds every queued volatile for that side+slot, including ours. OuronStartdoesn’t exist (we didn’t write one), so nothing visible happens at switch-in. - Whenever something in the simulator asks for Machamp’s Atk — a damage calc, an opposing move’s accuracy check, etc. — Showdown walks every active modifier on Machamp and calls each
onModifyAtkin order. Ours runs, returnsMath.floor(atk * 1.25), and the engine uses that as the new Atk for the calc. - The boost persists for the whole battle (no
turnDuration).
Things to be careful of:
-
We didn’t write a guard.
target = "SELF"would normally generate one — but only for sugar effects. With a tier-3 block, no guard is auto-generated. If Machamp switches out and a different Pokémon switches in, ouronModifyAtkwill also fire for that Pokémon, multiplying its Atk by 1.25 too. Add a guard inside the hook:function(atk, pokemon) {if (!pokemon.species || pokemon.species.id !== 'machamp') return;return Math.floor(atk * 1.25);}Or accept the leak if it’s harmless for your use case.
-
The 1.25 multiplier is multiplicative, not additive. If another modifier in the chain returns
atk * 2and yours returnsatk * 1.25, the engine composes them asatk * 2.5, notatk + (atk * 0.25 + atk * 1). The order of evaluation depends on each modifier’spriority(separate from the action priority), which defaults to 0. -
onModifyAtkfires every time something asks for the stat. If you have side effects in the function body (this.add(...), etc.), they’ll fire many times per turn. Keep modifier hooks pure.
For more invasive effects — changing move semantics, intercepting damage, mid-turn switching — you’ll need multiple coordinated hooks. The pattern is always: read the analogous existing condition in data/conditions.ts or data/abilities.ts, copy its structure, adapt the specifics.
Common Pitfalls
Section titled “Common Pitfalls”| Symptom | Likely cause |
|---|---|
Hook never fires (no [ShowdownActions][...] fired line). | Either the action didn’t match (read the matcher trace) or the condition didn’t propagate (read the propagation log). |
| Hook fires once and never again. | Working as intended — volatiles aren’t re-applied unless onRestart or oncePerBattle = false re-queues them, and the host has switched out without re-attaching the volatile. |
| Hook fires for the wrong Pokémon after a switch. | No guard. Sugar adds one for SELF/ALLIES/EVERYONE; tier-3 blocks don’t. Add an identity check inside the hook body. |
onModifyAtk returns 0 and the stat is zeroed. | The function returned 0 instead of the new value, or undefined was coerced to 0. Always explicit-return the modified value. |
Stack trace mentions Cannot read property 'X' of undefined. | pokemon.species, pokemon.side.foe.active[0], etc. can be undefined in edge cases (forfeit, fainted, between turns). Guard before dereferencing. |
addVolatile('myid') silently does nothing. | The condition isn’t registered, or the propagation step skipped a ModdedDex. The propagation step logs the count of dexes it touched — if it’s 0, registration didn’t happen yet. |
Reading List, in Order
Section titled “Reading List, in Order”If you want to write tier-3 hooks well, this is the path:
- Read
sim/HANDBOOK.mdin full. It’s not long. It’s the only narrative documentation that exists. - Skim
data/conditions.ts. Pick five or six familiar volatiles (confusion,safeguard,taunt,encore,leechseed,perishsong) and read their entries. Notice the patterns —onStartfor setup,onResidualfor per-turn ticks,onEndfor cleanup,onBeforeMovefor action interception. - Skim
data/abilities.ts. Same exercise. Pick a handful (intimidate,swiftswim,regenerator,multiscale,pressure) and read their hook patterns. - Bookmark
sim/battle.tsandsim/pokemon.ts. When you need to know whetherpokemon.Xexists or whatthis.Y(...)does, these are the answer. - Use the debug log religiously. It prints the compiled Molang, the propagation count, every hook fire, every error. If the trace shows it fired and your effect still didn’t happen, the bug is in the JS body — read the
print()output.
The skill compounds. Once you’ve read a dozen vanilla conditions, every new one fits a recognizable pattern. The first tier-3 hook is the hardest by a wide margin.
Next Steps
Section titled “Next Steps”- Raw Showdown Hooks — the catalogue and conventions, applied.
- Recipes — the bundled
custom_intimidate.confandrecipe_legendary_boss.confare good worked examples to dissect. - Molang Predicates — the gate side, which is much simpler.