Phaser AE

Combat Rules

Pro packv3.0.0-3Arcade

Damage, healing, armour, elemental resistance, shields and timed status effects, resolved in a fixed order with an event for each thing that happened. The rules hold the numbers; your game decides what they look like.

This is a Pro pack. Upgrade to Phaser Pro to use it in your games.See Phaser Pro →

Released October 14, 2026 · updated October 20, 2026

Combat Rules: a screenshot of its demo
A note about these graphics

Expansion packs don't come with graphics. This screenshot is one way of drawing what the pack does, made for its demo: none of the sprites, tiles or backgrounds in it are in the pack.

That's by design. A pack is a game's logic, mechanics and effects, and it works with whatever art you give it. Some packs draw effects, backdrops or 3D scenes of their own, but none carries a game's sprites.

So the look is yours. Make the art on phaser.io, bring your own, or let your agent draw it in code: the same pack can look like anything.

The pack

combatRules

function combatRules():Pack<{combatRules:typeof rules}>

Install renderer-independent combat rules policies.

Helpers

combatant

function combatant(id: string, team: string, maxHp = 100, options: Partial<Omit<Combatant, 'id' | 'team' | 'maxHp'>> = {}): Combatant

Construct a combatant with independent status and resistance storage.

createCombat

function createCombat(entities: readonly Combatant[], seed = 0)

Create a reusable combat resolver.

createCombatDuel

function createCombatDuel(seed: number, difficulty: Difficulty = 'medium')

Every generated turn challenge includes a verified winning action sequence.

solveCombatDuel

function solveCombatDuel(initial: CombatDuel, maxTurns = 28): DuelMove[] | null

Search a bounded beam of actual combat states; returns a winning sequence or null, never labels a timeout unsolvable.

Classes

Combat

class Combat

Renderer-independent, ordered effect resolution. Armour is flat, resistance fractional; shields absorb last.

  • readonly entities: Combatant[] — Every combatant, in the order given (copies of those passed in); their hit points, shields and statuses change as effects land.
  • readonly events: CombatEvent[] — Events not yet taken by drain, oldest first.
  • time: number — Elapsed combat time in seconds, advanced by tick.
  • turn: number — The turn counter, advanced by nextTurn; starts at 0.
  • constructor(entities: readonly Combatant[], seed = 0)
  • get(id: string): Combatant — The combatant with this id. Throws if there is none.
  • apply(source: string, target: string, effects: readonly Effect[]): CombatEvent[] — Validate the entire sequence before applying it; dead targets reject damage, healing and status.
  • hasTag(id: string, tag: string): boolean — Whether any status on the combatant has this tag. Throws if the id is unknown.
  • tick(seconds: number) — Advances combat time by seconds, running periodic damage and healing at every boundary and expiring seconds-clock statuses. Throws if seconds is negative or not finite.
  • nextTurn() — Ends the turn: adds one to turn and advances the turns-clock statuses by one, so periodic effects tick and durations fall.
  • drain(): CombatEvent[] — Takes every pending event, oldest first, and clears them.
  • snapshot(): CombatSnapshot — The whole combat as plain JSON, to save, with the critical-hit stream's state.
  • static restore(s: CombatSnapshot): Combat — Rebuilds a combat from snapshot(), continuing the same critical-hit stream. Throws if the snapshot's version is not 2 or its contents are invalid.

CombatDuel

class CombatDuel

Small seeded turn challenge demonstrating numerical policies, rather than owning any presentation.

  • readonly combat: Combat — The combat being played, with the combatants hero and enemy.
  • readonly enemyMoves: { name: string; effects: Effect[]; }[] — The enemy's moves, played in rotation: each a name and the effects it applies to the hero.
  • turn: number — Turns taken so far.
  • energy: number — Energy the hero has to spend, 0 to 6; starts at 5.
  • won: boolean — Whether the enemy has been defeated.
  • lost: boolean — Whether the hero has been defeated.
  • solution: DuelMove[] — A verified winning sequence of moves, filled in by createCombatDuel; empty on a duel built directly.
  • message: string — A line describing the last result, for the host to show.
  • constructor(readonly seed: number, readonly difficulty: Difficulty = 'medium')
  • get intent() — The enemy's next move, applied to the hero right after the hero's next move.
  • play(move: DuelMove) — Plays a move and then the enemy's reply, and returns true. Returns false instead if the duel is over or the hero lacks the energy for the move.

Types

Combatant

interface Combatant

Something that can fight: hit points, shield, armour, element resistances and the statuses on it. Build one with combatant.

  • id: string — Unique name used to address it in Combat calls; not empty.
  • team: string — The side it fights for. The rules store it for the host to group and target by and never read it; not empty.
  • hp: number — Current hit points, from 0 to maxHp; at 0 it is defeated and rejects further damage, healing and statuses.
  • maxHp: number — Hit points at full health; at least 1.
  • shield: number — Damage points that absorb a hit, after armour and resistance, before hit points are lost; 0 or more.
  • armour: number — Flat damage taken off every hit before resistance, unless the hit ignores armour; 0 or more.
  • resistances: Record<string, number> — Per element, the fraction of that element's damage resisted, from -1 (takes double) to 1 (immune). An element with no entry is not resisted; an effect with no element is physical.
  • statuses: Status[] — The statuses on it now; each id appears once.

CombatEvent

interface CombatEvent

One thing that happened to a combatant, in the order it happened. Combat.apply returns them and drain takes them.

  • sequence: number — Running number from 1, unique within the combat.
  • time: number — Combat time in seconds when it happened.
  • turn: number — The turn counter when it happened.
  • source: string — Id of the combatant that caused it.
  • target: string — Id of the combatant it happened to.
  • kind: string — What happened: damage, absorbed, defeated, heal, shield, status, cleansed, expired or rejected.
  • amount: number — Its size: hit points for damage, absorbed, heal and shield; stacks for status, cleansed and expired; 0 for defeated and rejected.
  • detail: string — Extra detail: the element or critical for damage, the status id for status, cleansed and expired, target dead for rejected.

CombatSnapshot

interface CombatSnapshot

A combat saved as plain JSON, to restore with Combat.restore.

  • version: 2 — 2 since the critical-hit stream became the kernel's generator; a version 1 snapshot is refused.
  • time: number — Combat time in seconds.
  • turn: number — The turn counter.
  • random: RngState — Where the critical-hit stream stands: the four words of the kernel's rng.snapshot().
  • sequence: number — The number of events recorded so far.
  • entities: Combatant[] — Every combatant in the combat, with its current state.

DuelMove

type DuelMove = 'strike' | 'fire' | 'guard' | 'heal' | 'cleanse'

A move the hero can make: strike (18 damage, may crit, restores 1 energy), fire (25 fire damage that ignores armour, costs 2), guard (24 shield, restores 1), heal (32 hit points, costs 3) or cleanse (removes statuses, costs 1).

Effect

type Effect = { kind: 'damage'; amount: number; element?: string; critChance?: number; critMultiplier?: number; ignoreArmour?: boolean; } | { kind: 'heal' | 'shield'; amount: number; } | { kind: 'status'; status: StatusSpec; } | { kind: 'cleanse'; statusId?: string; }

One thing that can be done to a target, applied in order by Combat.apply.

  • damage: amount hit points before armour and resistance; element default physical; critChance (0 to 1, default 0) is the chance of a critical hit, which multiplies the damage by critMultiplier (default 1.5, at least 1); ignoreArmour skips flat armour.
  • heal: restores amount hit points, up to maxHp.
  • shield: adds amount shield points.
  • status: applies a status.
  • cleanse: removes the status named by statusId, or every status when it is omitted.

Status

interface Status

A timed condition on a combatant that can deal or heal a set amount every interval: poison, regeneration, a marked target. Stacks multiply its per-tick amounts.

  • id: string — Names the status; applying one with an id already present stacks, refreshes or replaces it according to policy.
  • remaining: number — Time left before it expires, on its own clock: seconds or turns. Above 0.
  • clock: 'seconds' | 'turns' — What counts down remaining: seconds follow tick, turns follow nextTurn. A status keeps its clock unless it is replaced.
  • stacks: number — Current stacks, from 1 to maxStacks; the per-tick damage and healing are multiplied by it.
  • maxStacks: number — Most stacks it can hold, a whole number from 1 to 1000.
  • policy: 'refresh' | 'replace' | 'stack' — What reapplying it does: refresh keeps the longer remaining time, stack does the same and adds stacks up to maxStacks, replace swaps the old one for the new.
  • interval: number — Clock units (seconds or turns) between periodic ticks of its damage and healing. Above 0.
  • elapsed: number — Clock units since its last tick; 0 on a new status.
  • damage: number — Damage dealt to the holder on each tick, per stack. It ignores armour. 0 for none.
  • healing: number — Hit points healed on each tick, per stack. 0 for none.
  • element: string — The element of its tick damage, matched against the holder's resistances.
  • tags: string[] — Labels on the status; Combat.hasTag tests a combatant for them, so the host can ask whether it is burning.

StatusSpec

type StatusSpec = Omit<Status, 'elapsed' | 'stacks'> & { stacks?: number; }

A status as authored in an Effect: a Status without the running elapsed, and with stacks optional (default 1).