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
This commit is contained in:
prosolis
2026-07-10 16:02:15 -07:00
parent 9b6c1ff9a4
commit c37c95a3e3
8 changed files with 454 additions and 16 deletions

View File

@@ -34,6 +34,12 @@ package plugin
// 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{
@@ -108,6 +114,12 @@ func zoneManorBlackspireGraph() ZoneGraph {
{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},
@@ -174,6 +186,9 @@ func zoneManorBlackspireGraph() ZoneGraph {
{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},
@@ -185,6 +200,9 @@ func zoneManorBlackspireGraph() ZoneGraph {
{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},