Commit Graph
100 Commits
Author SHA1 Message Date
prosolisandClaude Opus 4.7 ad27077329 Phase 4 doc updates
Folds the 4-A diagnostic + 4-B roster pass into the plan doc with
before/after numbers, and notes the partial Phase 4 exit (per-zone
outliers fixed, tier-wide gap remains for T2/T3/T5). Adds the Phase
5 jumping-off list (per-tier elite threshold, gear-tier mapping
review, or shipping burn=75 as the live global).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:24:34 -07:00
prosolisandClaude Opus 4.7 765c738c99 Phase 4b (rosters): per-zone outlier re-classification
Phase 4-A's per-monster trace named one or two miscategorised entries
on each of the four outlier zones — Phase 2c's single-boss-elite
dilution pattern hadn't been applied uniformly, and several over-tier
creatures were sitting on standard slots they couldn't carry.

Roster changes (no monster stat-block touches — bestiary is shared):

  crypt_valdris (T1, 8.5% → 46.0%):
    Dropped flameskull (CR3, 68 kills — still in underforge T3 where
    its CR fits). Promoted specter to IsElite at SW=3 as the soft
    dilutor, mirroring Phase 2c's worg/owlbear shape so the elite
    bracket no longer auto-picks a one-shot.

  forest_shadows (T2, 0.0% → 7.5%):
    Standard pool was carrying two real killers — Displacer Beast
    (38% win) and Bandit Captain (57% win, 14hp loss/win). Both
    re-flagged IsElite; Owlbear SW cut 4→2 to keep the now-4-elite
    pool diluted instead of front-loading Owlbear (9% win) at 50% of
    elite rolls. Standards collapse to Dire Wolf + Dryad, matching
    sunken_temple's "all standards ≥90% win" floor.

  manor_blackspire (T3, 0.5% → 14.5%):
    Wraith was the dominant standard killer (85 attributed kills,
    45hp loss per win) — too punishing for the standard slot.
    Promoted to IsElite; standard floor becomes
    Shadow+Poltergeist+Banshee (all ≥92% win in trace).

  abyss_portal (T5, 0.0% → 25.0%):
    Nalfeshnee was a CR-13 demon on a T5 standard slot (2.8% win, 86
    kills); promoted to IsElite. Second pass: Vrock at 86% win as
    standard still bled 65 kills via 43hp/win attrition — promoted to
    elite too. Standard collapses to Quasit alone (99.9% win, 1.3hp
    loss), matching dragons_lair's near-zero standard-pool kill rate.

Tier-level effect on the Phase 3-B (b=50, f=tier) sweep:

  T1: 26.8% → 44.0%  (+17 pp)
  T2:  6.2% → 10.0%  (+4)
  T3:  2.5% →  8.5%  (+6)
  T4:  7.8% →  7.8%  (no T4 outlier identified)
  T5: 27.5% → 41.0%  (+14)

Outliers are no longer outliers (forest_shadows 7.5% vs sibling 13%
gap; abyss_portal 25% vs sibling 57.5%); the remaining T2/T3/T5
shortfall vs the target band (62-82%/54-74%/36-56%) is tier-wide —
both zones at each problem tier sit below band, so a per-zone tool
won't close it. Phase 4's exit condition ("all per-zone cells within
band") is therefore conditioned on a follow-up tier-wide pass, but
the per-zone bug-fix it was scoped to address is done.

-short suite: same two pre-existing failures
(TestAdv2Scenario_ZoneRunGoblinWarrens, TestMageSpellbookLineInRender).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:23:07 -07:00
prosolisandClaude Opus 4.7 dcd9e4158f Phase 4a (diag): per-zone outlier attribution
The Phase 3-B sweep wrung out the global lever surface and named four
zones that read ~0% completion under every (elite-threshold, drift,
nick-floor, supply-burn) combo: crypt_valdris T1, forest_shadows T2,
manor_blackspire T3, abyss_portal T5. Each has a healthy sibling at
the same tier (goblin_warrens 44%, sunken_temple 13.5%, underforge
2.5%, dragons_lair 57.5%) so the gap is per-zone, not tier-wide.

Adds a structured per-fight trace hook (traceFightStruct + the
harnessFightTrace struct that mirrors the existing string trace) so
diagnostics can aggregate without parsing the formatted log line.
Mirror-format with traceFight; if a field is added, update both
paths.

TestExpeditionBalance_Phase4A_OutlierDiagnostic walks the four
outlier-vs-sibling pairs at 200 trials each on the Phase 3-A/3-B best
cell (e=23 d=1 burn=50) and reports per-monster appearances /
win-rate / avg HP loss / kill attribution + day-of-end histogram +
elite-vs-standard fight mix.

Findings:
  - crypt_valdris: dual-killer elite pool (Wight 99 kills, Flameskull
    68). Phase 2c left this zone untouched ("already dual-elite") but
    both elites are over-tier for T1.
  - forest_shadows: standard pool too lethal — Displacer Beast (53
    kills, 38% win standard) + Bandit Captain (48 kills, 57% win).
  - manor_blackspire: Wraith on the standard slot is dragging the
    floor (85 kills, 45hp loss per win). Vampire Spawn + Revenant
    elite pair is also rough.
  - abyss_portal: Nalfeshnee mis-classified standard (86 kills, 2.8%
    win at L17). Vrock at 79% win is borderline.

Phase 4-B applies per-zone roster tweaks (IsElite re-flag, drop a
deadly entry, soften a SpawnWeight) — no monster stat-block changes.
T3 may need a follow-up tier-wide pass since the sibling underforge
also sits at 2.5%; out of scope here.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:18:44 -07:00
prosolisandClaude Opus 4.7 2d44c990f3 Phase 3b (sweep): nick-floor + supply-burn sweep, T5 supply unlock
Wired SurpriseNickFloorOverride and SupplyBurnRatePctOverride into the
harness day-loop via two new parameterized helpers (surpriseRoundNickF,
applyDailyBurnP). Live callers go through the existing constants;
sweep test sits on top of the Phase 3-A best cell (e=23, d=1).

TestExpeditionBalance_Phase3B_NickSupplySweep walks 3×3 (floor ∈ {0, 1,
tier=live}) × (burn% ∈ {50, 75, 100=live}) × 10 zones × 200 trials.

Strong partial T5 positive; nick-floor lever inert.

  - Supply burn is the T5 unlock: dragons_lair 0% → ~55% at burn=50.
    Fighter survives elites; burn=75 isn't enough margin.
  - T4 peaks at burn=75 (~12% underdark/feywild); burn=50 dips T4
    slightly (more elites survived into).
  - Nick-floor inert across tiers (≤3pp swing); wounded-clamp already
    eats the chip-damage budget. Recommend dropping from live-tuning
    candidates.
  - T2-T3 wall persists: forest_shadows, manor_blackspire,
    abyss_portal stuck at ~0% across every combo — outliers, not
    addressable by global levers.

Global levers wrung out. Plan-doc Phase 3-B section + memory pointers
updated; next is Phase 4 (per-zone outlier pass). -short shows the
same two pre-existing failures (TestAdv2Scenario_ZoneRunGoblinWarrens,
TestMageSpellbookLineInRender).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:10:21 -07:00
prosolisandClaude Opus 4.7 235122f2a1 Phase 3a (sweep): elite-bracket + drift sweep, strong partial positive
Wired two harness lever overrides — EliteInterruptThresholdOverride
(live=19) and ThreatDriftBaseOverride (live=3) — into the day-loop in
expedition_balance.go. Live runHarvestInterrupt / dailyThreatDrift are
untouched; the harness re-buckets Standard↔Elite after the live call.

TestExpeditionBalance_Phase3_GlobalLeverSweep walks a 3×3 grid
(elite ∈ {17,19,23} × drift ∈ {1,3,5}) over the Phase 1 matrix at
200 trials/cell. -short skips.

Elite-bracket threshold is the dominant lever for T1–T3. At
e=23/d=1: T1 mean 24.0% (goblin_warrens 40.5%), T2 7.2%
(sunken_temple 14.5%), T3 1.8%. Still well below target bands
(T1 70-90%, T2 62-82%) — the lever moves the needle in the right
direction but cannot land any tier on-band alone.

T4/T5 fingerprint changed but didn't lift. At e=23 dragons_lair
death drops 60% → 24% but starvation climbs to 75% — the fighter
now survives elites long enough to run out of supplies. T4 cells
shift the same way. Indicates a second lever is needed for the
higher tiers (standard-fight survivability or supply margin), to
be swept in Phase 3-B.

Plan doc updated. Renumbered the trailing "per-zone outlier pass"
to Phase 4 and "MAD / second-order" to Phase 5 so the test names
align with phase numbers going forward.

-short suite: same 2 pre-existing failures
(TestAdv2Scenario_ZoneRunGoblinWarrens, TestMageSpellbookLineInRender).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:00:04 -07:00
prosolisandClaude Opus 4.7 9a4a93e72e Phase 2c (roster gate): dilute single-boss elite pools
Each zone's elite pool was a single SpawnWeight=1 over-tier boss, so
InterruptElite bracket rolls auto-picked that boss 100% of the time.
The post-2b tier-lethality trace named these one-shots (Warchief, Hag,
Roper, Young Red Dragon) as the dominant remaining death source.

Promotes one mid-tier alt per zone to IsElite (underforge gets two
since fire_elemental was itself a non-elite killer). Boss SW=1 stays;
alt SW 3-4 gives the boss ~17-25% elite-roll share. pickZoneEnemy also
excludes IsElite from the standard pool, so promotion softens standard
encounters by one mid-difficulty entry — each zone still has 3-4
standard entries left. Crypt-Valdris already dual-elite, untouched.

Promotions: worg, owlbear, aboleth_thrall, vampire_spawn,
salamander+fire_elemental, drow_elite_warrior, night_hag,
dragonborn_cultist, hezrou.

Phase 1 matrix delta (200 trials/cell, Fighter, centerline):
  goblin_warrens: 0% → 3.0% completion (first non-zero T1 reading)
  sunken_temple:  0% → 0.5%
  dragons_lair:   death 100% → 60% (40% starve — different failure)
  underdark:      death 100% → 90% (10% starve)
  feywild:        death 100% → 92% (8% starve)
  T3 cells still 100% death — next pass is global supply/threat levers.

Tier-lethality trace shows Drow Elite Warrior + Dragonborn Cultist
spawning and being winnable, with arcs stretching to 7-10 encounters
instead of fresh-entry one-shots.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:45:40 -07:00
prosolisandClaude Opus 4.7 8cdd64b383 Phase 2 (sweep): lever-tuning sweep, negative result
Sweep the two knobs surfaced by Phases 2a/2b — retreatThreatBump
and clampSurpriseNick's wounded-entrant divisor — across a full
3×4 grid (bump ∈ {2, 5, 10} × divisor ∈ {3, 5, 8, 12}) at 200
trials/cell across every matrix zone.

Wiring is harness-only: clampSurpriseNick keeps its live shape and
delegates to a new clampSurpriseNickD(divisor) variant; the harness
profile gains RetreatThreatBumpOverride/SurpriseNickDivisorOverride
fields threaded onto expeditionHarness; resolvedRetreatBump and
resolvedNickDivisor pick override-or-live. Zero on either field
falls back to the shipped value so live runHarvestInterrupt is
untouched.

Sweep test: TestExpeditionBalance_Phase2_LeverSweep, -short skipped,
mirrors Phase2_CadenceCalibration's per-tier digest shape.

Outcome: across 24,000 trial-cells (12 lever combos × 10 zones
× 200 trials), every cell reports 0.0% completion / ~100% death.
The knobs are inert on the headline metric — even (b=2, d=12)
can't lift any tier off the floor. Confirms the post-2b
tier-lethality trace: remaining deaths are fresh-entry elite
one-shots (Warchief, Hag, Roper, Young Red Dragon), not chained-
interrupt cascades. Justifies Phase 2c (roster dilution) rather
than further tuning of these two levers.

Plan doc updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:39:45 -07:00
prosolisandClaude Opus 4.7 159daf8fc8 Phase 2b (lever): wounded-entrant surprise-nick clamp
The post-2a tier-lethality trace showed the remaining 0% completion
was driven by chained surprise-round nicks on already-wounded fighters,
not by the elites themselves. Pattern from the trace:

  fight day=5 Hobgoblin Warchief: hp_pre=24 → hp_post=14  WON
  fight day=5 Goblin Archer:      nick=6, hp_pre=8 → hp_post=0  LOST

The Warchief left the fighter at HP 14; the Goblin Archer's surprise
nick (6 HP) dropped them to 8 before combat resolved — and a standard
goblin then finished a fighter who should have survived. Same shape at
T2 (Dire Wolf nick 4 on hp=3), T3 (Fire Elemental nick 4 on hp=5),
T4-T5. The nick was acting as a hidden cascade multiplier, pre-empting
the combat engine on wounded entries.

clampSurpriseNick caps the nick at max(1, hpCurrent/5) when the
fighter enters wounded (HPCurrent < HPMax); at full HP the raw nick
stands. The existing 'nick < HPCurrent' KO-guard is preserved as a
backstop. /5 is the wounded-fighter lethality knob; tighter (/10) is
gentler, looser (/3) re-opens the cascade.

Live caller (runHarvestInterrupt) and harness (runHarnessFight) both
route through the new helper so the sim measures the same lever the
live caller applies.

Matrix delta is mild (encs +0.1-0.2 per cell, completion% still 0%)
but the tier-lethality trace stretches substantively: T1 trial 0 ran
5→8 encs / 5→7 days, T3 trial 1 saw a fighter survive multiple
chained interrupts at low HP that pre-2b would have ended on nick
alone. The remaining deaths are now legible as elite-one-shot fights
on fresh entries (Warchief, Green Hag, Roper, Young Red Dragon) —
that's the Phase 2c roster-gate signal.

Push-back on the original lever-order: the Phase 2a recap put roster
gate first, but the trace fingerprint named the wounded-entry nick
as the dominant cause-of-death in 4 of 5 tier traces. Doing nick-cap
first keeps Phase 2c's diagnostic clean and avoids re-tuning rosters
after another lever changes the shape under us.

Pre-existing failures unrelated to this change:
- TestAdv2Scenario_ZoneRunGoblinWarrens (advance regression, prior)
- TestMageSpellbookLineInRender (render assertion, prior)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:27:29 -07:00
prosolisandClaude Opus 4.7 04aa887d18 Phase 2a (lever): expedition retreat semantics split
Phase 2 diagnostics named the InterruptElite bracket as the likely
first lever; tier-walking the lethality probe at the matrix cadence
told a different story.  Phase 1's uniform-0% baseline isn't an
elite-bracket calibration issue at all — every tier reads 0% because
the engine's TimedOut contract was being ignored by every expedition
caller.

combat_engine.go:451 says: "Timeout = retreat, not lethal blow.
Caller treats a timeout loss as 'fight ended, no character death'".
But runHarvestInterrupt / tryPatrolEncounter / resolveCombatRoom all
called abandonZoneRun + retireAllRegionRuns on any !PlayerWon —
ending the expedition outright on a retreat.  The retreat flavor line
("X outlasts you. You retreat from the expedition, wounded but alive")
was already in the code, just stapled to an actual run-abort.

Splits the policy by caller:

  • runHarvestInterrupt   — autopilot daytime interrupt.  TimedOut →
    retreat: threat +5, HP carries over, run continues, harvest slot
    forfeit (no kill / loot).  HP<=0 still ends the run + marks dead.
  • tryPatrolEncounter   — !advance pre-room patrol roll.  Same
    retreat policy: patrols don't gate progress, so retreating from
    one and walking into the next room is the right shape.
  • resolveCombatRoom    — !advance room/elite combat.  Unchanged —
    this path gates room progression; a retreat has nowhere to go, so
    any loss still ends the run.  (Manual zone runs were always
    intended to end here.)

Harness mirrored: daytime interrupt timeout → carry HP + threat bump
+ continue day; night-encounter loss → terminate (mirrors
resolveCombatRoom, since live night encounters defer to !advance).

retreatThreatBump = 5 is the per-retreat threat penalty.  Low enough
not to compound brutally with chained retreats, high enough that 3–4
retreats noticeably walks the threat clock toward Stirring.  Easy to
dial in Phase 3 if zones go off-band.

Phase 1 matrix after the change still reads 0% completion at every
cell — but the encounter counts and survival shape are dramatically
different (T4 underdark 3.6→7.5 encs; T3 underforge trial saw 18
encounters across 10 days where the pre-change run died on day 2/3).
Adds TestExpeditionBalance_Phase2_TierLethality, a tier-walking
companion to the T1/rolls=1 probe, that traces every fight at the
matrix cadence across one zone per tier — the actual Phase 2b lever
work picks from this data, not the old T1-only probe.

The remaining 0% is now legibly driven by tier-disproportionate elite
rosters (Hobgoblin Warchief at T1, Green Hag at T2, Roper/Helmed
Horror higher up) that one-shot or two-shot tier-appropriate
fighters.  Phase 2b's lever shortlist:

  1. Roster gate / SpawnWeight tuning to dilute over-tier elites.
  2. Surprise-nick floor reduction on chained interrupts (carryover
     HP + nick is the death-spiral fingerprint at T1 specifically).
  3. Per-day cadence reduction if 1+2 don't carry T1 to band.

Pre-existing test failures (TestAdv2Scenario_ZoneRunGoblinWarrens,
TestMageSpellbookLineInRender) verified to fail identically on HEAD;
no new test regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:01:07 -07:00
prosolisandClaude Opus 4.7 0f09a421bc Phase 2 (diag): cadence sweep, gear-tier fix, lethality probe
Three Phase 2 diagnostic artifacts. No tuning knob has moved on
production code yet — these tests calibrate the harness and surface
the real first lever for next session.

1) Cadence calibration sweep
   TestExpeditionBalance_Phase2_CadenceCalibration sweeps
   HarvestRollsPerDay ∈ {1,2,3,4} across the full Phase 1 matrix and
   logs per-cell + per-tier completion. Required a new
   HarvestRollsPerDay field on expeditionBalanceProfile so cells can
   override the package-default constant. Finding: cadence is NOT the
   dominant lever — at rolls=1 the T1 cell only reaches 2%, with
   bimodal hp_left (100% survivors / 0% deaths). Killed the cadence
   hypothesis from Phase 1's commit message.

2) Gear-tier centerline fix
   phase1TierCenterline bumped for T3/T4/T5 (8→9, 11→13, 15→17). The
   shared gearTier ladder (5/9/13/17 boundaries) was placing T3/T4/T5
   centerlines one gear bracket *below* the zone's tier, so those
   cells fought with under-spec'd weapons/armor. New centerlines are
   the lowest level in each tier's design-doc range where gearTier ==
   tier. All centerlines still inside their design-doc ranges. Effect
   in the sweep at rolls=1: underforge T3 1.0% → 10.5% comp, underdark
   T4 flipped from pure combat-death to 14% starve (i.e. fighter now
   survives combat, runs out of food). Real bug, but small — the
   structural lethality problem remains.

3) Lethality probe + traceFight hook
   TestExpeditionBalance_Phase2_LethalityProbe runs 5 trials at the
   cleanest cell (T1 goblin_warrens L3 fighter, rolls=1) with a new
   optional traceFight hook on expeditionHarness that logs
   monster/AC/atk/HP-pre/HP-post/outcome per fight. Hook is nil in
   production runs, zero cost when unused. Finding: at T1, the
   InterruptElite branch keeps drawing Hobgoblin Warchief (AC 18,
   atk +5) from goblin_warrens' elite roster, and an L3 fighter has
   ~coin-flip odds against a CR-6-ish elite. One bad draw = dead;
   that's the bimodal hp_left fingerprint from the sweep. Non-elite
   draws (Worg, AC 13) play out as normal multi-round combats and
   are winnable.

Next-session lever choices, in order of suspected impact:
  - Roster gate: Hobgoblin Warchief out of (or weighted down in)
    the T1 elite pool — it's tier-disproportionate for goblin_warrens.
  - InterruptElite threshold: rarer elite-bracket draws at low threat
    so a single d20 swing doesn't equal expedition end.
  - Tier-floor cap on already-over-tier bestiary entries.

Plan doc: gogobee_expedition_difficulty.md §Phase 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:39:31 -07:00
prosolisandClaude Opus 4.7 881cbfca2f Phase 1: expedition-difficulty full matrix baseline
New permanent test TestExpeditionBalance_Phase1_FullMatrix — every
registered zone × its tier-centerline level, 200 trials/cell, Fighter
fixed (class parity is the class-balance pass's job).

Centerlines per plan doc §Method (median of design-doc level range):
T1=L3, T2=L5, T3=L8, T4=L11, T5=L15.

Gates split: wiring pathologies (zero-day loop, days > cap) are fatal;
"0% at T1 / 100% at T5" land as WARN log lines, not t.Errorf. Phase 1's
deliverable is a logged baseline, not a tuning gate — Phase 2 promotes
those to band assertions once the global levers move.

Baseline numbers (seed 0xE0FFEE1, 200 trials, current Phase 0 harness):

  all 10 cells: 0.0% completion, 100% death, median day 2,
  avg encounters 2.0-3.6, threat at end 5-7.

The uniform 0% says the placeholder harnessHarvestRollsPerDay=4 plus
the tier-floored AC/atk is overwhelming the fighter on day 1 across
all tiers, well before threat drift matters. Per-tier spread is 0.0pp
because every zone is floored — Phase 2 has plenty of headroom and a
clear first lever (encounter cadence).

Plan doc: gogobee_expedition_difficulty.md §Phase 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:09:47 -07:00
prosolisandClaude Opus 4.7 53d1608490 Phase 0: expedition-difficulty sim harness spike
New file internal/plugin/expedition_balance.go — sibling to
dnd_class_balance.go, no dnd_ prefix per feedback_avoid_dnd_naming.
Plan doc: gogobee_expedition_difficulty.md.

Critical de-risk settled: clock-injection seam. The harness does not
run the wall-clock tickers (expeditionBriefingTicker @ 06:00,
expeditionRecapTicker @ 21:00, expedition_ambient @ 3h) and does not
inject a virtual time.Now. Instead, advanceExpeditionOneDay
reimplements the morning→day→night pipeline by calling the math-pure
helpers directly: applyDailyBurn, dailyThreatDrift,
resolveCombatInterrupt, resolveWanderingCheck, simulateCombatWithRNG.
No DB, no goroutines, no clock.

Phase 0 trade-offs (documented in the file header):
- Boss completion deferred; survive-N-days is the proxy.
- Per-region zones, loot/XP, pardon/Sovereign, babysit, temporal
  stack effects all skipped — Phase 1+ scope.
- Encounter cadence (harnessHarvestRollsPerDay=4) is a placeholder
  to be calibrated against live traces in Phase 1.

Tests:
- TestExpeditionBalance_Phase0_Spike — T2 Crypt Valdris × L5 Fighter,
  100 trials, asserts only degenerate sentinels (no 0%/100%, days>0,
  days<=cap). Runs in <3s.
- TestExpeditionBalance_Phase0_SeedSpread — confirms the RNG seam is
  actually wired by showing two distinct seeds produce different
  trials. Full byte-for-byte reproducibility under same seed is not
  asserted at Phase 0; surpriseRoundNick + pickWanderingMonster
  draw from package-global rand (same caveat as class balance).

Spike numbers (calibration baseline, NOT a target): 1% completion,
99% deaths, median 4 days, ~6 encounters. The headline death rate is
loud but expected — Phase 1's full matrix will diagnose whether it's
HP-carryover punishment, bestiary-vs-abstract-monster delta, or kit
ladder mismatch. Phase 2 is where the centerline gets tuned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:37:23 -07:00
prosolisandClaude Opus 4.7 9aca6b202d Phase D: presence intent comment + sendZoneCombatMessages contract
D1: single block comment above the presence.go best-effort QueryRow
cluster (lines 149-198). Zero-value fallbacks are correct for new users
with no activity rows yet.

D2: contract comment on sendZoneCombatMessages — returns a done channel,
callers chaining post-flush work MUST consume it; in-flow handlers with
nothing to chain MUST explicitly discard. Blocking on flush would stall
command handlers behind the 2-3s/message pacing, defeating streaming.
Two fire-and-forget sites (adventure_arena.go round-advance,
dnd_zone_cmd.go streamFlow) now use `_ =` + comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:03:42 -07:00
prosolisandClaude Opus 4.7 2907a8ac1d Phase C2+C3: atomic RunMaintenance + weekly integrity_check
C2: wrap the 19-statement purge loop in a single BEGIN IMMEDIATE /
COMMIT so a mid-maintenance crash can't leave half the cache tables
purged. SetMaxOpenConns(1) guarantees the BEGIN, DELETEs, and COMMIT
all share one SQLite connection.

C3: run PRAGMA integrity_check at most once per week, gated on the
mtime of a sentinel file (.integrity_check_last) under dataPath. Any
result other than 'ok' is logged via slog.Error. The sentinel is
touched even on failure so a corrupted DB doesn't re-run the check
every cycle and spam logs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:57:10 -07:00
prosolis ce2f59c332 Phase C1: gate db.Backup() on GOGOBEE_BACKUP_DIR
Audit flagged C1 as "Backup() unwired" — actually
adventure_scheduler's dailyReset has been calling it every
midnight. The real gap was that Backup() always ran, always
to dataPath/backups, with no way for dev environments to opt
out.

Now: env var unset → no-op + debug log. Env var set → that
directory is the destination. Existing scheduler call site
unchanged; it cleanly no-ops in dev.
2026-05-15 07:50:11 -07:00
prosolis b4b1e0ee29 Phase B2 follow-up: first-person voice across all TwinBee flavor files
Sweep of the remaining four twinbee_*.go flavor files (gm, resource,
ambient, housing). Every 'TwinBee [verb]' / 'TwinBee says' / 'TwinBee
notes' rewritten into first-person or implicit-subject voice. Jokes,
references, and pacing preserved; a few lines got slightly sharper
punchlines on the way through.

Files touched:
  - twinbee_gm_flavor.go        (~270 lines, all narration pools)
  - twinbee_resource_flavor.go  (~100 lines, harvest/fish/loot pools)
  - twinbee_ambient_flavor.go   (~40 lines, between-day ticker)
  - twinbee_housing_flavor.go   (7 lines; Thom Krooke 3rd-person and
                                 Pastel 1st-person voices preserved)

twinbee_npc_flavor.go intentionally untouched: those are Pete
announcements referring to TwinBee, which is correctly third-person
(different speaker).

DO-NOT-REWRITE headers replaced with voice-convention notes covering
future additions.
2026-05-15 07:46:04 -07:00
prosolis 9e70f9db79 Phase B2: first-person voice pass on twinbee_expedition_flavor.go
Every 'TwinBee [verb]' / 'TwinBee says' / 'TwinBee notes' rewritten
into first-person or implicit-subject voice. Header DO-NOT-REWRITE
prohibition retired (it predated the realization that the third-person
voice was actively annoying); replaced with a voice-convention note
covering future additions.

Sibling twinbee_*.go files still carry third-person references; sweep
to follow.
2026-05-15 07:29:30 -07:00
prosolis 20647eeafc Phase B1 follow-up: regex catches bare-die notation; blink overlay shim
The A1 regex banned \b\d+d\d+\b (e.g. '1d20') but missed bare die
notation ('d20', 'd6') with no leading count. SRD's 'blink' Description
slipped through with 'Roll a d20 at the end of each of your turns...'.

- Add \bd(4|6|8|10|12|20|100)\b ban to the prose regex sweep.
- Add hand-authored overlay shim for blink in dnd_spells_data.go.
  Classes union with SRD preserves Cleric/Sorcerer/Warlock availability.

Test now passes the wider sweep; no other reachable spells leak.
2026-05-15 07:18:16 -07:00
prosolis f198041530 Phase B1: jargon-free !help blurbs in adventure Commands()
Strip 'Adv 2.0', dice formulas, DCs, and ability-mod math from the 21
player-facing Description strings in AdventurePlugin.Commands(). Rewrite
harvest verbs (forage/mine/scavenge/essence/commune/fish) into outcome
language. !roll keeps its dice syntax since that's a format spec, not
gameplay jargon.

No runtime changes; build + vet clean.
2026-05-15 07:11:40 -07:00
prosolis 2ef0b25266 Phase A1: jargon-free flavor on all reachable spell Descriptions
The audit plan flagged jargon leaks in the SRD spell prose surfaced by
!cast and !spellbook. Verifying against HEAD turned up two corrections
to the plan's framing:

- The overlay in dnd_spells_data.go wins on ID collision, so for the
  ~76 hand-authored spells the *overlay* Descriptions are what players
  actually see — and those were crunchier than the SRD ("Heal 1d8 +
  WIS mod HP. Touch.", "8d6 fire in 20 ft radius. DEX save half.",
  etc.). Touching only the generated SRD file would have missed the
  bigger leak.
- The Upcast field is never displayed at runtime (only the import
  generator reads it), so testing it against the regex would test
  dead data.

So this commit:
- rewrites every overlay Description into playful, jargon-free flavor
  in the charm_person voice ("Sweet-talk a humanoid…") — no dice, no
  saves, no AC math, no ability-mod references
- adds 31 overlay shims for SRD-only spells whose generated text was
  broken (placeholder gaps from the Open5e classifier — "no larger
  than a.", "up to to", "in a in range"), empty ("You touch a
  creature."), typo-laden ("magicou", "undeadou", "diseasesou"), or
  jargon-heavy (false_life's "1d4+4 temporary hit points")
- adds TestSpellDescriptionsAreJargonFree, a regex sweep over the
  merged registry that bans "saving throw", "spell slot of",
  "ability modifier", "hit points equal to", and \bNdN\b dice
  notation. Currently passing; will fail-loud on regressions or new
  SRD imports that need overlay shims.

The generated dnd_spells_srd_data.go is left untouched — overlay is
the regen-safe surface per its own header comment.
2026-05-15 07:04:37 -07:00
prosolis 368b980650 Expedition autopilot Phase 3: ambient between-day ticker
Active expeditions now tick on a 3-hour cadence between the 06:00
briefing and 21:00 recap. Each fire DMs the player a short TwinBee
moment — mostly pure flavor (dripping sounds, polite possums,
diplomatic moths) with a 50/50 chance of a small mechanical nudge
(±SU, +threat, 1d4 coins from a found purse, +1 HP if camped).

The dungeon is now louder while you're offline without being
balance-relevant. Multi-day expeditions feel populated by something
other than the player's calendar.

Mechanics:
- new last_ambient_at column with CAS idempotency (mirrors
  briefing/recap pattern in dnd_expedition_cycle.go)
- ambientCooldown=3h, skipped if in turn-based combat session
- 60min politeness window around scheduled briefing/recap so we
  don't pile DMs on the daily messages
- weighted event pool with per-event eligibility (camp_visitor
  requires camped, pack_rat requires SU>=1, faction_whisper
  requires threat>=30 and not siege)
- effects: applyThreatDelta, updateSupplies, euro.Credit +
  coins_earned bump, SaveDnDCharacter HP nudge (clipped at max)

Files:
- internal/db/db.go: ALTER TABLE + CREATE TABLE update
- internal/flavor/twinbee_ambient_flavor.go: 7 humor-heavy pools
- internal/plugin/expedition_ambient.go: ticker + resolver
- internal/plugin/expedition_ambient_test.go: 8 pure-logic tests
- internal/plugin/adventure.go: wire goroutine in Start()
2026-05-14 23:28:54 -07:00
prosolis 632e5ee315 Combat: fix Orc Rage missing fire when enemy two-shots across rounds
The rage threshold check sat only at the top of resolvePlayerAttack,
so a player who took a threshold-crossing hit and was then killed by
the very next enemy swing (before getting back to their own attack)
would never see "rage" emitted — even though HP visibly crossed 50%
while alive.

Extracted maybeTriggerOrcRage shared by both sites:
- Top of resolvePlayerAttack (unchanged UX: rage applies same-round
  when the player swings after the enemy hit).
- End of runRound as a backstop (catches cross-round two-shots).

st.raged guards against double-emit. Fixes the flaky
TestOrcRageFiresOnLowHP; 10/10 repeats green.
2026-05-14 23:17:12 -07:00
prosolis f404f95202 Expedition autopilot Phase 2: auto-harvest on room entry
Each walked room gets one auto-harvest pass across every applicable
action (forage, scavenge, mine, fish in water zones, plus the class-
restricted essence/commune). Class/kill/event gates are inherited
from the manual harvest path.

Stop rules:
- Rare+ nodes are NOT auto-attempted; they pause the walk via
  stopRareNode so the player spends the attempt deliberately.
- Standard/Elite/Patrol interrupts hard-stop the walk (stopEnded on
  death, new stopHarvestCombat on survive).
- Noise interrupts apply threat+2 and continue, matching manual
  !harvest behavior.

Display:
- Per-room compact footer ("+2 Scrap Iron, +1 Shadow Herb · 1 fail")
  streamed inline as autopilot walks.
- Walk-end cumulative tally appended to whatever final block fires.

No SU surcharge — parity with manual !harvest.

Pure helpers unit-tested (rarity classification, footer rendering,
walk tally sort order, rare-pending dedup). Full autoHarvestRoom
path needs prod DB and follows the existing setupAuditTestDB gating
pattern.
2026-05-14 23:13:01 -07:00
prosolis 14f9b3159f Expedition autopilot Phase 1: !explore / !expedition run
Adds an autopilot loop that walks rooms automatically until a
natural or threshold interrupt fires.

- New !explore (top-level alias) and !expedition run subcommand
  driven by expeditionCmdRun in dnd_expedition_cmd.go.
- advanceOnce extracted from zoneCmdAdvance so the autopilot and
  the manual !zone advance path share one room-step implementation.
- Stop reasons: fork, elite/boss doorway, death, complete,
  active-combat block, HP <= 30%, SU < 1 day's burn, 6-room cap.
- Trash combat continues to auto-resolve inside resolveCombatRoom;
  no changes there.
- TestAutopilotFooter_Reasons covers the pure stop-reason logic;
  preflight + walk tests skip without the prod DB, matching the
  existing setupAuditTestDB pattern.

Phase 2 (auto-harvest) and Phase 3 (real-time ticking) deferred.
2026-05-14 23:02:53 -07:00
prosolis 8ee170bb9b UX S7: sheet + menu jargon sweep — outcome-first copy
Drops D&D-handbook syntax from the player-facing caster UX in favor of
verbs and feelings (per feedback_accessibility_over_dnd_crunch).

R12 — Drop "Spell DC: N / Spell Atk: +N" line from the spellbook view.
The numbers are pure handbook noise; the engine still computes them.

R13 — renderSlotLine swaps "L1 3/4 · L2 1/3" for "Level 1 ●●●○ · Level 2
●○○○". Filled bullets = energy left, hollow = spent.

R14 — Caster passive Description strings rewritten outcome-first: no
"+5%", no "scaled by your Charisma". Verbs and texture instead. All ten
class passives reworded; mechanics in applyClassPassives unchanged.

R18 — Class menu drops the "(d8, INT/WIS)" suffix → "**Mage** — INT &
WIS". The HP-die number was leaking implementation. !setup class confirm
line gets the same treatment.

P5 — Spellbook headers: "Cantrip" → "Cantrips", "L1" → "Level 1".

P6 — Cast queue line: "_(upcast to L2)_" → "_(empowered)_". Queued-line
in the spellbook view follows suit — drops "(L2 slot)" for "(empowered)"
when the slot is above the spell's base level.

P8 — Comment-convention marker added to dnd_passives.go and
dnd_subclass_combat.go file headers: `// internal note (not user-facing)`
flags Phase 2/3 tuning history so future codegen doesn't lift it into
Description / Flavor strings.

P9 — Spellbook line drops the duplicate "(can learn N more)" trailer —
the "%d / %d" already conveys remaining capacity.

Bonus cleanup. The two Arcane-Trickster slot-ceiling errors ("Arcane
Trickster L5 only has up to L1 slots.") and the "No L1 slot available"
out-of-energy messages get the same jargon scrub: "At level 5, your
Arcane Trickster magic only reaches level-1 spells." / "You're out of
level-1 energy."

Tests. Full plugin suite green; no test pinned the old strings.
2026-05-14 22:17:03 -07:00
prosolis 6386161402 UX S6: race truth-up — wire Tiefling fire resist + best-fit hints
R22: replace race copy that promised mechanics the engine doesn't deliver.
- Tiefling: wire FireResist as a CombatModifier. Enemy main attack is
  halved when monster is FireAttacker-tagged; aoe_fire abilities are
  halved unconditionally; fire-tagged traps deal half damage to Tieflings.
  DnDMonsterTemplate carries FireAttacker; toCombatStats propagates it.
  Hand-authored fire entries tagged in dnd_bestiary.go (flameskull,
  magmin, azer, salamander, fire_elemental, emberlord_thyrak,
  young_red_dragon, infernax, belaxath).
- Open5e tuned generator derives FireAttacker from the highest-AvgDamage
  attack's DamageType (threshold AvgDamage>=5). 19 tuned monsters tag.
  Regenerated bestiary_tuned_data.go.
- Elf: drop "immune to sleep" (no sleep mechanic); reframe as keen
  senses + trance flavor.
- Half-Elf: drop "two bonus skill proficiencies" (no skill system);
  reframe as adaptable cross-cultural know-how.
- Tiefling copy: drop "bonus on CHA checks" (no checks); keep fire
  resistance with flavor framing.

R23: DnDRaceInfo grows a BestFit field; renderRaceMenu emits an
"_best with: …_" hint per race so spiky stat spreads (Orc -1/-1/-1)
read as specialist picks rather than a brick of penalties.

R24: dnd.go header comment for the caster classes now reflects the
shipped state (Playable=true, spell lists populated) instead of the
pre-Open5e scaffold language.

Tests: TestApplyRacePassives gains a FireResist column; new
TestTieflingFireResistance asserts ~0.5x ratio over a 300-trial sweep
against a FireAttacker enemy. Full suite green.
2026-05-14 22:06:31 -07:00
prosolis 1512f6cc50 UX S3: SRD copy-edit pass — sanitize jargon + curated overrides
Plumbed through the open5e importer so regen stays safe:

- New cmd/open5e-import/desc_overrides.go holds two per-ID override
  maps (spellDescOverride, magicItemDescOverride) and a regex-driven
  cleanDesc sanitizer. Override wins outright; otherwise the SRD
  first-sentence runs through cleanDesc, which strips the phrases
  the S3 acceptance criteria forbid (saving throw[s], spell slot,
  within range, 5-foot, DC <n>, "(save DC X)" parentheticals,
  "constitution score is N", "out to a range of N feet"). A small
  post-pass repairs the orphan stubs the strippers leave behind
  (" and." trailers, "must make." after the saving-throw object
  is gone).

- gen.go (spells) + magicitems.go now call spellDescription /
  magicItemDescription instead of raw firstSentence; same hand-
  authored override pattern, same cleanDesc fallthrough.

- Override coverage: the 19 SRD-only spells that show up in
  defaultKnownSpells (call_lightning, charm_person, vicious_mockery,
  …) plus ~35 high-visibility magic items (Amulet of Health, every
  Belt of Giant Strength variant, Cloak of Displacement, etc.).
  Tone is outcome-first second-person with bite — these surface
  in the spellbook and the curio shop, so they get to be funny.

- tuned.go (R21) + magicitems.go strip "(...)" from emitted Names
  via stripNameParenthetical. Slug keeps the variant; only the
  display text loses the qualifier. Two bestiary entries
  (giant_rat_diseased, deep_gnome_svirfneblin) and stone_of_good_luck
  affected.

- Regenerated all three data files. Acceptance grep is clean:
  zero hits for any banned phrase in Description/Desc fields.

- New cmd/open5e-import/desc_overrides_test.go covers the
  sanitizer regressions, orphan-repair, name-strip, and the
  override-wins-but-fallthrough-still-sanitizes path.

Conflicts: none. S4 (magic-item UX) wanted this in first so the
new curio renderer consumes clean text — ready for it now.
2026-05-14 21:53:06 -07:00
prosolis c48e12a296 UX S2: caster onboarding — strip reaction defaults, deterministic healing_word, route hints
B2: Removed shield/counterspell from Mage/Sorcerer defaults and hellish_rebuke/counterspell
    from Warlock — reactions are EffectReaction and combat has no reaction window yet, so
    auto-granting them just litters the spellbook with dead entries. Substituted L1/L3
    staples (sleep, fly, burning_hands, chromatic_orb where class-tagged appropriately).

B3: Replaced the stale "Mage, Cleric, Ranger, and Arcane Trickster (L5+)" enumeration in
    !cast/!spells learn/!prepare error paths with class-aware route hints via a new
    spellRouteHintFor() helper.

R15: Removed the duplicate hand-authored healing_word_spell entry; Cleric default now
     references the canonical SRD healing_word (cleric/druid/bard tagged). parseSpell
     and lookupSpell now resolve "healing word" deterministically.

R16: !cast --drop now appends "(slot refunded)" when applicable.

R17: Prep-cap message rewritten to "Your prepared list is full (N/N)."

R19: Removed shillelagh from Druid default — the overlay flags it cleric/ranger only and
     it has no real attack profile yet (BuffSelf, no DamageDice).

Bonus correctness: dndSpellRegistry now unions Classes on overlay collision instead of
replacing wholesale. The hand-authored buildSpellList() entries were tagged Mage-only,
so every Sorcerer/Warlock/Bard/Druid spell that collided with the SRD silently lost its
class list — meaning auto-granted defaults were rejected at the classOK gate. Union
merge restores SRD coverage without forcing a hand-edit of every overlay entry.

Tests:
- TestDefaultKnownSpellsHaveNoReactions covers B2.
- TestDefaultKnownSpellsAreCastableByClass guards the overlay-narrow regression.
- TestHealingWordResolvesDeterministically covers R15.
- TestCastNonCasterErrorHasNoClassEnumeration covers B3.
- Existing dnd_prepare_test.go updated to reference healing_word.
2026-05-14 21:30:15 -07:00
prosolis 25accab5c0 UX S1: sell-all guard against magic-item deletion (B1)
`!adventure sell all` and `!adventure sell <name>` previously treated
Type=="magic_item" rows as bulk loot, silently deleting bonded curios for
their base coin Value. Mirror the existing MasterworkGear/Arena/card
branch and reroute the player to `!adventure equip-magic`.

Tally kept curios in the sell-all summary; surface a dedicated
empty-rejection message when the only contents are curios; add the same
guard to the single-item path.

Tests cover: mixed bag (magic item preserved, junk sells), curio-only
bag (no-op + reroute), explicit sell <curio name> (refused + reroute).
2026-05-14 21:19:31 -07:00
prosolis d7fe32d893 D&D: class-balance Phase 3 — off-tier caster lift (druid + sorcerer)
Design steer from user — "relatively easy but not too easy" — narrowed the
target: lift the embarrassing caster trailers on off-tier cells (a casual
player walking into a slightly too-hard dungeon underleveled) without
pushing the already-saturated in-tier ceiling.

Levers:
- Druid passive: was the only chassis with a purely defensive passive
  (5% DR, no offense), and it read it — L1/T1 mean 0.77 (lowest at the
  entry tier), L1/T2 0.04. Added a level + WIS-scaled FlatDmgStart burst,
  same shape as the Phase-2 Bard/Mage/Warlock pass. Kept the DR; no
  DamageBonus rider so high-tier ceilings stay flat.
- Sorcerer passive: burst base 3→5. Sorcerer was second-worst caster
  off-tier (L1/T2 0.10 vs Mage 0.27 pre-tune) despite a comparable stat
  line; the bump pulls it toward arcane-chassis parity.

Observed lifts:
- Druid L1/T1: 0.77 → 0.86 (+9pp) — chassis now functional at its
  intended tier
- L2/T2 cross-class spread: 77pp → 63pp; druid trailer 0.23 → 0.35
- L1/T1 spread: 23pp → 14pp

Off-tier diagnostic: added a focused log to TestClassBalance_Phase1_FullMatrix
that names the trailing class at each off-tier (lvl, tier) cell. Not
asserted — L1 in T2 is *supposed* to be hard, so the diagnostic is for
watching the gap, not the absolute number.

In-tier parity assertion (35pp band on the diagonal) still passes;
TestApplyClassPassives updated for the new druid/sorcerer FlatDmgStart
values; full plugin -short suite clean.
2026-05-14 20:32:41 -07:00
prosolis 76f814c0c9 D&D: class-balance Phase 2 — passive + L5 subclass tuning + in-tier parity assertion
The Phase 1 per-class-mean summary was hiding the truth — most cells are
floor/ceiling-saturated (L10+ pinned at 1.0, L1-4 caster cells at high
tier pinned at 0.0), so means barely budge when you tune passives. Added
a per-(level, tier) cross-class spread diagnostic to the matrix log,
then tuned with the levers from doc §6 in priority order:

1. Class passives (dnd_passives.go) — caster trailers (Bard, Mage,
   Warlock, Sorcerer) gained level + casting-stat-scaled FlatDmgStart
   bursts so the L1-4 chassis isn't a quarterstaff + one weak spell
   against a T2-T3 monster; small DamageBonus riders (Mage/Bard/
   Sorcerer/Rogue +5%, Warlock 10→12%) and +1 attack for Bard/Warlock
   close the steady-DPS gap. Added clampNonNeg so ability-mod-scaled
   additions never go negative on sub-10-stat sheets.

2. Subclass L5 tiers (dnd_subclass_combat.go) — the three Sorcerer L5
   picks (Wild/Storm/Draconic) and Warlock Great Old One were defense-
   only or near-inert pre-tune; each gained a small bite (DamageBonus
   +0.10, or a FlatDmgStart burst for Storm) so the L5 chassis can press
   through a T4 monster.

Parity band locked in TestClassBalance_Phase1_FullMatrix: cross-class
spread ≤ 35pp on the in-tier diagonal — (level, tier) cells where the
level is appropriate for the tier (T1: L1-4, T2: L3-7, T3: L5-10, T4:
L7-15, T5: L10-20). Off-tier cells (L1 mage at T3 dungeon etc.) are
still logged but not asserted: those are level-vs-tier mismatches and
casters at L1-4 can't muscle through a T3 monster on a single L1-slot
spell the way martials muscle through with weapon dice. Worst in-tier
cell after tuning: ~26pp at L3/T2. The 35pp band gives ~9pp Monte-Carlo
headroom over the worst signal at 200 trials/cell.

TestApplyClassPassives expectations updated to match the new passives.
Phase 0 spike still green, full plugin suite (-short) clean.
2026-05-14 20:15:16 -07:00
prosolis ddfa89e7a7 D&D: class-balance Phase 1 — full 10×30 measurement matrix
Generalizes the Phase 0 spike harness to the full build matrix the
class-balance doc plans for. No tuning yet — just measurement.

- classBalanceProfile gains Subclass; buildHarnessCharacter sets it on
  the synthetic DnDCharacter; buildHarnessPlayer now calls
  applySubclassPassives after class+race passives, matching live order
  (combat_bridge.go, combat_session_build.go). Subclass="" is a no-op,
  so L1–L4 pre-unlock rows are unaffected.
- buildPhase1Profiles yields 190 rows: 10 classes × 4 pre-subclass
  levels (L1–L4) + 10 classes × 3 subclasses × 5 post-unlock checkpoints
  (L5/7/10/15/20). Order is registry order so output reads like the
  design doc / !class help.
- TestClassBalance_Phase1_FullMatrix runs the matrix at 200 trials/cell
  (~5.5s) and logs every cell plus a per-class tier-mean summary with
  min/max range. Only harness-broken pathologies fail the test (0% at
  T1 anywhere, or 100% at T5 for an L1 build); per-tier parity bands
  land in Phase 2 once we have data to calibrate the tolerance.

Phase-2 baseline from this run: at T4 the cross-class spread of mean
win rate runs Bard 0.62 → Fighter 0.80 (~18pp); at T5 0.48 → 0.64
(~16pp); casters trail martials at the post-unlock tier (T3) by ~20pp.

Phase 0 test (TestClassBalance_Phase0_FighterVsMage) still green with
identical numbers — the additional applySubclassPassives call is a
no-op for Subclass=="".
2026-05-14 20:00:00 -07:00
prosolis 4dd1ab9f96 D&D: class-balance Phase 0 — Fighter-vs-Mage Monte Carlo spike
Per gogobee_class_balance.md §5 Phase 0: harness skeleton, equipment
and spell-selection policies, Fighter-vs-Mage plausibility run before
Phase 1 generalizes to the full 10 × 30 matrix.

Bypassed in Phase 0 (per doc §2): DB-touching layers (magic items,
armed abilities, pending-cast persistence), subclass passives (none
below L5), and race passives beyond Human +1-all. Everything else
flows through the production combat path.

Initial numbers (400 trials/cell, dungeon T1..T5):

  fighter L1  .998 .805 .165 .020 .000
  fighter L3 1.000 .998 .795 .235 .035
  mage    L1  .880 .190 .003 .000 .000
  mage    L3 1.000 .950 .158 .003 .000

Both classes win at T1 (spell policy is firing — Magic Missile lands
each Mage fight); both collapse by their off-tier — monster scaling
works. The Mage's L1 T2 gap (-60pp vs Fighter) is real data, not a
broken harness. Phase 1 picks up the full matrix from here.
2026-05-14 19:54:01 -07:00
prosolis 0799b6a57b D&D: class-balance pass plan doc (gogobee_class_balance.md)
Phased plan for the class-balance pass. Unlike the race pass, classes
are *measured* not modeled: combat is one-shot auto-resolved through a
seedable simulateCombatWithRNG, so the harness runs Monte Carlo over the
real engine and reads win rates directly.

Scopes the full matrix (10 classes × 30 subclasses × level checkpoints ×
monster tiers), the two hard policies to de-risk in Phase 0 (equipment
loadout, spell selection), the per-tier win-rate parity rule, and the
tuning levers. Phase 0 spike in progress.
2026-05-14 19:47:10 -07:00
prosolis 9d6192dc6a D&D: weighted race-balance pass — tune races to equal effective power
Flat net ability mods aren't equal *effective* power: a +1 in a stat a
build uses beats a +1 in a dump stat. Add dnd_race_balance.go, which
scores each race's mods against a 60/40 blend of per-class combat stat
priorities and class-independent non-combat utility (zone locks,
expedition harvest, skill checks, haggling).

Retune all races so their mean score across playable classes lands
within ±0.5 of the Standard Human baseline (6.0); best-fit/worst-fit
spread is kept as intentional race identity. TestRaceBalance logs the
report and asserts the rule.
2026-05-14 19:47:01 -07:00
prosolis 9f762787f6 D&D: give Human +1 to all stats and rebalance races to +6 net
Human was mechanically blank (all-zero mods, "not yet implemented"
passive). Wire the Standard Human flavor: +1 to every ability score,
no setup-wizard choice so !setup stays uniform across races.

That put Human at +6 net with no downside, ahead of every other race.
Retune the rest to +6 net as well, keeping their negatives intact so
each stays a spiky specialist rather than a flat generalist.
2026-05-14 19:11:19 -07:00
prosolis 0d666beea3 D&D: wire the Open5e magic-item registry into live gameplay
Magic items now reach players through three surfaces: zone loot drops,
Luigi's "Curios" shelf, and combat effects. Effects are formulaic
(Rarity scalar x Kind), mirroring the bestiary tuning pass, with an
empty magicItemEffectOverlay as the hand-authored refinement path.

- magic_items_gameplay.go: rarity index, magic_item_equipped persistence
  (new table, DnDSlot-keyed), codified effect formula, applyMagicItemEffects
  combat hook, potion/scroll -> ConsumableDef bridge, !adventure equip-magic
- dropZoneLoot: 15% magic-item substitution roll by tier rarity
- Luigi's Curios category: daily UTC-seeded 8-item rotation
- combat_bridge / combat_session_build: applyMagicItemEffects after passives
- consumableDefByName falls through so loot/shop potions auto-resolve
- renderDnDSheet: new Magic Items section

Equippable items live entirely in the DnDSlot scheme, separate from the
legacy tier-gear. Attunement items equip inert until attuned (3-slot cap).
2026-05-14 18:38:57 -07:00
prosolis 297ce3d786 D&D: wire monster abilities from SRD traits in the tuning pass
The tuned bestiary previously left every generated entry with a nil
Ability. abilityFromTraits now classifies each creature's SRD trait
names against a priority-ordered rule table, mapping the most
combat-defining trait onto a MonsterAbility effect (death_aoe,
regenerate, spell_resist, evade, enrage, ...). Creatures whose traits
are all non-combat stay nil. 165 of 322 entries get an ability.
2026-05-14 18:00:29 -07:00
prosolis 15cfe065a3 D&D: import Open5e SRD magic items as a vendored reference registry
fetch/gen magicitems subcommands vendor data/open5e/magicitems.json (237
SRD items) and classify them into a generated registry. magic_items.go
holds the MagicItem struct + Kind enum + an init-time overlay merge where
a hand-authored entry wins on ID collision, mirroring the spell and
bestiary imports. Not yet wired into zone loot or the shop — that
integration is a deliberate follow-up.
2026-05-14 18:00:29 -07:00
prosolis 908e2b0855 D&D: codified bestiary tuning pass — derive tuned roster from SRD staging
Adds `gen tuned` to cmd/open5e-import: a deterministic formula that scales
every raw SRD stat block down to an engine-ready DnDMonsterTemplate. HP/AC/
AttackBonus are verbatim SRD (AC clamped to the engine min 10); the Attack
stat is interpolated from attackByCRPoints, a CR→Attack anchor table lifted
from the hand-tuned dndBestiary (CR is the calibration axis the 2026-05-10
rebalance used — raw SRD per-hit damage is ignored). Speed/BlockRate are
coarse baselines from SpeedWalk/AC.

bestiary_tuned.go merges the 322 generated templates into dndBestiary, but
hand-authored roster entries win — the merge only fills IDs the roster does
not already define, so playtested numbers and wired abilities are untouched.

Abilities are deliberately not wired: every generated entry has a nil
Ability, with the SRD multiattack/trait text parked in Notes as raw material
for the follow-up ability-wiring pass.
2026-05-14 16:45:09 -07:00
prosolis 53be17e6fe D&D: import Open5e SRD bestiary as a raw staging table
fetch|gen bestiary subcommands vendor data/open5e/monsters.json (322
SRD monsters) and generate bestiary_srd_data.go — all 322 as raw SRD
stat blocks (HP/AC/ability scores/CR + per-attack damage dice).

This is a balance-baseline reference, not an engine roster: raw SRD
damage one-shots the solo player, so nothing here feeds combat. It's
what the future tuning pass reads against when deriving dndBestiary /
srdProfiles entries. XP is derived from CR (Open5e has no XP field).
2026-05-14 15:28:42 -07:00
prosolis d6ea08bba6 D&D: class passives + subclasses for the five caster classes
Adds signature class passives for Druid/Bard/Sorcerer/Warlock/Paladin in
dnd_passives.go, each riding an existing CombatModifiers channel so a
playable caster is no longer mechanically empty: Druid Wild Resilience
(damage reduction), Bard Bardic Inspiration (initiative), Sorcerer Innate
Sorcery (CHA-scaled pre-combat burst), Warlock Agonizing Blast (+10%
damage), Paladin Divine Smite (level-scaled burst).

Adds 15 subclasses (3 per caster, registry now 30) using canonical 5e
archetype names, with passive-only L5/7/10/15 effects in
applySubclassPassives — consistent with the majority of the original
fifteen, no new resource pools or active abilities. subclassesForClass is
now non-empty for casters, so the L5 !subclass prompt and sheet display
light up through the existing generic plumbing.

Tests: TestApplyClassPassives extended to all ten classes and the new
channels; TestSubclassRegistry_* updated to expect 30 entries across all
ten classes.
2026-05-14 15:18:29 -07:00
prosolis 6ef8b9fd0a D&D: import Open5e SRD spell lists, make the five casters playable
Rebuilds the cmd/open5e-import CLI (fetch/gen × spells) to vendor
data/open5e/spells.json (319 SRD spells) and generate
dnd_spells_srd_data.go (237 after the level>5 filter). mapClasses unions
the API's incomplete structured spell_lists field with the complete
free-text dnd_class field so all eight casters get spells.

dndSpellRegistry loads buildSRDSpellList() first; the hand-authored
buildSpellList() overlays it (hand wins on ID collision). Playable=true
flipped for Druid/Bard/Sorcerer/Warlock/Paladin, each with a
defaultKnownSpells case. TestDefaultKnownSpellsExistInRegistry now covers
all eight classes.

.gitignore: vendor data/open5e/ while keeping the rest of data/ ignored,
and anchor the open5e-import binary pattern so it stops swallowing the
cmd/open5e-import source dir. NOTICE adds CC-BY-4.0 / SRD attribution.
2026-05-14 15:18:18 -07:00
prosolis e6e0009253 D&D: scaffold the five missing caster classes
Add Druid, Bard, Sorcerer, Warlock and Paladin as structural
scaffolding ahead of the Open5e spell-data import. They are wired
through the mechanical layer but gated out of !setup via the new
DnDClassInfo.Playable flag — they have no spell list yet.

- dnd.go: class constants, dndClasses rows, Playable gate, AC floors
- dnd_combat.go: classAttackStatMod, dndClassWeaponBonus,
  classStatPriority arrays for the five
- dnd_spells.go: mageSlots generalized to fullCasterSlots (shared by
  the full casters); Paladin shares rangerSlots; new warlockSlots
  simplified long-rest pool; spellcastingMod + classIsCaster extended
- dnd_setup.go: renderClassMenu and parseClass filter on Playable

Subclasses and spell lists are deferred to later sessions.
2026-05-14 14:43:05 -07:00
prosolisandClaude Opus 4.7 0cd8fd3337 Combat: back the flavor-only monster abilities with real effects
Turn the four placeholder ability effects into working mechanics:
spell_resist halves player spell damage, reveal_action rolls the
player's next swing at disadvantage, fear_immune fizzles control
spells, and ally_buff grants an accumulating enemy attack bonus.
All four are armed by applyAbility, read by the shared resolution
primitives, and round-tripped through CombatStatuses for turn-based
suspend/resume. New branches are guarded by zero-valued state so the
auto-resolve characterization golden is untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:59:51 -07:00
prosolisandClaude Opus 4.7 e629f8fd4d Combat: implement stateful monster ability effects
Slice 3 of the bestiary SRD upgrade: the monster abilities that need
per-fight state (evade, block, advantage, retaliate, regenerate,
survive_at_1, stat_drain, debuff, max_hp_drain). applyAbility arms
combatState flags that the shared resolution primitives read, so both
the auto-resolve and turn-based engines honor them; the turn-based
engine round-trips them through CombatStatuses so a suspended fight
resumes from exact mid-state. New branches are guarded by zero-valued
state so the auto-resolve characterization golden is untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:32:13 -07:00
prosolisandClaude Opus 4.7 f1aa9981f8 Combat: implement immediate-resolution monster ability effects
Wires up the ability effects that resolve fully within applyAbility with
no new persistent state: damage riders (bonus_damage, aoe/aoe_fire/
death_aoe, execute) via the shared calcDamage formula, self_heal, and
flavor-only placeholders for effects still pending per-fight state.
Works in both auto-resolve and the turn engine since both call
applyAbility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:17:11 -07:00
prosolisandClaude Opus 4.7 c5a2634657 Combat: wire pet procs into the turn-based engine
Pet attacks were never resolved in turn-based fights. Roll the proc once
at fight start (a per-round roll would make a proc near-certain over a
long manual fight), persist it on the session so suspend/resume and
reaper auto-play honor the same outcome, and land a single pet hit on
the player's first acting turn.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:08:26 -07:00
prosolisandClaude Opus 4.7 c84682abf7 Combat: guard daily-cron paths against mid-fight combat sessions
A turn-based elite/boss combat session locks the run for up to 1h and
can straddle any scheduled tick. Add hasActiveCombatSession() and consult
it from the morning DM, midnight idle reaper, expedition briefing/recap,
and mid-day random event paths so none of them fire DMs or mutate run
state into a live fight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:01:10 -07:00
prosolisandClaude Opus 4.7 146924818d Combat: TwinBee per-round narration for turn-based fights
Replace the MVP turn-based round renderer with RenderTurnRound, which
reuses the full auto-resolve narrative pools for shared combat events so
TwinBee's voice is identical across both engines. Only the four
turn-specific actions (flee, spell_held, spell_cast, use_consumable) get
new pools.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:53:17 -07:00
prosolisandClaude Opus 4.7 fd28f31de5 Combat: balance-tune SRD multiattack profiles for single-player
Slice 1 shipped raw SRD damage, which is calibrated for a 4-PC party
and one-shots gogobee's single player at every tier. Rescale each
profile's per-attack damage so the round-total lands at ~1.3x the
creature's tuned auto-resolve Attack stat, keeping the SRD structure
(attack count, to-hit, damage split) intact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:44:01 -07:00
prosolisandClaude Opus 4.7 96b291d831 Combat: SRD multiattack profiles + monster abilities in turn engine
Elites/bosses in turn-based fights now swing a full SRD multiattack
profile and fire their special ability — abilities never fired in the
turn-based path before, and every enemy made a single attack.

bestiary_srd.go adds SRDAttack/SRDProfile and a hand-authored registry
for the named bosses and multiattack elites; auto-resolve keeps its
tuned single-Attack blocks untouched. turnAbilityFires remaps the
auto-resolve phase clock onto fight progress for the phase-less duel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:37:29 -07:00
prosolisandClaude Opus 4.7 befb44ef03 Combat: persist fight-scoped one-shots + turn-based buffs
CombatStatuses now mirrors every persistent combatState one-shot —
depleting resources (ward/spore/reflect/autocrit/arcane-ward/heal-
charges), once-per-fight class/race/subclass flags, and accumulated
buff stat deltas. resumeTurnEngine restores them; commit writes them
back in place. Fixes turn-based bugs where Orc rage, Halfling Lucky
reroll, and the Assassin first-attack bonus re-fired every round and
Abjuration Arcane Ward did nothing.

Buff spells and buff-type consumables (ward/atk/def/crit/spore/reflect/
auto-crit) are now usable mid-fight: a flattened-delta model diffs the
reused applySpellBuff/ApplyConsumableMods math against a throwaway
combatant, folds the marginal effect into the session, and re-applies
the persistent stat deltas onto the rebuilt player each round. Pure-
utility spells diff to nothing and are refused before a slot is spent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:12:37 -07:00
prosolisandClaude Opus 4.7 5cd343af0c Combat: per-round !cast / !consume in turn-based fights
Turn-based Elite/Boss fights gain !cast and !consume as player-turn
actions, so casters and item-users make decisions per round instead of
pre-queuing a single effect. The command handler validates and resolves
the spell/item into a pre-rolled turnActionEffect; the engine just
applies the HP deltas and flows on into the enemy turn.

Scoped to effects that resolve within the casting round: damage, heal,
and control spells, plus heal/flat-damage consumables. Buff and utility
spells and buff-type consumables are refused without spending the
resource — they need cross-round stat persistence, a later sub-phase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 06:49:14 -07:00
prosolisandClaude Opus 4.7 a0961fee8a Combat: auto-play timed-out turn-based sessions to a real win/loss
A combat session abandoned past its 1h TTL is now resumed from persisted
mid-state and auto-played through the shared resolver to a real win or
loss, rather than flatly marked as a retreat. The bulk-UPDATE sweep is
replaced by listExpiredCombatSessions plus a reaper that locks the user,
auto-plays the fight, runs the normal close-out, and DMs the outcome.
markCombatSessionExpired remains the terminal fallback for sessions that
can't be reconstructed. Wired into eventTicker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 06:30:52 -07:00
prosolisandClaude Opus 4.7 886eb5a75b Combat: wire turn-based elite/boss fights to the command surface
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>
2026-05-14 06:16:45 -07:00
prosolisandClaude Opus 4.7 269d13ffe8 Combat: add turn-based session accessors and round-loop state machine
combat_session.go: CombatSession persistence layer mirroring the
dnd_expedition pattern — CRUD accessors, one-active-per-user enforcement,
and the timeout reaper (sweeps stale sessions to 'expired').

combat_turn_engine.go: the player_turn -> enemy_turn -> round_end -> over
state machine. advanceCombatSession seeds a deterministic per-(round,phase)
RNG, resolves one phase via the shared attack primitives, commits, and
persists. The deferred poison/status tick lands in round_end now that the
round-loop shape exists.

CombatStatuses persists only between-round monster-ability effects; the
reaper marks sessions 'expired' rather than auto-playing them — both gaps
depend on Combatant reconstruction, which lands with the command-wiring PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 05:55:32 -07:00
prosolisandClaude Opus 4.7 b0987eb229 Combat: add combat_session schema for turn-based fights
Schema-only: persistent per-fight session table so manual elite/boss
combat can resume or be auto-finished by the timeout reaper from exact
mid-state. State machine and accessors land in a later PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 00:02:09 -07:00
prosolisandClaude Opus 4.7 1e1fbf9056 Combat: extract shared attack-resolution primitives
Pull effectiveAttackBonus, attackCritFloor, attackConnects, and the
post-hit player damage stack out of combat_engine.go into
combat_primitives.go so the upcoming turn-based engine resolves an
attack identically to auto-resolve. Behavior-preserving — the combat
characterization golden file is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:05:33 -07:00
prosolisandClaude Opus 4.7 6141964c58 Combat: characterization test pinning the auto-resolve event stream
Splits SimulateCombat into a thin wrapper over simulateCombatWithRNG, a
deterministic core that accepts an optional *rand.Rand. Production passes
nil (package-global rand, behaviorally identical); the new test seeds it
per scenario.

Adds TestCombatCharacterization: 23 curated scenarios x 5 seeds, with the
full event stream + result summary serialized to a stable text form and
diffed against a golden file. This locks current auto-resolve behavior
before the shared-primitives extraction for the turn-based engine — any
perturbation fails loudly and forces a deliberate -update + diff review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:00:35 -07:00
prosolisandClaude Opus 4.7 0ca17628d4 WIP: DM window tiers, expedition activity dedup, harvest gating
- Split advDMResponseWindow into a 12h low-priority tier for passive
  overnight-tolerant prompts (pet arrival/type/name); bump pet arrival
  chance 15% -> 30%.
- Skip zone_run activity lines for runs that belong to an active
  expedition — the expedition rollup is the source of truth.
- Loosen currentRoomCleared: Entry/Exploration/Trap allow harvest
  unconditionally (risk is the combat-interrupt roll); Elite/Boss stay
  gated on RoomsCleared.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:53:58 -07:00
prosolisandClaude Opus 4.7 4af110cc20 Combat: inject per-session RNG into the engine
Adds an optional *rand.Rand to combatState so a fight can be driven by a
deterministic source. Auto-resolve leaves it nil and falls through to the
package global in the same call order — behaviorally identical. Threads
the source through calcDamage and rollWeaponDamage.

Groundwork for the turn-based elite/boss engine and its timeout reaper,
which seed the rng per combat_session to make fights resumable and
replayable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:53:06 -07:00
prosolisandClaude Opus 4.7 4e412219f3 WIP: in-flight combat/expedition/flavor changes
Bundle of uncommitted working-tree edits across combat engine, expedition
cycle, flavor pools, and TwinBee/zone narration. Includes new files:
combat_debug.go, dnd_boss_consumables.go, dnd_dex_floor.go, plus
CHANGES_24H.md and REBALANCE_NOTES.md scratch notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:59:19 -07:00
prosolisandClaude Opus 4.7 2b86f3df16 Expedition: skip idle reaper + legacy morning DM while on expedition
The midnight idle reaper inspected only the overworld CombatActionsUsed/
HarvestActionsUsed counters, so a player on an active expedition was
flagged idle — streak halved and a nonsense overworld shame DM sent. The
08:00 morning DM had the same gap and stomped on the 06:00 expedition
briefing. Both paths now early-out when getActiveExpedition is non-nil
(advancing the streak in the midnight branch).

Includes a one-shot bootstrap that doubles CurrentStreak (capped at
BestStreak, +1 when room allows to recover the integer-divide odd half)
for characters still flagged StreakDecayed with an active expedition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:54:40 -07:00
prosolisandClaude Opus 4.7 9ce82f7c67 Combat: round half-up on HP wound persistence to fix 101→100 drift
Wound carry-over went through three integer truncations across the
legacy combat scale (123 max) and the dndChar scale (78 max). 101/123
persisted as int(64.04)=64, then restored as int(100.92)=100 — losing
0.92 HP every fight.

Switching both sides to math.Round means most HPs round-trip exactly;
the residual error is bounded by ±1 HP from the inherent scale mismatch
(1 dndChar HP ≈ 1.58 legacy HP, so some pairs collide on the smaller
scale). Acceptable; the prior always-truncate behavior was directionally
biased against the player.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 22:20:24 -07:00
prosolisandClaude Opus 4.7 c5217e9ecf Combat: fix wounded-carry HP display + auto-crit narration
Two bugs reported back-to-back from a Rogue's run:

1. HP display reset between battles. End of fight 1: 101/123 → start of
   fight 2: 100/100. applyDnDHPScaling was overwriting Stats.MaxHP with
   the scaled wound value, so the display denominator dropped along with
   the numerator. Wounded carry-over became invisible.

   Fix: introduce CombatStats.StartHP. Combat engine reads it as the
   entry-HP when set; MaxHP stays put. Display now reads "100/123" as
   intended.

2. Auto-crit fired on a roll of 11 with no narrative tell. Rogue passive
   AutoCritFirst was triggering correctly, but the renderer used the
   generic crit pool, so the player saw "🎲 11 vs AC 10 → CRIT" with no
   indication their *class* caused it.

   Fix: tag the crit event with Desc="auto_crit" when the passive (not
   the dice) caused it; new narrativePlayerAutoCrit pool calls out the
   training/instinct/exploit theme.

Test bound for T5 dungeon death rate loosened from 0.02 to 0.01 — the
new Sudden Death phase from the previous commit shifted geared-T5
fights slightly toward player wins.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 22:17:14 -07:00
prosolisandClaude Opus 4.7 de02907e69 Combat: add Sudden Death phase + fix tiebreak to use absolute HP
Players reported the prior 6-round exhaustion timeout felt arbitrary
when both sides still had plenty of HP. Two fixes:

- Add a "Sudden Death" 4th phase (3-4 extra rounds) so most fights
  resolve naturally before any timeout. Total combat length now caps
  at ~10 rounds.
- When the timeout does fire, tiebreak by absolute HP instead of HP%.
  The %-based logic was unintuitive — a player at 88/123 (71%) would
  lose to a boss at 83/97 (86%) despite having more HP remaining.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 21:58:26 -07:00
prosolisandClaude Opus 4.7 3cf83e370e Euro: add !eurogrant admin command
Credits an arbitrary amount to any user, logged with the granting admin's
ID (and optional free-form note) for audit. Reuses Credit so balance
bootstrap and transaction history stay consistent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 21:37:29 -07:00
prosolis 34461030dc Merge branch 'feat/hospital-bill-flavor' into main 2026-05-09 21:32:47 -07:00
prosolis 86c6736a8b Merge branch 'feat/space-inviter' into main 2026-05-09 21:32:24 -07:00
prosolisandClaude Opus 4.7 97e4a502c9 Hospital: fix preposition in USA-standards descriptor (for→by)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:48:56 -07:00
prosolisandClaude Opus 4.7 b8e8f994d8 Hospital: swap "a lot" descriptor for USA-standards quip
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:48:36 -07:00
prosolisandClaude Opus 4.7 374d5ea374 Hospital: vary bill descriptor in discharge announcement
Replace the single "a lot" descriptor with a pool of 8 variants picked
via advPickFlavor (per-user dedup, last 5 indices avoided).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:46:04 -07:00
prosolisandClaude Opus 4.7 8e8d3783f9 Space inviter: DM admin to confirm invites for Space-less local users
New optional plugin (FEATURE_SPACE_INVITER) that detects local homeserver
users who aren't in any Matrix Space and prompts the configured admin via
DM with yes/no/ignore. Persistent prompt log in space_inviter_prompts so
restarts and upgrades don't re-ping for users who already replied.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:20:56 -07:00
prosolisandClaude Opus 4.7 2454f5f865 Hospital: cut revival to 5k/level + add CHA haggle roll
Drops the after-insurance revival cost from level × 50k to level × 5k
(before-insurance still 5×). On check-in, Nurse Joy now lets the player
attempt a CHA save vs DC 15:
  - nat 1  → auto-fail, full price
  - <  15  → fail, full price
  - >= 15  → pass, bill halved
  - nat 20 → bill fully waived (no debit, no community-pot cut)
Outcome is pinned on the pending interaction (Discounted/Waived) so the
TOCTOU recompute at confirm preserves the discount/waiver and can't be
re-rolled. Bill DM reordered: itemized → after-insurance (pre-haggle) →
🎲 roll line + Nurse Joy reaction → final amount with strikethrough on
the pre-haggle figure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:26:06 -07:00
prosolisandClaude Opus 4.7 7ec78f3cd9 Branching zones G9c: drop legacy columns + add purge gate
Final G9 step. The schema no longer creates current_room or
room_seq_json on fresh installs, and existing prod schemas drop them
via purgeLegacyZoneRunColumns() once the operator flips
GOGOBEE_BRANCHING_PURGE=1.

- CREATE TABLE dnd_zone_run loses both columns; comment updated to
  point at G1's columnMigrations for the graph-mode columns.
- cleanup_g9.go mirrors the L5 idempotent-purge shape: ALTER TABLE
  DROP COLUMN per legacy column, "no such column" treated as already
  done, default-off.
- Wired into adventure Init alongside purgeLegacyAdvCharacterTable.
- Plan doc marks G9 complete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:23:08 -07:00
prosolisandClaude Opus 4.7 a5c0a83e2a Branching zones G9b: stop persisting current_room / room_seq_json
The two legacy columns are no longer needed at runtime. CurrentRoom is
derived from len(VisitedNodes)-1 on read (lockstep with what the
dual-write era stored), and RoomSeq is a transient in-memory field
populated only when a fresh run is started.

- INSERT, SELECT, and UPDATE statements drop the two columns. The
  schema still carries them — they retire in G9c.
- scanZoneRun derives CurrentRoom and falls back to a linear-derived
  CurrentNode only if a row predates the G4 dual-write deploy.
- markRoomCleared rewritten to walk the registered graph (first
  outgoing edge / dead-end completion). Tests that use it as an
  end-to-end run driver continue to pass.
- advanceZoneRunNode drops its current_room dual-write.
- adventure_activity_unified reads visited_nodes for the daily
  "X/N rooms" progress fragment.
- Camp boss-room test pivots to setting current_node directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:21:33 -07:00
prosolisandClaude Opus 4.7 103cf30987 Branching zones G9a: remove GOGOBEE_BRANCHING_ZONES gate
All 9 zones have hand-authored graphs (G8a–G8i), so the POC gate has
served its purpose. Make graph mode the only runtime path:

- Drop branchingZonesEnabled() and the os import in zone_graph_nav.go.
- Inline the gate-on branches in zoneCmdMap, zoneCmdAdvance,
  CurrentRoomType, startZoneRun, and the expedition map renderer.
- Keep the legacy linear functions (markRoomCleared, renderZoneMap,
  generateRoomSequence) standing — their tests still exercise them and
  they'll retire alongside current_room / room_seq_json in G9b.
- Strip gate test (TestBranchingZonesGate, TestCurrentRoomType_GateOff)
  and t.Setenv calls from the surviving graph tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:17:19 -07:00
prosolisandClaude Opus 4.7 304ad27c0f Branching zones G8i: Underdark convergent triangle (multi-region)
T4 zone, ninth (final) authored graph. Shape: 3-way regional fork
where each arm crosses a different region boundary, all converging at
R4 (deep_throne). First and only authored zone where every node
carries a non-empty RegionID matching dnd_expedition_region.go.

Three arms from fork1:
  - R1→R2 drow_patrol → drow_captain (elite)
  - R1→R3 psionic_corridor → mind_flayer (elite)
  - R1→R1 deep_chasm (CON-gated harvest, stays in surface_tunnels)
…all converging at R4 throne_approach → boss.

Exercises G6 fireGraphRegionTransition end-to-end: edges crossing
region boundaries fire the transition hook (updates Expedition
.CurrentRegion, marks region visited, writes a transit log entry)
without burning supplies or advancing the day, since the graph is
the run state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:03:21 -07:00
prosolisandClaude Opus 4.7 122a3d7d5d Branching zones G8h: Abyss Portal three sequential forks
T5 zone, eighth authored graph. Shape: three fork nodes in series —
binary, binary, ternary capstone. Player makes three separate fork
decisions in this zone, more than any other shipping zone, matching
the "things keep getting worse" thematic descent.

First zone with LockStatCheck CON (mind_corridor, DC 16, psychic
pressure flavor) — completes the full ability roster (STR/DEX/CON/
INT/WIS/CHA) across shipping zones by G8h. reality_seam secret
LootBias 3.0, the highest authored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:00:57 -07:00
prosolisandClaude Opus 4.7 3a8f3c26d6 Branching zones G8g: Dragon's Lair converging mid-fork + capstone 3-way
T5 zone, seventh authored graph. Shape: long approach (kobold_warrens
→ drake_pens) → binary mid-fork that converges at the elite
(wyrmlings_nest) → 3-way capstone fork into the boss
(direct / CHA bargain / Perception-found hoard_pillar secret).

First zone combining two distinct fork stages where the first converges
diamond-style and the second spreads triple-wide. Secret carries
LootBias 2.5 — highest of any shipping zone, reserved for T5 — locked
in by TestDragonsLairGraph_LootBiasEscalation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:59:32 -07:00
prosolisandClaude Opus 4.7 dcef2ba734 Branching zones G8f: Feywild Crossing woven forks
T4 zone, sixth authored graph. Shape: woven forks — two first-stage
forks each lead to a second-stage fork; hag_circle is shared between
both first-stage paths while time_eddy (grove-only) and
illusion_garden (marsh-only, secret) are exclusive to one side.
Captures the Feywild theme of "the path you walked is not the path
you arrived on."

First authored zone with LockStatCheck CHA, completing the stat
roster (CHA joins STR/DEX/INT/WIS in shipping zones). Also the first
zone where fork1 has no LockNone option — both first-stage edges are
locked (CHA DC 14 vs. Perception DC 14), forcing the player to commit
to whichever check they're stronger at.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:57:48 -07:00
prosolisandClaude Opus 4.7 dd97a00332 Branching zones G8e: Underforge pre-boss gauntlet (late triple fork)
T3 zone, fifth authored graph. Shape: five-node linear preamble
(sealed_gate → forge_descent → cooling_river → magma_chamber elite)
followed by a single 3-way antechamber fork before the boss.

Bends the plan §G8 "two forks for T2+" guideline to a single late
fork-with-three-options because the gauntlet shape is the design
intent — Kharak Dûn is a one-way descent. Triple-option antechamber
(direct / DEX-checked catwalks / Perception-found forge_vault secret)
preserves agency at the commitment moment.

Map renders as a long horizontal stem with a 3-leaf fan at the right
edge — visually distinct from any earlier zone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:56:07 -07:00
prosolisandClaude Opus 4.7 737702ea20 Branching zones G8d: Manor Blackspire stacked 3-way forks (hub style)
T3 zone, fourth authored graph. Shape: two stacked 3-way forks
(great_hall, upper_hall). The player picks twice in a row from a
wide menu — distinct from the binary forks in G8a/b and the parallel
Y-trees in G8c.

Completes lock-kind coverage by G8d: this is the first authored use of
LockLevelMin (tower_observatory gated at L7). With Perception (DC 14
study, DC 15 hidden_oratory), StatCheck INT (library), and LockNone,
all four non-trivial lock kinds now appear in shipping zones.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:54:45 -07:00
prosolisandClaude Opus 4.7 675d70e85b Branching zones G8c: Sunken Temple sequential forks (no mid-path merge)
T2 zone, third authored graph. Shape: two-tier sequential forks where
each path is unique up to the boss room. Four committed leaves
(kuo_toa_pen / glyph_chamber / aboleth_thralls / coral_reliquary) all
terminate at the boss; no mid-path node has >1 incoming edge.

Differentiates from Crypt's diamond and Forest's asymmetric-diamond by
producing two parallel "Y" subtrees on the !zone map instead of a
converging branch. Exercises Perception + StatCheck (STR) + LockNone
in a single zone (covered by TestSunkenTempleGraph_LockKindCoverage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:52:52 -07:00
prosolisandClaude Opus 4.7 ea59b4e61c Branching zones G8b: Forest of Shadows asymmetric fork + stat-check secret
T2 zone, second authored graph. Deliberately diverges from the diamond
shape so the !zone map renders distinctly:

- Asymmetric main fork: long branch (grove_descent → dryad_circle elite,
  2 mid-nodes) vs. short branch (thorn_tunnel, 1 mid-node, Perception
  DC 13 hint). Tests lock in the asymmetry.
- Pre-boss fork to a sapling_shrine secret behind LockStatCheck WIS
  DC 14 (LootBias 2.0). First authored zone exercising stat_check —
  Crypt's secret was Perception, so this validates the second lock kind
  end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:51:19 -07:00
prosolisandClaude Opus 4.7 9312ef5275 Branching zones G8a: Goblin Warrens diamond graph
T1 single-fork shape with no secret — deliberately the simplest
authored topology, mirroring the Crypt of Valdris pattern minus the
secret tail. Serves as the baseline reference shape; G8b onward will
vary topology (stub branch, sequential forks, hub-and-spoke, gauntlet,
convergent triangle) so zones feel structurally distinct on the
!zone map.

Validator-checked at init via BuildGraph. Both branches reach boss;
the Perception (DC 12) side carries a player-facing hint per plan §G8
guideline ("locked paths should always have a hint").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:48:48 -07:00
prosolis b3d3db9142 Branching zones G7: collapse empty columns in !zone map
The graph-mode map renderer iterated 0..maxX, emitting a blank slot for
columns whose only inhabitant was a hidden secret node. In Matrix code
blocks the dark background painted that gap as a phantom cell with no
status mark — read in playtest as "a box missing a dot." Collapse
columns with no visible nodes before laying out rows; secret columns
reappear once the player visits them.
2026-05-09 16:41:46 -07:00
prosolisandClaude Opus 4.7 90ff7666bd Branching zones G7: Crypt of Valdris POC graph
Hand-authored zoneCryptValdrisGraph() (8 nodes incl. fork +
Perception DC 15 secret reliquary) self-registers at init. Two
surgical bridges so new runs traverse the authored graph when
GOGOBEE_BRANCHING_ZONES=1: startZoneRun seeds current_node from
g.Entry, and DungeonRun.CurrentRoomType resolves via the live
node's kind so divergent paths (secret_chamber) dispatch through
the right resolver. Gate off is bit-identical to pre-G7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:28:58 -07:00
prosolisandClaude Opus 4.7 893d3dacad Branching zones G6: dependent surfaces re-keyed on graph nodes
- 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>
2026-05-09 15:17:30 -07:00
prosolisandClaude Opus 4.7 2d249d7d0a Branching zones G5: navigation surface behind GOGOBEE_BRANCHING_ZONES=1
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>
2026-05-09 14:49:22 -07:00
prosolisandClaude Opus 4.7 a413c92844 Branching zones G1–G4: schema, graph types, legacy compiler, run-state dual-write
G1 — schema. Adds zone_node + zone_edge tables and three columns to
dnd_zone_run (current_node, visited_nodes, node_choices). Linear
columns stay during the migration; G9 retires them.

G2 — types + validator. New internal/plugin/zone_graph.go defines
ZoneNode/ZoneEdge/ZoneGraph + ZoneNodeKind/ZoneEdgeLockKind. BuildGraph
enforces: exactly one entry, exactly one boss, boss reachable via BFS,
no orphan nodes, no self-loops without explicit opt-in. BuildLinearGraph
synthesizes a chain for legacy zones.

G3 — legacy compiler + dual-mode loader. compileLegacyZoneGraph turns
a ZoneDefinition into a representative linear graph (MaxRooms shape).
loadZoneGraph returns the registered graph if hand-authored (G7+),
else the legacy fallback. compileRunGraph mirrors a per-run RoomSeq
exactly for hot-swap derivations.

G4 — run-state dual-write. DungeonRun gains CurrentNode / VisitedNodes
/ NodeChoices. scanZoneRun reads them and hot-swaps current_node from
current_room when a row predates the migration (deriveLegacyNodeID
matches BuildLinearGraph's "<zone>.r<n>" scheme). startZoneRun and
markRoomCleared write both columns.

No behavior change yet — navigation surface (forks, locked edges,
!zone go) lands in G5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:34:05 -07:00
prosolisandClaude Opus 4.7 57b6e009ec Branching zones: write Phase G plan (Option A — greenfield zone graph)
Full implementation plan for replacing the linear room_seq_json model
with a directed-graph zone topology. Enables Slay-the-Spire-style
forks, perception-gated secret rooms, locked shortcuts, multiple
paths converging on a shared boss.

Plan covers:
  - Schema (new zone_node + zone_edge tables; current_node /
    visited_nodes / node_choices columns on dnd_zone_run)
  - Go-side types (ZoneGraph, ZoneNode, ZoneEdge, lock kinds)
  - Authoring model with worked example (Crypt of Valdris POC)
  - Nine-phase rollout (G1 schema → G2 types → G3 legacy compiler →
    G4 dual-write run state → G5 navigation behind env gate → G6
    dependent surfaces → G7 POC zone → G8 mass migration → G9
    retire linear model)
  - Risk inventory + mitigations
  - Surface inventory of files likely to change
  - Open authoring questions with proposed defaults

Not yet implemented — handed off to a fresh session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:23 -07:00
prosolisandClaude Opus 4.7 35fa2b8323 Adv 2.0 daily report: migrate to D&D-native render + unified activity source
Two longstanding warts surfaced when the user inspected today's TwinBee
daily report:

1. Active players were rendering "Acted today — no log recorded." The
   adventure_activity_log path only captures !rest now; harvest/zone/
   expedition action logging was lost during Phase R/L migration. The
   stale HasActedToday() check fired on fossil harvest_actions_used
   counters that nothing modern increments.

2. Per-player headline still rendered legacy four-skill line ("Combat
   Lv.X | Mining Lv.Y | Forage Lv.Z | Fishing Lv.W"). No D&D level,
   race, class, or HP — discoverability gap for the layer that's now
   canonical.

Fix both:

- adventure_activity_unified.go: new loadAdvDailyActivity(date) unions
  dnd_zone_run (zone runs touched today) + dnd_expedition_log
  (expedition activity rolled up per expedition) + legacy
  adventure_activity_log (mostly !rest). One source of truth.

- adventure_render.go: AdvPlayerDaySummary gains HasDnDChar/DnDLevel/
  DnDRace/DnDClass/HPCurrent/HPMax. New advPlayerHeadline helper
  renders "Lv.N Race Class — HP cur/max" when a dnd_character row
  exists, falls back to the legacy four-skill line plus an inline
  "(no D&D sheet — run !setup)" nudge for accounts that haven't
  completed setup yet. titleizeDnDToken("half_elf") → "Half-Elf".

- adventure_scheduler.go: postDailySummary swaps loadAdvLogsForDate
  for loadAdvDailyActivity. IsResting now derives from "len(acts)==0"
  rather than the fossil HasActedToday() counter check, so dead
  harvest_actions_used bytes can't trip the active-player branch.

- proddb_daily_report_test.go: renders the report against a copy of
  data/gogobee.db and prints it for visual confirmation. Verified
  output for 2026-05-09 shows real zone-run outcomes (Withdrew from
  Sunken Temple at room 1/6, etc.) instead of the previous "no log
  recorded" placeholder.

go vet + go test ./... clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:23 -07:00
prosolisandClaude Opus 4.7 c8849fd819 Adv 2.0 L5 close-out fix: bootstrap player_meta from legacy on Init
The L5 close-out (596b2b7) rewired loadAdvCharacter to read from
player_meta and simultaneously deleted the 13 backfill one-shots from
Init. That's only safe if every account already has a player_meta row
from prior dual-writes — fresh restores from old backups, dev
environments cloned from older snapshots, or a prod upgrade that
skipped the soak window all silently strand every character (loader
returns empty).

Adds bootstrapPlayerMetaFromLegacy: a single consolidated migrator that
walks adventure_characters, finds rows missing from player_meta, and
fans them out via the audited upsertAllPlayerMetaFromAdvChar helper.
Idempotent (re-runs touch zero rows once migrated; no-ops post-purge).
Wired into AdventurePlugin.Init before loadAllAdvCharacters.

proddb_simulation_test.go exercises the full bootstrap + fan-out +
overlay round-trip against a copy of data/gogobee.db with 11 subtests
covering every migrated subsystem (skills, arena, babysit, streak,
death/revive, NPC counters, pet incl. JSON-packed flags, house, save
idempotency, last_active advance, fresh createAdvCharacter).

dnd_proddb_integration_test.go also calls the bootstrap explicitly to
mirror prod startup ordering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:23 -07:00
prosolisandClaude Opus 4.7 9d6e8f6bf2 Adv 2.0: fork !expedition list off zoneCmdList
!expedition list was routing to zoneCmdList, which rendered "Zones
available at L%d" with `!zone enter <id>` CTAs and referenced an active
zone run for the footer — wrong system for an expedition caller. New
expeditionCmdList mirrors the zone-list shape but uses expedition CTAs
(`!expedition start <id>`) and surfaces the active expedition (day
count, `!expedition status` / `!expedition abandon`) via
getActiveExpedition. zoneCmdList stays for !zone list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:23 -07:00
prosolisandClaude Opus 4.7 5d98e5684a Adv 2.0 post-L5 audit cleanup: drop redundant dual-writes + refresh stale comments
Audit confirmed saveAdvCharacter's fan-out (upsertAllPlayerMetaFromAdvChar)
covers all 13 migrated subsystems, so the per-call-site upsertPlayerMetaXxx
calls retained "as defense" through the L4-L5 migration are redundant
post-L5h. Removed 16 such call sites across arena (2), scheduler (4),
hospital (1), consumables (2), rival (1), housing (11), and reordered
masterwork to drop a now-pointless explicit upsert before the load+save.

Refreshed nine stale doc comments referencing the legacy
adventure_characters table or "soak window / fallback" language that no
longer applies after the L5 close-out (commit 596b2b7). No behavior
change; go vet + go test ./... pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:23 -07:00
prosolisandClaude Opus 4.7 c0f03a41b5 Adv 2.0 L5 close-out: loader rewire + adventure_characters purge gate
Rewires loadAdvCharacter / createAdvCharacter to source from player_meta,
drops every legacy-table fallback in the loadXxxState helpers, and adds a
GOGOBEE_LEGACY_PURGE=1 gate to drop the now-cold adventure_characters
table. Schema CREATE removed and column migrations tolerate the missing
table so the purge stays sticky across reboots.

§7.4 of the migration plan is reconciled to document that
player_meta.combat_level stays — combat_stats.go consumes it via the
overlay and is shared infrastructure, not legacy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:23 -07:00
prosolisandClaude Opus 4.7 36e14c6084 Adv 2.0 L5 close-out: §7.4 reconciled + retire 3 CombatLevel readers
Reconcile the migration plan with what actually shipped, and clear the
three remaining CombatLevel reader sites so the §7 grep gates hold.

Plan reconciliation (gogobee_legacy_migration.md §7.4):
The original deletion list was wrong on two counts. (1) AdventureCharacter
struct survives as the loaded-view shape post-L5h overlay; only the
backing table drops at purge. (2) combat_engine.go / combat_bridge.go /
combat_stats.go are not legacy — they're the live combat engine the D&D
system layers on top of (applyDnDPlayerLayer/EquipmentLayer/etc. mutate
CombatStats before SimulateCombat). Verified via grep: 21+ files reference
these symbols across arena/dungeon/zone/expedition. §7.4 rewritten to
reflect this; no L6 combat-engine migration needed.

Reader fixes:
- babysitDailyCost reframed for D&D-level scale. Formula 100 + level*100
  preserves the curve at every old-CL boundary given the 5:1 compression
  in dndLevelFromCombatLevel (Level 4 ≈ old CL 20 = €500/day, Level 10 ≈
  old CL 50 = €1100/day). Caller switched to dndLevelForUser.
- dnd_sheet.go drops the vestigial "Combat (legacy) %d" stat-block line.
- D&D onboarding retired. Post-L5g the welcome DM never fires (every
  legacy player already has a D&D row), so dnd_onboarding.go +
  dnd_onboarding_test.go are deleted, the 3 production caller invocations
  (dnd_zone_combat, combat_bridge, dnd_combat::ensureCharForDnDCmd) and
  the dnd_setup.go stub-creation branch removed. OnboardingSent column
  kept for a separate cleanup pass. dnd_audit_fixes_test.go ported.

go vet ./... && go test ./... clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:23 -07:00
prosolisandClaude Opus 4.7 1c7939c3c8 Adv 2.0 L5h: cold AdvCharacter table — saveAdvCharacter fan-outs to player_meta
saveAdvCharacter no longer writes the legacy adventure_characters row.
It's now a thin fan-out: bumps LastActiveAt and routes through
upsertAllPlayerMetaFromAdvChar, which calls the per-subsystem upserts
for display_name, hospital, arena, masterwork, rival, skills, babysit,
NPC, lifecycle, death, misc, pet, and house.

loadAdvCharacter calls applyPlayerMetaOverlay at the tail to re-source
every migrated subsystem from player_meta. The adventure_characters
SELECT still happens (for the user_id / equipment-table linkage and as
a fallback substrate) but its values are overwritten by the overlay.
This achieves the L4e in-place housing reader flip automatically —
adventure_housing.go's char.HouseFoo reads now see player_meta values
without per-site changes.

npcMidnightReset extended to write player_meta alongside the frozen
adventure_characters.

Per-call-site upsertPlayerMetaXxx duals are now redundant with the
saveAdvCharacter fan-out but kept as defense; cleanup deferred.
player_meta.combat_level column drop deferred — still read by babysit
cost / combat_stats / activities via the overlay; sequenced with the
final combat_engine teardown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:23 -07:00