9e8ee1d748
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>
245 lines
11 KiB
C#
245 lines
11 KiB
C#
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
|
||
};
|
||
}
|