What it does
You make a Battle from fighter definitions, each with a side, six stats (health, magic, attack, defence, speed and
luck), and optionally an element and Break shield points. next moves to the next fighter's turn, starting a new round
in speed order when one ends, and returns who is acting. Your screen shows a menu for a hero or a telegraph for a foe,
then calls act with the choice: attack, skill, item, defend, flee or wait. An action that cannot be done returns false
and keeps the turn, so a bad menu choice never skips anyone.
Every number lives in the rules. The base damage is attack less half the defence, with a spread and a minimum, and criticals come from luck. Round the element wheel each element beats another: a weakness hit deals more and knocks a point off the target's Break gauge, a resisted hit deals less, and when the gauge empties the fighter spends its next turn recovering and takes extra damage meanwhile. Defending halves the damage taken and restores some magic. Difficulty scales the foes' health and attack.
The battle draws nothing and waits for nothing. drain returns events in order, such as round, turn, hit,
break, recover, down, heal, defend, flee and end, and your screen plays them however it likes. Random
numbers come from the stream you pass, so the same seed and choices replay the same battle, and snapshot and
restore hold its whole state. The rules are also exported on their own (damage, isCrit, turnOrder,
fleeSucceeds and more), for a game that resolves fights its own way.
Features
- Speed-ordered rounds — fighters act in speed order, with
next,actor,roundandorderto drive a screen. - Six actions — attack, skill, item, defend, flee and wait.
- An element wheel — weakness hits at 1.5 times and resisted hits at half by default, on fire, ice, lightning and earth or a wheel of your own.
- The Break gauge — shield points that weakness hits empty, costing the fighter a turn and leaving it open to extra damage.
- Criticals — a chance from luck, double damage by default.
- Skill effects as a registry — damage, heal, guard and haste built in, and effects of your own, each saying whom it aims at and what it does.
- A foe policy —
choosedecides what foes do; by default they attack a random standing opponent. - Menu helpers —
targets,reachandpickssay whom an action can reach and what a menu should ask for. - Rules as data — every factor and chance, from
critFactortofleeChanceanddropChance, plus difficulty scaling. - Events, replays and snapshots — every change reported, the same battle from the same seed, and saves mid-fight.
Good for
- JRPG battles: a party against a group of monsters, with weaknesses to find and shields to break.
- Tactics skirmishes without a grid, where only turn order and stats matter.
- A card duel's combat phase, resolved turn by turn.
- Auto-battlers, with both sides choosing through policies.
- Turn-based encounters inside other genres, such as an adventure game or a dungeon crawler that cuts to a battle.
Make it yours
BattleOptions takes rng, rules over BATTLE_RULES, effects, choose and canFlee. The rules cover the
wheel, weakFactor (1.5), resistFactor (0.5), critFactor (2), critChance (luck over 100), the base formula,
the spread (0.2), minDamage, brokenFactor, defendFactor, defendMp, hasteFactor, hasteTurns, healAtk,
fleeChance (0.6), dropChance (0.3) and difficulty (gentle, fair and cruel multipliers).
Two different uses: a classic party against wolves on the default wheel, with Break shields on the wolves and fleeing allowed; and a battle of its own with a light-and-dark wheel, triple criticals, a skill that drains life, foes that gang up on the weakest hero, haste that wears off and no fleeing.
A quick look
Two fighters, a loop over turns, and the events for the screen:
import { Battle } from '@phaser-ae/turn-battle';
const b = new Battle([
{ id: 'wren', side: 'hero', stats: { hp: 40, mp: 12, atk: 12, def: 4, spd: 9, lck: 5 }, element: 'fire' },
{ id: 'wolf', side: 'foe', stats: { hp: 30, mp: 0, atk: 8, def: 3, spd: 7, lck: 2 }, element: 'ice', breaks: 2 },
], { rng: () => game.rng.next() });
for (let f = b.next(); f; f = b.next()) {
b.act(f.side === 'hero' ? { kind: 'attack', target: 'wolf' } : b.auto());
for (const e of b.drain()) { /* round, turn, hit, break, recover, down, heal, defend, flee, end */ }
}
b.result; // 'win' | 'lose' | 'flee'
Works with
rpg— its battle screen is a view over this battle.party— heroes with levels, gear and a bag to bring into a fight.rpg-world— monsters with elements, shields and drops, written as data.combat-rules— real-time damage with flat armour, resistances, shields and timed statuses instead; the two do not share a damage model.abilities— cooldowns and cast times for real-time combat.
Good to know
- Two sides and no positions: there are no rows, ranges or areas, and a skill that hits all hits a whole side.
- One action a turn, with no reactions, counters or interrupts.
- No timed statuses beyond defending and haste; a game's own statuses live in its fighters'
dataand its skill effects. - An item's
useis a function, so an action is not plain data, and a snapshot holds the battle's state rather than its history.






