Expansion phase: settlements — the North joins the world

Runs 5-6 logged in FINDINGS.md (soul layer validated: nature debris
0.000 vs selfish 0.225 after two years; the ledger works).

Towns are now real spatial structure: TownDef seeds (South at the
bottom of the map, North at the top), NPCs affiliate with a home town,
communal duty serves your own settlement (buildings are town-tagged,
founding uses the town centroid and per-town shelter caps), and each
town raises its own shelters and granary. Generational (North) soul
profile added — temperament only until the knowledge system lands.
Map shows town labels; rosters and CSVs carry town columns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 14:02:59 -04:00
parent 8aab1a763b
commit 3f2c5e2f18
11 changed files with 4124 additions and 6541 deletions
+72 -14
View File
@@ -13,10 +13,25 @@ public partial class GameManager : Node2D
public static GameManager? Instance { get; private set; }
[Export] public int Seed = 12345;
[Export] public int NpcCount = 15;
// --- Settlements ------------------------------------------------------
// Expansion phase: multiple towns, each a soul culture with its own
// centroid, buildings, and granary. NPCs affiliate with a home town
// but roam freely — the space between towns is where cultures meet.
[Export] public int SouthPopulation = 12; // nature souls
[Export(PropertyHint.Range, "0,15,1")]
public int SelfishCount = 3; // the contrast test (SPEC §11 amendment)
public int SelfishCount = 3; // east souls living in the South
[Export] public int NorthPopulation = 10; // generational souls
public record TownDef(string Name, Vector2 Center, SoulType Soul);
public IReadOnlyList<TownDef> Towns => _towns;
private readonly List<TownDef> _towns = new()
{
new("South", new Vector2(128f, 190f), SoulType.Nature),
new("North", new Vector2(128f, 62f), SoulType.Generational),
};
[Export(PropertyHint.Range, "1,2000,1")]
public float TicksPerRealSecond = 20f; // sim speed; ~1500 ≈ one day/sec for soaks
@@ -68,22 +83,65 @@ public partial class GameManager : Node2D
private void SpawnNpcs()
{
// Dropped near the map center with scatter — no scripted town layout.
// Where the community forms is the experiment (SPEC §11).
// Dropped near their town's seed point with scatter — no scripted
// layout. Where each community actually forms is the experiment.
var rng = new System.Random(Seed);
for (int i = 0; i < NpcCount; i++)
void Spawn(string town, Vector2 center, SoulProfile soul, int count, string prefix)
{
bool selfish = i >= NpcCount - SelfishCount;
Npcs.Add(new Npc
for (int i = 0; i < count; i++)
{
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)),
});
Npcs.Add(new Npc
{
Name = $"{prefix}_{(char)('A' + Npcs.Count % 26)}{(Npcs.Count >= 26 ? Npcs.Count.ToString() : "")}",
HomeTown = town,
Soul = soul,
PersonalityModifier = (float)(rng.NextDouble() * 0.4 - 0.2),
Position = center + new Vector2(rng.Next(-12, 13), rng.Next(-12, 13)),
});
}
}
foreach (var town in _towns)
{
if (town.Name == "South")
{
Spawn(town.Name, town.Center, SoulProfile.NatureSoul(), SouthPopulation, "South");
for (int i = 0; i < SelfishCount; i++)
Spawn(town.Name, town.Center, SoulProfile.SelfishSoul(), 1, "South");
}
else if (town.Name == "North")
{
Spawn(town.Name, town.Center, SoulProfile.GenerationalSoul(), NorthPopulation, "North");
}
}
}
public int TownPopulation(string town)
{
int count = 0;
foreach (var npc in Npcs)
if (npc.HomeTown == town) count++;
return count;
}
public Vector2 TownCentroid(string town)
{
Vector2 sum = Vector2.Zero;
int count = 0;
foreach (var npc in Npcs)
{
if (npc.HomeTown != town) continue;
sum += npc.Position;
count++;
}
if (count == 0)
{
foreach (var t in _towns)
if (t.Name == town) return t.Center;
return new Vector2(World.GridSize / 2f, World.GridSize / 2f);
}
return sum / count;
}
public override void _Process(double delta)