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:
EventStackOpen chip,CombatMarkersmap icon, Encounters sub-tab - Purpose: Pulls a player's attention to combat that happened and lets them drill in
- Status: Already shipped
- Lives in:
- Combat screen
- Lives in: New
Combatrail tab opening a full-screen panel - Purpose: Watch combat play out, review past combats, host dungeon raids
- Status: This document
- Lives in: New
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-idregistry - 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
locationIdon the snapshot. Currently the client proxies the MC's location at notification-receive time. The server should carry the explicitlocationIdso 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:
| Clock | Controls | Player intent |
|---|---|---|
| World clock | Tick rate (1× / 2× / 4× / 8× / paused) | Strategic time |
| Animation clock | Watch view round playback speed | Reading 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:
- 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
- Game-agnostic — usable for combat AI, NPC AI, settlement decisions, event triggers; not Valenar-specific
- 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
Bloodluststatus 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— seedocs/design/06-overrides-and-modding.md) applies unchanged. A mod caninjecta new branch,replacea 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
behaviorphase; behaviors are templates and ride the existing template phase. The hand-written stand-in inGenerated/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
addmodifier gated by the rule's condition) - The player's current profile points the MC's
behavior_reffield 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
TrainingDummyarchetype (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
Combatrail tab inMC_MODE_TABS(MCMode.tsx) - New full-screen panel mode in
shell/types.ts - New
CombatPanel.tsxhosting 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:
EventStackOpen chip → tabCombatMarkersmap click → tab- Encounters sub-tab row click → tab
- Retire
CombatDetailModal - Define
Behaviors.DefaultMeleetemplate +Behaviors.TrainingDummytemplate inContent/, lower toGenerated/, wire MC'sbehavior_reffield - Add
locationIdto the server-side combat snapshot
Phase 2 — Animation + sparring (1 session)
- Animation loop walks
record.payload.roundsat 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
.secsfiles 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:
- Retreat penalty model — see §11. Game-design call. Does not block Phase 1.
- 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.
- Concurrent combat. MC fighting in the field while a settlement
raid resolves. Two
isLive=truerecords. Either the screen has a switcher, or the server serializes. Worth a call before Phase 3 when Retreat introduces real player input pressure. - 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).
- 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.
- 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 recordsexamples/valenar/Client/src/components/notifications/CombatDetailModal.tsx— retired in Phase 1examples/valenar/Client/src/components/shell/CombatMarkers.tsx— map overlayexamples/valenar/Client/src/components/shell/EventStack.tsx— Open chipexamples/valenar/Client/src/components/modes/missions/MissionsExpansion.tsx(EncounterCard) — chronicle row rendererexamples/valenar/Client/src/components/shell/types.ts— panel mode declarations; full-screen mode added in Phase 1examples/valenar/Client/src/api/types.ts—CombatResolutionSnapshot+CombatRoundSnapshot;locationIdadded 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 templatesdocs/design/08-collections-and-propagation.md—ScopedList<T>/ScopedDictionary<TKey, T>, the collection primitives behaviors use to hold branchesdocs/design/04-behavior.mdanddocs/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.mdor similar - Dungeon structure — room generation, encounter pacing, dungeon
rewards. Belong in a separate
dungeon-design.mdwhen 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