mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +00:00
Branching zones G8e: Underforge pre-boss gauntlet (late triple fork)
T3 zone, fifth authored graph. Shape: five-node linear preamble (sealed_gate → forge_descent → cooling_river → magma_chamber elite) followed by a single 3-way antechamber fork before the boss. Bends the plan §G8 "two forks for T2+" guideline to a single late fork-with-three-options because the gauntlet shape is the design intent — Kharak Dûn is a one-way descent. Triple-option antechamber (direct / DEX-checked catwalks / Perception-found forge_vault secret) preserves agency at the commitment moment. Map renders as a long horizontal stem with a 3-leaf fan at the right edge — visually distinct from any earlier zone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
71
internal/plugin/zone_graph_underforge.go
Normal file
71
internal/plugin/zone_graph_underforge.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package plugin
|
||||
|
||||
// Phase G8e — The Underforge branching graph.
|
||||
//
|
||||
// T3 zone. Shape: pre-boss gauntlet — long linear preamble through the
|
||||
// forge-city, then a single 3-way antechamber fork right before the
|
||||
// boss. The plan §G8 calls for "two forks for T2+"; this zone bends
|
||||
// that to a single late fork with three options because the gauntlet
|
||||
// shape is the design intent (Kharak Dûn is a one-way descent — there
|
||||
// is no scenic route, only the approach to what was sealed in). The
|
||||
// triple-option antechamber preserves player agency at the
|
||||
// commitment moment.
|
||||
//
|
||||
// entry → sealed_gate → forge_descent → cooling_river → magma_chamber (elite) → antechamber (3-way)
|
||||
// ├─[unlocked]── direct_assault → boss
|
||||
// ├─[DEX DC 14]── catwalks → boss
|
||||
// └─[Perception DC 15]── forge_vault (secret) → boss
|
||||
//
|
||||
// Distinct from prior zones in two ways:
|
||||
// 1. Five-node linear preamble (no zone yet has > 2 linear preamble nodes).
|
||||
// 2. Fork is delayed to the final node before boss; the !zone map
|
||||
// therefore renders as a long horizontal stem with a 3-leaf fan at
|
||||
// the right edge.
|
||||
|
||||
func zoneUnderforgeGraph() ZoneGraph {
|
||||
nodes := []ZoneNode{
|
||||
{NodeID: "underforge.entry", Kind: NodeKindEntry, IsEntry: true,
|
||||
Label: "Sealed Threshold", PosX: 0, PosY: 1},
|
||||
{NodeID: "underforge.sealed_gate", Kind: NodeKindExploration,
|
||||
Label: "Sealed Gate", PosX: 1, PosY: 1},
|
||||
{NodeID: "underforge.forge_descent", Kind: NodeKindExploration,
|
||||
Label: "Forge Descent", PosX: 2, PosY: 1},
|
||||
{NodeID: "underforge.cooling_river", Kind: NodeKindTrap,
|
||||
Label: "Cooling River", PosX: 3, PosY: 1},
|
||||
{NodeID: "underforge.magma_chamber", Kind: NodeKindElite,
|
||||
Label: "Magma Chamber", PosX: 4, PosY: 1},
|
||||
{NodeID: "underforge.antechamber", Kind: NodeKindFork,
|
||||
Label: "Antechamber of Kharak Dûn", PosX: 5, PosY: 1},
|
||||
{NodeID: "underforge.direct_assault", Kind: NodeKindExploration,
|
||||
Label: "Direct Assault", PosX: 6, PosY: 0},
|
||||
{NodeID: "underforge.catwalks", Kind: NodeKindExploration,
|
||||
Label: "Forge Catwalks", PosX: 6, PosY: 1},
|
||||
{NodeID: "underforge.forge_vault", Kind: NodeKindSecret,
|
||||
Label: "Forge Vault", PosX: 6, PosY: 2,
|
||||
Content: ZoneNodeContent{LootBias: 2.0}},
|
||||
{NodeID: "underforge.boss", Kind: NodeKindBoss, IsBoss: true,
|
||||
Label: "The Sealed Hall", PosX: 7, PosY: 1},
|
||||
}
|
||||
edges := []ZoneEdge{
|
||||
{From: "underforge.entry", To: "underforge.sealed_gate", Lock: LockNone},
|
||||
{From: "underforge.sealed_gate", To: "underforge.forge_descent", Lock: LockNone},
|
||||
{From: "underforge.forge_descent", To: "underforge.cooling_river", Lock: LockNone},
|
||||
{From: "underforge.cooling_river", To: "underforge.magma_chamber", Lock: LockNone},
|
||||
{From: "underforge.magma_chamber", To: "underforge.antechamber", Lock: LockNone},
|
||||
{From: "underforge.antechamber", To: "underforge.direct_assault", Lock: LockNone, Weight: 1},
|
||||
{From: "underforge.antechamber", To: "underforge.catwalks",
|
||||
Lock: LockStatCheck, LockData: map[string]any{"stat": "DEX", "dc": 14},
|
||||
Hint: "rusted catwalks above the magma — quick footing required", Weight: 2},
|
||||
{From: "underforge.antechamber", To: "underforge.forge_vault",
|
||||
Lock: LockPerception, LockData: map[string]any{"dc": 15},
|
||||
Hint: "a draft from a stone seam, where no draft should be", Weight: 2},
|
||||
{From: "underforge.direct_assault", To: "underforge.boss", Lock: LockNone},
|
||||
{From: "underforge.catwalks", To: "underforge.boss", Lock: LockNone},
|
||||
{From: "underforge.forge_vault", To: "underforge.boss", Lock: LockNone},
|
||||
}
|
||||
return BuildGraph(ZoneUnderforge, nodes, edges)
|
||||
}
|
||||
|
||||
func init() {
|
||||
registerZoneGraph(zoneUnderforgeGraph())
|
||||
}
|
||||
50
internal/plugin/zone_graph_underforge_test.go
Normal file
50
internal/plugin/zone_graph_underforge_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package plugin
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestUnderforgeGraph_Registered(t *testing.T) {
|
||||
g, ok := zoneGraphRegistry[ZoneUnderforge]
|
||||
if !ok {
|
||||
t.Fatal("zoneUnderforgeGraph not registered")
|
||||
}
|
||||
if len(g.Nodes) != 10 {
|
||||
t.Errorf("nodes = %d, want 10", len(g.Nodes))
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnderforgeGraph_LinearPreamble locks in the gauntlet shape:
|
||||
// the first five nodes after entry must each have exactly one outgoing
|
||||
// edge (linear chain). If a future edit splits the preamble, this test
|
||||
// catches it — that change should re-author the shape comment too.
|
||||
func TestUnderforgeGraph_LinearPreamble(t *testing.T) {
|
||||
g := zoneUnderforgeGraph()
|
||||
for _, id := range []string{
|
||||
"underforge.entry",
|
||||
"underforge.sealed_gate",
|
||||
"underforge.forge_descent",
|
||||
"underforge.cooling_river",
|
||||
"underforge.magma_chamber",
|
||||
} {
|
||||
outs := g.outgoingEdges(id)
|
||||
if len(outs) != 1 {
|
||||
t.Errorf("preamble node %s outgoing = %d, want 1 (gauntlet)", id, len(outs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnderforgeGraph_AntechamberThreeWay(t *testing.T) {
|
||||
g := zoneUnderforgeGraph()
|
||||
outs := g.outgoingEdges("underforge.antechamber")
|
||||
if len(outs) != 3 {
|
||||
t.Fatalf("antechamber outgoing = %d, want 3", len(outs))
|
||||
}
|
||||
for _, leaf := range []string{
|
||||
"underforge.direct_assault",
|
||||
"underforge.catwalks",
|
||||
"underforge.forge_vault",
|
||||
} {
|
||||
if !reachable(g, leaf, "underforge.boss") {
|
||||
t.Errorf("%s unreachable to boss", leaf)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user