`!fight` seats the expedition's roster instead of the one player who typed
it. Seat 0 is the leader, always: the session row is theirs, the lock is
theirs, and `!flee`, the fork, and `!extract` stay their call.
A monster that wins initiative now swings before anyone speaks. The session
layer used to park every new fight on a player_turn, which is true of the
hardcoded solo order and a lie about a party's -- the enemy would forfeit
round 1 and nobody would notice. `startPartyCombatSession` rolls the order
and sets the phase from it; `handleFightCmd` settles the round before it
announces, so the opening block narrates the hit rather than quietly
showing its damage.
Members were invisible to two commands that had no business ignoring them:
`!cast` queued a spell for "next combat" while its caster was standing in
one, and `!rest` healed a seated member to full mid boss fight. Both now
resolve through the party.
Nobody leaves without an answer. A downed member's `!fight` opens the
party's fight and tells them why they are not in it. The leader's `!extract`
reaches everyone it drags out of the dungeon, and everyone rolls for what
moved into their house while they were gone.
Supplies burn at 50% x N x 4/5 -- a party eats more than one and less than
N. The ratio is exact: 0.8 as a float truncates a party of three to 119%,
a permanent tax nobody would have found.
Solo is untouched, byte for byte. One seat means one build, one INSERT, no
participant rows, the same RNG draws in the same order -- the combat
characterization golden does not move, and neither does the balance corpus.
Backtracking (revisit R2) breaks the assumption every zone-run caller
quietly relied on: that CurrentRoom == len(VisitedNodes)-1. Position and
progress have been the same number only because navigation was
forward-only.
Split them. CurrentRoom is now the first-entry index of CurrentNode in
VisitedNodes -- the room number the player was shown on the way in, and
the salt that enemy/trap/harvest/encounter keys hash. A revisited room
therefore resolves to the same room. RoomsTraversed is a new monotonic
step counter, persisted, backfilled from visited_nodes for in-flight rows.
The audit found only two progress-shaped reads. narrationCadence moves to
RoomsTraversed so a backtracking player draws fresh flavor rather than
replaying the entry-room lines. The other was a latent bug: zoneCmdGo
labelled the room it moved into as CurrentRoom+1, which is only correct at
the frontier; advanceZoneRunNode now returns the true path index.
VisitedNodes becomes an ordered set and RoomsCleared becomes idempotent --
both no-ops while navigation is forward-only, both load-bearing after R2.
No player-visible behavior change. Nothing to route off RoomsTraversed for
threat/SU: verified at HEAD that movement charges neither. Threat comes
from combat, supplies burn per day. The revisit plan's cost model claimed
otherwise and has been corrected -- R2's "discount" is really a net-new
cost decision.
Camp was a stationary "rest here until the next briefing" intent, but
nothing struck it when the autopilot / !zone advance / fork picks walked
the party into a new room. The stale CampState then blocked !camp <type>
with "already camped" even though the player was several rooms away.
autoBreakCampOnMove clears the camp (no rest bonuses — those only land
at briefing time via processOvernightCamp) whenever Camp.RoomIndex no
longer matches run.CurrentRoom. Wired into advanceOnceWithOpts (covers
advance/autopilot, complete/fork/next-room branches all surface a
"⛺ Camp struck" banner) and zoneCmdGo (fork commit).
Four fixes for the auto-walk loop that was re-clearing the same room
every 15 min while a fork was pending, ignoring the rest lockout, and
not counting expedition activity toward the daily streak:
- advanceOnceWithOpts / runAutopilotWalk now short-circuit to stopFork
when NodeChoices has a pending fork. Stops phantom kills + duplicate
loot drops + fork re-prompts on the same cleared room.
- fireExpeditionAutoRuns honors restingLockoutRemaining so the
background ticker no longer walks through a long rest.
- autoRunCooldown 15m -> 2h, autoRunTickInterval 5m -> 15m. Auto-walk
DMs are now a once-in-a-while ping, not a steady drip.
- markActedToday + HasActedToday recognise LastActionDate. Wired into
!rest short/long, !expedition start/abandon, !extract, foreground
!zone advance, and !zone go so DnD-side activity credits the streak
even when the expedition ends before midnight.
(cherry picked from commit 9e27fd8257a4c92150ad584b393bf5a72270b82c)
On an expedition the autopilot drives the walk, so the manual Elite/Boss
fight close-outs and per-room next-room lines pointed players at the wrong
surface (`!zone advance` instead of `!expedition run`). Route every
"keep moving" prompt through continueHint(), which picks the verb by mode.
Special-case the boss victory to read as the expedition win.
(cherry picked from commit 30b51b91445f3bb9680cd252df6c761e3ce61d0a)
Routes Elite/Boss rooms off the auto-resolve SimulateCombat path and onto
the persisted turn-based engine. !zone advance now stops at an Elite/Boss
doorway; the player engages with !fight, then resolves one full round per
!attack / !flee. A won CombatSession is the record that the room's combat
is done, so a fresh !zone advance clears the room and advances the graph.
- buildZoneCombatants: shared player/enemy Combatant builder extracted from
runZoneCombat; combatantsForSession rebuilds the pair from a session row.
- runCombatRound loops the phase state machine through a whole round;
finishCombatSession runs HP/XP/loot/kill/threat/mood close-out.
- getCombatSessionForEncounter lets the room resolver tell "already won"
apart from "not yet fought".
- !zone advance/enter/go blocked while a session is active.
- resolveBossRoom deleted (dead after the reroute).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Harvest tables (expedition + standalone) now keyed by node_id; legacy
room-idx entries auto-migrate via read-fallback + drop-on-save.
- Narration salts swapped from run.CurrentRoom to narrationCadence(run)
(= len(visited_nodes)-1) so flavor pickers survive G9 column drop.
- !zone map renders the graph (BFS by PosX/PosY) when the gate is on:
✓/▶/· status, ╳ for locked-only edges, secrets hidden until visited.
- Region-boundary hook in graph advance/!zone go updates expedition
CurrentRegion + visited list when from/to nodes differ — without
burning supplies or retiring runs (the graph IS the run state).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the graph types from G2/G3/G4 into the !zone advance flow.
Default off; with the gate on, room-clear branches on outgoing-edge
count instead of the linear current_room+1 walk.
zone_graph_nav.go — env gate, edge unlock evaluation
(LockNone/Perception/Key/LevelMin/RegionClear/StatCheck), pendingFork
JSON shape with encode/decode helpers, perception roll seeded
deterministically from (runID, from, to) so reload doesn't grant
retries, plus fork prompt rendering and the
recordRoomCleared/advanceZoneRunNode/completeRunAtNode persistence
trio.
dnd_zone_cmd_graph.go — advanceTransitionGraph drives the graph-mode
branch in zoneCmdAdvance: 0 outs → run completes (boss_defeated set
when current node IsBoss); 1 unlocked out → auto-advance and emit
the same teaser the legacy path uses; 2+ outs (or a single locked
out) → write pending fork to node_choices and render the menu. New
!zone go <n> / !zone choose <n> consumes the prompt, validates the
chosen edge is unlocked, advances, and emits the arrival teaser.
Old "go" alias for "enter" was unused in tests — repurposed.
dnd_zone_cmd.go — extracts formatNextRoomMessage so the legacy and
graph-mode paths share the post-advance teaser. Adds the new
subcommand to the dispatch switch and help text.
Tests: pure-logic coverage for unlock evaluation, perception
determinism, pendingFork roundtrip + decode-edge-cases, choice
resolution, fork prompt rendering with hint vs no-hint locked
options, edge ordering by (Weight, To), and the env gate. Existing
zone tests pass with and without the gate set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>