Skip to main content

Combat & Dungeon Screen — Design

Status: design proposal, not yet implemented (see Phases below). Last revised: 2026-05-07 (initial draft + same-day refinement pass).

This document specifies a unified combat surface for Valenar. It replaces the current CombatDetailModal drill-down with a first-class full-screen panel that serves both passive (Act 0 / ambient) combat and the future dungeon-raid gameplay loop.

Taxonomy alignment for this doc: it does not introduce a separate dungeon world model or a top-level Dungeon tab. A dungeon begins in Location context as a clue, Feature, or threat sign and becomes a Site once discovered or enterable. The combat screen is the dedicated active-combat or raid surface launched from that site context.

Channel shorthand note for this proposal: if this document says HP, read it as combat-facing shorthand for the current health pool only. The broader character model still separates current pools, max values, recovery channels, and resolved ratings or outputs.

The chronicle work shipped in the prior pass — combatHistoryStore, EventStack Open chip, CombatMarkers map overlay, Encounters sub-tab in the Objectives expansion — is kept and reused. This document is about the active combat screen that sits alongside the chronicle.

0. Design log (2026-05-07 refinement pass)

The initial draft proposed a panelMode: 'wide' split-screen layout with a streaming-per-round server contract. After design discussion the following calls were made:

  • Layout is a full-screen panel that overlays the map, not a wide split. Combat is an opt-in destination, so it gets its own dedicated surface.
  • Server contract simplifies — combat resolves in one tick, the full snapshot ships in one shot, the client animates at its own pace. No streaming required.
  • Two clocks are kept independent: world clock advances the simulation; animation clock paces the Watch view at human-readable speed regardless of game speed.
  • Death is deferred — Act 0 has no MC death. Loss = retreat with TBD penalty. The penalty model is a separate decision.
  • AI / configurable behavior is built on existing SECS primitives (template, modifier, scope). No new keyword. No DSL. Behaviors are templates, branches are templates, branch weights are modifier-stacked channels.
  • Sparring is a small diegetic drill against a Training Dummy archetype, separate from a future power-user practice harness.
  • Map navigation generalizes — markers and chips route to the appropriate panel + record via a small spatial-index dispatcher.

1. Why a real surface, not a modal

The shipped modal works for Act 0 because combat is server-resolved and instantaneous: the player gets a recap card, opens it, reads the rounds, closes it. That is a report. It is correct for what Act 0 is.

It is wrong for the rest of the game:

  • Dungeon raiding (future later-act play) needs a place where the MC visibly enters rooms, fights, loots, advances. A modal cannot host a multi-room flow without becoming its own pseudo-app.
  • Configurable inputs (retreat, eventually AI-driven targeting and ability use) need a persistent surface, not a one-shot popup.
  • Spectating auto-combat is more readable when the rounds are animated in space rather than listed as text.
  • Reviewing past combats belongs in a focused screen, not a modal that steals over everything else.

The combat screen is therefore designed as a tab + full-screen panel, not a popup overlay.

2. Two surfaces, one data store

  • Chronicle
    • Lives in: EventStack Open chip, CombatMarkers map icon, Encounters sub-tab
    • Purpose: Pulls a player's attention to combat that happened and lets them drill in
    • Status: Already shipped
  • Combat screen
    • Lives in: New Combat rail tab opening a full-screen panel
    • Purpose: Watch combat play out, review past combats, host dungeon raids
    • Status: This document

Click flow: player clicks an EventStack Open chip, a map combat marker, or an Encounters row → switches to the Combat tab and loads that record. The modal (CombatDetailModal) is retired in Phase 1.

3. Layout — full-screen panel

The combat tab is the first consumer of a new full-screen panel mode. Clicking the Combat rail tab opens a panel covering the entire client work area, hiding the map underneath the way every other tab's extended expansion already does, just maximally so. The rail itself stays visible so the player can hop to other tabs without an explicit close.

Closing is symmetric: click the Combat rail tab again, OR press Escape, OR click another rail tab.

This replaces the original panelMode: 'wide' proposal. Reasons:

  • The Watch view needs ~1400 px arena width to feel comfortable; the wide split gave ~600 px and crammed the round log
  • Phase 4 dungeon raids will host a loot panel + room navigation + boss banner inside this surface — they need the headroom
  • Combat is opt-in (the player chose to look), so taking the full screen is honest UX
  • Standard panel ergonomics (compact panel beside expansion) are wrong for this kind of focused view

The panel mode is added to shell/types.ts as a new value (e.g. 'fullscreen'). Naming is left to the panel-extension work in flight. Combat is the first consumer; future deep tabs (skill tree, encyclopedia) may be the second.

3.1 Three views inside the panel

The panel surfaces a master/detail/detail flow: List → Summary → Watch.

List view (default landing)

Wide table or card grid. Each row is one combat. Columns:

  • Date / tick
  • Location (clickable → map jumps there)
  • Opponent composition (with archetype icons)
  • Outcome (Won / Retreated, color-coded; no Lost in Act 0)
  • MC health-pool delta (HP shorthand in combat recap copy)
  • Loot summary
  • Round count

Sorted newest-first. Active combat appears as a pinned row at the top with a pulse indicator. Clicking an active row jumps directly to Watch; clicking a past row goes to Summary. Filtering is deferred — surface only when there are hundreds of records. Empty state: prescriptive copy.

Summary view

The drill-in. Header: opponent, location, date, outcome badge.

  • Channel tiles — rounds, damage dealt, damage taken, MC HP final, current health-pool final, loot value
  • Branches fired — counts of which behavior branches resolved per round (e.g. Attack ×8 / Cleave ×3 / Heal ×1). This is the AI feedback section; see §9
  • Loot — itemized rewards
  • Key events — curated highlights (crits, status applications, the retreat moment if any)
  • Watch button — opens View 3
  • Edit profile — shortcut to the AI configuration surface (Phase 3+)
  • For active combat: same shape, channels stream in from the snapshot, Watch auto-fires once on first open

Watch view — the SVG arena

The actual replay/spectate surface. Layout:

  • Top bar — opponent identity, location, "Round X of Y"
  • Center — SVG arena. Combatants as circles, attack lines flash on round ticks, damage numbers float and fade, status pops at edges
  • Right column — full-height round log. Each row shows the activity fired and which branch produced it (e.g. Round 4 — MC executed Cleave (branch: cleave_when_3+_enemies))
  • Below arena — replay controls: play/pause, speed (0.5× / 1× / 2× / 4×), scrub bar
  • Sidebar (optional, ~240 px) — MC live state during active combat: current pools such as health or HP, stamina, mana when relevant, equipped weapon, current target, status effects. In replay mode, scrub-synced
  • Bottom-right — Retreat button (Phase 3+, only during active combat)

Scrub is disabled during active combat (no peeking the future), enabled during replay.

4. Visualization tech — SVG

SVG, not Canvas2D, for the arena. Reasons:

  • Sparse scene (5–20 combatants, vs the map's thousands of voronoi polygons) — no performance reason to skip the DOM
  • Tooltips work natively via the existing data-tip-id registry
  • Hover/click per combatant is one event listener per element
  • CSS animations handle damage-number floats and ring pulses cleanly
  • DOM elements are screen-reader accessible
  • Round scrubbing is just toggling element states, not rebuilding scenes

The Canvas2D pattern in MapCanvas.tsx is correct for dense voronoi terrain. Arena combat is the opposite shape; the right tool is SVG.

For damage numbers and status pops, a small <span> with a CSS keyframe animation is enough — no library needed. If performance ever becomes a concern (it will not for this density), a requestAnimationFrame loop can replace CSS animations without changing the rendering layer.

5. State

The screen has one state field: loadedRecord, plus a derived isLive: boolean flag computed from whether the record matches the currently streaming combat.

loadedRecord = null → List view (no record open)
loadedRecord != null → Summary view (initial drill-in)
OR Watch view (after Watch click,
OR auto-played for live)
isLive = true → Watch auto-fires on open, scrub disabled,
Retreat button visible, animation tracks
the snapshot at the animation clock rate
isLive = false → Replay mode, full controls, no Retreat

There is no separate ACTIVE / RECAP state machine. Live combat is just a record with isLive = true; when combat ends, the same record flips to isLive = false. Same components render either way; the difference is one boolean.

6. Server contract

Combat resolves on the server in one tick. The server emits a single CombatResolutionSnapshot { rounds, locationId, ... } via SignalR. The client receives the snapshot and animates rounds at its own pace.

Required server change from current state:

  • Explicit locationId on the snapshot. Currently the client proxies the MC's location at notification-receive time. The server should carry the explicit locationId so non-MC combat (settlement raids, party combat, future co-op) pin to the right map location.

The original draft proposed streaming rounds individually. No longer needed — combat is one server tick; per-round streaming would be synthetic delay. The client animates the snapshot it already has.

7. Pacing — two clocks

Two clocks must stay independent:

ClockControlsPlayer intent
World clockTick rate (1× / 2× / 4× / 8× / paused)Strategic time
Animation clockWatch view round playback speedReading time

Combat resolves on the server regardless of world speed (one tick, instant). The Watch view animates at the animation clock, which the player controls separately via the speed picker. Default 1 s/round.

Auto-pacing for long combats:

  • Rounds 1–5: 1.0 s/round (full readability for the first damage moment)
  • Rounds 6–15: ~0.67 s/round
  • Rounds 16+: ~0.5 s/round
  • "Skip to end" always available
  • Manual scrub overrides auto-pacing

Edge cases:

  • 1-round combat: minimum playback duration ~1.5 s (animate, then a beat on the final state)
  • Background combats during fast-forward: server resolves instantly, EventStack chip + map marker fire, world keeps galloping — the animation clock never blocks the world clock
  • Player on the Watch view during world fast-forward: animation runs at player-chosen animation-clock speed, world keeps advancing in the background

8. Map → screen navigation

The map markers and EventStack chips already exist. Generalize the click handler into a small dispatcher:

marker / chip click → (panelId, recordId) tuple
→ open panelId
→ load recordId in that panel

Combat is the first consumer ({ panelId: 'combat', recordId: ... }). Same dispatcher handles future record types (raids, deaths, discoveries, festivals) without per-marker ad-hoc routing. The shell needs to grow this dispatcher in Phase 1; combat is what motivates building it.

9. AI architecture — built on existing SECS primitives

Combat is currently fully automated. Phase 3+ adds player-configurable AI for the MC (and, by extension, NPCs, enemies, settlement workers, future companions). The architecture must satisfy three constraints:

  1. Future-proof — must support player-edited rules, modder-shipped behaviors, archetype personalities, status-effect-driven shifts, and richer algorithms (planning, learned policies) without new primitives
  2. Game-agnostic — usable for combat AI, NPC AI, settlement decisions, event triggers; not Valenar-specific
  3. No new keyword, no DSL — must compose from existing SECS constructs (template, modifier, scope, event, system, formula)

9.1 Behaviors are templates

A "behavior" is a template that holds an ordered/named collection of branches. Each branch is itself a template instance with three fields:

  • weight — a channel with a base value plus modifier stack (the same modifier-stacked semantic that drives every other channel in SECS)
  • activity_ref — reference to an activity that fires when the branch wins
  • condition — a scope predicate determining whether the branch is eligible this evaluation

This composes from primitives that already exist. There is no behavior keyword, no branch keyword, no DSL. The exact template declaration syntax for these compositions follows existing SECS conventions (docs/design/02-templates.md for templates, docs/design/08-collections-and-propagation.md for branch collections).

9.2 Resolution

A resolver activity takes a behavior reference and current context, walks the behavior's branches, evaluates conditions, computes weights through the standard modifier resolution order, and returns the highest-weighted eligible branch's activity. The combat resolver invokes this activity on each decision point.

There is no special-case AI runtime — the resolver is itself an activity, running through the existing activity call path.

9.3 Why this is the right shape

  • Reuses the modifier system end-to-end. Branch weights are channels, so any source can stack a modifier onto a branch weight: a Bloodlust status raises attack-branch weights; a personality template raises certain decision weights; difficulty modes shift global weights. This is how Paradox-lineage scripting languages compose AI behavior, and SECS already has the machinery for it.
  • Modding is automatic. Behaviors are templates, so the existing override matrix (inject, replace, try inject, try replace, inject_or_create, replace_or_create — see docs/design/06-overrides-and-modding.md) applies unchanged. A mod can inject a new branch, replace a behavior wholesale, or modify branch weights externally.
  • Generalizes beyond combat. Same primitive serves NPC AI, settlement decisions, enemy archetypes, event triggers. The combat resolver is the first consumer, but the primitive is general.
  • No new compiler work. The compiler bring-up does not need a behavior phase; behaviors are templates and ride the existing template phase. The hand-written stand-in in Generated/ follows the same rule as every other definition.

9.4 Player-facing AI configuration (Phase 3+)

The player edits an ordered if-then list (FFXII-style gambits). The list is compiled into a player-owned behavior template instance:

  • Each rule becomes a branch with its weight stack reflecting rule priority (a high add modifier gated by the rule's condition)
  • The player's current profile points the MC's behavior_ref field at this generated template
  • Editing a rule recompiles the template; the engine's existing template-registration flow handles the update

This is a compilation target, not a DSL. The player UI is a list editor; the compiler emits standard template definitions; the engine sees regular templates. Mods can override generated profiles the same way they override any other template.

9.5 Weight stacks follow the Paradox shape

A branch's weight is computed as base + Σ adds, then × Π factors (or whatever order the existing SECS modifier resolution specifies in docs/design/05-expressions.md). Modifiers are gated by scope-bound conditions. This is the same shape Paradox uses for weight / ai_chance / ai_will_do blocks across CK3 / V3 / Stellaris / EU4 — designers and modders coming from that lineage will recognize it.

The key architectural property: branch weights and channel values use the same modifier resolution path. Designers do not learn two rules.

10. Sparring vs practice combat

Two distinct features that both run combat against synthetic opponents, distinguished by scope and audience:

  • Sparring (skeleton-friendly)
    • Where it lives: Camp facility (Sparring Post) or Combat-tab affordance
    • Opponent: Fixed TrainingDummy archetype (template)
    • Output: Quick result, optional flavor reward
    • Determinism: Random
    • Why: Diegetic flavor; useful as a Phase 1 dev vehicle
    • Phase: Phase 1–2
  • Practice combat (Phase 3+)
    • Where it lives: Inside the AI configuration surface
    • Opponent: Player-picked from encyclopedia, custom compositions
    • Output: Full diagnostics — branch firing counts, close-calls, comparable runs
    • Determinism: Optional fixed-seed
    • Why: Power-user behavior tuning
    • Phase: Phase 3+ (ships with AI configuration)

Sparring uses the same combat resolver, the same Watch view (with a Sparring badge), and isolates effects: no persistent HP loss, no loot, no XP, no chronicle entry. Practice combat extends the same isolation but adds custom opponent configuration and diagnostic output.

11. Death and retreat

Act 0: no MC death. Combat outcomes are Won or Retreated.

Retreat: the MC's behavior includes a retreat branch (in the default behavior, gated by an HP threshold). When that branch wins, MC disengages and the snapshot records the retreat. Penalty model is TBD — possibilities include HP debt, time-bank cost, durability hit, morale impact, or no penalty beyond loss of progress. This is a separate game-design decision that does not block screen design.

The Watch view renders a retreat the same way it renders any other outcome: animation runs to the retreat round, screen freezes on that state, Summary surfaces with a Retreated badge.

Later-act death: out of scope for this document. When death is added to the game, the screen needs a third outcome state (Lost) and the layout absorbs whatever consequence framework the design lands on (funeral, replacement MC, soul-link, etc.).

12. Phased plan

Each phase is independently shippable.

Phase 1 — Skeleton (1–2 sessions)

Replace the modal with the new Combat tab and a static final-frame arena. The behavior architecture is stubbed: MC has a behavior reference field, pointing at a hardcoded DefaultMelee behavior template (one or two branches), so the seam exists for Phase 3+ without committing AI work yet.

  • New Combat rail tab in MC_MODE_TABS (MCMode.tsx)
  • New full-screen panel mode in shell/types.ts
  • New CombatPanel.tsx hosting the three views
  • New CombatList.tsx, CombatSummary.tsx, CombatWatch.tsx
  • New CombatArena.tsx — SVG component rendering combatants in their final positions, no animation
  • Map navigation dispatcher in the shell
  • Reroute Open/click flows to the tab:
    • EventStack Open chip → tab
    • CombatMarkers map click → tab
    • Encounters sub-tab row click → tab
  • Retire CombatDetailModal
  • Define Behaviors.DefaultMelee template + Behaviors.TrainingDummy template in Content/, lower to Generated/, wire MC's behavior_ref field
  • Add locationId to the server-side combat snapshot

Phase 2 — Animation + sparring (1 session)

  • Animation loop walks record.payload.rounds at the animation clock rate
  • Speed controls 0.5× / 1× / 2× / 4×, scrubber bar
  • Damage-number CSS keyframe + attack-line flash on each round tick
  • HP ring updates in step
  • Sparring affordance — Camp facility or Combat-tab button — runs a combat against TrainingDummy, no real consequences

Phase 3 — Configurable AI + practice combat (multi-session)

  • Player-facing AI configuration surface (gambit-list editor)
  • Compiler from gambit list to behavior template instance
  • Profile save / load
  • Practice combat — pick opponents, optional fixed-seed, diagnostic output (branch firing counts, close-calls)
  • Retreat button activates in the Watch view during active combat

Phase 4 — Dungeon raid wrapper (multi-session)

The combat screen becomes the room of a dungeon. Adds:

  • Dungeon as a stack of rooms with their own encounter table + loot
  • Loot panel after each room (reuses Inventory move flow)
  • "Next room" / "Leave dungeon" affordances
  • Boss rooms + final loot tally
  • A clue, threat sign, or partial place record in a Location becomes a Site entry once dungeon access is discovered or enterable

Dungeon-specific data (room state, encounter generation, loot tables) is out of scope for this doc.

Phase 5+ — Stretch

  • Tactical interrupts (opportunistic player input during active combat)
  • Scenario library (named challenge fights, possibly with leaderboards)
  • Batch-run / Monte Carlo for practice combat
  • Behavior modding surface — modders ship .secs files with custom behaviors; players pick them in the configuration UI

13. Trade-offs

  • Phase 1 makes the modal redundant. All chronicle work (combatHistoryStore, EventStack Open chip, map markers, Encounters card click) keeps working unchanged; the modal is the only thing retired. ~70 lines of removable code.
  • Full-screen panel adds a new shell concept. The panel-extension work in flight will absorb this, but it is a real new mode. Naming
    • CSS plumbing is a Phase 1 deliverable.
  • SVG arena adds a new visualization layer to the codebase. Whoever maintains Valenar has to learn the pattern alongside the existing Canvas map. Contained to one component tree; not a systemic complexity bump.
  • Animation timing is fiddly. Will need iteration once real combat data shows up — the difference between "readable" and "sluggish" for round pacing is small but matters.
  • Behavior-as-template leans on existing primitives heavily. Branch templates, modifier-stacked weights, and scope-evaluated conditions all already exist; this is composition, not invention. Risk: existing template syntax may be verbose for collections of branches. Mitigation: same as for any other hand-written Generated/ file in Phase 1 — write it; iterate when the compiler bring-up reaches Phase 1's translation layer.
  • Player input scope creep. The retreat affordance + future AI-config surface will attract feature requests ("can MC throw a potion?", "can MC switch weapons mid-fight?"). Phase 1's hidden affordances and Phase 3's gambit-list constraint are deliberate hedges.

14. Open decisions remaining

Most decisions from the original draft are resolved (see Design log §0). The following remain:

  1. Retreat penalty model — see §11. Game-design call. Does not block Phase 1.
  2. Off-tab combat notification. When combat fires while the player is on a different tab: pulse the Combat rail icon? Just chip via EventStack? Default: rail-icon dot indicator + EventStack chip; player decides whether to look.
  3. Concurrent combat. MC fighting in the field while a settlement raid resolves. Two isLive=true records. Either the screen has a switcher, or the server serializes. Worth a call before Phase 3 when Retreat introduces real player input pressure.
  4. AI configuration depth (Phase 3). Three tiers of complexity: simple stance + retreat threshold; ability triggers with conditions; full priority list with multi-clause rules. Player-facing depth is a game-design call separate from the underlying behavior-template primitive (which can express any of them).
  5. AI configuration unlock timing. Act 0 has nothing meaningful to configure (one weapon, no abilities). When abilities and the gambit editor unlock together is an act-design call. Likely a later act when ability slots are introduced.
  6. Naming for the full-screen panel mode. 'fullscreen' / 'full' / 'workspace'. Whichever fits the panel-extension work in flight. Combat is the first consumer.

15. References

Code already in tree that this design builds on:

  • examples/valenar/Client/src/store/combatHistoryStore.ts — persistent combat records
  • examples/valenar/Client/src/components/notifications/CombatDetailModal.tsx — retired in Phase 1
  • examples/valenar/Client/src/components/shell/CombatMarkers.tsx — map overlay
  • examples/valenar/Client/src/components/shell/EventStack.tsx — Open chip
  • examples/valenar/Client/src/components/modes/missions/MissionsExpansion.tsx (EncounterCard) — chronicle row renderer
  • examples/valenar/Client/src/components/shell/types.ts — panel mode declarations; full-screen mode added in Phase 1
  • examples/valenar/Client/src/api/types.tsCombatResolutionSnapshot + CombatRoundSnapshot; locationId added in Phase 1

Engine + design references:

  • docs/design/02-templates.md — template declaration syntax (behaviors and branches are templates)
  • docs/design/05-expressions.md — modifier resolution order (branch weights inherit it)
  • docs/design/06-overrides-and-modding.md — override matrix that applies unchanged to behavior templates
  • docs/design/08-collections-and-propagation.mdScopedList<T> / ScopedDictionary<TKey, T>, the collection primitives behaviors use to hold branches
  • docs/design/04-behavior.md and docs/design/05-expressions.md — the live activity and saved-scope primitives that branches reference and the resolver invokes

16. What this document does NOT decide

  • Game-design aspects of combat — damage formulas, ability lists, enemy archetypes, loot tables. Belong in a combat-rules.md or similar
  • Dungeon structure — room generation, encounter pacing, dungeon rewards. Belong in a separate dungeon-design.md when Phase 4 becomes active work
  • Retreat penalty model — see §11
  • Audio / haptics — out of scope for the first three phases
  • Multiplayer / co-op — single-MC right now
  • Death model — Act 0 has no MC death; deferred to whenever death is added