Files
gogobee/gogobee_legacy_migration.md
prosolis 3b4dfa44d3 Adv 2.0 L2: rip legacy arena combat path; boss flow is the only path
Cancels the planned ARENA_BOSS_FLOW soak. The boss flow is shipped
unconditionally with no legacy fallback — boss-flow errors now surface
to the player and abort the round rather than silently falling back to
the old CombatPower path.

Deleted:
- runArenaCombat (combat_bridge.go) — legacy CombatPower-vs-Lethality
  arena combat driver.
- RenderCombatLogArena, renderArenaOutcome (combat_narrative.go) — legacy
  arena renderers. RenderCombatLogArena was a thin alias for
  RenderCombatLog; renderArenaOutcome was already dead.
- renderArenaCombatFinalMessage (combat_bridge.go) — legacy "rewards +
  closer" trailing text.
- arenaWinCloser, arenaLoseCloser (adventure_arena_combat.go) — flavor
  helpers that only fed the deleted renderers. File now just holds
  arenaParticipationXP.
- arenaBossFlowEnabled + the ARENA_BOSS_FLOW env var (adventure_arena.go,
  .env.example). The env gate served only the now-removed fallback.
- sendArenaCombatMessages (adventure_arena.go) — wrapper that switched
  pacing between boss flow and legacy. Replaced with direct
  sendZoneCombatMessages calls at the three call sites.
- TestArenaBossFlowEnabled (bossflow test) and
  TestRenderCombatLogArena_ProducesPhaseMessages (narrative test).

Simplified resolveArenaRound / resolveArenaSurvival / resolveArenaDeath:
the bossNarr-vs-nil branches collapsed since narration is now always
produced by resolveArenaBoss. Equipment degradation and the death-save
reprieve hook are unchanged — both already worked off the boss-flow
CombatResult.

Migration doc (§4) updated to record the cancellation and the
remaining tuning approach (playtest-driven, no flag soak window).

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

449 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# GogoBee — Legacy Adventure → D&D Engine Migration Plan
> **Companion to:** `gogobee_dnd_design_doc.md`, `gogobee_expedition_system.md`, `gogobee_resource_combat_integration.md`
> **Version:** 0.1 (planning draft)
> **Status:** Pre-implementation — scoping
> **Branch:** `adv-2.0`
---
## 0. Why this doc exists
Phase R (resource/combat integration) shipped the new harvest/combat surface and gated the player-facing legacy daily-loop dispatch. The internal helpers (`resolveAdvAction`, `advEligibleLocations`, `AdvLocation` tier data, `AdventureCharacter`, `CombatLevel`, `combat_engine.go`) are still alive because non-deprecated subsystems consume them: arena, co-op dungeons, babysit/scheduler, hospital, rival, masterwork, pets, housing, mortgage, twinbee morning DM.
This doc inventories every surviving caller and lays out a phased deletion path. The endgame is: `AdventureCharacter` deleted, `CombatLevel` field gone, `combat_engine.go` removed, `combat_bridge.go` removed, branching dungeon paths become buildable on a clean schema.
**Non-goals.** This is not a redesign. We are not adding features during the migration. New mechanics (branching paths, multi-region expansions, new arena formats) are sequenced *after* L5 teardown lands.
---
## 1. Inventory — what's still on the legacy engine
| Subsystem | Files | LOC | AdvCharacter coupling | CombatLevel coupling | D&D parallel | Risk |
|---|---|---|---|---|---|---|
| Babysit/scheduler | `adventure_babysit.go`, `adventure_scheduler.go` | 1,169 | mutates: CombatXP, Alive, ActionTakenToday | reads | none (auto-actions) | low |
| Arena | `adventure_arena*.go` (4 files) | 1,228 | mutates: Alive, ArenaWins/Losses, CombatXP | gates tier, scales rewards | `dnd_zone_combat.go` | high |
| Co-op dungeons | `coop_dungeon*.go` (9 files) | 4,027 | reads-only (minLevel gate, liability) | gates participation | `dnd_expedition_combat.go` | medium |
| Hospital | `adventure_hospital.go` | 288 | mutates: Alive, HospitalVisits | cost = level × 25k/125k | none | low |
| Rival | `adventure_rival.go` | 866 | mutates: RivalPool, CombatXP | unlock at L5, stake calc | none | medium |
| Masterwork | `adventure_masterwork.go` | 543 | reads equip, no char mutation | none | none | low |
| Pets | `adventure_pets.go` | 448 | mutates pet fields on AdvCharacter | none | none | low |
| Housing/mortgage | `adventure_housing.go`, `adventure_mortgage.go` | 922 | mutates House* fields | none | none | low |
| TwinBee/render | `adventure_render.go`, `adventure_twinbee.go` | 1,472 | reads stats; simulator calls `resolveAdvAction` | reads | none | low |
| Combat bridge | `combat_bridge.go` | 363 | translation layer | — | — | (delete) |
**Source of truth:** `AdventureCharacter` is defined at `adventure_character.go:27` (76 fields). `DnDCharacter` at `dnd.go:152`.
**Live `resolveAdvAction` callers (post-R1):** `adventure_babysit.go:349`, `adventure_twinbee.go:135` (simulator).
---
## 2. Architectural decision — char unification strategy
Three options were considered:
**A. Big-bang field merge.** Move every surviving AdvCharacter field onto DnDCharacter, then s/AdventureCharacter/DnDCharacter/g. Rejected: unsafe, no incremental landing, breaks every test for one PR.
**B. Two-character coexistence forever.** Keep both, route by feature. Rejected: this is what we have now; doesn't actually retire anything.
**C. Phased subsystem migration with shared "extra" record.** ✅ Chosen. Each subsystem is migrated to read/write D&D state where there's a parallel, and to a new `player_meta` table for fields that don't fit on a character (HouseTier, RivalPool, ArenaWins, BabysitActive, MistyLastSeen, etc.). When all subsystems are migrated, AdvCharacter is deleted in a single L5 commit.
The `player_meta` table is the holding pen for non-stat per-user state that's currently squatting on AdvCharacter. It's not a redesign — it's a structural move so the deletion can happen cleanly.
### 2.1 `player_meta` schema (proposed)
> **Naming note.** New tables/types in this migration deliberately avoid the `dnd_` prefix used by older code (legal/trademark exposure flagged by user). Existing `dnd_*` symbols stay in place — only new surface uses neutral names.
```sql
CREATE TABLE player_meta (
user_id TEXT PRIMARY KEY,
-- arena
arena_wins INTEGER NOT NULL DEFAULT 0,
arena_losses INTEGER NOT NULL DEFAULT 0,
invasion_score INTEGER NOT NULL DEFAULT 0,
-- rival
rival_pool INTEGER NOT NULL DEFAULT 0,
rival_unlocked_notified INTEGER NOT NULL DEFAULT 0,
-- housing
house_tier INTEGER NOT NULL DEFAULT 0,
house_loan_balance INTEGER NOT NULL DEFAULT 0,
house_loan_frozen INTEGER NOT NULL DEFAULT 0,
house_missed_payments INTEGER NOT NULL DEFAULT 0,
house_autopay INTEGER NOT NULL DEFAULT 0,
house_current_rate REAL NOT NULL DEFAULT 0,
-- pets
pet_type TEXT NOT NULL DEFAULT '',
pet_name TEXT NOT NULL DEFAULT '',
pet_xp INTEGER NOT NULL DEFAULT 0,
pet_level INTEGER NOT NULL DEFAULT 0,
pet_armor_tier INTEGER NOT NULL DEFAULT 0,
pet_flags_json TEXT NOT NULL DEFAULT '{}', -- chased_away/reactivated/arrived/morning_defense/etc
-- npcs
misty_last_seen INTEGER,
arina_last_seen INTEGER,
misty_buff_expires INTEGER,
misty_debuff_expires INTEGER,
arina_buff_expires INTEGER,
misty_encounter_count INTEGER NOT NULL DEFAULT 0,
misty_donated_count INTEGER NOT NULL DEFAULT 0,
misty_roll_target INTEGER NOT NULL DEFAULT 0,
arina_roll_target INTEGER NOT NULL DEFAULT 0,
-- babysit
babysit_active INTEGER NOT NULL DEFAULT 0,
babysit_expires_at INTEGER,
babysit_skill_focus TEXT NOT NULL DEFAULT '',
auto_babysit INTEGER NOT NULL DEFAULT 0,
auto_babysit_focus TEXT NOT NULL DEFAULT '',
-- hospital / death
hospital_visits INTEGER NOT NULL DEFAULT 0,
last_death_date TEXT NOT NULL DEFAULT '',
death_source TEXT NOT NULL DEFAULT '',
death_location TEXT NOT NULL DEFAULT '',
last_pardon_used INTEGER,
death_reprieve_last INTEGER,
-- streaks / chores
current_streak INTEGER NOT NULL DEFAULT 0,
best_streak INTEGER NOT NULL DEFAULT 0,
last_action_date TEXT NOT NULL DEFAULT '',
grudge_location TEXT NOT NULL DEFAULT '',
holiday_action_taken INTEGER NOT NULL DEFAULT 0,
streak_decayed INTEGER NOT NULL DEFAULT 0,
-- shop / drops
masterwork_drops_received INTEGER NOT NULL DEFAULT 0,
treasures_locked INTEGER NOT NULL DEFAULT 0,
pet_supply_shop_unlocked INTEGER NOT NULL DEFAULT 0,
pet_level_10_date TEXT NOT NULL DEFAULT '',
thom_animal_line_fired INTEGER NOT NULL DEFAULT 0,
-- chat / npc msg
npc_msg_count INTEGER NOT NULL DEFAULT 0,
npc_msg_count_date TEXT NOT NULL DEFAULT '',
robbie_visit_count INTEGER NOT NULL DEFAULT 0,
crafts_succeeded INTEGER NOT NULL DEFAULT 0
);
```
### 2.2 Stat mapping — AdvCharacter → DnDCharacter
**Decisions locked in by user 2026-05-08:**
- **CombatLevel→Level:** direct copy. CombatLevel doesn't carry semantic meaning beyond "starting Level for the new system." No compression formula. Players keep their numeric value as their D&D Level.
- **CombatXP→XP:** **discarded.** Players start fresh on the new XP track (CombatXP is not migrated). The Level set above is their floor; XP resets to 0 at that level's threshold.
- **AdvEquipment:** kept as-is. Stays at `AdvEquipment` type and `adventure_equipment` table. No rename to `dnd_*` (per naming guidance — see §2.0 callout above).
| AdvCharacter | DnDCharacter destination | Notes |
|---|---|---|
| `CombatLevel` | `Level` | Direct copy on backfill; no formula. |
| `CombatXP` | — | **Not migrated.** XP starts at the floor of the new Level. |
| `MiningSkill` / `ForagingSkill` / `FishingSkill` | retained as new fields on DnDCharacter | These are *skills*, not class levels. Add `MiningSkill/ForagingSkill/FishingSkill int` to DnDCharacter; keep XP fields too. |
| `Alive` / `DeadUntil` | DnDCharacter `HPCurrent==0` + `DeadUntil` | Need to add `DeadUntil *time.Time` to DnDCharacter. |
| `Title` | new field on DnDCharacter | Direct copy. |
| equipment (`AdvEquipment`) | stays at `AdvEquipment` / `adventure_equipment` | Type and table name unchanged. Read paths drop AdvCharacter unwrap; no rename pass. |
### 2.3 `CombatLevel` retuning
Legacy CombatLevel scaled 150ish through grindy XP. D&D Level scales 120. Hospital/rival/co-op gates that read CombatLevel will be re-tuned to D&D Level brackets:
| Legacy gate | New gate |
|---|---|
| Hospital cost = `CombatLevel × 25k` (or × 125k post-cap) | `Level × 50k` (or × 250k at L20+) |
| Rival unlock at CombatLevel ≥ 5 | Rival unlock at Level ≥ 3 |
| Arena tier brackets (every 10 CL) | Arena tier brackets (every 4 levels) |
| Co-op dungeon minLevel | Re-tune per dungeon (table in L3 section) |
These thresholds are placeholders — final tuning happens during each phase based on playtest.
---
## 3. Phase L1 — Babysit & scheduler
**Goal:** Kill the last live `resolveAdvAction` caller. Auto-actions become expedition-aware.
**Files touched:** `adventure_babysit.go`, `adventure_scheduler.go`, `adventure_twinbee.go` (simulator), `adventure_activities.go` (delete `resolveAdvAction` + `advEligibleLocations` after callers gone).
**Design:**
- Babysit currently picks one of three skills (mining/foraging/fishing) and runs `resolveAdvAction` once a day for the user. The new flow:
- If user has an active expedition → babysit attempts a single harvest action (`!forage`/`!mine`/`!fish` per skill focus) inside the user's current room. Goes through `handleHarvestCmd` headlessly with `babysit:true` flag so combat interrupts force-extract instead of running combat. (User isn't online; can't fight.)
- If user has no active expedition → babysit posts a TwinBee nudge (one per real day, rate-limited to once/3d) suggesting `!expedition start`. No XP awarded.
- `auto_babysit` becomes "auto-harvest while expedition active." `babysit_skill_focus` carries through unchanged.
- `adventure_twinbee.go:135` simulator (uses fake CombatLevel=35 character) — replace with a fixed flavor-only narration. The simulator was always fake; deleting the AdvCharacter dependency is mechanical.
**Steps:**
1. Add `auto_babysit` / `babysit_*` columns to `player_meta` (per §2.1) — migration + read/write helpers.
2. Add `babysit:true` flag to harvest dispatch path; on combat-interrupt branches Standard/Elite/Patrol, force-extract the region run instead of entering combat. Test.
3. Rewrite `babysitDo` to call new harvest path. Remove `resolveAdvAction` call. Test (covers: active expedition, no expedition, harvest yields → inventory, combat interrupt force-extract).
4. Rewrite scheduler entrypoint analogously.
5. Replace twinbee simulator with static flavor.
6. Delete `resolveAdvAction`, `advEligibleLocations`, `AdvActionResult`, `parseActivityLocation` from `adventure_activities.go`. Audit for last callers via grep.
7. `go test ./... && go vet`.
**Tests added:** `adventure_babysit_test.go` (currently absent — create). Cases: babysit-with-expedition harvests, babysit-without-expedition nudges, combat-interrupt during babysit force-extracts cleanly, daily rate-limit holds.
**Exit criteria:**
- `grep resolveAdvAction internal/plugin/` returns 0 hits.
- `go test ./...` clean.
- Production users with `auto_babysit=true` and an active expedition see harvest deposits, not legacy activity output.
**Risk:** Babysit users have a behavior change (now auto-harvests instead of always rolling). Announce in `ADVENTURE_2.0_ANNOUNCEMENT.md`.
---
## 4. Phase L2 — Arena (rebuild on Adventure 2.0 boss flow)
**Decision (2026-05-08):** Arena migrates to the **Adventure 2.0 boss flow** — the same staged narrative path zone bosses run through (`resolveBossRoom` in `dnd_zone_cmd.go:630`). Each arena fight is treated as a self-contained boss encounter: full combat log via `RenderCombatLog`, TwinBee mood lines on Nat20/Nat1, phase-two narration when the enemy crosses its `PhaseTwoAt` HP threshold, win/loss outcome blocks. Arena stops being "numbers vs numbers" and becomes a narrated event with the same texture as a zone boss room.
This is also the right shape because zone-boss plumbing (bestiary, mood events, phase transitions, kill records, loot drops) is already built and tested. We re-use it rather than re-implementing.
**Files touched:**
- `adventure_arena.go` — entrypoint, gating, payout. Heavy rewrite.
- `adventure_arena_combat.go` — round driver. Replaced by a call into `runZoneCombat` + a new arena-specific staged renderer (or direct re-use of the zone boss render — see step 4).
- `adventure_arena_render.go` — reads switch to DnDCharacter + player_meta.
- `adventure_arena_monsters.go`**deleted as standalone**. Arena monsters are folded into the existing bestiary as boss-shaped entries (with `PhaseTwoAt`).
- `adventure_arena_test.go` — ported.
- New (small): `adventure_arena_flavor.go` — only if zone TwinBee pools don't cover arena context. Check existing `adventure_flavor_*.go` first per the reuse-flavor feedback rule before creating this file.
**Design specifics:**
- **Arena monster definitions.** Each existing `ArenaMonster` becomes a `BestiaryEntry` shape: `{ID, Name, HP, AC, AB, DamageDie, CR, PhaseTwoAt, Tags}`. Arena IDs are namespaced `arena_<tier>_<slug>` so they don't collide with zone bestiary IDs. Stored in a new `arenaBosses` map (still in `adventure_arena_monsters.go` if the file is kept; otherwise inlined). Tier brackets controlled by Arena tier (see below).
- **Combat invocation.** Arena round driver builds `BossEncounter{Monster, PhaseTwoAt, ZoneIDForFlavor}` and calls a small new function `resolveArenaBoss(userID, encounter)` that wraps the same `runZoneCombat``RenderCombatLog` → mood/phase-two lines flow as `resolveBossRoom`. **Important:** arena is not inside a zone, so we factor the staged-narration body out of `resolveBossRoom` into a shared helper `renderBossOutcome(...)` that both call sites use. (`resolveBossRoom` keeps its zone-specific glue: zone loot drop, `applyBossDefeatThreat`, `abandonZoneRun`. Arena keeps its glue: Arena win/loss counters, payout, no zone loot.)
- **TwinBee flavor in arena.** `twinBeeLine(zoneID, ...)` is keyed by zone; Arena passes a synthetic `ZoneArena` ID. Add `ZoneArena` to the zone enum and populate a small TwinBee mood/Nat20/Nat1/BossDeath/PlayerDeath pool — reuse existing pools where possible (per `feedback_reuse_existing_flavor`); only add new lines if the existing pools genuinely don't read right in arena context.
- **Phase-two thresholds.** Arena bosses get `PhaseTwoAt` defaults: tier 12 = no phase two (numeric drama only); tier 3 = 50% HP; tier 45 = 50% HP + a flavor barb. This gives arena fights a structural beat without overengineering.
- **Tier gating.** `tierFromCombatLevel``tierFromLevel`. Brackets:
| DnDCharacter.Level | Arena tier |
|---|---|
| 13 | I (Pup) |
| 47 | II (Pit) |
| 812 | III (Crucible) |
| 1317 | IV (Coliseum) |
| 1820 | V (Apex) |
- **State.** `ArenaWins`, `ArenaLosses`, `InvasionScore` move to `player_meta` (per §2.1). XP awards go through whatever the existing D&D XP path is (single source of truth — verify in `dnd_sheet.go` during implementation).
- **Death.** Arena death now flows through the same `markAdventureDead(userID, "arena", arenaTierLabel)` hook the zone boss path uses. Hospital revive (post-L4a) heals from this same state.
**Steps:**
1. Factor `renderBossOutcome` helper out of `resolveBossRoom`. Two callers post-extraction: zone boss path and (TBD) arena path. Smoke-test the zone boss path is unchanged via `adventure_arena_test.go` parity once arena lands on it.
2. Add `ZoneArena` ID + minimal TwinBee mood pools (reuse where possible).
3. Convert arena monsters to boss-shaped bestiary entries with `PhaseTwoAt`.
4. Build `resolveArenaBoss(userID, encounter)` calling `runZoneCombat` + `renderBossOutcome`. Wire into existing arena entrypoint in `adventure_arena.go`.
5. Migrate `ArenaWins/Losses/InvasionScore` to `player_meta`. Dual-write per §11.
6. Swap render reads to DnDCharacter + player_meta.
7. Port `tierFromCombatLevel``tierFromLevel` with new brackets.
8. Port `adventure_arena_test.go`. Add a new test asserting arena win surfaces the staged combat log + TwinBee BossDeath line.
9. Drop AdvCharacter imports from arena files. **DEFERRED (2026-05-09):** blocked on DisplayName migration (L4f-prep, see §7) plus L4 hospital/pets/XP work. Arena code still needs `char.DisplayName` (stats DM, T5/helm announces, render), `char.PetName` (pet-recovery game-room msg), `char.DeathReprieveLast`, `char.CombatXP`, `char.Alive`/`transitionDeath`. Counter dual-writes to `char.ArenaWins/Losses/InvasionScore` could be dropped now (player_meta is source of truth post-step 6), but the surrounding `loadAdvCharacter`/`saveAdvCharacter` calls stay for the other fields, so step 9's exit grep can't pass at L2 time. Revisit after DisplayName migration ships and L4 (hospital/pets/render) lands.
10. `go test ./... && go vet`.
**Feature flag.** ~~Originally planned an `ARENA_BOSS_FLOW=1` soak.~~ Cancelled 2026-05-09: shipped no-flag, no legacy fallback. The legacy `runArenaCombat` / `RenderCombatLogArena` / `renderArenaCombatFinalMessage` / `renderArenaOutcome` / `arenaWin/LoseCloser` paths were deleted in the same commit. Boss-flow errors surface to the player and abort the round rather than falling back.
**Exit criteria:**
- ~~`grep -l 'AdventureCharacter\|CombatLevel\|combat_engine' internal/plugin/adventure_arena*.go` empty.~~ **Deferred** with step 9 — see note above. The grep will only clear once DisplayName migration + L4 (hospital/pets/XP/render) ship; L2 closes without it.
- Arena win produces a staged combat log + TwinBee BossDeath line + payout, just like a zone boss.
- Arena loss produces a TwinBee PlayerDeath line + arena death flag, no zone-run side effects.
- Existing arena DB rows readable; ArenaWins backfill from AdvCharacter into `player_meta` (§8).
**Risk:**
- D&D combat is swingier than legacy CombatPower; tune HP/AC tables against in-flight playtest data (no flag soak — legacy path is gone).
- Refactoring `resolveBossRoom` to extract `renderBossOutcome` could regress zone bosses. Mitigate: do the extraction in step 1 alone, ship it, run a full zone-boss test pass before continuing to step 2.
---
## 5. Phase L3 — Co-op dungeons (DELETION, not migration)
**Decision (2026-05-08):** Drop co-op dungeons entirely. Do **not** migrate. The user has a different design in mind for the future co-op slot; the current implementation is being retired wholesale.
**Files deleted:**
- `coop_dungeon.go`
- `coop_dungeon_db.go`
- `coop_dungeon_render.go`
- `coop_dungeon_scheduler.go`
- `coop_dungeon_balance.go`
- `coop_dungeon_balance_test.go`
- `coop_dungeon_betting.go`
- `coop_dungeon_gifts.go`
- `coop_dungeon_stats.go`
- `coop_dungeon_test.go`
- `coop_event_meta.go`
- `coop_flavor_twinbee.go`
**Steps:**
1. Find all co-op references outside the `coop_*` files (commands registered in `adventure.go`, scheduler hooks, render hooks, betting integration). Grep `coop` and `Coop` across the package.
2. Remove command handlers from `adventure.go::OnMessage` and `Commands()`.
3. Remove scheduler ticks that call coop entrypoints.
4. Drop the `coop_dungeon*` SQL tables in a separate `cleanup_l3.go` migration gated behind `GOGOBEE_COOP_PURGE=1`. Default off — keep historical rows around until manually flipped.
5. Delete the files.
6. Add a TwinBee announcement line for active players: "co-op dungeons closed for now; expedition is the way forward; better thing coming." Wire into morning DM for one week post-deploy.
7. `go test ./... && go vet`.
**Exit criteria:**
- `grep -rn 'coop_dungeon\|CoopDungeon' internal/plugin/ --include='*.go'` returns 0 (or only in archive/migration).
- `go test ./...` clean.
**Risk:** Players with active co-op state at deploy time. Mitigate: scheduler closes any in-flight session at L3 deploy with a refund of staked coins; no XP/loot awarded for cancelled sessions. Refund logic is one-shot in the L3 migration, not preserved code.
---
## 6. Phase L4 — Stat/perk gates
**Goal:** Hospital, rival, masterwork, pets, housing, mortgage all read DnDCharacter + `player_meta` exclusively. AdvCharacter still exists but is unreferenced outside its own file.
### 6.1 L4a — Hospital
- `adventure_hospital.go`: cost formula `CombatLevel × 25k``Level × 50k`. `HospitalVisits``player_meta`. Revive flips DnDCharacter HP from 0 → full instead of `Alive=true`.
### 6.2 L4b — Rival
- `adventure_rival.go`: unlock at Level ≥ 3 (was CombatLevel ≥ 5). RivalPool → `player_meta`. Duel combat already uses `combat_engine` — port to `simulateCombat` here too (small extra step).
- Move rival flavor into existing `adventure_flavor_rival.go` — no new file.
### 6.3 L4c — Masterwork
- `adventure_masterwork.go`: no AdvCharacter mutations today; only equipment reads. Port `advMasterworkSkillBonus` to take `*DnDCharacter` (skill fields added in §2.2). MasterworkDropsReceived → `player_meta`.
### 6.4 L4d — Pets
- `adventure_pets.go`: pet fields move from AdvCharacter to `player_meta` (see §2.1 `pet_*` columns). Pet combat hooks (PetMorningDefense, pet damage rolls) target DnDCharacter HP.
### 6.5 L4e — Housing & mortgage
- `adventure_housing.go`, `adventure_mortgage.go`: HouseTier/loan fields → `player_meta`. `houseHPBonus` → applied to `DnDCharacter.HPMax` calculation (already a parallel hook in `dnd_sheet.go`'s `recomputeHPMax`).
- Mortgage scheduler tick stays — only the field source changes.
### 6.6 L4f — TwinBee/render
- `adventure_render.go`: morning DM reads from DnDCharacter + `player_meta`. Coop teaser gating moves to DnDCharacter.Level.
- `adventure_twinbee.go`: simulator already addressed in L1. Remaining usage is read-only stat display — port to DnDCharacter.
**Steps (per sub-phase):** column add → backfill → swap reads → swap writes → port tests → grep check.
**Exit criteria L4 overall:** `grep -l 'AdventureCharacter\|CombatLevel' internal/plugin/adventure_{hospital,rival,masterwork,pets,housing,mortgage,render,twinbee}.go` empty.
**Risk:** Each sub-phase is independent — can interleave with playtest. Housing has the most fields; do it last in L4 since the column count is largest.
---
## 7. Phase L5 — Teardown
**Goal:** Delete legacy types, files, and `combat_engine`. Branching dungeon paths become buildable.
**Pre-condition: DisplayName migration (L4f-prep, ~2026-05-09 note).** The plan as originally written never picked a new home for `AdventureCharacter.DisplayName`. It is *not* legacy-specific — it's the player's chosen identity, used in ~250 spots across arena, rival, scheduler, expedition, holdem, coop. L5's pre-condition `grep AdventureCharacter == 0` cannot pass while DisplayName still lives on AdvCharacter.
**Recommendation:** add `display_name TEXT NOT NULL DEFAULT ''` to `player_meta` as a self-contained step before L4f. Backfill from `adventure_character.display_name` (same shape as §8). Add a `loadDisplayName(userID)` helper that reads from `player_meta` and falls back to AdvCharacter for one soak week, then flip readers. Per-call-site swap is mechanical (`char.DisplayName``loadDisplayName(uid)` or pass-through from caller).
**Sizing:** ~0.5 day for the schema/backfill/helper, then 1 day to swap the ~250 read sites in batches per file. Slot it after L2 wraps and before L4f starts.
**Pre-conditions:**
- L1L4 all shipped.
- DisplayName migrated to `player_meta` (see above).
- `grep -rn 'AdventureCharacter' internal/plugin/ --include='*.go' | grep -v adventure_character.go | grep -v _test.go` returns 0.
- `grep -rn 'CombatLevel' internal/plugin/ --include='*.go'` returns 0 (or only in archive/migration code).
**Files deleted:**
- `adventure_character.go`
- `adventure_activities.go` (already gutted in L1)
- `combat_engine.go`
- `combat_engine_test.go`
- `combat_bridge.go`
- `combat_bridge_test.go`
- `combat_stats.go` (if unused; verify)
- `combat_stats_test.go`
**DB cleanup:**
- `adventure_character` table: drop after a 30-day grace period in case of rollback. Schedule with a migration script `dnd_l5_cleanup.go` that runs only when env var `GOGOBEE_LEGACY_PURGE=1` is set, to gate the destructive op. Default off; manual flip after confirming.
- Equipment table stays — it was always per-user, never per-AdvCharacter.
**Code cleanup:**
- Remove `AdvLocation` tier data unless something else picked it up.
- Remove all `AdvBonusSummary` plumbing.
- Final `go vet ./... && go test ./...` clean.
**Branching dungeon paths:** unblocked. The `dnd_zone_run` schema is the only one we still own; rebuild/expand it without AdvCharacter pressure. Out of scope for this doc — see `project_branching_paths_post_legacy.md` memory note.
---
## 8. Cross-cutting — DB backfills
Every phase that moves a field from `adventure_character` to `player_meta` runs the same shape of backfill:
```go
// In Init() or a one-shot migration runner
func backfillPlayerMeta(db *sql.DB) error {
rows, _ := db.Query(`SELECT user_id, arena_wins, arena_losses, ... FROM adventure_character`)
for rows.Next() {
// INSERT OR IGNORE into player_meta
}
}
```
**Idempotency rules (per Phase R1's `archiveOrphanZoneRuns` precedent):**
- All backfills use `INSERT OR IGNORE`.
- All backfills are safe to re-run.
- Each backfill logs the row count it touched.
**Order of backfills:**
1. L1 — `babysit_*`, `auto_babysit*` columns.
2. L2 — `arena_wins`, `arena_losses`, `invasion_score`.
3. L3 — none (co-op didn't store on AdvCharacter).
4. L4a — `hospital_visits`, `last_death_date`, `death_source`, `death_location`, `last_pardon_used`, `death_reprieve_last`.
5. L4b — `rival_pool`, `rival_unlocked_notified`.
6. L4c — `masterwork_drops_received`, `treasures_locked`.
7. L4d — pet fields.
8. L4e — house fields.
9. L4f — streak/title/grudge fields, NPC msg counters, NPC seen/buff timestamps.
10. L5 — drop `adventure_character` table.
---
## 9. Testing strategy
- Each phase ships with new tests for the migrated subsystem and a `*_migration_test.go` covering backfill idempotency (matching R1's pattern).
- Existing tests are *ported*, not deleted — same test names, fixtures swap from AdvCharacter to DnDCharacter.
- A new `legacy_residue_test.go` runs at the package level and `grep`s the source tree for forbidden symbols once each phase lands; this guards against regressions.
```go
// legacy_residue_test.go skeleton
func TestNoCombatLevelReferencesAfterL5(t *testing.T) {
if !legacyMigrationDone { t.Skip() }
// walk filepath, fail on grep "CombatLevel"
}
```
---
## 10. Sequence & rough sizing
| Phase | Est. session count | Blocking on |
|---|---|---|
| L1 — Babysit/scheduler | 1 | — |
| L2 — Arena (boss flow rebuild) | 23 | feature flag in place; `renderBossOutcome` extraction lands first |
| L3 — Co-op dungeons (DELETE) | 0.5 | — (independent of L2) |
| L4a — Hospital | 0.5 | — |
| L4b — Rival | 1 | — |
| L4c — Masterwork | 0.5 | DnDCharacter skill fields exist (§2.2) |
| L4d — Pets | 1 | `player_meta` schema |
| L4e — Housing/mortgage | 1 | — |
| L4f — TwinBee/render | 0.5 | L1L4 done |
| L5 — Teardown | 1 | all above + 30-day grace |
**Total:** ~10 sessions over ~36 weeks of real time, including playtest soak between phases. Don't compress this into a single sprint — each phase needs production exposure before the next one builds on it.
---
## 11. Rollback strategy
Each phase is reversible until L5:
- L1L4 do not delete columns or tables. They add `player_meta` columns and dual-read briefly during the swap.
- If a phase regresses, revert the read-site swap; backfill data still flows back to `adventure_character` because L1L4 do **not** stop writing to AdvCharacter until §6 dual-write is dropped per phase.
**Dual-write rule:** for the duration of L1L4, every write to a migrated field goes to **both** AdvCharacter and `player_meta`. Reads come from `player_meta`. This is removed phase-by-phase only after one full real week with no rollback triggered.
L5 is the irreversible step. Gate on `GOGOBEE_LEGACY_PURGE=1`.
---
## 12. Decisions locked (2026-05-08)
All four pre-implementation questions answered:
1. **CombatLevel→Level:** direct copy. CombatLevel doesn't have semantic meaning — it's just "starting Level." No formula, no cap-compression.
2. **AdvEquipment:** kept as-is. No rename. Type stays `AdvEquipment`, table stays `adventure_equipment`. (Naming guidance: avoid new `dnd_*` symbols throughout this migration — see §2.1 callout.)
3. **CombatXP→XP:** discarded. Players start fresh; new XP accumulates from 0 at the floor of their migrated Level.
4. **Co-op dungeons:** dropped entirely (see §5). Not migrated. Different design coming later.
No outstanding pre-implementation questions. Tuning questions during each phase (HP/AC tables, threshold brackets) get decided in the moment.
---
*End of Legacy Migration document.*
*Pair with companion docs in Claude Code sessions when working any L-phase.*