using System.Collections.Generic; using Godot; namespace WorldSim; /// /// 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. /// 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 Npcs { get; } = new(); public List 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", }; /// Food regen multiplier — winter gives almost nothing. public float SeasonFoodMult => SeasonIndex switch { 0 => 1.5f, 1 => 1.0f, 2 => 0.8f, _ => 0.05f, }; /// Wood regen multiplier — growth slows in the cold. 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(); } } /// Where the community's feet actually are — new communal /// buildings are founded here, not at a scripted town center. 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); if (TotalTicks % 1440 == 0) { World.RegenerateDaily(SeasonFoodMult, SeasonWoodMult); foreach (var building in Buildings) building.DailyUpkeep(); StatsLogger.LogDay(this); if (Day % 10 == 0) GD.Print($"[WorldSim] Day {Day} complete."); } } }