Skip to content

Chapter 47 — Combat System

Part VII — Systems Reference

Complete reference for the combat system — roles, encounters, bosses, danger, and AI intent.

Combat spans three layers, each building on the last:

LayerFilePurpose
Combat Corecombat-core.tsAttack resolution, damage, defeat, stamina, guard, disengage
Combat Rolescombat-roles.ts8 role templates, encounter composition, boss phases, danger rating
Encounter Libraryencounter-library.tsArchetype factories, boss templates, pack audit
Combat Intentcombat-intent.tsAI decision-making biases, morale, flee logic
Combat Engagementengagement-core.tsFrontline/backline positioning, bodyguard interception
Combat Recoverycombat-recovery.tsPost-combat wound statuses, safe-zone healing
Combat Reviewcombat-review.tsFormula explanation, hit-chance breakdown
Defeat Falloutdefeat-fallout.tsPost-combat faction consequences, reputation shifts
Combat Summarycombat-summary.tsQuery, audit, format, and inspect combat content

Combat Core is an EngineModule (runtime). Combat Roles, Encounter Library, and Combat Summary are pure functions (authoring time). The rest are EngineModules that layer behavior onto the core.

Eight built-in role templates define enemy archetypes. Each entity carries its role as a tag (role:brute, role:boss, etc.).

RoleHPStaminaBiasPositionMorale
brute1.5x1.0x+5 attack, +3 finish, -2 guardFrontlineStands firm
skirmisher0.8x1.3x+5 pressure, +3 disengageFlankerUnpredictable
backliner0.7x1.0x+3 guard, +5 disengage, -2 attackBacklineBreaks early
bodyguard1.3x1.0x+5 protect, +4 guard, -2 attackFrontlineStands firm
coward0.6x1.0x+8 disengage, -3 attackVariableBreaks early
boss3.0x2.0x+3 attack/guard/pressure, +2 finishFrontlineNever flees
minion0.4x0.8x+3 attack/pressure, -5 guardFrontlineBreaks early
elite1.8x1.5x+2 attack/guard/pressure/finishFrontlineStands firm

HP and stamina multipliers apply relative to a base value. A brute with base 10 HP gets 15; a minion gets 4.

Add a role tag to an entity’s tags array:

export const guard: EntityState = {
id: 'gate-guard',
name: 'Gate Guard',
tags: ['enemy', 'human', 'role:brute'],
// ...
};

Use createRoledEnemy() to apply multipliers and engagement tags automatically:

import { createRoledEnemy } from '@ai-rpg-engine/modules';
const scaledGuard = createRoledEnemy(baseGuard, 'brute');
// HP scaled by 1.5x, tags include 'role:brute'

Each starter maps its genre-specific stats to the three combat roles:

const weirdWestFormulas: CombatFormulas = {
statMapping: { attack: 'grit', precision: 'draw-speed', resolve: 'grit' },
hitChance: (attacker, target) => { /* ... */ },
damage: (attacker) => Math.max(1, attacker.stats.grit ?? 3),
// ...
};

The statMapping tells combat-core which entity stats to read for attack (damage), precision (accuracy/evasion), and resolve (defense/guard).

Seven composition types describe the shape of a fight:

CompositionDescription
soloSingle enemy
patrolRoutine sweep, mixed roles
ambushSudden surprise attack
boss-fightBoss + optional support
hordeMany minions, optional leader
duel1-on-1 or small elite fight
customAuthor-defined

Each encounter is an EncounterDefinition:

export type EncounterDefinition = {
id: string;
name: string;
participants: EncounterParticipant[];
composition?: EncounterComposition;
validZoneIds?: string[];
narrativeHooks?: { tone?: string; trigger?: string; stakes?: string };
};

Five archetype factories build encounters from common patterns:

import {
createPatrolEncounter,
createAmbushEncounter,
createBossFightEncounter,
createHordeEncounter,
createDuelEncounter,
} from '@ai-rpg-engine/modules';
const patrol = createPatrolEncounter(
{ id: 'gate-patrol', name: 'Gate Patrol', validZoneIds: ['castle-gate', 'great-hall'] },
[{ entityId: 'guard-a', role: 'brute' }, { entityId: 'guard-b', role: 'skirmisher' }],
);
const ambush = createAmbushEncounter(
{ id: 'alley-ambush', name: 'Alley Ambush', validZoneIds: ['back-alley'] },
[{ entityId: 'assassin', role: 'skirmisher' }],
);

Boss is always first in participants:

const bossFight = createBossFightEncounter(
{ id: 'throne-room', name: 'Throne Room Showdown', validZoneIds: ['lords-chamber'] },
{ entityId: 'dragon-lord', role: 'boss' },
[{ entityId: 'minion-a', role: 'minion' }, { entityId: 'minion-b', role: 'minion' }],
);

Optional leader placed first:

const horde = createHordeEncounter(
{ id: 'swarm-attack', name: 'Swarm Attack' },
[{ entityId: 'm1', role: 'minion' }, { entityId: 'm2', role: 'minion' }],
{ entityId: 'alpha', role: 'elite' }, // optional leader
);
const duel = createDuelEncounter(
{ id: 'rival-duel', name: 'Rival Duel' },
[{ entityId: 'rival', role: 'elite' }],
);

Bosses shift behavior at HP thresholds. A BossDefinition declares the entity ID and an array of phase transitions:

export type BossPhaseTransition = {
hpThreshold: number; // 0-1, triggers when HP drops at or below
narrativeKey: string; // e.g. 'enraged', 'desperate'
addTags?: string[]; // tags added on transition
removeTags?: string[]; // tags removed on transition
spawnEntityIds?: string[]; // entities spawned on transition
};

Three factory functions cover common boss patterns:

Escalating — 2 phases at 50% and 25% HP. Adds aggression tags.

const boss = createEscalatingBoss({ entityId: 'warlord' });
// Phase 1 (50%): adds 'enraged'
// Phase 2 (25%): removes 'enraged', adds 'desperate'

Summoner — 3 phases at 75%, 50%, 25% HP. Spawns reinforcements each phase.

const boss = createSummonerBoss(
{ entityId: 'necromancer' },
{ phase1Spawns: ['skeleton-a'], phase2Spawns: ['skeleton-b'], phase3Spawns: ['skeleton-c'] },
);

Phase-Shift — Custom phases with tag swaps. Full control.

const boss = createPhaseShiftBoss({ entityId: 'shapeshifter' }, [
{ hpThreshold: 0.7, narrativeKey: 'charging', addTags: ['charging'] },
{ hpThreshold: 0.3, narrativeKey: 'overloaded', addTags: ['overloaded'], removeTags: ['charging'] },
]);

Register the boss phase listener in your setup:

import { createBossPhaseListener } from '@ai-rpg-engine/modules';
const engine = new Engine({
modules: [
// ...other modules
createBossPhaseListener(myBossDef),
],
});

The listener watches combat.damage.applied events and emits boss.phase.transition when HP crosses a threshold.

Boss entities need maxHp and maxStamina in their resources for the phase system to calculate HP ratios:

export const dragon: EntityState = {
id: 'dragon-lord',
tags: ['enemy', 'role:boss'],
resources: { hp: 50, maxHp: 50, stamina: 14, maxStamina: 14 },
// ...
};

calculateDangerRating() scores an encounter’s threat level (0–100):

ScoreLevel
0–20Trivial
21–40Routine
41–60Dangerous
61–80Deadly
81–100Overwhelming

Factors: total enemy HP vs player HP, total attack vs player attack, enemy count, boss presence (1.5x multiplier).

combat-intent.ts drives NPC decision-making through PackBias:

type PackBias = {
tag: string;
name: string;
modifiers: Partial<Record<CombatIntentType, number>>;
moraleFleeThreshold?: number;
};

Intent types: attack, guard, pressure, finish, disengage, protect.

Each role template includes a built-in PackBias. Brutes favor attack/finish. Cowards favor disengage. Bosses are well-rounded but almost never flee.

createCombatRecovery() handles post-combat healing. Safe zones (tagged safe or custom tags) restore HP and remove wound statuses. Configure with:

createCombatRecovery({ safeZoneTags: ['safe', 'colony-core'] })

createDefeatFallout() triggers faction-level consequences when entities are defeated — reputation shifts, morale changes, and narrative events.

combat-summary.ts provides inspection functions:

FunctionPurpose
queryCombatEntities()Find entities by role, tag, or stat range
summarizeCombatContent()Pack-level overview of all combat content
auditCombatContent()Flag warnings (missing roles, unbalanced encounters)
formatCombatSummary()Human-readable text output for director prompts

auditPackCoverage() checks a starter against the minimum content bar:

  • 3+ role-tagged enemies (one with role:boss + maxHp/maxStamina)
  • 3+ encounters (including at least one patrol and one boss-fight)
  • 1+ boss definition with 2+ phases
  • Encounters spread across zones
import { auditPackCoverage } from '@ai-rpg-engine/modules';
const result = auditPackCoverage('my-pack', enemies, encounters, bossDefs, zoneIds);
if (result.missingMinimumBar.length > 0) {
console.warn('Missing:', result.missingMinimumBar);
}

All 10 starters meet the minimum content bar:

StarterPack IDEnemiesRolesEncountersBoss Defs
Fantasychapel-threshold3brute, skirmisher, boss31
Gladiatoriron-colosseum3brute, skirmisher, boss31
Zombieashfall-dead3brute, skirmisher, boss31
Pirateblack-flag-requiem3brute, skirmisher, boss31
Roninjade-veil3skirmisher, bodyguard, boss31
Vampirecrimson-court3elite, minion, boss31
Cyberpunkneon-lockbox3bodyguard, skirmisher, boss31
Colonysignal-loss3bodyguard, minion, boss31
Detectivegaslight-detective3minion, brute, boss31
Weird Westdust-devils-bargain3elite, skirmisher, boss31
  1. Role diversity — Use at least 3 different roles per pack. Brute + skirmisher + boss is the minimum. Adding a bodyguard or minion creates more interesting compositions.

  2. Zone spread — Spread encounters across zones. Players should encounter combat in multiple areas, not just the final boss room.

  3. Danger curves — Design encounters that escalate: patrols (routine) → ambushes (dangerous) → boss fights (deadly). Avoid overwhelming encounters early.

  4. Narrative hooks — Use narrativeHooks on encounters to guide the AI narrator. Tone, trigger, and stakes give context beyond raw mechanics.

  5. Boss pacing — Two phases is the minimum. Place the first threshold at 50% and the second at 25% for natural escalation. Use addTags/removeTags to shift behavior rather than just damage.

  6. Validation — Run validateBossDefinition() and validateEncounter() in tests. Both return arrays of warning strings — empty means valid.

Combat entities can also use abilities — powers with costs, checks, cooldowns, and status effects. The ability system layers on top of combat core, adding tactical depth through genre-native powers, resistance profiles, and AI-aware decision making. See Chapter 48: Abilities System for the full reference.