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:
+63
-21
@@ -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;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 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)
|
||||
|
||||
Reference in New Issue
Block a user