Commit Graph

323 Commits

Author SHA1 Message Date
prosolis
4dd1ab9f96 D&D: class-balance Phase 0 — Fighter-vs-Mage Monte Carlo spike
Per gogobee_class_balance.md §5 Phase 0: harness skeleton, equipment
and spell-selection policies, Fighter-vs-Mage plausibility run before
Phase 1 generalizes to the full 10 × 30 matrix.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Subclasses and spell lists are deferred to later sessions.
2026-05-14 14:43:05 -07:00
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
prosolis
f1aa9981f8 Combat: implement immediate-resolution monster ability effects
Wires up the ability effects that resolve fully within applyAbility with
no new persistent state: damage riders (bonus_damage, aoe/aoe_fire/
death_aoe, execute) via the shared calcDamage formula, self_heal, and
flavor-only placeholders for effects still pending per-fight state.
Works in both auto-resolve and the turn engine since both call
applyAbility.

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 06:30:52 -07:00
prosolis
886eb5a75b Combat: wire turn-based elite/boss fights to the command surface
Routes Elite/Boss rooms off the auto-resolve SimulateCombat path and onto
the persisted turn-based engine. !zone advance now stops at an Elite/Boss
doorway; the player engages with !fight, then resolves one full round per
!attack / !flee. A won CombatSession is the record that the room's combat
is done, so a fresh !zone advance clears the room and advances the graph.

- buildZoneCombatants: shared player/enemy Combatant builder extracted from
  runZoneCombat; combatantsForSession rebuilds the pair from a session row.
- runCombatRound loops the phase state machine through a whole round;
  finishCombatSession runs HP/XP/loot/kill/threat/mood close-out.
- getCombatSessionForEncounter lets the room resolver tell "already won"
  apart from "not yet fought".
- !zone advance/enter/go blocked while a session is active.
- resolveBossRoom deleted (dead after the reroute).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 06:16:45 -07:00
prosolis
269d13ffe8 Combat: add turn-based session accessors and round-loop state machine
combat_session.go: CombatSession persistence layer mirroring the
dnd_expedition pattern — CRUD accessors, one-active-per-user enforcement,
and the timeout reaper (sweeps stale sessions to 'expired').

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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