ed78d560ff
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>
163 lines
5.4 KiB
C#
163 lines
5.4 KiB
C#
using System.Collections.Generic;
|
|
using Godot;
|
|
|
|
namespace WorldSim;
|
|
|
|
/// <summary>
|
|
/// Main loop and time system (SPEC §9).
|
|
/// 1 tick = 1 in-game minute; 1440 ticks = 1 day.
|
|
/// Day/night drives activity bias: gather/build by day, rest/socialize by night.
|
|
/// </summary>
|
|
public partial class GameManager : Node2D
|
|
{
|
|
public static GameManager? Instance { get; private set; }
|
|
|
|
[Export] public int Seed = 12345;
|
|
[Export] public int NpcCount = 15;
|
|
|
|
[Export(PropertyHint.Range, "0,15,1")]
|
|
public int SelfishCount = 3; // the contrast test (SPEC §11 amendment)
|
|
|
|
[Export(PropertyHint.Range, "1,2000,1")]
|
|
public float TicksPerRealSecond = 20f; // sim speed; ~1500 ≈ one day/sec for soaks
|
|
|
|
public World World { get; private set; } = null!;
|
|
public List<Npc> Npcs { get; } = new();
|
|
public List<Building> Buildings { get; } = new();
|
|
|
|
public long TotalTicks { get; private set; }
|
|
public int Day => (int)(TotalTicks / 1440);
|
|
public int MinuteOfDay => (int)(TotalTicks % 1440);
|
|
public bool IsNight => MinuteOfDay < 6 * 60 || MinuteOfDay >= 22 * 60;
|
|
|
|
// --- Seasons: the pressure that makes community structure matter ------
|
|
|
|
[Export] public int DaysPerSeason = 30;
|
|
|
|
public int SeasonIndex => (Day / DaysPerSeason) % 4;
|
|
|
|
public string SeasonName => SeasonIndex switch
|
|
{
|
|
0 => "spring", 1 => "summer", 2 => "autumn", _ => "winter",
|
|
};
|
|
|
|
/// <summary>Food regen multiplier — winter gives almost nothing.</summary>
|
|
public float SeasonFoodMult => SeasonIndex switch
|
|
{
|
|
0 => 1.5f, 1 => 1.0f, 2 => 0.8f, _ => 0.05f,
|
|
};
|
|
|
|
/// <summary>Wood regen multiplier — growth slows in the cold.</summary>
|
|
public float SeasonWoodMult => SeasonIndex switch
|
|
{
|
|
0 => 1.2f, 1 => 1.0f, 2 => 1.0f, _ => 0.4f,
|
|
};
|
|
|
|
private double _tickAccumulator;
|
|
|
|
public override void _Ready()
|
|
{
|
|
Instance = this;
|
|
TotalTicks = 8 * 60; // start at 08:00 — day one begins mid-morning, not asleep
|
|
World = new World(Seed);
|
|
World.Generate();
|
|
SpawnNpcs();
|
|
StatsLogger.Init();
|
|
GD.Print($"[WorldSim] World generated (seed {Seed}), {Npcs.Count} NPCs dropped in.");
|
|
}
|
|
|
|
private void SpawnNpcs()
|
|
{
|
|
// Dropped near the map center with scatter — no scripted town layout.
|
|
// Where the community forms is the experiment (SPEC §11).
|
|
var rng = new System.Random(Seed);
|
|
for (int i = 0; i < NpcCount; i++)
|
|
{
|
|
bool selfish = i >= NpcCount - SelfishCount;
|
|
Npcs.Add(new Npc
|
|
{
|
|
Name = $"Villager_{(char)('A' + i)}",
|
|
Soul = selfish ? SoulProfile.SelfishSoul() : SoulProfile.NatureSoul(),
|
|
PersonalityModifier = (float)(rng.NextDouble() * 0.4 - 0.2),
|
|
Position = new Vector2(
|
|
World.GridSize / 2f + rng.Next(-20, 21),
|
|
World.GridSize / 2f + rng.Next(-20, 21)),
|
|
});
|
|
}
|
|
}
|
|
|
|
public override void _Process(double delta)
|
|
{
|
|
_tickAccumulator += delta * TicksPerRealSecond;
|
|
while (_tickAccumulator >= 1.0)
|
|
{
|
|
_tickAccumulator -= 1.0;
|
|
Tick();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Soul-to-soul atmospheric pressure (ported from the Unity POC's
|
|
/// SoulFieldSystem): souls in proximity press on each other's
|
|
/// atmospheres. An encased soul darkens the room; a clear soul makes
|
|
/// it slightly easier for everyone near them to radiate. Both
|
|
/// directions apply; the net effect depends on relative debris.
|
|
/// </summary>
|
|
private void ApplySoulPressure()
|
|
{
|
|
const float radius = 8f;
|
|
const float strength = 0.00004f;
|
|
|
|
for (int i = 0; i < Npcs.Count; i++)
|
|
{
|
|
for (int j = i + 1; j < Npcs.Count; j++)
|
|
{
|
|
var a = Npcs[i];
|
|
var b = Npcs[j];
|
|
float dist = a.Position.DistanceTo(b.Position);
|
|
if (dist > radius) continue;
|
|
|
|
float falloff = 1f - dist / radius;
|
|
|
|
float fromA = Mathf.Lerp(-strength, strength, a.Imprint.Total) * falloff;
|
|
b.Imprint.ExternalPerception = Mathf.Clamp(b.Imprint.ExternalPerception + fromA, 0f, 1f);
|
|
|
|
float fromB = Mathf.Lerp(-strength, strength, b.Imprint.Total) * falloff;
|
|
a.Imprint.ExternalPerception = Mathf.Clamp(a.Imprint.ExternalPerception + fromB, 0f, 1f);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Where the community's feet actually are — new communal
|
|
/// buildings are founded here, not at a scripted town center.</summary>
|
|
public Vector2 CommunityCentroid()
|
|
{
|
|
if (Npcs.Count == 0) return new Vector2(World.GridSize / 2f, World.GridSize / 2f);
|
|
Vector2 sum = Vector2.Zero;
|
|
foreach (var npc in Npcs) sum += npc.Position;
|
|
return sum / Npcs.Count;
|
|
}
|
|
|
|
private void Tick()
|
|
{
|
|
TotalTicks++;
|
|
|
|
foreach (var npc in Npcs)
|
|
npc.Tick(World, this);
|
|
|
|
ApplySoulPressure();
|
|
|
|
if (TotalTicks % 1440 == 0)
|
|
{
|
|
World.RegenerateDaily(SeasonFoodMult, SeasonWoodMult);
|
|
foreach (var building in Buildings)
|
|
building.DailyUpkeep();
|
|
foreach (var npc in Npcs)
|
|
npc.FadeEchoes(0.005f);
|
|
StatsLogger.LogDay(this);
|
|
if (Day % 10 == 0)
|
|
GD.Print($"[WorldSim] Day {Day} complete.");
|
|
}
|
|
}
|
|
}
|