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:
+42
-9
@@ -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:00–22: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);
|
||||
|
||||
// 2–3. 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user