Mechanical `gofmt -w ./internal ./cmd`. Mostly struct-field realignment that
had drifted, plus a few trailing-newline fixes. No behaviour change — gofmt is
semantics-preserving, and build/vet/test are green either side.
Split out from the code-review fixes that follow so those stay reviewable
instead of hiding inside a wall of realignment.
!expedition invite / accept / decline / party / leave. The invitee buys
their own loadout and it pools -- a party is a shared burden, not a free
ride.
The plan said invites close "before the first walk". That is not a window,
it is a race: autoRunMinExpeditionAge leaves a fresh expedition alone for
thirty minutes and then the autopilot starts walking it, and the leader's
own !expedition run can beat it there. Thirty minutes is not enough to ask
a friend who is asleep.
So two changes to what the plan specified:
- The window is all of Day 1, not the first step. Supplies burn at the
night rollover, so a companion who arrives three rooms in pays and
receives exactly what one who arrived at the gate does.
- An unanswered invite pins the autopilot: loadExpeditionsForAutoRun skips
any expedition somebody has been asked to join. The leader must not be
dragged into a boss room while their friend reads the DM. Bounded by
expeditionInviteTTL (2h) in the query itself, so a forgotten invite
costs an afternoon, not the expedition.
New table expedition_invite. Absent == nobody was asked, which is true of
every expedition predating N3 -- nothing to backfill, same reading that
let expedition_party and roster_size ship without one.
Details worth keeping:
- Outstanding invites count against expeditionPartyMax. Otherwise a leader
asks four people and three accept.
- Pooling raises Current *and* Max. supplyDepletion reads the ratio, so
folding in only Current would read as the party suddenly starving.
- A member's supplies stay in the pool when they !leave. They were spent
on the expedition, not lent to it; clawing them back would let someone
starve the party on their way out.
- assertNotAdventuring guards expeditions and rosters but not bare zone
runs, so accept checks getActiveZoneRun itself -- startExpedition does.
- A party is not a taxi: zoneOpenToLevel gates the invitee on the same
tier rule !expedition start applies to the leader.
- releaseParty now clears invites too, or someone could accept onto a
corpse.
- expeditionCmdStatus and the bare `!expedition` switched to
activeExpeditionFor, and a member typing `!expedition go 2` is told the
leader picks the path instead of falling through to `start` and being
told "2" is an unknown zone.
Combat still seats one player -- handleFightCmd is P6c. go test ./...
green, golden byte-identical.
Every ownership lookup in the adventure module keys on a user id, and a
party member owns no row: not the expedition, not the zone run. P4 gave
them activeExpeditionFor; this gives them activeZoneRunFor, and gives the
DM seams the audience they never had.
- activeZoneRunFor(user) -> (run, isLeader, err). An owner's lookup is
exactly getActiveZoneRun, side effects and all -- in particular the
§4.3 idle reap, which force-extracts the wrapping expedition. A member
must never re-enter it, or glancing at the map would end the leader's
run. Pinned.
- expeditionAudience / fanOutExpeditionDM. Briefing, recap and digest all
DM'd id.UserID(e.UserID) alone. They now loop the roster, which
partyMemberIDs collapses to exactly the owner when there is none -- so
a solo expedition sends the same bytes to the same user it always has.
The briefing's body is expedition-scoped but its pet prefix is not:
each member has their own pet and their own sheet, so the roll rides a
per-reader decorator (the shape P5 settled on for combat narration).
The digest's A6 event anchor rolls per member for the same reason.
- releaseParty on every terminal transition. A seated member is barred
from adventuring elsewhere, so a roster outliving its expedition
strands the party. Deliberately NOT on 'extracting': that is a 7-day
resumable limbo and !resume must bring everyone back. The roster clears
when the window lapses to 'failed', which routes through
completeExpedition like the rest.
Rosters are still empty in production -- nothing seats a member yet -- so
every loop here has exactly one element and the whole change is a no-op
until P6b. Golden byte-identical, go test ./... green.
Suppress per-tick auto-walk DMs in compact mode. tryAutoRun now only
surfaces for fork / death / run-complete / boss-safety camp / Night-camp
pitch. A Night-camp pitch flushes the day as an EoD digest (counts of
walk/harvest/interrupt plus threat/milestone/narrative bullets, built
from dnd_expedition_log) followed by the camp block. Each successful
background walk writes a `walk` log entry so the digest can count rooms
without persisting raw stream narration. maybeAutoCamp and
pitchBossSafetyCamp now return the autoCampDecision so callers can
branch on dec.Night.
New expedition_autocamp.go: pure decideAutopilotCamp + pitchAutopilotCamp
+ dwell-window lifecycle. Wired into tryAutoRun so the background ticker
pitches a rest camp on low HP and a base-camp waypoint on region-boss
clear; auto-pitched camps last minAutoCampDwell (4h) before the next tick
breaks them and walks. CampState.AutoPitched separates auto- vs player-
pitched camp lifetimes so a player !camp stays sticky.
Day-rollover semantics unchanged — still UTC-anchored; D2-b moves
day++/burn/threat-drift onto the camp-pitch event.
!camp now applies long-rest effects (HP, spell slots, exhaustion,
threat -5, Underforge heat) immediately instead of waiting for the
06:00 UTC briefing. New CampState.RestApplied flag stops
processOvernightCamp from re-applying at briefing — it just strikes
the camp.
Also dials ambient cooldown 3h → 6h so a workday gives ~1–2 hits
instead of ~3.
Tedium-removal pass driven by live play feedback. Three big threads:
Expedition autopilot Phase 4 — background auto-run + harvest-until-dry:
- New expeditionAutoRunTicker walks active expeditions every 15min
(5min tick, per-expedition CAS on new last_autorun_at column). Skips
combat sessions, briefing/recap quiet windows, expeditions <30min old.
- Walks up to 3 rooms/tick with compact narration; suppresses DMs when
0 rooms walked or just hitting the per-tick cap (no "stretch complete"
filler). Player only hears from the bot when a real decision is needed.
- Compact mode: one-line combat narration for trash/elite, auto-resolves
elite doorways via the forward-sim engine (boss still pauses). Threaded
via new advanceOnceWithOpts → resolveRoom → resolveCombatRoom.
- Auto-harvest now grinds each Common/Uncommon node until dry (cap at 8
attempts/visit), mirroring manual !scavenge retries. Rare+ still pauses.
- Ambient ticker: anti-repeat by Kind via new last_ambient_kind column
(avoids two pack_rat DMs in a row when the pool only has 6 lines).
- !expedition go <n> now routes to the fork-choice handler when active +
numeric; fork footer rewritten to suggest !expedition go instead of
!zone go. Boss/Elite doorway: formatNextRoomMessage routes the action
hint by next room type and autopilot loop breaks via new nextRoomType
field so "Room X/Y — Boss" doesn't double-print with contradictory
hints (!zone advance vs !fight).
- runAutopilotWalk extracted from expeditionCmdRun so foreground and
background share the loop body.
Rogue Sneak Attack actually exists now:
- Audit found the rogue's "Sneak Attack" passive was AutoCritFirst +
5% damage rider. No Nd6, ever. Phase 2 Monte Carlo masked this
with a small flat buff; the class's defining mechanic never matched
its tooltip or 5e identity.
- Added SneakAttackDie int to CombatModifiers, per-hit Nd6 in
combat_primitives.go (same lane as DivineStrikePerHit). Scales with
level: 1d6 at L1-2 ... 4d6 at L7-8 ... capped at 10d6 at L19-20.
- AutoCritFirst + 5% rider retained as bonuses on top of working sneak
attack — preserves the opener-burst feel.
- L7 rogue expected per-hit ~8 → ~22 damage. Valdris fight math goes
from "16 rounds to kill, die in 8" to winnable.
TwinBee voice sweep (Phase B3):
- ~530 line changes across 24 files replacing third-person "TwinBee
notes/files/tracks/respects/..." constructions with first-person
inside flavor string literals. Code identifiers (TwinBeeLine,
twinBeeLine, etc.), chat-prefix labels (🎭 **TwinBee:**), item names
(TwinBee's Bell), achievements, and game-title references in
fun.go (Konami TwinBee ship) left intact.
- Catches a real bug: Valdris fight rendered "TwinBee marks the
Legendary Resistance" mid-combat in third person, contradicting
the Phase B2 convention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GM→DM rename across docs and code (GMNarrationType→DMNarrationType,
GMState→DMState, narration constants, comments) so the system reads as
"Dungeon Master" everywhere. Player-visible "GM mood" wording stays
where it appears in flavor.
Streamed zone/expedition combat: zone advance now stages patrol →
patrol play-by-play → patrol resolution → room intro → room play-by-play
→ final outcome through sendZoneCombatMessages with 2–3s pacing
(arena keeps its 5–8s window). Combat narrative lines pick up a compact
d20-vs-AC roll annotation for hit/crit/miss/block events.
Combat outcome polish: dndHPSnapshot lets narration show sheet HP
rather than legacy combat-engine HP, and markAdventureDead clears the
zombie state where hp_current was 0 but the legacy alive flag stayed
true after a D&D-layer KO.
Adv 2.0 announcement (ADVENTURE_2.0_ANNOUNCEMENT.md), README rewrite
covering the new layer, and adv2_scenario_test.go — a full
zone-run + expedition + harvest playthrough against a copy of the prod
DB asserting persisted state end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
High-priority fixes from the multi-agent audit of Adventure 2.0:
- Phase 11 nil-deref cluster: zoneOrFallback() helper replaces 8 unsafe
getZone(_) sites in dnd_zone_cmd.go. Corrupted zone IDs render a
placeholder instead of panicking.
- Briefing/recap idempotency: deliverBriefing/deliverRecap now claim
the rollover via a conditional UPDATE. Double-fires from clock skew
or restart become no-ops; supply burn and day++ no longer reapply.
- Graceful ticker shutdown: AdventurePlugin gains stopCh + Stop(); all
11 background tickers now select on stopCh in addition to ticker.C.
- Mood decay (§3.2): math.Round(elapsed*2) replaces int() truncation
so sub-hour gaps decay correctly.
- 24h auto-abandon (§4.3): getActiveZoneRun returns clean slate and
abandons stale runs whose LastActionAt is over 24h old.
- Respec / auto-migrate orphan cleanup: !respec and the auto-migrated
draft wipe now abandon active zone runs and expeditions before
deleting the dnd_character row.
- Phase R combat-link: applyBossDefeatThreat now wired from
resolveBossRoom (-20 threat); applyRoomCombatThreatForUser adds
+5/+8 from non-boss/elite kills (§8.1).
- Starvation → forced extraction: briefing-time check forces extract
with §10.2 coin tax when supplies hit zero.
- GMNat20/Nat1 narration wired into resolveCombatRoom and
resolveBossRoom (nat-20 takes precedence over nat-1).
- Treasure-undo race: LoadAndDelete on both timer-fire and `undo`
paths so only one side wins.
- Battle Master: Disarming, Menacing, Parry maneuvers added (3 → 6
of 10). Remaining 4 (Pushing, Goading, Riposte, Commander's Strike)
documented inline as needing ally/reaction mechanics the engine
doesn't model.
- Threat-70 warning: tracked in RegionState["siege_warning_fired"]
so a drop-and-recross doesn't re-fire the beat.
- region_state JSON decode error now logged via slog.Warn instead
of silently discarded.
Failing TestProdDB_DnDLayer fixed via option (a): track migrated
characters and only run round-trip / idempotency assertions on those,
skipping pre-existing prod-DB rows accumulated from live bot use.
New tests in dnd_audit_phase_R7_test.go cover: 24h auto-abandon,
briefing double-fire idempotency, threat-70 warning idempotency,
multi-region extract→resume state preservation, and starvation
forced-extraction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-zone resource registry (§3, all 10 zones) and full per-room harvest
layer wired into expeditions: !forage / !mine / !scavenge / !essence /
!commune / !resources. Auto-spawns a DungeonRun per region on
!expedition start, swaps it on !region travel, retires on
abandon/extract. Long rest at camp replenishes nodes across every
room and region. Reuses existing flavor.Harvest* / RichYield /
NodeDepleted / per-zone Harvest<Zone> pools.
Also clears two pre-existing test failures introduced by the E6c
flavor expansion (briefing substring list + combat-lift trial count).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Voluntary !extract burns 1 day, flips status to 'extracting', keeps
all rewards, resumable for 7 real days. Forced extraction (Abyss
collapse, future HP-0/supply-0 hooks) flips to 'abandoned' and applies
the §10.2 20% coin tax via the cycle layer's euro handle. !resume
within the 7-day window repurchases supplies and restores threat &
temporal stacks as-is; expired rows demote to failed.
getActiveExpedition / briefing / recap loaders narrow to status='active'
so 'extracting' rows don't get phantom day rollovers. Reuses
flavor.ExtractionVoluntary, ExtractionForced, ExpeditionResume from
twinbee_expedition_flavor.go.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Multi-region zones now default CurrentRegion to the entry region at
startExpedition; that region is also written to regions_visited so
the visited/cleared distinction is meaningful from Day 1.
MarkRegionVisited / MarkRegionBossDefeated expose the combat-link
hook surface. MarkRegionBossDefeated also flips Expedition.BossDefeated
when the cleared region's IsZoneBoss is set, since the zone-boss kill
is the same event from §7's POV (suppresses temporal accumulation,
kills further awareness pulses, etc.). setCurrentRegion is the
companion writer used by E4c's travel command.
!expedition status now shows the region line ("🗺 Region: Drow
Outpost (2/4)") with a ✓ marker when the region boss is down.
§7.3 heat stacks: +1/day in the Underforge (cap 10), persisted via
new updateTemporalStack helper. Band thresholds:
• 1–3 ambient (no narration)
• 4–6 warning band (combat fire damage applies, narration logged)
• 7–9 supply band (forces harsh-conditions burn = +50%, narration)
• 10 critical (exhaustion narration, log entry)
Fortified/base camp long rest in the Underforge drops heat by 2
(updates briefing's "Fortified rest" summary line), per §7.3.
Replaces the placeholder `TemporalStack > 0 → harsh` predicate in
deliverBriefing with zoneTemporalHarsh(), which is now per-zone aware
— Heat 1–3 in the Underforge is correctly flavor-only and no longer
prematurely doubles supply burn from Day 1.
Combat-side knobs (+1d4 fire round-start damage at heat 4–6, dis CON
saves at 7–9, -2 to all rolls at 10) are exposed via heat band but
not yet applied — combat-link phase reads heat band off the active
expedition.
Reuses flavor.UnderforgHeapWarning / UnderforgHeapCritical per
feedback_reuse_existing_flavor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>