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:
+33
-30
@@ -19,18 +19,28 @@ public partial class GameManager : Node2D
|
||||
// centroid, buildings, and granary. NPCs affiliate with a home town
|
||||
// but roam freely — the space between towns is where cultures meet.
|
||||
|
||||
[Export] public int SouthPopulation = 12; // nature souls
|
||||
[Export(PropertyHint.Range, "0,15,1")]
|
||||
public int SelfishCount = 3; // east souls living in the South
|
||||
[Export] public int NorthPopulation = 10; // generational souls
|
||||
[Export] public int TownPopulationEach = 20; // per settlement (~80 total)
|
||||
|
||||
public record TownDef(string Name, Vector2 Center, SoulType Soul);
|
||||
|
||||
// Four towns at the compass points of the world, each its own culture
|
||||
// (per DESIGN.md): North reveres lineage, South lives with the land,
|
||||
// West performs material wealth, East schemes for self.
|
||||
public IReadOnlyList<TownDef> Towns => _towns;
|
||||
private readonly List<TownDef> _towns = new()
|
||||
{
|
||||
new("South", new Vector2(128f, 190f), SoulType.Nature),
|
||||
new("North", new Vector2(128f, 62f), SoulType.Generational),
|
||||
new("North", new Vector2(384f, 130f), SoulType.Generational),
|
||||
new("South", new Vector2(384f, 638f), SoulType.Nature),
|
||||
new("West", new Vector2(130f, 384f), SoulType.Materialist),
|
||||
new("East", new Vector2(638f, 384f), SoulType.Selfish),
|
||||
};
|
||||
|
||||
private static SoulProfile ProfileFor(SoulType t) => t switch
|
||||
{
|
||||
SoulType.Generational => SoulProfile.GenerationalSoul(),
|
||||
SoulType.Nature => SoulProfile.NatureSoul(),
|
||||
SoulType.Materialist => SoulProfile.MaterialistSoul(),
|
||||
_ => SoulProfile.SelfishSoul(),
|
||||
};
|
||||
|
||||
[Export(PropertyHint.Range, "1,2000,1")]
|
||||
@@ -45,6 +55,10 @@ public partial class GameManager : Node2D
|
||||
public List<Npc> Npcs { get; } = new();
|
||||
public List<Building> Buildings { get; } = new();
|
||||
|
||||
/// <summary>Rebuilt each tick; proximity queries go through it so the
|
||||
/// hot loops stay near-linear as population grows.</summary>
|
||||
public SpatialGrid Grid { get; } = new(cellSize: 12f);
|
||||
|
||||
public long TotalTicks { get; private set; }
|
||||
public int Day => (int)(TotalTicks / 1440);
|
||||
public int MinuteOfDay => (int)(TotalTicks % 1440);
|
||||
@@ -114,18 +128,7 @@ public partial class GameManager : Node2D
|
||||
}
|
||||
|
||||
foreach (var town in _towns)
|
||||
{
|
||||
if (town.Name == "South")
|
||||
{
|
||||
Spawn(town.Name, town.Center, SoulProfile.NatureSoul(), SouthPopulation, "South");
|
||||
for (int i = 0; i < SelfishCount; i++)
|
||||
Spawn(town.Name, town.Center, SoulProfile.SelfishSoul(), 1, "South");
|
||||
}
|
||||
else if (town.Name == "North")
|
||||
{
|
||||
Spawn(town.Name, town.Center, SoulProfile.GenerationalSoul(), NorthPopulation, "North");
|
||||
}
|
||||
}
|
||||
Spawn(town.Name, town.Center, ProfileFor(town.Soul), TownPopulationEach, town.Name);
|
||||
}
|
||||
|
||||
/// <summary>How many villagers (other than <paramref name="except"/>)
|
||||
@@ -189,23 +192,21 @@ public partial class GameManager : Node2D
|
||||
const float radius = 8f;
|
||||
const float strength = 0.00004f;
|
||||
|
||||
for (int i = 0; i < Npcs.Count; i++)
|
||||
// Spatial grid: each soul presses only on the few within reach,
|
||||
// not on everyone. Directed pass (a→b for every nearby b) so each
|
||||
// ordered pair is handled once from the source's side.
|
||||
foreach (var a in Npcs)
|
||||
{
|
||||
for (int j = i + 1; j < Npcs.Count; j++)
|
||||
Grid.ForEachNear(a.Position, radius, b =>
|
||||
{
|
||||
var a = Npcs[i];
|
||||
var b = Npcs[j];
|
||||
if (b == a) return;
|
||||
float dist = a.Position.DistanceTo(b.Position);
|
||||
if (dist > radius) continue;
|
||||
|
||||
if (dist > radius) return;
|
||||
float falloff = 1f - dist / radius;
|
||||
|
||||
float fromA = Mathf.Lerp(-strength, strength, a.Imprint.Total) * falloff;
|
||||
b.Imprint.ExternalPerception = Mathf.Clamp(b.Imprint.ExternalPerception + fromA, 0f, 1f);
|
||||
|
||||
float fromB = Mathf.Lerp(-strength, strength, b.Imprint.Total) * falloff;
|
||||
a.Imprint.ExternalPerception = Mathf.Clamp(a.Imprint.ExternalPerception + fromB, 0f, 1f);
|
||||
}
|
||||
b.Imprint.ExternalPerception =
|
||||
Mathf.Clamp(b.Imprint.ExternalPerception + fromA, 0f, 1f);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +281,8 @@ public partial class GameManager : Node2D
|
||||
|
||||
private void Tick()
|
||||
{
|
||||
Grid.Rebuild(Npcs);
|
||||
|
||||
TotalTicks++;
|
||||
|
||||
foreach (var npc in Npcs)
|
||||
|
||||
Reference in New Issue
Block a user