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 bydrain, oldest first.time: number— Elapsed combat time in seconds, advanced bytick.turn: number— The turn counter, advanced bynextTurn; 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 byseconds, running periodic damage and healing at every boundary and expiringseconds-clock statuses. Throws ifsecondsis negative or not finite.nextTurn()— Ends the turn: adds one toturnand advances theturns-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 fromsnapshot(), 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 combatantsheroandenemy.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 bycreateCombatDuel; 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 inCombatcalls; 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 tomaxHp; 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 isphysical.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,expiredorrejected.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 orcriticalfor damage, the status id for status, cleansed and expired,target deadfor 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'srng.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:amounthit points before armour and resistance;elementdefaultphysical;critChance(0 to 1, default 0) is the chance of a critical hit, which multiplies the damage bycritMultiplier(default 1.5, at least 1);ignoreArmourskips flat armour.heal: restoresamounthit points, up tomaxHp.shield: addsamountshield points.status: applies a status.cleanse: removes the status named bystatusId, 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 topolicy.remaining: number— Time left before it expires, on its ownclock: seconds or turns. Above 0.clock: 'seconds' | 'turns'— What counts downremaining:secondsfollowtick,turnsfollownextTurn. A status keeps its clock unless it is replaced.stacks: number— Current stacks, from 1 tomaxStacks; 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:refreshkeeps the longer remaining time,stackdoes the same and adds stacks up tomaxStacks,replaceswaps 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'sresistances.tags: string[]— Labels on the status;Combat.hasTagtests 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).


