The seven-day window was a promise the code never kept. releaseParty fires
only on a terminal status, and dnd_expedition.go justified skipping it for
'extracting' by asserting the roster gets cleared when the window lapses and
the row flips to 'failed'. Nothing did that: the only extracting -> failed
transition lived inside handleResumeCmd, a lazy expiry that runs only when the
leader personally types !resume.
A leader who quit, forgot, or simply started a different expedition therefore
left the row 'extracting' forever. releaseParty never ran, every member stayed
seated, and assertNotAdventuring kept refusing them a run of their own.
Three holes:
- No sweeper. sweepLapsedExtractions reaps every row past completed_at + 7d
through completeExpedition, which releases the roster, and DMs the
audience. Hourly ticker plus a one-shot at Start() -- a lapse that happened
while the bot was down is blocking those members now. The audience is read
before the close-out, since completeExpedition disbands the roster
expeditionAudience reads. handleResumeCmd's lazy expiry stays, now sharing
the extractionLapsed predicate.
- The leader could orphan their own party. !expedition start checked only
getActiveExpedition, and !resume resolves the newest 'extracting' row, so
starting fresh on top of one left it unreachable with its roster still
held. It now refuses when that row has a roster; a solo extraction strands
nobody, so walking away from one stays normal.
- The leader had no way out but to pay. !expedition abandon could not see the
extracted row it owns, so closing it meant buying a !resume first. Both it
and abandonExpedition now span 'extracting' via ownedLiveExpedition, which
sorts active rows first so expeditionCmdStart's run-spawn rollback still
tears down the row it just created. This fixes dnd_setup.go for free: a
leader who rerolled their character used to strand their whole party.
Note the review item this came from (C) was mis-stated: it claimed members and
leaders disagree during 'extracting' because expeditionForMember filters
status = 'active'. getActiveExpedition filters 'active' too, so they already
agree -- and honestly, since nobody is standing in the dungeon. Widening
expeditionForMember would have made the member the only player who can see a
paused expedition. Not done.
Abandoning now DMs the members, and says the true thing when the party is in
town rather than the dungeon (supplies already spent, loot already banked).
New: dnd_expedition_extract_sweep_test.go, 6 cases.
Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
Five bugs found reviewing n1-restoration end to end.
beginCombatTurn settles any phase the engine owes before reading whose turn it
is. That settle can end the fight — and the old code then answered "you're not
in a fight" and returned. The terminal status was already persisted, so nothing
ever paid the party out: no XP, no loot, no death recorded, no run teardown. The
reaper cannot recover it either, because listExpiredCombatSessions filters on
status='active'. Close the fight out there, the way the !fight start path and
the reaper already do.
A party member was permanently soft-locked when their leader extracted and never
resumed. seatedExpeditionFor (the guard) spans 'extracting'; expeditionForMember
(what !expedition leave resolved through) saw only 'active'. So the member was
refused any new adventure by the guard and told "No active expedition" by the
command the guard points them at, with nothing sweeping stale rows and only the
leader able to clear one. Resolve the exit through the same lookup as the gate.
updateSupplies overwrites supplies_json wholesale, and expeditionCmdAccept folded
a member's packs onto a snapshot read before the coin debit, unlocked. Handlers
run one goroutine per event, so two invitees accepting genuinely interleave and
one member's packs vanish. advUserLock cannot help — it is keyed by sender, so
racing members take different mutexes. Add advExpeditionLock and re-read the pool
under it. Closes accept-vs-accept; the six other updateSupplies callers still
race and are written up separately.
runHarvestInterrupt picked an elite enemy and elite narration off a local `elite`
flag, then passed a hardcoded false as isElite to closeOutZoneWin. dropZoneLoot
gates masterwork on isBoss||isElite, so beating an elite interrupt skipped the
masterwork roll and took standard treasure weight — while the same elite fought
via !zone paid out correctly.
arenaSeasonRollover marked its job complete even when recordArenaSeasonTitle
failed, and JobCompleted short-circuits every later run for that quarter, so a
transient SQLite BUSY lost the crown forever. Defer completion on failure; the
insert is ON CONFLICT DO NOTHING against PRIMARY KEY (season, kind) and a past
season's data is frozen, so the retry is safe.
Also: drop dead partySurvivors, collapse the zoneCombatRoster alias into
fightRoster, route partyCasualtyLine through joinNames, fold four copies of the
expedition column projection into expeditionSelectCols, stop replyDM sending a
blank DM, and correct two doc comments describing a path that no longer exists.
Deliberately not fixed, with reasons, in gogobee_code_review_followups.md — most
notably that both turn-based close-outs skip grantCombatAchievements and
persistDnDPostCombatSubclass, which the auto-resolve paths run.
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.
Retires forward-only navigation. `!revisit <N>` (alias `!zone revisit`)
walks one graph edge back to a room in VisitedNodes, chosen by the number
the player reads off !map's Path strip. Edges are directed, but a corridor
you walked down is a corridor you can walk back up, so adjacency checks
both directions. !map now prints the legal targets.
Cost is +1 threat, not the discount the plan specced. There was nothing to
discount: movement charges neither threat nor supplies, and a cleared room
fires no combat, so backtracking was free. +1 mirrors harvest noise -- less
than a fight (+5) or an elite (+8). Standalone runs have no threat clock and
pay nothing. A siege guard refuses the move at threat >= 99: siege is
one-way sticky, and a player must not be able to strand their own expedition
on a move they made to go pick up a herb.
The plan claimed terminal CombatSession rows already prevent re-triggering a
cleared room. They gate only Elite and Boss, at the doorway. An Exploration
room re-entered resolveCombatRoom unconditionally and a Trap room re-armed --
so revisit would have been an infinite farm: step back, advance, repeat.
resolveRoom now early-returns on an already-cleared room. No-op forward-only,
since advance clears a room only after resolving it.
Autopilot is ticker-driven with no on/off switch, so it would have walked the
player straight back out of the room they revisited. grantAutorunGrace stamps
last_autorun_at to buy one cooldown window. That is a stopgap; R5 still owes
the real policy.
Fork re-pick stays deferred to R3: revisit refuses while a fork is pending,
because advance and `!zone go` resolve node_choices without checking which
node the player is standing on.
Backtracking (revisit R2) breaks the assumption every zone-run caller
quietly relied on: that CurrentRoom == len(VisitedNodes)-1. Position and
progress have been the same number only because navigation was
forward-only.
Split them. CurrentRoom is now the first-entry index of CurrentNode in
VisitedNodes -- the room number the player was shown on the way in, and
the salt that enemy/trap/harvest/encounter keys hash. A revisited room
therefore resolves to the same room. RoomsTraversed is a new monotonic
step counter, persisted, backfilled from visited_nodes for in-flight rows.
The audit found only two progress-shaped reads. narrationCadence moves to
RoomsTraversed so a backtracking player draws fresh flavor rather than
replaying the entry-room lines. The other was a latent bug: zoneCmdGo
labelled the room it moved into as CurrentRoom+1, which is only correct at
the frontier; advanceZoneRunNode now returns the true path index.
VisitedNodes becomes an ordered set and RoomsCleared becomes idempotent --
both no-ops while navigation is forward-only, both load-bearing after R2.
No player-visible behavior change. Nothing to route off RoomsTraversed for
threat/SU: verified at HEAD that movement charges neither. Threat comes
from combat, supplies burn per day. The revisit plan's cost model claimed
otherwise and has been corrected -- R2's "discount" is really a net-new
cost decision.
B1 — tempering. `!adventure temper` pushes an owned magic item one rung up
the rarity ladder at the blacksmith, capped at Legendary. Rarity lives on
the registry definition, so an upgraded instance records its progress as a
`temper` step count on its own row (adventure_inventory, magic_item_equipped)
and effective rarity is derived on read. The stored base rarity is never
written back, so an item cannot double-bump across a load/save round-trip.
Effects flow through the existing rarity scalars — no new combat math, and
the Legendary cap means this accelerates reaching the current ceiling rather
than raising it.
Costs mirror the crafting ladder (10k/25k/60k/150k, Foraging 15/20/25/30).
Only the Legendary rung consumes a T5 material; gating the lower rungs on
one would make tempering unreachable until a player already clears T5.
Two plan anchors were wrong: thyraks_core and portal_fragment are not
loot-note strings needing promotion — they are UniqueAlways slate entries
that already materialize as inventory rows on every T5 clear.
B4 — expedition achievement wing: first-clear and all-zones-cleared per tier
(10), a quiet-clear for keeping peak threat under 50, and temper_legendary.
The quiet-clear requires max_threat_seen to be *present*, not merely low:
recordMaxThreat samples only on day rollover and never stores a zero, so an
absent key means no threat history, not low threat. The plan's no-death-T4
achievement is not shipped — no per-expedition death counter exists — and
pet-saved-my-life already ships as combat_pet_save.
C4 — arena seasons. Standings are derived from arena_history.created_at per
calendar quarter rather than wiping arena_stats: same visible reset, but
lifetime totals survive for `!arena stats` and no destructive job can fire
twice. Crowns archive to arena_season_titles rather than player_meta.title,
which already carries the Survivalist milestone. Rollover rides the existing
midnight ticker and self-dedups on the season key, so a bot that was down on
the boundary catches up on its next wake.
A4 — the three "deferred to hookup" milestone grants now pay out:
Long Game (T5 clear) guaranteed Legendary via pickMagicItemForRarity
-> dropMagicItemLoot, rendered in a new
milestoneAward.Extra block.
Survivalist (clean T3+) writes AdventureCharacter.Title and announces to
the games room. No schema bump — player_meta.title
already exists and saveAdvCharacter persists it.
Two Weeks (day 15) restocks 3 days of rations (clamped to Supplies.Max)
and grants 3 zone-tier consumables.
Two Weeks was specced as +5 max HP "via the expedition row". Dropped: combat
MaxHP comes from stats.HPBonus, built in combat_stats.go from gear/arena/
housing with no expedition in scope. Threading one through would leak an
expedition-only buff into the sim's balance corpus. A supply cache
self-expires with the run and needs no combat math.
A6 — mid-day events rolled 0.5%/player/day from a deferred ticker slot: one
sighting per ~200 days. The roll and its roll-minute scheduler are gone.
Events now fire where the player is demonstrably present and reading a DM:
the end-of-day digest (8%), a sale at Thom's (5%), and an arena cashout (5%),
capped at one event per player per UTC day. A player who hits all three lands
at ~1.19/week. tryTriggerEvent returns bool so a bail (dead / mid-fight /
event already active) hands the day's slot back rather than burning it.
The frequency test drives its own seeded PCG over the chance constants and
the daily cap, so it measures the policy and can't flake on global RNG order.
Treasure, masterwork, and consumable drops each had zero call sites: they
only ever fired from the legacy daily activity loop, which adventure.go now
intercepts with a deprecation DM. Hook all three to the zone-combat seam.
A1 - treasure: rollAdvTreasureDropDetailed takes a weight, applied to the
base rate. Boss x4, elite x2, standard x1, plus one x1 roll on zone clear.
Near-miss DMs now fire only for weighted moments; at x1 on autopilot they'd
land on ~3% of every kill.
A2 - masterwork: the catalog is keyed to mining/fishing/foraging, so the
dungeon lookup returned nil and the hook would have been a silent no-op.
masterworkDefForZone rolls across all three slot lines at the zone's tier.
Flavor now follows loc.Activity rather than the item's catalog line, with a
new dungeon pool - a crypt boss must not narrate a pickaxe striking ore.
A3 - consumables + ingredients: the audit found more than the four named
ingredients were stranded. generateAdvLoot is reachable only from
resolveDungeonAction, which has no callers, so all four legacy loot tables
were dead and every one of the 12 recipes was uncraftable. rollZoneIngredient
draws from those tables directly at the zone's tier, reviving them wholesale
rather than re-keying 24 ingredients into the per-zone slates.
Diagnosed a cleric "death loop" (L14 dying at T2/T3 bosses while
over-levelled): the boss isn't overtuned — caster sustained DPS is
under-delivered, compounded by a fragile healer build.
Engine fix — concentration AOE re-tick:
- Concentration damage spells (spirit_guardians, heat_metal, spike_growth,
call_lightning, flaming_sphere) now tick the enemy every round at
round_end instead of resolving as a one-shot, via a new
CombatStatuses.ConcentrationDmg armed on cast and round-tripped through
the turn engine. Closes the long-tracked turn-engine concentration gap;
the burst still lands the casting round, then the aura lingers.
- Sim picker skips re-casting an already-active aura (models competent play
and prevents a burst+aura double-dip). Re-baseline (n=30 sweep + n=100
confirm): bard +47pp T3 (heat_metal), druid +3-7, cleric/mage flat,
fighter unchanged — no regressions.
Player-data bootstraps (idempotent, run once on Init):
- bootstrapCasterSpellBackfill: ensureSpellsForCharacter only seeds an empty
book, so defaults added after a character's roll never reach it. Backfill
missing defaults into known+prepared for existing casters (gives the
affected cleric inflict_wounds + a working healing_word, since her
healing_word_spell is a dead alias).
- bootstrapGrantStarterPet: one-off L10 pet for an endgame player who never
got the morning arrival roll; adds per-round proc damage + deflect.
- TestScenario_JosieCasterAid verifies both against a copy of the live DB,
incl. idempotency.
Also fix a pre-existing wall-clock flake in
TestFireBriefings_EventAnchoredActivePlayerDelivers (start_date defaulted to
real now, filtering the row out when the suite runs after 06:00 UTC).
Event-anchored expeditions no longer pin their re-engagement DM to 06:00
UTC. The ticker skips idle players (last_activity older than today's
threshold, inside the 28h safety net); maybeDeliverDeferredBriefing fires
the owed briefing on the next inbound message in any room. The OnMessage
hook also stamps last_activity so chat presence (not just bot commands)
counts toward "the player is here."
resolvePendingInteraction deletes the pending entry before dispatch, but
resolvePetName then re-ran LoadAndDelete to recover the carried PetType.
That second lookup always missed, so the if !ok branch returned nil: the
save never ran and no pet was ever persisted (adoption was 100% broken).
Pass the already-loaded interaction in, matching every other handler.
Add a regression test driving the real dispatcher path.
Removes !forage / !mine / !scavenge / !fish / !essence / !commune from
the command surface. Typing one now returns a one-line deprecation DM
pointing at the auto-harvest flow ("Harvest is automatic now — just
walk."). The internal handleHarvestCmd / handleStandaloneHarvest paths
are left in place; operator deletes after soak per plan §H3.
Help text and the supplies-low autopilot pause line no longer mention
the manual verbs.
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>
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>
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.
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()
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.
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).
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>
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>
- 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>
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>
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>
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>
The L5 close-out (596b2b7) rewired loadAdvCharacter to read from
player_meta and simultaneously deleted the 13 backfill one-shots from
Init. That's only safe if every account already has a player_meta row
from prior dual-writes — fresh restores from old backups, dev
environments cloned from older snapshots, or a prod upgrade that
skipped the soak window all silently strand every character (loader
returns empty).
Adds bootstrapPlayerMetaFromLegacy: a single consolidated migrator that
walks adventure_characters, finds rows missing from player_meta, and
fans them out via the audited upsertAllPlayerMetaFromAdvChar helper.
Idempotent (re-runs touch zero rows once migrated; no-ops post-purge).
Wired into AdventurePlugin.Init before loadAllAdvCharacters.
proddb_simulation_test.go exercises the full bootstrap + fan-out +
overlay round-trip against a copy of data/gogobee.db with 11 subtests
covering every migrated subsystem (skills, arena, babysit, streak,
death/revive, NPC counters, pet incl. JSON-packed flags, house, save
idempotency, last_active advance, fresh createAdvCharacter).
dnd_proddb_integration_test.go also calls the bootstrap explicitly to
mirror prod startup ordering.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewires loadAdvCharacter / createAdvCharacter to source from player_meta,
drops every legacy-table fallback in the loadXxxState helpers, and adds a
GOGOBEE_LEGACY_PURGE=1 gate to drop the now-cold adventure_characters
table. Schema CREATE removed and column migrations tolerate the missing
table so the purge stays sticky across reboots.
§7.4 of the migration plan is reconciled to document that
player_meta.combat_level stays — combat_stats.go consumes it via the
overlay and is shared infrastructure, not legacy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
backfillDnDCharactersFromAdv walks adventure_characters rows that have no
dnd_character row and inserts an auto-migrated character (race/class
inferred from archetypes, Level seeded from dndLevelFromCombatLevel).
Idempotent via LEFT JOIN — pending-setup drafts and existing rows are
skipped. Wired into Init after the L5f backfill.
With every legacy player guaranteed a D&D row, the soak-window fallbacks
in dndLevelForUser, rivalLevelForUser, and hospitalCostsForUser are
retired (they now floor at level 1). dndLevelFromCombatLevel itself
stays — still used by autoBuildCharacter and the !setup seed paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three player_meta columns (title TEXT, treasures_locked INT,
crafts_succeeded INT). MiscState struct + load/upsert/backfill/
projection helpers. Dual-write rides saveAdvCharacter alongside the L5e
death-state hook — Title is dormant, the other two already mutate at
sites that save.
Tests: TestPlayerMetaMiscStateBackfill_Idempotent,
TestLoadMiscState_FallsBackToAdvCharacter,
TestUpsertPlayerMetaMiscState_RoundTrip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eight player_meta columns: alive (default 1), dead_until,
death_reprieve_last, last_pardon_used (DATETIME), last_death_date,
grudge_location, death_source, death_location (TEXT).
DeathState struct + load/upsert/backfill/projection helpers. Dual-write
strategy switches: instead of per-site upserts, the dual-write rides
saveAdvCharacter itself (after the existing display_name dual-write).
Death-state mutation surface spans ~50 save sites; the saveAdvCharacter-
internal hook catches every one without scatter.
Backfill idempotent (only fills rows where every death field is still
default). Tests cover backfill, fallback, round-trip, and the
saveAdvCharacter dual-write.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ten player_meta columns: current_streak, best_streak, last_action_date,
streak_decayed, action_taken_today, holiday_action_taken,
combat_actions_used, harvest_actions_used, created_at, last_active_at.
LifecycleState struct with HasLifecycle() marker + load/upsert/backfill/
projection helpers. New resetAllPlayerMetaDailyActions parallels the
existing resetAllAdvDailyActions for the bulk midnight reset.
createAdvCharacter now seeds created_at/last_active_at (CURRENT_TIMESTAMP)
so player_meta rows are fully formed at creation.
Mutation surface turned out to be small: only `rest` writes
ActionTakenToday (combat/harvest counters are dormant in current code);
plus streak halve + streak update in scheduler. Dual-writes wired at all
four sites + the bulk reset.
Tests: TestPlayerMetaLifecycleStateBackfill_Idempotent,
TestLoadLifecycleState_FallsBackToAdvCharacter,
TestUpsertPlayerMetaLifecycleState_RoundTrip,
TestResetAllPlayerMetaDailyActions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five player_meta columns (babysit_active, babysit_expires_at,
babysit_skill_focus, auto_babysit, auto_babysit_focus). BabysitState
struct with IsActive() marker mirrors SkillState/HouseState shape.
loadBabysitState / upsertPlayerMetaBabysitState /
backfillPlayerMetaBabysitState / babysitStateFromAdvChar helpers.
Dual-writes wired at handleBabysitStart, handleBabysitCancel,
checkBabysitExpiry. Backfill is idempotent — only fills inactive rows
whose legacy counterpart is active.
AutoBabysit / AutoBabysitFocus / BabysitSkillFocus are dormant in
current code but preserved for the schema migration.
Tests: TestPlayerMetaBabysitStateBackfill_Idempotent,
TestLoadBabysitState_FallsBackToAdvCharacter,
TestUpsertPlayerMetaBabysitState_RoundTrip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eight player_meta columns (combat_level, combat_xp, mining_skill,
mining_xp, foraging_skill, foraging_xp, fishing_skill, fishing_xp).
SkillState struct with HasSkills() marker mirrors PetState/HouseState
shape. loadSkillState / upsertPlayerMetaSkillState /
backfillPlayerMetaSkillState / skillStateFromAdvChar helpers.
Dual-writes wired at every mutation site: consumables craft XP (both
success and failure branches), events.go XP grant across all skills,
arena death + session-complete combat XP. Backfill is idempotent (only
fills rows where every skill column is still zero AND the legacy row
has any non-zero value).
CombatLevel/CombatXP are transitional — dropped at L5g after the DnD
mass-backfill retires the legacy CombatLevel-derived fallback.
Tests: TestPlayerMetaSkillStateBackfill_Idempotent,
TestLoadSkillState_FallsBackToAdvCharacter,
TestUpsertPlayerMetaSkillState_RoundTrip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Eight pet_* columns added to player_meta (flags packed into pet_flags_json).
PetState + load/upsert/backfill helpers; dual-writes wired at every pet
mutation site (arrival, naming, supply-shop unlock, babysit trickle,
morning defense, misty reactivation). Backfill idempotent, runs on Init.
L4 grep-empty exit criterion intentionally NOT met for adventure_pets.go:
helpers still take *AdventureCharacter because external callers (babysit,
scheduler, npcs, combat_bridge, combat_stats, dnd_sheet, arena, character)
read pet fields directly. Cross-file signature flip belongs after the
dual-write soak.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
player_meta.hospital_visits column added with idempotent backfill.
Hospital cost now DnDCharacter.Level x 50k (5x before insurance), falling
back to dndLevelFromCombatLevel when no D&D row exists. Revive paths also
restore DnDCharacter HP to full; char.Alive still dual-writes during soak.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap all char.DisplayName / c.DisplayName reader sites in
internal/plugin/ to loadDisplayName(userID). Touches 12 files
across combat (combat_bridge, dnd_zone_combat, dnd_zone_cmd,
dnd_expedition_combat), arena, hospital, events, render,
masterwork, robbie, scheduler, and adventure.go.
Only intentional char.DisplayName references remaining are in
adventure_character.go (scan + save + dual-write) and the
player_meta.go doc comment. go vet + go test ./internal/plugin/...
clean.
Unblocks deferred L2 step 9 (arena AdvCharacter import drop)
once L4 hospital/pets/render also lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds player_meta.display_name with idempotent backfill, dual-writes from
createAdvCharacter (in-tx) and saveAdvCharacter (post-commit), and a
loadDisplayName helper with AdvCharacter fallback. Readers are not flipped
yet — soak begins now; per-call-site swap follows in a later session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the coop_dungeon system per migration plan §5. 11 coop_*.go files
removed; cleanup_l3.go added with an idempotent per-run status-guarded
UPDATE that cancels any open/active runs and refunds member contributions
plus unsettled bets via EuroPlugin.Credit. Tables retained for now;
schema purge deferred to a future GOGOBEE_COOP_PURGE pass.
adventure.go drops !coop dispatch, coopTicker, the startup combat-lock
hook, and the !coop help line. adventure_scheduler.go loses the
activeCoopMemberSet skip branch and the post-reset combat-lock call.
adventure_render.go drops the teaser plus the in-coop status block;
replaced with a one-week morning-DM closure announcement
(TODO: remove after 2026-05-16). TestPickCoopTeaserCandidate_SkipsLeader
removed.
go vet ./... and go test ./... clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Creates the player_meta table (gogobee_legacy_migration.md §2.1) with the
three columns L2 needs: arena_wins, arena_losses, invasion_score. Future
phases ALTER in their own columns. Naming follows the §2.0 callout — no
dnd_ prefix on new tables.
Helpers in internal/plugin/player_meta.go: loadPlayerMeta,
upsertPlayerMetaArena, backfillPlayerMetaArena (INSERT OR IGNORE,
idempotent, logs row count per Phase R1 precedent). Plugin Init runs
the backfill once on startup.
Dual-write per §11: every char.ArenaWins++/ArenaLosses++ in
adventure_arena.go also calls upsertPlayerMetaArena. AdvCharacter
columns stay in place — reads will switch back to AdvCharacter trivially
if a rollback is needed before L5 teardown.
Read swap: handleArenaStats and renderDnDSheet now load player_meta and
prefer its values (falling back to AdvCharacter if the row is missing,
which only matters during the deploy window before backfill runs).
renderArenaPersonalStats's signature changed from (char, stats) to
(displayName, wins, losses, stats) so the render is DB-free.
Tests: TestPlayerMetaArenaBackfill_Idempotent verifies double-run
backfill is a no-op and doesn't clobber post-backfill dual-writes;
TestUpsertPlayerMetaArena_RoundTrip covers insert/update/missing-user
paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GM→DM rename across docs and code (GMNarrationType→DMNarrationType,
GMState→DMState, narration constants, comments) so the system reads as
"Dungeon Master" everywhere. Player-visible "GM mood" wording stays
where it appears in flavor.
Streamed zone/expedition combat: zone advance now stages patrol →
patrol play-by-play → patrol resolution → room intro → room play-by-play
→ final outcome through sendZoneCombatMessages with 2–3s pacing
(arena keeps its 5–8s window). Combat narrative lines pick up a compact
d20-vs-AC roll annotation for hit/crit/miss/block events.
Combat outcome polish: dndHPSnapshot lets narration show sheet HP
rather than legacy combat-engine HP, and markAdventureDead clears the
zombie state where hp_current was 0 but the legacy alive flag stayed
true after a D&D-layer KO.
Adv 2.0 announcement (ADVENTURE_2.0_ANNOUNCEMENT.md), README rewrite
covering the new layer, and adv2_scenario_test.go — a full
zone-run + expedition + harvest playthrough against a copy of the prod
DB asserting persisted state end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Retires the standalone !adventure dungeon/mine/forage/fish daily loop in
favor of !expedition. Gate intercepts legacy 1/2/3/4 + word-form input
and DMs a deprecation notice; town services (shop/blacksmith/rest/thom)
stay on !adventure.
- Delete dead resolveActivity (269 lines) + parseActivityLocation; add
isLegacyActivityInput + renderLegacyActivityDeprecation.
- Morning-DM menu rewritten to point at !expedition and the !forage etc.
harvest commands; writeAdvLocationLines removed (sole caller).
- New dnd_r1_migration.go runs archiveOrphanZoneRuns at Init: archives
any active dnd_zone_run not linked to an active expedition (via
exp.run_id or region_state.region_runs). Idempotent.
- Pre-existing flake in TestPickEveningRecap_BossOverridesAll fixed
(E6c pool added entries without literal "boss"; widen substring set).
- Internal helpers (resolveAdvAction, advEligibleLocations, AdvLocation
tier data, consumable drops) preserved — babysit/scheduler still use
them passively.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
R3 — Combat Event Integration:
- dnd_expedition_combat.go: Combat Interrupt rolls (§4.2) with
threat-clock and Ranger-wilderness modifiers; Patrol Encounters
scaled by threat level; recordZoneKill writer with monsterKillTags.
- Interrupt gate at head of handleHarvestCmd; patrol gate before
resolveRoom in zoneCmdAdvance; kill writer wired into combat-win
paths.
R4a — Zone Loot Tables:
- dnd_zone_loot.go: §5 loot drop tables for all 10 zones × 5 tiers
with §8.1 sell-value bands. dropTierFromCR brackets + boss floor.
- Hooks on combat-win in resolveCombatRoom, resolveBossRoom,
runHarvestInterrupt, tryPatrolEncounter.
R4b — Fishing Integration:
- dnd_zone_fish.go: fishingZones allow-list, fishingSkillBonus,
rangerRareCatchUpgrade (§6.2), feywildFishDistortion narration.
- §6.1 fish entries added to resource registry for Forest, Sunken
Temple, Underdark, Feywild zones.
- !fish wired through handleHarvestCmd; zoneItemFlavor matrix
populated for all 10 zones × all loot items.
R5 — Economy Integration:
- dnd_economy.go: !sell (post-expedition gate, single CHA Persuasion
DC 17 → +15% bump), !craft (§8.2 4 exemplar recipes), !lore
(INT/Arcana DC 15 recipe discovery).
- dnd_known_recipe table for persistent recipe discovery.
Flavor reuse: HarvestInterrupt, PatrolEncounter, CombatVictory,
PlayerDeath, LootDrop*, FeywildTimeDistortion* — all existing pools.
No new flavor file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-zone resource registry (§3, all 10 zones) and full per-room harvest
layer wired into expeditions: !forage / !mine / !scavenge / !essence /
!commune / !resources. Auto-spawns a DungeonRun per region on
!expedition start, swaps it on !region travel, retires on
abandon/extract. Long rest at camp replenishes nodes across every
room and region. Reuses existing flavor.Harvest* / RichYield /
NodeDepleted / per-zone Harvest<Zone> pools.
Also clears two pre-existing test failures introduced by the E6c
flavor expansion (briefing substring list + combat-lift trial count).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds renderRegionChain — for tier 4-5 multi-region zones, renders the
4-region progression (e.g. ST──DO──IW──TDT for Underdark) with ✓
cleared / ▶ here / · pending markers, and ⛺ overlay where a base
camp has been pitched. Single-region zones skip the chain and fall
through to the existing renderZoneMap room layout.
!map (top-level) and !expedition map both invoke handleExpeditionMapCmd
which composes: region chain → current region label → per-run room
map → legend. Region abbreviations are decoded in a "Regions:" footer
so the glyph row stays compact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>