mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +00:00
Secret rooms were dead content: every NodeKindSecret node silently collapsed to a normal exploration fight and its authored LootBias (1.5-3.0) was never read at runtime. D4 makes them what they read as — no-combat treasure caches. resolveRoom now diverts a secret node (keyed off the graph node, since CurrentRoomType has already lost the kind) to resolveSecretRoom before the RoomType switch — shared by manual !zone advance, !expedition run autopilot, and the sim. Each secret pays a guaranteed journal page (the D1a grant hook built "for secret rooms"), a LootBias-weighted treasure roll (floored at elite weight), and a guaranteed zone-tier consumable cache, with bespoke in-world discovery flavor. Underdark was the only T2+ zone with no secret; added at throne_gallery on the universal R4 tail (Lost Reliquary, Perception DC 17), merging to throne_steps so it's length-neutral. Two cross-zone keys: the Sunken Temple's Coral Reliquary grants a Sunken Sigil that opens a Sealed Reliquary in Manor Blackspire; the Underforge's Forge Vault grants an Underforge Seal that opens a Sealed Vault in the Underdark. Keys are persistent inventory items matched against LockKey key_id; grants are idempotent. Graph validator + no-soft-lock pass on both touched graphs; combat golden byte-identical; go build/vet/test green repo-wide. Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
148 lines
4.7 KiB
Go
148 lines
4.7 KiB
Go
package plugin
|
|
|
|
import "testing"
|
|
|
|
func TestUnderdarkGraph_Registered(t *testing.T) {
|
|
g, ok := zoneGraphRegistry[ZoneUnderdark]
|
|
if !ok {
|
|
t.Fatal("zoneUnderdarkGraph not registered")
|
|
}
|
|
// Long-expedition D1-d widened this zone from 10 → 46 nodes so the
|
|
// longest entry→boss walk lands in the T4 [28,34] traversal band.
|
|
// N5/D4 added two throne-gallery secret spokes (Lost Reliquary +
|
|
// Sealed Vault) → 48.
|
|
if len(g.Nodes) != 48 {
|
|
t.Errorf("nodes = %d, want 48", len(g.Nodes))
|
|
}
|
|
}
|
|
|
|
func TestUnderdarkGraph_LongestPathInBand(t *testing.T) {
|
|
g := zoneUnderdarkGraph()
|
|
got := graphLongestPath(g)
|
|
if got < 28 || got > 34 {
|
|
t.Errorf("longest path = %d, want in T4 band [28,34]", got)
|
|
}
|
|
}
|
|
|
|
// TestUnderdarkGraph_AllNodesHaveRegion confirms Underdark is the
|
|
// canonical multi-region zone — every node carries a non-empty RegionID
|
|
// matching the dnd_expedition_region.go registry.
|
|
func TestUnderdarkGraph_AllNodesHaveRegion(t *testing.T) {
|
|
g := zoneUnderdarkGraph()
|
|
validRegions := map[string]bool{
|
|
"underdark_surface_tunnels": true,
|
|
"underdark_drow_outpost": true,
|
|
"underdark_illithid_warren": true,
|
|
"underdark_deep_throne": true,
|
|
}
|
|
for id, n := range g.Nodes {
|
|
if n.RegionID == "" {
|
|
t.Errorf("node %s has empty RegionID — Underdark requires region authoring on every node", id)
|
|
}
|
|
if !validRegions[n.RegionID] {
|
|
t.Errorf("node %s RegionID = %q, not in dnd_expedition_region.go registry", id, n.RegionID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestUnderdarkGraph_AllFourRegionsRepresented confirms each authored
|
|
// region has at least one node on the canonical entry→boss path.
|
|
func TestUnderdarkGraph_AllFourRegionsRepresented(t *testing.T) {
|
|
g := zoneUnderdarkGraph()
|
|
regions := map[string]int{}
|
|
for _, n := range g.Nodes {
|
|
regions[n.RegionID]++
|
|
}
|
|
for _, r := range []string{
|
|
"underdark_surface_tunnels",
|
|
"underdark_drow_outpost",
|
|
"underdark_illithid_warren",
|
|
"underdark_deep_throne",
|
|
} {
|
|
if regions[r] == 0 {
|
|
t.Errorf("region %q has no nodes — multi-region invariant broken", r)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestUnderdarkGraph_ForkCrossesRegionBoundaries verifies the fork
|
|
// edges actually traverse region boundaries (this is what makes the
|
|
// G6 fireGraphRegionTransition hook fire end-to-end on this zone).
|
|
func TestUnderdarkGraph_ForkCrossesRegionBoundaries(t *testing.T) {
|
|
g := zoneUnderdarkGraph()
|
|
fork := g.Nodes["underdark.fork1"]
|
|
if fork.RegionID != "underdark_surface_tunnels" {
|
|
t.Fatalf("fork1 region = %q, want surface_tunnels", fork.RegionID)
|
|
}
|
|
for _, e := range g.outgoingEdges("underdark.fork1") {
|
|
toNode := g.Nodes[e.To]
|
|
switch e.To {
|
|
case "underdark.drow_patrol":
|
|
if toNode.RegionID != "underdark_drow_outpost" {
|
|
t.Errorf("drow_patrol region = %q", toNode.RegionID)
|
|
}
|
|
case "underdark.psionic_corridor":
|
|
if toNode.RegionID != "underdark_illithid_warren" {
|
|
t.Errorf("psionic_corridor region = %q", toNode.RegionID)
|
|
}
|
|
case "underdark.deep_chasm":
|
|
// Deep_chasm intentionally stays in R1 (surface_tunnels) — the
|
|
// vertical-shaft path explores R1 deeper before joining R4.
|
|
if toNode.RegionID != "underdark_surface_tunnels" {
|
|
t.Errorf("deep_chasm region = %q, want surface_tunnels (intentional R1 stay)", toNode.RegionID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUnderdarkGraph_AllArmsReachBoss(t *testing.T) {
|
|
g := zoneUnderdarkGraph()
|
|
for _, leaf := range []string{
|
|
"underdark.drow_captain",
|
|
"underdark.mind_flayer",
|
|
"underdark.deep_chasm",
|
|
} {
|
|
if !reachable(g, leaf, "underdark.boss") {
|
|
t.Errorf("%s unreachable to boss", leaf)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestUnderdarkGraph_TrapAnchor verifies D1-d added the missing R1 Trap
|
|
// (collapsed_arch) and D10 added the illithid-arm Trap (silenced_chamber).
|
|
func TestUnderdarkGraph_TrapAnchor(t *testing.T) {
|
|
g := zoneUnderdarkGraph()
|
|
var trapCount int
|
|
for _, n := range g.Nodes {
|
|
if n.Kind == NodeKindTrap {
|
|
trapCount++
|
|
}
|
|
}
|
|
if trapCount != 2 {
|
|
t.Errorf("trap nodes = %d, want 2 (collapsed_arch + D10 silenced_chamber)", trapCount)
|
|
}
|
|
if g.Nodes["underdark.silenced_chamber"].Kind != NodeKindTrap {
|
|
t.Error("D10: silenced_chamber (illithid arm) should be a Trap")
|
|
}
|
|
}
|
|
|
|
// TestUnderdarkGraph_EliteAnchors verifies the per-arm elites (drow
|
|
// Captain, illithid Mind Flayer) plus the D10 region-guardian elite at
|
|
// the drow→throne boundary (drow_gate), so the drow arm carries two
|
|
// elites while the chasm spur stays anchor-light.
|
|
func TestUnderdarkGraph_EliteAnchors(t *testing.T) {
|
|
g := zoneUnderdarkGraph()
|
|
var eliteCount int
|
|
for _, n := range g.Nodes {
|
|
if n.Kind == NodeKindElite {
|
|
eliteCount++
|
|
}
|
|
}
|
|
if eliteCount != 3 {
|
|
t.Errorf("elite nodes = %d, want 3 (drow_captain + mind_flayer + D10 drow_gate)", eliteCount)
|
|
}
|
|
if g.Nodes["underdark.drow_gate"].Kind != NodeKindElite {
|
|
t.Error("D10: drow_gate (R2→R4 boundary) should be a region-guardian Elite")
|
|
}
|
|
}
|