A small Kotlin engine that runs turn-based monster battles defined entirely by JSON files. One engine runs both:
- Classic-style battles (
"turnSystem": "speed_order"): every active monster picks an action each round, and actions run by priority, then speed. - Action-bar / ATB-style battles (
"turnSystem": "action_bar"): each monster fills an action bar based on its speed, and whoever fills it first acts.
Each folder in Setups/ is a separate game (a setup) with its own monsters, moves, types and rules.
Bakemon.bat :: compile build\jar\Bakemon.jar and package build\Bakemon\Bakemon.exe
Bakemon.bat jar :: compile only the jar
Bakemon.bat run :: run the jar (builds it first if missing); extra args are forwarded
Bakemon.bat test :: compile and run the self tests in Tests\
Bakemon.bat clean :: delete build\The first build downloads the Kotlin compiler into .tools\. You need a JDK 17+ (java and jpackage).
Command-line options for the app (Bakemon.exe [options] or Bakemon.bat run [options]):
| Option | Meaning |
|---|---|
--setups <dir> |
Folder that contains the setups (default: auto-detects the Setups folder) |
--setup <name> |
Open this setup directly |
--seed <n> |
Fix the random seed so battles can be reproduced |
--no-color / --color |
Turn ANSI colours off or on (auto-detected by default) |
--validate |
Load every setup, print errors and warnings, then exit (exit code 1 if any setup is invalid) |
--simulate <setup> [--count N] [--teams A,B] [--ai easy|normal|hard] [--fields a,b] |
Run N AI-vs-AI battles without the UI and print statistics. --fields adds starting fields (Fields.json ids; permanent; side fields go on both sides) to every battle, on top of Rules.json startFields. An unknown id is an error (exit code 2) |
Setups/Demo is a small example game (speed_order): 6 monsters, 4 types, 3v3 singles. Start from it or make your own with the setup creator.
It is the only setup in this repository: any other folder in Setups/ is git-ignored. Self tests that need a larger setup are skipped when it is not there.
The setup creator makes new setups and edits any setup through menus, with no JSON or formulas to write. Open it from the main menu (Setup creator), or from the setup selection screen (Create a new setup / Setup creator). The selection screen offers both even when no setup can be loaded, so a new or broken setup can always be reached and fixed.
Home. The setups with their status: OK, N warnings, N errors, incomplete (N things missing) (something
needed to battle is missing) or N files cannot be read (a JSON syntax error). Pick a setup to edit it, or create a
new setup, rename a setup folder, or delete a setup (asked twice; "no" is the default both times).
A new setup starts from rule templates: a name, how turns work (Classic-style rounds or an action-bar turn order), which stats monsters have (Classic, Action Bar, Simple or your own list), then the rules as a list of questions (team size, levels, stat growth, damage, accuracy, critical hits, PP or cooldowns...). Each choice has a one-line help and each question a recommended answer in brackets: Enter accepts it. The rules and their formulas are generated from the answers. The setup starts empty (plus a typeless Desperate Strike fallback move when moves use PP); it is saved at once and a checklist shows what is still needed to battle (types, moves, bakemons...), with a link to each section.
The setup editor has Rules, Types, Statuses, Moves, Abilities, Items, Fields and Bakemons, plus:
- Checklist: what is still needed to battle, with links;
- Validate: the full check of the setup (the one
--validatedoes), without saving; - Save: lists the changes per file (added / changed / removed entries), writes only the changed files (the unchanged parts keep their exact text), then shows the validation. An incomplete setup can be saved. When the setup that is loaded for battles is saved, it is reloaded;
- Discard changes, Raw JSON files (view or replace a whole file; the way to repair a file with a syntax error), and Back, which asks about unsaved changes. Nothing is written before Save.
Each section lists its entries, 20 per page: type text at the prompt to search (by name, id, type, kind...). Pick an entry to see it in plain words, then:
- Edit: a form with every field in plain words. Done keeps the changes, Cancel drops them;
- Copy, under a new name;
- Rename id: shows every place that uses the id (bakemons, moves, effects, teams, rules, the type chart, formulas) and updates them all;
- Delete: shows where it is used and offers to remove those references too (references inside formulas must be fixed by hand; the saved teams that used it are named so you can check them in the team creator);
- View / Replace the raw JSON of the entry.
Add is a short wizard, then the form: statuses, abilities, items and fields start from a ready-made behaviour if you
want one (damage over time, skip turns, paralysis-like, stat change, shield, revive on death; +X% to a stat, boost a
type, immunity to a type, heal each turn, status on contact, survive a lethal hit, used-up items; weather, screen,
speed boost, entry hazard, reverse turn order, block statuses / stat drops / moves). A move asks its type, category and
how strong it is (fixed power, doubles below X% HP, stronger at low HP, doubles against a status, power from a stat,
fixed damage, grows with each hit, per buff / debuff on the target, per ally standing, faster than the target, the HP
the user lost, doubles if the target already acted), then its hits (one, a fixed number, a random range such as 2
to 5, or a formula; with several hits: does the power grow with each hit, does each hit pick a random enemy, does each
hit roll the accuracy), how much of the target's defense it ignores, and whether it is a charge move (turns of
charging, out of reach meanwhile, a message, and "skip the charge when..." built from conditions, e.g. a field is
active). The move form has the same rows, plus the new automatic targets (the weakest of the team, the weakest enemy,
a random enemy for each hit). Something that happens on one hit only ("the 3rd hit stuns") is an effect "when it
hits" with the condition "It is hit number N". A bakemon asks its name, types, one base value per stat (named as in
the rules), abilities, learnable moves (with search and the shortcuts "all moves of its types" and "all moves
without a type"), description and image
(default Assets/<id>.png; a missing image is only a warning). Types have a colour and a chart editor: for each
matchup pick strong (x2), weak (x0.5), immune (x0), neutral, a multiplier already used in the chart, or another one.
Effects are triggers, shown as one line each ("When it hits, 30% chance: apply a status (status: Burn)"). Opening
one shows WHEN (the event, in plain words: also "when it is targeted by a move", "when it has been attacked", "when
an ally / enemy takes damage", "when an ally / enemy uses a move", "when it applies a status to someone" and "when its
HP drops below X%", which is "when it takes damage" with the condition "its HP just dropped below X%", once per battle
unless you change it), IF (conditions that must all hold: HP below X%, has a status or a type, number of buffs /
debuffs, the move's type / category / flag, critical hit, super effective, hit number N, the last hit, the attack
landed, the other monster is an enemy, already acted, faster than the other, allies standing, charging, a field is
active...), the chance, how often (every time, once per battle, at most N times), a message, and what it DOES. Long
lists of choices come in two steps, a group then a choice: Damage (% of its HP, recoil, % of a stat, an amount, an
extra strike with the damage formula, multiply the damage), Healing and HP (heal, drain, shields, set HP, share HP
within a group, swap HP, revive), Statuses and stats (apply, remove, steal, copy or pass on statuses, raise or lower
stats, copy stat changes), Moves and turns (strike back with its first move, join an ally's attack, use this move
again, use any move, an extra turn, a delayed effect whose own effects are edited like a DO list, cancel the move,
action bar, cooldowns), Switching (drag the target out, switch out like U-turn) and Fields, items and messages.
The "% of its HP" effects work on groups: "heal its allies 20% of its max HP" gives each ally 20% of its own max
HP (recipient). Targets include the weakest of its team, a random ally, the weakest / healthiest enemy and, when the
other monster used a move, the target of that move. Only the choices that work in that place are offered (for example
"multiply the damage" only when damage is about to be dealt or taken, "strike back" only when it was attacked).
Rules. Change the rules asks the template questions again (the answers are read back from Rules.json) and
regenerates only the parts you changed. Parts written by hand are marked "(edited by hand)" and are only replaced if
you agree. Adding a stat gives every bakemon a base value for it; removing one takes it out of their base stats.
Rename a stat id updates every reference (base stats, stat points of teams, stat changes, formulas such as
user.atk). Edit one value (advanced) and the raw JSON views edit Rules.json directly.
No formulas needed. Values are numbers and choices. Where a formula would also work, the prompt says
a = advanced formula: type a, then a formula (see Formulas). It is checked at once, in the place it
goes, and problems are explained (for example "unknown variable 'user.bogus'"); it is asked again until it is valid
or you cancel. Formulas only appear in these advanced choices and in the raw JSON views.
What it cannot do. Everything the forms cannot express is kept exactly as it is: such a condition or effect is
shown as an "advanced" part (a formula or raw JSON), and a trigger with keys the forms do not know is shown as a
"Custom trigger" that can only be replaced as raw JSON (or left untouched). The rule template only knows its own
formulas: other damage or stat formulas are "edited by hand" and are changed with Edit one value or raw JSON.
The template answers are not stored; they are recognised from Rules.json each time. Images are not created, teams
are made in the team creator, and Fields.json "special rules" (reverse order, blocks...) come from the ready-made
behaviours or are edited as JSON. "The power grows with each hit" is offered for a fixed power only (an advanced power
formula can use hitIndex), and a charge with keys or values the form does not know is kept as advanced JSON.
Bakemon
├ Main.kt entry point (menus, command-line options)
├ Bakemon.bat build script
├ Source
│ ├ Util/Json.kt JSON parser/writer + typed accessors with readable errors
│ ├ Engine
│ │ ├ Expression.kt formula language used everywhere in the JSON
│ │ ├ ActionEvents.kt event catalog, triggers, event dispatch, battle log
│ │ ├ Effects.kt effect catalog (damage, heal, applyStatus...) and their execution
│ │ ├ DamageCalculator.kt hit chance, crits, hit types, type effectiveness, damage formula
│ │ ├ BattleState.kt runtime state: Battle, Side, Battler, formula context
│ │ ├ Controller.kt Action + Controller interface (who decides what to do)
│ │ ├ AI.kt AI controller
│ │ └ Manager.kt battle loop: turn systems, move execution, fainting, victory
│ ├ Bakemon
│ │ ├ Rules.kt Rules.json
│ │ ├ Types.kt Types.json (type chart)
│ │ ├ Bakedex.kt Bakemons.json (species)
│ │ ├ Bakemon.kt a configured monster (team member) + Team
│ │ ├ Moves.kt Moves.json
│ │ ├ Ability.kt Abilities.json
│ │ ├ Item.kt Items.json
│ │ ├ Status.kt Statuses.json
│ │ ├ Fields.kt Fields.json (weather, terrain, screens, hazards...) + start fields
│ │ └ Setup.kt loads and cross-validates a whole setup folder, team storage
│ ├ Creator setup creator logic, independent of the UI (drafts, references, rule templates,
│ │ recipes, triggers, forms, presets)
│ └ UI
│ ├ Terminal.kt terminal helpers (menus, input, colours, bars)
│ ├ Combat.kt battle screen + human controller
│ ├ TeamCreator.kt team editor + Bakedex browser
│ ├ SetupCreator.kt setup creator: home, new setup, setup editor, sections, rules
│ ├ CreatorForms.kt setup creator: entry forms, add wizards, trigger editor, type chart editor
│ └ CreatorInput.kt setup creator: choices, paged lists, numbers / advanced formulas, recipe values
├ Setups
│ └ <SetupName>
│ ├ Rules.json Types.json Bakemons.json Moves.json
│ ├ Abilities.json Items.json Statuses.json Fields.json (optional)
│ ├ Assets/ monster images (not used by the terminal UI)
│ ├ Teams/ teams saved by the team creator
│ └ NOTES.md optional notes about the setup (not read by the engine)
└ Tests self tests (Bakemon.bat test)
MIT, see LICENSE. It covers Setups/Demo but no other setup.
Every list file can be either a bare JSON array or an object with one list key
({"bakemons": [...]}, {"moves": [...]}, {"abilities": [...]}, {"items": [...]}, {"statuses": [...]}, {"fields": [...]}).
Ids are free text. By convention they are lower_snake_case, and every id must be unique within its file.
Unknown keys produce a warning, so a typo like "acuracy" gets reported instead of silently ignored.
Fields marked "number or formula" accept a formula string instead of a number, e.g. "power": "user.spe > target.spe ? 80 : 40".
These are: move power, hits, critBonus, ignoreDefense and charge.skipIf; the Rules formulas (statFormulas,
statStages.multiplier, damage, hitChance, critChance, critMultiplier, hitTypes chance and multiplier,
actionBar.fill, actionBar.start, startFields duration); status and field duration; every statModifiers
value; the field rules formulas; trigger condition and chance; and the numbers of effects (amount, chance,
duration, stacks, stages, value, count, turns, power).
Every other number (accuracy, priority, pp, cooldown, maxStacks, trigger limit, type-chart multipliers, base stats,
team/level/statPoints/statStages limits, minDamage, actionBar.size, turnLimit...) must be a plain JSON number.
Syntax:
| numbers | 12, 0.5 |
| strings | 'fire' or "fire" (single quotes are easier inside JSON) |
| booleans | true, false (true = 1, false = 0; any non-zero number counts as true) |
| arithmetic | + - * / %, ^ (power, right-associative), unary - |
| comparison | < <= > >= == != (==/!= also compare strings) |
| logic | && || ! |
| conditional | cond ? a : b |
| grouping | ( ... ) |
| variables | user.atk, target.hpPercent, move.type, damage, ... (see below) |
Division by zero and the square root of a negative number give 0.
Built-in functions: min(a, b, ...), max(a, b, ...), floor(x), ceil(x), round(x), abs(x), sqrt(x),
pow(a, b), clamp(x, lo, hi), if(cond, a, b), random() (0 to 1), random(a, b) (a real number between a and b),
randint(a, b) (an integer from a to b inclusive), chance(p) (1 with probability p percent, otherwise 0).
Battle functions: hasType(m, 'fire'), hasStatus(m, 'burn'), statusStacks(m, 'burn'), hasAbility(m, 'blaze'),
hasItem(m), hasItem(m, 'leftovers'), hasFlag('contact') (the current move has that flag),
statusCount(m) (how many statuses m has; each status counts once, whatever its stacks) and
statusCount(m, 'debuff') (only those of one kind: 'major', 'volatile', 'buff' or 'debuff').
Here m is a monster reference: self, other, user, target or recipient.
hasAbility and hasItem read 0 while a field's disableAbilities / disableItems rule suppresses them.
Field functions (see Fields.json):
| function | value |
|---|---|
fieldActive('rain') |
1 if that field is active globally or on any side |
sideField(m, 'reflect') |
1 if m's side has that side field |
fieldStacks('rain') |
stacks of that global field (0 if inactive) |
sideFieldStacks(m, 'spikes') |
stacks of that side field on m's side (0 if absent) |
fieldTurns('rain') |
turns left of that global field (0 if permanent or inactive) |
typeMultiplier('rock', m) |
type chart multiplier of an attack of that type against m's current types (1 if m is absent), e.g. for a Stealth Rock-like hazard |
sameSide(a, b) |
1 if both monsters are present and on the same side |
A field id or type id written as a string literal (fieldActive('rain')) must exist; fieldStacks/fieldTurns on a
side field and sideField/sideFieldStacks on a global field get a warning (they always read 0).
random, randint and chance cannot be used in Rules.statFormulas (a monster's stats must not change between calculations)
nor in statModifiers (an effective stat must be the same every time it is read: the turn order preview, the battle
screen and the AI read stats too, and must not change the battle by rolling its dice).
| reference | in ability / item / status triggers | in moves and move triggers | in Rules damage/hit/crit/hit type formulas |
|---|---|---|---|
self |
the owner of the trigger | the move user | the attacker |
other |
the other monster of the event (may be absent) | the current target | the defender |
user |
the move user if a move is involved, otherwise self |
the move user | the attacker |
target |
the move target if a move is involved, otherwise other |
the current target | the defender |
recipient |
the monster the effect is being applied to | the monster the effect is being applied to | - |
recipient only exists in the values of effects that have a target (amount, chance, duration, stacks, stages,
count, power...): the value is computed again for each monster the effect resolves to, with that monster as
recipient. So "heal every ally 20% of its own max HP" is
{"type": "heal", "target": "allies", "amount": "recipient.maxHp * 0.2"} (with self.maxHp they would all get 20%
of the owner's). In a useMove effect it is the actor. Elsewhere (trigger conditions and chances, durations, the
values of multiplyDamage, setField and delay) the setup check reports it as an error.
If a reference is absent (for example other during turnEnd), its numbers read as 0 and its strings as ''.
In a status's triggers, other is the monster that applied the status (if it is still active) whenever the
event itself has no other monster. For example, a leech-seed status can drain HP on turnEnd and heal other.
Monster fields (m is any reference):
| field | meaning |
|---|---|
m.hp, m.maxHp, m.hpPercent |
current HP, max HP, HP percent (0-100) |
m.level |
level |
m.<stat> |
effective stat: raw stat x stage multiplier x modifiers from ability/item/statuses (e.g. m.atk) |
m.raw.<stat> |
stat before stages and modifiers |
m.base.<stat> |
species base stat |
m.stage.<stat> |
current stat stage |
m.gauge |
action-bar fill, 0-100 percent |
m.shield |
remaining shield points |
m.fainted |
1 if fainted |
m.revives |
how many times it was revived in this battle |
m.reviveBlocked |
1 if preventRevive made it unrevivable |
m.acted |
speed_order: 1 once it acted this round (also for a monster that came in during the round, which does not act); action_bar: 1 while it is the monster whose turn it is |
m.charging |
1 while it is charging a move (see charge in Moves.json) |
m.id, m.name |
species id, display name (strings) |
Inside statModifiers formulas, self.<stat> returns the raw stat, which avoids infinite recursion.
move.id, move.name, move.type ('' when typeless), move.category, move.power (evaluated; 0 inside the power formula itself),
move.accuracy (0 when the move never misses), move.priority, move.pp, move.cooldown.
When no move is involved, these read as 0 / ''.
A power formula is evaluated once per hit, with self/user = the move user and other/target = the target, whoever
reads move.power: the damage formula and the triggers of that hit (modifyDamageDealt, modifyDamageTaken, hit) all see that value.
It can read hitIndex and hitCount, so a Triple Kick is "power": "20 * hitIndex" (20, 40, 60).
| variable | where | meaning |
|---|---|---|
turn |
everywhere in battle | current round (speed_order) or turn (action_bar) number, starting at 1 |
allies, enemies |
everywhere in battle | number of active, non-fainted allies of self (excluding self) / enemies |
damage |
hit, modifyDamageDealt, modifyDamageTaken, dealtDamage, damaged, allyDamaged, enemyDamaged, attacked |
damage of this event (see below) |
hpBefore |
damaged |
the owner's HP just before this damage (self.hpPercent < 50 && hpBefore * 100 / self.maxHp >= 50 = "its HP just dropped below 50%") |
landed |
attacked |
1 if at least one hit of the move landed on the owner (0 when it missed, could not reach it or the type was immune) |
hitIndex, hitCount |
hit, modifyDamageDealt, modifyDamageTaken; move power and ignoreDefense; damage formula, Rules.hitTypes formulas |
the number of the current hit on this target (from 1) and the hits planned on it (the move's hits; for randomEnemies, the hit number among all of them and the total). 1/1 for a move that deals no damage and for the attack effect. A condition hitIndex == 3 makes "the 3rd hit stuns" |
amount |
healed |
HP restored |
effectiveness |
hit, modifyDamageDealt, modifyDamageTaken, damage formula, Rules.hitTypes formulas, Rules.hitChance / critChance / critMultiplier, move critBonus |
type multiplier of the move against the target (1 in the hit of a move that deals no damage) |
isCrit |
hit, modifyDamageDealt, modifyDamageTaken, damage formula, Rules.hitTypes formulas |
1 if critical hit (0 once a cancelsCrit hit type cancelled it) |
hitType |
hit, modifyDamageDealt, modifyDamageTaken, damage formula |
id of this hit's hit type (string, '' for a normal hit, a move that deals no damage, or a setup without hitTypes) |
cancelled |
beforeMove |
1 once an earlier trigger of this event cancelled the move (e.g. so a second "can't move" status stays quiet) |
status |
statusApplied, statusRemoved, statusInflicted, a field's blockStatus rule |
id of the status (string) |
statusKind |
a field's blockStatus rule |
kind of the status being applied ('major', 'volatile', 'buff', 'debuff') |
stat, stages |
a field's blockStatStage rule |
the stat id and the stage change being made (e.g. -1) |
field |
fieldSet, fieldRemoved |
id of the field (string) |
stacks, turnsLeft |
a status's own statModifiers and triggers; a field's statModifiers, triggers, rules and duration |
its stacks; remaining turns (0 if permanent) |
attack, defense |
damage formula | effective attacking/defending stat for the move's category |
sameType |
damage formula, Rules.hitTypes formulas, Rules.hitChance / critChance / critMultiplier, move critBonus |
1 if the user has the move's type |
crit |
damage formula, Rules.hitTypes formulas |
the crit multiplier if this hit is critical, otherwise 1 |
targets |
damage formula, Rules.hitTypes formulas |
how many targets the move hits |
base, level, points |
Rules.statFormulas |
species base stat, level, stat points invested in that stat |
stage |
Rules.statStages.multiplier |
the stage being converted to a multiplier |
speed |
Rules.actionBar.fill, Rules.actionBar.start |
effective speed of self |
damage is:
modifyDamageDealt/modifyDamageTaken: the damage still pending. It starts as the damage formula's result, times the hit type'smultiplier(modifyDamageTaken: after all the attacker'smodifyDamageDealtmodifiers) and, after eachmultiplyDamage, already includes it. So an item of the defender sees what its ability left: a "survive at 1 HP" item does not trigger on a hit the ability already absorbed, and"value": "(self.hp - 1) / damage"leaves exactly 1 HP.hit,damaged/dealtDamage,allyDamaged/enemyDamaged: the damage the hit (or effect) really did, counting the part a shield absorbed but not damage beyond the remaining HP. So "recover half of the damage dealt" or "1/3 of it as recoil" never count the overkill of a finishing blow.hithas0for moves that deal no damage and for hits a modifier cancelled.attacked: the total of all the move's hits on the owner (HP + shield; not the damage of the move's trigger effects).
Unknown variable or function names, and battle functions called with the wrong number of arguments, are reported when the setup loads.
The example shows typical values, not the defaults. Required: turnSystem, stats, categories, damage.
Defaults of the optional fields:
| field | default |
|---|---|
name, description |
"" (the folder name is shown) |
team |
min 1, max 6, active 1, speciesClause false, itemClause false |
switching |
true |
level |
min 1, max 100, default 50 |
maxMoves |
4 |
heldItems |
true |
hpStat, speedStat |
"hp", "spe" |
statFormulas |
"default": "base" (a stat without its own formula uses default) |
statPoints |
none: stat points are disabled (maxPerStat defaults to total) |
statStages |
min -6, max 6, multiplier "stage >= 0 ? (2 + stage) / 2 : 2 / (2 - stage)" |
hitChance |
"move.accuracy / 100" |
critChance |
0 (no critical hits) |
critMultiplier |
1.5 |
hitTypes |
none (every hit is a normal hit); in an entry: chance 0, multiplier 1, no message, cancelsCrit false |
minDamage |
1 |
pp, cooldowns |
true, false |
fallbackMove |
none (a monster without usable moves waits) |
actionBar |
size 100, fill "speed", start 0 |
turnLimit |
0 (no limit) |
startFields |
none |
hitChance, critChance and critMultiplier (and a move's critBonus) see the type matchup against the current
target: effectiveness and sameType. For example "critChance": "user.crate / 100 + (effectiveness > 1 ? 0.15 : 0)".
The damage formula and the hitTypes formulas also see hitIndex and hitCount (the hit in progress).
Hit types
hitTypes is an ordered list of special kinds of hits, such as the crushing and glancing hits of action bar games.
On every hit that deals damage (each hit of a multi-hit move, never a hit a type immunity stops), right after the crit
roll, the entries are tried in order: the first whose chance roll succeeds is this hit's type; if none does, it is a
normal hit. Each entry:
| key | meaning |
|---|---|
id |
required, unique simple name (letters, digits and _, not starting with a digit); hitType reads it |
chance |
percent (formula). 0 or less never happens and 100 or more always does, without rolling dice |
multiplier |
multiplies the damage formula's result (formula), before rounding, minDamage and modifyDamage* |
message |
logged with the crit message (only when the hit deals damage); placeholders {user} {target} {move} |
cancelsCrit |
true: a critical hit of this type is not critical any more (no crit message, crit 1, isCrit 0) |
chance and multiplier see the variables of the damage formula except attack, defense and hitType: battle
variables, user/target/self/other, move.*, effectiveness, sameType, targets, and the crit roll as isCrit
and crit. multiplier is evaluated once the hit type is chosen, so after a cancelsCrit it sees isCrit 0 and crit 1.
Then the damage formula runs with hitType (the id, '' for a normal hit), and its result is multiplied by the
multiplier. hit, modifyDamageDealt and modifyDamageTaken read hitType too ('' for moves that deal no damage).
The AI's damage estimates roll no dice: they use the expected multiplier (for each crit outcome, p1 x m1 + (1 - p1) x p2 x m2 + ... + the rest x 1, with the crit multiplier dropped where a hit type cancels the crit).
Turn systems
speed_order: each round, each active monster's controller picks an action (move or switch). Switches go first, then moves bypriority, then by effective speed (ties are random). A move that became unusable before its turn came (e.g. a cooldown effect) fails; a switch that became impossible (e.g. a teammate already sent that monster in) is replaced by the first usable move, at that move's place in the order.turnStart/turnEndfire for every active monster at the start/end of the round, fastest first (effective speed, random ties; each monster'sturnEndis followed by its own countdown). The battle end is checked after all of them, so if the last monsters of both sides faint there (burn, poison...), the battle is a draw. Fainted monsters are replaced at the end of the round. A monster that got an extra turn (theextraTurneffect) chooses and runs one more action after everybody's action, before theturnEndphase (in the order the extra turns were given; at most one per monster per round).action_bar: every active monster's bar fills atactionBar.fillpoints per time unit. The first to reachsizetakes a turn, and its bar resets to 0 when the turn starts.turnStart/turnEndfire only for the monster whose turn it is. Fainted monsters are replaced immediately if a bench exists.actionBar.startis the initial fill, in percent. A monster that got an extra turn has a full bar and takes the next turn, before anyone else, without time passing (a complete turn:turnStart, its action,turnEnd, countdowns; the turn line says "- extra turn"). It can get at most one extra turn between two of its normal turns, so extra turns cannot loop.
Monsters that enter together are all placed first, then their switchIn triggers run fastest first (effective speed,
random ties): the leads at battle start (then battleStart, in the same order) and the replacements of monsters that
fainted at the same time. Each side chooses its replacements without seeing the other side's choice, so a switch-in
effect such as "lower the enemies' Attack" also reaches a replacement that enters at the same time.
Durations and cooldowns are counted in the owner's turns. A status with duration: 2 affects 2 of the owner's
turns (2 of its actions), wherever it comes from. In speed_order every round counts as every active monster's own turn.
- A status that arrives before the owner acts in its turn (a faster enemy's move, a
turnStarteffect) already affects that turn: it counts down at the end of it. So sleep for 1 turn applied by a faster enemy costs exactly that turn's move. - A status that arrives once the owner's action has started (its own move, a slower enemy's move, a
turnEndeffect before the owner's countdown) has not affected any of its actions yet: it is fresh and does not count down at the end of that turn. A monster switched in during a speed_order round has no action that round, so the same applies to it. - A status that arrives outside the owner's turn (at battle start, with a replacement sent in at the end of a round, after the owner's countdown) counts down at the end of the owner's next turn.
duration: 0 means "until the end of the current turn" (useful for flinch- or protect-like effects).
A move with cooldown: 2 cannot be used for the owner's next 2 turns after it is used.
The cooldown effect counts the same way: amount: 1 on a ready move blocks one of the owner's actions. Arriving
before the owner acts in its turn, it blocks that turn's action; arriving once the owner's action has started (or
outside its turn), it blocks its next one. A move already on cooldown simply waits amount more turns.
In detail, at the end of each of the owner's turns, each status with a duration does this:
if it is fresh and turnsLeft > 0, it stops being fresh.
Otherwise turnsLeft goes down by 1, and the status is removed when it reaches 0 or less.
Fields count down differently (they have no owner): see Fields.json.
{
"types": [ { "id": "fire", "name": "Fire", "color": "red" } ], // color: black red green yellow blue magenta cyan white gray/grey (or bright*, e.g. brightRed)
"chart": { // attacker -> defender -> multiplier; missing pairs = 1
"fire": { "grass": 2, "water": 0.5, "fire": 0.5 }
},
"messages": { "strong": "It's super effective!", "weak": "It's not very effective...", "immune": "It doesn't affect {target}..." }
}A monster with several types multiplies the multipliers together. The messages can only use {target}.
{ "bakemons": [ {
"id": "embercub", "name": "Embercub",
"types": ["fire"],
"baseStats": { "hp": 45, "atk": 60, "def": 40, "spa": 70, "spd": 50, "spe": 65 }, // every stat in Rules.stats
"abilities": ["blaze", "flash_fire"], // the abilities it may have (first = default)
"moves": ["ember", "tackle", "growl"], // the moves it can learn
"image": "Assets/embercub.png", // optional
"description": "A cub with a smouldering tail."
} ] }{ "moves": [ {
"id": "ember", "name": "Ember", "description": "May burn the target.",
"type": "fire", // optional (typeless if omitted)
"category": "special", // a key of Rules.categories
"power": 40, // number or formula; omit/0 = deals no direct damage (a formula that gives 0 for a hit: that hit deals none)
"accuracy": 100, // percent; omit = never misses
"priority": 0,
"pp": 25, // 0 = unlimited (only if Rules.pp)
"cooldown": 0, // only if Rules.cooldowns
"target": "enemy", // see below
"hits": 1, // formula, e.g. "randint(2, 5)"
"hitAccuracy": "once", // "once" | "each" (every hit rolls the accuracy)
"critBonus": 0, // added to the crit chance (0..1); same variables as Rules.critChance
"ignoreDefense": 0, // percent (formula) of the target's defense the damage formula ignores
"charge": { "turns": 1, "message": "{user} is charging up!", "invulnerable": false, "skipIf": "fieldActive('sun')" }, // optional
"flags": ["contact"], // free tags, readable with hasFlag('contact')
"triggers": [ { "on": "hit", "chance": 10, "effects": [ { "type": "applyStatus", "status": "burn" } ] } ]
} ] }| key | default | meaning |
|---|---|---|
hits |
1 | hits on each target (formula evaluated once per target, before the first hit; it cannot use hitIndex / hitCount) |
hitAccuracy |
"once" |
"once": one accuracy roll per target, before the first hit. "each": every hit rolls; the first miss ("X avoided the attack!", missed) ends the move against that target |
ignoreDefense |
none | percent (formula, 0-100, may use hitIndex): the damage formula's defense is multiplied by 1 - value / 100 for this move, but never goes below 1 (so 100 makes it 1 and a formula that divides by the defense still works) |
charge |
none | a charge move, see below |
Charge moves. With "charge", the first use only charges: beforeMove runs, then the charge message (default
"{user} is charging up!"; placeholders {user} {move} {target}) instead of "X used Y!", and the PP / cooldown
are paid. The monster is then locked: its next turns turns (default 1) are not chosen by its controller (the battle
menu does not ask, the AI does not choose): each one runs beforeMove again, then either charges on (the message
again) or, on the last one, releases the move with the normal procedure ("X used Y!", accuracy, hits...) on the same
target (retargeted like a normal move if it is gone), without paying again.
invulnerable: true: while it charges, every other monster's move aimed at it misses before the accuracy roll ("X can't be reached!",missed), even moves that never miss. Effects (damage,attack...) still reach it.skipIf: a formula (user/target/move, likepower) checked at the first use; when true the move is used at once, like a normal move (Solar Beam in sun:"skipIf": "fieldActive('sun')").- The charge ends without a release when the monster faints, leaves the field (switch,
forceSwitch,switchOut), or its release is cancelled (a status cancelsbeforeMove, a field'sblockMovestops it).m.chargingreads 1 from the charging turn until the release starts. A charge move used through theuseMoveeffect is released at once (it does not charge). The battle screen shows "Charging " (and "(out of reach)").
Move target values: enemy (choose one enemy), allEnemies, randomEnemy, self, ally (choose one ally),
allyOrSelf (choose an ally or yourself), allAllies (allies, not yourself), team (yourself and allies), all (every other active monster),
faintedAlly (choose a fainted teammate that can still be revived, on the field or not: see Revives),
lowestHpAlly (yourself or the ally with the lowest HP percent, chosen automatically when the move runs; ties: field
order), lowestHpEnemy (the enemy with the lowest HP percent, chosen automatically), randomEnemies (every hit picks
a new random active enemy: hits is the total number of hits, each hit is aimed separately, with its own targeted
for a first-time target, reach check and accuracy roll; "Hit N times!" counts all of them).
A move that has nobody to affect when it is chosen (ally or allAllies with no ally left) cannot be chosen, like a
move without PP (the same goes for faintedAlly when no teammate has fainted); a monster left with no other move uses Rules.fallbackMove (or waits).
How a move runs: beforeMove (can be cancelled) → "X used Y!" → pay PP/cooldown → for each target:
targeted (another monster's move) → out of reach (charging, invulnerable) → accuracy check → type immunity → for
each hit: (accuracy again with hitAccuracy: "each") → crit roll → hit type → damage formula →
modifyDamageDealt/modifyDamageTaken → damage → hit → after all targets: afterMove → attacked for every
targeted monster → allyUsedMove / enemyUsedMove for the user's allies and enemies. Moves that deal no damage fire hit once per target that wasn't missed;
put their effects there (type immunity only stops damaging moves). A hit whose power formula gives 0 deals no damage
and fires hit with damage 0.
A move cancelled in beforeMove by a status or an ability (sleep, flinch...) is not used and costs nothing.
A move cancelled only by its own beforeMove trigger counts as a failed use: "X used Y!", then the cancel message
(default "But it failed!"), and its PP/cooldown are spent.
Move triggers can use the events beforeMove, hit, missed, modifyDamageDealt, afterMove, knockedOut.
The effect target defaults to target.
{ "abilities": [ {
"id": "blaze", "name": "Blaze", "description": "Fire moves are 1.5x stronger when HP is low.",
"statModifiers": { "atk": 1.0 }, // optional: stat multipliers while it is active (formulas)
"triggers": [ {
"on": "modifyDamageDealt",
"condition": "move.type == 'fire' && self.hpPercent <= 33",
"effects": [ { "type": "multiplyDamage", "value": 1.5 } ]
} ]
} ] }Items use the same format ({"items": [...]}). Use the consumeItem effect for single-use items.
The effect target defaults to self.
statModifiers multiply the effective stat (abilities first, then the item, then statuses); each formula reads the
raw stat as self.<stat>, so a flat bonus written as 1 + 15 / max(self.crate, 1) must add the flat bonuses
applied before it to that denominator to add up with them. Being a multiplier, such a bonus cannot raise a raw stat
of 0 (0 x anything is 0): give every species a base value of at least 1 in the stats that get flat bonuses.
statModifiers cannot change max HP or HP.
{ "statuses": [ {
"id": "burn", "name": "Burn", "short": "BRN", "description": "Loses 1/16 HP each turn, Attack halved.",
"kind": "major", // major (only one major status at a time) | volatile | buff | debuff
"duration": null, // formula in owner turns; omit/null = until removed
"stacking": "ignore", // ignore | refresh (reset duration, stays at 1 stack) | stack (add stacks up to maxStacks, refresh duration)
"maxStacks": 1, // only with "stack"
"immuneTypes": ["fire"],
"statModifiers": { "atk": 0.5 }, // formulas, may use `stacks`
"triggers": [ { "on": "turnEnd", "effects": [ { "type": "damage", "target": "self", "amount": "self.maxHp / 16" } ] } ],
"applyMessage": "{target} was burned!", // placeholders: {target} {self} (the holder), {user} {other} (who applied it), {status}, {stacks}
"removeMessage": "{target}'s burn wore off."
} ] }When a monster switches out, it loses every status that is not major, and its stat stages reset.
When it faints, its statuses are cleared after its fainted triggers ran (statuses can react to their holder fainting).
Optional. A field is something that affects the battle itself rather than one monster: weather, a terrain, a
screen, an entry hazard, a "speed inversion"... A field is either global (it covers every monster of both sides) or
on one side (it covers that side's monsters). Fields never belong to a monster: they keep working when the monster
that set them faints or switches out. Moves, abilities, items, statuses and other fields start and end them with the
setField and removeField effects; a battle can also start with some (Rules.json startFields, or a choice in the
battle menu).
{ "fields": [ {
"id": "rain", "name": "Rain", "short": "RAIN", "description": "Water moves 1.5x, fire moves 0.5x.",
"scope": "global", // global (default) | side
"group": "weather", // optional exclusive group, see below
"duration": 5, // formula in turns, see "Countdown"; omit/null = until removed
"stacking": "ignore", // ignore | refresh | stack, when it is set again while active (like statuses)
"maxStacks": 1, // only with "stack"
"selectable": true, // may be picked as a starting field in the battle menu (default false)
"statModifiers": { "spd": "hasType(self, 'rock') ? 1.5 : 1" }, // for every covered monster
"triggers": [ { "on": "modifyDamageDealt", "condition": "move.type == 'water'",
"effects": [ { "type": "multiplyDamage", "value": 1.5 } ] } ],
"rules": { }, // overrides and blocks, see below
"startMessage": "It started to rain!", // placeholders {field} {side} {user} {stacks}; "" = silent
"endMessage": "The rain stopped."
} ] }| key | meaning |
|---|---|
id |
required, unique ("all" is reserved) |
name, short, description |
display texts (short defaults to the first 4 letters of the name, upper-case) |
scope |
"global" (default) or "side" |
group |
setting a field removes every other active field of the same group in the same place (globally, or on that side), silently but with their fieldRemoved events (rain replaces sun) |
duration |
turns (formula, evaluated when the field is set: self/user = the monster that sets it, absent for start fields, plus stacks and turnsLeft like a status duration); omitted = permanent until removed |
stacking, maxStacks |
ignore (default): setting it again fails. refresh: the duration starts again. stack: adds layers up to maxStacks and restarts the duration (a full permanent stack fails). A refresh or a new layer logs the start message again and fires fieldSet again |
selectable |
the battle menu offers it as a starting field |
statModifiers |
stat multipliers for every covered monster, applied in m.<stat> after the statuses' (variables: self, stacks, turnsLeft, battle variables; self.<stat> reads raw stats; no dice) |
triggers |
see below |
rules |
see below |
startMessage, endMessage |
defaults: "<Name> started." / "<Name> ended." (side fields: "<Name> started on <side>'s side." / "... ended on <side>'s side."). {side} = the side's name, {user} = the monster that set it (start message) or removed it (end message; the one that set it when it expires), if any |
Triggers. A field's triggers are normal triggers plus an optional "for" key that says whose events
they listen to. The trigger behaves as if the monster the event is about owned it (self):
for |
listens to events about... |
|---|---|
"covered" (default) |
a monster the field covers |
"opponents" |
side fields only: a monster of the other side. "on": "damaged", "for": "opponents" = "when an enemy takes damage" |
"all" |
any monster on the field |
other, user and target are as in a status's triggers: when the event has no other monster, other is the
monster that set the field (if it is still on the field and is not self). stacks and turnsLeft are the field's,
{field} is its name in messages, and the effect target defaults to self. For one event, the field triggers run
after the move's, the ability's, the item's and the statuses' triggers, field by field in the order they were set. A
field that ends also hears its own fieldRemoved (it can react to its own end, like a status).
Countdown. A field with a duration counts down by itself, not in a monster's turns:
speed_order: at the end of every round, after every monster'sturnEndand status countdown, including the round it was set in. A 5-turn Rainfall used in round 1 ends at the end of round 5 (as in Classic-style rounds).action_bar: at the end of every turn of a monster of the side that set it (the turn it was set in counts). Fields with no setter (start fields) count down at the end of every turn. WhenturnsLeftreaches 0 the field ends: its end message, thenfieldRemoved.
Rules. The "rules" object changes how the battle works while the field is active. Every key is optional:
| key | does |
|---|---|
damage, hitChance, critChance, critMultiplier |
replace the Rules.json formula of the same name (same variables, plus stacks/turnsLeft) when the attacker is covered. If several active fields replace the same formula, the most recently set one wins |
actionBarFill |
replaces Rules.actionBar.fill for covered monsters (action_bar) |
reverseSpeed |
true: in speed_order, actions of the same priority run slowest first (switches still go first, ties stay random). Decided when the round's actions are sorted, so a field set during a round changes the next one. Global fields only; does nothing in action_bar |
disableItems, disableAbilities |
condition (formula or true; self = a covered monster, self.<stat> reads raw stats, no dice): while true, that monster's held item / ability does nothing (no statModifiers, no triggers) and hasItem / hasAbility read 0. The item or ability is kept |
blockMove, blockMoveMessage |
condition (user/self = the covered monster about to move, move, target/other = the primary target; no dice). When choosing, the target is absent: a true result makes the move unusable, like a move on cooldown (the battle menu and the AI skip it). When the move runs, it is checked again with the target: a true result makes it fail with the message (default "{user} can't use {move} because of {field}!"; placeholders {user} {move} {target} {field}), without spending PP (the AI also checks its chosen target and avoids such a move unless nothing else is legal). The fallback move is only checked when it runs |
blockStatus, blockStatusMessage |
condition (self/target = the covered monster about to get a status, other/user = the source, which is self for a self-inflicted status; variables status, statusKind): true = the status is not applied. The message (default "{self} is protected by {field}!"; placeholders {self} {target} {other} {status} {field}) is logged when a move applied the status (once per target and move) or when the status would have announced itself (not "") |
blockStatStage, blockStatStageMessage |
condition (self = the covered target, other = the source; variables stat, stages = the requested change): true = the stage change does not happen. Message default "{self} is protected by {field}!" (placeholders {self} {target} {other} {stat} (its name) {field}; "" = silent) |
For example Safeguard is "blockStatus": "statusKind == 'major' && !sameSide(self, other)" on a side field, Mist is
"blockStatStage": "stages < 0 && !sameSide(self, other)", Speed Inversion is a global field with "reverseSpeed": true,
Gravity is "blockMove": "hasFlag('flying')" and Stealth Rock is a side field with a switchIn trigger that deals
"floor(self.maxHp * typeMultiplier('rock', self) / 8)".
Start fields. Rules.json "startFields" lists fields set when the battle starts, before the leads enter (so
their switchIn already sees them), silently except their start message:
"startFields": [
{ "field": "rain", "duration": 5 }, // global field, 5 turns
{ "field": "spikes", "side": "side2" }, // side fields: "both" (default), "side1" or "side2"
{ "field": "grassy_terrain", "permanent": false } // the field's own duration
]duration is a formula (battle variables only); permanent defaults to true when there is no duration (the
field never expires) and to false otherwise. Start fields have no setter, so in action_bar they count down at the end
of every turn.
In the app. When the setup has fields with "selectable": true, the battle menu asks for starting fields after
the teams are chosen:
- It lists the
startFieldsunder "Set at the start of every battle" (with their duration when they have one). - It offers one choice per exclusive group (for example Weather: Rain / Harsh Sunlight / None), then a yes/no for every selectable field without a group. A side field also asks for the side: either player, or both.
- "No extra fields" skips the questions. A setup without selectable fields shows no field menu.
Fields picked there have no duration (they do not expire on their own) and are set after the startFields, so a
pick replaces a start field of the same group. They are still ordinary fields: a move or ability can replace them
(another field of the same group) or remove them (removeField). A rematch keeps them.
During the battle:
- The field display shows the global fields on one line:
Field: Rain (3) | Grassy Terrain (2). The number in brackets is the turns left, andx2means two layers. - Each side's fields are shown next to the side's name:
Player Reflect (4), Spikes x2. - Moves blocked by a
blockMoverule are marked[blocked by <field>]and cannot be picked. - Info lists the active fields with their descriptions.
The Bakedex browser also has a Fields page.
{
"on": "turnEnd", // event, see below
"condition": "self.hp < self.maxHp", // optional formula (fires only if true)
"chance": 30, // optional percent (formula)
"message": "{self} restored a little HP with its {item}!", // optional, logged when it fires
"limit": 1, // optional: at most this many times per battle (0 = unlimited)
"effects": [ ... ]
}Triggers are evaluated in this order: the move in use, then the owner's ability, then its held item,
then its statuses (oldest first), then the active fields that listen to the owner (in the order they were set).
Effects can trigger further events, up to 10 levels deep.
A fainted monster's triggers only fire for its own fainted event.
The owner is the monster whose ability, item or status (or the move it is using) holds the trigger; for a field's trigger it is the monster the event is about.
| event | fires when... | other is |
|---|---|---|
battleStart |
once, for every monster on the field when the battle begins | - |
switchIn |
the owner enters the field (including at battle start) | - |
switchOut |
the owner is withdrawn by a switch | - |
turnStart |
the owner's turn begins (speed_order: every round; action_bar: its own turn) | - |
turnEnd |
the owner's turn ends | - |
beforeMove |
the owner is about to use a move (cancelAction allowed) |
the primary target |
afterMove |
the owner finished using a move | the primary target |
hit |
the owner's move connected (once per hit) | the target |
missed |
the owner's move missed a target | the target |
modifyDamageDealt |
the owner is about to deal move damage (multiplyDamage allowed) |
the target |
modifyDamageTaken |
the owner is about to take move damage (multiplyDamage allowed) |
the attacker |
dealtDamage |
the owner damaged another monster (move or effect), even if a shield absorbed it all | the damaged monster |
damaged |
the owner took damage from any source (also when its shield absorbed it all) | the source, if any |
healed |
the owner recovered HP | the source, if any |
statusApplied |
a status was applied to the owner | the source, if any |
statusRemoved |
one of the owner's statuses ended | - |
fainted |
the owner fainted (move is the move that knocked it out, if any; user is its user) |
the attacker, if any |
knockedOut |
the owner made another monster faint | the fainted monster |
allyFainted |
an ally of the owner fainted | the fainted ally |
enemyFainted |
an enemy of the owner fainted | the fainted enemy |
revived |
the owner was revived; amount = the HP it came back with |
the monster that revived it, if any |
allyRevived |
an ally of the owner was revived | the revived ally |
enemyRevived |
an enemy of the owner was revived | the revived enemy |
fieldSet |
a field started (or was refreshed / stacked): fires once for every monster on the field, fastest first; field = its id |
the monster that set it, if any |
fieldRemoved |
a field ended (expired, removed, or replaced by its group): fires once for every monster on the field, fastest first; field = its id |
the monster that removed it, else the one that set it (if still on the field) |
targeted |
the owner is a target of another monster's move (an enemy's or an ally's), before the accuracy check; once per move use | the attacker (move = its move) |
attacked |
another monster's move that targeted the owner finished, whether it hit, missed or did nothing (after its afterMove; also when the attacker fainted during it); damage = what its hits took from the owner, landed = 1 if one hit landed. "When I've been attacked", unlike damaged ("when I lost HP") |
the attacker (move = its move) |
allyDamaged |
an ally of the owner took damage (any source, also when a shield absorbed it all); damage |
the damaged ally (user = the attacker when a move did it) |
enemyDamaged |
an enemy of the owner took damage (any source); damage |
the damaged enemy |
allyUsedMove |
an ally of the owner finished using a move (after its afterMove; not for a charging turn or a move that failed) |
the ally (user = the ally, target = its move's primary target) |
enemyUsedMove |
an enemy of the owner finished using a move | the enemy (user / target as for allyUsedMove) |
statusInflicted |
the owner applied a status to another monster (after that monster's statusApplied); status = its id |
the monster that got it |
Events that happen to the allies or enemies of a monster (allyDamaged, allyUsedMove...) fire in field order
(the owner's side first), like allyFainted. For moves used inside these events (counterattacks...), see useMove.
Every effect has a type. Most also take a target.
| type | fields | does |
|---|---|---|
damage |
target, amount |
direct damage (ignores the damage formula and types) |
heal |
target, amount |
restores HP (up to max) |
applyStatus |
target, status, chance?, duration?, stacks?, message? |
applies a status (chance in percent; duration overrides the status default; message replaces the status's applyMessage, "" = silent) |
removeStatus |
target, status? (id or "all"), kind? |
removes statuses by id or by kind |
statStage |
target, stat, stages |
raises/lowers a stat stage (clamped to Rules.statStages) |
multiplyDamage |
value |
multiplies the pending damage (only in modifyDamageDealt / modifyDamageTaken) |
cancelAction |
message? |
cancels the move (only in beforeMove) |
gauge |
target, amount |
adds amount percent of the action bar (negative = push back; action_bar only) |
shield |
target, amount |
adds a shield that absorbs damage first |
cooldown |
target, amount |
adds amount to the remaining cooldown of all the target's moves (negative = reduce) |
consumeItem |
target, message? |
removes the target's held item for the rest of the battle (message replaces the default "X's item was used up." line, "" = silent) |
message |
text |
prints text |
setField |
field, side?, duration?, stacks?, chance?, message? |
sets a field. side (side fields only, relative to the trigger owner): "self" (default), "enemy" or "both". duration overrides the field's, stacks default 1, chance in percent, message replaces the start message ("" = silent; also {field} and {side}). Follows the group and stacking rules; when it changes nothing, a status move logs "But it failed!" |
revive |
target, amount, message? |
brings a fainted target back with amount HP (at least 1, at most max HP); message replaces the default line ("" = silent). See Revives |
preventRevive |
target, message? |
the target (fainted or not) can no longer be revived in this battle |
removeField |
field (an id or "all") or group, side? |
ends the matching active fields: global ones always, side ones on side ("self", "enemy", "both" = default). message replaces each end message ("" = silent; also {field} and {side}) |
useMove |
actor?, move?, moveTarget?, power?, free?, message? |
every monster actor resolves to (default "self") uses a move right away, with the full move procedure (beforeMove, accuracy, hits, triggers...), outside its turn and without changing its turn or action bar. See below |
extraTurn |
target |
the target gets another turn ("X gets another turn!"; see Turn systems). Fails (silently, or "But it failed!" for a status move) when it already has one waiting or already had its extra turn (speed_order: this round; action_bar: since its last normal turn) |
transferStatus |
target, to?, status? and/or kind?, count?, mode? |
moves statuses from target (the source) to every to monster (default "self"): those matching status (an id or "all") and/or kind, the oldest count of them (formula, default 1; 0 = all). mode: "steal" (default: removed from the source once a receiver got it) or "copy" (the source keeps them). Turns left and stacks are kept; the receiver's immunities, the single-major rule and fields' blocks still apply (a status nobody could take stays on the source). "Move my debuffs onto the target" = "target": "self", "to": "target", "kind": "debuff" |
copyStages |
target, to? |
the stat stages of every to monster (default "self") become a copy of target's ("X copied Y's stat changes!") |
setHp |
target, amount |
sets the target's HP to amount (clamped to 0..max HP). Lowering it is damage from the owner that no shield absorbs (damaged, dealtDamage..., it can faint); raising it is healing (healed) |
balanceHp |
target |
every monster target resolves to (a group: "team", "allies", "enemies", "all") gets the group's average HP percent (at least 1 HP; computed before anybody changes; damage / healing events as for setHp) |
swapHp |
target |
the owner and the target swap their HP percentages (at least 1 HP each) |
forceSwitch |
target |
the target is replaced right away by a random benched teammate ("X was dragged out!", then its switchOut and the replacement's switchIn). Fails if it has nobody on the bench |
switchOut |
target? (default "self", also in move triggers) |
the target switches out right away and its controller picks who comes in from the bench (a U-turn: in a move's afterMove trigger). Fails if the bench is empty. Works even with Rules.switching false |
delay |
turns, attachTo?, effects, message? |
runs effects later, see below |
attack |
target, power, moveType?, category? |
a damaging strike of the owner computed with the damage formula: power (formula), moveType (a type id; default the current move's type, typeless without a move) and category (default the current move's if it deals damage, else the first damaging category of Rules.json). Type immunity, crits, hit types and modifyDamageDealt / modifyDamageTaken apply, it never misses and fires dealtDamage / damaged but not hit (so a "on hit: extra strike" trigger cannot loop). move in those events is the current move (its name and flags), or a stand-in named after the trigger's ability / item / status... (The key is moveType because type is the effect type.) |
useMove.
move:"first"(default: the actor's first move),"same"(the move of the current event, if the actor knows it; else its first move) or a move id (one the actor knows, or any move of the setup, which is then used without PP).moveTarget: a target selector resolved from the actor's point of view ("self"= the actor,"enemies"= its enemies,"lowestHpEnemy"...;"other","user","target"and"otherTarget"are the event's roles). Default: the event'sothermonster (a counterattack hits the attacker); when the event has none (or it is the actor itself), the move's normal target (a random enemy for anenemymove...). A single-target move (enemy,ally,allyOrSelf,faintedAlly) that cannot be aimed at the chosen monster (it fainted, left the field or is on the wrong side) fails, and so does an explicitmoveTargetthat selects nobody; the other move targets ignore the choice."otherTarget"= the target of the other monster's move: inallyUsedMove("join the attack") it is the target of the ally's move.power: a percent (formula, default 100;recipient= the actor) that multiplies the move's damage for this use (before the damage modifiers, at leastminDamage).free:true(default) = no PP or cooldown spent;false= it pays them, and fails if the move has no PP left or is on cooldown.messageis logged just before the move, only when it is really used (a trigger's ownmessageis logged even when the move then fails).- It fails silently when the actor is not on the field, has no move to use or nobody to aim it at. Counters of
counters are limited: at most 3 moves used by
useMovecan be nested inside one another. - Examples: counterattack =
"on": "attacked", "condition": "landed && !sameSide(self, other)"(an ally's heal targets it too) with{"type": "useMove", "move": "first", "message": "{self} strikes back!"}(moveTargetdefaults toother, the attacker); an ally joining an attack ="on": "allyUsedMove"with{"type": "useMove", "moveTarget": "otherTarget", "power": 50}; "use this move again" = a move'safterMovetrigger{"type": "useMove", "move": "same", "moveTarget": "target"}(limit it with a triggerchanceorlimit).
delay. effects run later, at the end of the attachTo monster's turnsth turn from now (turns is a formula,
at least 1; attachTo defaults to "target" in move triggers and "self" elsewhere). In speed_order that is the end
of the turnsth round, counting the current one (after every turnEnd, before the fields count down); in
action_bar the end of that monster's turnsth turn, counting the current one when it is in its turn. The delayed
effects use the roles of the trigger that scheduled them (self, other, user, target, move, the event
variables) and recipient per target; message is logged when they run. Nothing happens if the attached monster
faints or leaves the field before. Future Sight = "on": "hit" with
{"type": "delay", "turns": 2, "effects": [{"type": "attack", "power": 120}]} (attachTo and the strike's target
default to the move's target); Wish = {"type": "delay", "turns": 1, "attachTo": "self", "effects": [{"type": "heal", "target": "self", "amount": "self.maxHp / 2"}]}. A delay scheduled while delayed effects run waits at least until the
next count, so delays cannot repeat endlessly in one moment.
Effect target values: self, other, user, target, allies (the owner's allies), team (the owner + allies),
enemies, randomEnemy, all (every active monster), faintedAllies (every fainted teammate that can still be revived),
randomFaintedAlly (one of them), lowestHpAlly (the owner or the ally with the lowest HP percent; ties: field order),
lowestHpEnemy / highestHpEnemy (the enemy with the lowest / highest HP percent), randomAlly (one random ally,
not the owner; nobody when it is alone), otherTarget (the target of the other monster's move, when the event's
other is the move user: allyUsedMove, enemyUsedMove, targeted, attacked...). setField and removeField
take no target.
Only revive and preventRevive affect fainted monsters: they reach them through self, other, user, target,
faintedAllies and randomFaintedAlly. Every other effect skips fainted monsters.
Message placeholders: {self}, {other}, {user}, {target}, {move}, {damage}, {amount}, {status}, {ability}, {item}, {field}.
In an effect's message, they refer to the trigger's event like in the trigger message, except that {status} is the
applied status and {item} the consumed item. Some depend on the event: {damage} is filled only in the events that
have damage, {amount} only in healed and revived, {move} only in move triggers and in the events that can involve a move,
{status} in statusApplied/statusRemoved and in a status's own triggers, {field} in fieldSet/fieldRemoved and in
a field's own triggers; {other}/{target} are empty in ability
and item triggers of battleStart, switchIn, switchOut, turnStart, turnEnd and statusRemoved.
A status's applyMessage / removeMessage has its own set ({target}, {self}, {user}, {other}, {status}, {stacks})
and the Types.json messages only {target}; the setup check warns about placeholders a message can never fill.
The fainted event is the "on death" hook; revived, allyRevived and enemyRevived are the "on revive" ones.
A fainted monster comes back with the revive effect:
- Where it comes back: a monster still in its field slot (the usual case in action_bar battles, or before the end of the round in speed_order) comes back right there. One that was already replaced comes back to the bench, ready to be switched in or to replace the next monster that faints.
- Its state: the HP given by
amount(1 to max HP), no statuses, no stat stages, no shield and an empty action bar. It keeps its moves (with their PP and cooldowns), its item, and how many times its triggers fired (limit). In speed_order it does not run the action it chose before fainting (it acts again from the next round); in action_bar a monster that faints at the start of its own turn (e.g. from continuous damage) and is revived loses that turn. - Revive on death: a
revivewith"target": "self"in afaintedtrigger (of an ability, item or status) brings the monster back once its fainting has been fully handled. Thefainted,knockedOut,allyFaintedandenemyFaintedevents all happen first, then its statuses are cleared, then it is revived, sorevivedtriggers can give it new statuses. Use"limit": 1(orm.revivesin a condition) to make it work only once. - Revive moves: with
"target": "faintedAlly"the player picks a fallen teammate. The move'shittriggers run with that fainted monster astarget, so areviveeffect there brings it back.faintedAlliesandrandomFaintedAllywork in any trigger. - Anti-revive:
preventRevivemakes a monster unrevivable for the rest of the battle, for example from aknockedOuttrigger. A blocked or living monster ignoresrevive.
// Statuses.json: revive with 30% HP when the holder dies
{ "id": "soul_protection", "name": "Soul Protection", "kind": "buff", "duration": 2,
"triggers": [ { "on": "fainted", "effects": [ { "type": "revive", "target": "self", "amount": "self.maxHp * 0.3" } ] } ] }
// Moves.json: revive a chosen fallen ally with half its HP
{ "id": "resurrect", "name": "Resurrect", "category": "support", "target": "faintedAlly", "cooldown": 5,
"triggers": [ { "on": "hit", "effects": [ { "type": "revive", "amount": "target.maxHp / 2" } ] } ] }{
"name": "My team",
"members": [
{ "species": "embercub", "nickname": "Sparky", "level": 50, "ability": "blaze", "item": "leftovers",
"moves": ["ember", "tackle"], "points": { "atk": 252, "spe": 252 } }
]
}The team creator saves a new team as Teams/<name>.json, with the characters Windows does not allow in file names
replaced by _ (trailing dots and spaces removed, at most 100 characters); it asks before a save would overwrite the
file of another team, and an edited team is saved back to the file it was loaded from (written again under that file
name if the file was renamed or deleted meanwhile, never into another team's file).
The first team.active members start the battle on the field.
When both teams have a monster with the same name (e.g. the same species), the battle adds the side's name to both,
like Embercub (Player 1) and Embercub (Player 2), so the battle log stays unambiguous.
{ "name": "Demo", "description": "Classic-style singles", "turnSystem": "speed_order", // "speed_order" | "action_bar" "team": { "min": 1, "max": 6, "active": 1, "speciesClause": true, "itemClause": false }, "switching": true, // can a player switch as their action? "level": { "min": 1, "max": 100, "default": 50 }, "maxMoves": 4, "heldItems": true, "stats": [ { "id": "hp", "name": "HP" }, { "id": "atk", "name": "Attack" }, { "id": "spe", "name": "Speed" } ], "hpStat": "hp", "speedStat": "spe", "statFormulas": { // per stat id, "default" for the rest. vars: base, level, points "hp": "floor((2 * base + floor(points / 4)) * level / 100) + level + 10", "default": "floor((2 * base + floor(points / 4)) * level / 100) + 5" }, "statPoints": { "total": 510, "maxPerStat": 252 }, // optional, omit to disable stat points "statStages": { "min": -6, "max": 6, "multiplier": "stage >= 0 ? (2 + stage) / 2 : 2 / (2 - stage)" }, "categories": { // move categories; {} = does not deal damage "physical": { "attack": "atk", "defense": "def" }, "special": { "attack": "spa", "defense": "spd" }, "status": {} }, "damage": "floor(floor(floor(2 * user.level / 5 + 2) * move.power * attack / defense) / 50 + 2) * (sameType ? 1.5 : 1) * effectiveness * crit * random(0.85, 1)", "hitChance": "move.accuracy / 100", // 0..1, only used when the move has an accuracy "critChance": "1 / 24", // 0..1 "critMultiplier": "1.5", "hitTypes": [ // optional, see "Hit types" below { "id": "crushing", "chance": "effectiveness > 1 && !isCrit ? 100 : 0", "multiplier": 1.3, "message": "Crushing hit!" }, { "id": "glancing", "chance": "effectiveness < 1 ? 50 : 0", "multiplier": 0.7, "cancelsCrit": true, "message": "Glancing hit!" } ], "minDamage": 1, // damaging hits deal at least this much (unless immune) "pp": true, // use the moves' pp "cooldowns": false, // use the moves' cooldown "fallbackMove": "desperate_strike", // used when no move is usable (optional) "actionBar": { "size": 100, "fill": "speed", "start": "0" }, // action_bar only "turnLimit": 300, // 0 = no limit; the battle is a draw after this many turns "startFields": [ { "field": "rain", "duration": 5 } ] // optional, see "Fields.json" }