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})");
}
}
}
}