using Theriapolis.Core.Data;
using Theriapolis.Core.Items;
using Theriapolis.Core.Loot;
using Theriapolis.Core.World;
namespace Theriapolis.Core.Dungeons;
///
/// Phase 7 M2 — populate a generated 's encounter
/// and container slots. Pure deterministic given the same inputs:
/// (worldSeed, poiId, dungeon, content, levelBand).
///
/// For each room:
/// - Walk the room's source
/// and resolve each slot to a concrete via
///
/// (with a fallback to
/// middle tier if the per-dungeon-type table doesn't list that kind).
/// - Walk the source and
/// pre-roll each container's loot via
/// , mapping the container's
/// loot_table_band through the layout's
/// loot_table_per_band to a concrete table id.
///
/// Boss-room encounter slots use the dungeon-type's "Boss"
/// template if available, otherwise fall back to the "PoiGuard"
/// template — no boss → strong-guard graceful degradation.
///
/// Per-NPC-spawn RNG sub-seed:
/// populateSeed = dungeonLayoutSeed ^ C.RNG_DUNGEON_POPULATE ^ roomId ^ slotIdx
/// — present so future variant rolls (e.g. random which-of-N-equivalent-
/// templates) stay deterministic. M2 doesn't read it (per-kind template
/// is fixed by the override map) but the helper is wired up so M3+
/// content can use it without a contract change.
///
public static class DungeonPopulator
{
///
/// Populate a freshly-generated dungeon.
/// is the dungeon-layout id (from );
/// the populator looks it up via to read
/// loot-band → table mappings. is the
/// PoI's authored level band (0..3) and selects which loot tier
/// each container slot rolls.
///
public static DungeonPopulation Populate(
Dungeon dungeon,
DungeonLayoutDef layout,
ContentResolver content,
int levelBand,
ulong worldSeed)
{
if (dungeon is null) throw new System.ArgumentNullException(nameof(dungeon));
if (layout is null) throw new System.ArgumentNullException(nameof(layout));
if (content is null) throw new System.ArgumentNullException(nameof(content));
ulong dungeonLayoutSeed = worldSeed ^ C.RNG_DUNGEON_LAYOUT ^ (ulong)dungeon.PoiId;
// Resolve loot-band → table-id mapping for this layout + level band.
string lootBand = ResolveLootBand(layout, levelBand);
layout.LootTablePerBand.TryGetValue(lootBand, out var lootTableForBand);
var spawns = new List();
var containers = new List();
// Per-dungeon spawn-kind resolver lookup.
string typeKey = dungeon.Type.ToString();
content.Npcs.SpawnKindToTemplateByDungeonType.TryGetValue(typeKey, out var kindMap);
int globalContainerIdx = 0;
int globalSpawnIdx = 0;
foreach (var room in dungeon.Rooms)
{
if (!content.RoomTemplates.TryGetValue(room.TemplateId, out var def))
continue;
// Encounter slots → concrete spawns.
foreach (var slot in def.EncounterSlots)
{
var template = ResolveSpawnTemplate(slot.Kind, room.Role, kindMap, content.Npcs, dungeon.Type);
if (template is null) { globalSpawnIdx++; continue; }
int absX = room.AabbX + slot.X;
int absY = room.AabbY + slot.Y;
spawns.Add(new DungeonSpawn(
RoomId: room.Id,
X: absX,
Y: absY,
Template: template,
Kind: slot.Kind));
globalSpawnIdx++;
}
// Container slots → pre-rolled loot.
foreach (var slot in def.ContainerSlots)
{
int absX = room.AabbX + slot.X;
int absY = room.AabbY + slot.Y;
// Per-container band: room's container slot may declare its
// own band (e.g. boss room slot says "t3"); otherwise we
// use the layout's level-band → loot-band lookup.
string slotBand = !string.IsNullOrEmpty(slot.LootTableBand) ? slot.LootTableBand : lootBand;
layout.LootTablePerBand.TryGetValue(slotBand, out var slotTableId);
slotTableId ??= lootTableForBand ?? "";
var drops = string.IsNullOrEmpty(slotTableId)
? System.Array.Empty()
: LootGenerator.RollContainer(
tableId: slotTableId,
dungeonLayoutSeed: dungeonLayoutSeed,
slotIdx: globalContainerIdx,
tables: content.LootTables,
items: content.Items);
containers.Add(new DungeonContainer(
RoomId: room.Id,
X: absX,
Y: absY,
TableId: slotTableId ?? "",
Drops: drops,
Locked: slot.Locked,
LockTier: slot.Lock));
globalContainerIdx++;
}
}
return new DungeonPopulation(spawns.ToArray(), containers.ToArray());
}
private static string ResolveLootBand(DungeonLayoutDef layout, int levelBand)
{
var key = levelBand.ToString(System.Globalization.CultureInfo.InvariantCulture);
if (layout.LevelBandToLootBand.TryGetValue(key, out var band) && !string.IsNullOrEmpty(band))
return band;
// Default per the Phase 7 plan §5.5 thresholds:
// levelBand 0..1 → t1, 2 → t2, 3+ → t3.
return levelBand switch
{
<= 1 => "t1",
2 => "t2",
_ => "t3",
};
}
///
/// Resolve a slot's spawn-kind tag to a concrete NPC template. Boss
/// slots get the dungeon-type's "Boss" template if listed; otherwise
/// the slot's own kind, with a graceful fall-through to the
/// existing per-zone table at the mid tier.
///
private static NpcTemplateDef? ResolveSpawnTemplate(
string slotKind,
RoomRole roomRole,
IReadOnlyDictionary? kindMap,
NpcTemplateContent npcs,
PoiType dungeonType)
{
// Boss-role rooms with a boss kind: prefer the per-dungeon-type
// "Boss" entry. If neither the slot says "Boss" nor the room is
// a boss room, this branch doesn't fire.
string effectiveKind = slotKind;
if (roomRole == RoomRole.Boss && string.Equals(slotKind, "Boss", System.StringComparison.OrdinalIgnoreCase))
{
effectiveKind = "Boss";
}
// 1. Per-dungeon-type override.
if (kindMap is not null && kindMap.TryGetValue(effectiveKind, out var tplId))
{
foreach (var t in npcs.Templates)
if (string.Equals(t.Id, tplId, System.StringComparison.OrdinalIgnoreCase))
return t;
}
// 2. Boss kind unmapped → fall back to PoiGuard for the same dungeon type.
if (effectiveKind == "Boss" && kindMap is not null && kindMap.TryGetValue("PoiGuard", out var guardId))
{
foreach (var t in npcs.Templates)
if (string.Equals(t.Id, guardId, System.StringComparison.OrdinalIgnoreCase))
return t;
}
// 3. Final fallback: the per-zone table at zone 2 (mid).
if (npcs.SpawnKindToTemplateByZone.TryGetValue(slotKind, out var byZone) && byZone.Length > 0)
{
int z = System.Math.Min(2, byZone.Length - 1);
string id = byZone[z];
foreach (var t in npcs.Templates)
if (string.Equals(t.Id, id, System.StringComparison.OrdinalIgnoreCase))
return t;
}
return null;
}
}