diff --git a/scripts/Building.cs b/scripts/Building.cs
index a0fa056..b0c8e17 100644
--- a/scripts/Building.cs
+++ b/scripts/Building.cs
@@ -92,6 +92,11 @@ public class Building
Kind == BuildingKind.Shelter && IsComplete && !IsRuined &&
Modules < ModuleCap && !WantsUpkeepWood;
+ /// How many can sleep here. Expansion adds beds — growth has a
+ /// human meaning, not just a bigger square.
+ public int RestCapacity =>
+ Kind == BuildingKind.Shelter && IsComplete && !IsRuined ? 3 + Modules : 0;
+
// --- Granary: food store ---------------------------------------------
public const float FoodStockCap = 150f;
diff --git a/scripts/GameManager.cs b/scripts/GameManager.cs
index 0866be9..87983d5 100644
--- a/scripts/GameManager.cs
+++ b/scripts/GameManager.cs
@@ -117,6 +117,16 @@ public partial class GameManager : Node2D
}
}
+ /// How many villagers are currently sleeping at this shelter.
+ public int RestingOccupancy(Building b)
+ {
+ int count = 0;
+ foreach (var npc in Npcs)
+ if (npc.State == NpcState.Resting && npc.Position.DistanceTo(b.Site) <= 2.5f)
+ count++;
+ return count;
+ }
+
public int TownPopulation(string town)
{
int count = 0;
diff --git a/scripts/NPC.cs b/scripts/NPC.cs
index c0d1a64..d15e75e 100644
--- a/scripts/NPC.cs
+++ b/scripts/NPC.cs
@@ -97,6 +97,16 @@ public class Npc
private Npc? _giftSource; // neighbor being asked for food
private float _stateTimer;
+ // Organic construction: nights spent sleeping outside. Three rough
+ // nights and a person starts gathering stone for a roof of their own.
+ private int _unshelteredRests;
+ private bool _shelteredDuringRest;
+
+ // Hunger remembered: someone who has truly starved stockpiles food once
+ // the land gives again. The town's first granary is founded by memory,
+ // not by milestone.
+ private bool _hungerMemory;
+
private static readonly MaterialKind[] GatherKinds =
{ MaterialKind.Wood, MaterialKind.Stone, MaterialKind.Clay };
@@ -140,6 +150,7 @@ public class Npc
Nourishment = Mathf.Min(1f, Nourishment + 0.25f * (1f - debris * 0.5f));
}
if (Nourishment <= 0.05f) Health = Mathf.Max(0f, Health - 0.01f);
+ if (Nourishment < 0.3f) _hungerMemory = true; // real hunger is not forgotten
Nourishment = Mathf.Clamp(Nourishment, 0f, 1f);
// 4. Fatigue deltas (SPEC §10) — scaled per tick
@@ -163,6 +174,7 @@ public class Npc
_targetNpc = null;
_foodSource = null;
_giftSource = null;
+ _shelteredDuringRest = false;
}
public float TotalCarried()
@@ -198,6 +210,12 @@ public class Npc
// town's hearths. (Helping a neighbor town is a later, deliberate
// system, not an accident of the work queue.)
+ // 1.5. A roof of your own: kept sleeping outside? Gather stone.
+ // (The founding itself happens in TickBuilding once you're
+ // carrying enough — need drives supply drives construction.)
+ if (_unshelteredRests >= 3)
+ return MaterialKind.Stone;
+
// 2. Construction sites.
foreach (var b in gm.Buildings)
if (b.Town == HomeTown && !b.IsComplete && b.NeededMaterial is MaterialKind needed)
@@ -213,6 +231,11 @@ public class Npc
if (b.Town == HomeTown && b.WantsFood)
return MaterialKind.Food;
+ // 4.5. Hunger remembered, and nowhere to store against it:
+ // gather the surplus that will found the granary.
+ if (_hungerMemory && !TownHasFoodStorage(gm))
+ return MaterialKind.Food;
+
// 5. Ambition: expansion modules for sound, stocked shelters.
foreach (var b in gm.Buildings)
if (b.Town == HomeTown && b.WantsExpansion)
@@ -229,6 +252,16 @@ public class Npc
return best;
}
+ /// True if this town has any granary (built or building) with room.
+ private bool TownHasFoodStorage(GameManager gm)
+ {
+ foreach (var b in gm.Buildings)
+ if (b.Town == HomeTown && b.Kind == BuildingKind.Granary &&
+ (!b.IsComplete || b.FoodStock < Building.FoodStockCap))
+ return true;
+ return false;
+ }
+
///
/// When full but mostly carrying the wrong thing, set the excess down to
/// make room for what's actually needed. Lossy for now —
@@ -431,30 +464,26 @@ public class Npc
if (site == null)
{
- // 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++;
- }
+ // Organic founding — need, not quota. No shelter caps, no
+ // granary milestones. Reaching here means nothing in town
+ // currently needs delivering, so ask: what does *this person*
+ // need that doesn't exist yet?
+ var jitter = new Vector2(PersonalityModifier * 4f, -PersonalityModifier * 4f);
- int shelterCap = gm.TownPopulation(HomeTown) / 3;
- var jitter = new Vector2(PersonalityModifier * 25f, -PersonalityModifier * 25f);
- if (shelters < shelterCap && carriesStone)
+ // Kept sleeping outside → raise a roof where you actually live.
+ if (_unshelteredRests >= 3)
{
- site = Building.NewShelter(gm.TownCentroid(HomeTown) + jitter);
+ site = Building.NewShelter(Position + jitter);
site.Town = HomeTown;
gm.Buildings.Add(site);
+ _unshelteredRests = 0;
}
- else if (!granaryExists && shelters >= shelterCap && carriesStone)
+ // Carrying a surplus with nowhere to put it → the barn goes
+ // where the harvest is. A second granary appears when the
+ // first fills; storage scales with abundance.
+ else if (Inventory.TryGetValue(MaterialKind.Food, out float surplus) && surplus > 8f)
{
- site = Building.NewGranary(gm.TownCentroid(HomeTown) + jitter);
+ site = Building.NewGranary(Position + jitter);
site.Town = HomeTown;
gm.Buildings.Add(site);
}
@@ -481,12 +510,15 @@ public class Npc
private void TickResting(GameManager gm)
{
- // Rest at home: walk to the nearest sound shelter and recover faster there.
+ // Rest at the nearest shelter WITH A FREE BED. Beds are real:
+ // capacity is 3 + modules, and a full house turns you away.
Building? shelter = null;
float bestDist = float.MaxValue;
foreach (var b in gm.Buildings)
{
- if (!b.IsComplete || b.IsRuined) continue;
+ if (b.RestCapacity <= 0) continue;
+ bool occupyingHere = Position.DistanceTo(b.Site) <= 2.5f;
+ if (!occupyingHere && gm.RestingOccupancy(b) >= b.RestCapacity) continue;
float d = Position.DistanceSquaredTo(b.Site);
if (d < bestDist) { bestDist = d; shelter = b; }
}
@@ -501,10 +533,20 @@ public class Npc
: shelter != null ? "walking home to rest"
: "resting in the open";
+ if (sheltered)
+ {
+ _shelteredDuringRest = true;
+ _unshelteredRests = 0; // a bed found is a grievance forgotten
+ }
+
Health = Mathf.Min(100f, Health + (sheltered ? 0.4f : 0.1f));
if (sheltered) Fatigue = Mathf.Max(0f, Fatigue - 0.3f); // on top of the state delta
- if (Fatigue < 40f && !gm.IsNight) Enter(NpcState.Gathering);
+ if (Fatigue < 40f && !gm.IsNight)
+ {
+ if (!_shelteredDuringRest) _unshelteredRests++;
+ Enter(NpcState.Gathering);
+ }
}
private void TickSocializing(GameManager gm)