Town-colored homes, split diagonally for mixed-culture households

Shelters now wear their town's culture color on the map (North cool-blue,
South green, West gold, East red), dimmed by wear; granaries stay green,
ruins dark. And bonds can now cross town lines: usually you pair within
your own people (that's who you're near), but a wanderer who warms to
someone from another town shacks up across cultures - the first quiet
thread of contact between two peoples. When two towns share a roof, the
house is drawn split diagonally, half each culture, visible across the map.

Mechanics: buildings track their top-two resident home-towns (recomputed
with occupancy, throttled to every 30 ticks since it's map-only); the
bonding rule dropped its same-town requirement. A 240-day run produced
126 bonds, 40 of them cross-town (East+North, West+South, all combos),
with populations still balanced - inter-cultural contact without
destabilizing the towns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 21:38:57 -04:00
parent 36f6ba1f1b
commit 9e8ee1d748
4 changed files with 112 additions and 11 deletions
+55
View File
@@ -164,6 +164,14 @@ public partial class GameManager : Node2D
return new Vector2(World.GridSize / 2f, World.GridSize / 2f);
}
/// <summary>The soul culture of a named town (for map coloring).</summary>
public SoulType TownSoul(string town)
{
foreach (var t in _towns)
if (t.Name == town) return t.Soul;
return SoulType.Nature;
}
public Vector2 TownCentroid(string town)
{
Vector2 sum = Vector2.Zero;
@@ -325,6 +333,48 @@ public partial class GameManager : Node2D
Npcs.AddRange(newborns);
}
/// <summary>
/// Tally which home-towns sleep under each shelter, storing the top two.
/// A house with residents from two towns is a mixed household — someone
/// shacked up across cultures — and gets drawn split on the map.
/// </summary>
private void RecomputeHouseholds()
{
var counts = new Dictionary<Building, Dictionary<string, int>>();
foreach (var npc in Npcs)
{
if (npc.State != NpcState.Resting) continue;
Building? at = null; float best = 6.25f;
foreach (var b in Buildings)
{
if (b.RestCapacity <= 0) continue;
float d = npc.Position.DistanceSquaredTo(b.Site);
if (d < best) { best = d; at = b; }
}
if (at == null) continue;
if (!counts.TryGetValue(at, out var byTown))
counts[at] = byTown = new Dictionary<string, int>();
byTown[npc.HomeTown] = byTown.GetValueOrDefault(npc.HomeTown) + 1;
}
foreach (var b in Buildings)
{
b.ResidentTownA = "";
b.ResidentTownB = "";
if (!counts.TryGetValue(b, out var byTown)) continue;
string a = "", bTown = ""; int ca = 0, cb = 0;
foreach (var kv in byTown)
{
if (kv.Value > ca) { bTown = a; cb = ca; a = kv.Key; ca = kv.Value; }
else if (kv.Value > cb) { bTown = kv.Key; cb = kv.Value; }
}
b.ResidentTownA = a;
b.ResidentTownB = cb > 0 ? bTown : "";
}
}
/// <summary>Where the community's feet actually are — new communal
/// buildings are founded here, not at a scripted town center.</summary>
public Vector2 CommunityCentroid()
@@ -356,6 +406,11 @@ public partial class GameManager : Node2D
if (at != null) at.Occupancy++;
}
// Household composition (which cultures share each roof) — for the
// map only, so it's cheap and refreshed occasionally, not every tick.
if (TotalTicks % 30 == 0)
RecomputeHouseholds();
TotalTicks++;
foreach (var npc in Npcs)