Organic construction: buildings from need, not quotas
Removes every predetermined construction trigger (shelters-per-capita cap, granary-at-five-shelters milestone). Buildings now emerge from lived need: - Shelters have real bed capacity (3 + modules). A full house turns you away; three nights sleeping outside and a person gathers stone and raises a roof where they actually live. When nobody sleeps outside, nobody builds. Ruined shelters spill their sleepers, so settlements can migrate over time. - The first granary is founded by memory: someone who has truly starved stockpiles food once the land gives again and builds storage where the harvest is. A second granary appears only when the first fills. - Emergent consequence: freeriders compete for beds but never build, so scarce housing squeezes them into the open. The village never decides to punish them; the housing supply does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -92,6 +92,11 @@ public class Building
|
|||||||
Kind == BuildingKind.Shelter && IsComplete && !IsRuined &&
|
Kind == BuildingKind.Shelter && IsComplete && !IsRuined &&
|
||||||
Modules < ModuleCap && !WantsUpkeepWood;
|
Modules < ModuleCap && !WantsUpkeepWood;
|
||||||
|
|
||||||
|
/// <summary>How many can sleep here. Expansion adds beds — growth has a
|
||||||
|
/// human meaning, not just a bigger square.</summary>
|
||||||
|
public int RestCapacity =>
|
||||||
|
Kind == BuildingKind.Shelter && IsComplete && !IsRuined ? 3 + Modules : 0;
|
||||||
|
|
||||||
// --- Granary: food store ---------------------------------------------
|
// --- Granary: food store ---------------------------------------------
|
||||||
|
|
||||||
public const float FoodStockCap = 150f;
|
public const float FoodStockCap = 150f;
|
||||||
|
|||||||
@@ -117,6 +117,16 @@ public partial class GameManager : Node2D
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>How many villagers are currently sleeping at this shelter.</summary>
|
||||||
|
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)
|
public int TownPopulation(string town)
|
||||||
{
|
{
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
|||||||
+63
-21
@@ -97,6 +97,16 @@ public class Npc
|
|||||||
private Npc? _giftSource; // neighbor being asked for food
|
private Npc? _giftSource; // neighbor being asked for food
|
||||||
private float _stateTimer;
|
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 =
|
private static readonly MaterialKind[] GatherKinds =
|
||||||
{ MaterialKind.Wood, MaterialKind.Stone, MaterialKind.Clay };
|
{ MaterialKind.Wood, MaterialKind.Stone, MaterialKind.Clay };
|
||||||
|
|
||||||
@@ -140,6 +150,7 @@ public class Npc
|
|||||||
Nourishment = Mathf.Min(1f, Nourishment + 0.25f * (1f - debris * 0.5f));
|
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.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);
|
Nourishment = Mathf.Clamp(Nourishment, 0f, 1f);
|
||||||
|
|
||||||
// 4. Fatigue deltas (SPEC §10) — scaled per tick
|
// 4. Fatigue deltas (SPEC §10) — scaled per tick
|
||||||
@@ -163,6 +174,7 @@ public class Npc
|
|||||||
_targetNpc = null;
|
_targetNpc = null;
|
||||||
_foodSource = null;
|
_foodSource = null;
|
||||||
_giftSource = null;
|
_giftSource = null;
|
||||||
|
_shelteredDuringRest = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public float TotalCarried()
|
public float TotalCarried()
|
||||||
@@ -198,6 +210,12 @@ public class Npc
|
|||||||
// town's hearths. (Helping a neighbor town is a later, deliberate
|
// town's hearths. (Helping a neighbor town is a later, deliberate
|
||||||
// system, not an accident of the work queue.)
|
// 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.
|
// 2. Construction sites.
|
||||||
foreach (var b in gm.Buildings)
|
foreach (var b in gm.Buildings)
|
||||||
if (b.Town == HomeTown && !b.IsComplete && b.NeededMaterial is MaterialKind needed)
|
if (b.Town == HomeTown && !b.IsComplete && b.NeededMaterial is MaterialKind needed)
|
||||||
@@ -213,6 +231,11 @@ public class Npc
|
|||||||
if (b.Town == HomeTown && b.WantsFood)
|
if (b.Town == HomeTown && b.WantsFood)
|
||||||
return MaterialKind.Food;
|
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.
|
// 5. Ambition: expansion modules for sound, stocked shelters.
|
||||||
foreach (var b in gm.Buildings)
|
foreach (var b in gm.Buildings)
|
||||||
if (b.Town == HomeTown && b.WantsExpansion)
|
if (b.Town == HomeTown && b.WantsExpansion)
|
||||||
@@ -229,6 +252,16 @@ public class Npc
|
|||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>True if this town has any granary (built or building) with room.</summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When full but mostly carrying the wrong thing, set the excess down to
|
/// When full but mostly carrying the wrong thing, set the excess down to
|
||||||
/// make room for what's actually needed. Lossy for now —
|
/// make room for what's actually needed. Lossy for now —
|
||||||
@@ -431,30 +464,26 @@ public class Npc
|
|||||||
|
|
||||||
if (site == null)
|
if (site == null)
|
||||||
{
|
{
|
||||||
// Communal-first founding, near where this settlement's people
|
// Organic founding — need, not quota. No shelter caps, no
|
||||||
// actually gather. Shelters first; once the village stands, the
|
// granary milestones. Reaching here means nothing in town
|
||||||
// granary.
|
// currently needs delivering, so ask: what does *this person*
|
||||||
bool carriesStone = Inventory.TryGetValue(MaterialKind.Stone, out float stone) && stone > 0f;
|
// need that doesn't exist yet?
|
||||||
int shelters = 0;
|
var jitter = new Vector2(PersonalityModifier * 4f, -PersonalityModifier * 4f);
|
||||||
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;
|
// Kept sleeping outside → raise a roof where you actually live.
|
||||||
var jitter = new Vector2(PersonalityModifier * 25f, -PersonalityModifier * 25f);
|
if (_unshelteredRests >= 3)
|
||||||
if (shelters < shelterCap && carriesStone)
|
|
||||||
{
|
{
|
||||||
site = Building.NewShelter(gm.TownCentroid(HomeTown) + jitter);
|
site = Building.NewShelter(Position + jitter);
|
||||||
site.Town = HomeTown;
|
site.Town = HomeTown;
|
||||||
gm.Buildings.Add(site);
|
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;
|
site.Town = HomeTown;
|
||||||
gm.Buildings.Add(site);
|
gm.Buildings.Add(site);
|
||||||
}
|
}
|
||||||
@@ -481,12 +510,15 @@ public class Npc
|
|||||||
|
|
||||||
private void TickResting(GameManager gm)
|
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;
|
Building? shelter = null;
|
||||||
float bestDist = float.MaxValue;
|
float bestDist = float.MaxValue;
|
||||||
foreach (var b in gm.Buildings)
|
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);
|
float d = Position.DistanceSquaredTo(b.Site);
|
||||||
if (d < bestDist) { bestDist = d; shelter = b; }
|
if (d < bestDist) { bestDist = d; shelter = b; }
|
||||||
}
|
}
|
||||||
@@ -501,10 +533,20 @@ public class Npc
|
|||||||
: shelter != null ? "walking home to rest"
|
: shelter != null ? "walking home to rest"
|
||||||
: "resting in the open";
|
: "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));
|
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 (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)
|
private void TickSocializing(GameManager gm)
|
||||||
|
|||||||
Reference in New Issue
Block a user