Run 13: the full world - four towns, resources, bonds, spatial grid
World scaled 3x (768x768) with all four towns at compass points, each running its own soul culture: North generational, South nature, West materialist (new profile), East selfish. Two new resources: Ore (metal tier) and Herb (medicine - mends health, reason to forage, trade good). Spatial hash grid replaces O(n^2) proximity scans (soul pressure, neighbor, teacher) - 3.6x headless speedup at ~80 NPCs. First social verb beyond work: pair-bonds - settled adults who keep warm company pair up; warmth now accrues from proximity not only gifts; bonds break at death. Reproduction deferred (bonds-first per plan). The four-town thesis fully realized on one engine (240-day run): North know 1.27 clean, South nourish 0.85 clean, West prosperous but debris 0.23, East hungriest 0.60 and lowest knowledge 1.08. Positive cultures stay soul-clean, negative ones carry real debris, all from behavior weights - nothing scripted. 99 bonds, 80 deaths, four living societies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+64
-8
@@ -40,6 +40,15 @@ public class Npc
|
||||
// --- Knowledge (SPEC §5) ----------------------------------------------
|
||||
public Knowledge Know = new();
|
||||
|
||||
// --- Bonds: the first social verb beyond work (relationships) ---------
|
||||
// A pair-bond forms between two settled adults who keep warm company.
|
||||
// Reproduction is deferred (bonds-first); for now a bond is a standing
|
||||
// relationship — chosen companionship, warmth that compounds, someone
|
||||
// whose grave you would tend.
|
||||
public Npc? Partner;
|
||||
public bool IsBonded => Partner != null;
|
||||
public bool IsAdult => AgeDays >= 20f; // childhood is the first ~20 days
|
||||
|
||||
// --- State -----------------------------------------------------------
|
||||
public NpcState State = NpcState.Gathering;
|
||||
|
||||
@@ -110,6 +119,7 @@ public class Npc
|
||||
{
|
||||
if (_targetNpc == dead) _targetNpc = null;
|
||||
if (_giftSource == dead) _giftSource = null;
|
||||
if (Partner == dead) Partner = null; // a bond outlives nothing but memory
|
||||
}
|
||||
|
||||
// --- Diagnostics (read-only views for the stats logger) ---------------
|
||||
@@ -209,6 +219,16 @@ 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);
|
||||
|
||||
// Herbs are medicine: the hurt who carry them mend faster, and a
|
||||
// skilled forager gets more from the same leaf. Reason to gather
|
||||
// herbs, and a trade good worth carrying between towns.
|
||||
if (Health < 90f &&
|
||||
Inventory.TryGetValue(MaterialKind.Herb, out float herb) && herb >= 1f)
|
||||
{
|
||||
Inventory[MaterialKind.Herb] = herb - 1f;
|
||||
Health = Mathf.Min(100f, Health + 6f * (1f + Know[KnowledgeDomain.Forage]));
|
||||
}
|
||||
if (Nourishment < 0.3f) _hungerMemory = true; // real hunger is not forgotten
|
||||
Nourishment = Mathf.Clamp(Nourishment, 0f, 1f);
|
||||
|
||||
@@ -694,12 +714,12 @@ public class Npc
|
||||
{
|
||||
Npc? teacher = null;
|
||||
float bestWorth = 0.15f; // must actually know more than us
|
||||
foreach (var other in gm.Npcs)
|
||||
gm.Grid.ForEachNear(Position, 40f, other =>
|
||||
{
|
||||
if (other == this || other.HomeTown != HomeTown) continue;
|
||||
if (other == this || other.HomeTown != HomeTown) return;
|
||||
float worth = (other.Know.Total - Know.Total) * (1f + other.Soul.TeachingDrive);
|
||||
if (worth > bestWorth) { bestWorth = worth; teacher = other; }
|
||||
}
|
||||
});
|
||||
if (teacher != null)
|
||||
{
|
||||
if (Position.DistanceTo(teacher.Position) > 3f)
|
||||
@@ -717,13 +737,46 @@ public class Npc
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise: drift toward the nearest neighbor — community forms
|
||||
// where feet do.
|
||||
foreach (var other in gm.Npcs)
|
||||
// A bonded soul keeps their partner's company by preference —
|
||||
// relationships are a pull the work queue doesn't override.
|
||||
if (IsBonded && Partner!.Health > 0f)
|
||||
{
|
||||
if (other == this) continue;
|
||||
if (Position.DistanceTo(Partner.Position) > 3f)
|
||||
{
|
||||
Activity = $"with {Partner.Name}";
|
||||
MoveToward(Partner.Position);
|
||||
}
|
||||
else
|
||||
{
|
||||
RecordEcho(Partner.Name, 0.02f);
|
||||
Activity = $"beside {Partner.Name}";
|
||||
}
|
||||
if (!gm.IsNight && _stateTimer > 20f) Enter(NpcState.Gathering);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise: drift toward the nearest neighbor — community forms
|
||||
// where feet do. Grid-scoped so it stays cheap at scale.
|
||||
Npc? found = null;
|
||||
float fBest = float.MaxValue;
|
||||
gm.Grid.ForEachNear(Position, 30f, other =>
|
||||
{
|
||||
if (other == this) return;
|
||||
float d = Position.DistanceSquaredTo(other.Position);
|
||||
if (d < bestDist) { bestDist = d; nearest = other; }
|
||||
if (d < fBest) { fBest = d; found = other; }
|
||||
});
|
||||
nearest = found;
|
||||
|
||||
// Consider bonding: an unbonded adult who keeps warm company with a
|
||||
// compatible, unbonded neighbor may pair with them. Same town, real
|
||||
// warmth, both grown — companionship chosen, not assigned.
|
||||
if (!IsBonded && IsAdult && nearest is { IsBonded: false, IsAdult: true } n &&
|
||||
n.HomeTown == HomeTown && Position.DistanceTo(n.Position) < 4f &&
|
||||
GetWarmth(n.Name) > 0.4f && n.GetWarmth(Name) > 0.4f)
|
||||
{
|
||||
Partner = n;
|
||||
n.Partner = this;
|
||||
Godot.GD.Print($"[Bond] day {gm.Day}: {Name} and {n.Name} ({HomeTown}) pair up.");
|
||||
}
|
||||
if (nearest != null && Position.DistanceTo(nearest.Position) > 3f)
|
||||
{
|
||||
@@ -732,6 +785,9 @@ public class Npc
|
||||
}
|
||||
else if (nearest != null)
|
||||
{
|
||||
// Company that isn't teaching still warms — this is how bonds
|
||||
// become possible: familiarity accrues just by spending time.
|
||||
RecordEcho(nearest.Name, 0.01f);
|
||||
Activity = LearnFrom(nearest)
|
||||
? $"learning from {nearest.Name}"
|
||||
: $"chatting with {nearest.Name}";
|
||||
|
||||
Reference in New Issue
Block a user