using Theriapolis.Core.Data; using Theriapolis.Core.World; namespace Theriapolis.Core.Dungeons; /// /// Phase 7 M1 — top-level deterministic entry point for generating the /// interior of a PoI on first visit. /// /// Determinism contract: (worldSeed, poiId) → byte-identical /// across runs. Internally: /// dungeonLayoutSeed = worldSeed ^ C.RNG_DUNGEON_LAYOUT ^ poiId /// /// Anchor-locked PoIs (Old Howl mine, Imperium Ruin showcase, Phase 7 M5 /// content) bypass the procedural pipeline by routing to a pinned-rooms /// layout JSON. The pinned layout names the exact templates to use, in /// order; the assembler's branching policy still applies (typically /// linear). M1 does NOT pin any specific anchor PoI to a layout — that /// wiring lands in M5 alongside side_act_i_old_howl.json and the /// showcase rebuild. M1 ships the routing infrastructure. /// public static class DungeonGenerator { /// /// Pure deterministic generator. Caller supplies the PoI's id (used as /// the per-dungeon sub-seed nonce), the dungeon type (drives layout /// selection + tile family), and the resolved content. /// /// — optional anchor id; when set, /// the generator looks up /// first. M1 /// callers leave it null; M5+ wires the Old Howl + Imperium showcase /// anchor routing. /// public static Dungeon Generate( ulong worldSeed, int poiId, PoiType type, ContentResolver content, string? anchorOverride = null) { if (content is null) throw new ArgumentNullException(nameof(content)); ulong layoutSeed = worldSeed ^ C.RNG_DUNGEON_LAYOUT ^ (ulong)poiId; DungeonLayoutDef layout = ResolveLayout(type, content, anchorOverride); // Build the room-graph plan. RoomGraphAssembler.Plan plan; if (layout.PinnedRooms.Length > 0) { plan = AssemblePinnedLayout(layout, content, layoutSeed); } else { // Procedural layout — pick from typed templates. string typeKey = TypeKeyFor(type); if (!content.RoomTemplatesByType.TryGetValue(typeKey, out var typeTemplates) || typeTemplates.Count == 0) throw new InvalidOperationException( $"No room templates for dungeon type '{typeKey}' (PoiType.{type}). " + "Did you author Content/Data/room_templates/{type}/*.json?"); plan = DungeonLayoutBuilder.Build(layout, typeTemplates, layoutSeed); } // Paint the tiles. var tiles = RoomTilePainter.Paint(plan, content.RoomTemplates, type); return new Dungeon( poiId: poiId, type: type, tiles: tiles, rooms: plan.Rooms, connections: plan.Connections, entranceTile: plan.EntranceTile); } /// /// Find a procedural layout for the given type. M1 picks the first /// matching layout deterministically (room-count band tied to layout /// id); M2+ may add LevelBand-driven small/medium/large selection. /// private static DungeonLayoutDef ResolveLayout(PoiType type, ContentResolver content, string? anchorOverride) { if (!string.IsNullOrEmpty(anchorOverride) && content.DungeonLayoutsByAnchor.TryGetValue(anchorOverride, out var pinned)) return pinned; // Find the first non-anchor layout for this type. Stable order: // dictionary iteration is unordered in C# but DungeonLayouts is built // from a sorted file list (LoadDungeonLayouts orders by file path), // so iteration follows that order on .NET 8. foreach (var l in content.DungeonLayouts.Values) { if (!string.IsNullOrEmpty(l.Anchor)) continue; if (string.Equals(l.DungeonType, type.ToString(), StringComparison.OrdinalIgnoreCase)) return l; } throw new InvalidOperationException( $"No dungeon layout found for PoiType.{type}. " + "Author Content/Data/dungeon_layouts/_.json."); } private static string TypeKeyFor(PoiType type) => type switch { PoiType.ImperiumRuin => "imperium", PoiType.AbandonedMine => "mine", PoiType.CultDen => "cult", PoiType.NaturalCave => "cave", PoiType.OvergrownSettlement => "overgrown", _ => "imperium", }; /// /// Build a plan from a pinned-rooms layout. Pinned layouts always use /// linear branching (the canonical Old Howl + Imperium showcase shape) /// — no retry, no fallback, no random pick. /// private static RoomGraphAssembler.Plan AssemblePinnedLayout( DungeonLayoutDef layout, ContentResolver content, ulong layoutSeed) { var picks = new List(layout.PinnedRooms.Length); var roles = new List(layout.PinnedRooms.Length); foreach (var pin in layout.PinnedRooms) { if (!content.RoomTemplates.TryGetValue(pin.Template, out var def)) throw new InvalidOperationException( $"Pinned layout '{layout.Id}' references unknown template '{pin.Template}'. " + "ContentLoader should have caught this — re-run content-validate."); picks.Add(def); roles.Add(RoomRoleExtensions.Parse(pin.Role)); } var rng = new Util.SeededRng(layoutSeed); var plan = RoomGraphAssembler.TryAssemble(picks, roles, "linear", rng); if (plan is null) throw new InvalidOperationException( $"Pinned layout '{layout.Id}' failed to assemble. Pinned layouts must be hand-validated."); return plan; } }