Commit Graph

434 Commits

Author SHA1 Message Date
prosolis
dcc039b7d5 Close expedition/run seam on combat loss & flee
Run-loss paths (turn-based elite/boss death + flee, exploration combat
death/retreat, interrupt/patrol death) abandoned the zone run but left
the wrapping expedition row at status='active', so the ambient ticker
kept DMing about a dungeon the player had walked away from.

Adds forceExtractExpeditionForRunLoss helper and wires it into every
run-loss site; ambient ticker also skips when the expedition's run is
no longer active as a safety net.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:48:52 -07:00
prosolis
98ba416359 Class identity audit close-out: Fighter/Ranger/Paladin/Druid + Bard-Valor
Phase 2 Monte Carlo tuning had papered over four classes whose 5e-defining
mechanics never actually existed in the engine — only flat DamageBonus or
FlatDmgStart compensation riders that hit the right aggregate win rate.
This pass replaces the proxies with the real primitives.

New CombatModifiers fields: ExtraAttacks (additional swings/round, looped
in a new resolvePlayerSwings helper); HuntersMarkDie (per-hit Nd6 bonus,
same path as Sneak Attack); ThornLashDmg (flat counter on landed enemy
hits, fires in resolveEnemyAttack after ward/block).

Fighter L5/11/20 Extra Attack, Ranger L5 Extra Attack + Hunter's Mark
(1→4 d6 by level), Paladin L5 Extra Attack + per-hit Divine Smite
(replaces one-shot opener), Druid thorn lash. Bard College of Valor L7
"Extra Attack" subclass tier converted from +1 attack/+10% damage proxy
to the real primitive.

Post-fix T5 class-balance: martials cluster at top (Fighter/Rogue 0.90,
Ranger 0.89, Paladin 0.88), casters keep their relative ordering, spread
14pp top-to-bottom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 12:53:00 -07:00
prosolis
41adfce9f1 Expedition autopilot Phase 4 + rogue sneak attack + TwinBee voice sweep
Tedium-removal pass driven by live play feedback. Three big threads:

Expedition autopilot Phase 4 — background auto-run + harvest-until-dry:
- New expeditionAutoRunTicker walks active expeditions every 15min
  (5min tick, per-expedition CAS on new last_autorun_at column). Skips
  combat sessions, briefing/recap quiet windows, expeditions <30min old.
- Walks up to 3 rooms/tick with compact narration; suppresses DMs when
  0 rooms walked or just hitting the per-tick cap (no "stretch complete"
  filler). Player only hears from the bot when a real decision is needed.
- Compact mode: one-line combat narration for trash/elite, auto-resolves
  elite doorways via the forward-sim engine (boss still pauses). Threaded
  via new advanceOnceWithOpts → resolveRoom → resolveCombatRoom.
- Auto-harvest now grinds each Common/Uncommon node until dry (cap at 8
  attempts/visit), mirroring manual !scavenge retries. Rare+ still pauses.
- Ambient ticker: anti-repeat by Kind via new last_ambient_kind column
  (avoids two pack_rat DMs in a row when the pool only has 6 lines).
- !expedition go <n> now routes to the fork-choice handler when active +
  numeric; fork footer rewritten to suggest !expedition go instead of
  !zone go. Boss/Elite doorway: formatNextRoomMessage routes the action
  hint by next room type and autopilot loop breaks via new nextRoomType
  field so "Room X/Y — Boss" doesn't double-print with contradictory
  hints (!zone advance vs !fight).
- runAutopilotWalk extracted from expeditionCmdRun so foreground and
  background share the loop body.

Rogue Sneak Attack actually exists now:
- Audit found the rogue's "Sneak Attack" passive was AutoCritFirst +
  5% damage rider. No Nd6, ever. Phase 2 Monte Carlo masked this
  with a small flat buff; the class's defining mechanic never matched
  its tooltip or 5e identity.
- Added SneakAttackDie int to CombatModifiers, per-hit Nd6 in
  combat_primitives.go (same lane as DivineStrikePerHit). Scales with
  level: 1d6 at L1-2 ... 4d6 at L7-8 ... capped at 10d6 at L19-20.
- AutoCritFirst + 5% rider retained as bonuses on top of working sneak
  attack — preserves the opener-burst feel.
- L7 rogue expected per-hit ~8 → ~22 damage. Valdris fight math goes
  from "16 rounds to kill, die in 8" to winnable.

TwinBee voice sweep (Phase B3):
- ~530 line changes across 24 files replacing third-person "TwinBee
  notes/files/tracks/respects/..." constructions with first-person
  inside flavor string literals. Code identifiers (TwinBeeLine,
  twinBeeLine, etc.), chat-prefix labels (🎭 **TwinBee:**), item names
  (TwinBee's Bell), achievements, and game-title references in
  fun.go (Konami TwinBee ship) left intact.
- Catches a real bug: Valdris fight rendered "TwinBee marks the
  Legendary Resistance" mid-combat in third person, contradicting
  the Phase B2 convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 12:40:17 -07:00
prosolis
f424300d87 Ship-audit P0/P1 sweep on open5e: prose, combat math, UX hygiene
P0 — actual bugs:
- eldritch_blast: SRD-imported "Whatever" placeholder leaked to players;
  overlay added in dnd_spells_data.go.
- Battle Master disarming_attack + parry: math.Max on multiplicative
  DamageReduct was a no-op vs the 1.0 default. Switched to *= and added
  regression tests covering the stacking case too.
- Extended TestSpellDescriptionsAreJargonFree to catch "Whatever",
  trailing ellipses, mid-word truncation, and empty-size phrases. This
  surfaced 4 more genuinely-broken SRD descriptions (find_familiar,
  clairvoyance, glyph_of_warding, teleportation_circle); all now
  overlayed in dnd_spells_data.go.

P1 — UX hygiene:
- Setup completion now nudges new casters toward !spells/!cast.
- Magic-item attunement vocab unified to "bond" everywhere user-facing;
  renamed the Treasures sheet section to avoid clashing with the bond
  vocab.
- Ambient DM footers stripped of "Threat +N" / "Supplies −N SU" hidden-
  meter leakage; verb-form footers instead.
- !expedition status defaults to a days-left + threat-label summary;
  raw SU / threat-out-of-100 / zone-stack / roll-modifier moved behind
  `!expedition status --debug`.
- !zone taunt/compliment help text dropped the "(mood −10)" / "(mood +5)"
  numerics that turned the hidden TwinBee-mood mechanic into a min/max
  knob.
- TwinBee pronoun pass: switched she/her to singular they on the two
  surfaces I touched plus achievements.bj_beat_twinbee.

P1 — correctness/scale:
- adventure_shop curio purchase now guards the euro.Debit bool return
  before inventory grant + pot cut; the GetBalance preflight isn't
  enough since a concurrent blackjack/lottery debit can slip the balance
  across BLACKJACK_DEBT_LIMIT between read and write.
- equipMagicItem reordered: remove inventory item BEFORE equip, with a
  best-effort rollback if equip fails. The prior order left a dup
  window if the inventory delete tripped on a transient DB error.
- magicItemsByRarityCache promoted to sync.Once — concurrent cold-start
  loot rolls otherwise raced on the lazy assignment.

Stale-audit verifications (no code change needed):
- light / druidcraft / water_walk overlays already exist and win the
  SRD merge — the broken raw text never reaches players.
- bootstrapPhase5BHPRefresh is already wired at adventure.go:228.

Deferred:
- combatantsForSession per-turn 6-load DB chatter (perf P1-A): touches
  the hot combat path; needs its own test pass before shipping. Tracked
  in memory at project_combat_session_cache_deferred.

go vet clean; go test -race ./... green (229s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:43:11 -07:00
prosolis
ba84d38979 Audit A1 close-out: 12 SRD-only utility-spell voice shims
The May audit's Phase A1 was overstated. Re-verification at HEAD
found:

- TestSpellDescriptionsAreJargonFree already exists with the
  proposed regex set and passes.
- .Upcast is dead text (grep across internal/ shows no runtime
  reader); audit's Upcast scope was moot.
- All four Description leaks cited by example (bless, command,
  create_or_destroy_water, cure_wounds) were already overlaid in
  the prior 2026-05 voice pass.

Real residual: 12 SRD-only utility/control entries whose Open5e
prose was bland (but not jargon-leaking). Overlay shims added in
the charm_person voice — second-person, verb-led, lightly wry.
Mechanics (Effect/CastTime/SaveStat/Concentration/AOE/
MaterialCost) preserved from the SRD originals.

Also closes out B1 (help jargon — already shipped) and B2
(TwinBee voice — shipped in Phase B2 voice pass) in the audit
doc; both were re-asserting completed work.

Remaining in the May audit: C1 backup wiring, C2+C3 maintenance
hardening, D1+D2 hygiene. No player-facing P0 left.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:00:35 -07:00
prosolis
25ea23f9d9 Phase 5C (rosters): T3 polish + feywild T4-spread fix
Phase 5-B's exit named two follow-ups: T3 below band (manor 39 /
underforge 47 vs 55-75) and the T4 spread (feywild 59 vs underdark
88, 30pp asymmetry). Phase 5-C re-runs the Phase 4-A trace on the
four affected zones under the shipped Phase 5-B cell and applies
roster knobs only (IsElite + SpawnWeight; no bestiary stat-block
touches).

TestExpeditionBalance_Phase5C_OutlierDiagnostic names the killer
per zone:

  manor_blackspire (53%): Vampire Spawn (72% win, 42 kills) +
    Revenant (16% win, 31 kills at SW=1, can't go lower). First
    attempt trimmed VS SW 3 -> 2 and backfired by concentrating
    elite share on Revenant (kills jumped to 48); reverted.
  underforge (49.5%): Fire Elemental (57 kills, 61hp/win) +
    Salamander (33 kills) carry the lethality; Helmed Horror only
    100%-win elite but SW=1, no dilution effect.
  feywild_crossing (54%): only 2 elites in the pool; Fomorian
    (50% win, SW=1) still pulls 25% of elite picks and owns 65
    kills.
  underdark (86%): Drow at SW=7 fills nearly half of standard
    rolls at 100% win / 1.1hp loss -- free-HP filler.

Roster changes:

  manor    Banshee promoted to elite SW=2 (was standard SW=3,
           99.6% win). Soft 4th elite slot dilutes Revenant share
           ~14% -> ~11%. Standards collapse to Shadow+Poltergeist
           (>=99% win).

  underforge  Helmed Horror SW 1 -> 3. Three-way elite pool drops
              Fire Elemental's share from ~44% to ~33%.

  feywild  Green Hag promoted to elite SW=2 (was standard SW=4).
           Adds soft 3rd elite + removes a 16hp/win standard.
           Standards become Redcap+Will-o-Wisp+Quickling.

  underdark  Drow SW 7 -> 5 (light trim per the user's "lift
             trailers, don't nerf leaders" stance). Standards
             shift toward Hook Horror / Drow Mage.

Phase 1 matrix after Phase 5-C (200 trials, Fighter @ centerline):

  T1  88.5%  spread  1.0   (in band 70-90)
  T2  74.5%  spread 15.0   (in band 62-82)
  T3  56.7%  spread  0.5   (in band 55-75)  +13.5pp
  T4  77.0%  spread 13.0   (over band 45-65, matches 5-B target;
                            spread halved from 25.5)
  T5  58.0%  spread 40.0   (abyss_portal 38% is the residual)

T3 mean lifted +13.5pp; both zones inside band with 0.5pp residual
spread. T4 spread halved (25.5 -> 13.0pp) by lifting feywild
+11.5pp and trimming underdark -4.5pp. T1/T2/T5 untouched.

No test debt -- no production tests pin SpawnWeight or IsElite on
the changed entries (TestMonsterKillTags_GatesKnownMonsters checks
vampire_spawn tags only, unaffected). -short suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:24:03 -07:00
prosolis
5ef10e35dc Phase 5b: player power floor + Phase-3 winners shipped to live
Closes the 'fairly breezy with some death' target the user picked
for Phase 5. Five-piece ship; Phase 1 matrix lands T1 88%, T2 74%,
T4 72%, T5 ~57% in or above band. T3 remains the design hump at
~45% (manor 39, underforge 47) — Wraith promotion to elite was
already done in Phase 4-B, the remaining standard-pool deaths are
the irreducible part of T3.

Pieces:
  1. computeMaxHP × 1.5 (phase5BHPMult in dnd.go). Uniform across
     class/level so the class-balance harness's in-tier parity
     assertion stays green. Bootstrap (bootstrap_phase5b_hp.go)
     refreshes hp_max for existing characters at startup;
     idempotent via db.JobCompleted. hp_current is bumped by the
     same delta so a full-HP character stays at full.
  2. applyPhase5BPlayerFloor (dnd_combat.go): +3 AC, +3 AttackBonus,
     +3 weapon.MagicBonus (damage). Applied at the END of
     applyDnDEquipmentLayer (after computeArmorAC's AC override)
     and inside buildHarnessPlayer so live and harness measurement
     match.
  3. Elite bracket 19 → 23 (resolveCombatInterrupt). Case order
     puts Elite (≥23) before Patrol (≥22) so a 23+ total prefers
     the single dangerous fight. Elite is now effectively a
     high-threat event reachable only via the +1-per-20-threat-
     above-40 mod — Phase 4-B's elite-pool monsters still appear,
     just less often.
  4. dailyThreatDrift base 3 → 1. Slows the threat clock so
     players have the days they need before threat tips zones
     into the new 23+ elite band.
  5. applyDailyBurn default → 50% (phase5BDailyBurnRatePct). Also
     applied in the temporal-override branch in
     dnd_expedition_cycle.go so tidal / unraveling days scale by
     the same 0.5× — otherwise those days would be
     disproportionately harsh against the new baseline.

The harness's expedition_balance.go reads phase5BDailyBurnRatePct
as the default-burn fallback when the override knob is zero, so
Phase 1 matrix measurements now reflect what live players
experience.

Test debt: 13 pinned-numbers unit tests across combat_stats_test,
dnd_test, dnd_xp_test, dnd_equipment_profiles_test,
dnd_expedition_supplies_test, dnd_expedition_cycle_test,
dnd_expedition_extract_test, dnd_expedition_region_cmd_test,
dnd_expedition_combat_test, dnd_expedition_threat_test,
dnd_expedition_temporal_test, expedition_balance_test were
pinning pre-Phase-5-B baselines; updated with comments noting
the cause. Class-balance suite stayed green (uniform buff
preserves spread).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:11:27 -07:00
prosolis
d0a8505c76 Phase 5a (diag): tier-wide sensitivity sweep on T2/T3/T5 under-band zones
Phase 4-B closed the per-zone outliers but T2/T3/T5 sibling pairs sit
below band as a group (T2 7-13% vs 62-82%; T3 3-14% vs 54-74%; T5
25-57% vs 36-56%). The plan doc's three tier-wide candidates (gear-
tier centerline remap, per-tier elite threshold, ship burn=75) needed
disambiguation before pulling any of them.

TestExpeditionBalance_Phase5A_TierWideSensitivity runs the 6 problem
zones through three one-axis sweeps holding the other two levers at
Phase 3-B best (e=23, d=1, burn=50):

  Axis L  player level    {centerline-2, centerline, centerline+2}
  Axis E  elite threshold {18, 23, 28}
  Axis B  supply burn pct {40, 50, 60}

200 trials/cell × 6 zones × 9 cells = 10.8k trials; runs in 0.31s.
Reuses the harness's traceFightStruct plumbing for elite-vs-standard
fight share alongside comp/death/starve.

Reading (full numbers in the plan doc):

  - Player level is the dominant lever at T2/T3. +2 levels lifts
    completion meaningfully; the elite gate and burn are inert.
  - Elite threshold is already at its sweet spot at e=23 — e=18
    floods elites and collapses every tier; e=28 is flat. Per-tier
    threshold candidate is killed.
  - burn=75 globally is killed. burn=60 alone produces 36%/61%
    starve at T5; the Phase 3-B negative result holds tier-wide.
  - T5 dragons_lair is already in-band at the baseline (60.5%);
    the T5 gap is really an abyss_portal-only gap.
  - The level-bump path has a catch: L7 at T2 (max of design range,
    still gearTier=2) only reaches ~27%, well below the 62-82% band.
    Closing T2 properly likely needs a cross-gearTier-boundary
    centerline (T2 → L9 = gearTier 3) — a design call for Phase 5-B,
    not a knob twist.

Diagnostic-only — no gates. -short skips. Plan doc updated with
numbers and three Phase 5-B candidate moves (cross-bracket centerline
bump recommended; player combat-math retune is the bigger option;
lowering the band target is the fallback).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:36:17 -07:00
prosolis
cf092742bb Tests: catch up stale assertions to current prod UX
Two tests had been failing on -short for several phases; the
comfortable "pre-existing failures" framing was masking that both
were stale, not flaky — each test predated a deliberate UX rewire
that nobody updated the assertion against.

TestMageSpellbookLineInRender — commit 8bf7d35 (UX S7 jargon sweep)
reshaped the spellbook line to "**Spellbook:** N / M leveled spells
learned" and intentionally dropped the redundant "(can learn N more)"
trailer. Test was still asserting "2/10" (no spaces) and the dropped
hint. Updated to track the live "N / M leveled spells learned"
format; comment cites the sweep commit.

TestAdv2Scenario_ZoneRunGoblinWarrens — commit 886eb5a (turn-based
elite/boss combat) moved Elite/Boss rooms off the auto-resolve
SimulateCombat path onto the manual !fight + !attack engine; !zone
advance now stops at the doorway and only progresses past a won
CombatSession. The scenario test was only calling !advance, so it
spun in place at room 4 (the elite). Loop now detects Elite/Boss
prev-rooms, opens with !fight if the session doesn't exist, drains
!attack rounds until the session terminates, then falls through to
!advance to clear the room (won → walk graph; lost/fled → terminate
next pass). maxSteps doubled to absorb the fight-then-advance pairs;
inner !attack cap at 60 rounds covers RNG tail.

Both -short tests now pass; full -short suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:29:03 -07:00
prosolis
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
prosolis
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
prosolis
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
prosolis
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
prosolis
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
prosolis
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
prosolis
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
prosolis
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
prosolis
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
prosolis
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
prosolis
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
prosolis
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
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
c2fdc63b51 UX S4: magic-item polish — slot fixes, swap-back, shop & sheet truth-up
B4: Slot classifier no longer treats "Springing" / "Snaring" / "Devouring"
as ring matches; tokenises by word boundary instead of raw substring.
Adds DnDSlotCloak so cloaks/capes/mantles/wings stop evicting body armor
from the chest slot. Regenerated magic_items_srd_data.go: boots_of_*,
gloves_of_missile_snaring, bag_of_devouring, talismans, and 6 cloaks all
land in the right slot.

R4: equipMagicItem swap-back returns the prior occupant at full Value
instead of half — swapping a curio shouldn't tax it.

R5: Attunement count is recomputed *after* the swap-back so freeing the
prior occupant's bond opens the slot for the incoming item.

R1: Inventory tags magic_item rows with 🔮 + rarity label and prints a
single equip-magic footer when any are present.

R6: Sheet's Magic Items block marks unbonded items as **(inactive)**
with the reason (cap full vs unbonded), so over-cap items aren't silent.

R7: New activeMagicItemsLine surfaces a one-shot "your curios stir: …"
at combat-start in both the dungeon path and !fight, mirroring the way
class passives are surfaced.

R8/R9: dropMagicItemLoot pretty-prints rarity, drops "wondrous", calls
attunement "needs bonding", appends "auto-uses in combat" for
potions/scrolls, and routes persistence errors to slog instead of
leaking %v into chat.

R2/R3: Curios shelf now shows "Very Rare" not "very_rare", drops the
bare "wondrous" word (the effect line carries it), renders the codified
magicItemEffectSummary above the SRD desc, and ends with a one-line
plain-language "what is bonding" footnote.

R10: Curios stock day flips at 06:00 UTC instead of midnight so EU
players don't see a fresh shelf at 1 a.m. mid-session.

R11: Curios buy resolver disambiguates fuzzy matches — typing "ring"
when several rings are on the shelf lists candidates instead of
silently selling the first.

P1: Greeting grid pairs Curios with an Exit chip so the 2-column
emoji layout doesn't dangle.

P2: Equip-magic empty state trimmed to one line.

P4 (back-from-curios reprompt) deferred — the existing back-flow is
correct, just verbose; not worth the surface-area expansion this
session.

Tests: word-boundary classifier, cloak/chest coexistence, full-value
swap-back. go test ./... + go vet clean.
2026-05-14 22:37:47 -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
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
prosolis
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
prosolis
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