Files
Soul_Game/scripts/Visualization.cs
T
mmcghen 10e772d087 Run 15: known-nodes exploration (addendum stage 1), per-town debug, O(n^2) fix
Exploration addendum stage 1: per-NPC known-nodes. NPCs gather only from
resources they've personally sensed (radius 22) or been told about;
founders seed with home ground, children inherit parents' maps, node
knowledge diffuses between neighbors like skills. Starving NPCs venture
outward and sense as they go - non-random needs-driven exploration, the
seed of first contact. Repopulation preserved (seeding + survival food
fallback): all towns fed, population stable 86-109 over 240 days.

Debug screen rebuilt: the ~68-row per-villager roster (overflowing at
four-town scale) replaced with a per-town summary - pop, fed%, knowledge,
debris, state breakdown, buildings, culture-colored, plus world
births/deaths. Scales to any population.

Performance: added phase timers and measured instead of guessing - npc.Tick
was 94% of cost, O(n^2) via TickResting calling RestingOccupancy (full
population scan) per building per resting NPC. Fixed with a once-per-tick
occupancy cache on Building. npc phase ~4x faster and now linear; full
240-day four-town run in 180s, faster than the pre-exploration baseline.
Profiling scaffolding removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 21:17:24 -04:00

211 lines
9.2 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 ----------------------------------------------
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);
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
};
}