Skip to content

feat: Battle System (Initiative, Combat, Resolution) (spec #22) #23

@Fortinbra

Description

@Fortinbra

Feature Spec: Battle System (Initiative, Combat, Resolution)

Field Value
Feature Battle System (Initiative, Combat, Resolution)
Issue #TBD
Status Draft
Author The Doctor
Date 2026-04-10

Overview

What it does

Defines a D&D-style combat loop including initiative, turn order, attack resolution, damage calculation, and combat end conditions. Integrates with the Enemy/Encounter system (spec #19) and Dice Engine (spec #16), while preserving Human DM authority to pause combat, adjust HP, skip turns, or end encounters early.

Why it's needed

Combat is a core pillar of gameplay. This spec provides deterministic, testable combat mechanics that can be executed by the bot while keeping DM control and auditability.

Out of scope

  • Spellcasting rules and spell slot mechanics (future spec)
  • Advanced action economy (bonus actions, reactions) beyond a single action per turn
  • Environmental hazards and complex terrain rules
  • AI-driven combat narration (handled by spec [SPEC] Narrative Engine (Ollama Integration) #13 where needed)

Architecture Notes (The Doctor)

Projects / layers touched

  • DungeonMaster.Core — combat entities, services, status effects
  • DungeonMaster.Infrastructure — EF Core mappings, repositories
  • DungeonMaster.Api — combat endpoints
  • DungeonMaster.Bot/combat command suite
  • DungeonMaster.Shared — DTOs for combat state

New interfaces in DungeonMaster.Core

  • ICombatService — combat lifecycle, turn execution, damage application
  • IInitiativeService — initiative rolls using IDiceEngine
  • IStatusEffectService — apply/expire status effects

Data flow

/combat start
  → Bot validates encounter exists + active
  → ICombatService.StartCombatAsync(encounterId)
  → IInitiativeService rolls initiative for PCs + enemies
  → CombatState persisted and returned
  → Bot posts turn order summary

Integration points with existing systems


Domain Model

Entities

CombatState

  • EncounterId (Guid FK)
  • Status (CombatStatus enum)
  • RoundNumber (int, starts at 1)
  • ActiveTurnIndex (int)
  • StartedAt (DateTimeOffset)
  • EndedAt (DateTimeOffset?)
  • InitiativeOrder (ICollection)

InitiativeEntry

  • CombatStateId (Guid FK)
  • CombatantId (Guid) — PlayerCharacterId or EncounterEnemyId
  • CombatantType (CombatantType enum)
  • InitiativeRoll (int)
  • OrderIndex (int)

CombatantState

  • CombatStateId (Guid FK)
  • CombatantId (Guid)
  • CombatantType (CombatantType enum)
  • CurrentHitPoints (int)
  • ArmorClass (int)
  • IsDefeated (bool)

StatusEffect

  • CombatantStateId (Guid FK)
  • Type (StatusEffectType enum)
  • AppliedAtRound (int)
  • DurationRounds (int)

Enums

CombatStatus

  • Active
  • Paused
  • Completed
  • Fled

CombatantType

  • PlayerCharacter
  • Enemy
  • Companion

StatusEffectType

  • Stunned
  • Poisoned
  • Prone
  • Bleeding
  • Charmed

Records

public sealed record CombatTurnResult(
    Guid CombatantId,
    bool Hit,
    int Damage,
    bool IsCritical,
    bool TargetDefeated);

Service Interfaces (CQRS)

public interface IInitiativeService
{
    Task<IReadOnlyList<InitiativeEntry>> RollInitiativeAsync(Guid encounterId, CancellationToken ct = default);
}

public interface ICombatService
{
    Task<CombatState> StartCombatAsync(Guid encounterId, CancellationToken ct = default);
    Task<CombatState?> GetCombatStateAsync(Guid encounterId, CancellationToken ct = default);
    Task<CombatTurnResult> ExecutePlayerAttackAsync(Guid encounterId, Guid playerCharacterId, Guid targetEncounterEnemyId, CancellationToken ct = default);
    Task<CombatTurnResult> ExecuteEnemyAttackAsync(Guid encounterId, Guid encounterEnemyId, Guid targetPlayerCharacterId, CancellationToken ct = default);
    Task AdvanceTurnAsync(Guid encounterId, CancellationToken ct = default);
    Task ApplyStatusEffectAsync(Guid encounterId, Guid combatantId, StatusEffectType effect, int durationRounds, CancellationToken ct = default);
    Task AdjustHitPointsAsync(Guid encounterId, Guid combatantId, int delta, CancellationToken ct = default);
    Task EndCombatAsync(Guid encounterId, CombatStatus status, CancellationToken ct = default);
}

API Contract (Rory)

Endpoints

  • POST /api/encounters/{id}/combat/start — start combat, returns CombatState
  • GET /api/encounters/{id}/combat — get current combat state
  • POST /api/encounters/{id}/combat/player-attack — resolve a player attack
  • POST /api/encounters/{id}/combat/enemy-attack — resolve an enemy attack
  • POST /api/encounters/{id}/combat/advance — advance turn (DM only)
  • POST /api/encounters/{id}/combat/adjust-hp — DM override HP changes
  • POST /api/encounters/{id}/combat/end — end combat (Completed/Fled)

Notes

  • Only one active CombatState per encounter.
  • Attack endpoints validate turn order; DM override bypasses turn checks.

Discord Commands (Rory — Bot Layer)

  • /combat start — starts combat for the active encounter
  • /combat status — shows turn order and active combatant
  • /combat attack {target} — player attack command (requires active turn)
  • /combat skip-turn — DM-only command
  • /combat set-hp {target} {delta} — DM-only override
  • /combat end — DM-only end combat

Test Scenarios (Danny) ⚠️ COMPLETE BEFORE IMPLEMENTATION

Happy path

  1. Given an active encounter with two PCs and two enemies
    When /combat start executes
    Then initiative is rolled, turn order is persisted, and combat status is Active

  2. Given a player’s turn
    When they execute /combat attack
    Then a d20 roll determines hit/miss and damage is applied

Edge cases

  1. Given two combatants tie on initiative
    When combat starts
    Then ties are resolved deterministically (higher Dexterity, then stable ordering)

  2. Given a combatant is stunned
    When their turn begins
    Then the system skips their action and advances the turn

Error / failure cases

  1. Given a player tries to attack out of turn
    When /combat attack runs
    Then the bot rejects the action with an error message

  2. Given the encounter already ended
    When /combat start runs
    Then the API returns 409 Conflict


Acceptance Criteria

Functional

  • Initiative order is rolled using IDiceEngine and persisted
  • Combat turn order advances deterministically and enforces active turn rules
  • Attack rolls apply d20 + modifiers and determine hit/miss
  • Damage rolls reduce HP and mark defeated combatants
  • Status effects apply and expire by duration
  • Human DM can pause, skip turns, adjust HP, and end combat early

Non-functional


Dependencies


Agent Work Breakdown

Agent Task Depends On
The Doctor Approve spec
Danny Write failing tests from Test Scenarios Spec approved
Rory Implement combat domain + services Danny's tests
Rory Add API endpoints + Bot commands Core entities
Danny Confirm all tests pass All implementation

Definition of Done

  • Test Scenarios section completed and approved by The Doctor before implementation
  • All failing tests written by Danny before implementation (TDD)
  • All tests written and passing (xUnit + bot command tests)
  • Code reviewed and approved by The Doctor
  • Combat state persisted and queryable via API
  • DM override commands verified
  • GitHub issue closed and linked to merged PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestspecSpecification work

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions