mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Long expeditions D1-b: crypt_valdris + T2 zones to new room bands
Three more zones follow the goblin_warrens pattern from D1-a: add the missing Trap anchor and deepen the graph so the longest entry→boss walk lands in its tier's target band. crypt_valdris (T1): 8 → 17 nodes, longest 13 (band 12–14) forest_shadows (T2): 9 → 19 nodes, longest 16 (band 16–20) sunken_temple (T2): 10 → 26 nodes, longest 16 (band 16–20) Topology preserved per zone: - Crypt diamond + Perception-DC-15 secret off side_chapel. - Forest asymmetric main fork (long branch carries the elite) + WIS-DC-14 secret pre-boss. - Sunken Temple sequential forks with no mid-path merge (four leaves all terminate at the single boss room). MinRooms/MaxRooms re-pitched to the §2 bands so the dice fallback also lands in band for any future graphless zone.
This commit is contained in:
@@ -258,8 +258,8 @@ func zoneCryptValdris() ZoneDefinition {
|
||||
Faction: "Undead",
|
||||
Atmosphere: "Stone corridors, dripping water, candles that shouldn't still be burning.",
|
||||
Hook: "The iron gate hangs open — someone left in a hurry. Carved into the stone above: \"HERE LIES VALDRIS. DO NOT.\" The rest has been chiseled away. I decline to speculate.",
|
||||
MinRooms: 6,
|
||||
MaxRooms: 7,
|
||||
MinRooms: 12,
|
||||
MaxRooms: 14,
|
||||
Enemies: []ZoneEnemy{
|
||||
// Phase 4-B (outlier fix): the live roster was the only
|
||||
// dual-killer-elite zone (wight + flameskull, both CR3+ on
|
||||
@@ -314,8 +314,8 @@ func zoneForestShadows() ZoneDefinition {
|
||||
Faction: "Beasts, Fey-corrupted creatures, Bandits",
|
||||
Atmosphere: "Ancient forest, twisted paths, eerie silence, bioluminescent fungi, things in the canopy.",
|
||||
Hook: "The forest was beautiful once. Travelers still say so, usually right before they stop saying anything at all. The trees lean in when you're not looking. I have noted this is not a metaphor.",
|
||||
MinRooms: 6,
|
||||
MaxRooms: 8,
|
||||
MinRooms: 16,
|
||||
MaxRooms: 20,
|
||||
Enemies: []ZoneEnemy{
|
||||
// Phase 4-B (outlier fix): standard pool was carrying two
|
||||
// real killers — Displacer Beast (38% win as a standard
|
||||
@@ -377,8 +377,8 @@ func zoneSunkenTemple() ZoneDefinition {
|
||||
Faction: "Kuo-toa, Water Elementals, Aboleth-touched",
|
||||
Atmosphere: "Flooded stone chambers, barnacled pillars, salt smell, alien glyphs, things that swim in the dark water.",
|
||||
Hook: "The tide went out thirty years ago and never fully came back. The temple stayed wet anyway. Something down there keeps it that way. I suggest waterproofing your spellbook.",
|
||||
MinRooms: 6,
|
||||
MaxRooms: 8,
|
||||
MinRooms: 16,
|
||||
MaxRooms: 20,
|
||||
Enemies: []ZoneEnemy{
|
||||
{BestiaryID: "kuo_toa", SpawnWeight: 7},
|
||||
{BestiaryID: "kuo_toa_whip", SpawnWeight: 4},
|
||||
|
||||
@@ -1,54 +1,101 @@
|
||||
package plugin
|
||||
|
||||
// Phase G7 — Crypt of Valdris POC graph.
|
||||
// Crypt of Valdris branching graph.
|
||||
//
|
||||
// First hand-authored branching graph. Topology per
|
||||
// gogobee_branching_zones_plan.md §3:
|
||||
// Long-expedition plan D1-b: extended from the original 8-node POC sketch
|
||||
// to the new T1 length band (12–14 rooms traversed). Topology preserves
|
||||
// the original diamond — main_hall (elite) vs side_chapel (perception
|
||||
// fork), with a Perception-DC-15 secret dangling off side_chapel — and
|
||||
// adds the missing Trap anchor + extra exploration on both arms so the
|
||||
// longest entry→boss walk lands at 13.
|
||||
//
|
||||
// entry → corridor → fork
|
||||
// ├──[unlocked]── main_hall (elite) ──┐
|
||||
// │ ├── antechamber → boss
|
||||
// └──[Perception DC 12]── side_chapel ─┘
|
||||
// │
|
||||
// └──[Perception DC 15]── secret_chamber → antechamber
|
||||
// entry → processional_stair → bone_corridor →
|
||||
// grave_dust_trap (TRAP) → ossuary_walk → fork
|
||||
// ├──[unlocked]── main_hall (ELITE)
|
||||
// │ → reliquary_hall → ash_passage ──┐
|
||||
// │ ├── antechamber
|
||||
// └──[Perception DC 12]── side_chapel │ → choir_landing
|
||||
// │ → catacomb_passage │ → throne_steps
|
||||
// │ → vault_landing ──────────┘ → boss
|
||||
// └──[Perception DC 15]── secret_chamber (SECRET) → antechamber
|
||||
//
|
||||
// Two paths to the boss (main_hall vs side_chapel); secret_chamber
|
||||
// dangles off side_chapel for an extra Perception gate with loot.
|
||||
// Both main and side arms walk 3 mid-nodes after the fork; secret stays a
|
||||
// 1-node side dangle off side_chapel that merges back at antechamber, so
|
||||
// the Perception-DC-15 path is a loot dip rather than a length detour.
|
||||
|
||||
func zoneCryptValdrisGraph() ZoneGraph {
|
||||
nodes := []ZoneNode{
|
||||
{NodeID: "crypt_valdris.entry", Kind: NodeKindEntry, IsEntry: true,
|
||||
Label: "Crypt Threshold", PosX: 0, PosY: 1},
|
||||
{NodeID: "crypt_valdris.corridor", Kind: NodeKindExploration,
|
||||
Label: "Bone Corridor", PosX: 1, PosY: 1},
|
||||
{NodeID: "crypt_valdris.processional_stair", Kind: NodeKindExploration,
|
||||
Label: "Processional Stair", PosX: 1, PosY: 1},
|
||||
{NodeID: "crypt_valdris.bone_corridor", Kind: NodeKindExploration,
|
||||
Label: "Bone Corridor", PosX: 2, PosY: 1},
|
||||
{NodeID: "crypt_valdris.grave_dust_trap", Kind: NodeKindTrap,
|
||||
Label: "Grave-Dust Cascade", PosX: 3, PosY: 1},
|
||||
{NodeID: "crypt_valdris.ossuary_walk", Kind: NodeKindExploration,
|
||||
Label: "Ossuary Walk", PosX: 4, PosY: 1},
|
||||
{NodeID: "crypt_valdris.fork", Kind: NodeKindFork,
|
||||
Label: "Branching Passage", PosX: 2, PosY: 1},
|
||||
Label: "Branching Passage", PosX: 5, PosY: 1},
|
||||
|
||||
// Left arm — default, contains the elite.
|
||||
{NodeID: "crypt_valdris.main_hall", Kind: NodeKindElite,
|
||||
Label: "Main Hall", PosX: 3, PosY: 0},
|
||||
Label: "Main Hall", PosX: 6, PosY: 0},
|
||||
{NodeID: "crypt_valdris.reliquary_hall", Kind: NodeKindExploration,
|
||||
Label: "Reliquary Hall", PosX: 7, PosY: 0},
|
||||
{NodeID: "crypt_valdris.ash_passage", Kind: NodeKindExploration,
|
||||
Label: "Ash Passage", PosX: 8, PosY: 0},
|
||||
|
||||
// Right arm — perception-gated, elite-free.
|
||||
{NodeID: "crypt_valdris.side_chapel", Kind: NodeKindExploration,
|
||||
Label: "Side Chapel", PosX: 3, PosY: 2},
|
||||
Label: "Side Chapel", PosX: 6, PosY: 2},
|
||||
{NodeID: "crypt_valdris.catacomb_passage", Kind: NodeKindExploration,
|
||||
Label: "Catacomb Passage", PosX: 7, PosY: 2},
|
||||
{NodeID: "crypt_valdris.vault_landing", Kind: NodeKindExploration,
|
||||
Label: "Vault Landing", PosX: 8, PosY: 2},
|
||||
|
||||
// Secret dangle off side_chapel — high-DC Perception, loot-biased.
|
||||
{NodeID: "crypt_valdris.secret_chamber", Kind: NodeKindSecret,
|
||||
Label: "Hidden Reliquary", PosX: 4, PosY: 3,
|
||||
Label: "Hidden Reliquary", PosX: 7, PosY: 3,
|
||||
Content: ZoneNodeContent{LootBias: 2.0}},
|
||||
|
||||
// Merge + approach.
|
||||
{NodeID: "crypt_valdris.antechamber", Kind: NodeKindMerge,
|
||||
Label: "Antechamber", PosX: 5, PosY: 1},
|
||||
Label: "Antechamber", PosX: 9, PosY: 1},
|
||||
{NodeID: "crypt_valdris.choir_landing", Kind: NodeKindExploration,
|
||||
Label: "Choir Landing", PosX: 10, PosY: 1},
|
||||
{NodeID: "crypt_valdris.throne_steps", Kind: NodeKindExploration,
|
||||
Label: "Throne Steps", PosX: 11, PosY: 1},
|
||||
{NodeID: "crypt_valdris.boss", Kind: NodeKindBoss, IsBoss: true,
|
||||
Label: "Valdris's Sanctum", PosX: 6, PosY: 1},
|
||||
Label: "Valdris's Sanctum", PosX: 12, PosY: 1},
|
||||
}
|
||||
edges := []ZoneEdge{
|
||||
{From: "crypt_valdris.entry", To: "crypt_valdris.corridor", Lock: LockNone},
|
||||
{From: "crypt_valdris.corridor", To: "crypt_valdris.fork", Lock: LockNone},
|
||||
{From: "crypt_valdris.entry", To: "crypt_valdris.processional_stair", Lock: LockNone},
|
||||
{From: "crypt_valdris.processional_stair", To: "crypt_valdris.bone_corridor", Lock: LockNone},
|
||||
{From: "crypt_valdris.bone_corridor", To: "crypt_valdris.grave_dust_trap", Lock: LockNone},
|
||||
{From: "crypt_valdris.grave_dust_trap", To: "crypt_valdris.ossuary_walk", Lock: LockNone},
|
||||
{From: "crypt_valdris.ossuary_walk", To: "crypt_valdris.fork", Lock: LockNone},
|
||||
|
||||
{From: "crypt_valdris.fork", To: "crypt_valdris.main_hall", Lock: LockNone, Weight: 1},
|
||||
{From: "crypt_valdris.fork", To: "crypt_valdris.side_chapel",
|
||||
Lock: LockPerception, LockData: map[string]any{"dc": 12},
|
||||
Hint: "a faint draft from a crack in the wall", Weight: 2},
|
||||
{From: "crypt_valdris.main_hall", To: "crypt_valdris.antechamber", Lock: LockNone},
|
||||
{From: "crypt_valdris.side_chapel", To: "crypt_valdris.antechamber", Lock: LockNone, Weight: 1},
|
||||
|
||||
{From: "crypt_valdris.main_hall", To: "crypt_valdris.reliquary_hall", Lock: LockNone},
|
||||
{From: "crypt_valdris.reliquary_hall", To: "crypt_valdris.ash_passage", Lock: LockNone},
|
||||
{From: "crypt_valdris.ash_passage", To: "crypt_valdris.antechamber", Lock: LockNone},
|
||||
|
||||
{From: "crypt_valdris.side_chapel", To: "crypt_valdris.catacomb_passage", Lock: LockNone, Weight: 1},
|
||||
{From: "crypt_valdris.side_chapel", To: "crypt_valdris.secret_chamber",
|
||||
Lock: LockPerception, LockData: map[string]any{"dc": 15},
|
||||
Hint: "a faint scratching behind the altar", Weight: 2},
|
||||
{From: "crypt_valdris.catacomb_passage", To: "crypt_valdris.vault_landing", Lock: LockNone},
|
||||
{From: "crypt_valdris.vault_landing", To: "crypt_valdris.antechamber", Lock: LockNone},
|
||||
{From: "crypt_valdris.secret_chamber", To: "crypt_valdris.antechamber", Lock: LockNone},
|
||||
{From: "crypt_valdris.antechamber", To: "crypt_valdris.boss", Lock: LockNone},
|
||||
|
||||
{From: "crypt_valdris.antechamber", To: "crypt_valdris.choir_landing", Lock: LockNone},
|
||||
{From: "crypt_valdris.choir_landing", To: "crypt_valdris.throne_steps", Lock: LockNone},
|
||||
{From: "crypt_valdris.throne_steps", To: "crypt_valdris.boss", Lock: LockNone},
|
||||
}
|
||||
return BuildGraph(ZoneCryptValdris, nodes, edges)
|
||||
}
|
||||
|
||||
@@ -18,8 +18,10 @@ func TestCryptValdrisGraph_Registered(t *testing.T) {
|
||||
if g.Boss != "crypt_valdris.boss" {
|
||||
t.Errorf("boss node = %q, want crypt_valdris.boss", g.Boss)
|
||||
}
|
||||
if len(g.Nodes) != 8 {
|
||||
t.Errorf("nodes = %d, want 8", len(g.Nodes))
|
||||
// Long-expedition D1-b widened this zone from 8 → 17 nodes so the
|
||||
// longest entry→boss walk lands in the T1 [12,14] traversal band.
|
||||
if len(g.Nodes) != 17 {
|
||||
t.Errorf("nodes = %d, want 17", len(g.Nodes))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,64 +1,111 @@
|
||||
package plugin
|
||||
|
||||
// Phase G8b — Forest of Shadows branching graph.
|
||||
// Forest of Shadows branching graph.
|
||||
//
|
||||
// T2 zone. Per plan §G8 ("two forks for T2+") and the G8 design
|
||||
// session: deliberately a different shape from the Crypt diamond.
|
||||
// Long-expedition plan D1-b: extended from the original 9-node sketch to
|
||||
// the new T2 length band (16–20 rooms traversed). Topology preserves the
|
||||
// G8 design intent — asymmetric main fork (long branch carries the
|
||||
// elite) + WIS-gated secret hanging pre-boss — and adds the missing Trap
|
||||
// anchor + extra exploration on both arms and the boss approach.
|
||||
//
|
||||
// Shape: asymmetric main fork + WIS-gated secret pre-boss.
|
||||
// entry → mossy_threshold → twisted_path → root_snare (TRAP) →
|
||||
// creeping_brush → dappled_hollow → witchlight_clearing → fork1
|
||||
// ├─[unlocked, w=1]── grove_descent →
|
||||
// │ dryad_circle (ELITE) → torn_meadow ──┐
|
||||
// │ ├── fork2
|
||||
// └─[Perception DC 13, w=2]── thorn_tunnel │
|
||||
// → briar_warren ────────────────────────┘
|
||||
// │
|
||||
// ┌─[unlocked, w=1]── ritual_glade → │
|
||||
// │ antler_path → hollow_steps → boss ─┘
|
||||
// │
|
||||
// └─[WIS DC 14, w=2]── sapling_shrine (SECRET) → boss
|
||||
//
|
||||
// entry → twisted_path → fork1
|
||||
// ├─[unlocked, w=1]── grove_descent → dryad_circle (elite) ─┐
|
||||
// │ ├── fork2
|
||||
// └─[Perception DC 13, w=2]── thorn_tunnel ──────────────────┘
|
||||
// │
|
||||
// ┌─[unlocked, w=1]── boss ────────────────┘
|
||||
// │
|
||||
// └─[WIS DC 14, w=2]── sapling_shrine (secret) → boss
|
||||
//
|
||||
// Asymmetry: long branch is two mid-nodes (grove_descent → dryad_circle
|
||||
// (elite)), short branch is one (thorn_tunnel). Map renders the long
|
||||
// branch as a "+1 column" arm so the topology reads at a glance.
|
||||
// Secret uses LockStatCheck WIS (not Perception) to exercise the
|
||||
// stat-check lock kind — Crypt's secret is Perception, so this is the
|
||||
// first authored zone using stat_check.
|
||||
// Asymmetry preserved: long branch is 3 mid-nodes (grove_descent →
|
||||
// dryad_circle (elite) → torn_meadow), short branch is 2 (thorn_tunnel →
|
||||
// briar_warren). Longest entry→boss walk = 16 (long branch path).
|
||||
|
||||
func zoneForestShadowsGraph() ZoneGraph {
|
||||
nodes := []ZoneNode{
|
||||
{NodeID: "forest_shadows.entry", Kind: NodeKindEntry, IsEntry: true,
|
||||
Label: "Forest Edge", PosX: 0, PosY: 1},
|
||||
{NodeID: "forest_shadows.mossy_threshold", Kind: NodeKindExploration,
|
||||
Label: "Mossy Threshold", PosX: 1, PosY: 1},
|
||||
{NodeID: "forest_shadows.twisted_path", Kind: NodeKindExploration,
|
||||
Label: "Twisted Path", PosX: 1, PosY: 1},
|
||||
Label: "Twisted Path", PosX: 2, PosY: 1},
|
||||
{NodeID: "forest_shadows.root_snare", Kind: NodeKindTrap,
|
||||
Label: "Root Snare", PosX: 3, PosY: 1},
|
||||
{NodeID: "forest_shadows.creeping_brush", Kind: NodeKindExploration,
|
||||
Label: "Creeping Brush", PosX: 4, PosY: 1},
|
||||
{NodeID: "forest_shadows.dappled_hollow", Kind: NodeKindExploration,
|
||||
Label: "Dappled Hollow", PosX: 5, PosY: 1},
|
||||
{NodeID: "forest_shadows.witchlight_clearing", Kind: NodeKindExploration,
|
||||
Label: "Witchlight Clearing", PosX: 6, PosY: 1},
|
||||
{NodeID: "forest_shadows.fork1", Kind: NodeKindFork,
|
||||
Label: "Splintered Trail", PosX: 2, PosY: 1},
|
||||
Label: "Splintered Trail", PosX: 7, PosY: 1},
|
||||
|
||||
// Long branch (3 mid-nodes, carries the elite).
|
||||
{NodeID: "forest_shadows.grove_descent", Kind: NodeKindExploration,
|
||||
Label: "Grove Descent", PosX: 3, PosY: 0},
|
||||
Label: "Grove Descent", PosX: 8, PosY: 0},
|
||||
{NodeID: "forest_shadows.dryad_circle", Kind: NodeKindElite,
|
||||
Label: "Dryad's Circle", PosX: 4, PosY: 0},
|
||||
Label: "Dryad's Circle", PosX: 9, PosY: 0},
|
||||
{NodeID: "forest_shadows.torn_meadow", Kind: NodeKindExploration,
|
||||
Label: "Torn Meadow", PosX: 10, PosY: 0},
|
||||
|
||||
// Short branch (2 mid-nodes, perception-gated).
|
||||
{NodeID: "forest_shadows.thorn_tunnel", Kind: NodeKindExploration,
|
||||
Label: "Thorn Tunnel", PosX: 3, PosY: 2},
|
||||
Label: "Thorn Tunnel", PosX: 8, PosY: 2},
|
||||
{NodeID: "forest_shadows.briar_warren", Kind: NodeKindExploration,
|
||||
Label: "Briar Warren", PosX: 9, PosY: 2},
|
||||
|
||||
{NodeID: "forest_shadows.fork2", Kind: NodeKindFork,
|
||||
Label: "Hollow King's Approach", PosX: 5, PosY: 1},
|
||||
Label: "Hollow King's Approach", PosX: 11, PosY: 1},
|
||||
|
||||
// Post-fork2 main approach.
|
||||
{NodeID: "forest_shadows.ritual_glade", Kind: NodeKindExploration,
|
||||
Label: "Ritual Glade", PosX: 12, PosY: 1},
|
||||
{NodeID: "forest_shadows.antler_path", Kind: NodeKindExploration,
|
||||
Label: "Antler Path", PosX: 13, PosY: 1},
|
||||
{NodeID: "forest_shadows.hollow_steps", Kind: NodeKindExploration,
|
||||
Label: "Hollow Steps", PosX: 14, PosY: 1},
|
||||
|
||||
// Secret dangle off fork2 — WIS gate, loot-biased, shortcut to boss.
|
||||
{NodeID: "forest_shadows.sapling_shrine", Kind: NodeKindSecret,
|
||||
Label: "Sapling Shrine", PosX: 6, PosY: 2,
|
||||
Label: "Sapling Shrine", PosX: 13, PosY: 2,
|
||||
Content: ZoneNodeContent{LootBias: 2.0}},
|
||||
|
||||
{NodeID: "forest_shadows.boss", Kind: NodeKindBoss, IsBoss: true,
|
||||
Label: "Throne of Antlers", PosX: 7, PosY: 1},
|
||||
Label: "Throne of Antlers", PosX: 15, PosY: 1},
|
||||
}
|
||||
edges := []ZoneEdge{
|
||||
{From: "forest_shadows.entry", To: "forest_shadows.twisted_path", Lock: LockNone},
|
||||
{From: "forest_shadows.twisted_path", To: "forest_shadows.fork1", Lock: LockNone},
|
||||
{From: "forest_shadows.entry", To: "forest_shadows.mossy_threshold", Lock: LockNone},
|
||||
{From: "forest_shadows.mossy_threshold", To: "forest_shadows.twisted_path", Lock: LockNone},
|
||||
{From: "forest_shadows.twisted_path", To: "forest_shadows.root_snare", Lock: LockNone},
|
||||
{From: "forest_shadows.root_snare", To: "forest_shadows.creeping_brush", Lock: LockNone},
|
||||
{From: "forest_shadows.creeping_brush", To: "forest_shadows.dappled_hollow", Lock: LockNone},
|
||||
{From: "forest_shadows.dappled_hollow", To: "forest_shadows.witchlight_clearing", Lock: LockNone},
|
||||
{From: "forest_shadows.witchlight_clearing", To: "forest_shadows.fork1", Lock: LockNone},
|
||||
|
||||
{From: "forest_shadows.fork1", To: "forest_shadows.grove_descent", Lock: LockNone, Weight: 1},
|
||||
{From: "forest_shadows.fork1", To: "forest_shadows.thorn_tunnel",
|
||||
Lock: LockPerception, LockData: map[string]any{"dc": 13},
|
||||
Hint: "a deer-track winding into thicker brush", Weight: 2},
|
||||
|
||||
{From: "forest_shadows.grove_descent", To: "forest_shadows.dryad_circle", Lock: LockNone},
|
||||
{From: "forest_shadows.dryad_circle", To: "forest_shadows.fork2", Lock: LockNone},
|
||||
{From: "forest_shadows.thorn_tunnel", To: "forest_shadows.fork2", Lock: LockNone},
|
||||
{From: "forest_shadows.fork2", To: "forest_shadows.boss", Lock: LockNone, Weight: 1},
|
||||
{From: "forest_shadows.dryad_circle", To: "forest_shadows.torn_meadow", Lock: LockNone},
|
||||
{From: "forest_shadows.torn_meadow", To: "forest_shadows.fork2", Lock: LockNone},
|
||||
|
||||
{From: "forest_shadows.thorn_tunnel", To: "forest_shadows.briar_warren", Lock: LockNone},
|
||||
{From: "forest_shadows.briar_warren", To: "forest_shadows.fork2", Lock: LockNone},
|
||||
|
||||
{From: "forest_shadows.fork2", To: "forest_shadows.ritual_glade", Lock: LockNone, Weight: 1},
|
||||
{From: "forest_shadows.fork2", To: "forest_shadows.sapling_shrine",
|
||||
Lock: LockStatCheck, LockData: map[string]any{"stat": "WIS", "dc": 14},
|
||||
Hint: "a humming you can almost place — antlers, somewhere", Weight: 2},
|
||||
|
||||
{From: "forest_shadows.ritual_glade", To: "forest_shadows.antler_path", Lock: LockNone},
|
||||
{From: "forest_shadows.antler_path", To: "forest_shadows.hollow_steps", Lock: LockNone},
|
||||
{From: "forest_shadows.hollow_steps", To: "forest_shadows.boss", Lock: LockNone},
|
||||
{From: "forest_shadows.sapling_shrine", To: "forest_shadows.boss", Lock: LockNone},
|
||||
}
|
||||
return BuildGraph(ZoneForestShadows, nodes, edges)
|
||||
|
||||
@@ -13,8 +13,10 @@ func TestForestShadowsGraph_Registered(t *testing.T) {
|
||||
if g.Boss != "forest_shadows.boss" {
|
||||
t.Errorf("boss node = %q, want forest_shadows.boss", g.Boss)
|
||||
}
|
||||
if len(g.Nodes) != 9 {
|
||||
t.Errorf("nodes = %d, want 9", len(g.Nodes))
|
||||
// Long-expedition D1-b widened this zone from 9 → 19 nodes so the
|
||||
// longest entry→boss walk lands in the T2 [16,20] traversal band.
|
||||
if len(g.Nodes) != 19 {
|
||||
t.Errorf("nodes = %d, want 19", len(g.Nodes))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,11 +44,13 @@ func TestForestShadowsGraph_AsymmetricMainFork(t *testing.T) {
|
||||
g := zoneForestShadowsGraph()
|
||||
longLen := bfsHops(g, "forest_shadows.fork1", "forest_shadows.fork2", "forest_shadows.grove_descent")
|
||||
shortLen := bfsHops(g, "forest_shadows.fork1", "forest_shadows.fork2", "forest_shadows.thorn_tunnel")
|
||||
if longLen != 3 {
|
||||
t.Errorf("long branch hops = %d, want 3 (grove_descent → dryad_circle → fork2)", longLen)
|
||||
// D1-b: long branch is now 3 mid-nodes (grove_descent → dryad_circle
|
||||
// → torn_meadow), short is 2 (thorn_tunnel → briar_warren).
|
||||
if longLen != 4 {
|
||||
t.Errorf("long branch hops = %d, want 4 (grove_descent → dryad_circle → torn_meadow → fork2)", longLen)
|
||||
}
|
||||
if shortLen != 2 {
|
||||
t.Errorf("short branch hops = %d, want 2 (thorn_tunnel → fork2)", shortLen)
|
||||
if shortLen != 3 {
|
||||
t.Errorf("short branch hops = %d, want 3 (thorn_tunnel → briar_warren → fork2)", shortLen)
|
||||
}
|
||||
if longLen <= shortLen {
|
||||
t.Errorf("expected asymmetric branches (long > short); got long=%d short=%d", longLen, shortLen)
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
package plugin
|
||||
|
||||
// Phase G8c — Sunken Temple of Dar'eth branching graph.
|
||||
// Sunken Temple of Dar'eth branching graph.
|
||||
//
|
||||
// T2 zone. Shape: sequential forks (no mid-path merge). Four distinct
|
||||
// paths to the boss; the player commits at fork1 (dry vs. wet) and
|
||||
// commits again at fork2a/fork2b. Path is unique up to the boss room.
|
||||
// Long-expedition plan D1-b: extended from the original 10-node sketch to
|
||||
// the new T2 length band (16–20 rooms traversed). Topology preserves the
|
||||
// G8c design intent — sequential forks with no mid-path merge — and adds
|
||||
// the missing Trap anchor + deeper pre-fork and per-leaf chains. The
|
||||
// four leaf chains still terminate at the single boss room (validator
|
||||
// requires exactly one boss).
|
||||
//
|
||||
// entry → flooded_atrium → fork1
|
||||
// ├─[unlocked, w=1]── fork2a (dry)
|
||||
// │ ├─[unlocked]── kuo_toa_pen (elite) → boss
|
||||
// │ └─[STR DC 13]── glyph_chamber → boss
|
||||
// └─[Perception DC 13, w=2]── fork2b (wet)
|
||||
// ├─[unlocked]── aboleth_thralls → boss
|
||||
// └─[Perception DC 15]── coral_reliquary (secret) → boss
|
||||
// entry → flooded_atrium → tide_passage → kelp_trap (TRAP) →
|
||||
// barnacled_steps → drowned_atrium → sunken_nave → fork1
|
||||
// ├─[unlocked, w=1]── dry_corridor → silent_columns →
|
||||
// │ echoing_vault → fork2a
|
||||
// │ ├─[unlocked]── kuo_toa_pen (ELITE) →
|
||||
// │ │ trophy_pool → idol_court → boss
|
||||
// │ └─[STR DC 13]── glyph_chamber → silt_arch → boss
|
||||
// └─[Perception DC 13, w=2]── submerged_passage →
|
||||
// coral_lattice → drowned_nave → fork2b
|
||||
// ├─[unlocked]── aboleth_thralls →
|
||||
// │ thrall_basin → sodden_hall → boss
|
||||
// └─[Perception DC 15]── coral_reliquary (SECRET) → boss
|
||||
//
|
||||
// Distinct from the Crypt diamond and the Forest asymmetric-diamond:
|
||||
// no two paths share a mid-node, so the !zone map paints two parallel
|
||||
// "Y" subtrees instead of a converging branch. The four leaves all
|
||||
// terminate at the boss (validator requires exactly one boss node).
|
||||
// Distinct from the Crypt diamond and the Forest asymmetric-diamond: no
|
||||
// two paths share a mid-node, so the !zone map paints two parallel "Y"
|
||||
// subtrees instead of a converging branch. Longest entry→boss = 16.
|
||||
|
||||
func zoneSunkenTempleGraph() ZoneGraph {
|
||||
nodes := []ZoneNode{
|
||||
@@ -25,42 +32,104 @@ func zoneSunkenTempleGraph() ZoneGraph {
|
||||
Label: "Tide-Stained Threshold", PosX: 0, PosY: 2},
|
||||
{NodeID: "sunken_temple.flooded_atrium", Kind: NodeKindExploration,
|
||||
Label: "Flooded Atrium", PosX: 1, PosY: 2},
|
||||
{NodeID: "sunken_temple.tide_passage", Kind: NodeKindExploration,
|
||||
Label: "Tide Passage", PosX: 2, PosY: 2},
|
||||
{NodeID: "sunken_temple.kelp_trap", Kind: NodeKindTrap,
|
||||
Label: "Kelp-Snare", PosX: 3, PosY: 2},
|
||||
{NodeID: "sunken_temple.barnacled_steps", Kind: NodeKindExploration,
|
||||
Label: "Barnacled Steps", PosX: 4, PosY: 2},
|
||||
{NodeID: "sunken_temple.drowned_atrium", Kind: NodeKindExploration,
|
||||
Label: "Drowned Atrium", PosX: 5, PosY: 2},
|
||||
{NodeID: "sunken_temple.sunken_nave", Kind: NodeKindExploration,
|
||||
Label: "Sunken Nave", PosX: 6, PosY: 2},
|
||||
{NodeID: "sunken_temple.fork1", Kind: NodeKindFork,
|
||||
Label: "Split Stair", PosX: 2, PosY: 2},
|
||||
Label: "Split Stair", PosX: 7, PosY: 2},
|
||||
|
||||
// Dry subtree.
|
||||
{NodeID: "sunken_temple.dry_corridor", Kind: NodeKindExploration,
|
||||
Label: "Dry Corridor", PosX: 8, PosY: 0},
|
||||
{NodeID: "sunken_temple.silent_columns", Kind: NodeKindExploration,
|
||||
Label: "Silent Columns", PosX: 9, PosY: 0},
|
||||
{NodeID: "sunken_temple.echoing_vault", Kind: NodeKindExploration,
|
||||
Label: "Echoing Vault", PosX: 10, PosY: 0},
|
||||
{NodeID: "sunken_temple.fork2a", Kind: NodeKindFork,
|
||||
Label: "Dry Crossing", PosX: 3, PosY: 0},
|
||||
{NodeID: "sunken_temple.fork2b", Kind: NodeKindFork,
|
||||
Label: "Submerged Crossing", PosX: 3, PosY: 4},
|
||||
Label: "Dry Crossing", PosX: 11, PosY: 0},
|
||||
{NodeID: "sunken_temple.kuo_toa_pen", Kind: NodeKindElite,
|
||||
Label: "Kuo-toa Pen", PosX: 4, PosY: 0},
|
||||
Label: "Kuo-toa Pen", PosX: 12, PosY: -1},
|
||||
{NodeID: "sunken_temple.trophy_pool", Kind: NodeKindExploration,
|
||||
Label: "Trophy Pool", PosX: 13, PosY: -1},
|
||||
{NodeID: "sunken_temple.idol_court", Kind: NodeKindExploration,
|
||||
Label: "Idol Court", PosX: 14, PosY: -1},
|
||||
{NodeID: "sunken_temple.glyph_chamber", Kind: NodeKindExploration,
|
||||
Label: "Glyph Chamber", PosX: 4, PosY: 1},
|
||||
Label: "Glyph Chamber", PosX: 12, PosY: 1},
|
||||
{NodeID: "sunken_temple.silt_arch", Kind: NodeKindExploration,
|
||||
Label: "Silt Arch", PosX: 13, PosY: 1},
|
||||
|
||||
// Wet subtree.
|
||||
{NodeID: "sunken_temple.submerged_passage", Kind: NodeKindExploration,
|
||||
Label: "Submerged Passage", PosX: 8, PosY: 4},
|
||||
{NodeID: "sunken_temple.coral_lattice", Kind: NodeKindExploration,
|
||||
Label: "Coral Lattice", PosX: 9, PosY: 4},
|
||||
{NodeID: "sunken_temple.drowned_nave", Kind: NodeKindExploration,
|
||||
Label: "Drowned Nave", PosX: 10, PosY: 4},
|
||||
{NodeID: "sunken_temple.fork2b", Kind: NodeKindFork,
|
||||
Label: "Submerged Crossing", PosX: 11, PosY: 4},
|
||||
{NodeID: "sunken_temple.aboleth_thralls", Kind: NodeKindExploration,
|
||||
Label: "Thrall Pool", PosX: 4, PosY: 3},
|
||||
Label: "Thrall Pool", PosX: 12, PosY: 3},
|
||||
{NodeID: "sunken_temple.thrall_basin", Kind: NodeKindExploration,
|
||||
Label: "Thrall Basin", PosX: 13, PosY: 3},
|
||||
{NodeID: "sunken_temple.sodden_hall", Kind: NodeKindExploration,
|
||||
Label: "Sodden Hall", PosX: 14, PosY: 3},
|
||||
{NodeID: "sunken_temple.coral_reliquary", Kind: NodeKindSecret,
|
||||
Label: "Coral Reliquary", PosX: 4, PosY: 4,
|
||||
Label: "Coral Reliquary", PosX: 12, PosY: 5,
|
||||
Content: ZoneNodeContent{LootBias: 1.8}},
|
||||
|
||||
{NodeID: "sunken_temple.boss", Kind: NodeKindBoss, IsBoss: true,
|
||||
Label: "Aboleth's Pool", PosX: 5, PosY: 2},
|
||||
Label: "Aboleth's Pool", PosX: 15, PosY: 2},
|
||||
}
|
||||
edges := []ZoneEdge{
|
||||
{From: "sunken_temple.entry", To: "sunken_temple.flooded_atrium", Lock: LockNone},
|
||||
{From: "sunken_temple.flooded_atrium", To: "sunken_temple.fork1", Lock: LockNone},
|
||||
{From: "sunken_temple.fork1", To: "sunken_temple.fork2a", Lock: LockNone, Weight: 1},
|
||||
{From: "sunken_temple.fork1", To: "sunken_temple.fork2b",
|
||||
{From: "sunken_temple.flooded_atrium", To: "sunken_temple.tide_passage", Lock: LockNone},
|
||||
{From: "sunken_temple.tide_passage", To: "sunken_temple.kelp_trap", Lock: LockNone},
|
||||
{From: "sunken_temple.kelp_trap", To: "sunken_temple.barnacled_steps", Lock: LockNone},
|
||||
{From: "sunken_temple.barnacled_steps", To: "sunken_temple.drowned_atrium", Lock: LockNone},
|
||||
{From: "sunken_temple.drowned_atrium", To: "sunken_temple.sunken_nave", Lock: LockNone},
|
||||
{From: "sunken_temple.sunken_nave", To: "sunken_temple.fork1", Lock: LockNone},
|
||||
|
||||
{From: "sunken_temple.fork1", To: "sunken_temple.dry_corridor", Lock: LockNone, Weight: 1},
|
||||
{From: "sunken_temple.fork1", To: "sunken_temple.submerged_passage",
|
||||
Lock: LockPerception, LockData: map[string]any{"dc": 13},
|
||||
Hint: "wet stone glistens down a side passage", Weight: 2},
|
||||
|
||||
{From: "sunken_temple.dry_corridor", To: "sunken_temple.silent_columns", Lock: LockNone},
|
||||
{From: "sunken_temple.silent_columns", To: "sunken_temple.echoing_vault", Lock: LockNone},
|
||||
{From: "sunken_temple.echoing_vault", To: "sunken_temple.fork2a", Lock: LockNone},
|
||||
|
||||
{From: "sunken_temple.fork2a", To: "sunken_temple.kuo_toa_pen", Lock: LockNone, Weight: 1},
|
||||
{From: "sunken_temple.fork2a", To: "sunken_temple.glyph_chamber",
|
||||
Lock: LockStatCheck, LockData: map[string]any{"stat": "STR", "dc": 13},
|
||||
Hint: "a stone door wedged half-shut by silt", Weight: 2},
|
||||
|
||||
{From: "sunken_temple.kuo_toa_pen", To: "sunken_temple.trophy_pool", Lock: LockNone},
|
||||
{From: "sunken_temple.trophy_pool", To: "sunken_temple.idol_court", Lock: LockNone},
|
||||
{From: "sunken_temple.idol_court", To: "sunken_temple.boss", Lock: LockNone},
|
||||
|
||||
{From: "sunken_temple.glyph_chamber", To: "sunken_temple.silt_arch", Lock: LockNone},
|
||||
{From: "sunken_temple.silt_arch", To: "sunken_temple.boss", Lock: LockNone},
|
||||
|
||||
{From: "sunken_temple.submerged_passage", To: "sunken_temple.coral_lattice", Lock: LockNone},
|
||||
{From: "sunken_temple.coral_lattice", To: "sunken_temple.drowned_nave", Lock: LockNone},
|
||||
{From: "sunken_temple.drowned_nave", To: "sunken_temple.fork2b", Lock: LockNone},
|
||||
|
||||
{From: "sunken_temple.fork2b", To: "sunken_temple.aboleth_thralls", Lock: LockNone, Weight: 1},
|
||||
{From: "sunken_temple.fork2b", To: "sunken_temple.coral_reliquary",
|
||||
Lock: LockPerception, LockData: map[string]any{"dc": 15},
|
||||
Hint: "a coral arch glittering under the surface", Weight: 2},
|
||||
{From: "sunken_temple.kuo_toa_pen", To: "sunken_temple.boss", Lock: LockNone},
|
||||
{From: "sunken_temple.glyph_chamber", To: "sunken_temple.boss", Lock: LockNone},
|
||||
{From: "sunken_temple.aboleth_thralls", To: "sunken_temple.boss", Lock: LockNone},
|
||||
|
||||
{From: "sunken_temple.aboleth_thralls", To: "sunken_temple.thrall_basin", Lock: LockNone},
|
||||
{From: "sunken_temple.thrall_basin", To: "sunken_temple.sodden_hall", Lock: LockNone},
|
||||
{From: "sunken_temple.sodden_hall", To: "sunken_temple.boss", Lock: LockNone},
|
||||
|
||||
{From: "sunken_temple.coral_reliquary", To: "sunken_temple.boss", Lock: LockNone},
|
||||
}
|
||||
return BuildGraph(ZoneSunkenTemple, nodes, edges)
|
||||
|
||||
@@ -13,8 +13,12 @@ func TestSunkenTempleGraph_Registered(t *testing.T) {
|
||||
if g.Boss != "sunken_temple.boss" {
|
||||
t.Errorf("boss node = %q", g.Boss)
|
||||
}
|
||||
if len(g.Nodes) != 10 {
|
||||
t.Errorf("nodes = %d, want 10", len(g.Nodes))
|
||||
// Long-expedition D1-b widened this zone from 10 → 26 nodes so the
|
||||
// longest entry→boss walk lands in the T2 [16,20] traversal band.
|
||||
// (Sunken Temple is wide by design — no mid-merge means each leaf
|
||||
// owns its own chain to the boss.)
|
||||
if len(g.Nodes) != 26 {
|
||||
t.Errorf("nodes = %d, want 26", len(g.Nodes))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user