Adv 2.0 L5c: NPC counters/debuffs migration off AdvCharacter

Thirteen player_meta columns: misty/arina_last_seen,
misty_buff_expires, misty_debuff_expires, arina_buff_expires (all
nullable DATETIME), npc_msg_count + npc_msg_count_date,
misty/arina_roll_target, misty_encounter_count, misty_donated_count,
thom_animal_line_fired, robbie_visit_count.

NPCState struct with HasNPCActivity() marker + load/upsert/backfill/
projection helpers. Dual-writes wired at every NPC mutation site:
processNPCEncounters msg-count save, npcFireEncounter last-seen,
resolveMisty (insufficient balance, debit failure, buff/donated,
declined-debuff), resolveArina buff, adventure_robbie visit count,
adventure_housing Thom animal line.

Hidden discovery mechanic — buffs/debuffs and counters never surface
in player-facing output.

Tests: TestPlayerMetaNPCStateBackfill_Idempotent,
TestLoadNPCState_FallsBackToAdvCharacter,
TestUpsertPlayerMetaNPCState_RoundTrip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-09 11:24:55 -07:00
parent d5b4834d44
commit c83b73b655
8 changed files with 435 additions and 1 deletions

View File

@@ -417,7 +417,7 @@ Each sub-phase is the same shape as L4aL4e: column-add → backfill (idempote
|---|---|---|---|
| **L5a** | Skills (8 fields) | 1 day | `SkillState` struct mirroring `PetState`/`HouseState`. Mutation surface is small: `checkAdvLevelUp(char, skill)` + the few sites that bump `*XP` directly. Reader flip unblocks dnd_sheet's legacy display row and `babysitDailyCost`. CombatLevel rejoins this struct as a transitional column — dropped at teardown after the DnD mass-backfill. **Status (2026-05-09):** column + dual-write SHIPPED on `adv-2.0`. Eight `combat_*` / `mining_*` / `foraging_*` / `fishing_*` columns added via columnMigration. `SkillState` struct + `HasSkills()` / `loadSkillState` / `upsertPlayerMetaSkillState` / `backfillPlayerMetaSkillState` / `skillStateFromAdvChar` helpers in `player_meta.go`. Dual-writes wired at every mutation site: `adventure_consumables.go` (craft success + craft failure XP grant), `adventure_events.go` (event XP grant across combat/mining/foraging), `adventure_arena.go::resolveArenaDeath` + `arenaCompleteSession` (combat XP + level-up). Backfill `backfillPlayerMetaSkillState()` wired into Init, idempotent (only fills rows where every skill column is still zero AND the legacy row has at least one non-zero skill/XP). Tests: `TestPlayerMetaSkillStateBackfill_Idempotent`, `TestLoadSkillState_FallsBackToAdvCharacter`, `TestUpsertPlayerMetaSkillState_RoundTrip` in `player_meta_test.go`. `go vet ./... && go test ./...` clean. **Reader flip deferred** — bundled with cutting AdvCharacter writes (post-soak), per the L4e pattern. |
| **L5b** | Babysit state (5 fields) | 0.5 day | Mutation surface is `runBabysitDailyTrickle` save in scheduler + the babysit toggle handlers. Tightly scoped. **Status (2026-05-09):** column + dual-write SHIPPED on `adv-2.0`. Five columns (`babysit_active`, `babysit_expires_at` DATETIME, `babysit_skill_focus`, `auto_babysit`, `auto_babysit_focus`) added via columnMigration. `BabysitState` + `IsActive()` + load/upsert/backfill/projection helpers in `player_meta.go`. Dual-writes wired at `handleBabysitStart`, `handleBabysitCancel`, `checkBabysitExpiry`. Backfill idempotent — only fills inactive rows whose legacy counterpart is active. AutoBabysit/AutoBabysitFocus + BabysitSkillFocus are dormant fields (no live mutation site) preserved for the schema migration. Tests: `TestPlayerMetaBabysitStateBackfill_Idempotent`, `TestLoadBabysitState_FallsBackToAdvCharacter`, `TestUpsertPlayerMetaBabysitState_RoundTrip`. `go vet ./... && go test ./...` clean. **Reader flip deferred** per L4e/L5a pattern. |
| **L5c** | NPC counters/debuffs (~13 fields) | 1 day | "Later phase" referenced in L4d note. Pack the four `*Expires` and three `*LastSeen` timestamps as nullable columns; counters as ints. The roll-targets are write-once-per-encounter so dual-write is cheap. **Hidden discovery mechanic — never surface in player-facing output.** |
| **L5c** | NPC counters/debuffs (~13 fields) | 1 day | "Later phase" referenced in L4d note. Pack the four `*Expires` and three `*LastSeen` timestamps as nullable columns; counters as ints. The roll-targets are write-once-per-encounter so dual-write is cheap. **Hidden discovery mechanic — never surface in player-facing output.** **Status (2026-05-09):** column + dual-write SHIPPED on `adv-2.0`. Thirteen columns added (5 nullable DATETIME for Misty/Arina last-seen + buff/debuff expiries, 1 int + 1 text for npc_msg counters, 2 ints for roll targets, 4 ints for encounter/donated/thom-fired/robbie counters). `NPCState` + `HasNPCActivity()` + load/upsert/backfill/projection helpers. Dual-writes wired at `processNPCEncounters` (msg-count save), `npcFireEncounter` (last-seen save), `resolveMisty` (insufficient balance, debit failure, buff/donated, declined-debuff), `resolveArina` (buff), `adventure_robbie.go` (visit count), `adventure_housing.go::handleThomGreet` (animal line fired). Backfill idempotent. Tests added. `go vet ./... && go test ./...` clean. **Reader flip deferred** per pattern. |
| **L5d** | Streak + action state + lifecycle (10 fields) | 1 day | Action state mutates every action, so the dual-write hook is hot — ensure the upsert is in the same code path that already does `saveAdvCharacter`. |
| **L5e** | Death state (8 fields) | 1 day | Decision point: do `DnDCharacter.HPCurrent == 0` semantics replace `Alive`/`DeadUntil`, or do we keep them as `player_meta.alive` / `player_meta.dead_until`? Recommendation: keep them as columns. The hospital revive path (L4a) already restores `DnDCharacter.HPCurrent = HPMax` alongside `Alive=true`, so the two are kept in sync; flipping the readers is the migration, not the death-state semantics. |
| **L5f** | Misc (`Title`, `TreasuresLocked`, `CraftsSucceeded`) | 0.5 day | Each is a single-mutation-site field. |