From 122a3d7d5dd51f0d89198c6df1aab3bde1a1403d Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Sat, 9 May 2026 17:00:57 -0700 Subject: [PATCH] Branching zones G8h: Abyss Portal three sequential forks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T5 zone, eighth authored graph. Shape: three fork nodes in series — binary, binary, ternary capstone. Player makes three separate fork decisions in this zone, more than any other shipping zone, matching the "things keep getting worse" thematic descent. First zone with LockStatCheck CON (mind_corridor, DC 16, psychic pressure flavor) — completes the full ability roster (STR/DEX/CON/ INT/WIS/CHA) across shipping zones by G8h. reality_seam secret LootBias 3.0, the highest authored. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/plugin/zone_graph_abyss_portal.go | 92 +++++++++++++++++++ .../plugin/zone_graph_abyss_portal_test.go | 70 ++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 internal/plugin/zone_graph_abyss_portal.go create mode 100644 internal/plugin/zone_graph_abyss_portal_test.go diff --git a/internal/plugin/zone_graph_abyss_portal.go b/internal/plugin/zone_graph_abyss_portal.go new file mode 100644 index 0000000..f815db1 --- /dev/null +++ b/internal/plugin/zone_graph_abyss_portal.go @@ -0,0 +1,92 @@ +package plugin + +// Phase G8h — The Abyss Portal branching graph. +// +// T5 zone. Shape: three sequential forks at increasing depth — binary, +// binary, ternary capstone. The player makes more decisions in this +// zone than in any other (3 separate fork prompts), matching the +// "things keep getting worse" thematic descent. +// +// entry → fractured_threshold → fork1 (binary) +// ├─[unlocked]── burning_wastes ─┐ +// └─[Perception DC 16]── silent_chambers ─┤ +// ↓ +// fork2_a (binary) +// ├─[unlocked]── vrock_aerie (elite marilith) ─┐ +// └─[CON DC 16]── mind_corridor ───────────────┤ +// ↓ +// fork3 (3-way capstone) +// ├─[unlocked]── direct_assault → boss +// ├─[CHA DC 18]── usurper_throne → boss +// └─[Perception DC 18]── reality_seam (secret) → boss +// +// First authored zone with three sequential forks AND first use of +// LockStatCheck CON — completing the full ability roster (STR/DEX/ +// CON/INT/WIS/CHA all appear in shipping zones by G8h). reality_seam +// LootBias 3.0 reflects the Abyss capstone secret. + +func zoneAbyssPortalGraph() ZoneGraph { + nodes := []ZoneNode{ + {NodeID: "abyss_portal.entry", Kind: NodeKindEntry, IsEntry: true, + Label: "The Open Door", PosX: 0, PosY: 2}, + {NodeID: "abyss_portal.fractured_threshold", Kind: NodeKindExploration, + Label: "Fractured Threshold", PosX: 1, PosY: 2}, + {NodeID: "abyss_portal.fork1", Kind: NodeKindFork, + Label: "First Reality-Break", PosX: 2, PosY: 2}, + {NodeID: "abyss_portal.burning_wastes", Kind: NodeKindExploration, + Label: "Burning Wastes", PosX: 3, PosY: 1}, + {NodeID: "abyss_portal.silent_chambers", Kind: NodeKindExploration, + Label: "Silent Chambers", PosX: 3, PosY: 3}, + {NodeID: "abyss_portal.fork2", Kind: NodeKindFork, + Label: "Second Reality-Break", PosX: 4, PosY: 2}, + {NodeID: "abyss_portal.vrock_aerie", Kind: NodeKindElite, + Label: "Marilith's Aerie", PosX: 5, PosY: 1}, + {NodeID: "abyss_portal.mind_corridor", Kind: NodeKindExploration, + Label: "Corridor of Whispers", PosX: 5, PosY: 3}, + {NodeID: "abyss_portal.fork3", Kind: NodeKindFork, + Label: "Third Reality-Break", PosX: 6, PosY: 2}, + {NodeID: "abyss_portal.direct_assault", Kind: NodeKindExploration, + Label: "Direct Assault", PosX: 7, PosY: 1}, + {NodeID: "abyss_portal.usurper_throne", Kind: NodeKindExploration, + Label: "Usurper's Approach", PosX: 7, PosY: 2}, + {NodeID: "abyss_portal.reality_seam", Kind: NodeKindSecret, + Label: "Reality Seam", PosX: 7, PosY: 3, + Content: ZoneNodeContent{LootBias: 3.0}}, + {NodeID: "abyss_portal.boss", Kind: NodeKindBoss, IsBoss: true, + Label: "Belaxath's Throne", PosX: 8, PosY: 2}, + } + edges := []ZoneEdge{ + {From: "abyss_portal.entry", To: "abyss_portal.fractured_threshold", Lock: LockNone}, + {From: "abyss_portal.fractured_threshold", To: "abyss_portal.fork1", Lock: LockNone}, + // Fork1 — perception side path. + {From: "abyss_portal.fork1", To: "abyss_portal.burning_wastes", Lock: LockNone, Weight: 1}, + {From: "abyss_portal.fork1", To: "abyss_portal.silent_chambers", + Lock: LockPerception, LockData: map[string]any{"dc": 16}, + Hint: "a silence so complete you can hear your own pulse — and something else's", Weight: 2}, + {From: "abyss_portal.burning_wastes", To: "abyss_portal.fork2", Lock: LockNone}, + {From: "abyss_portal.silent_chambers", To: "abyss_portal.fork2", Lock: LockNone}, + // Fork2 — CON wall (psychic pressure). + {From: "abyss_portal.fork2", To: "abyss_portal.vrock_aerie", Lock: LockNone, Weight: 1}, + {From: "abyss_portal.fork2", To: "abyss_portal.mind_corridor", + Lock: LockStatCheck, LockData: map[string]any{"stat": "CON", "dc": 16}, + Hint: "the air thickens — your bones know the wrong direction is also down", Weight: 2}, + {From: "abyss_portal.vrock_aerie", To: "abyss_portal.fork3", Lock: LockNone}, + {From: "abyss_portal.mind_corridor", To: "abyss_portal.fork3", Lock: LockNone}, + // Fork3 — capstone 3-way. + {From: "abyss_portal.fork3", To: "abyss_portal.direct_assault", Lock: LockNone, Weight: 1}, + {From: "abyss_portal.fork3", To: "abyss_portal.usurper_throne", + Lock: LockStatCheck, LockData: map[string]any{"stat": "CHA", "dc": 18}, + Hint: "Belaxath has not noticed you yet — claim authority before he does", Weight: 2}, + {From: "abyss_portal.fork3", To: "abyss_portal.reality_seam", + Lock: LockPerception, LockData: map[string]any{"dc": 18}, + Hint: "a hairline crack in the air itself — somewhere there is a place that is not here", Weight: 3}, + {From: "abyss_portal.direct_assault", To: "abyss_portal.boss", Lock: LockNone}, + {From: "abyss_portal.usurper_throne", To: "abyss_portal.boss", Lock: LockNone}, + {From: "abyss_portal.reality_seam", To: "abyss_portal.boss", Lock: LockNone}, + } + return BuildGraph(ZoneAbyssPortal, nodes, edges) +} + +func init() { + registerZoneGraph(zoneAbyssPortalGraph()) +} diff --git a/internal/plugin/zone_graph_abyss_portal_test.go b/internal/plugin/zone_graph_abyss_portal_test.go new file mode 100644 index 0000000..bcd64e0 --- /dev/null +++ b/internal/plugin/zone_graph_abyss_portal_test.go @@ -0,0 +1,70 @@ +package plugin + +import "testing" + +func TestAbyssPortalGraph_Registered(t *testing.T) { + g, ok := zoneGraphRegistry[ZoneAbyssPortal] + if !ok { + t.Fatal("zoneAbyssPortalGraph not registered") + } + if len(g.Nodes) != 13 { + t.Errorf("nodes = %d, want 13", len(g.Nodes)) + } +} + +// TestAbyssPortalGraph_ThreeSequentialForks confirms the design shape: +// three fork nodes in series, each binary or ternary. +func TestAbyssPortalGraph_ThreeSequentialForks(t *testing.T) { + g := zoneAbyssPortalGraph() + wants := map[string]int{ + "abyss_portal.fork1": 2, + "abyss_portal.fork2": 2, + "abyss_portal.fork3": 3, + } + for id, want := range wants { + if got := len(g.outgoingEdges(id)); got != want { + t.Errorf("%s outgoing = %d, want %d", id, got, want) + } + } +} + +func TestAbyssPortalGraph_AllCapstoneLeavesReachBoss(t *testing.T) { + g := zoneAbyssPortalGraph() + for _, leaf := range []string{ + "abyss_portal.direct_assault", + "abyss_portal.usurper_throne", + "abyss_portal.reality_seam", + } { + if !reachable(g, leaf, "abyss_portal.boss") { + t.Errorf("%s unreachable to boss", leaf) + } + } +} + +// TestAbyssPortalGraph_FullStatRosterCoverage confirms the +// project-wide claim: by G8h, all six abilities (STR/DEX/CON/INT/WIS/ +// CHA) appear as authored stat-check locks across shipping zones. +// CON is the missing one prior to this zone — locked here on +// mind_corridor. +func TestAbyssPortalGraph_FullStatRosterCoverage(t *testing.T) { + g := zoneAbyssPortalGraph() + conSeen := false + for _, outs := range g.Edges { + for _, e := range outs { + if e.Lock == LockStatCheck && lockDataString(e.LockData, "stat") == "CON" { + conSeen = true + } + } + } + if !conSeen { + t.Error("expected at least one CON stat-check edge — completes ability roster by G8h") + } +} + +func TestAbyssPortalGraph_RealitySeamHighestBias(t *testing.T) { + g := zoneAbyssPortalGraph() + seam := g.Nodes["abyss_portal.reality_seam"] + if seam.Content.LootBias < 3.0 { + t.Errorf("reality_seam LootBias = %v, want >= 3.0 (Abyss capstone)", seam.Content.LootBias) + } +}