Adv 2.0 D&D polish: GM→DM rename, streamed zone combat, end-to-end scenario test

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>
This commit is contained in:
prosolis
2026-05-09 07:43:11 -07:00
parent 83a71173b1
commit dfa7beeb96
32 changed files with 1262 additions and 334 deletions

View File

@@ -487,3 +487,36 @@ func persistDnDHPAfterCombat(userID id.UserID, legacyStartHP, legacyEndHP int) {
slog.Error("dnd: persist hp after combat", "user", userID, "err", err)
}
}
// dndHPSnapshot returns the user's D&D-scale (current, max) HP. Caller
// captures pre-combat values via this helper, runs combat (which calls
// persistDnDHPAfterCombat internally), then re-snapshots for the post
// values. Used so combat outcome narration shows sheet HP rather than
// the legacy combat-engine HP scale.
func dndHPSnapshot(userID id.UserID) (cur, max int) {
c, err := LoadDnDCharacter(userID)
if err != nil || c == nil {
return 0, 0
}
return c.HPCurrent, c.HPMax
}
// markAdventureDead flips the legacy adventure_characters.alive flag and
// starts the 6h respawn timer for a player who went down in a D&D-layer
// combat. Without this, hp_current persists as 0 but the legacy alive
// flag stays true — the "zombie" state where !hospital says "you're
// alive!" while the sheet shows 0/33. Idempotent: bails if already dead.
//
// source is "zone" / "expedition" / "patrol"; location is human-readable
// (e.g. "Forest of Shadows") and surfaces in the daily report and
// standout-loss flavor.
func markAdventureDead(userID id.UserID, source, location string) {
char, err := loadAdvCharacter(userID)
if err != nil || char == nil || !char.Alive {
return
}
char.Kill(source, location)
if err := saveAdvCharacter(char); err != nil {
slog.Error("dnd: kill on combat loss", "user", userID, "err", err)
}
}