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.
Closes D1. Both T5 zones follow the D1-a/b/c/d pattern: extend the
graph so the longest entry→boss walk lands in band, keep the zone's
authored topology intact. Also backfills the missing per-node
RegionID authoring against dnd_expedition_region.go — the D1-d
deferral note flagged this as a prerequisite for T5.
dragons_lair (T5): 12 → 47 nodes, longest 38 (band 36-44)
abyss_portal (T5): 13 → 51 nodes, longest 39 (band 36-44)
Topology preserved per zone:
- Dragon's Lair: 4-region authoring (every node carries a valid
RegionID); binary-converging fork1 (ash_bridge TRAP / treasure_vault
Perception 15) still converges at the R3 wyrmlings_nest elite;
capstone fork2 still 3-way (LockNone / CHA 16 / Perception 17
SECRET LootBias 2.5) with each spur preserving its own named
capstone node. New R4 infernax_doors MERGE consolidates the long
final hall instead of triplicating it.
- Abyss Portal: 4-region authoring; three sequential forks preserved
(binary / binary / ternary) — fork1 Perception 16, fork2 CON 16
(full STR/DEX/CON/INT/WIS/CHA roster still covered), fork3 capstone
3-way with reality_seam SECRET LootBias 3.0. New R4 belaxath_doors
MERGE consolidates the final tear-approach.
MinRooms/MaxRooms re-pitched 9-10 → 36-44 so the dice fallback also
lands in band for any future graphless variant.
Both T4 zones follow the D1-a/b/c pattern: extend the graph so the
longest entry→boss walk lands in band, keep the zone's authored
topology intact.
underdark (T4): 10 → 46 nodes, longest 30 (band 28-34)
feywild_crossing (T4): 9 → 53 nodes, longest 30 (band 28-34)
Topology preserved per zone:
- Underdark: 4-region authoring intact (every node carries a valid
RegionID); 3-way fork1 (drow R2 / illithid R3 / deep_chasm R1)
with LockNone / LockPerception / LockStatCheck CON; deep_chasm
intentionally stays R1; all three arms converge at the R4
throne_approach merge. Adds the missing Trap anchor (Collapsed
Arch) in the R1 preamble.
- Feywild Crossing: woven forks intact — hag_circle still has two
incoming edges (one per first-stage path), time_eddy stays
grove-exclusive, illusion_garden stays marsh-exclusive, fork1
remains CHA-vs-Perception with no LockNone option. Adds the
missing Trap anchor (Cursed Thicket) and a fae_court MERGE so
the three second-stage endings converge into one final boss
approach instead of triplicating it.
MinRooms/MaxRooms re-pitched 8-10 → 28-34 so the dice fallback also
lands in band for any future graphless variant.
T5 zones (dragons_lair, abyss_portal) deferred to D1-e — those
need a RegionID backfill on top of the length extension since their
regionsByZone entries are not currently honored by their graphs.
Both T3 zones follow the D1-a/b pattern: extend the graph so the longest
entry→boss walk lands in band, keep the zone's authored topology intact.
manor_blackspire (T3): 11 → 35 nodes, longest 23 (band 22-26)
underforge (T3): 10 → 31 nodes, longest 23 (band 22-26)
Topology preserved per zone:
- Manor: two-stacked-3-way forks identity + full lock-kind coverage
(LockNone, LockPerception, LockStatCheck, LockLevelMin). Adds the
missing Trap anchor (Cursed Threshold). All six spokes are 3 mid-nodes
so route choice is loot/encounter character, not shortcut economics.
- Underforge: one-way-descent gauntlet identity preserved — 17-node
linear preamble through the existing Cooling River (TRAP) and Magma
Chamber (ELITE), single 3-way antechamber fork before boss. All three
antechamber spokes are 4 mid-nodes.
MinRooms/MaxRooms re-pitched 7-9 → 22-26 so the dice fallback also lands
in band for any future graphless variant.
Three more zones follow the goblin_warrens pattern from D1-a: add the
missing Trap anchor and deepen the graph so the longest entry→boss walk
lands in its tier's target band.
crypt_valdris (T1): 8 → 17 nodes, longest 13 (band 12–14)
forest_shadows (T2): 9 → 19 nodes, longest 16 (band 16–20)
sunken_temple (T2): 10 → 26 nodes, longest 16 (band 16–20)
Topology preserved per zone:
- Crypt diamond + Perception-DC-15 secret off side_chapel.
- Forest asymmetric main fork (long branch carries the elite) +
WIS-DC-14 secret pre-boss.
- Sunken Temple sequential forks with no mid-path merge (four leaves
all terminate at the single boss room).
MinRooms/MaxRooms re-pitched to the §2 bands so the dice fallback also
lands in band for any future graphless zone.
Length is now sourced from the zone graph's longest entry→boss path,
not the dice roll, so "Room X/Y" matches what the player walks. The
dice fallback stays for graphless zones.
Goblin Warrens grows to the T1 12–14 band (13-node traversal): adds the
missing Trap anchor and deepens both fork branches + the post-merge
approach. Pattern reference for the remaining zones in D1-b…d.
!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.
midnightReset gated shame DMs on LastActionDate + a fallback busy check
for active expedition / combat session. Players running on background
autopilot got shamed at midnight: dnd_zone_cmd.go gates markActedToday
on !compact (to keep autopilot out of streak credit), so LastActionDate
stayed stale; and if the autopilot's expedition completed / extracted /
got force-extracted before midnight, the busy check also missed it
(getActiveExpedition only matches status='active').
Switch the reaper to loadAdvDailyActivity — the same oracle the daily
report uses, which unions adventure_activity_log + dnd_zone_run +
dnd_expedition_log. Three explicit branches:
- engaged (LastActionDate today/yesterday or legacy counters bumped)
→ streak bumps, LastActionDate restamped
- activity-without-tap (autopilot beats, expedition log, or live
active expedition/combat as safety net) → hold: no bump, no shame,
no decay
- truly idle → shame DM + streak halve
The old busy branch bumped streak; new behavior holds. Aligns autopilot
days with mid-fight-at-midnight days: both engaged earlier but typed
nothing today, neither pumps the streak.
Tests cover all three branches + the death-window early-return.
Spiritual Weapon used to ride the pet-attack channel, so a petless
cleric saw "🐾 Your faithful companion" each round and couldn't tell
the spell was firing. Split it to SpiritWeaponProc/Dmg with its own
✨ flavor; damage now scales with spell mod + upcast.
Rest also fired mid-dungeon — only the autorun honored RestingUntil,
the !rest commands themselves had no gate. Block both short and long
rest when an expedition or combat session is active.
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).
These three stateful enemy-effect cases emitted their flavor pool via bare
pickRand, leaking the literal %d placeholder to players ("-%d damage on
every strike from here"). The magnitude is carried in CombatEvent.Damage,
exactly like the already-correct max_hp_drain / spell_fizzle cases; wrap
them in fmt.Sprintf. Add a regression test asserting no placeholder leaks.
resolvePendingInteraction deletes the pending entry before dispatch, but
resolvePetName then re-ran LoadAndDelete to recover the carried PetType.
That second lookup always missed, so the if !ok branch returned nil: the
save never ran and no pet was ever persisted (adoption was 100% broken).
Pass the already-loaded interaction in, matching every other handler.
Add a regression test driving the real dispatcher path.
Synthetic sim chars are vanilla base-class (no pet, no subclass), so the
per-round pet attack/deflect/whiff path was never exercised in the sim.
Add -pet-level N (1-10) which stamps a base Massive Dog (no armor) onto the
AdventureCharacter via the normal save path before the run, so combat's
DerivePlayerStats sees HasPet()==true. 0 (default) stays petless.
Measured lift (n=40, 10 classes x L3/7/12 x 3 zones): overall clear-rate
38.3% petless -> 47.8% at pet L10 (+9.4pp); biggest gains go to the caster
trailers (bard +13.3, warlock +11.9, cleric +11.4), narrowing class spread.
- forex_crypto.go: cap the CoinGecko response with io.LimitReader(64KiB);
build the simple/price query via url.Values instead of bare Sprintf.
- dnd_expedition_camp.go: a combat-session lookup error now fails open
(allow camp) + logs a warning, instead of blocking standard camp with a
misleading empty 'not cleared' rejection.
The run-complete emergence seam rolled the pet-arrival DM synchronously,
then handed the run narration to streamFlow's paced (fire-and-forget)
streamer. The 'animal in your house' prompt landed ahead of the queued
'Run complete' + loot beats. Add streamFlowThen to run the roll after the
final message is delivered; fix the same inverted order in tryAutoRun.
- Camp: campLocationCheck rejects only on live combat; no-encounter and
post-kill rooms are campable (kills the misleading "clear it first").
- Fork: markActedToday moved after the pending-fork early-return so spamming
!zone advance at a fork no longer keeps the daily streak alive.
- Pet whiff/deflect: single proc spent on the first multiattack swing only
(was applied to every swing); also dedups the per-swing event spam.
- Autopilot: background region crossing now runs an HP/SU preflight and
pauses if low, instead of burning a transit day while the player is idle.
- Legacy streak: acted-branch restamps LastActionDate=today so a purely-legacy
actor's streak no longer resets to 1 every night.
midnightReset runs at 00:00 UTC closing out yesterday, but gated idle
shame on HasActedToday(), which compares LastActionDate to today (the
new day). DnD-side players credit the day via LastActionDate only, so
anyone who played yesterday and extracted before midnight read as idle
and had their streak halved. Credit activity on the closing day
(LastActionDate==yesterday) directly, matching the streak-bump branch.
(cherry picked from commit f6cb4889e7e6bfacce41ea40853456b08d6857a9)
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)
Camp rest rules:
• Standard camp now rejects an uncleared room with a clear message
instead of silently downgrading to rough — let the player make the
call rather than spending supplies on a worse rest they didn't pick.
• campLocationCheck treats a room with a resolved (non-active) combat
session as cleared, so the natural "just killed, not yet advanced"
pause still qualifies for a standard camp.
• Reword fortified-camp gating + babysit help to "zone boss defeated"
(cache sites aren't a thing yet) so the requirement isn't misleading.
Map: renderVisitedPath adds a 1-indexed breadcrumb of visited rooms to
!expedition map, so players can reference rooms by index (e.g. !revisit 2).
(cherry picked from commit a38fc77eed888e9790c7a7cff24369b98910b43e)
The live turn engine only struck once per fight and never rolled pet
deflect or whiff, so pet armor (deflect-only) bought nothing in real
runs. Roll pet attack each player turn and roll deflect/whiff per enemy
turn, mirroring the auto-resolve engine; retire the one-shot pet-proc
machinery (rollCombatSessionPetProc / PetProcReady).
(cherry picked from commit a0e41c97801e500efad13c7e9a06be4c345e464e)
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)
Mirror runAutopilotWalk's multi-region auto-advance in the headless sim:
on a mid-zone stopComplete (active multi-region exp with a next region),
advanceToNextRegion and keep simulating instead of scoring a premature
"cleared". A transit error there is now recorded as halted, not cleared.
Also fix misleading run-complete wording: a non-boss region clear of a
multi-region zone now reads "Cleared {region}. The way to {next} opens
ahead." instead of "Cleared {zone}. Run complete." New midZoneRegionClear
helper shares finalizeExpeditionOnZoneClear's gating; the path is shared
with manual !region travel, so both autopilot and manual play get it.
(cherry picked from commit 5d2bba70849a0a3fdeac285cc55ea9b8fadea29c)
Extract regionCmdTravel's transit core into advanceToNextRegion (shared
by manual !region travel), then have runAutopilotWalk auto-advance into
the next region on a mid-zone stopComplete instead of dead-stopping the
walk. Transit cost (day + supplies + wandering check) is identical on
both paths.
(cherry picked from commit 8ac09cf3b059a086c431859c8aaaf046aa992342)
Recovers the genuinely-missing half of ade0335 (orphaned on the unmerged
phase-H-harvest-charges branch). 3ed2e1d redid pet *arrival* via the
emergence seam and explicitly deferred the morning event; this closes
that gap.
- deliverBriefing now rolls the 25% morning pet event (petMorningEvent,
already present but only wired into the overworld DM that underground
players never see) and prepends it to the briefing body, granting the
one-day PetMorningDefense buff.
- Add resetAllPetMorningDefense + wire it into midnightReset. The buff was
leaking permanently even on HEAD's existing overworld path — it was set
on the 25% roll but never cleared. Resets the pet_flags_json key in place.
Deliberately omits ade0335's ArrivalPending machinery: arrival is now the
emergence seam's job (3ed2e1d + 978dc5e + 513cf32), so queuing arrival
from the briefing would double-roll it.
finalizeExpeditionOnZoneClear and its wiring into the run-complete seam
were introduced in 73b7809 but that commit never made it into the
current history — it's not an ancestor of HEAD, so the function, its
call site in advanceOnceWithOpts, and its tests were all absent.
Symptom: clearing a zone (boss down, no outgoing edges) set the run's
boss_defeated flag but never flipped the wrapping expedition to
'complete' or retired the run. The expedition sat 'active' pointing at a
boss_defeated run, so getActiveZoneRun (filters boss_defeated=0) returned
nil and the next '!expedition run' errored with 'No active zone run'. The
ambient ticker also kept DMing about a finished dungeon.
Re-apply the bridge verbatim against current HEAD (deps verified present:
IsMultiRegionZone / CurrentRegion / MarkRegionBossDefeated /
completeExpedition / retireAllRegionRuns / AwardCompletionMilestones) and
restore the test.
The emergence-seam roll added in 3ed2e1d covered extract, abandon,
forced extraction, and death-respawn — but not a natural run-complete
(boss down / dead-end node), which is the most common successful
emergence. Players who cleared a run cleanly never got the arrival roll
despite meeting every condition.
Wire maybeRollPetArrivalOnEmerge into the two real run-complete callers
(expeditionCmdRun foreground + the autorun background ticker), gated on
reason == stopComplete. Kept out of runAutopilotWalk itself so the sim
path (which calls the walk directly) never fires arrival DMs.
The analysis path silently normalized junk tokens (BTC/USD, XYZ) to the
default EUR/JPY/CAD set, giving no signal the input was rejected. Validate
tokens up front: crypto symbols get nudged toward the conversion path,
anything else returns an unknown-currency error.
The pet-arrival roll only ran in sendMorningDMs (the 08:00 overworld
morning DM), which is skipped for anyone underground. Expedition players
are almost never in the overworld at 08:00, so the roll never reached
them — the encounter never fired despite the conditions being met.
Move the roll onto the emergence seam via a shared helper
maybeRollPetArrivalOnEmerge, called when a player surfaces alive
(voluntary extract, abandon, survived forced extraction) and on respawn
for underground deaths. Remove the now-dead morning-DM arrival roll.
Story-wise: while the player was out, an animal wandered into the empty
house looking for food.
The 25% morning pet event still rolls only in the overworld DM and has
the same reachability gap; left for a follow-up.
Wire a curated set of crypto assets (BTC, ETH, SOL, XRP, DOGE, ADA, LTC)
into the !fx conversion path only. fxLivePairRate now resolves each side
to units-per-USD and crosses them, so USD/fiat/crypto mix uniformly; fiat
sides still batch into one Frankfurter call, crypto hits CoinGecko's
keyless simple/price with a 60s cache.
Analysis (rate/report/setalert) stays fiat-only on purpose -- a daily
snapshot buy-signal/52w range is meaningless for 24/7 crypto.
resolveMagicEquipReply silently sold the previously equipped curio back
to inventory at full value, but the success DM only named the new item.
Players had to !sheet or !inv to confirm the old one wasn't lost.
Capture the prior occupant's name before the delete and append a
"📦 **<name>** moved back to inventory." line to the equip confirmation
when a swap actually happened.
H3 retired the manual !forage/!mine/!scavenge/!fish/!essence/!commune
verbs from the command dispatcher in e05da91; the underlying handlers
were left parked behind an operator-gated 2-week soak. The expedition
sim has since exercised the auto-harvest path 15k+ times per sweep
across every class, level, and zone — vastly more coverage than the
soak would have collected in production. Pull the dead handlers now.
Removed:
- handleHarvestCmd (dnd_expedition_harvest.go, ~175 lines)
- handleStandaloneHarvest (dnd_zone_harvest.go, ~80 lines)
- dnd_zone_harvest_test.go (three tests against the deleted handler;
the autopilot's standalone-storage path is what runs in prod and is
covered by the cmd/expedition-sim sweep + adv2 scenario test)
The deprecation DM in adventure.go ("Harvest is automatic now — just
walk.") stays for now — muscle memory takes longer than two weeks to
fade and the redirect costs 7 lines.
adv2 scenario test rewired to drive autoHarvestRoom directly.
53 resources affected in dnd_resource_registry.go; Common (mostly already
2), VeryRare, and Legendary untouched. Calibration sim (15k rows, 10
classes × L3/7/12 × 5 zones) lands cleared-run yield deltas at −2.5% to
+4.7% vs the J2b baseline — well within the ±10% DoD band, but with much
less leverage than the plan anticipated.
The finding (sim_results/h4_findings.md): per-room yield is gated by
non-Noise interrupt truncation, not charge count. The Common consolation
bracket (delta ≥ -4 yields +1) already absorbs most of the Josie nerf.
Future yield tuning should reach for resolveCombatInterrupt, not charges
— parked as out-of-scope for this branch.
H4 closes; the phase-H branch can now soak H1/H2/H3/H5 + this bump.
Removes !forage / !mine / !scavenge / !fish / !essence / !commune from
the command surface. Typing one now returns a one-line deprecation DM
pointing at the auto-harvest flow ("Harvest is automatic now — just
walk."). The internal handleHarvestCmd / handleStandaloneHarvest paths
are left in place; operator deletes after soak per plan §H3.
Help text and the supplies-low autopilot pause line no longer mention
the manual verbs.
autoResolveCombat now calls maybeShortRest after a won fight (HP < 60%
or any slots used, charges > 0), mirroring what a competent prod player
does between rooms. Re-baseline at n=100 across the full 10×3×5 matrix
written to baseline_h5sim_all10.jsonl.
Headline (vs baseline_j2a_v2_all10.jsonl, full delta in h5sim_findings.md):
mage L12 manor +8pp, cleric L12 manor +7pp, but bard L12 manor stays at
1pp and bard/cleric L12 underdark still 0. J2c (defaultKnownSpells
damage option at L3+ for bard, L4+ for cleric) is still the lever for
the trailers. Four borderline -9pp regressions on n=100 cells; flagged
for an n=300 re-run before declaring a sim defect.
handleDnDShortRest early-returned when HPCurrent >= HPMax, which dates
from when short rest was HP-only. After H5 (1cd53eb) added the partial
slot refresh, that gate left casters who hadn't been hit unable to
recover slots between fights — the most common slot-attrition scenario.
Loosen the gate: short rest is allowed when HP can heal OR any spell
slot has used > 0. New helper casterHasUsedSlots gates the slot branch.
Martial behavior unchanged (still bails at full HP). Two new tests.
The post-J1 sweep had the casters clustered at 19–22% L12 clear, vs
martials at 70–80%. A per-round trace across 240 boss-room fights showed
why: autoResolveCombat dispatched !attack only — zero spell_casts, zero
mid-fight consumable uses across every caster class. The entire "caster
cliff" was the sim measuring a strawman where casters couldn't cast.
J2a teaches the sim's autoResolveCombat to mirror a competent prod
player: heal at low HP if an inventory consumable is available, otherwise
cast the highest-EV damage spell (slot or cantrip), otherwise swing.
BuildCharacter now seeds the known-spell list via ensureSpellsForCharacter
so the synthetic spellbook is populated. A -trace flag on the cmd
attaches the raw CombatEvent stream to the last combat of each run for
post-hoc diagnostics.
A first re-baseline (n=100, all 10 classes) showed Ranger regressed
-35.8pp — the picker was burning L3 slots on lightning_arrow when
Ranger's weapon chassis (Hunter's Mark + Extra Attack) was the better
play. Added simMartialFirstClass to gate the picker off for Ranger and
Paladin (whose default kit is also weapon-first / no damage spells).
J2c experimented with widening the picker to control + heal spells;
heal-spell preempt cost druid 10pp (slot heals are 10HP vs 40HP
consumables) and control-spell scoring at 22 cost warlock 6.6pp. Both
reverted. Corpora retained under baseline_j2c*.jsonl for the post-mortem
in sim_results/j2b_findings.md.
Post-J2 L12 leaderboard (baseline_j2a_v2_all10.jsonl, n=100):
fighter 80.0, ranger 80.0, paladin 78.4, rogue 76.8,
druid 61.6, mage 53.4, sorcerer 50.6, warlock 48.2,
bard 40.4, cleric 39.0.
The caster cluster is dissolved; martials are within ±5pp of J1 (sweep
noise). Bard/cleric still trail, but it's no longer a sim artifact —
their defaultKnownSpells damage rosters cap at L2 and the picker can't
pick spells they don't have. That's a prod-level fix, deferred.
J3 trace (sim_results/j3_findings.md): T5 dragons_lair walls every solo
class at 0% (Infernax 546 HP vs solo player HP 110–175; ~25% boss HP
eaten before TPK across all classes). Per the J3 plan menu, this is
party-shaped content the engine doesn't yet have parties for. Surface
a TwinBee-voiced heads-up in handleDnDExpeditionCmd's start path and a
matching tag in !expedition list — players see "raid-shaped — solo
runs not yet survivable" before they spend outfitting coin. No combat
or class balance changes.
Files: cmd/expedition-sim/main.go +trace flag; expedition_sim.go picker
+ SimCombatSummary.Events + spellbook seed; dnd_expedition_cmd.go
raidContentWarning + list tag. All baselines + traces + findings
checked in under sim_results/.
The class-identity audit (98ba416) wired Extra Attack via the new
resolvePlayerSwings helper, but only SimulateCombat (auto-resolve)
called it. The turn-based engine — every !fight/!attack and every
elite/boss gate the sim drives via autoResolveCombat — still called
single-swing resolvePlayerAttack, so Fighter L11+ got 1 swing/turn at
the gates instead of 3. The audit close-out was correct in spirit but
half-applied.
J1 baseline matrix surfaced it: Fighter L12 cleared 100% of T2 forest
but 2% of T3 manor and 7% of T4 underdark, with %boss_reached at 100%
across the board. The wall was the boss-room damage exchange, not
mid-zone attrition. Trace dump on a sample fight: Fighter dealt 79
dmg in 14 rounds (7 hits / 9 swings) — exactly one swing per round —
versus 167 enemy dmg. With multi-swing wired in, the same fight ends
in 7 rounds with the boss dead, Fighter at 87/168 HP, 16 hits in 19
swings.
n=100 matrix after the fix:
Fighter L12 manor: 2% → 100% clr
Fighter L12 underdark: 7% → 98% clr
Fighter L12 forest: 94% → 100% (no leader regression)
Mage cells unchanged (J2 territory). Rogue cells within noise.
Sim infra changes that landed alongside (needed to read the J1
signal):
* expedition_sim auto-arms class-default defensive abilities
(Second Wind / Healing Word) via the new simAutoArmEnabled toggle
+ trySimAutoArm helper, hooked before applyArmedAbility in both
combat builders. Production code paths untouched (toggle stays
off). Without this the sim simulated a player who never types
!arm, which under-counts class survival.
* SimResult.Combats captures per-fight turn-log summaries (rounds,
hits/misses, damage by side, AC values inferred from RollAgainst)
so future J-phase questions can dig into the engine without
re-running the matrix.
* sim_results/run_matrix.sh fans the matrix across (class,level,zone)
cells via xargs -P (one process per cell — each owns its global
sqlite handle). ~6× wall-clock speedup on a 14-core box; n=100
matrix runs in ~3min.
* sim_results/summarize.sh gains p50_yld_clr + %boss_reached columns
so future sweeps don't conflate "reaches boss" with "clears zone".
Baselines:
sim_results/baseline_j0_n100.jsonl — pre-fix (1350 rows)
sim_results/baseline_j1_extra_attack.jsonl — post-fix (4500 rows)
Phase J state: J0 baseline locked, J1 done. T5 dragons_lair still
0% clear universally (J3). Mage T2+ wall still real (J2).
Harden expedition-sim with the three layers a real player carries:
race mods (+1 Human across the board), tier-appropriate equipment
(weapon/armor/helmet/boots/tool promoted from tier 0 to a level-mapped
tier), and a lean consumables bundle (2 heals + 1 buff at T2-T4,
3 heals + 2 buffs at T5). Capture material yields in SimResult so
calibration sweeps actually read out the H4 signal.
Three sweeps in sim_results/ (n=10): bare baseline, +gear/race,
+lean consumables. Headline: yield is gated by combat survival, not
charge counts. Fighter and Mage hit a sharp wall at T3+ even kitted;
no class extracts the T5 dragons_lair boss. Tuning charges before
fixing class survival would over-tune against a strawman, so H4
is now flagged blocked on Phase J.
Phase J appended to gogobee_harvest_charges_plan.md:
J0 baseline at n=30, J1 Fighter T3+ wall, J2 Mage T2+ wall,
J3 T5 boss (sim artifact vs over-tuned vs raid content),
J4 validation matrix.
RunExpedition now drives expeditions end-to-end through the production
plugin paths:
- TickDay calls deliverRecap + deliverBriefing with a synthetic clock
anchored on exp.StartDate, advancing one day per call without
real-time waits.
- Autopilot bursts use compact mode so elite gates auto-resolve inline.
- Boss / surviving elite gates are auto-resolved via handleFightCmd +
looped handleAttackCmd until the session flips Won/Lost/Fled.
- Forks resolve to path 1 deterministically.
- SimResult exposes StopCode, Walks, DayTicks, Threat, plus the full
dnd_expedition_log projected as SimLogEntry rows.
SimRunner re-uses production plugin code paths against a fresh sqlite DB —
SendDM no-ops without a Matrix client, so ground truth comes from the DB.
BuildCharacter persists adv+dnd character rows with class-appropriate
ability scores and spell slots; RunExpedition loops !expedition run with
stall detection until the autopilot halts or the expedition closes.
The cmd binary is a thin orchestrator: one flag-driven run, JSON output.
Per-room structured logging, day-cycle fast-forward, and matrix batching
are next session.
Short rest now restores all L1 slots plus floor(level/4) additional slots
at the next-available tier ≥L2, lowest-first. Long rest still does a full
wipe; martials and full-pool casters see no DM change.
Refactor autoHarvestRoom to accept a nillable Expedition and pull harvest
state from per-run storage when none is active. Wire the pass into
advanceOnceWithOpts so foreground !zone advance and the expedition
autopilot share the same Josie-semantics harvest loop. Standalone (no
expedition) skips threat/interrupts/log/region conditions; expedition
mode keeps the existing interrupt + threat behavior.
Replace retry-to-success grind with one-roll-per-charge. Every node
swing — Common through Legendary — consumes a charge regardless of
outcome. Rarity no longer pauses the autopilot.
Removed: autoHarvestPerNodeAttempts cap, isRarePlus filter, RarePending
field, renderRarePendingFooter, stopRareNode reason. Pluralize "fails"
since whiffs are now expected.
Per gogobee_harvest_charges_plan.md H1.
Run-loss paths (turn-based elite/boss death + flee, exploration combat
death/retreat, interrupt/patrol death) abandoned the zone run but left
the wrapping expedition row at status='active', so the ambient ticker
kept DMing about a dungeon the player had walked away from.
Adds forceExtractExpeditionForRunLoss helper and wires it into every
run-loss site; ambient ticker also skips when the expedition's run is
no longer active as a safety net.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 Monte Carlo tuning had papered over four classes whose 5e-defining
mechanics never actually existed in the engine — only flat DamageBonus or
FlatDmgStart compensation riders that hit the right aggregate win rate.
This pass replaces the proxies with the real primitives.
New CombatModifiers fields: ExtraAttacks (additional swings/round, looped
in a new resolvePlayerSwings helper); HuntersMarkDie (per-hit Nd6 bonus,
same path as Sneak Attack); ThornLashDmg (flat counter on landed enemy
hits, fires in resolveEnemyAttack after ward/block).
Fighter L5/11/20 Extra Attack, Ranger L5 Extra Attack + Hunter's Mark
(1→4 d6 by level), Paladin L5 Extra Attack + per-hit Divine Smite
(replaces one-shot opener), Druid thorn lash. Bard College of Valor L7
"Extra Attack" subclass tier converted from +1 attack/+10% damage proxy
to the real primitive.
Post-fix T5 class-balance: martials cluster at top (Fighter/Rogue 0.90,
Ranger 0.89, Paladin 0.88), casters keep their relative ordering, spread
14pp top-to-bottom.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
P0 — actual bugs:
- eldritch_blast: SRD-imported "Whatever" placeholder leaked to players;
overlay added in dnd_spells_data.go.
- Battle Master disarming_attack + parry: math.Max on multiplicative
DamageReduct was a no-op vs the 1.0 default. Switched to *= and added
regression tests covering the stacking case too.
- Extended TestSpellDescriptionsAreJargonFree to catch "Whatever",
trailing ellipses, mid-word truncation, and empty-size phrases. This
surfaced 4 more genuinely-broken SRD descriptions (find_familiar,
clairvoyance, glyph_of_warding, teleportation_circle); all now
overlayed in dnd_spells_data.go.
P1 — UX hygiene:
- Setup completion now nudges new casters toward !spells/!cast.
- Magic-item attunement vocab unified to "bond" everywhere user-facing;
renamed the Treasures sheet section to avoid clashing with the bond
vocab.
- Ambient DM footers stripped of "Threat +N" / "Supplies −N SU" hidden-
meter leakage; verb-form footers instead.
- !expedition status defaults to a days-left + threat-label summary;
raw SU / threat-out-of-100 / zone-stack / roll-modifier moved behind
`!expedition status --debug`.
- !zone taunt/compliment help text dropped the "(mood −10)" / "(mood +5)"
numerics that turned the hidden TwinBee-mood mechanic into a min/max
knob.
- TwinBee pronoun pass: switched she/her to singular they on the two
surfaces I touched plus achievements.bj_beat_twinbee.
P1 — correctness/scale:
- adventure_shop curio purchase now guards the euro.Debit bool return
before inventory grant + pot cut; the GetBalance preflight isn't
enough since a concurrent blackjack/lottery debit can slip the balance
across BLACKJACK_DEBT_LIMIT between read and write.
- equipMagicItem reordered: remove inventory item BEFORE equip, with a
best-effort rollback if equip fails. The prior order left a dup
window if the inventory delete tripped on a transient DB error.
- magicItemsByRarityCache promoted to sync.Once — concurrent cold-start
loot rolls otherwise raced on the lazy assignment.
Stale-audit verifications (no code change needed):
- light / druidcraft / water_walk overlays already exist and win the
SRD merge — the broken raw text never reaches players.
- bootstrapPhase5BHPRefresh is already wired at adventure.go:228.
Deferred:
- combatantsForSession per-turn 6-load DB chatter (perf P1-A): touches
the hot combat path; needs its own test pass before shipping. Tracked
in memory at project_combat_session_cache_deferred.
go vet clean; go test -race ./... green (229s).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The May audit's Phase A1 was overstated. Re-verification at HEAD
found:
- TestSpellDescriptionsAreJargonFree already exists with the
proposed regex set and passes.
- .Upcast is dead text (grep across internal/ shows no runtime
reader); audit's Upcast scope was moot.
- All four Description leaks cited by example (bless, command,
create_or_destroy_water, cure_wounds) were already overlaid in
the prior 2026-05 voice pass.
Real residual: 12 SRD-only utility/control entries whose Open5e
prose was bland (but not jargon-leaking). Overlay shims added in
the charm_person voice — second-person, verb-led, lightly wry.
Mechanics (Effect/CastTime/SaveStat/Concentration/AOE/
MaterialCost) preserved from the SRD originals.
Also closes out B1 (help jargon — already shipped) and B2
(TwinBee voice — shipped in Phase B2 voice pass) in the audit
doc; both were re-asserting completed work.
Remaining in the May audit: C1 backup wiring, C2+C3 maintenance
hardening, D1+D2 hygiene. No player-facing P0 left.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 5-B's exit named two follow-ups: T3 below band (manor 39 /
underforge 47 vs 55-75) and the T4 spread (feywild 59 vs underdark
88, 30pp asymmetry). Phase 5-C re-runs the Phase 4-A trace on the
four affected zones under the shipped Phase 5-B cell and applies
roster knobs only (IsElite + SpawnWeight; no bestiary stat-block
touches).
TestExpeditionBalance_Phase5C_OutlierDiagnostic names the killer
per zone:
manor_blackspire (53%): Vampire Spawn (72% win, 42 kills) +
Revenant (16% win, 31 kills at SW=1, can't go lower). First
attempt trimmed VS SW 3 -> 2 and backfired by concentrating
elite share on Revenant (kills jumped to 48); reverted.
underforge (49.5%): Fire Elemental (57 kills, 61hp/win) +
Salamander (33 kills) carry the lethality; Helmed Horror only
100%-win elite but SW=1, no dilution effect.
feywild_crossing (54%): only 2 elites in the pool; Fomorian
(50% win, SW=1) still pulls 25% of elite picks and owns 65
kills.
underdark (86%): Drow at SW=7 fills nearly half of standard
rolls at 100% win / 1.1hp loss -- free-HP filler.
Roster changes:
manor Banshee promoted to elite SW=2 (was standard SW=3,
99.6% win). Soft 4th elite slot dilutes Revenant share
~14% -> ~11%. Standards collapse to Shadow+Poltergeist
(>=99% win).
underforge Helmed Horror SW 1 -> 3. Three-way elite pool drops
Fire Elemental's share from ~44% to ~33%.
feywild Green Hag promoted to elite SW=2 (was standard SW=4).
Adds soft 3rd elite + removes a 16hp/win standard.
Standards become Redcap+Will-o-Wisp+Quickling.
underdark Drow SW 7 -> 5 (light trim per the user's "lift
trailers, don't nerf leaders" stance). Standards
shift toward Hook Horror / Drow Mage.
Phase 1 matrix after Phase 5-C (200 trials, Fighter @ centerline):
T1 88.5% spread 1.0 (in band 70-90)
T2 74.5% spread 15.0 (in band 62-82)
T3 56.7% spread 0.5 (in band 55-75) +13.5pp
T4 77.0% spread 13.0 (over band 45-65, matches 5-B target;
spread halved from 25.5)
T5 58.0% spread 40.0 (abyss_portal 38% is the residual)
T3 mean lifted +13.5pp; both zones inside band with 0.5pp residual
spread. T4 spread halved (25.5 -> 13.0pp) by lifting feywild
+11.5pp and trimming underdark -4.5pp. T1/T2/T5 untouched.
No test debt -- no production tests pin SpawnWeight or IsElite on
the changed entries (TestMonsterKillTags_GatesKnownMonsters checks
vampire_spawn tags only, unaffected). -short suite green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the 'fairly breezy with some death' target the user picked
for Phase 5. Five-piece ship; Phase 1 matrix lands T1 88%, T2 74%,
T4 72%, T5 ~57% in or above band. T3 remains the design hump at
~45% (manor 39, underforge 47) — Wraith promotion to elite was
already done in Phase 4-B, the remaining standard-pool deaths are
the irreducible part of T3.
Pieces:
1. computeMaxHP × 1.5 (phase5BHPMult in dnd.go). Uniform across
class/level so the class-balance harness's in-tier parity
assertion stays green. Bootstrap (bootstrap_phase5b_hp.go)
refreshes hp_max for existing characters at startup;
idempotent via db.JobCompleted. hp_current is bumped by the
same delta so a full-HP character stays at full.
2. applyPhase5BPlayerFloor (dnd_combat.go): +3 AC, +3 AttackBonus,
+3 weapon.MagicBonus (damage). Applied at the END of
applyDnDEquipmentLayer (after computeArmorAC's AC override)
and inside buildHarnessPlayer so live and harness measurement
match.
3. Elite bracket 19 → 23 (resolveCombatInterrupt). Case order
puts Elite (≥23) before Patrol (≥22) so a 23+ total prefers
the single dangerous fight. Elite is now effectively a
high-threat event reachable only via the +1-per-20-threat-
above-40 mod — Phase 4-B's elite-pool monsters still appear,
just less often.
4. dailyThreatDrift base 3 → 1. Slows the threat clock so
players have the days they need before threat tips zones
into the new 23+ elite band.
5. applyDailyBurn default → 50% (phase5BDailyBurnRatePct). Also
applied in the temporal-override branch in
dnd_expedition_cycle.go so tidal / unraveling days scale by
the same 0.5× — otherwise those days would be
disproportionately harsh against the new baseline.
The harness's expedition_balance.go reads phase5BDailyBurnRatePct
as the default-burn fallback when the override knob is zero, so
Phase 1 matrix measurements now reflect what live players
experience.
Test debt: 13 pinned-numbers unit tests across combat_stats_test,
dnd_test, dnd_xp_test, dnd_equipment_profiles_test,
dnd_expedition_supplies_test, dnd_expedition_cycle_test,
dnd_expedition_extract_test, dnd_expedition_region_cmd_test,
dnd_expedition_combat_test, dnd_expedition_threat_test,
dnd_expedition_temporal_test, expedition_balance_test were
pinning pre-Phase-5-B baselines; updated with comments noting
the cause. Class-balance suite stayed green (uniform buff
preserves spread).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>