The doc's item M claimed all three mage subclass spell hooks were dead on the
turn path because applyMageSubclassSpellHooks had one caller. It has three.
resolveTurnSpell has called it since 5cd343a, so Empowered Evocation and
Overchannel always worked -- they only move mods.SpellPreDamage, which
resolveTurnSpell returns as EnemyDamage.
Grim Harvest was the real defect, with a narrower cause: the hook wrote
mods.GrimHarvestSlot into a local CombatModifiers that resolveTurnSpell
discarded, because turnSpellOutcome had no field to carry it out. A Necromancy
Mage who killed with a spell in a manual fight never healed.
The stash can't ride on fight-start mods the way auto-resolve's does -- the
spell is cast mid-fight and the turn engine rebuilds combatants every round --
so it rides on the casting seat's ActorStatuses, like ArmedAbility. Each
damaging cast overwrites it; snapshotActor carries it across commit().
grimHarvestHeal also scanned for the *first* spell_cast event to ask whether
the spell landed the killing blow. Auto-resolve casts once, pre-combat, so
first == last there. A turn-based mage casts every round, so a non-lethal
opening cantrip vetoed the heal the killing spell had earned. Now scans for the
last spell_cast -- provably identical on the auto-resolve path, so the golden
corpus does not move.
Balance: a caster buff on the manual surface only, and the one the subclass was
written to have. Auto-resolve already paid it out.
Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
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>
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>
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.
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.
High-priority fixes from the multi-agent audit of Adventure 2.0:
- Phase 11 nil-deref cluster: zoneOrFallback() helper replaces 8 unsafe
getZone(_) sites in dnd_zone_cmd.go. Corrupted zone IDs render a
placeholder instead of panicking.
- Briefing/recap idempotency: deliverBriefing/deliverRecap now claim
the rollover via a conditional UPDATE. Double-fires from clock skew
or restart become no-ops; supply burn and day++ no longer reapply.
- Graceful ticker shutdown: AdventurePlugin gains stopCh + Stop(); all
11 background tickers now select on stopCh in addition to ticker.C.
- Mood decay (§3.2): math.Round(elapsed*2) replaces int() truncation
so sub-hour gaps decay correctly.
- 24h auto-abandon (§4.3): getActiveZoneRun returns clean slate and
abandons stale runs whose LastActionAt is over 24h old.
- Respec / auto-migrate orphan cleanup: !respec and the auto-migrated
draft wipe now abandon active zone runs and expeditions before
deleting the dnd_character row.
- Phase R combat-link: applyBossDefeatThreat now wired from
resolveBossRoom (-20 threat); applyRoomCombatThreatForUser adds
+5/+8 from non-boss/elite kills (§8.1).
- Starvation → forced extraction: briefing-time check forces extract
with §10.2 coin tax when supplies hit zero.
- GMNat20/Nat1 narration wired into resolveCombatRoom and
resolveBossRoom (nat-20 takes precedence over nat-1).
- Treasure-undo race: LoadAndDelete on both timer-fire and `undo`
paths so only one side wins.
- Battle Master: Disarming, Menacing, Parry maneuvers added (3 → 6
of 10). Remaining 4 (Pushing, Goading, Riposte, Commander's Strike)
documented inline as needing ally/reaction mechanics the engine
doesn't model.
- Threat-70 warning: tracked in RegionState["siege_warning_fired"]
so a drop-and-recross doesn't re-fire the beat.
- region_state JSON decode error now logged via slog.Warn instead
of silently discarded.
Failing TestProdDB_DnDLayer fixed via option (a): track migrated
characters and only run round-trip / idempotency assertions on those,
skipping pre-existing prod-DB rows accumulated from live bot use.
New tests in dnd_audit_phase_R7_test.go cover: 24h auto-abandon,
briefing double-fire idempotency, threat-70 warning idempotency,
multi-region extract→resume state preservation, and starvation
forced-extraction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hunter / Beast Master / Gloom Stalker L10/L15 capstones — last subclass
batch of Phase 10 SUB3.
Hunter L10 Multiattack (Volley/Whirlwind): +15% DamageBonus, collapsing
the AoE extra-target throughput onto the single 1v1 target.
Hunter L15 Superior Hunter's Defense: ~15% incoming damage reduction
(Uncanny Dodge proxy — most generally applicable of the three picks).
Beast Master L10 Share Spells: pet floor lifts to PetAttackProc 0.45 /
PetAttackDmg 6 + lvl/2, plus +5% DamageBonus rider for the Ranger's own
buffed swings. L15 Superior Bond: pet floor 0.55 proc / 8 + lvl/2 dmg
plus 0.92 DamageReduct ("body-blocks more reliably"). Both still use
the existing max-style floors so a stronger external pet (legacy
adventure pet, etc.) is preserved.
Gloom Stalker L10 Stalker's Flurry: +12% DamageBonus representing
recovered misses across the fight (L7 already covers the first-attack
re-roll via AssassinateAdvantage). L15 Shadowy Dodge: ~10% incoming
damage reduction (reaction-imposed disadvantage proxy).
11 new tests; ranger suite + full plugin suite green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Life/War/Trickery Domain L10/L15 capstones.
L10 Divine Strike (all 3 subclasses): +4 flat per weapon hit, scaling
to +9 at L14. New CombatModifiers.DivineStrikePerHit channel, gated on
Weapon != nil since 5e specs "weapon hit". Damage type (radiant/weapon/
poison) varies by domain but isn't tracked by the engine.
Life L15 Supreme Healing: heal-spell dice resolve at max instead of
rolled. Wired through resolveHealOutOfCombat via lifeDomainSupremeHealing
helper.
War L15 Avatar of Battle: 5e physical resistance vs. non-magical
weapons → flat 0.80 DamageReduct (softer than full 50% to account for
elemental hits).
Trickery L15 Improved Duplicity: 5e duplicates grant ally-flank
advantage; no allies in 1v1, so proxied passively as +1 SporeCloud
round and +5% damage (duplicates flicker around the foe).
10 new tests; cleric-suite green. Pre-existing rng flakes
(TestOrcRageFiresOnLowHP, TestSimulateCombat_FirstAttackBonus...) are
unrelated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Evocation L10 Overchannel maximizes leveled-spell dice (slot 1-5),
proxied as +50% SpellPreDamage on the queued cast. Self-damage drawback
omitted — our model queues at most one cast/fight, so the "first per
rest" exemption applies. L15 Sculptural Mastery skipped: AoE-only, no
allies in 1v1 (same shape as SUB2b skips).
Abjuration L10 Spell Resistance → DamageReduct *= 0.92 (8% generic DR
proxy; the saves-advantage half is inert since enemies rarely force
saves). L15 Spell Reflection → ReflectNext += 0.30, riding the existing
reflect channel since Counterspell is reaction-deferred to Phase 11.
Necromancy L10 Command Undead has no surface in 1v1 (no thrall/ally
system), re-fluffed as deeper authority over death amplifying the
harvest itself: GrimHarvest multipliers tick up by 1 (2x→3x non-necrotic,
3x→4x necrotic). L15 Improved Undead Thralls proxied as a permanent
skeletal minion via PetAttack channel (proc 0.30, dmg 6 + CON mod);
respects existing higher-pet dmg/proc.
11 new tests; full plugin suite green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L10 Use Magic Device: no scroll/wand item system to gate, so we proxy the
"now allowed to use magic items" fantasy as a small per-fight wand zap
(FlatDmgStart += 6) plus a +5% DamageBonus representing better tactical use
of magical implements.
L15 Thief's Reflexes: engine has no "extra turn round 1" primitive, so we
ride the AssassinateBonusDmg + AssassinateAdvantage channel (same one Gloom
Stalker's Dread Ambusher uses for bonus opener damage). Flat +8 on first
hit approximates one extra turn's hit; advantage on first attack covers
the "you swing first" piece.
3 new tests cover the L10 thresholds, L9 gate, and the L15 layer stacking
on top of L10. Full suite green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L10 Improved Combat Superiority — superiority dice grow d8 → d10. The
two scalar maneuvers (Precision Attack +4→+5; Rally heal +4→+5) bump in
their Apply hooks; Trip Attack has no scalar to scale.
L15 Relentless — 5e regenerates one die when initiative is rolled with
an empty pool. Proxied as +1 max pool size (4→5), which gives the same
"always at least one die per encounter" guarantee across the long-rest
cycle without introducing per-fight regen state.
subclassResourceMax now takes level; initSubclassResources upserts the
max (and bumps current by the delta) so existing characters grow on
level-up. Hooked into the dnd_xp level-up loop so Battle Masters
crossing L15 see the new die without waiting for a long rest. 4 new
tests; no regression in subclass/resource/level-up paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Champion L10 Additional Fighting Style → +1 AC + 10% damage (Defense +
Dueling collapse, since one-shot combat can't surface a style picker).
Champion L15 Superior Critical → CritThreshold drops 19 → 18.
Berserker L10 Intimidating Presence → 2 rounds of SporeCloud miss
chance (Action-based fright proxied via the existing fog-cloud channel).
Berserker L15 Retaliation → +15% damage (reaction-strike average uplift,
since the engine has no reaction primitive).
6 new tests; full plugin suite green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L5/L7 abilities for the three Ranger subclasses, plus Gloom Stalker
Stealth advantage from Umbral Sight. Beast Master pet floors use a
max-style merge so the legacy adventure-pet path can still upgrade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Channel Divinity resource pool (2/long-rest) shared across all three
Cleric domains. One armed expression per fight, mirroring Battle Master.
Life Domain
- L5 Disciple of Life: heal spells restore +(2 + slot level) extra HP
(lifeDomainHealBonus, hooked into resolveHealOutOfCombat).
- L5 Preserve Life: Channel Divinity heal — sets HealItem to 5×Cleric
level, capped at HPMax/2; fires when player drops below 50%.
- L7 Blessed Healer skipped — no allies in 1v1 combat to "heal an ally".
War Domain
- L5 War Priest passive: +1 attack bonus + 0.15 DamageBonus, proxy for
bonus-action extra-attack throughput one-shot combat can't model
discretely.
- L5 Guided Strike: Channel Divinity → +10 to first attack roll
(FirstAttackBonus).
- L7 War God's Blessing skipped — ally reaction, no allies.
Trickery Domain
- L5 Invoke Duplicity: Channel Divinity → AssassinateAdvantage (reroll
first miss) + 0.10 DamageBonus, proxy for "advantage on attacks vs.
creatures adjacent to the duplicate".
- L7 Cloak of Shadows passive: +2 SporeCloud rounds (15% enemy miss)
proxy for the brief invisibility window.
- L5 Blessing of the Trickster skipped — non-combat Stealth flavor.
15 new tests, all green. Same pre-existing
TestSimulateCombat_FirstAttackBonusImprovesEarlyHits flake.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Evocation L7 Empowered Evocation adds INT mod (min +1) to Mage evocation
spell damage. Abjuration L5 Arcane Ward grants a 2×level HP buffer
absorbed before player HP, +prof at L7. Necromancy L5 Grim Harvest heals
2× spell level on spell-kill (3× necrotic) via post-combat hook.
Sculpt Spells / Potent Cantrip / Inured to Undeath skipped — no allies,
save-half is already uniform, and no necrotic enemy damage to resist.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Battle Master:
- New "superiority" resource pool (4/long-rest at L5), provisioned via
initSubclassResources at !subclass selection.
- Three armed maneuvers fueled by superiority dice:
* Precision Attack — +d8 (≈+4) to first attack roll
* Tripping Attack — enemy skips first attack (reuses Phase 9
SpellEnemySkipFirst)
* Rally — +(d8 + CHA) HealItem at <50% HP
- L7 Know Your Enemy proxied as +1 AttackBonus passive.
Assassin:
- L5 Assassinate: AssassinateAdvantage (re-roll first miss, take better
of two d20s) + AssassinateBonusDmg = level (5 at L5) stacked on top
of the Rogue's existing Sneak Attack auto-crit.
- L7 Impostor bumps the bonus damage by +3.
- L5 Infiltration Expertise → +5 Deception; L7 Impostor → +10.
Engine:
- CombatModifiers gains FirstAttackBonus, AssassinateAdvantage,
AssassinateBonusDmg.
- resolvePlayerAttack consumes each on the first attack only via new
combatState one-shot flags.
Tests added: 14 covering passive gating (BM L7+, Assassin L5/L7),
maneuver Apply flags, superiority pool init for BM only, !arm gating
across class/subclass for precision_attack, Deception bonus tiers,
statistical lift from FirstAttackBonus and AssassinateBonusDmg in
SimulateCombat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First mechanical slice of the subclass system. Each subclass gets its L5
and L7 abilities wired through the existing combat engine + skill check
substrate; no new resource pools introduced.
Champion (L5/L7):
- Improved Critical: new Mods.CritThreshold lowers crit floor to nat 19+
in resolvePlayerAttack. Default 20 preserved for non-Champions.
- Remarkable Athlete: +½ proficiency bonus to STR/DEX/CON skill checks
via subclassSkillBonus, layered on raceSkillBonus.
Berserker (L5/L7):
- Rage: new !arm-able active ability gated to Berserker subclass via
DnDAbility.Subclass field. Reuses the Fighter stamina pool (3/long-rest
matches 5e's rage uses). On fire: +2 flat damage per hit, halve incoming
weapon damage, +50% damage approximation for Frenzy's bonus-attack-per-
turn (one-shot combat can't model that literally).
- Frenzy exhaustion: new exhaustion column on dnd_character; incremented
by persistDnDPostCombatSubclass after any rage'd combat. Decremented by
one per long rest.
- Mindless Rage: documented; charm/frighten conditions don't exist in the
combat engine yet, so no functional effect until P11 zone bosses land.
Thief (L5/L7):
- Sleight of Hand added to dndSkillTable.
- Fast Hands: +5 to Sleight of Hand checks via subclassSkillBonus.
- Supreme Sneak: advantage on Stealth (best of two d20s) via new
subclassSkillAdvantage hook in performSkillCheck. 5e's half-speed gate
dropped — we don't track movement speed.
Plumbing:
- applySubclassPassives layered after applyClassPassives in both arena
and dungeon combat bridges.
- DnDAbility.Subclass enables per-subclass gating; characterActiveAbilities
filters !arm and !abilities lists by both class and subclass.
- Existing !respec full-wipe also clears Exhaustion.
Tests: Champion CritThreshold gating across L4/L5/no-subclass; rage Apply
flag-set; rage probabilistic win-rate lift vs tougher enemy; exhaustion
increment only on raged combats; long-rest decrement; Champion bonus on
STR/DEX/CON only and only at L7+; Thief Fast Hands SoH-only; Thief
Supreme Sneak L7+ Stealth-only; statistical advantage roll lifts Thief
stealth average by ≥2 vs plain Rogue; !arm rage subclass gating; schema
roundtrip; characterActiveAbilities visibility.