b451f83174
Captures the pre-Godot-port state of the codebase. This is the rollback anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md). All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
135 lines
5.6 KiB
C#
135 lines
5.6 KiB
C#
using Theriapolis.Core;
|
|
using Theriapolis.Core.Data;
|
|
using Theriapolis.Core.Dungeons;
|
|
using Theriapolis.Core.World;
|
|
using Xunit;
|
|
|
|
namespace Theriapolis.Tests.Dungeons;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<string>
|
|
{
|
|
"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})");
|
|
}
|
|
}
|
|
}
|
|
}
|