Organic construction v2: three verified fixes, one open problem

Fixes (headless-verified): homes founded at the habitual sleeping spot
within reach of the community (not at distant stone mines); night sends
everyone to bed so beds are used and rough nights actually occur; a bed
must exist at arrival - floor-sleepers at full shelters now count as
unsheltered. Diagnostics added: incomplete_sites stat column, founding
logs.

Open (documented in FINDINGS.md, paused by request): the loop builds
forward but cannot rebuild after loss - winter crises ruin shelters and
recovery stalls (4 foundings in 240 days, no granary after two famines).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 14:58:43 -04:00
parent 41cd417601
commit 676a9f3f31
7 changed files with 6955 additions and 6387 deletions
+42 -9
View File
@@ -101,12 +101,18 @@ public class Npc
// nights and a person starts gathering stone for a roof of their own.
private int _unshelteredRests;
private bool _shelteredDuringRest;
private bool _nightDuringRest;
// 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;
// Where this person actually sleeps — homes are founded where you live,
// not where you happened to be standing with a pack full of stone.
private Vector2 _homeSpot;
private bool _hasHomeSpot;
private static readonly MaterialKind[] GatherKinds =
{ MaterialKind.Wood, MaterialKind.Stone, MaterialKind.Clay };
@@ -123,9 +129,12 @@ public class Npc
if (State != NpcState.Resting && (Fatigue > 80f || Health < 30f))
Enter(NpcState.Resting);
// Day/night bias (SPEC §10): work by day, rest/socialize by night
if (gm.IsNight && State is NpcState.Gathering or NpcState.Trading or NpcState.Building)
Enter(Fatigue > 50f ? NpcState.Resting : NpcState.Socializing);
// Night sends everyone to bed — home if they have one, the open sky
// if they don't. (Evenings 18:0022:00 remain the social hours; the
// old rule let low-fatigue villagers socialize all night, which
// meant beds went unused and rough nights were never experienced.)
if (gm.IsNight && State != NpcState.Resting)
Enter(NpcState.Resting);
// 23. State behavior
switch (State)
@@ -175,6 +184,7 @@ public class Npc
_foodSource = null;
_giftSource = null;
_shelteredDuringRest = false;
_nightDuringRest = false;
}
public float TotalCarried()
@@ -480,13 +490,23 @@ public class Npc
// need that doesn't exist yet?
var jitter = new Vector2(PersonalityModifier * 4f, -PersonalityModifier * 4f);
// Kept sleeping outside → raise a roof where you actually live.
// Kept sleeping outside → raise a roof where you actually live:
// your sleeping spot, not wherever the stone happened to be.
if (_unshelteredRests >= 3)
{
site = Building.NewShelter(Position + jitter);
// Build where you live — but within reach of your people.
// A communal soul doesn't homestead a hundred cells from
// everyone they know.
Vector2 centroid = gm.TownCentroid(HomeTown);
Vector2 home = _hasHomeSpot ? _homeSpot : centroid;
Vector2 offset = home - centroid;
if (offset.Length() > 20f) home = centroid + offset.Normalized() * 20f;
site = Building.NewShelter(home + jitter);
site.Town = HomeTown;
gm.Buildings.Add(site);
_unshelteredRests = 0;
GD.Print($"[Found] day {gm.Day}: {Name} founds shelter at {site.Site} " +
$"(centroid dist {(site.Site - gm.TownCentroid(HomeTown)).Length():0})");
}
// Hunger remembered + a surplus with nowhere to put it → the
// barn goes where the harvest is. Founded by scarcity survived,
@@ -528,17 +548,18 @@ public class Npc
foreach (var b in gm.Buildings)
{
if (b.RestCapacity <= 0) continue;
bool occupyingHere = Position.DistanceTo(b.Site) <= 2.5f;
if (!occupyingHere && gm.RestingOccupancy(b) >= b.RestCapacity) continue;
if (gm.RestingOccupancy(b, this) >= b.RestCapacity) continue;
float d = Position.DistanceSquaredTo(b.Site);
if (d < bestDist) { bestDist = d; shelter = b; }
}
// A bed is only yours if it exists when you lie down in it — twenty
// people on the floor of a three-bed house are not "sheltered."
bool sheltered = false;
if (shelter != null)
{
if (Position.DistanceTo(shelter.Site) > 2.5f) MoveToward(shelter.Site);
else sheltered = true;
else sheltered = gm.RestingOccupancy(shelter, this) < shelter.RestCapacity;
}
Activity = sheltered ? "resting at shelter"
: shelter != null ? "walking home to rest"
@@ -550,12 +571,24 @@ public class Npc
_unshelteredRests = 0; // a bed found is a grievance forgotten
}
if (gm.IsNight) _nightDuringRest = true;
// Where you habitually sleep is where you live — a running average,
// so one night camping at a mine doesn't redefine your life.
if (sheltered || shelter == null)
{
_homeSpot = _hasHomeSpot ? _homeSpot.Lerp(Position, 0.2f) : Position;
_hasHomeSpot = true;
}
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)
{
if (!_shelteredDuringRest) _unshelteredRests++;
// Only unsheltered NIGHTS count as grievances — a rough nap
// in a field is life; a rough night is a reason to build.
if (!_shelteredDuringRest && _nightDuringRest) _unshelteredRests++;
Enter(NpcState.Gathering);
}
}