Files
Soul_Game/scripts/Visualization.cs
T
mmcghen 9e8ee1d748 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>
2026-07-21 21:38:57 -04:00

245 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Godot;
namespace WorldSim;
/// <summary>
/// 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.
/// </summary>
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 = 880f;
private const float CellPixels = MapPixels / World.GridSize; // ~3.44
private const float PanelX = 910f;
private const float PanelWidth = 680f;
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<GameManager>();
}
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
};
}