diff --git a/scripts/Building.cs b/scripts/Building.cs
index 9a8f7bf..c23a0bf 100644
--- a/scripts/Building.cs
+++ b/scripts/Building.cs
@@ -99,6 +99,13 @@ public class Building
/// whole population per building (that was the sim's O(n²) hot spot).
public int Occupancy;
+ /// The two most-represented home-towns among this shelter's
+ /// current sleepers — for the map's household coloring. When two towns
+ /// share a roof (someone shacked up across cultures), the house is drawn
+ /// split. Recomputed with Occupancy each tick.
+ public string ResidentTownA = "";
+ public string ResidentTownB = "";
+
public int RestCapacity =>
Kind == BuildingKind.Shelter && IsComplete && !IsRuined ? 3 + Modules : 0;
diff --git a/scripts/GameManager.cs b/scripts/GameManager.cs
index ddf351a..57df8fe 100644
--- a/scripts/GameManager.cs
+++ b/scripts/GameManager.cs
@@ -164,6 +164,14 @@ public partial class GameManager : Node2D
return new Vector2(World.GridSize / 2f, World.GridSize / 2f);
}
+ /// The soul culture of a named town (for map coloring).
+ 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);
}
+ ///
+ /// 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.
+ ///
+ private void RecomputeHouseholds()
+ {
+ var counts = new Dictionary>();
+
+ 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();
+ 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 : "";
+ }
+ }
+
/// Where the community's feet actually are — new communal
/// buildings are founded here, not at a scripted town center.
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)
diff --git a/scripts/NPC.cs b/scripts/NPC.cs
index cd6ca89..5338d27 100644
--- a/scripts/NPC.cs
+++ b/scripts/NPC.cs
@@ -950,15 +950,20 @@ public class Npc
nearest = found;
// Consider bonding: an unbonded adult who keeps warm company with a
- // compatible, unbonded neighbor may pair with them. Same town, real
- // warmth, both grown — companionship chosen, not assigned.
+ // compatible, unbonded neighbor may pair with them — real warmth,
+ // both grown, companionship chosen not assigned. Town is NOT a
+ // requirement: usually you bond within your own people (that's who
+ // you're near), but a wanderer who warms to someone from another
+ // town can shack up across cultures — the first quiet thread of
+ // contact between two peoples.
if (!IsBonded && IsAdult && nearest is { IsBonded: false, IsAdult: true } n &&
- n.HomeTown == HomeTown && Position.DistanceTo(n.Position) < 4f &&
+ Position.DistanceTo(n.Position) < 4f &&
GetWarmth(n.Name) > 0.4f && n.GetWarmth(Name) > 0.4f)
{
Partner = n;
n.Partner = this;
- Godot.GD.Print($"[Bond] day {gm.Day}: {Name} and {n.Name} ({HomeTown}) pair up.");
+ string kind = n.HomeTown == HomeTown ? HomeTown : $"{HomeTown}+{n.HomeTown}";
+ Godot.GD.Print($"[Bond] day {gm.Day}: {Name} and {n.Name} ({kind}) pair up.");
}
if (nearest != null && Position.DistanceTo(nearest.Position) > 3f)
{
diff --git a/scripts/Visualization.cs b/scripts/Visualization.cs
index 30e3ca7..c11f805 100644
--- a/scripts/Visualization.cs
+++ b/scripts/Visualization.cs
@@ -83,18 +83,52 @@ public partial class Visualization : Node2D
}
// --- Map: buildings ----------------------------------------------
+ // Shelters wear their town's culture-color; granaries stay green;
+ // ruins go dark. A house shared by two towns (a mixed household) is
+ // split diagonally, half each culture — you can see across the map
+ // where someone shacked up with a neighbor from another people.
for (int i = 0; i < _gm.Buildings.Count; i++)
{
var b = _gm.Buildings[i];
- Color c = b.IsRuined ? new Color(0.25f, 0.23f, 0.20f)
- : b.Kind == BuildingKind.Granary
- ? (b.IsComplete ? new Color(0.55f, 0.70f, 0.40f)
- : new Color(0.45f, 0.52f, 0.32f))
- : b.IsComplete ? new Color(0.9f, 0.85f, 0.6f).Darkened((1f - b.Condition) * 0.5f)
- : new Color(0.6f, 0.5f, 0.3f);
float size = CellPixels + 3f + (b.CurrentStage + b.StageProgress) * 2f + b.Modules * 1.5f;
var p = MapPos(b.Site);
- DrawRect(new Rect2(p.X - size / 2f, p.Y - size / 2f, size, size), c);
+ var rect = new Rect2(p.X - size / 2f, p.Y - size / 2f, size, size);
+
+ if (b.IsRuined)
+ {
+ DrawRect(rect, new Color(0.25f, 0.23f, 0.20f));
+ }
+ else if (b.Kind == BuildingKind.Granary)
+ {
+ DrawRect(rect, b.IsComplete ? new Color(0.55f, 0.70f, 0.40f)
+ : new Color(0.45f, 0.52f, 0.32f));
+ }
+ else
+ {
+ // Base color = the house's own town culture, dimmed by wear.
+ float dim = b.IsComplete ? (1f - b.Condition) * 0.5f : 0.35f;
+ Color own = TownColor(_gm.TownSoul(b.Town)).Darkened(dim);
+
+ bool mixed = b.ResidentTownB != "" && b.ResidentTownB != b.ResidentTownA;
+ if (mixed)
+ {
+ // Split diagonally: town A on the upper-left triangle,
+ // town B on the lower-right.
+ Color ca = TownColor(_gm.TownSoul(b.ResidentTownA)).Darkened(dim);
+ Color cb = TownColor(_gm.TownSoul(b.ResidentTownB)).Darkened(dim);
+ var tl = new Vector2(rect.Position.X, rect.Position.Y);
+ var tr = new Vector2(rect.End.X, rect.Position.Y);
+ var br = new Vector2(rect.End.X, rect.End.Y);
+ var bl = new Vector2(rect.Position.X, rect.End.Y);
+ DrawColoredPolygon(new[] { tl, tr, bl }, ca); // upper-left ▲
+ DrawColoredPolygon(new[] { tr, br, bl }, cb); // lower-right ▼
+ }
+ else
+ {
+ DrawRect(rect, own);
+ }
+ }
+
DrawString(font, p + new Vector2(size / 2f + 2f, 4f), $"{i + 1}",
fontSize: 10, modulate: TextDim);
}