What it does
You build combatants with hit points, armour, shields and resistances per element, and put them in a combat with a seed.
Then you apply a list of effects from a source to a target: damage, a heal, a shield, a status or a cleanse. The whole
list is checked before anything changes, and each effect lands in order. Damage takes the critical multiplier first,
then flat armour, then the target's resistance to its element, then whatever shield is left, and only then hit points. A
defeated combatant rejects further damage, healing and statuses, so healing never brings one back.
Statuses are data, not scripts. Each has a clock, seconds or turns, a duration, an interval, damage or healing per
stack, a stack cap and a policy for being applied again: refresh keeps the longer time, stack also adds stacks, and
replace starts it over. tick(seconds) advances the seconds clock and nextTurn() the turns clock, and every
periodic tick is found exactly, including one on the moment a status expires, however you split the time. Tags on a
status let the host ask questions such as "is it burning?" through hasTag.
Everything that happens becomes an event with a sequence number, the time, the turn, the source and target, a kind and
an amount: damage, absorbed, defeated, heal, shield, status, cleansed, expired or rejected. Your game
drains them to show numbers, flashes and log lines. The pack also ships a small seeded turn duel, with a solver that
proves each generated challenge can be won.
Features
- An exact damage order — critical hit, flat armour, elemental resistance, shield absorption, then hit points, with options to ignore armour and to set the critical chance and multiplier per hit.
- Elemental resistance — a fraction per element from -1 to 1, where negative values are a weakness and 1 is immunity.
- Shields — shield points accumulate and absorb what armour and resistance let through.
- Timed statuses — poison, regeneration and marks with a seconds or turns clock, intervals, stacks up to a cap, and refresh, stack or replace on reapplication.
- Ordered events — every result with a sequence number, for damage numbers, combat logs and sound.
- Checked before it changes — a bad effect list is refused whole, so a fight is never left half-applied.
- Snapshot and restore — the combat as plain JSON, including where the critical-hit stream stands, so a restored fight draws the same criticals the original would have.
- A turn duel with a proof —
createCombatDuel(seed, difficulty)makes a challenge ateasy,medium,hardorvery-hardwith a winningsolutionfound bysolveCombatDuel.
Good for
- Action RPGs and brawlers that need armour, resistances and damage over time without writing the arithmetic again.
- Turn-based skirmishes and card duels, with statuses on a turns clock that tick as each turn ends.
- Tower defence and survival games with burning, poisoned or shielded enemies.
- Server-side or headless simulation, where a fight must resolve the same way without a renderer.
- Balancing and testing, since a seed and a list of effects always give the same events.
Make it yours
The model is configured by the data you pass in. Each combatant sets its hit points, armour, starting shield, resistances and statuses; each effect sets its amount, element, critical chance and multiplier and whether it ignores armour; each status sets its clock, duration, interval, damage and healing per stack, element, stack cap, policy and tags. Element names are yours, so fire and ice or holy and shadow are just strings.
Two different uses: a real-time arena where enemies take poison that ticks every second through tick, with fire-weak
and armoured foes; or a turn-based duel where every status runs on turns, bleeds stack to a cap of three, and the game
calls nextTurn as each round ends.
A quick look
A hero and a slime, a fire hit, and the events it produced:
import { combatant, createCombat } from '@phaser-ae/combat-rules';
const combat = createCombat([combatant('hero', 'party'), combatant('slime', 'foes', 40)], 29);
combat.apply('hero', 'slime', [{ kind: 'damage', amount: 20, element: 'fire' }]);
const feedback = combat.drain();
Works with
abilities— costs, cast timing, charges and cooldowns on top of these rules.turn-battle— a turn-taking battle model with its own attack-against-defence formula, for a classic JRPG.melee-combat— swing phases, combo windows, blocking and parries for the moment a hit lands.
Good to know
- Statuses tick data-defined damage, healing and tags; they do not run arbitrary scripts.
- Resurrection, elemental reaction chains and networking are left to your game.
- Use one clock per status:
tickadvances seconds-clock effects andnextTurnturns-clock effects. - Events queue until drained, so drain them regularly.
- The duel's solver searches a bounded set of states: a null answer means no win was found within its limits, not that none exists.






