Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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)

2. Combat Types (crates/types)

  • Create src/combat.rs:
    • CombatState: tracks all participants, HP, resources, active conditions, round number
    • Participant: id, name, is_player, hp, max_hp, resources, attributes, armor, evasion, skill_queue, active_conditions, initiative_roll
    • CombatAction: 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 round
    • EncounterLog: list of rounds + outcome for one encounter
    • RunLog: list of encounter logs + final status
    • EncounterOutcome: Victory, Survived, PartyWipe, Fled
    • RunStatus: Completed, Failed
    • StatusEffect enum: 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-100
    • calculate_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
    • 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):
        1. For each participant in initiative order:
          • If dead/stunned, skip
          • Evaluate skill queue → get skill
          • Select target
          • Execute skill
          • Log action
        2. Tick conditions on all participants
        3. Check end conditions:
          • All enemies dead → Victory
          • All players dead → PartyWipe
          • Round limit reached → draw (treated as Victory with reduced rewards)
      • 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 builder
    • resolve_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
    • ChaCha8Rng seeded from blake3::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 } }

Tests

Unit Tests

  • roll_d100: always returns 1-100 (fuzz with 10,000 rolls)
  • calculate_hit_chance: 50% base when attacker/defender attributes equal
  • calculate_hit_chance: higher attacker stat → higher chance
  • calculate_hit_chance: clamped to 5-95 range
  • is_critical: roll of 3 is critical, roll of 50 is not
  • is_critical_fail: roll of 98 is critical fail
  • calculate_damage: critical doubles damage
  • calculate_damage: armor reduces damage
  • calculate_damage: minimum 1 damage
  • roll_initiative: lower speed → higher initiative value (slower)
  • sort_by_initiative: sorts fastest first
  • apply_condition: adds condition to participant
  • apply_condition: refreshes duration if already present
  • tick_conditions: poison deals damage each round
  • tick_conditions: removes expired conditions
  • tick_conditions: stunned participant is flagged
  • evaluate_queue: returns first matching skill
  • evaluate_queue: skips skills whose conditions aren’t met
  • evaluate_queue: skips skills when resource cost can’t be paid
  • evaluate_queue: returns None when nothing matches
  • execute_skill: deducts resource cost
  • execute_skill: applies conditions on hit
  • execute_skill: AoE hits all enemies
  • resolve_combat_encounter: party wins when all enemies die
  • resolve_combat_encounter: party wipe when all players die
  • resolve_combat_encounter: respects 50-round limit
  • resolve_combat_encounter: initiative order is respected
  • resolve_combat_encounter: deterministic with same seed (run twice → identical log)
  • resolve_trap: Perception check succeeds/fails correctly
  • resolve_rest_point: restores HP by 25%
  • resolve_run: processes all encounters in sequence
  • resolve_run: stops on party wipe
  • resolve_run: carries HP state between encounters
  • roll_loot: higher difficulty → better rarity distribution
  • roll_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