Files
gogobee/internal/plugin/zone_graph_manor_blackspire.go
prosolis c37c95a3e3 N5/D4: secret content pass — treasure-cache secret rooms + cross-zone keys
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
2026-07-10 16:02:15 -07:00

222 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package plugin
// Haunted Manor of Blackspire branching graph.
//
// Long-expedition plan D1-c: extended from the original 11-node sketch to
// the new T3 length band (2226 rooms traversed). Topology preserves the
// G8d design intent — two stacked 3-way forks ("first zone where the
// player picks twice in a row from a wide menu") with full lock-kind
// coverage (LockNone, LockPerception, LockStatCheck, LockLevelMin) — and
// adds the missing Trap anchor + extends every branch to 3 mid-nodes so
// the longest entry→boss walk lands at 23.
//
// entry → grounds_walk → carriage_path → manor_gate → foyer →
// drawing_room → gallery_corridor → cursed_threshold (TRAP) →
// grand_staircase → great_hall (3-way fork)
// ├─[unlocked]── portrait_gallery → cold_solarium → west_landing ─┐
// ├─[Perception DC 14]── locked_study → study_archive → │
// │ secretary_hall ─────────────────────── ├── second_floor_landing
// └─[INT DC 14]── forbidden_library → library_loft → │
// reading_rotunda ────────────────────────────── ┘
// │
// → portrait_landing → upper_hall (3-way fork) │
// ├─[unlocked]── master_bedroom (ELITE) → grim_balcony → │
// │ moonlit_passage ─────────────────────────────── ┐
// ├─[Perception DC 15]── hidden_oratory (SECRET) → choir_balcony │
// │ → reliquary_dust ───────────────────── ├── spire_corridor
// └─[LevelMin 7]── tower_observatory → spiral_ascent → │
// telescope_attic ───────────────────────────── ┘
// → ancestral_gallery → sanctum_threshold → boss
//
// All six fork branches are 3 mid-nodes long so any route through the
// manor lands at the same 23-node traversal length — the player's choice
// is about loot/encounter character, not shortcut economics. The ELITE
// hangs off the open spoke of upper_hall (cheapest gate, hardest fight);
// the SECRET sits behind the highest-DC Perception with its loot bias
// preserved.
//
// N5/D4 adds a fourth upper_hall spoke: the Sealed Reliquary, a cross-zone
// key vault (LockKey "sunken sigil", earned in the Sunken Temple's Coral
// Reliquary). A one-node loot shortcut back to spire_corridor — reachable
// only by a player who found the sigil, so it never shows on the longest
// path and the 23-node band is unchanged.
func zoneManorBlackspireGraph() ZoneGraph {
nodes := []ZoneNode{
// Pre-fork preamble (path positions 110).
{NodeID: "manor_blackspire.entry", Kind: NodeKindEntry, IsEntry: true,
Label: "Manor Gate", PosX: 0, PosY: 2},
{NodeID: "manor_blackspire.grounds_walk", Kind: NodeKindExploration,
Label: "Overgrown Grounds", PosX: 1, PosY: 2},
{NodeID: "manor_blackspire.carriage_path", Kind: NodeKindExploration,
Label: "Carriage Path", PosX: 2, PosY: 2},
{NodeID: "manor_blackspire.manor_gate", Kind: NodeKindExploration,
Label: "Iron Gate", PosX: 3, PosY: 2},
{NodeID: "manor_blackspire.foyer", Kind: NodeKindExploration,
Label: "Foyer", PosX: 4, PosY: 2},
{NodeID: "manor_blackspire.drawing_room", Kind: NodeKindExploration,
Label: "Drawing Room", PosX: 5, PosY: 2},
{NodeID: "manor_blackspire.gallery_corridor", Kind: NodeKindExploration,
Label: "Gallery Corridor", PosX: 6, PosY: 2},
{NodeID: "manor_blackspire.cursed_threshold", Kind: NodeKindTrap,
Label: "Cursed Threshold", PosX: 7, PosY: 2},
{NodeID: "manor_blackspire.grand_staircase", Kind: NodeKindExploration,
Label: "Grand Staircase", PosX: 8, PosY: 2},
{NodeID: "manor_blackspire.great_hall", Kind: NodeKindFork,
Label: "Great Hall", PosX: 9, PosY: 2},
// great_hall — open spoke (portrait gallery).
{NodeID: "manor_blackspire.portrait_gallery", Kind: NodeKindExploration,
Label: "Portrait Gallery", PosX: 10, PosY: 0},
{NodeID: "manor_blackspire.cold_solarium", Kind: NodeKindExploration,
Label: "Cold Solarium", PosX: 11, PosY: 0},
{NodeID: "manor_blackspire.west_landing", Kind: NodeKindExploration,
Label: "West Landing", PosX: 12, PosY: 0},
// great_hall — perception spoke (locked study).
{NodeID: "manor_blackspire.locked_study", Kind: NodeKindExploration,
Label: "Locked Study", PosX: 10, PosY: 2},
{NodeID: "manor_blackspire.study_archive", Kind: NodeKindExploration,
Label: "Study Archive", PosX: 11, PosY: 2},
{NodeID: "manor_blackspire.secretary_hall", Kind: NodeKindExploration,
Label: "Secretary's Hall", PosX: 12, PosY: 2},
// great_hall — INT spoke (forbidden library).
{NodeID: "manor_blackspire.forbidden_library", Kind: NodeKindExploration,
Label: "Forbidden Library", PosX: 10, PosY: 4},
{NodeID: "manor_blackspire.library_loft", Kind: NodeKindExploration,
Label: "Library Loft", PosX: 11, PosY: 4},
{NodeID: "manor_blackspire.reading_rotunda", Kind: NodeKindExploration,
Label: "Reading Rotunda", PosX: 12, PosY: 4},
// Merge + bridge to upper_hall.
{NodeID: "manor_blackspire.second_floor_landing", Kind: NodeKindMerge,
Label: "Second-Floor Landing", PosX: 13, PosY: 2},
{NodeID: "manor_blackspire.portrait_landing", Kind: NodeKindExploration,
Label: "Portrait Landing", PosX: 14, PosY: 2},
{NodeID: "manor_blackspire.upper_hall", Kind: NodeKindFork,
Label: "Upper Hall", PosX: 15, PosY: 2},
// upper_hall — open spoke (elite).
{NodeID: "manor_blackspire.master_bedroom", Kind: NodeKindElite,
Label: "Master Bedroom", PosX: 16, PosY: 0},
{NodeID: "manor_blackspire.grim_balcony", Kind: NodeKindExploration,
Label: "Grim Balcony", PosX: 17, PosY: 0},
{NodeID: "manor_blackspire.moonlit_passage", Kind: NodeKindExploration,
Label: "Moonlit Passage", PosX: 18, PosY: 0},
// upper_hall — perception spoke (secret).
{NodeID: "manor_blackspire.hidden_oratory", Kind: NodeKindSecret,
Label: "Hidden Oratory", PosX: 16, PosY: 2,
Content: ZoneNodeContent{LootBias: 2.0}},
{NodeID: "manor_blackspire.choir_balcony", Kind: NodeKindExploration,
Label: "Choir Balcony", PosX: 17, PosY: 2},
{NodeID: "manor_blackspire.reliquary_dust", Kind: NodeKindExploration,
Label: "Reliquary Dust", PosX: 18, PosY: 2},
// upper_hall — cross-zone key spoke (N5/D4). A Sunken Temple sigil
// opens this vault; a loot-rich shortcut back to the spire corridor.
{NodeID: "manor_blackspire.sealed_reliquary", Kind: NodeKindSecret,
Label: "Sealed Reliquary", PosX: 16, PosY: -2,
Content: ZoneNodeContent{LootBias: 2.2}},
// upper_hall — level-min spoke (tower).
{NodeID: "manor_blackspire.tower_observatory", Kind: NodeKindExploration,
Label: "Tower Observatory", PosX: 16, PosY: 4},
{NodeID: "manor_blackspire.spiral_ascent", Kind: NodeKindExploration,
Label: "Spiral Ascent", PosX: 17, PosY: 4},
{NodeID: "manor_blackspire.telescope_attic", Kind: NodeKindExploration,
Label: "Telescope Attic", PosX: 18, PosY: 4},
// Merge + boss approach.
{NodeID: "manor_blackspire.spire_corridor", Kind: NodeKindMerge,
Label: "Spire Corridor", PosX: 19, PosY: 2},
{NodeID: "manor_blackspire.ancestral_gallery", Kind: NodeKindExploration,
Label: "Ancestral Gallery", PosX: 20, PosY: 2},
{NodeID: "manor_blackspire.sanctum_threshold", Kind: NodeKindExploration,
Label: "Sanctum Threshold", PosX: 21, PosY: 2},
{NodeID: "manor_blackspire.boss", Kind: NodeKindBoss, IsBoss: true,
Label: "Aldric's Sanctum", PosX: 22, PosY: 2},
}
edges := []ZoneEdge{
// Preamble (linear).
{From: "manor_blackspire.entry", To: "manor_blackspire.grounds_walk", Lock: LockNone},
{From: "manor_blackspire.grounds_walk", To: "manor_blackspire.carriage_path", Lock: LockNone},
{From: "manor_blackspire.carriage_path", To: "manor_blackspire.manor_gate", Lock: LockNone},
{From: "manor_blackspire.manor_gate", To: "manor_blackspire.foyer", Lock: LockNone},
{From: "manor_blackspire.foyer", To: "manor_blackspire.drawing_room", Lock: LockNone},
{From: "manor_blackspire.drawing_room", To: "manor_blackspire.gallery_corridor", Lock: LockNone},
{From: "manor_blackspire.gallery_corridor", To: "manor_blackspire.cursed_threshold", Lock: LockNone},
{From: "manor_blackspire.cursed_threshold", To: "manor_blackspire.grand_staircase", Lock: LockNone},
{From: "manor_blackspire.grand_staircase", To: "manor_blackspire.great_hall", Lock: LockNone},
// great_hall fork — three spokes (open, perception 14, INT 14).
{From: "manor_blackspire.great_hall", To: "manor_blackspire.portrait_gallery", Lock: LockNone, Weight: 1},
{From: "manor_blackspire.great_hall", To: "manor_blackspire.locked_study",
Lock: LockPerception, LockData: map[string]any{"dc": 14},
Hint: "an out-of-place draft from a doorframe", Weight: 2},
{From: "manor_blackspire.great_hall", To: "manor_blackspire.forbidden_library",
Lock: LockStatCheck, LockData: map[string]any{"stat": "INT", "dc": 14},
Hint: "a runed door — you'd need to read it", Weight: 3},
// Portrait spoke.
{From: "manor_blackspire.portrait_gallery", To: "manor_blackspire.cold_solarium", Lock: LockNone},
{From: "manor_blackspire.cold_solarium", To: "manor_blackspire.west_landing", Lock: LockNone},
{From: "manor_blackspire.west_landing", To: "manor_blackspire.second_floor_landing", Lock: LockNone},
// Study spoke.
{From: "manor_blackspire.locked_study", To: "manor_blackspire.study_archive", Lock: LockNone},
{From: "manor_blackspire.study_archive", To: "manor_blackspire.secretary_hall", Lock: LockNone},
{From: "manor_blackspire.secretary_hall", To: "manor_blackspire.second_floor_landing", Lock: LockNone},
// Library spoke.
{From: "manor_blackspire.forbidden_library", To: "manor_blackspire.library_loft", Lock: LockNone},
{From: "manor_blackspire.library_loft", To: "manor_blackspire.reading_rotunda", Lock: LockNone},
{From: "manor_blackspire.reading_rotunda", To: "manor_blackspire.second_floor_landing", Lock: LockNone},
// Bridge to upper_hall.
{From: "manor_blackspire.second_floor_landing", To: "manor_blackspire.portrait_landing", Lock: LockNone},
{From: "manor_blackspire.portrait_landing", To: "manor_blackspire.upper_hall", Lock: LockNone},
// upper_hall fork — three spokes (open elite, perception 15 secret, level-min 7).
{From: "manor_blackspire.upper_hall", To: "manor_blackspire.master_bedroom", Lock: LockNone, Weight: 1},
{From: "manor_blackspire.upper_hall", To: "manor_blackspire.hidden_oratory",
Lock: LockPerception, LockData: map[string]any{"dc": 15},
Hint: "a candle behind a panel that shouldn't have a back", Weight: 2},
{From: "manor_blackspire.upper_hall", To: "manor_blackspire.tower_observatory",
Lock: LockLevelMin, LockData: map[string]any{"min_level": 7},
Hint: "a spiral stair that creaks ominously — climb only if you trust your footing", Weight: 3},
{From: "manor_blackspire.upper_hall", To: "manor_blackspire.sealed_reliquary",
Lock: LockKey, LockData: map[string]any{"key_id": "sunken sigil"},
Hint: "a panel scored with a drowned god's mark — you'd need its twin to open it", Weight: 4},
// Master Bedroom spoke (elite).
{From: "manor_blackspire.master_bedroom", To: "manor_blackspire.grim_balcony", Lock: LockNone},
{From: "manor_blackspire.grim_balcony", To: "manor_blackspire.moonlit_passage", Lock: LockNone},
{From: "manor_blackspire.moonlit_passage", To: "manor_blackspire.spire_corridor", Lock: LockNone},
// Hidden Oratory spoke (secret).
{From: "manor_blackspire.hidden_oratory", To: "manor_blackspire.choir_balcony", Lock: LockNone},
{From: "manor_blackspire.choir_balcony", To: "manor_blackspire.reliquary_dust", Lock: LockNone},
{From: "manor_blackspire.reliquary_dust", To: "manor_blackspire.spire_corridor", Lock: LockNone},
// Sealed Reliquary spoke (cross-zone key — Sunken Sigil).
{From: "manor_blackspire.sealed_reliquary", To: "manor_blackspire.spire_corridor", Lock: LockNone},
// Tower spoke.
{From: "manor_blackspire.tower_observatory", To: "manor_blackspire.spiral_ascent", Lock: LockNone},
{From: "manor_blackspire.spiral_ascent", To: "manor_blackspire.telescope_attic", Lock: LockNone},
{From: "manor_blackspire.telescope_attic", To: "manor_blackspire.spire_corridor", Lock: LockNone},
// Boss approach.
{From: "manor_blackspire.spire_corridor", To: "manor_blackspire.ancestral_gallery", Lock: LockNone},
{From: "manor_blackspire.ancestral_gallery", To: "manor_blackspire.sanctum_threshold", Lock: LockNone},
{From: "manor_blackspire.sanctum_threshold", To: "manor_blackspire.boss", Lock: LockNone},
}
return BuildGraph(ZoneManorBlackspire, nodes, edges)
}
func init() {
registerZoneGraph(zoneManorBlackspireGraph())
}