Skip to content

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.


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:

FieldTypeDefaultPurpose
conditionIdStringrequiredShowdown 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.
conditionNameStringconditionIdDisplay name shown in debug tooling and Showdown’s name slot.
scopeString"volatile""volatile", "side", or "field". Currently informational — ShowdownActions registers all three the same way.
turnDurationInt?nullAuto-remove after N turns. null = permanent (lasts the whole battle).
hooksMap<String, String>{}Showdown event handlers, keyed by handler name. Values are raw JS function strings.
extrasMap<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 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.

HookArgsWhen
onStartpokemon, source, sourceEffectThe volatile attaches. With the patched runSwitch in this environment, this re-fires on every switch-in.
onEndpokemonThe volatile is removed — expired, cured, replaced, or battle ended.
onResidualpokemonEvery turn during the residual phase, after moves resolve. Standard place for per-turn ticks (poison damage, terrain damage, etc.).
onRestartpokemon, source, sourceEffectThe volatile is reapplied to a host that already has it. Useful for stacking semantics.
HookArgsReturns
onModifyAtkatk, pokemon, target, moveNew Atk. Returning undefined leaves it unchanged. Returning a number replaces it.
onModifyDefdef, pokemon, source, moveNew Def.
onModifySpAspa, pokemon, target, moveNew Special Atk.
onModifySpDspd, pokemon, source, moveNew Special Def.
onModifySpespe, pokemonNew 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.

Forwarded by name through extras-style passthrough — just put them under hooks { } and they’ll wire up.

HookCommon use
onAnyDamageReact to any damage event (own or other’s). Args: damage, target, source, effect.
onTryHitIntercept an incoming move. Return false to cancel, null to silently fail. Args: target, source, move.
onAfterMoveSecondaryFire after a move’s secondary effects resolve.
onSwitchIn / onSwitchOutBefore/after the host swaps.
onBeforeMoveBefore the host’s move resolves. Return false to skip the move.
onModifyMoveMutate a move object before damage calc.
onModifyTypeChange a move’s type.
onSetStatus / onTryAddStatusBlock or allow status application.
onWeatherModifyDamageMultiply damage based on active weather.
onFractionalPriority / onModifyPriorityAdjust 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.


Inside any hook body:

ReferenceWhat it is
thisThe Battle instance. this.add(protocol, ...), this.boost(boosts, target, source), this.field, this.sides, this.activeMove, this.randomChance(num, den), this.dex, etc.
this.fieldThe 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.sideThe owning Side. pokemon.side.active, pokemon.side.foe, pokemon.side.addSideCondition('reflect', source), pokemon.side.removeSideCondition('reflect').
pokemon.side.foeThe opposing Side. Most often used as pokemon.side.foe.active[0] for “the foe’s lead”.
DexThe 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.


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.

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.

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.

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.


A few patterns that come up enough to be worth standardizing.

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:

showdownactionsmymodironfistbuff

The bundled actions all follow this convention.

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.

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.

Always explicit-return the new value:

// good
onModifyAtk = """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 return
onModifyAtk = """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.


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-generated onStart that 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.


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:

  1. Re-read the analogous existing condition in data/conditions.ts or data/abilities.ts.
  2. Check argument names and order against the call site in sim/battle.ts (search for runEvent('YourHook').
  3. 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.


  • Showdown Research — the engine-level orientation, if you haven’t read it.
  • Recipes — the bundled custom_intimidate and legendary_boss recipes are good worked tier-3 patterns.