using Theriapolis.Core; using Theriapolis.Core.Data; using Theriapolis.Core.Dungeons; using Theriapolis.Core.World; using Xunit; namespace Theriapolis.Tests.Dungeons; /// /// Phase 7 M2 — populator tests. Verifies: /// - The same (seed, poi, levelBand) → byte-identical population. /// - Encounter slots resolve to the per-dungeon-type templates. /// - Boss-role rooms use the type's Boss template. /// - Container slots pre-roll loot from the layout's loot-band table. /// public sealed class DungeonPopulatorTests { private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory)); private DungeonPopulation Populate(ulong seed, int poi, PoiType type, int levelBand) { var d = DungeonGenerator.Generate(seed, poi, type, _content); // Find the matching layout (procedural; not anchor-locked). DungeonLayoutDef? layout = null; foreach (var l in _content.DungeonLayouts.Values) if (string.IsNullOrEmpty(l.Anchor) && string.Equals(l.DungeonType, type.ToString(), System.StringComparison.OrdinalIgnoreCase)) { layout = l; break; } Assert.NotNull(layout); return DungeonPopulator.Populate(d, layout!, _content, levelBand, seed); } [Fact] public void Populate_SameInputs_ProducesIdenticalPopulation() { var a = Populate(0xCAFE12345UL, 7, PoiType.ImperiumRuin, levelBand: 2); var b = Populate(0xCAFE12345UL, 7, PoiType.ImperiumRuin, levelBand: 2); Assert.Equal(a.Spawns.Length, b.Spawns.Length); for (int i = 0; i < a.Spawns.Length; i++) { Assert.Equal(a.Spawns[i].RoomId, b.Spawns[i].RoomId); Assert.Equal(a.Spawns[i].X, b.Spawns[i].X); Assert.Equal(a.Spawns[i].Y, b.Spawns[i].Y); Assert.Equal(a.Spawns[i].Template.Id, b.Spawns[i].Template.Id); Assert.Equal(a.Spawns[i].Kind, b.Spawns[i].Kind); } Assert.Equal(a.Containers.Length, b.Containers.Length); for (int i = 0; i < a.Containers.Length; i++) { Assert.Equal(a.Containers[i].TableId, b.Containers[i].TableId); Assert.Equal(a.Containers[i].Drops.Length, b.Containers[i].Drops.Length); for (int j = 0; j < a.Containers[i].Drops.Length; j++) Assert.Equal(a.Containers[i].Drops[j].Def.Id, b.Containers[i].Drops[j].Def.Id); } } [Fact] public void Populate_EncounterSlots_ResolveToTypeTemplates() { var pop = Populate(0x42UL, 1, PoiType.ImperiumRuin, levelBand: 2); // Imperium templates: imperium_undead_thrall (PoiGuard), // imperium_feral_canid (WildAnimal), brigand_marauder (Brigand), // imperium_undead_overseer (Boss). var expected = new HashSet { "imperium_undead_thrall", "imperium_feral_canid", "brigand_marauder", "imperium_undead_overseer", }; foreach (var s in pop.Spawns) Assert.Contains(s.Template.Id, expected); } [Fact] public void Populate_BossRoom_GetsBossTemplate() { // Imperium medium layout requires a boss room. The boss-role room's // encounter slots that declare Boss kind should resolve to the // dungeon type's Boss template. for (int i = 0; i < 5; i++) { var d = DungeonGenerator.Generate(0xB05UL + (ulong)i, i, PoiType.ImperiumRuin, _content); var layout = _content.DungeonLayouts["imperium_medium"]; var pop = DungeonPopulator.Populate(d, layout, _content, levelBand: 2, worldSeed: 0xB05UL + (ulong)i); // Find the boss room. int bossRoomId = -1; foreach (var r in d.Rooms) if (r.Role == RoomRole.Boss) { bossRoomId = r.Id; break; } Assert.NotEqual(-1, bossRoomId); // The boss-room's Boss-kind spawn should be the overseer. bool foundBoss = false; foreach (var s in pop.Spawns) if (s.RoomId == bossRoomId && s.Kind == "Boss") { foundBoss = true; Assert.Equal("imperium_undead_overseer", s.Template.Id); } Assert.True(foundBoss, "Boss room should have a Boss-kind spawn"); } } [Fact] public void Populate_ContainerSlots_HaveDropsAndTable() { var pop = Populate(0xC0FFEEUL, 1, PoiType.ImperiumRuin, levelBand: 2); // Imperium pillar_room_cardinal + sarcophagus_chamber + boss_throne_room // each have a container slot, so we should see at least one. Assert.NotEmpty(pop.Containers); foreach (var c in pop.Containers) { Assert.False(string.IsNullOrEmpty(c.TableId), $"container in room {c.RoomId} has no table id"); } } [Fact] public void Populate_AllContainerTableIds_ResolveToRealTables() { // Across multiple seeds and level bands, every populated container // should reference a loot table that exists in the resolver. This // catches band-mapping bugs (e.g. layout missing a t3 entry) and // confirms the resolver→populator wiring stays coherent. for (int band = 0; band <= 3; band++) for (int i = 0; i < 5; i++) { ulong seed = 0x1007UL + (ulong)i * 1000UL; var pop = Populate(seed, i, PoiType.ImperiumRuin, levelBand: band); foreach (var c in pop.Containers) { Assert.True(_content.LootTables.ContainsKey(c.TableId), $"populator emitted unknown loot table '{c.TableId}' (band={band}, room={c.RoomId})"); } } } }