Adv 2.0 D&D Phase 11 D1e: zone combat + trap + boss resolution

Wires real combat into !zone advance. Each room now resolves through
its own path: Entry is pure flavor, Exploration spawns a SpawnWeight-
biased non-elite from the zone roster, Elite filters to elite-flagged
entries, Trap nicks 8–20% MaxHP scaled by tier (KO-protected), Boss
runs the bestiary entry from zone.Boss with the zone Loot table on
victory. Combat reuses the existing dungeonCombatPhases pipeline with
the player's full D&D layer (class/race/subclass passives, equipment,
HP scaling, armed abilities, pending casts) and persists HP, subclass
state, and CR-weighted XP after each kill.

Mood event triggers fold in: nat-20s/nat-1s scanned from CombatResult
events apply +3/-2 deltas, player_death applies -5 + abandons the run,
zone_complete (already wired in D1d) lands when the boss falls. Loot
drops materialize into adventure_inventory with coin patterns
("coins_2d10x5") expanded into rolled gold-pouch treasure rows and
named items rendered as tier-scaled placeholder treasure (real
equipment-registry wiring is a later content phase).

Updates the D1d mood-on-completion test to drive the persistence layer
directly, since real combat against the L1 Goblin Warrens roster is
non-deterministic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-08 12:29:17 -07:00
parent 9dc0e37340
commit d2a1b79ee3
4 changed files with 774 additions and 18 deletions

View File

@@ -168,24 +168,28 @@ func TestZoneCmd_AdvanceFinalRoomBumpsMood(t *testing.T) {
zoneCmdTestCharacter(t, uid, 1)
defer cleanupZoneRuns(uid)
p := &AdventurePlugin{}
if err := p.handleDnDZoneCmd(MessageContext{Sender: uid}, "enter goblin_warrens"); err != nil {
// D1e wired real combat into !zone advance, so the cmd-level path
// is no longer deterministic for an L1 fighter against the Goblin
// Warrens roster. Drive the persistence layer directly to assert
// the mood-on-completion contract.
run, err := startZoneRun(uid, ZoneGoblinWarrens, 1, nil)
if err != nil {
t.Fatal(err)
}
run, _ := getActiveZoneRun(uid)
// Advance through every room. zone_complete fires on the boss-room
// advance and bumps mood by +10 (50 → 60).
for i := 0; i < run.TotalRooms; i++ {
if err := p.handleDnDZoneCmd(MessageContext{Sender: uid}, "advance"); err != nil {
t.Fatalf("advance %d: %v", i, err)
if _, err := markRoomCleared(run.RunID); err != nil {
t.Fatalf("markRoomCleared %d: %v", i, err)
}
}
if _, err := applyMoodEvent(run.RunID, MoodEventZoneComplete); err != nil {
t.Fatal(err)
}
final, err := getZoneRun(run.RunID)
if err != nil || final == nil {
t.Fatal("could not fetch completed run")
}
if !final.BossDefeated {
t.Error("boss should be defeated after final advance")
t.Error("boss should be defeated after final markRoomCleared")
}
if final.GMMood != 60 {
t.Errorf("post-completion mood = %d, want 60 (50 + zone_complete +10)", final.GMMood)