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

@@ -103,6 +103,9 @@ func renderPhaseBlock(pg phaseGroup, playerName, enemyName string, result Combat
for _, e := range pg.Events {
line := renderEvent(e, playerName, enemyName, result, picker)
if line != "" {
if roll := rollAnnotation(e); roll != "" {
line = line + " " + roll
}
sb.WriteString(line + "\n")
}
}
@@ -125,6 +128,26 @@ func clampHP(hp int) int {
return hp
}
// rollAnnotation returns a compact d20 tag for events that came from the
// d20-vs-AC resolution path. Empty for ability/heal/consumable events
// where Roll is unset. Format: "_(🎲 14 vs AC 13)_" — italicized so the
// dice fact reads as a sidebar to the narrative line.
func rollAnnotation(e CombatEvent) string {
if e.Roll <= 0 {
return ""
}
switch e.Action {
case "hit", "crit", "miss", "block":
// d20 contests — annotate with roll vs target AC.
default:
return ""
}
if e.RollAgainst > 0 {
return fmt.Sprintf("_(🎲 %d vs AC %d)_", e.Roll, e.RollAgainst)
}
return fmt.Sprintf("_(🎲 %d)_", e.Roll)
}
func phaseHeader(name string) string {
switch name {
case "Opening", "opening":