using Theriapolis.Core.Data;
using Theriapolis.Core.Entities;
using Theriapolis.Core.Tactical;
using Theriapolis.Core.World;
using Theriapolis.Core.World.Settlements;
using Theriapolis.Core.Util;
namespace Theriapolis.Core.Rules.Combat;
///
/// Phase 6 M1 — turns a chunk's records
/// into live s.
///
/// Each with kind Resident sits at a
/// world-pixel position that emitted from a
/// . Resolution:
///
/// 1. Walk the world's settlements. Find the one whose
/// contains a building footprint
/// that contains this spawn point. Within that building, find the
/// slot whose SpawnX/SpawnY match — that's the role tag.
/// 2. Look up the resident template. Named (anchor-prefixed) tags hit
/// directly. Generic
/// tags hit filtered by
/// equality, weighted by
/// .
/// 3. Build an from the chosen template. Register
/// named-role NPCs in the so quest
/// scripts can resolve them by symbolic id.
///
/// The lookup is a linear walk over settlements (small N — < 100) but is
/// deterministic for a given (worldSeed, chunk, spawnIndex).
///
public static class ResidentInstantiator
{
///
/// Resolve and spawn an NpcActor for a single Resident spawn record.
/// Returns null when the world has no resident template configured for
/// this slot's role tag (the spawn is silently dropped — the building
/// just stays empty, which is fine).
///
public static NpcActor? Spawn(
ulong worldSeed,
TacticalChunk chunk,
int spawnIndex,
TacticalSpawn spawn,
WorldState world,
ContentResolver content,
ActorManager actors,
AnchorRegistry? registry = null)
{
if (spawn.Kind != SpawnKind.Resident) return null;
int worldPxX = chunk.OriginX + spawn.LocalX;
int worldPxY = chunk.OriginY + spawn.LocalY;
if (!TryFindSlot(world, worldPxX, worldPxY, out var settlement, out var building, out var slot))
return null;
var template = ResolveTemplate(slot.RoleTag, content, worldSeed, settlement!.Id, building!.Id, spawnIndex);
if (template is null) return null;
var npc = new NpcActor(template)
{
Id = -1, // ActorManager assigns
Position = new Vec2(worldPxX, worldPxY),
SourceChunk = chunk.Coord,
SourceSpawnIndex = spawnIndex,
// The named role tag wins over the generic one declared on the
// template — preserves "millhaven.innkeeper" identity even when
// the generic "innkeeper" template is what spawned.
RoleTag = string.IsNullOrEmpty(slot.RoleTag) ? template.RoleTag : slot.RoleTag,
// Phase 6 M5 — anchor the resident to its host settlement so
// RepPropagation can compute their local faction standing.
HomeSettlementId = settlement.Id,
};
var spawned = actors.SpawnNpc(npc);
if (registry is not null)
{
if (settlement.Anchor is not null)
registry.RegisterAnchor(settlement.Anchor.Value, settlement.Id);
registry.RegisterRole(spawned.RoleTag, spawned.Id);
}
return spawned;
}
///
/// Pick the resident template for a given role tag. Named anchor-
/// prefixed tags ("millhaven.innkeeper") prefer named templates;
/// generic tags ("innkeeper") roll among matching generics by weight.
///
public static ResidentTemplateDef? ResolveTemplate(
string roleTag,
ContentResolver content,
ulong worldSeed,
int settlementId,
int buildingId,
int spawnIndex)
{
// Named, anchor-prefixed: prefer the exact match.
if (content.ResidentsByRoleTag.TryGetValue(roleTag, out var named))
return named;
// Generic: collect all unnamed templates whose RoleTag equals the
// suffix-stripped tag (e.g. "millhaven.innkeeper" → "innkeeper").
string suffix = roleTag;
int dot = roleTag.LastIndexOf('.');
if (dot >= 0) suffix = roleTag[(dot + 1)..];
var pool = new List();
foreach (var r in content.Residents.Values)
if (!r.Named && string.Equals(r.RoleTag, suffix, System.StringComparison.OrdinalIgnoreCase))
pool.Add(r);
if (pool.Count == 0) return null;
if (pool.Count == 1) return pool[0];
// Weighted roll, deterministic per (worldSeed, settlementId, buildingId, spawnIndex).
var rng = SeededRng.ForSubsystem(worldSeed,
unchecked(C.RNG_NPC_SPAWN ^ (ulong)settlementId
^ ((ulong)buildingId << 16)
^ ((ulong)spawnIndex << 32)));
// Sort for stable iteration before the RNG roll.
pool.Sort(static (a, b) => string.Compare(a.Id, b.Id, System.StringComparison.Ordinal));
float total = 0f;
foreach (var t in pool) total += System.Math.Max(0f, t.Weight);
if (total <= 0f) return pool[0];
float roll = rng.NextFloat() * total;
float acc = 0f;
foreach (var t in pool)
{
acc += System.Math.Max(0f, t.Weight);
if (roll <= acc) return t;
}
return pool[^1];
}
///
/// Walk the world's settlements to find the one whose building footprint
/// contains / AND
/// whose resident slot sits exactly on that point.
///
public static bool TryFindSlot(
WorldState world, int worldPxX, int worldPxY,
out Settlement? settlement, out BuildingFootprint? building, out BuildingResidentSlot slot)
{
settlement = null;
building = null;
slot = default;
foreach (var s in world.Settlements)
{
if (!s.BuildingsResolved) continue;
foreach (var b in s.Buildings)
{
if (!b.ContainsTile(worldPxX, worldPxY)) continue;
foreach (var r in b.Residents)
{
if (r.SpawnX == worldPxX && r.SpawnY == worldPxY)
{
settlement = s;
building = b;
slot = r;
return true;
}
}
}
}
return false;
}
}