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); }