maybeAutoCamp / pitchAutopilotCamp / pitchBossSafetyCamp now take a now time.Time so the sim can inject a synthetic clock; tryAutoRun still passes time.Now().UTC(). SimRunner.RunExpedition advances simNow by autoRunCooldown per walk and runs the production camp scheduler after each soft stop (and pitchBossSafetyCamp on stopBossSafety), dwelling minAutoCampDwell + breakAutoCampIfDue so the next walk can proceed. Effect: HP-low mid-day rests, base-camp waypoints, Night-camp rollovers, and boss-safety holds all fire under the sim; D7-a's tickEventAnchoredRollover shortcut is retained on TickDay for tests and the pre-cutoff legacy path.
23 KiB
Long Expeditions — multi-session plan
Goal: dungeon expeditions become real multi-day journeys (T1 ≈ 2 days → T5 ≈ 7 days). Player picks camp supplies at launch, then watches it play out. Camp pitch/break, elite engagement, and boss engagement all become autonomous. Foreground
!camp/!fightsurvive as overrides, not the expected path.
1. Why the current shape is "too short"
| Tier | Zones | Rooms | Real-time exit |
|---|---|---|---|
| T1 | Goblin Warrens, Crypt Valdris | 6–7 | usually <1 calendar day |
| T2 | Forest Shadows, Sunken Temple | 7–8 | usually 1 day |
| T3 | Manor Blackspire, Underforge | 7–9 | 1–2 days |
| T4 | Underdark, Feywild Crossing | 8–10 (×regions) | 2–3 days |
| T5 | Dragon's Lair, Abyss Portal | 9–10 (×regions) | 2–4 days |
(Room counts from internal/plugin/dnd_zone.go:70-134; tier zones from memory project_expedition_difficulty.)
Because the autopilot walks ~3 rooms / 2h (expedition_autorun.go:43,53) and boss/elite stops are manual, a player who pays attention clears T1–T3 in an evening. Briefing/recap UTC ticks fire but rarely matter — the expedition is over before "day 2" ever lands.
2. Target shape
| Tier | Target duration | New room budget (single-region) | Notes |
|---|---|---|---|
| T1 | 2 days | 12–14 | one autopilot-camp |
| T2 | 3 days | 16–20 | two camps |
| T3 | 4 days | 22–26 | three camps; threat starts to bite |
| T4 | 5–6 days | 28–34 (split across 3–4 regions) | base camp emerges |
| T5 | 7 days | 36–44 (split across 3–4 regions) | full siege/temporal pressure |
(Numbers are first-draft anchors; class-balance sim drives the actual dial in D7.)
Duration is measured in expedition-days advanced by autopilot camp, not real-time UTC days. Each autopilot night-camp = day++. Real-time UTC still drives the morning briefing DM cadence, but day-count and supply burn become event-driven (camp = sleep = day++ = burn).
The autopilot decides:
- when to camp (and which camp type)
- whether to engage elites and bosses, or to camp first and retry
- whether to extract early on starvation/HP collapse
The player decides:
- supply pack purchases at launch (only meaningful pre-expedition choice)
- whether to override (
!camp <kind>,!fight,!expedition extract)
3. Phasing
Built across multiple sessions; each phase ships independently with tests and a small DM-side feedback loop.
D1 — Length + room-count rework
Files: dnd_zone.go (per-zone Min/MaxRooms), dnd_expedition_region.go (multi-region region sizes), dnd_zone_run.go:generateRoomSequence (still terminates Entry → … → Elite → Boss, but with more Explorations).
Work:
- Re-pitch Min/MaxRooms per zone to the table in §2.
- Multi-region zones: extend per-region room budgets so the total stays in band when summed.
- Keep the fixed Entry / Trap / Elite / Boss anchors; only Exploration count scales.
- Consider 2 traps + 2 elites for T4–T5 to break up the long middle (deferred decision — see §6).
- Update existing zone graphs in
zone_graph_*.gofor the new lengths. Auditing whether each zone has a hand-tuned graph or relies on the linear compiler is part of D1 scoping.
Exit criteria: expedition-sim shows mean-room-cleared per zone in the new band; no test fixtures hard-code old counts.
D2 — Autopilot camp scheduler
D2-a (shipped 2026-05-27): new expedition_autocamp.go with pure decideAutopilotCamp + pitchAutopilotCamp + dwell-window lifecycle (shouldSkipAutoRunForCamp, breakAutoCampIfDue). Wired into tryAutoRun: skip while inside minAutoCampDwell (4h), break + walk past it, post-walk scheduler pitches HP-low rest camps and region-boss base-camp waypoints. CampState gained AutoPitched so auto- vs player-pitched lifetimes diverge. Day rollover is still UTC-anchored; D2-b moves it event-anchored.
D2-b (shipped 2026-05-27): event-anchored day rollover. dnd_expedition_cycle.go adds eventAnchoredCutoff + isEventAnchored, splits the briefing body into nightRolloverBurn + nightRolloverDrift (+ a convenience processNightCamp that runs both back-to-back). For expeditions started ≥ cutoff, deliverBriefing skips mutators and posts a re-engagement DM; if last_briefing_at is older than nightSafetyNet (28h) it force-fires processNightCamp so a stalled autopilot doesn't freeze the expedition. The autopilot scheduler grew a Night flag — decideAutopilotCamp sets it when time.Since(LastBriefingAt) ≥ nightCampWindow (16h); pitchAutopilotCamp then runs the burn → camp → rest → drift sequence in one pitch. Manual !camp <type> runs the same flow when it's the first camp since the last rollover. Legacy UTC-anchored expeditions (start_date < cutoff) keep the original mutator flow via the same staged helpers, preserving rest-before-drift ordering through processOvernightCamp. Tests fence cutoff to year 9999 in TestMain so existing legacy assertions stand; new tests exercise the night decision, night-pitch rollover, event-anchored skip, and safety-net force.
Files: new expedition_autocamp.go; hooks in expedition_autorun.go:tryAutoRun; reuse dnd_expedition_camp.go:campPitch / applyCampRest.
Heuristics (in priority order):
- Day-budget pacing — pitch when rooms-walked-this-day ≥ tier-specific day-room target. Standard if room cleared, rough if not.
- Resource gates — pitch if HP < 35% of max, or supplies projected to cover <1 more day's burn (
exp.Supplies.Current < exp.Supplies.DailyBurn * harshMod). - Post-elite breath — auto-pitch a standard camp in the cleared elite room.
- Region boss cleared in multi-region zone — auto-pitch base camp at first eligible site.
- Don't pitch when mid-fight, in a trap/boss room, in a fork-pending state, or already camped.
Auto-break already happens on move (autoBreakCampOnMove in dnd_expedition_camp.go:401). Autopilot just needs to not break a freshly-pitched camp by immediately walking on — gate by time.Since(camp.EstablishedAt) > minCampDwell (15min real-time? or simply: don't auto-walk while camped).
Day-rollover semantics change (decided event-anchored): today, day++ happens at the 06:00 UTC briefing (dnd_expedition_cycle.go:236). After D2, the autopilot night-camp pitch is the event that fires day++, supply burn, overnight rest, and threat drift — in one atomic rollover the camp scheduler triggers. The 06:00 UTC briefing tick survives only as a re-engagement DM anchor (and a safety net: if for some reason no camp pitched in N hours, force one). Concretely:
- Move the body of
deliverBriefing(dnd_expedition_cycle.go:186-) into aprocessNightCamp(exp)helper called from the autopilot camp scheduler when it pitches a night camp (rough/standard/fortified — not mid-day breath stops). - Distinguish "night camp" (advances day) from "rest stop" (doesn't). Heuristic: a camp pitched at the end of the day-budget room count is night; one pitched purely for HP/SU recovery mid-day is a rest stop. Both apply
applyCampRest; only night also runs supply burn + day++ + threat drift. - The existing UTC briefing ticker becomes: if
last_briefing_atis older than ~20h and the user is active, post a "Day N — here's where you stand" re-engagement DM. It calls no mutators. - Manual
!camp <type>from the player still does what it does today (apply rest immediately) but also counts as a night camp if invoked with no other camp today — keep parity with the autopilot behavior so the override doesn't accidentally stall day advancement.
Migration: in-flight expeditions at deploy time keep their existing 06:00 UTC day++ until they end (fence by start_date). New expeditions use the event-anchored model.
Exit criteria: sim shows autopilot pitches the right kind of camp at the right room with no player input; supply economy still survives a 7-day T5.
D3 — Autonomous elite + boss engagement
Files: dnd_zone_cmd.go:497-535 (the prev==RoomElite / RoomBoss branch).
Shipped 2026-05-27. dnd_zone_cmd.go adds stopBossSafety + bossSafetyGate(uid, exp) (HP < 80%, supplies < daily burn, exhaustion ≥ 3 — the "active low status" interpretation). The elite/boss doorway branch drops the prev == RoomBoss || !compact carve-out: in compact mode bosses fall through to the same resolveCombatRoom path elites have used, after the gate clears. resolveCombatRoom selects monster + label + loot-drop by run.CurrentRoomType() so the same call site handles boss kills (zone.Boss bestiary, "👑 Boss — name down", boss-loot drop, elite-tier threat bump). Loss on auto-resolved boss falls through to the existing player-death narration / forceExtractExpeditionForRunLoss path — no special treatment.
The walk loop (dnd_expedition_cmd.go:717) only breaks at the elite/boss doorway when !compact; compact lets the next iteration auto-resolve. stopBossSafety is excluded from the rooms-walked tally and its autopilotFooter returns "" — res.final already carries the held-back line.
When the gate trips, tryAutoRun calls pitchBossSafetyCamp(exp) (new in expedition_autocamp.go): force-pitches Standard (or Rough fallback) regardless of decideAutopilotCamp's HP threshold or its RoomBoss room-type block, with the same Night event-anchored rollover handling. Dwell expires → next tick retries the boss. Foreground !fight and foreground !expedition run are unchanged: both still stop at the boss doorway for the player who wants to watch. Sim path is unaffected — stopBossSafety falls into expedition_sim.go's default branch (tick-a-day, retry next walk).
Open question: do we want a "set autopilot off" toggle for players who actively want to play the boss themselves? My take: default-on, single-flag per expedition (autopilot_engage) settable at start or via !expedition autopilot off. Defer until D3 lands and we see how it reads.
Exit criteria: sim runs a full T5 expedition launch → boss kill with zero player commands besides !expedition start.
D4 — DM volume + day surfaces
The big risk: a 7-day T5 with autopilot walking, camping, fighting elites, and killing bosses will spam DMs unless we bundle. The user has already pushed back on recap chatter (feedback_skip_recaps).
D4-a (shipped 2026-05-27): autopilot DM bundling. expedition_autorun.go:tryAutoRun now drops per-tick auto-walk DMs in compact mode. New surface rules:
- Fork / death / run-complete still DM the walk stream — interactive + climax beats.
- A Night=true camp pitch flushes the day as an end-of-day digest (
renderEndOfDayDigestin newexpedition_autorun_digest.go) followed by the camp block. - Boss-safety camp pitch gets a short "holding before the boss" header + camp block (walk stream dropped).
- Non-night auto-camp (mid-day rest / base-camp waypoint) surfaces the camp block by itself.
- Everything else — uneventful walks, preflight pauses, harvest interrupts — goes silent.
Each successful background walk now logs a
walkentry (appendExpeditionLog(... "walk" ...)) so the digest can count rooms walked from structured logs (the rawr.streamnarration is no longer persisted across ticks). Digest groups counts of walk/harvest/interrupt and inlines threat/milestone/narrative lines for the prior day (dayExpeditionLoghelper).maybeAutoCamp+pitchBossSafetyCampnow return theautoCampDecisionsotryAutoRuncan branch ondec.Night.
D4-b (shipped 2026-05-27): morning re-engagement DM anchored to user activity, not 06:00 UTC. fireExpeditionBriefings now skips event-anchored expeditions whose last_activity is older than today's 06:00 UTC threshold (and we're still inside nightSafetyNet — stalled-autopilot force-fires still win). New maybeDeliverDeferredBriefing runs at the top of AdventurePlugin.OnMessage on every inbound message in any room: it stamps last_activity (so the next ticker pass sees the player as present, even from non-bot chatter) and posts the deferred briefing if one is owed (CAS-gated by today's 06:00 threshold; pre-06:00 lazy fires are suppressed to avoid double-emit with the ticker sweep that follows). Day-1 inbounds don't trigger a briefing before the first night camp has happened.
Remaining work:
- TwinBee voice + no-recap copy pass on the digest once the shape settles.
Exit criteria: a 7-day T5 produces ≤ 10 DMs across the whole expedition (1 launch + 6 end-of-day + 1 boss + 1 run-complete + 1 emergency).
D5 — Supplies economics retune
D5-a (shipped 2026-05-27): per-tier pack caps. dnd_expedition_supplies.go retires the global SupplyPackStandardMax/SupplyPackDeluxeMax constants in favor of supplyPackCaps(tier) (std, dlx int) — T1/T2: (2,1); T3: (3,1) unchanged; T4: (5,1); T5: (7,2). SupplyPurchase.Validate is now Validate(tier ZoneTier); both call sites (!expedition start, !resume) pass the resolved zone's tier. The cap clears DailyBurn(raw) × intendedDays × 1.3 for every tier under the §2 target durations even with harsh×3 layered on top, so a player who wants to buy for the long shape can. DailyBurn / harsh-multiplier / phase5BDailyBurnRatePct are unchanged here — they're a D7 lever, alongside the sim work that should unblock empirical baselining. The help text now describes the cap as "scales by zone tier"; the holiday +1 standard pack still bypasses the cap on purpose (it's a freebie on top).
Note: the original "Empirical (sim-driven)" path was blocked: under D2-b event-anchored expeditions,
SimRunner.TickDaycallsdeliverBriefing→deliverBriefingEventAnchored, which readstime.Now().UTC()for its safety-net check, so synthetic ticks never advanceCurrentDayandDaysAtEndstays at 0. Re-baselining off real day-counts requires teaching the sim to drive the event-anchored rollover (D7).
D5-b (shipped 2026-05-27): "Pick your loadout" preset prompt at launch. !expedition start <zone> with no pack arg now DMs a Lean / Balanced / Heavy menu (sized per zone tier via new loadoutPurchase(tier, SupplyLoadout) in dnd_expedition_supplies.go); player replies with !expedition start <zone> lean|balanced|heavy (short forms l/b/h also work). !resume got the same treatment. Raw Ns Md syntax remains the advanced override — resolveLoadoutOrParse in dnd_expedition_cmd.go tries a single-token preset first, falls back to parseSupplyArgs. Heavy always equals supplyPackCaps (the D5-a harsh×3 ceiling); Lean covers intended days at raw burn; Balanced sits between. parseSupplyArgs("") still returns 1 standard for any tier — it's no longer reachable from !expedition start (the empty path is intercepted to prompt), but the 1s default is preserved so future callers don't get surprise zero-pack purchases. Help text updated; two existing scenario tests now pass lean explicitly, three new tests cover preset resolution + the prompt-vs-start guard.
D5-c (shipped 2026-05-27): Ranger forage wired against the new caps. Audit found SupplyForageMaxSU = 4 defined in dnd_expedition_supplies.go since Phase 12 E1b but never referenced — ForagedToday was only ever reset to false, never set to true, so the §4.2 "Ranger, WIS DC 12, 1d4 SU/day" perk had never actually granted supplies. New helper applyRangerForage(e, c, rng) rolls 1d4 SU once per day for Ranger characters, headroom-capped against Supplies.Max so a Heavy loadout doesn't overflow its purchased ceiling; non-Rangers and post-roll repeats are no-ops. Fires at the top of nightRolloverBurn (before the daily burn) so the +SU lands on today's bag and a "forage" log entry flows into the end-of-day digest via a new renderEndOfDayDigest case. No DC roll — accessibility over crunch (feedback_accessibility_over_dnd_crunch); the DC-12 gate would've added a fail case with no upside given the new headroom. Average +2.5 SU/day off a Ranger is a quiet Lean-loadout cushion (~1 extra day of T5 late-stage burn over a 7-day run), not a Heavy multiplier. Tests cover ranger-grants, non-ranger no-op, repeat-day no-op, headroom-cap, full-bag-still-stamps-flag, nil-safety.
Remaining work (D5-d+):
- DailyBurn /
phase5BDailyBurnRatePctrevisit once D7 sim measures real elapsed-day counts.
Exit criteria: balanced loadout completes intended-duration expedition in ≥ 85% of sim runs without starvation extraction.
D6 — Player-facing surface cleanup
D6 (shipped 2026-05-27): player-facing copy now matches the autopilot-first reality.
expeditionHelpTextrewritten around the loop "pick a zone → pick a loadout →!expedition runwatches it play out"; commands grouped into Run an expedition / Mid-expedition / Overrides with a one-line shape paragraph at the top.!campand!fightwere already absent from the primary!expeditionhelp; both are now explicitly listed under Overrides and framed as "autopilot covers these — only reach for them if you want manual control."campHelpTextitself opens with the same override framing so a player who types!camplands on the right mental model.expeditionCmdStatusImpl(default!expedition status) now leads with Day X / ~Y expected (driven by newexpeditionTargetDays(tier ZoneTier)indnd_expedition_supplies.go— T1=2 → T5=7, the §2 anchor table), adds a Rooms: N / Total in this region line under the region marker (sourced fromgetActiveZoneRun(...).CurrentRoom/TotalRooms), and appends a Recent: block with the last 3 log entries (summary fallback to flavor). Supplies and threat lines unchanged; the--debugview is untouched.- No new public command surface, no schema change, no test churn (no existing test asserted on the rewritten strings); full
go test ./...green.
D7 — Sim + class re-baseline
D7-a (shipped 2026-05-27): SimRunner.TickDay now drives event-anchored expeditions. expedition_sim.go short-circuits when isEventAnchored(exp) is true: instead of calling deliverBriefing (whose deliverBriefingEventAnchored branch reads time.Now().UTC() and would never trip the safety-net under synthetic ticks), it runs nightRolloverBurn → optional applyCampRest(Standard) if affordable (Rough fallback, skipped on low-SU) → nightRolloverDrift(briefAt). Mirrors pitchAutopilotCamp with Night=true. Production paths untouched. New expedition_sim_test.go covers (i) 3 ticks advancing day 1→4 with supply burn + stamped LastBriefingAt, and (ii) the low-SU branch where the rest is skipped but burn/drift still fire. Unblocks D5-d empirical re-baseline of phase5BDailyBurnRatePct and the class corpus re-run.
D7-b (shipped 2026-05-27): SimRunner.RunExpedition now drives the production camp scheduler under a synthetic clock (simWalkInterval = autoRunCooldown, 2h per walk). After every soft-stop walk it calls maybeAutoCamp(exp, simNow); stopBossSafety routes to pitchBossSafetyCamp(exp, simNow). The helpers (applyAutoCamp / applyAutoCampBossSafety) advance simNow past minAutoCampDwell (4h) and break the auto-pitched camp via breakAutoCampIfDue so the next walk proceeds. maybeAutoCamp, pitchAutopilotCamp, and pitchBossSafetyCamp gained a now time.Time parameter (the only prod caller, tryAutoRun, still passes time.Now().UTC()). Effect: HP-low mid-day rests, multi-region base-camp waypoints, Night-camp rollovers, and boss-safety holds all fire under the sim — the D7-a tickEventAnchoredRollover shortcut is no longer used by RunExpedition (kept on TickDay for tests + the pre-cutoff legacy path). Coverage in expedition_sim_test.go: Night-camp day advance, HP-low mid-day rest, boss-safety force-pitch, no-op when neither trigger fires.
Remaining work (D7-c+):
cmd/expedition-simextensions: multi-day mode CLI flag (-days/ per-day SU/HP/threat snapshots in SimResult).- Re-run the class win-rate corpus (cross-link
project_class_balance,project_j2_sim_artifact). - Re-check trailers (bard/cleric per
project_j1_post_sweep_findings) — autopilot camp pacing may relieve their HP-cliff issue. - D5-d retune of
DailyBurn/phase5BDailyBurnRatePctagainst measured day-counts. - New balance memory entry once corpus stabilizes.
4. Cross-cutting risks
- Existing in-flight expeditions at deploy time. Either fence by
start_date(old expeditions finish under old rules) or write a one-shot migration that retunes their room counts. Lean: fence — these are typically <2 days. - The 24h zone-run inactivity timeout (
dnd_zone_run.go:254) — must be raised, or the autopilot must self-pokelast_action_atper tick. With multi-day expeditions, a quiet stretch overnight is now expected. - Idle reaper (commit
0f72484, seeexpedition_autorun.goreferences) — already holds streak on autopilot; verify it tolerates 5-day expeditions. - Babysit perk —
BabysitSafeRestalready upgrades Standard → Fortified at rest (dnd_expedition_camp.go:241-245); confirm interaction with autopilot picking Standard (no change needed; the upgrade fires insideapplyCampRest). - Multi-region travel still unwired in autopilot (memory
project_multiregion_travel_unwired). For T4/T5 to actually take 5–7 days, autopilot must auto-advance regions on region-clear. D2/D3 covers this implicitly but call it out — likely a small dedicated commit. - Pet arrival (memory
project_pet_event_reachability) — emergence seam is at run-complete; longer expedition just delays the roll, no code change needed. - Twinbee voice + secret NPC buffs (memories
feedback_twinbee_voice,feedback_npc_buffs_are_secret) — none of the new copy can break these.
5. Schema impact
Likely none. Existing columns cover it:
current_day→ autopilot increments viaadvanceExpeditionDay.camp_json,supplies_json,region_state,threat_*already in place.- One possible new column:
expected_daysint (set at launch from zone tier + loadout) so status/end-of-day DMs can render "Day 3/5". Cheaper to derive than store — defer.
6. Open questions for next session
Day-advance semantics— decided 2026-05-27: event-anchored. day++ fires on autopilot camp-pitch (autopilot night-camp = sleep = day burn + briefing). UTC clock becomes a re-engagement-DM anchor only, not a state mutator. See §3-D2 for implementation impact.- Boss autopilot default: always-on, opt-out, or opt-in? My lean: always-on with a per-expedition opt-out flag; revisit after D3 reads.
- Per-tier room counts: the table in §2 is a starting point. Worth a sim pass before D1 commits — or fine to ship a guess and refine in D7?
- Extra anchor rooms: T4/T5 with one elite + one trap may feel thin spread over 30+ rooms. Add a second elite mid-zone, or rely on multi-region + patrols + threat for variety?
- Mobile/AFK friendliness: should the autopilot-DM cadence aim for "1 DM per real-time evening" regardless of room count, or "1 DM per in-game day" (which could be hours apart if the player set the pacing fast)?
7. Sequence
D1 → D2 → D3 (independent of D4/D5) → D5 → D4 → D6 → D7.
D2 and D3 can land in either order; D4 needs the volume problem to be real (so after D2/D3 at minimum). D5 wants D1's new lengths but can run in parallel with D2/D3. D7 always last.
Estimated sessions: 5–7. D2 and D5 are the biggest single-session chunks.