Files
Soul_Game/scripts/Visualization.cs
T
mmcghen ed78d560ff Soul layer: imprints, warmth echoes, and consequences
Ports the Unity POC soul systems into the sim:
- Warmth echoes: villagers remember who fed them (+0.15) and who turned
  them away (-0.25). Ask-for-food targeting runs on memory, not
  omniscience; the remembered-cold are not asked. Echoes fade daily.
- Refusing the hungry while holding food marks the refuser (others-regard
  debris) and is logged; empty-handed is not refusal.
- Harvesting below the 50% commons line marks the taker per unit taken.
- Soul-to-soul atmospheric pressure (SoulFieldSystem port): encased souls
  darken nearby atmospheres, clear souls lighten them.
- Debris costs biologically: nourishment drains faster and meals restore
  less as the soul encases.
- Instrumentation: avg_debris + refusals in daily stats; per-villager
  nourish/health/debris; refusals in the contact log; map dots dim with
  encasement.

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

176 lines
7.8 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),
_ => 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: 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: villager roster --------------------------------------
float y = 66f;
DrawString(font, new Vector2(PanelX + 16, y), "VILLAGERS",
fontSize: 13, modulate: TextDim);
y += 10f;
for (int i = 0; i < _gm.Npcs.Count; i++)
{
var npc = _gm.Npcs[i];
y += 22f;
if (npc.Soul.Type == SoulType.Selfish)
DrawCircle(new Vector2(PanelX + 24, y - 5f), 6f, new Color(0.75f, 0.15f, 0.15f));
DrawCircle(new Vector2(PanelX + 24, y - 5f), 4.5f,
StateColor(npc.State).Darkened(npc.Imprint.Total * 0.7f));
DrawString(font, new Vector2(PanelX + 36, y),
$"{i + 1,2} {npc.State,-11} {npc.Activity}",
fontSize: 13, modulate: TextMain);
DrawString(font, new Vector2(PanelX + 400, y),
$"F {npc.Fatigue,3:0} N {npc.Nourishment * 100,3:0} D {npc.Imprint.Total * 100,2:0}" +
$" C {npc.TotalCarried(),4:0.0} {npc.DominantStateToday().ToString().ToLower()[..4]}",
fontSize: 12, modulate: TextDim);
}
// --- Panel: buildings --------------------------------------------
y += 34f;
DrawString(font, new Vector2(PanelX + 16, y), "BUILDINGS",
fontSize: 13, modulate: TextDim);
y += 10f;
for (int i = 0; i < _gm.Buildings.Count; i++)
{
var b = _gm.Buildings[i];
y += 20f;
string status = b.IsRuined ? "RUIN"
: !b.IsComplete
? $"building: {b.Stages[b.CurrentStage].Name} {b.StageProgress * 100,3:0}%"
: b.Kind == BuildingKind.Granary
? $"granary food {b.FoodStock,5:0.0}/{Building.FoodStockCap:0}"
: $"cond {b.Condition * 100,3:0}% wood {b.UpkeepStock,4:0.0}/{Building.UpkeepStockCap:0}" +
$" mods {b.Modules}/{Building.ModuleCap} burn {b.DailyBurn:0.0}/day";
DrawString(font, new Vector2(PanelX + 36, y),
$"{i + 1} {status}", fontSize: 13,
modulate: b.IsRuined ? TextDim : TextMain);
}
// --- Panel: world totals -----------------------------------------
y += 34f;
float wood = 0f, stone = 0f, clay = 0f, foodTotal = 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.Clay: clay += n.Amount; break;
case MaterialKind.Food: foodTotal += n.Amount; break;
}
}
DrawString(font, new Vector2(PanelX + 16, y),
$"WORLD wood {wood:0} stone {stone:0} clay {clay:0} food {foodTotal:0}",
fontSize: 13, modulate: TextDim);
}
}