using Godot; namespace WorldSim; /// /// Top-down 2D view + debug roster (SPEC §9). /// Draws straight from simulation state every frame — no per-NPC nodes, /// no camera: fixed screen-space layout, map on the left, roster on the /// right. The player-facing game has no meters; the designer sees everything. /// public partial class Visualization : Node2D { private GameManager _gm = null!; // Layout: 1600×900 window — map fills the left square, panel the right. private const float MapOffset = 10f; private const float MapPixels = 1060f; // fills most of the 1600px window private const float CellPixels = MapPixels / World.GridSize; private const float PanelX = 1085f; private const float PanelWidth = 505f; private static readonly Color PanelBg = new(0.10f, 0.10f, 0.12f); private static readonly Color MapBg = new(0.13f, 0.13f, 0.15f); private static readonly Color TextDim = new(0.65f, 0.65f, 0.70f); private static readonly Color TextMain = new(0.92f, 0.92f, 0.95f); public override void _Ready() { _gm = GetParent(); } public override void _Process(double delta) => QueueRedraw(); private static Color StateColor(NpcState state) => state switch { NpcState.Gathering => Colors.White, NpcState.Building => Colors.Orange, NpcState.Trading => Colors.Yellow, NpcState.Resting => new Color(0.45f, 0.45f, 0.95f), NpcState.Socializing => new Color(0.9f, 0.4f, 0.9f), _ => Colors.Red, }; private static Vector2 MapPos(Vector2 gridPos) => new(MapOffset + gridPos.X * CellPixels, MapOffset + gridPos.Y * CellPixels); public override void _Draw() { if (_gm.World == null) return; var font = ThemeDB.FallbackFont; // --- Backgrounds -------------------------------------------------- DrawRect(new Rect2(MapOffset, MapOffset, MapPixels, MapPixels), MapBg); DrawRect(new Rect2(PanelX, 0, PanelWidth, 900), PanelBg); // --- Map: resource nodes ----------------------------------------- foreach (var node in _gm.World.Nodes) { Color c = node.Kind switch { MaterialKind.Wood => new Color(0.20f, 0.55f, 0.20f), MaterialKind.Stone => new Color(0.55f, 0.55f, 0.55f), MaterialKind.Clay => new Color(0.75f, 0.45f, 0.25f), MaterialKind.Water => new Color(0.25f, 0.45f, 0.85f), MaterialKind.Food => new Color(0.80f, 0.30f, 0.40f), MaterialKind.Ore => new Color(0.65f, 0.55f, 0.75f), MaterialKind.Herb => new Color(0.45f, 0.80f, 0.55f), _ => Colors.Magenta, }; // Fade toward depletion so dying groves read at a glance if (node.Kind != MaterialKind.Water) c = c.Darkened(0.65f * (1f - node.Amount / node.MaxAmount)); var p = MapPos(node.Cell); DrawRect(new Rect2(p.X, p.Y, CellPixels, CellPixels), c); } // --- Map: town labels -------------------------------------------- foreach (var town in _gm.Towns) { var tp = MapPos(town.Center); DrawString(font, tp + new Vector2(-20f, -14f), town.Name.ToUpper(), fontSize: 13, modulate: TextDim); } // --- 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]; float size = CellPixels + 3f + (b.CurrentStage + b.StageProgress) * 2f + b.Modules * 1.5f; var p = MapPos(b.Site); 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); } // --- Map: NPCs with roster numbers ------------------------------- for (int i = 0; i < _gm.Npcs.Count; i++) { var npc = _gm.Npcs[i]; var p = MapPos(npc.Position); // Selfish souls get a dark red ring — same dot, different halo. if (npc.Soul.Type == SoulType.Selfish) DrawCircle(p, 5.5f, new Color(0.75f, 0.15f, 0.15f)); // Debris dims the dot — encasement made visible. DrawCircle(p, 3.5f, StateColor(npc.State).Darkened(npc.Imprint.Total * 0.7f)); DrawString(font, p + new Vector2(5f, -4f), $"{i + 1}", fontSize: 11, modulate: TextMain); } // --- Panel: clock header ----------------------------------------- DrawString(font, new Vector2(PanelX + 16, 30), $"Day {_gm.Day} · {_gm.SeasonName} {_gm.MinuteOfDay / 60:00}:{_gm.MinuteOfDay % 60:00} " + $"{(_gm.IsNight ? "· night" : "· day")} {_gm.TicksPerRealSecond:0}x", fontSize: 18, modulate: TextMain); // --- Panel: per-town summary (scales to any population) ---------- float y = 74f; foreach (var town in _gm.Towns) { // Aggregate this town's people. int pop = 0; float nour = 0f, know = 0f, debris = 0f; int gathering = 0, building = 0, resting = 0, social = 0, other = 0; foreach (var npc in _gm.Npcs) { if (npc.HomeTown != town.Name) continue; pop++; nour += npc.Nourishment; know += npc.Know.Total; debris += npc.Imprint.Total; switch (npc.State) { case NpcState.Gathering: gathering++; break; case NpcState.Building: building++; break; case NpcState.Resting: resting++; break; case NpcState.Socializing: social++; break; default: other++; break; } } // Aggregate this town's buildings. int shelters = 0, ruins = 0; float granary = 0f; foreach (var b in _gm.Buildings) { if (b.Town != town.Name) continue; if (b.IsRuined) ruins++; else if (b.Kind == BuildingKind.Granary) granary += b.FoodStock; else if (b.IsComplete) shelters++; } var head = TownColor(town.Soul); DrawString(font, new Vector2(PanelX + 16, y), $"{town.Name.ToUpper()} · {town.Soul.ToString().ToLower()}", fontSize: 15, modulate: pop > 0 ? head : TextDim); DrawString(font, new Vector2(PanelX + 250, y), pop > 0 ? $"pop {pop}" : "— extinct —", fontSize: 15, modulate: pop > 0 ? TextMain : new Color(0.8f, 0.4f, 0.4f)); y += 22f; if (pop > 0) { float inv = 1f / pop; DrawString(font, new Vector2(PanelX + 28, y), $"fed {nour * inv * 100,3:0}% know {know * inv,4:0.00} debris {debris * inv * 100,2:0}%", fontSize: 13, modulate: TextDim); DrawString(font, new Vector2(PanelX + 320, y), $"⌂{shelters} ▦{granary:0}" + (ruins > 0 ? $" ✗{ruins}" : ""), fontSize: 13, modulate: TextDim); y += 19f; DrawString(font, new Vector2(PanelX + 28, y), $"gather {gathering} build {building} rest {resting} social {social}" + (other > 0 ? $" other {other}" : ""), fontSize: 12, modulate: TextDim); y += 19f; } y += 12f; } // --- Panel: world vitals (bottom) -------------------------------- y += 8f; float wood = 0f, stone = 0f, food = 0f; foreach (var n in _gm.World.Nodes) { switch (n.Kind) { case MaterialKind.Wood: wood += n.Amount; break; case MaterialKind.Stone: stone += n.Amount; break; case MaterialKind.Food: food += n.Amount; break; } } DrawString(font, new Vector2(PanelX + 16, y), $"WORLD pop {_gm.Npcs.Count} +{_gm.BirthsToday}/−{_gm.DeathsToday} today", fontSize: 14, modulate: TextMain); y += 20f; DrawString(font, new Vector2(PanelX + 16, y), $"land wood {wood:0} stone {stone:0} food {food:0}", fontSize: 13, modulate: TextDim); } private static Color TownColor(SoulType t) => t switch { SoulType.Generational => new Color(0.55f, 0.75f, 0.95f), // North — cool SoulType.Nature => new Color(0.55f, 0.85f, 0.55f), // South — green SoulType.Materialist => new Color(0.90f, 0.80f, 0.45f), // West — gold _ => new Color(0.90f, 0.45f, 0.45f), // East — red }; }