Adv 2.0 L5h: cold AdvCharacter table — saveAdvCharacter fan-outs to player_meta

saveAdvCharacter no longer writes the legacy adventure_characters row.
It's now a thin fan-out: bumps LastActiveAt and routes through
upsertAllPlayerMetaFromAdvChar, which calls the per-subsystem upserts
for display_name, hospital, arena, masterwork, rival, skills, babysit,
NPC, lifecycle, death, misc, pet, and house.

loadAdvCharacter calls applyPlayerMetaOverlay at the tail to re-source
every migrated subsystem from player_meta. The adventure_characters
SELECT still happens (for the user_id / equipment-table linkage and as
a fallback substrate) but its values are overwritten by the overlay.
This achieves the L4e in-place housing reader flip automatically —
adventure_housing.go's char.HouseFoo reads now see player_meta values
without per-site changes.

npcMidnightReset extended to write player_meta alongside the frozen
adventure_characters.

Per-call-site upsertPlayerMetaXxx duals are now redundant with the
saveAdvCharacter fan-out but kept as defense; cleanup deferred.
player_meta.combat_level column drop deferred — still read by babysit
cost / combat_stats / activities via the overlay; sequenced with the
final combat_engine teardown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-09 11:52:03 -07:00
parent 0d2e554e22
commit 1c7939c3c8
4 changed files with 199 additions and 151 deletions

View File

@@ -422,7 +422,7 @@ Each sub-phase is the same shape as L4aL4e: column-add → backfill (idempote
| **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. **Status (2026-05-09):** column + dual-write SHIPPED on `adv-2.0`. Eight columns added (alive INTEGER DEFAULT 1, dead_until/death_reprieve_last/last_pardon_used DATETIME, last_death_date/grudge_location/death_source/death_location TEXT). `DeathState` + load/upsert/backfill/projection helpers. **Strategy switch:** dual-write moved inside `saveAdvCharacter` itself (after the existing display_name dual-write). The mutation surface for death state spans ~50 save sites — per-site upserts would be too noisy. Every `saveAdvCharacter` call now propagates `DeathState` to player_meta. Backfill idempotent (only fills rows where every death field is still default). Tests: `TestPlayerMetaDeathStateBackfill_Idempotent`, `TestLoadDeathState_FallsBackToAdvCharacter`, `TestUpsertPlayerMetaDeathState_RoundTrip`, `TestSaveAdvCharacter_DualWritesDeathState`. `go vet ./... && go test ./...` clean. |
| **L5f** | Misc (`Title`, `TreasuresLocked`, `CraftsSucceeded`) | 0.5 day | Each is a single-mutation-site field. **Status (2026-05-09):** column + dual-write SHIPPED on `adv-2.0`. Three columns (title TEXT, treasures_locked INT, crafts_succeeded INT). `MiscState` + load/upsert/backfill/projection helpers. Dual-write rides `saveAdvCharacter` alongside L5e (Title is dormant; TreasuresLocked + CraftsSucceeded mutate at known sites that already save). Backfill idempotent. Tests: `TestPlayerMetaMiscStateBackfill_Idempotent`, `TestLoadMiscState_FallsBackToAdvCharacter`, `TestUpsertPlayerMetaMiscState_RoundTrip`. `go vet ./... && go test ./...` clean. |
| **L5g** | DnDCharacter mass-backfill | 0.5 day | One-shot: for every active user without a D&D row, insert a default-class character at `dndLevelFromCombatLevel(adventure_characters.combat_level)`. Run once, idempotent (skip rows that already exist). After this lands, drop the `dndLevelFromCombatLevel` fallback branch in `loadHospitalVisits`/`rivalLevelForUser`/`dndLevelForUser`/zone gates. CombatLevel column in player_meta becomes unused at this point — drop it during the L5h cleanup. **Status (2026-05-09):** SHIPPED on `adv-2.0`. `backfillDnDCharactersFromAdv` in `dnd_combat.go` walks `adventure_characters LEFT JOIN dnd_character WHERE d.user_id IS NULL`, calls `autoBuildCharacter` (race/class inferred from archetypes; HP/AC computed; Level seeded from `dndLevelFromCombatLevel(CombatLevel)`), saves with `AutoMigrated=1`, `PendingSetup=0`. Idempotent — pre-existing rows (including pending-setup drafts) are skipped via the LEFT JOIN. Wired into Init after the L5f backfill. Fallbacks dropped: `dndLevelForUser` (dnd.go), `rivalLevelForUser` (adventure_rival.go), `hospitalCostsForUser` (adventure_hospital.go) — all now floor at level 1 when no D&D row exists. `dndLevelFromCombatLevel` itself is preserved (used by `autoBuildCharacter` for fresh-account level seeding and by `dndSetupConfirm`/preview/stub paths). Tests: `TestL5gBackfillDnDCharacters_Idempotent`, `TestL5gBackfillDnDCharacters_SkipsPendingSetup` in `dnd_l5g_test.go`. `go vet ./... && go test ./...` clean. |
| **L5h** | L4e in-place housing reader flip + cut all AdvCharacter dual-writes | 1 day | Bundled per L4e note. Once writes stop, AdvCharacter rows go cold; dual-write removal is mechanical (delete `saveAdvCharacter` calls and the upserts that follow them in pairs). |
| **L5h** | L4e in-place housing reader flip + cut all AdvCharacter dual-writes | 1 day | Bundled per L4e note. Once writes stop, AdvCharacter rows go cold; dual-write removal is mechanical (delete `saveAdvCharacter` calls and the upserts that follow them in pairs). **Status (2026-05-09):** SHIPPED on `adv-2.0`. **Strategy switch:** rather than deleting 51 saveAdvCharacter call sites in pairs (which would need every site to also know about display_name/death/misc/skills/etc. internal mirrors), `saveAdvCharacter` itself was rewritten as a thin fan-out: `LastActiveAt = now()` + `upsertAllPlayerMetaFromAdvChar(char)`. The legacy `UPDATE adventure_characters` is gone — the row is now read-only after `createAdvCharacter` seeds it. `applyPlayerMetaOverlay(c)` runs at the tail of `loadAdvCharacter`, re-sourcing every migrated subsystem (display_name, hospital, arena, masterwork, rival, skills, babysit, NPC, lifecycle, death, misc, pet, house) from player_meta so callers continue to use `char.X` readers transparently. The L4e housing in-place reader flip is now automatic — `adventure_housing.go`'s `char.HouseFoo` reads see player_meta values via the overlay. `npcMidnightReset` extended to write player_meta alongside the (frozen) adventure_characters. Per-call-site `upsertPlayerMetaXxx` dual-writes are now redundant with `saveAdvCharacter`'s fan-out but retained as defense — cleanup deferred. **player_meta.combat_level deferred.** Still actively used by babysit cost / combat_stats / activities via the overlay (those sites will be cut in final teardown alongside `combat_engine.go`); column drop sequenced with that. `go vet ./... && go test ./...` clean. |
### 7.4 Final teardown (after L5aL5h)