mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Long expeditions D1-d: T4 zones to new 28-34 room band
Both T4 zones follow the D1-a/b/c pattern: extend the graph so the longest entry→boss walk lands in band, keep the zone's authored topology intact. underdark (T4): 10 → 46 nodes, longest 30 (band 28-34) feywild_crossing (T4): 9 → 53 nodes, longest 30 (band 28-34) Topology preserved per zone: - Underdark: 4-region authoring intact (every node carries a valid RegionID); 3-way fork1 (drow R2 / illithid R3 / deep_chasm R1) with LockNone / LockPerception / LockStatCheck CON; deep_chasm intentionally stays R1; all three arms converge at the R4 throne_approach merge. Adds the missing Trap anchor (Collapsed Arch) in the R1 preamble. - Feywild Crossing: woven forks intact — hag_circle still has two incoming edges (one per first-stage path), time_eddy stays grove-exclusive, illusion_garden stays marsh-exclusive, fork1 remains CHA-vs-Perception with no LockNone option. Adds the missing Trap anchor (Cursed Thicket) and a fae_court MERGE so the three second-stage endings converge into one final boss approach instead of triplicating it. MinRooms/MaxRooms re-pitched 8-10 → 28-34 so the dice fallback also lands in band for any future graphless variant. T5 zones (dragons_lair, abyss_portal) deferred to D1-e — those need a RegionID backfill on top of the length extension since their regionsByZone entries are not currently honored by their graphs.
This commit is contained in:
@@ -548,8 +548,8 @@ func zoneUnderdark() ZoneDefinition {
|
||||
Faction: "Drow, Mind Flayers, Beholders (far), Ropers, Hook Horrors",
|
||||
Atmosphere: "Absolute darkness, phosphorescent mushroom groves, vast underground seas, carved drow cities in the distance, things older than the surface world.",
|
||||
Hook: "There is a world below the world. It has its own cities, its own wars, its own sky — which is stone, and has never once been kind. I speak more quietly here. Something might be listening.",
|
||||
MinRooms: 8,
|
||||
MaxRooms: 10,
|
||||
MinRooms: 28,
|
||||
MaxRooms: 34,
|
||||
Enemies: []ZoneEnemy{
|
||||
// Phase 5-C: underdark ran 88% (band 45-65, way over)
|
||||
// at the T4 centerline — its sibling feywild sat at
|
||||
@@ -605,8 +605,8 @@ func zoneFeywildCrossing() ZoneDefinition {
|
||||
Faction: "Hags, Redcaps, Will-o-Wisps, Fomorians, Unseelie Fey",
|
||||
Atmosphere: "Impossible beauty, treacherous whimsy, time distortion, rules that change without notice, bargains with terrible fine print.",
|
||||
Hook: "The veil between worlds is thin here. Colors are too saturated. The mushrooms are too large. A small creature made of starlight just offered you a deal. I advise extreme caution regarding deals.",
|
||||
MinRooms: 8,
|
||||
MaxRooms: 10,
|
||||
MinRooms: 28,
|
||||
MaxRooms: 34,
|
||||
Enemies: []ZoneEnemy{
|
||||
// Phase 5-C: feywild trailed at 54% (band 45-65, but
|
||||
// the design goal was to close the 30pp gap with its
|
||||
|
||||
@@ -1,77 +1,275 @@
|
||||
package plugin
|
||||
|
||||
// Phase G8f — Feywild Crossing branching graph.
|
||||
// Feywild Crossing branching graph.
|
||||
//
|
||||
// T4 zone. Shape: woven forks — two first-stage forks each lead to a
|
||||
// second-stage fork; the inner nodes partially overlap so the player's
|
||||
// first choice does not fully determine which second-stage rooms they
|
||||
// can reach. Captures the Feywild theme of "the path you walked is
|
||||
// not the path you arrived on."
|
||||
// Long-expedition plan D1-d: extended from the original 9-node sketch
|
||||
// to the new T4 length band (28–34 rooms traversed). Topology preserves
|
||||
// the G8f design intent — woven first-stage forks (CHA vs Perception,
|
||||
// no LockNone), hag_circle shared between the two first-stage paths,
|
||||
// time_eddy grove-exclusive, illusion_garden marsh-exclusive — and
|
||||
// adds the linear depth each arm now needs.
|
||||
//
|
||||
// entry → twilight_path → fork1
|
||||
// ├─[CHA DC 14]── glamoured_grove ─┬─ hag_circle (elite) ─┐
|
||||
// │ └─[DEX DC 15]── time_eddy ─┤
|
||||
// │ ├── boss
|
||||
// └─[Perception DC 14]── wisp_marsh ─┬─ hag_circle ──────────┤
|
||||
// └─[Perception DC 16]── illusion_garden (secret) ─┘
|
||||
// New in D1-d: a single fae_court MERGE node gathers all three second-
|
||||
// stage endings (hag_circle / time_eddy / illusion_garden) before the
|
||||
// final walk to boss. The Court of the Antlered Queen is one venue, so
|
||||
// the convergence reads thematically; this also avoids triplicating the
|
||||
// long pre-boss approach.
|
||||
//
|
||||
// hag_circle is reachable from BOTH first-stage paths — the elite
|
||||
// encounter is the "you'll meet her either way" room. time_eddy is
|
||||
// only reachable via the grove (CHA path); illusion_garden is only
|
||||
// reachable via the marsh (Perception path).
|
||||
// Also new: cursed_thicket TRAP anchor in the preamble — every walk
|
||||
// hits it. The original G8f graph had no Trap node.
|
||||
//
|
||||
// First authored zone using LockStatCheck CHA — every prior secret/
|
||||
// gate has been Perception/STR/INT/DEX. Both fork1 entries are locked
|
||||
// (no LockNone option from fork1) which is the first instance of "no
|
||||
// free choice"; the player must commit to the lock-check style they're
|
||||
// stronger at.
|
||||
// Preamble (10 nodes, ending in fork1):
|
||||
// entry → twilight_path → veiled_glade → faerie_lights →
|
||||
// cursed_thicket (TRAP) → revel_road → moonshadow_bridge →
|
||||
// bargain_walk → fey_market → fork1
|
||||
//
|
||||
// Fork1 → both options locked (CHA vs Perception, no free choice):
|
||||
//
|
||||
// Grove approach (8 nodes, CHA DC 14):
|
||||
// grove_threshold → starlight_path → singing_orchard → mirror_pond
|
||||
// → prismatic_arbor → moonpetal_clearing → dawnglow_walk →
|
||||
// glamoured_grove (fork2a)
|
||||
//
|
||||
// Marsh approach (8 nodes, Perception DC 14):
|
||||
// marsh_threshold → wisp_lights → fog_basin → moaning_reeds →
|
||||
// mire_steps → submerged_stones → drowned_grove → wisp_marsh
|
||||
// (fork2b)
|
||||
//
|
||||
// Fork2a (glamoured_grove) →
|
||||
// LockNone: hag_corridor_a → dryad_promenade → moonbridge_a → hag_circle
|
||||
// LockStatCheck DEX 15: eddy_corridor → splintered_minute → unwoven_step → time_eddy
|
||||
//
|
||||
// Fork2b (wisp_marsh) →
|
||||
// LockNone: hag_corridor_b → fen_walk → moonbridge_b → hag_circle (SHARED)
|
||||
// LockPerception 16: garden_approach → mirror_walk → unseen_path → illusion_garden
|
||||
//
|
||||
// Post-elite/secret tails to fae_court MERGE (2 mid-nodes each):
|
||||
// hag_circle → courtly_walk → hawthorn_arch → fae_court
|
||||
// time_eddy → backward_corridor → reverberation_hall → fae_court
|
||||
// illusion_garden → false_arbor → unseen_atrium → fae_court
|
||||
//
|
||||
// Final approach (4 mid-nodes + boss):
|
||||
// fae_court → antlered_approach → queens_promenade → throne_steps →
|
||||
// glamoured_dais → boss
|
||||
//
|
||||
// Longest entry→boss walk lands at 30 nodes (e.g. preamble 10 + grove
|
||||
// approach 8 + eddy spur 3 + time_eddy 1 + eddy tail 2 + fae_court 1 +
|
||||
// final approach 4 + boss 1 = 30), inside [28,34]. All four meaningful
|
||||
// walks (grove/marsh × hag/exclusive) hit the same 30-node target by
|
||||
// construction.
|
||||
|
||||
func zoneFeywildCrossingGraph() ZoneGraph {
|
||||
nodes := []ZoneNode{
|
||||
// Preamble.
|
||||
{NodeID: "feywild_crossing.entry", Kind: NodeKindEntry, IsEntry: true,
|
||||
Label: "Veil's Edge", PosX: 0, PosY: 2},
|
||||
{NodeID: "feywild_crossing.twilight_path", Kind: NodeKindExploration,
|
||||
Label: "Twilight Path", PosX: 1, PosY: 2},
|
||||
{NodeID: "feywild_crossing.veiled_glade", Kind: NodeKindExploration,
|
||||
Label: "Veiled Glade", PosX: 2, PosY: 2},
|
||||
{NodeID: "feywild_crossing.faerie_lights", Kind: NodeKindExploration,
|
||||
Label: "Faerie Lights", PosX: 3, PosY: 2},
|
||||
{NodeID: "feywild_crossing.cursed_thicket", Kind: NodeKindTrap,
|
||||
Label: "Cursed Thicket", PosX: 4, PosY: 2},
|
||||
{NodeID: "feywild_crossing.revel_road", Kind: NodeKindExploration,
|
||||
Label: "The Revel Road", PosX: 5, PosY: 2},
|
||||
{NodeID: "feywild_crossing.moonshadow_bridge", Kind: NodeKindExploration,
|
||||
Label: "Moonshadow Bridge", PosX: 6, PosY: 2},
|
||||
{NodeID: "feywild_crossing.bargain_walk", Kind: NodeKindExploration,
|
||||
Label: "The Bargain Walk", PosX: 7, PosY: 2},
|
||||
{NodeID: "feywild_crossing.fey_market", Kind: NodeKindExploration,
|
||||
Label: "Hush Market", PosX: 8, PosY: 2},
|
||||
{NodeID: "feywild_crossing.fork1", Kind: NodeKindFork,
|
||||
Label: "The Bargain Crossroads", PosX: 2, PosY: 2},
|
||||
Label: "The Bargain Crossroads", PosX: 9, PosY: 2},
|
||||
|
||||
// Grove approach (CHA route to glamoured_grove).
|
||||
{NodeID: "feywild_crossing.grove_threshold", Kind: NodeKindExploration,
|
||||
Label: "Grove Threshold", PosX: 10, PosY: 0},
|
||||
{NodeID: "feywild_crossing.starlight_path", Kind: NodeKindExploration,
|
||||
Label: "Starlight Path", PosX: 11, PosY: 0},
|
||||
{NodeID: "feywild_crossing.singing_orchard", Kind: NodeKindExploration,
|
||||
Label: "Singing Orchard", PosX: 12, PosY: 0},
|
||||
{NodeID: "feywild_crossing.mirror_pond", Kind: NodeKindExploration,
|
||||
Label: "Mirror Pond", PosX: 13, PosY: 0},
|
||||
{NodeID: "feywild_crossing.prismatic_arbor", Kind: NodeKindExploration,
|
||||
Label: "Prismatic Arbor", PosX: 14, PosY: 0},
|
||||
{NodeID: "feywild_crossing.moonpetal_clearing", Kind: NodeKindExploration,
|
||||
Label: "Moonpetal Clearing", PosX: 15, PosY: 0},
|
||||
{NodeID: "feywild_crossing.dawnglow_walk", Kind: NodeKindExploration,
|
||||
Label: "Dawnglow Walk", PosX: 16, PosY: 0},
|
||||
{NodeID: "feywild_crossing.glamoured_grove", Kind: NodeKindFork,
|
||||
Label: "Glamoured Grove", PosX: 3, PosY: 1},
|
||||
Label: "Glamoured Grove", PosX: 17, PosY: 0},
|
||||
|
||||
// Marsh approach (Perception route to wisp_marsh).
|
||||
{NodeID: "feywild_crossing.marsh_threshold", Kind: NodeKindExploration,
|
||||
Label: "Marsh Threshold", PosX: 10, PosY: 4},
|
||||
{NodeID: "feywild_crossing.wisp_lights", Kind: NodeKindExploration,
|
||||
Label: "Wisp Lights", PosX: 11, PosY: 4},
|
||||
{NodeID: "feywild_crossing.fog_basin", Kind: NodeKindExploration,
|
||||
Label: "Fog Basin", PosX: 12, PosY: 4},
|
||||
{NodeID: "feywild_crossing.moaning_reeds", Kind: NodeKindExploration,
|
||||
Label: "Moaning Reeds", PosX: 13, PosY: 4},
|
||||
{NodeID: "feywild_crossing.mire_steps", Kind: NodeKindExploration,
|
||||
Label: "Mire Steps", PosX: 14, PosY: 4},
|
||||
{NodeID: "feywild_crossing.submerged_stones", Kind: NodeKindExploration,
|
||||
Label: "Submerged Stones", PosX: 15, PosY: 4},
|
||||
{NodeID: "feywild_crossing.drowned_grove", Kind: NodeKindExploration,
|
||||
Label: "Drowned Grove", PosX: 16, PosY: 4},
|
||||
{NodeID: "feywild_crossing.wisp_marsh", Kind: NodeKindFork,
|
||||
Label: "Wisp Marsh", PosX: 3, PosY: 3},
|
||||
Label: "Wisp Marsh", PosX: 17, PosY: 4},
|
||||
|
||||
// Grove → hag_circle spur.
|
||||
{NodeID: "feywild_crossing.hag_corridor_a", Kind: NodeKindExploration,
|
||||
Label: "Whispering Hedge", PosX: 18, PosY: 1},
|
||||
{NodeID: "feywild_crossing.dryad_promenade", Kind: NodeKindExploration,
|
||||
Label: "Dryad Promenade", PosX: 19, PosY: 1},
|
||||
{NodeID: "feywild_crossing.moonbridge_a", Kind: NodeKindExploration,
|
||||
Label: "Moonbridge", PosX: 20, PosY: 1},
|
||||
|
||||
// Grove → time_eddy spur.
|
||||
{NodeID: "feywild_crossing.eddy_corridor", Kind: NodeKindExploration,
|
||||
Label: "Eddy Corridor", PosX: 18, PosY: 0},
|
||||
{NodeID: "feywild_crossing.splintered_minute", Kind: NodeKindExploration,
|
||||
Label: "Splintered Minute", PosX: 19, PosY: 0},
|
||||
{NodeID: "feywild_crossing.unwoven_step", Kind: NodeKindExploration,
|
||||
Label: "Unwoven Step", PosX: 20, PosY: 0},
|
||||
{NodeID: "feywild_crossing.time_eddy", Kind: NodeKindExploration,
|
||||
Label: "Time Eddy", PosX: 4, PosY: 0},
|
||||
{NodeID: "feywild_crossing.hag_circle", Kind: NodeKindElite,
|
||||
Label: "Hag Circle", PosX: 4, PosY: 2},
|
||||
Label: "Time Eddy", PosX: 21, PosY: 0},
|
||||
|
||||
// Marsh → hag_circle spur.
|
||||
{NodeID: "feywild_crossing.hag_corridor_b", Kind: NodeKindExploration,
|
||||
Label: "Sunken Causeway", PosX: 18, PosY: 3},
|
||||
{NodeID: "feywild_crossing.fen_walk", Kind: NodeKindExploration,
|
||||
Label: "Fen Walk", PosX: 19, PosY: 3},
|
||||
{NodeID: "feywild_crossing.moonbridge_b", Kind: NodeKindExploration,
|
||||
Label: "Drowned Moonbridge", PosX: 20, PosY: 3},
|
||||
|
||||
// Marsh → illusion_garden spur.
|
||||
{NodeID: "feywild_crossing.garden_approach", Kind: NodeKindExploration,
|
||||
Label: "Garden Approach", PosX: 18, PosY: 4},
|
||||
{NodeID: "feywild_crossing.mirror_walk", Kind: NodeKindExploration,
|
||||
Label: "Mirror Walk", PosX: 19, PosY: 4},
|
||||
{NodeID: "feywild_crossing.unseen_path", Kind: NodeKindExploration,
|
||||
Label: "Unseen Path", PosX: 20, PosY: 4},
|
||||
{NodeID: "feywild_crossing.illusion_garden", Kind: NodeKindSecret,
|
||||
Label: "Illusion Garden", PosX: 4, PosY: 4,
|
||||
Label: "Illusion Garden", PosX: 21, PosY: 4,
|
||||
Content: ZoneNodeContent{LootBias: 2.0}},
|
||||
|
||||
// hag_circle (shared elite).
|
||||
{NodeID: "feywild_crossing.hag_circle", Kind: NodeKindElite,
|
||||
Label: "Hag Circle", PosX: 21, PosY: 2},
|
||||
|
||||
// Hag tail.
|
||||
{NodeID: "feywild_crossing.courtly_walk", Kind: NodeKindExploration,
|
||||
Label: "Courtly Walk", PosX: 22, PosY: 2},
|
||||
{NodeID: "feywild_crossing.hawthorn_arch", Kind: NodeKindExploration,
|
||||
Label: "Hawthorn Arch", PosX: 23, PosY: 2},
|
||||
|
||||
// Eddy tail.
|
||||
{NodeID: "feywild_crossing.backward_corridor", Kind: NodeKindExploration,
|
||||
Label: "Backward Corridor", PosX: 22, PosY: 0},
|
||||
{NodeID: "feywild_crossing.reverberation_hall", Kind: NodeKindExploration,
|
||||
Label: "Reverberation Hall", PosX: 23, PosY: 0},
|
||||
|
||||
// Garden tail.
|
||||
{NodeID: "feywild_crossing.false_arbor", Kind: NodeKindExploration,
|
||||
Label: "False Arbor", PosX: 22, PosY: 4},
|
||||
{NodeID: "feywild_crossing.unseen_atrium", Kind: NodeKindExploration,
|
||||
Label: "Unseen Atrium", PosX: 23, PosY: 4},
|
||||
|
||||
// Final approach.
|
||||
{NodeID: "feywild_crossing.fae_court", Kind: NodeKindMerge,
|
||||
Label: "Fae Court", PosX: 24, PosY: 2},
|
||||
{NodeID: "feywild_crossing.antlered_approach", Kind: NodeKindExploration,
|
||||
Label: "Antlered Approach", PosX: 25, PosY: 2},
|
||||
{NodeID: "feywild_crossing.queens_promenade", Kind: NodeKindExploration,
|
||||
Label: "Queen's Promenade", PosX: 26, PosY: 2},
|
||||
{NodeID: "feywild_crossing.throne_steps", Kind: NodeKindExploration,
|
||||
Label: "Throne Steps", PosX: 27, PosY: 2},
|
||||
{NodeID: "feywild_crossing.glamoured_dais", Kind: NodeKindExploration,
|
||||
Label: "Glamoured Dais", PosX: 28, PosY: 2},
|
||||
{NodeID: "feywild_crossing.boss", Kind: NodeKindBoss, IsBoss: true,
|
||||
Label: "Court of the Antlered Queen", PosX: 5, PosY: 2},
|
||||
Label: "Court of the Antlered Queen", PosX: 29, PosY: 2},
|
||||
}
|
||||
edges := []ZoneEdge{
|
||||
// Preamble.
|
||||
{From: "feywild_crossing.entry", To: "feywild_crossing.twilight_path", Lock: LockNone},
|
||||
{From: "feywild_crossing.twilight_path", To: "feywild_crossing.fork1", Lock: LockNone},
|
||||
// Fork1 — both options are locked (CHA vs. Perception).
|
||||
{From: "feywild_crossing.fork1", To: "feywild_crossing.glamoured_grove",
|
||||
{From: "feywild_crossing.twilight_path", To: "feywild_crossing.veiled_glade", Lock: LockNone},
|
||||
{From: "feywild_crossing.veiled_glade", To: "feywild_crossing.faerie_lights", Lock: LockNone},
|
||||
{From: "feywild_crossing.faerie_lights", To: "feywild_crossing.cursed_thicket", Lock: LockNone},
|
||||
{From: "feywild_crossing.cursed_thicket", To: "feywild_crossing.revel_road", Lock: LockNone},
|
||||
{From: "feywild_crossing.revel_road", To: "feywild_crossing.moonshadow_bridge", Lock: LockNone},
|
||||
{From: "feywild_crossing.moonshadow_bridge", To: "feywild_crossing.bargain_walk", Lock: LockNone},
|
||||
{From: "feywild_crossing.bargain_walk", To: "feywild_crossing.fey_market", Lock: LockNone},
|
||||
{From: "feywild_crossing.fey_market", To: "feywild_crossing.fork1", Lock: LockNone},
|
||||
|
||||
// Fork1 — both options locked (CHA vs. Perception, no LockNone).
|
||||
{From: "feywild_crossing.fork1", To: "feywild_crossing.grove_threshold",
|
||||
Lock: LockStatCheck, LockData: map[string]any{"stat": "CHA", "dc": 14},
|
||||
Hint: "a starlight creature offering a deal — you'd have to play along", Weight: 1},
|
||||
{From: "feywild_crossing.fork1", To: "feywild_crossing.wisp_marsh",
|
||||
{From: "feywild_crossing.fork1", To: "feywild_crossing.marsh_threshold",
|
||||
Lock: LockPerception, LockData: map[string]any{"dc": 14},
|
||||
Hint: "wisp-light flickering between two trees — they look like the same tree", Weight: 1},
|
||||
// Grove → hag_circle (open) or time_eddy (DEX).
|
||||
{From: "feywild_crossing.glamoured_grove", To: "feywild_crossing.hag_circle", Lock: LockNone, Weight: 1},
|
||||
{From: "feywild_crossing.glamoured_grove", To: "feywild_crossing.time_eddy",
|
||||
|
||||
// Grove approach.
|
||||
{From: "feywild_crossing.grove_threshold", To: "feywild_crossing.starlight_path", Lock: LockNone},
|
||||
{From: "feywild_crossing.starlight_path", To: "feywild_crossing.singing_orchard", Lock: LockNone},
|
||||
{From: "feywild_crossing.singing_orchard", To: "feywild_crossing.mirror_pond", Lock: LockNone},
|
||||
{From: "feywild_crossing.mirror_pond", To: "feywild_crossing.prismatic_arbor", Lock: LockNone},
|
||||
{From: "feywild_crossing.prismatic_arbor", To: "feywild_crossing.moonpetal_clearing", Lock: LockNone},
|
||||
{From: "feywild_crossing.moonpetal_clearing", To: "feywild_crossing.dawnglow_walk", Lock: LockNone},
|
||||
{From: "feywild_crossing.dawnglow_walk", To: "feywild_crossing.glamoured_grove", Lock: LockNone},
|
||||
|
||||
// Marsh approach.
|
||||
{From: "feywild_crossing.marsh_threshold", To: "feywild_crossing.wisp_lights", Lock: LockNone},
|
||||
{From: "feywild_crossing.wisp_lights", To: "feywild_crossing.fog_basin", Lock: LockNone},
|
||||
{From: "feywild_crossing.fog_basin", To: "feywild_crossing.moaning_reeds", Lock: LockNone},
|
||||
{From: "feywild_crossing.moaning_reeds", To: "feywild_crossing.mire_steps", Lock: LockNone},
|
||||
{From: "feywild_crossing.mire_steps", To: "feywild_crossing.submerged_stones", Lock: LockNone},
|
||||
{From: "feywild_crossing.submerged_stones", To: "feywild_crossing.drowned_grove", Lock: LockNone},
|
||||
{From: "feywild_crossing.drowned_grove", To: "feywild_crossing.wisp_marsh", Lock: LockNone},
|
||||
|
||||
// glamoured_grove (fork2a) → hag spur (open) | eddy spur (DEX).
|
||||
{From: "feywild_crossing.glamoured_grove", To: "feywild_crossing.hag_corridor_a", Lock: LockNone, Weight: 1},
|
||||
{From: "feywild_crossing.glamoured_grove", To: "feywild_crossing.eddy_corridor",
|
||||
Lock: LockStatCheck, LockData: map[string]any{"stat": "DEX", "dc": 15},
|
||||
Hint: "the seconds run sideways here — sidestep at the right one", Weight: 2},
|
||||
// Marsh → hag_circle (shared) or illusion_garden (high Perception secret).
|
||||
{From: "feywild_crossing.wisp_marsh", To: "feywild_crossing.hag_circle", Lock: LockNone, Weight: 1},
|
||||
{From: "feywild_crossing.wisp_marsh", To: "feywild_crossing.illusion_garden",
|
||||
{From: "feywild_crossing.hag_corridor_a", To: "feywild_crossing.dryad_promenade", Lock: LockNone},
|
||||
{From: "feywild_crossing.dryad_promenade", To: "feywild_crossing.moonbridge_a", Lock: LockNone},
|
||||
{From: "feywild_crossing.moonbridge_a", To: "feywild_crossing.hag_circle", Lock: LockNone},
|
||||
{From: "feywild_crossing.eddy_corridor", To: "feywild_crossing.splintered_minute", Lock: LockNone},
|
||||
{From: "feywild_crossing.splintered_minute", To: "feywild_crossing.unwoven_step", Lock: LockNone},
|
||||
{From: "feywild_crossing.unwoven_step", To: "feywild_crossing.time_eddy", Lock: LockNone},
|
||||
|
||||
// wisp_marsh (fork2b) → hag spur (open) | garden spur (Perception secret).
|
||||
{From: "feywild_crossing.wisp_marsh", To: "feywild_crossing.hag_corridor_b", Lock: LockNone, Weight: 1},
|
||||
{From: "feywild_crossing.wisp_marsh", To: "feywild_crossing.garden_approach",
|
||||
Lock: LockPerception, LockData: map[string]any{"dc": 16},
|
||||
Hint: "a garden visible only when you don't look directly at it", Weight: 2},
|
||||
// All three second-stage nodes terminate at boss.
|
||||
{From: "feywild_crossing.hag_circle", To: "feywild_crossing.boss", Lock: LockNone},
|
||||
{From: "feywild_crossing.time_eddy", To: "feywild_crossing.boss", Lock: LockNone},
|
||||
{From: "feywild_crossing.illusion_garden", To: "feywild_crossing.boss", Lock: LockNone},
|
||||
{From: "feywild_crossing.hag_corridor_b", To: "feywild_crossing.fen_walk", Lock: LockNone},
|
||||
{From: "feywild_crossing.fen_walk", To: "feywild_crossing.moonbridge_b", Lock: LockNone},
|
||||
{From: "feywild_crossing.moonbridge_b", To: "feywild_crossing.hag_circle", Lock: LockNone},
|
||||
{From: "feywild_crossing.garden_approach", To: "feywild_crossing.mirror_walk", Lock: LockNone},
|
||||
{From: "feywild_crossing.mirror_walk", To: "feywild_crossing.unseen_path", Lock: LockNone},
|
||||
{From: "feywild_crossing.unseen_path", To: "feywild_crossing.illusion_garden", Lock: LockNone},
|
||||
|
||||
// Post-second-stage tails into fae_court merge.
|
||||
{From: "feywild_crossing.hag_circle", To: "feywild_crossing.courtly_walk", Lock: LockNone},
|
||||
{From: "feywild_crossing.courtly_walk", To: "feywild_crossing.hawthorn_arch", Lock: LockNone},
|
||||
{From: "feywild_crossing.hawthorn_arch", To: "feywild_crossing.fae_court", Lock: LockNone},
|
||||
{From: "feywild_crossing.time_eddy", To: "feywild_crossing.backward_corridor", Lock: LockNone},
|
||||
{From: "feywild_crossing.backward_corridor", To: "feywild_crossing.reverberation_hall", Lock: LockNone},
|
||||
{From: "feywild_crossing.reverberation_hall", To: "feywild_crossing.fae_court", Lock: LockNone},
|
||||
{From: "feywild_crossing.illusion_garden", To: "feywild_crossing.false_arbor", Lock: LockNone},
|
||||
{From: "feywild_crossing.false_arbor", To: "feywild_crossing.unseen_atrium", Lock: LockNone},
|
||||
{From: "feywild_crossing.unseen_atrium", To: "feywild_crossing.fae_court", Lock: LockNone},
|
||||
|
||||
// Final approach.
|
||||
{From: "feywild_crossing.fae_court", To: "feywild_crossing.antlered_approach", Lock: LockNone},
|
||||
{From: "feywild_crossing.antlered_approach", To: "feywild_crossing.queens_promenade", Lock: LockNone},
|
||||
{From: "feywild_crossing.queens_promenade", To: "feywild_crossing.throne_steps", Lock: LockNone},
|
||||
{From: "feywild_crossing.throne_steps", To: "feywild_crossing.glamoured_dais", Lock: LockNone},
|
||||
{From: "feywild_crossing.glamoured_dais", To: "feywild_crossing.boss", Lock: LockNone},
|
||||
}
|
||||
return BuildGraph(ZoneFeywildCrossing, nodes, edges)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,18 @@ func TestFeywildCrossingGraph_Registered(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("zoneFeywildCrossingGraph not registered")
|
||||
}
|
||||
if len(g.Nodes) != 9 {
|
||||
t.Errorf("nodes = %d, want 9", len(g.Nodes))
|
||||
// Long-expedition D1-d widened this zone from 9 → 53 nodes so the
|
||||
// longest entry→boss walk lands in the T4 [28,34] traversal band.
|
||||
if len(g.Nodes) != 53 {
|
||||
t.Errorf("nodes = %d, want 53", len(g.Nodes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeywildCrossingGraph_LongestPathInBand(t *testing.T) {
|
||||
g := zoneFeywildCrossingGraph()
|
||||
got := graphLongestPath(g)
|
||||
if got < 28 || got > 34 {
|
||||
t.Errorf("longest path = %d, want in T4 band [28,34]", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,3 +83,38 @@ func TestFeywildCrossingGraph_FirstCHALock(t *testing.T) {
|
||||
t.Error("expected at least one LockStatCheck CHA edge — Feywild theme")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFeywildCrossingGraph_TrapAnchor verifies D1-d added the missing
|
||||
// Trap node. Original G8f graph had elite/secret but no trap.
|
||||
func TestFeywildCrossingGraph_TrapAnchor(t *testing.T) {
|
||||
g := zoneFeywildCrossingGraph()
|
||||
var trapCount int
|
||||
for _, n := range g.Nodes {
|
||||
if n.Kind == NodeKindTrap {
|
||||
trapCount++
|
||||
}
|
||||
}
|
||||
if trapCount != 1 {
|
||||
t.Errorf("trap nodes = %d, want 1", trapCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFeywildCrossingGraph_FaeCourtMerge verifies all three second-
|
||||
// stage endings (hag_circle, time_eddy, illusion_garden) converge at
|
||||
// the fae_court merge before the final boss approach. D1-d added this
|
||||
// merge to avoid triplicating the long pre-boss walk.
|
||||
func TestFeywildCrossingGraph_FaeCourtMerge(t *testing.T) {
|
||||
g := zoneFeywildCrossingGraph()
|
||||
for _, ending := range []string{
|
||||
"feywild_crossing.hag_circle",
|
||||
"feywild_crossing.time_eddy",
|
||||
"feywild_crossing.illusion_garden",
|
||||
} {
|
||||
if !reachable(g, ending, "feywild_crossing.fae_court") {
|
||||
t.Errorf("%s does not reach fae_court merge", ending)
|
||||
}
|
||||
}
|
||||
if !reachable(g, "feywild_crossing.fae_court", "feywild_crossing.boss") {
|
||||
t.Error("fae_court → boss path missing")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,56 @@
|
||||
package plugin
|
||||
|
||||
// Phase G8i — The Underdark branching graph (multi-region).
|
||||
// The Underdark branching graph (multi-region).
|
||||
//
|
||||
// T4 zone. Shape: 3-way regional fork — fork1 routes the player into
|
||||
// one of three distinct regions (R1 deep, R2 drow, R3 illithid), each
|
||||
// with its own internal mid-node, all converging at R4 (deep_throne).
|
||||
// Long-expedition plan D1-d: extended from the original 10-node sketch
|
||||
// to the new T4 length band (28–34 rooms traversed). Topology preserves
|
||||
// the G8i design intent — three regional arms converge at the deep
|
||||
// throne — and adds the linear depth each arm now needs to feel like a
|
||||
// region in its own right rather than a single side-room.
|
||||
//
|
||||
// R1 surface_tunnels: entry → tunnel_descent → fork1
|
||||
// ┌─[CON DC 15]── deep_chasm (R1, rich harvest) ──────────┐
|
||||
// │ │
|
||||
// ├─[unlocked]── drow_patrol (R2) → drow_captain (R2 elite)─┤
|
||||
// │ ├── throne_approach (R4) → boss (R4)
|
||||
// └─[Perception DC 16]── psionic_corridor (R3) → mind_flayer (R3 elite)─┘
|
||||
// R1 surface_tunnels preamble (8 nodes):
|
||||
// entry → tunnel_descent → moss_corridor → fungal_grove →
|
||||
// collapsed_arch (TRAP) → ledge_walk → shrine_to_lolth → fork1
|
||||
//
|
||||
// Each colored arm crosses a region boundary, exercising the G6
|
||||
// fireGraphRegionTransition hook end-to-end. The four authored regions
|
||||
// match dnd_expedition_region.go:
|
||||
// R1 = underdark_surface_tunnels
|
||||
// R2 = underdark_drow_outpost
|
||||
// R3 = underdark_illithid_warren
|
||||
// R4 = underdark_deep_throne
|
||||
// Fork1 → three regional arms (each crosses a region boundary except
|
||||
// the deep_chasm arm, which intentionally stays in R1 — same as G8i):
|
||||
//
|
||||
// Distinct from prior zones in two ways:
|
||||
// 1. First (and only) authored zone with non-empty RegionID on every
|
||||
// node. Region transitions fire on fork1→{drow_patrol|psionic_corridor}
|
||||
// and on the elite→throne_approach edges.
|
||||
// 2. Convergent triangle: three colored arms, one merge.
|
||||
// R2 drow_outpost arm (12 nodes):
|
||||
// drow_patrol → drow_picket → corridor_of_eyes → spider_warren →
|
||||
// web_passage → drow_chapel → drow_armory → drow_captain (ELITE) →
|
||||
// captain_quarters → drow_descent → drow_passage → drow_gate
|
||||
//
|
||||
// R3 illithid_warren arm (12 nodes):
|
||||
// psionic_corridor → whispering_hall → silenced_chamber →
|
||||
// mind_tank_room → thrall_pens → broodling_chamber →
|
||||
// mind_flayer (ELITE) → flayer_sanctum → illithid_descent →
|
||||
// illithid_passage → illithid_gate → illithid_threshold
|
||||
//
|
||||
// R1 deep_chasm spur (4 nodes — short arm, the “rich-harvest if you
|
||||
// can take the CON-15 climb” route):
|
||||
// deep_chasm (HARVEST) → chasm_floor → chasm_bridge → chasm_ascent
|
||||
//
|
||||
// R4 deep_throne tail (10 nodes including boss):
|
||||
// throne_approach (MERGE) → throne_stairs → throne_corridor →
|
||||
// throne_antechamber → throne_guard_post → throne_doors →
|
||||
// throne_gallery → throne_inner_hall → throne_steps → boss
|
||||
//
|
||||
// Longest entry→boss walk is via either R2 or R3 arm + R4 tail:
|
||||
// 8 (preamble) + 12 (arm) + 10 (tail) = 30 nodes, inside [28,34].
|
||||
// The deep_chasm spur reaches the boss in 8 + 4 + 10 = 22 nodes — a
|
||||
// faster route that trades the elite encounter for the harvest spur,
|
||||
// preserving the original “high-CON cost, no elite, denser loot”
|
||||
// trade-off.
|
||||
//
|
||||
// Per-region totals (single-traverse): R1 surface_tunnels = 8 preamble,
|
||||
// R1 chasm spur = 4 (only one is on any given walk), R2 = 12, R3 = 12,
|
||||
// R4 = 10. Boundary transitions remain at fork1 → arm-entry and
|
||||
// arm-tail → throne_approach; the G6 fireGraphRegionTransition hook
|
||||
// still fires exactly once per fork choice (or twice if the player
|
||||
// later !region-travels into a non-walked arm).
|
||||
//
|
||||
// Trap anchor (Collapsed Arch) is new — the original G8i graph had no
|
||||
// Trap node. Placed in the R1 preamble so every walk hits it.
|
||||
|
||||
func zoneUnderdarkGraph() ZoneGraph {
|
||||
r1 := "underdark_surface_tunnels"
|
||||
@@ -34,32 +59,120 @@ func zoneUnderdarkGraph() ZoneGraph {
|
||||
r4 := "underdark_deep_throne"
|
||||
|
||||
nodes := []ZoneNode{
|
||||
// R1 preamble.
|
||||
{NodeID: "underdark.entry", Kind: NodeKindEntry, IsEntry: true, RegionID: r1,
|
||||
Label: "Cave Mouth", PosX: 0, PosY: 2},
|
||||
{NodeID: "underdark.tunnel_descent", Kind: NodeKindExploration, RegionID: r1,
|
||||
Label: "Tunnel Descent", PosX: 1, PosY: 2},
|
||||
{NodeID: "underdark.moss_corridor", Kind: NodeKindExploration, RegionID: r1,
|
||||
Label: "Glowmoss Corridor", PosX: 2, PosY: 2},
|
||||
{NodeID: "underdark.fungal_grove", Kind: NodeKindExploration, RegionID: r1,
|
||||
Label: "Fungal Grove", PosX: 3, PosY: 2},
|
||||
{NodeID: "underdark.collapsed_arch", Kind: NodeKindTrap, RegionID: r1,
|
||||
Label: "Collapsed Arch", PosX: 4, PosY: 2},
|
||||
{NodeID: "underdark.ledge_walk", Kind: NodeKindExploration, RegionID: r1,
|
||||
Label: "Ledge Walk", PosX: 5, PosY: 2},
|
||||
{NodeID: "underdark.shrine_to_lolth", Kind: NodeKindExploration, RegionID: r1,
|
||||
Label: "Defaced Shrine", PosX: 6, PosY: 2},
|
||||
{NodeID: "underdark.fork1", Kind: NodeKindFork, RegionID: r1,
|
||||
Label: "Three-Way Pass", PosX: 2, PosY: 2},
|
||||
Label: "Three-Way Pass", PosX: 7, PosY: 2},
|
||||
|
||||
// R1 deep_chasm spur (HARVEST, stays in surface_tunnels).
|
||||
{NodeID: "underdark.deep_chasm", Kind: NodeKindHarvest, RegionID: r1,
|
||||
Label: "Deep Chasm", PosX: 3, PosY: 2,
|
||||
Label: "Deep Chasm", PosX: 8, PosY: 4,
|
||||
Content: ZoneNodeContent{LootBias: 1.8}},
|
||||
{NodeID: "underdark.chasm_floor", Kind: NodeKindExploration, RegionID: r1,
|
||||
Label: "Chasm Floor", PosX: 9, PosY: 4},
|
||||
{NodeID: "underdark.chasm_bridge", Kind: NodeKindExploration, RegionID: r1,
|
||||
Label: "Stone-Web Bridge", PosX: 10, PosY: 4},
|
||||
{NodeID: "underdark.chasm_ascent", Kind: NodeKindExploration, RegionID: r1,
|
||||
Label: "Chasm Ascent", PosX: 11, PosY: 4},
|
||||
|
||||
// R2 drow_outpost arm.
|
||||
{NodeID: "underdark.drow_patrol", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Drow Patrol", PosX: 3, PosY: 1},
|
||||
Label: "Drow Patrol", PosX: 8, PosY: 0},
|
||||
{NodeID: "underdark.drow_picket", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Sentry Picket", PosX: 9, PosY: 0},
|
||||
{NodeID: "underdark.corridor_of_eyes", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Corridor of Eyes", PosX: 10, PosY: 0},
|
||||
{NodeID: "underdark.spider_warren", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Spider Warren", PosX: 11, PosY: 0},
|
||||
{NodeID: "underdark.web_passage", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Web Passage", PosX: 12, PosY: 0},
|
||||
{NodeID: "underdark.drow_chapel", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Lolth's Chapel", PosX: 13, PosY: 0},
|
||||
{NodeID: "underdark.drow_armory", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Drow Armory", PosX: 14, PosY: 0},
|
||||
{NodeID: "underdark.drow_captain", Kind: NodeKindElite, RegionID: r2,
|
||||
Label: "Drow Captain's Camp", PosX: 4, PosY: 1},
|
||||
Label: "Drow Captain's Camp", PosX: 15, PosY: 0},
|
||||
{NodeID: "underdark.captain_quarters", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Captain's Quarters", PosX: 16, PosY: 0},
|
||||
{NodeID: "underdark.drow_descent", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Drow Descent", PosX: 17, PosY: 0},
|
||||
{NodeID: "underdark.drow_passage", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Lower Passage", PosX: 18, PosY: 0},
|
||||
{NodeID: "underdark.drow_gate", Kind: NodeKindExploration, RegionID: r2,
|
||||
Label: "Drow Gate", PosX: 19, PosY: 0},
|
||||
|
||||
// R3 illithid_warren arm.
|
||||
{NodeID: "underdark.psionic_corridor", Kind: NodeKindExploration, RegionID: r3,
|
||||
Label: "Psionic Corridor", PosX: 3, PosY: 3},
|
||||
Label: "Psionic Corridor", PosX: 8, PosY: 2},
|
||||
{NodeID: "underdark.whispering_hall", Kind: NodeKindExploration, RegionID: r3,
|
||||
Label: "Whispering Hall", PosX: 9, PosY: 2},
|
||||
{NodeID: "underdark.silenced_chamber", Kind: NodeKindExploration, RegionID: r3,
|
||||
Label: "Silenced Chamber", PosX: 10, PosY: 2},
|
||||
{NodeID: "underdark.mind_tank_room", Kind: NodeKindExploration, RegionID: r3,
|
||||
Label: "Brine Tanks", PosX: 11, PosY: 2},
|
||||
{NodeID: "underdark.thrall_pens", Kind: NodeKindExploration, RegionID: r3,
|
||||
Label: "Thrall Pens", PosX: 12, PosY: 2},
|
||||
{NodeID: "underdark.broodling_chamber", Kind: NodeKindExploration, RegionID: r3,
|
||||
Label: "Broodling Chamber", PosX: 13, PosY: 2},
|
||||
{NodeID: "underdark.mind_flayer", Kind: NodeKindElite, RegionID: r3,
|
||||
Label: "Mind Flayer Elder", PosX: 4, PosY: 3},
|
||||
Label: "Mind Flayer Elder", PosX: 14, PosY: 2},
|
||||
{NodeID: "underdark.flayer_sanctum", Kind: NodeKindExploration, RegionID: r3,
|
||||
Label: "Flayer Sanctum", PosX: 15, PosY: 2},
|
||||
{NodeID: "underdark.illithid_descent", Kind: NodeKindExploration, RegionID: r3,
|
||||
Label: "Illithid Descent", PosX: 16, PosY: 2},
|
||||
{NodeID: "underdark.illithid_passage", Kind: NodeKindExploration, RegionID: r3,
|
||||
Label: "Slime Corridor", PosX: 17, PosY: 2},
|
||||
{NodeID: "underdark.illithid_gate", Kind: NodeKindExploration, RegionID: r3,
|
||||
Label: "Illithid Gate", PosX: 18, PosY: 2},
|
||||
{NodeID: "underdark.illithid_threshold", Kind: NodeKindExploration, RegionID: r3,
|
||||
Label: "Threshold of Thought", PosX: 19, PosY: 2},
|
||||
|
||||
// R4 deep_throne tail.
|
||||
{NodeID: "underdark.throne_approach", Kind: NodeKindMerge, RegionID: r4,
|
||||
Label: "Throne Approach", PosX: 5, PosY: 2},
|
||||
Label: "Throne Approach", PosX: 20, PosY: 2},
|
||||
{NodeID: "underdark.throne_stairs", Kind: NodeKindExploration, RegionID: r4,
|
||||
Label: "Obsidian Stairs", PosX: 21, PosY: 2},
|
||||
{NodeID: "underdark.throne_corridor", Kind: NodeKindExploration, RegionID: r4,
|
||||
Label: "Throne Corridor", PosX: 22, PosY: 2},
|
||||
{NodeID: "underdark.throne_antechamber", Kind: NodeKindExploration, RegionID: r4,
|
||||
Label: "Antechamber", PosX: 23, PosY: 2},
|
||||
{NodeID: "underdark.throne_guard_post", Kind: NodeKindExploration, RegionID: r4,
|
||||
Label: "Guard Post", PosX: 24, PosY: 2},
|
||||
{NodeID: "underdark.throne_doors", Kind: NodeKindExploration, RegionID: r4,
|
||||
Label: "Riven Doors", PosX: 25, PosY: 2},
|
||||
{NodeID: "underdark.throne_gallery", Kind: NodeKindExploration, RegionID: r4,
|
||||
Label: "Throne Gallery", PosX: 26, PosY: 2},
|
||||
{NodeID: "underdark.throne_inner_hall", Kind: NodeKindExploration, RegionID: r4,
|
||||
Label: "Inner Hall", PosX: 27, PosY: 2},
|
||||
{NodeID: "underdark.throne_steps", Kind: NodeKindExploration, RegionID: r4,
|
||||
Label: "Throne Steps", PosX: 28, PosY: 2},
|
||||
{NodeID: "underdark.boss", Kind: NodeKindBoss, IsBoss: true, RegionID: r4,
|
||||
Label: "Deep Throne", PosX: 6, PosY: 2},
|
||||
Label: "Deep Throne", PosX: 29, PosY: 2},
|
||||
}
|
||||
edges := []ZoneEdge{
|
||||
// R1 preamble.
|
||||
{From: "underdark.entry", To: "underdark.tunnel_descent", Lock: LockNone},
|
||||
{From: "underdark.tunnel_descent", To: "underdark.fork1", Lock: LockNone},
|
||||
// Fork1 — three regional arms.
|
||||
{From: "underdark.tunnel_descent", To: "underdark.moss_corridor", Lock: LockNone},
|
||||
{From: "underdark.moss_corridor", To: "underdark.fungal_grove", Lock: LockNone},
|
||||
{From: "underdark.fungal_grove", To: "underdark.collapsed_arch", Lock: LockNone},
|
||||
{From: "underdark.collapsed_arch", To: "underdark.ledge_walk", Lock: LockNone},
|
||||
{From: "underdark.ledge_walk", To: "underdark.shrine_to_lolth", Lock: LockNone},
|
||||
{From: "underdark.shrine_to_lolth", To: "underdark.fork1", Lock: LockNone},
|
||||
|
||||
// Fork1 — three regional arms (unchanged lock/hint identity).
|
||||
{From: "underdark.fork1", To: "underdark.drow_patrol", Lock: LockNone, Weight: 1},
|
||||
{From: "underdark.fork1", To: "underdark.psionic_corridor",
|
||||
Lock: LockPerception, LockData: map[string]any{"dc": 16},
|
||||
@@ -67,15 +180,51 @@ func zoneUnderdarkGraph() ZoneGraph {
|
||||
{From: "underdark.fork1", To: "underdark.deep_chasm",
|
||||
Lock: LockStatCheck, LockData: map[string]any{"stat": "CON", "dc": 15},
|
||||
Hint: "a vertical shaft humming with cold air — the climb will hurt", Weight: 3},
|
||||
|
||||
// R1 spur.
|
||||
{From: "underdark.deep_chasm", To: "underdark.chasm_floor", Lock: LockNone},
|
||||
{From: "underdark.chasm_floor", To: "underdark.chasm_bridge", Lock: LockNone},
|
||||
{From: "underdark.chasm_bridge", To: "underdark.chasm_ascent", Lock: LockNone},
|
||||
{From: "underdark.chasm_ascent", To: "underdark.throne_approach", Lock: LockNone},
|
||||
|
||||
// R2 arm.
|
||||
{From: "underdark.drow_patrol", To: "underdark.drow_captain", Lock: LockNone},
|
||||
{From: "underdark.drow_captain", To: "underdark.throne_approach", Lock: LockNone},
|
||||
{From: "underdark.drow_patrol", To: "underdark.drow_picket", Lock: LockNone},
|
||||
{From: "underdark.drow_picket", To: "underdark.corridor_of_eyes", Lock: LockNone},
|
||||
{From: "underdark.corridor_of_eyes", To: "underdark.spider_warren", Lock: LockNone},
|
||||
{From: "underdark.spider_warren", To: "underdark.web_passage", Lock: LockNone},
|
||||
{From: "underdark.web_passage", To: "underdark.drow_chapel", Lock: LockNone},
|
||||
{From: "underdark.drow_chapel", To: "underdark.drow_armory", Lock: LockNone},
|
||||
{From: "underdark.drow_armory", To: "underdark.drow_captain", Lock: LockNone},
|
||||
{From: "underdark.drow_captain", To: "underdark.captain_quarters", Lock: LockNone},
|
||||
{From: "underdark.captain_quarters", To: "underdark.drow_descent", Lock: LockNone},
|
||||
{From: "underdark.drow_descent", To: "underdark.drow_passage", Lock: LockNone},
|
||||
{From: "underdark.drow_passage", To: "underdark.drow_gate", Lock: LockNone},
|
||||
{From: "underdark.drow_gate", To: "underdark.throne_approach", Lock: LockNone},
|
||||
|
||||
// R3 arm.
|
||||
{From: "underdark.psionic_corridor", To: "underdark.mind_flayer", Lock: LockNone},
|
||||
{From: "underdark.mind_flayer", To: "underdark.throne_approach", Lock: LockNone},
|
||||
// R1 deep arm — single node back to merge.
|
||||
{From: "underdark.deep_chasm", To: "underdark.throne_approach", Lock: LockNone},
|
||||
{From: "underdark.throne_approach", To: "underdark.boss", Lock: LockNone},
|
||||
{From: "underdark.psionic_corridor", To: "underdark.whispering_hall", Lock: LockNone},
|
||||
{From: "underdark.whispering_hall", To: "underdark.silenced_chamber", Lock: LockNone},
|
||||
{From: "underdark.silenced_chamber", To: "underdark.mind_tank_room", Lock: LockNone},
|
||||
{From: "underdark.mind_tank_room", To: "underdark.thrall_pens", Lock: LockNone},
|
||||
{From: "underdark.thrall_pens", To: "underdark.broodling_chamber", Lock: LockNone},
|
||||
{From: "underdark.broodling_chamber", To: "underdark.mind_flayer", Lock: LockNone},
|
||||
{From: "underdark.mind_flayer", To: "underdark.flayer_sanctum", Lock: LockNone},
|
||||
{From: "underdark.flayer_sanctum", To: "underdark.illithid_descent", Lock: LockNone},
|
||||
{From: "underdark.illithid_descent", To: "underdark.illithid_passage", Lock: LockNone},
|
||||
{From: "underdark.illithid_passage", To: "underdark.illithid_gate", Lock: LockNone},
|
||||
{From: "underdark.illithid_gate", To: "underdark.illithid_threshold", Lock: LockNone},
|
||||
{From: "underdark.illithid_threshold", To: "underdark.throne_approach", Lock: LockNone},
|
||||
|
||||
// R4 tail.
|
||||
{From: "underdark.throne_approach", To: "underdark.throne_stairs", Lock: LockNone},
|
||||
{From: "underdark.throne_stairs", To: "underdark.throne_corridor", Lock: LockNone},
|
||||
{From: "underdark.throne_corridor", To: "underdark.throne_antechamber", Lock: LockNone},
|
||||
{From: "underdark.throne_antechamber", To: "underdark.throne_guard_post", Lock: LockNone},
|
||||
{From: "underdark.throne_guard_post", To: "underdark.throne_doors", Lock: LockNone},
|
||||
{From: "underdark.throne_doors", To: "underdark.throne_gallery", Lock: LockNone},
|
||||
{From: "underdark.throne_gallery", To: "underdark.throne_inner_hall", Lock: LockNone},
|
||||
{From: "underdark.throne_inner_hall", To: "underdark.throne_steps", Lock: LockNone},
|
||||
{From: "underdark.throne_steps", To: "underdark.boss", Lock: LockNone},
|
||||
}
|
||||
return BuildGraph(ZoneUnderdark, nodes, edges)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,18 @@ func TestUnderdarkGraph_Registered(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("zoneUnderdarkGraph not registered")
|
||||
}
|
||||
if len(g.Nodes) != 10 {
|
||||
t.Errorf("nodes = %d, want 10", len(g.Nodes))
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnderdarkGraph_LongestPathInBand(t *testing.T) {
|
||||
g := zoneUnderdarkGraph()
|
||||
got := graphLongestPath(g)
|
||||
if got < 28 || got > 34 {
|
||||
t.Errorf("longest path = %d, want in T4 band [28,34]", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +28,10 @@ func TestUnderdarkGraph_Registered(t *testing.T) {
|
||||
func TestUnderdarkGraph_AllNodesHaveRegion(t *testing.T) {
|
||||
g := zoneUnderdarkGraph()
|
||||
validRegions := map[string]bool{
|
||||
"underdark_surface_tunnels": true,
|
||||
"underdark_drow_outpost": true,
|
||||
"underdark_illithid_warren": true,
|
||||
"underdark_deep_throne": true,
|
||||
"underdark_surface_tunnels": true,
|
||||
"underdark_drow_outpost": true,
|
||||
"underdark_illithid_warren": true,
|
||||
"underdark_deep_throne": true,
|
||||
}
|
||||
for id, n := range g.Nodes {
|
||||
if n.RegionID == "" {
|
||||
@@ -95,3 +105,18 @@ func TestUnderdarkGraph_AllArmsReachBoss(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnderdarkGraph_TrapAnchor verifies D1-d added the missing Trap
|
||||
// node. Original G8i graph had elite/boss/harvest but no trap.
|
||||
func TestUnderdarkGraph_TrapAnchor(t *testing.T) {
|
||||
g := zoneUnderdarkGraph()
|
||||
var trapCount int
|
||||
for _, n := range g.Nodes {
|
||||
if n.Kind == NodeKindTrap {
|
||||
trapCount++
|
||||
}
|
||||
}
|
||||
if trapCount != 1 {
|
||||
t.Errorf("trap nodes = %d, want 1", trapCount)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user