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
+26 -15
View File
@@ -22,6 +22,7 @@ public class Npc
{
// --- Identity & soul -------------------------------------------------
public string Name = "";
public string HomeTown = ""; // settlement affiliation — home, not a cage
public SoulProfile Soul = SoulProfile.NatureSoul();
public SoulImprint Imprint = new(); // triad — see SPEC §2 amendment
public float PersonalityModifier; // per-NPC variance within a soul type
@@ -193,24 +194,28 @@ public class Npc
if (IsCommunal)
{
// Communal duty is to your own settlement — you heat your own
// town's hearths. (Helping a neighbor town is a later, deliberate
// system, not an accident of the work queue.)
// 2. Construction sites.
foreach (var b in gm.Buildings)
if (!b.IsComplete && b.NeededMaterial is MaterialKind needed)
if (b.Town == HomeTown && !b.IsComplete && b.NeededMaterial is MaterialKind needed)
return needed;
// 3. Firewood for shelters running low.
foreach (var b in gm.Buildings)
if (b.WantsUpkeepWood)
if (b.Town == HomeTown && b.WantsUpkeepWood)
return MaterialKind.Wood;
// 4. Winter insurance: stock the granary while the land still gives.
foreach (var b in gm.Buildings)
if (b.WantsFood)
if (b.Town == HomeTown && b.WantsFood)
return MaterialKind.Food;
// 5. Ambition: expansion modules for sound, stocked shelters.
foreach (var b in gm.Buildings)
if (b.WantsExpansion)
if (b.Town == HomeTown && b.WantsExpansion)
return MaterialKind.Wood;
}
@@ -381,12 +386,13 @@ public class Npc
// shelters and eat from the granary — freeriding is the whole point.
if (!IsCommunal) { Enter(NpcState.Socializing); return; }
// Priority 1: construction sites. Priority 2: shelters low on firewood.
// Serve your own settlement's buildings, nearest first:
// construction → firewood → granary deposits → expansion.
Building? site = null;
float bestDist = float.MaxValue;
foreach (var b in gm.Buildings)
{
if (b.IsComplete) continue;
if (b.Town != HomeTown || b.IsComplete) continue;
float d = Position.DistanceSquaredTo(b.Site);
if (d < bestDist) { bestDist = d; site = b; }
}
@@ -394,7 +400,7 @@ public class Npc
{
foreach (var b in gm.Buildings)
{
if (!b.WantsUpkeepWood) continue;
if (b.Town != HomeTown || !b.WantsUpkeepWood) continue;
float d = Position.DistanceSquaredTo(b.Site);
if (d < bestDist) { bestDist = d; site = b; }
}
@@ -407,7 +413,7 @@ public class Npc
{
foreach (var b in gm.Buildings)
{
if (!b.WantsFood) continue;
if (b.Town != HomeTown || !b.WantsFood) continue;
float d = Position.DistanceSquaredTo(b.Site);
if (d < bestDist) { bestDist = d; site = b; }
}
@@ -417,7 +423,7 @@ public class Npc
{
foreach (var b in gm.Buildings)
{
if (!b.WantsExpansion) continue;
if (b.Town != HomeTown || !b.WantsExpansion) continue;
float d = Position.DistanceSquaredTo(b.Site);
if (d < bestDist) { bestDist = d; site = b; }
}
@@ -425,26 +431,31 @@ public class Npc
if (site == null)
{
// Communal-first founding, near where the community actually
// gathers. Shelters first; once the village stands, the granary.
// Communal-first founding, near where this settlement's people
// actually gather. Shelters first; once the village stands, the
// granary.
bool carriesStone = Inventory.TryGetValue(MaterialKind.Stone, out float stone) && stone > 0f;
int shelters = 0;
bool granaryExists = false;
foreach (var b in gm.Buildings)
{
if (b.Town != HomeTown) continue;
if (b.Kind == BuildingKind.Granary) granaryExists = true;
else shelters++;
}
int shelterCap = gm.TownPopulation(HomeTown) / 3;
var jitter = new Vector2(PersonalityModifier * 25f, -PersonalityModifier * 25f);
if (shelters < gm.NpcCount / 3 && carriesStone)
if (shelters < shelterCap && carriesStone)
{
site = Building.NewShelter(gm.CommunityCentroid() + jitter);
site = Building.NewShelter(gm.TownCentroid(HomeTown) + jitter);
site.Town = HomeTown;
gm.Buildings.Add(site);
}
else if (!granaryExists && shelters >= gm.NpcCount / 3 && carriesStone)
else if (!granaryExists && shelters >= shelterCap && carriesStone)
{
site = Building.NewGranary(gm.CommunityCentroid() + jitter);
site = Building.NewGranary(gm.TownCentroid(HomeTown) + jitter);
site.Town = HomeTown;
gm.Buildings.Add(site);
}
else { Enter(NpcState.Socializing); return; }