Skip to content

Match Clause

The match { } block declares the conditions under which an action fires. Every populated field is ANDed together. Empty or null fields are ignored — they aren’t “match anything”, they’re “don’t care”.

A match clause is split into three parts:

  1. Property fields — direct checks against the Pokémon’s stats and identity. Evaluated server-side as plain comparisons.
  2. Sugar predicates — a small sugar { } sub-block of common gates (time, weather, biome, dimension) that compile to Molang at load time.
  3. Raw Molang — an optional whenMolang = "..." expression for anything else.

ShowdownActions runs them in roughly that order: cheap property checks first, then the compiled Molang. A property mismatch short-circuits the whole evaluation — the Molang never runs if the species was wrong.

match {
species = "umbreon"
minLevel = 50
sugar {
time = "night"
}
whenMolang = "q.pokemon.shiny || q.world.is_thundering"
}

That clause matches a level-50+ Umbreon at night that is either shiny or in a thunderstorm.


These are evaluated against the live Pokémon. Most are case-insensitive string equality. The integer ones are inclusive bounds.

FieldTypeCompares againstRe-checked on switch-in?
speciesString?Cobblemon species id, e.g. "lycanroc".Yes
formString?Form name, e.g. "midday", "midnight".Yes
typeString?One of the Pokémon’s elemental types must equal this (lowercase, e.g. "water").Yes
abilityString?Cobblemon ability id (Showdown-style lowercase, e.g. "swiftswim").Yes
heldItemString?Full namespaced item id, e.g. "cobblemon:choice_band".Yes
aspectString?A required aspect tag, e.g. "shiny", "dusk", "gmax".No
natureString?Nature id (lowercase, e.g. "modest", "adamant").No
teraTypeString?Tera type (lowercase, e.g. "fire", "stellar").Yes
genderString?"male", "female", or "genderless".No
minLevelInt?Inclusive lower bound (1..100).Yes
maxLevelInt?Inclusive upper bound (1..100).Yes
minFriendshipInt?Inclusive lower bound (0..255).No
isLegendaryBoolean?Tri-state. true requires the species to be labeled legendary; false requires it not to be; null ignores.No

The “re-checked on switch-in” column matters when target = "SELF" or "ALLIES" and the host Pokémon switches out and a different one switches in. a guard is auto-applied around the switch-in hook that re-evaluates these fields on every switch-in, so the effect doesn’t bleed onto a different Pokémon. The static fields aren’t re-checked because their values can’t change mid-battle.

Tera type is in the re-checked column on purpose — a Pokémon can Terastallize partway through a battle and change its Tera type, so a “match the original Tera type” rule wouldn’t be quite right.

These are the predicates Showdown’s Pokémon object exposes natively (pokemon.species.id, pokemon.types, pokemon.ability, pokemon.item, pokemon.level, pokemon.teraType). The server-side check is the fast path; the JS guard re-checks the same fields on the Showdown side at every switch-in.


The sugar { } sub-block is a small set of common world gates. Each one compiles to a Molang expression at load time, which is then ANDed with whenMolang (if you also set one). They exist because nearly every action that needs a “world condition” gate uses one of these four, and writing them as Molang every time is repetitive.

FieldValuesCompiles to
timemorning, day, noon, evening, dusk, night, midnightq.world.is_time('<bucket>')
weatherclear, rain, thunder, snowq.world.is_raining, q.world.is_thundering, (!q.world.is_raining && !q.world.is_thundering), depending on the value
biomeBiome id ("minecraft:forest") or biome tag ("#minecraft:is_forest")q.world.biome == '...' or q.world.biome_in_tag('...')
dimensionDimension id, e.g. "minecraft:overworld", "minecraft:the_nether"q.world.dimension == '...'

Sugar predicates are evaluated once at battle start, against the player’s current world position. They aren’t re-checked on switch-in — the world doesn’t move between Pokémon swaps.

The time bucket boundaries are deterministic Minecraft tick ranges:

BucketTick range (mod 24000)
morning0..2999
day0..11999
noon5000..7000
evening9000..11999
dusk12000..13000
night13000..23999
midnight17500..18500

day and morning overlap; night and midnight overlap. That’s intentional — day is “is it daytime at all?”, morning is “is it specifically morning?”. Pick the one that matches what you actually want to gate on.

clear is (!q.world.is_raining && !q.world.is_thundering) — “no rain and no thunder”. Snow is rendered as a separate weather state; weather = "snow" checks q.world.is_snowing directly. Anything outside the four documented values falls back to q.world.weather == '<value>', which works against Cobblemon’s native weather query if the value is one of Cobblemon’s recognized weather names.

If the value starts with #, it’s interpreted as a biome tag and compiled to a q.world.biome_in_tag('...') call. Without the # it’s an exact biome id check. The tag form lets you write biome = "#minecraft:is_forest" instead of listing every individual forest biome separately.


The full escape hatch for predicates that don’t fit any of the above. Set whenMolang to a Cobblemon Molang expression — anything the runtime can evaluate — and a non-zero result counts as a match.

match {
species = "umbreon"
whenMolang = """
q.world.is_time('night') &&
q.pokemon.level >= 50 &&
(q.pokemon.shiny || q.world.is_thundering)
"""
}

The runtime binds q.pokemon, q.player, q.world, and q.battle — the same surface Cobblemon’s NPC dialogue and Journey filters use. ShowdownActions also registers a few extra helpers under q.world.*. The full surface and helpers are documented at Molang Predicates.

whenMolang evaluates once at battle start for each (action, pokemon) candidate, after the property checks and before the final gates (permission, chance, mutexGroup). It is not re-evaluated on switch-in.

If whenMolang throws or fails to parse, ShowdownActions logs a warning and treats the result as false (no match). Actions never crash a battle.

HOCON supports """...""" for multi-line strings without escaping. Use that for any non-trivial predicate — the result is much more readable than wrapping a long expression in single quotes.


The match pipeline at battle start:

  1. For each action and each Pokémon on each player’s team:
  2. Property check runs first. If any populated property field doesn’t match, the action is rejected for this (actor, pokemon) pair. No Molang is run.
  3. Compile the sugar predicates and whenMolang into a single combined Molang expression. (The compilation result is logged at debug-trace level.)
  4. Evaluate the combined Molang against the live battle context. A non-zero result is “match”.
  5. The first matching Pokémon on the actor’s team wins for this action — ShowdownActions doesn’t fire the same action multiple times per actor even if several team members would match.

After matching:

  1. Permission gate — if permission is set, the player must hold that node (looked up via fabric-permissions-api / LuckPerms). Missing nodes default to denied.
  2. Chance gate — if chance < 1.0, a uniform [0, 1) roll must come in below it.
  3. Mutex resolution — within a mutexGroup, only the highest-priority winner survives.

Then the action’s effects fire. Cobblemon-side effects (forced aspects, console commands) run immediately. Showdown-side effects are queued for the matched Pokémon’s slot and applied as a volatile when it switches in.


A simple species + level gate:

match {
species = "garchomp"
minLevel = 50
}

A type filter combined with weather:

match {
type = "fire"
sugar { weather = "clear" }
}

Held-item check with a property fallthrough:

match {
heldItem = "cobblemon:choice_band"
}

Tag-based biome gate (any forest variant):

match {
sugar { biome = "#minecraft:is_forest" }
}

Pure Molang with no property fields:

match {
whenMolang = "q.pokemon.hp_ratio < 0.25"
}

Mixing all three:

match {
ability = "swiftswim"
sugar { weather = "rain" }
whenMolang = "q.pokemon.level >= 30 && q.pokemon.shiny"
}