Mechanical `gofmt -w ./internal ./cmd`. Mostly struct-field realignment that
had drifted, plus a few trailing-newline fixes. No behaviour change — gofmt is
semantics-preserving, and build/vet/test are green either side.
Split out from the code-review fixes that follow so those stay reviewable
instead of hiding inside a wall of realignment.
Phase 2 Monte Carlo tuning had papered over four classes whose 5e-defining
mechanics never actually existed in the engine — only flat DamageBonus or
FlatDmgStart compensation riders that hit the right aggregate win rate.
This pass replaces the proxies with the real primitives.
New CombatModifiers fields: ExtraAttacks (additional swings/round, looped
in a new resolvePlayerSwings helper); HuntersMarkDie (per-hit Nd6 bonus,
same path as Sneak Attack); ThornLashDmg (flat counter on landed enemy
hits, fires in resolveEnemyAttack after ward/block).
Fighter L5/11/20 Extra Attack, Ranger L5 Extra Attack + Hunter's Mark
(1→4 d6 by level), Paladin L5 Extra Attack + per-hit Divine Smite
(replaces one-shot opener), Druid thorn lash. Bard College of Valor L7
"Extra Attack" subclass tier converted from +1 attack/+10% damage proxy
to the real primitive.
Post-fix T5 class-balance: martials cluster at top (Fighter/Rogue 0.90,
Ranger 0.89, Paladin 0.88), casters keep their relative ordering, spread
14pp top-to-bottom.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tedium-removal pass driven by live play feedback. Three big threads:
Expedition autopilot Phase 4 — background auto-run + harvest-until-dry:
- New expeditionAutoRunTicker walks active expeditions every 15min
(5min tick, per-expedition CAS on new last_autorun_at column). Skips
combat sessions, briefing/recap quiet windows, expeditions <30min old.
- Walks up to 3 rooms/tick with compact narration; suppresses DMs when
0 rooms walked or just hitting the per-tick cap (no "stretch complete"
filler). Player only hears from the bot when a real decision is needed.
- Compact mode: one-line combat narration for trash/elite, auto-resolves
elite doorways via the forward-sim engine (boss still pauses). Threaded
via new advanceOnceWithOpts → resolveRoom → resolveCombatRoom.
- Auto-harvest now grinds each Common/Uncommon node until dry (cap at 8
attempts/visit), mirroring manual !scavenge retries. Rare+ still pauses.
- Ambient ticker: anti-repeat by Kind via new last_ambient_kind column
(avoids two pack_rat DMs in a row when the pool only has 6 lines).
- !expedition go <n> now routes to the fork-choice handler when active +
numeric; fork footer rewritten to suggest !expedition go instead of
!zone go. Boss/Elite doorway: formatNextRoomMessage routes the action
hint by next room type and autopilot loop breaks via new nextRoomType
field so "Room X/Y — Boss" doesn't double-print with contradictory
hints (!zone advance vs !fight).
- runAutopilotWalk extracted from expeditionCmdRun so foreground and
background share the loop body.
Rogue Sneak Attack actually exists now:
- Audit found the rogue's "Sneak Attack" passive was AutoCritFirst +
5% damage rider. No Nd6, ever. Phase 2 Monte Carlo masked this
with a small flat buff; the class's defining mechanic never matched
its tooltip or 5e identity.
- Added SneakAttackDie int to CombatModifiers, per-hit Nd6 in
combat_primitives.go (same lane as DivineStrikePerHit). Scales with
level: 1d6 at L1-2 ... 4d6 at L7-8 ... capped at 10d6 at L19-20.
- AutoCritFirst + 5% rider retained as bonuses on top of working sneak
attack — preserves the opener-burst feel.
- L7 rogue expected per-hit ~8 → ~22 damage. Valdris fight math goes
from "16 rounds to kill, die in 8" to winnable.
TwinBee voice sweep (Phase B3):
- ~530 line changes across 24 files replacing third-person "TwinBee
notes/files/tracks/respects/..." constructions with first-person
inside flavor string literals. Code identifiers (TwinBeeLine,
twinBeeLine, etc.), chat-prefix labels (🎭 **TwinBee:**), item names
(TwinBee's Bell), achievements, and game-title references in
fun.go (Konami TwinBee ship) left intact.
- Catches a real bug: Valdris fight rendered "TwinBee marks the
Legendary Resistance" mid-combat in third person, contradicting
the Phase B2 convention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
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.
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.
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.
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.