Feature 06: Combat Engine
Overview
The heart of Delve: the server-side simulation engine that resolves combat encounters. Uses d100 percentile dice, processes skill queues round-by-round in initiative order, applies damage/healing/conditions, and produces a detailed log of every action and roll. This is a pure game logic feature — no API routes yet (that’s Feature 07).
Dependencies
- Feature 05 (Skill Queue) — for skill definitions and queue validation
- Feature 03 (Items & Inventory) — for equipped gear stats
Technical Tasks
1. Static Game Data — Creatures
- Create
data/creatures/directory with TOML files:- Organized by zone/theme:
goblins.toml,undead.toml,beasts.toml, etc. - Per creature: id, name, level, hp, attributes (might/logic/speed/presence/fortitude/luck), armor, evasion, skills (ordered list — creature’s own “queue”), loot_table_id, xp_reward
- Start with 15-20 creatures for levels 1-10 (enough to test the engine)
- Boss creatures: higher stats, multi-phase (phase 2 activates at HP threshold with new skill list)
- Organized by zone/theme:
2. Combat Types (crates/types)
- Create
src/combat.rs:CombatState: tracks all participants, HP, resources, active conditions, round numberParticipant: id, name, is_player, hp, max_hp, resources, attributes, armor, evasion, skill_queue, active_conditions, initiative_rollCombatAction: the result of a single skill execution (actor, target, skill, roll, hit, damage, healing, conditions applied)RoundLog: list of actions + condition tick results for one roundEncounterLog: list of rounds + outcome for one encounterRunLog: list of encounter logs + final statusEncounterOutcome: Victory, Survived, PartyWipe, FledRunStatus: Completed, FailedStatusEffectenum: Poisoned, Bleeding, Burning, Stunned, Blinded, Slowed, Weakened, Shielded, Regenerating, Empowered, Hasted, etc.Condition: effect type, duration (rounds remaining), source_id, potency
3. d100 Roll System (crates/game)
- Create
src/combat/rolls.rs:roll_d100(rng: &mut impl Rng) -> u8: returns 1-100calculate_hit_chance(attacker: &Participant, defender: &Participant, skill: &SkillDef) -> u8:- Base: 50% + (attacker relevant attribute - defender relevant attribute) + skill success_modifier
- Clamp to 5-95 (before crit/fail rules)
is_critical(roll: u8, luck: u8) -> bool: roll <= 5 (modified by luck)is_critical_fail(roll: u8, luck: u8) -> bool: roll >= 96 (modified by luck)calculate_damage(base_damage: (u16, u16), attacker: &Participant, defender: &Participant, is_crit: bool, rng: &mut impl Rng) -> u32:- Roll between base damage range
- Add might modifier (for physical) or logic modifier (for magic)
- Subtract defender armor (physical) or resistance (magic)
- Critical: double damage
- Floor at 1 (always deal at least 1 damage)
4. Initiative System (crates/game)
- Create
src/combat/initiative.rs:roll_initiative(participant: &Participant, rng: &mut impl Rng) -> u16:- Base: Speed attribute + weapon speed modifier
- Add d100 roll
- Lower total = faster (acts first)
sort_by_initiative(participants: &mut [Participant]):- Sort ascending (lowest initiative acts first)
5. Condition System (crates/game)
- Create
src/combat/conditions.rs:apply_condition(target: &mut Participant, condition: Condition):- Add to active conditions (or refresh duration if already present)
tick_conditions(participant: &mut Participant, log: &mut Vec<ConditionTick>):- For each active condition:
- Apply per-round effect (poison damage, regen healing, etc.)
- Decrement duration
- Remove if expired
- Log all effects
- For each active condition:
has_condition(participant: &Participant, effect: StatusEffect) -> bool- Condition effects:
- Poisoned: X damage per round
- Bleeding: X damage per round, reduced healing
- Burning: X fire damage per round
- Stunned: skip next action
- Blinded: -30% hit chance
- Slowed: +50 initiative (acts later)
- Weakened: -25% damage
- Shielded: absorb X damage before HP loss
- Regenerating: heal X per round
- Empowered: +25% damage
- Hasted: -25 initiative (acts sooner)
6. Skill Queue Execution (crates/game)
- Create
src/combat/execution.rs:evaluate_queue(participant: &Participant, combat_state: &CombatState) -> Option<&SkillQueueSlot>:- Iterate queue in order
- For each slot, evaluate condition against current combat state
- Return first skill whose condition is met AND resource cost can be paid
- Return None if no valid skill found (participant passes turn)
execute_skill(actor: &mut Participant, target: &mut Participant, skill: &SkillDef, rng: &mut impl Rng) -> CombatAction:- Deduct resource cost from actor
- Roll d100 for hit
- Calculate damage/healing if hit
- Apply conditions if hit
- Handle AoE (apply to multiple targets)
- Return CombatAction log entry
select_target(actor: &Participant, skill: &SkillDef, combat_state: &CombatState) -> Option<ParticipantId>:- Single target attack: target enemy with lowest HP
- AoE: all enemies
- Self: the actor
- Ally heal: ally with lowest HP
7. Encounter Resolution (crates/game)
- Create
src/combat/encounter.rs:resolve_combat_encounter(players: Vec<Participant>, enemies: Vec<Participant>, rng: &mut impl Rng) -> EncounterLog:- Roll initiative for all participants
- Loop rounds (max 50 per encounter):
- For each participant in initiative order:
- If dead/stunned, skip
- Evaluate skill queue → get skill
- Select target
- Execute skill
- Log action
- Tick conditions on all participants
- Check end conditions:
- All enemies dead → Victory
- All players dead → PartyWipe
- Round limit reached → draw (treated as Victory with reduced rewards)
- For each participant in initiative order:
- Return EncounterLog with all rounds and outcome
8. Non-Combat Encounter Handlers (crates/game)
- Create
src/combat/encounters_noncombat.rs:resolve_trap(party: &[CharacterSnapshot], trap: &TrapDef, rng: &mut impl Rng) -> EncounterLog:- Skill check (Perception to detect, Acrobatics/Athletics to avoid)
- Failure: party takes damage
resolve_decision(party: &[CharacterSnapshot], decision: &DecisionDef, rng: &mut impl Rng) -> EncounterLog:- Auto-pick based on highest relevant skill in party
- Each choice leads to different outcomes (bonus loot, skip encounters, etc.)
resolve_hazard(party: &[CharacterSnapshot], hazard: &HazardDef, rng: &mut impl Rng) -> EncounterLog:- Environmental damage, skill checks to mitigate
resolve_rest_point(party: &mut [Participant]) -> EncounterLog:- Restore HP by 25%, restore some resources
resolve_puzzle(party: &[CharacterSnapshot], puzzle: &PuzzleDef, rng: &mut impl Rng) -> EncounterLog:- Logic/Arcana/Perception checks, success grants bonus
9. Full Run Simulation (crates/game)
- Create
src/simulation.rs:SimulationContext: run record, party snapshots, quest definition, seeded RNG, log builderresolve_run(ctx: &mut SimulationContext) -> RunResult:- For each encounter in quest:
- Create participants from party snapshots + encounter enemies
- Dispatch to appropriate resolver (combat, trap, decision, etc.)
- Record encounter log
- If party wipe → return Failed
- Carry forward HP/resource state between encounters
- All encounters survived → calculate rewards (XP, gold, loot)
- Return RunResult with status, full log, and rewards
- For each encounter in quest:
ChaCha8Rngseeded fromblake3::hash(run_id + started_at)for deterministic replay
10. Loot Resolution (crates/game)
- Create
src/loot.rs:roll_loot(quest: &QuestDefinition, difficulty: Difficulty, encounters_completed: usize, rng: &mut impl Rng) -> Vec<GeneratedItem>:- Reference quest’s loot table
- Roll for each encounter’s loot drop (chance based on difficulty)
- Generate items via
item_generation::generate_item - Bonus chest for completing all encounters
- Loot tables defined in
data/loot_tables/TOML files:- Each table: list of
{ template_id, weight, rarity_weights: { common: 60, uncommon: 30, rare: 10 } }
- Each table: list of
Tests
Unit Tests
roll_d100: always returns 1-100 (fuzz with 10,000 rolls)calculate_hit_chance: 50% base when attacker/defender attributes equalcalculate_hit_chance: higher attacker stat → higher chancecalculate_hit_chance: clamped to 5-95 rangeis_critical: roll of 3 is critical, roll of 50 is notis_critical_fail: roll of 98 is critical failcalculate_damage: critical doubles damagecalculate_damage: armor reduces damagecalculate_damage: minimum 1 damageroll_initiative: lower speed → higher initiative value (slower)sort_by_initiative: sorts fastest firstapply_condition: adds condition to participantapply_condition: refreshes duration if already presenttick_conditions: poison deals damage each roundtick_conditions: removes expired conditionstick_conditions: stunned participant is flaggedevaluate_queue: returns first matching skillevaluate_queue: skips skills whose conditions aren’t metevaluate_queue: skips skills when resource cost can’t be paidevaluate_queue: returns None when nothing matchesexecute_skill: deducts resource costexecute_skill: applies conditions on hitexecute_skill: AoE hits all enemiesresolve_combat_encounter: party wins when all enemies dieresolve_combat_encounter: party wipe when all players dieresolve_combat_encounter: respects 50-round limitresolve_combat_encounter: initiative order is respectedresolve_combat_encounter: deterministic with same seed (run twice → identical log)resolve_trap: Perception check succeeds/fails correctlyresolve_rest_point: restores HP by 25%resolve_run: processes all encounters in sequenceresolve_run: stops on party wiperesolve_run: carries HP state between encountersroll_loot: higher difficulty → better rarity distributionroll_loot: deterministic with seeded RNG
Integration Tests
- (None — this is pure game logic with no DB or API. All tests are unit tests.)
- Full simulation test: create a party with known stats vs. known enemies, run simulation with fixed seed, assert exact sequence of events in the log