Branching zones G8h: Abyss Portal three sequential forks

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) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-09 17:00:57 -07:00
parent 3a8f3c26d6
commit 122a3d7d5d
2 changed files with 162 additions and 0 deletions

View File

@@ -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())
}

View File

@@ -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)
}
}