Raw Showdown Hooks
Raw Showdown Hooks
Section titled “Raw Showdown Hooks”The showdown { } block is ShowdownActions’ escape hatch for effects sugar can’t reach. It registers a Dex.data.Conditions[…] entry verbatim, with hook bodies you supply as raw JavaScript. Inside each hook, this is Showdown’s Battle instance and you have the full simulator API to work with.
This page covers the mechanics of writing those blocks. For the engine concepts behind them — what the events mean, where the source of truth lives, how to learn the API — start with Showdown Research.
Block Structure
Section titled “Block Structure”showdown { conditionId = "showdownactionsmycondition" conditionName = "My Custom Condition" scope = "volatile" turnDuration = 5 hooks { onStart = """function(pokemon) { ... }""" onModifyAtk = """function(atk, pokemon) { ... }""" onResidual = """function(pokemon) { ... }""" } extras { basePower = "100" }}Field reference:
| Field | Type | Default | Purpose |
|---|---|---|---|
conditionId | String | required | Showdown condition id. Lowercase alphanumeric (Showdown’s toID() rules). The registry normalizes anything else. By convention, prefix with your mod / category to avoid colliding with vanilla Showdown conditions. |
conditionName | String | conditionId | Display name shown in debug tooling and Showdown’s name slot. |
scope | String | "volatile" | "volatile", "side", or "field". Currently informational — ShowdownActions registers all three the same way. |
turnDuration | Int? | null | Auto-remove after N turns. null = permanent (lasts the whole battle). |
hooks | Map<String, String> | {} | Showdown event handlers, keyed by handler name. Values are raw JS function strings. |
extras | Map<String, String> | {} | Extra raw JS properties merged into the condition object literal. |
When a showdown {} block is present, ShowdownActions does not auto-generate a species/form guard for you. You’re writing the entire effect; you’re responsible for any identity checks.
The Hook Catalogue
Section titled “The Hook Catalogue”The names below are the slots that map to convenience-typed fields on ShowdownCondition. Anything else you put under hooks { } is forwarded verbatim — if Showdown calls runEvent('Foo', ...) somewhere in its source, an onFoo entry will fire when that event runs.
Lifecycle
Section titled “Lifecycle”| Hook | Args | When |
|---|---|---|
onStart | pokemon, source, sourceEffect | The volatile attaches. With the patched runSwitch in this environment, this re-fires on every switch-in. |
onEnd | pokemon | The volatile is removed — expired, cured, replaced, or battle ended. |
onResidual | pokemon | Every turn during the residual phase, after moves resolve. Standard place for per-turn ticks (poison damage, terrain damage, etc.). |
onRestart | pokemon, source, sourceEffect | The volatile is reapplied to a host that already has it. Useful for stacking semantics. |
Stat modifiers
Section titled “Stat modifiers”| Hook | Args | Returns |
|---|---|---|
onModifyAtk | atk, pokemon, target, move | New Atk. Returning undefined leaves it unchanged. Returning a number replaces it. |
onModifyDef | def, pokemon, source, move | New Def. |
onModifySpA | spa, pokemon, target, move | New Special Atk. |
onModifySpD | spd, pokemon, source, move | New Special Def. |
onModifySpe | spe, pokemon | New Speed. |
These fire every time something asks for the stat. Keep them pure — avoid this.add(...) or any other side effect inside a modifier hook, because it’ll run many times per turn.
Other commonly-used events
Section titled “Other commonly-used events”Forwarded by name through extras-style passthrough — just put them under hooks { } and they’ll wire up.
| Hook | Common use |
|---|---|
onAnyDamage | React to any damage event (own or other’s). Args: damage, target, source, effect. |
onTryHit | Intercept an incoming move. Return false to cancel, null to silently fail. Args: target, source, move. |
onAfterMoveSecondary | Fire after a move’s secondary effects resolve. |
onSwitchIn / onSwitchOut | Before/after the host swaps. |
onBeforeMove | Before the host’s move resolves. Return false to skip the move. |
onModifyMove | Mutate a move object before damage calc. |
onModifyType | Change a move’s type. |
onSetStatus / onTryAddStatus | Block or allow status application. |
onWeatherModifyDamage | Multiply damage based on active weather. |
onFractionalPriority / onModifyPriority | Adjust move priority. |
The full list lives in sim/HANDBOOK.md — read it for the precise argument shapes and return semantics. The handbook is the authoritative source; this catalogue only lists the slots ShowdownActions exposes by name.
What this and the Common Globals Are
Section titled “What this and the Common Globals Are”Inside any hook body:
| Reference | What it is |
|---|---|
this | The Battle instance. this.add(protocol, ...), this.boost(boosts, target, source), this.field, this.sides, this.activeMove, this.randomChance(num, den), this.dex, etc. |
this.field | The Field instance. this.field.setWeather('raindance', source), this.field.setTerrain('grassyterrain', source), this.field.weather, this.field.terrain. |
pokemon (typically the first arg) | The host Pokémon — a Pokemon instance. pokemon.species, pokemon.types, pokemon.ability, pokemon.item, pokemon.hp, pokemon.maxhp, pokemon.heal(n), pokemon.trySetStatus('brn', source), pokemon.cureStatus(), pokemon.addVolatile('confusion'), pokemon.removeVolatile('confusion'), pokemon.side. |
pokemon.side | The owning Side. pokemon.side.active, pokemon.side.foe, pokemon.side.addSideCondition('reflect', source), pokemon.side.removeSideCondition('reflect'). |
pokemon.side.foe | The opposing Side. Most often used as pokemon.side.foe.active[0] for “the foe’s lead”. |
Dex | The dex object. Dex.species.get(id), Dex.moves.get(id), etc. Use sparingly — usually you can avoid Dex lookups by working with the Pokemon arg directly. |
The exact set of methods and properties is whatever exists on the matching class in the Showdown source. The two files to skim are sim/battle.ts and sim/pokemon.ts.
Worked Examples
Section titled “Worked Examples”Custom Intimidate
Section titled “Custom Intimidate”The bundled custom_intimidate.conf. Drops the foe’s Atk by 2 and broadcasts a flavor message, with a fainted-foe guard:
id = "custom_intimidate"match { species = "gyarados" }
showdown { conditionId = "showdownactionsgyaradosintimidate" conditionName = "Sigil Intimidate" scope = "volatile" hooks { onStart = """function(pokemon) { var foe = pokemon.side.foe.active[0]; if (foe && !foe.fainted) { this.boost({atk: -2}, foe, pokemon); this.add('-message', pokemon.name + ' glares menacingly at ' + foe.name + '!'); } }""" }}Why no auto-guard problem: this hook reads pokemon.side.foe.active[0], which is whoever’s currently on the opposing lead — not the matched Pokémon’s identity. The foe’s species is irrelevant to whether the effect should fire.
Permanent +25% Atk
Section titled “Permanent +25% Atk”Stat-modifier hook. Multiplies Atk by 1.25 forever, with an explicit identity guard so a teammate switching in doesn’t inherit the buff:
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) { if (!pokemon.species || pokemon.species.id !== 'machamp') return; return Math.floor(atk * 1.25); }""" }}The guard is necessary because tier-3 blocks don’t auto-generate one, and the patched runSwitch in this environment keeps the volatile on the slot when a different Pokémon switches in. Without the guard, Charizard switching in after Machamp would inherit the +25% Atk modifier.
Per-turn HP regen
Section titled “Per-turn HP regen”Combine onResidual with a turnDuration:
id = "regenerative_field"target = "SELF"
match { species = "venusaur" sugar { weather = "rain" }}
showdown { conditionId = "showdownactionsregenerativefield" conditionName = "Regenerative Field" scope = "volatile" turnDuration = 5 hooks { onStart = """function(pokemon) { this.add('-message', pokemon.name + ' is regenerating!'); }""" onResidual = """function(pokemon) { if (pokemon.fainted) return; this.heal(Math.floor(pokemon.maxhp / 16), pokemon); }""" onEnd = """function(pokemon) { this.add('-message', 'The regenerative field fades.'); }""" }}onResidual fires every turn during the residual phase. this.heal(amount, target) is the Showdown method; pokemon.heal(amount) is the alternative if you want the heal attributed to the Pokémon itself.
Status block
Section titled “Status block”Block any status application as long as the volatile is active:
showdown { conditionId = "showdownactionsstatusward" conditionName = "Status Ward" scope = "volatile" turnDuration = 3 hooks { onStart = """function(pokemon) { this.add('-start', pokemon, 'Status Ward'); }""" onSetStatus = """function(status, pokemon, source, effect) { if (effect && effect.status) { this.add('-immune', pokemon, '[from] ability: Status Ward'); return false; } }""" }}onSetStatus is called when something tries to set a major status. Returning false cancels it. The add('-immune', ...) line is a Showdown protocol message that produces “Pokémon is immune!” battle text on clients.
Conventions
Section titled “Conventions”A few patterns that come up enough to be worth standardizing.
Condition id prefixes
Section titled “Condition id prefixes”The registry normalizes ids to lowercase alphanumeric, but it doesn’t namespace them. Vanilla Showdown has confusion, taunt, safeguard; if you also register confusion, you’ve replaced Showdown’s built-in. To avoid collisions, prefix every id with showdownactions plus your mod or feature name:
showdownactionsmymodironfistbuffThe bundled actions all follow this convention.
print() for instrumentation
Section titled “print() for instrumentation”Every hook is auto-wrapped so fires and errors land in the Showdown log automatically. If you want extra instrumentation inside the body, use print(...) directly:
function(pokemon) { print('[mycond] before: hp=' + pokemon.hp + ' / ' + pokemon.maxhp); pokemon.heal(50); print('[mycond] after: hp=' + pokemon.hp);}print() writes to the Showdown log channel. With debug = true in config.conf, those lines show up alongside the auto-emitted fire/error logs.
Defensive dereferencing
Section titled “Defensive dereferencing”pokemon.side.foe.active[0] is the standard “the foe’s lead” pattern, but active[0] can be null (between turns, after a faint, in some edge cases). Always check before dereferencing:
var foe = pokemon.side.foe.active[0];if (!foe || foe.fainted) return;// safe to use foe...Modifier hooks especially run in odd contexts where the simulator may be querying for a hypothetical — a damage calc preview, an AI lookup — with arguments that aren’t fully populated. Guard generously.
Returning from modifiers
Section titled “Returning from modifiers”Always explicit-return the new value:
// goodonModifyAtk = """function(atk, pokemon) { if (!pokemon.species || pokemon.species.id !== 'machamp') return; return Math.floor(atk * 1.25);}"""
// bad - implicit return of undefined when the guard passes through to no returnonModifyAtk = """function(atk, pokemon) { if (pokemon.species.id === 'machamp') Math.floor(atk * 1.25);}"""Modifier semantics treat undefined as “no change”, but only because that’s how the simulator interprets it. Explicit-return makes the intent visible to anyone reading.
Mixing With Sugar
Section titled “Mixing With Sugar”A showdown {} block can coexist with a populated apply { }. Both fire at switch-in:
- The
apply { }block produces an auto-applied condition with an auto-generatedonStartthat runs the sugar effects (with the auto-guard). - The
showdown { }block registers as a separate condition with whatever id you specified and whatever hooks you wrote.
Both volatiles attach when the host switches in. They run independently — there’s no ordering guarantee between them, so don’t have one depend on side effects from the other.
In practice, mixing is most useful when you want the sugar’s “broadcast a battle message” or “play a stat-up animation” alongside a custom hook the sugar can’t express:
apply { boosts { atk = 1 } message = "{name}'s technique sharpens!"}
showdown { conditionId = "showdownactionsmyextra" hooks { onModifyAtk = """function(atk, pokemon) { return Math.floor(atk * 1.1); }""" }}The +1 stage at switch-in plays the standard animation and broadcasts the message. The 1.1× modifier persists for the whole battle.
When You’re Stuck
Section titled “When You’re Stuck”The fastest way to unstick a tier-3 hook is the debug log — it tells you whether the hook fired, how many times, and whether it threw. After that:
- Re-read the analogous existing condition in
data/conditions.tsordata/abilities.ts. - Check argument names and order against the call site in
sim/battle.ts(search forrunEvent('YourHook'). - Try the simplest possible version of the hook (
function() { print('fired'); }) and confirm the trace shows it firing — if it doesn’t, the bug is in registration or routing, not in the body.
See Showdown Research for the deeper orientation.
Next Steps
Section titled “Next Steps”- Showdown Research — the engine-level orientation, if you haven’t read it.
- Recipes — the bundled
custom_intimidateandlegendary_bossrecipes are good worked tier-3 patterns.