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

@@ -9,8 +9,10 @@ func TestUnderdarkGraph_Registered(t *testing.T) {
}
// Long-expedition D1-d widened this zone from 10 → 46 nodes so the
// longest entry→boss walk lands in the T4 [28,34] traversal band.
if len(g.Nodes) != 46 {
t.Errorf("nodes = %d, want 46", len(g.Nodes))
// N5/D4 added two throne-gallery secret spokes (Lost Reliquary +
// Sealed Vault) → 48.
if len(g.Nodes) != 48 {
t.Errorf("nodes = %d, want 48", len(g.Nodes))
}
}