Toroidal world: boundless wrapping globe, 2x2 even town grid
The map is now a torus - walk off any edge and reappear on the opposite side, no edges or corners. A new Toroidal helper carries all wrap-aware distance/direction/movement math, and every position comparison in the sim routes through it: soul pressure, node + structure sensing, gift/teacher/ neighbor scans, bonding, movement, the spatial grids (NPC and node cell index both wrap their bucket lookups at the seam), and town centroid (averaged in anchor-relative deltas, since averaging raw coordinates across the seam is meaningless). Towns re-laid as a 2x2 even grid on the torus (quarter/three-quarter points, 320 & 960 on a 1280 world): every town sits exactly 640 units from each of its two axis-neighbours in all directions, wrap included - no more edge-boxed North/South that could only forage inward. World grew 1152 -> 1280 with resources scaled to area. Verified over a 240-day soak: all four towns alive and balanced (pop 23-25), population stable ~96, 119 bonds (27 cross-town), cultures still diverge, and wrapped distances correctly cap at the torus half-diagonal (no false across-seam blowups). 183s headless. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+24
-16
@@ -29,10 +29,14 @@ public partial class GameManager : Node2D
|
||||
public IReadOnlyList<TownDef> Towns => _towns;
|
||||
private readonly List<TownDef> _towns = new()
|
||||
{
|
||||
new("North", new Vector2(576f, 190f), SoulType.Generational),
|
||||
new("South", new Vector2(576f, 962f), SoulType.Nature),
|
||||
new("West", new Vector2(190f, 576f), SoulType.Materialist),
|
||||
new("East", new Vector2(962f, 576f), SoulType.Selfish),
|
||||
// 2×2 even grid on the torus: towns at the quarter/three-quarter
|
||||
// points of each axis (320 and 960 on a 1280 world). Every town is
|
||||
// exactly 640 units from each of its two axis-neighbours in all
|
||||
// directions, wrap included — no edges, no corners, no town boxed in.
|
||||
new("North", new Vector2(320f, 320f), SoulType.Generational),
|
||||
new("East", new Vector2(960f, 320f), SoulType.Selfish),
|
||||
new("West", new Vector2(320f, 960f), SoulType.Materialist),
|
||||
new("South", new Vector2(960f, 960f), SoulType.Nature),
|
||||
};
|
||||
|
||||
private static SoulProfile ProfileFor(SoulType t) => t switch
|
||||
@@ -116,7 +120,7 @@ public partial class GameManager : Node2D
|
||||
HomeTown = town,
|
||||
Soul = soul,
|
||||
PersonalityModifier = (float)(rng.NextDouble() * 0.4 - 0.2),
|
||||
Position = center + new Vector2(rng.Next(-12, 13), rng.Next(-12, 13)),
|
||||
Position = Toroidal.Wrap(center + new Vector2(rng.Next(-12, 13), rng.Next(-12, 13))),
|
||||
// Staggered ages so the founding generation doesn't die
|
||||
// in one terrible week.
|
||||
AgeDays = rng.Next(0, 100),
|
||||
@@ -142,7 +146,7 @@ public partial class GameManager : Node2D
|
||||
int count = 0;
|
||||
foreach (var npc in Npcs)
|
||||
if (npc != except && npc.State == NpcState.Resting &&
|
||||
npc.Position.DistanceTo(b.Site) <= 2.5f)
|
||||
Toroidal.Distance(npc.Position, b.Site) <= 2.5f)
|
||||
count++;
|
||||
return count;
|
||||
}
|
||||
@@ -174,21 +178,25 @@ public partial class GameManager : Node2D
|
||||
|
||||
public Vector2 TownCentroid(string town)
|
||||
{
|
||||
// Averaged in ANCHOR-RELATIVE torus deltas, not raw positions —
|
||||
// averaging absolute coordinates across the wrap seam gives garbage
|
||||
// (two people on opposite edges are neighbours, but their coordinates
|
||||
// average to the far side of the world). Deltas keep the seam honest.
|
||||
Vector2 anchor = TownAnchor(town);
|
||||
Vector2 sum = Vector2.Zero;
|
||||
int count = 0;
|
||||
foreach (var npc in Npcs)
|
||||
{
|
||||
if (npc.HomeTown != town) continue;
|
||||
sum += npc.Position;
|
||||
sum += Toroidal.Delta(anchor, npc.Position);
|
||||
count++;
|
||||
}
|
||||
if (count == 0) return TownAnchor(town);
|
||||
if (count == 0) return anchor;
|
||||
|
||||
// Anchor-weighted: the settlement's build-center stays near its
|
||||
// cardinal seed even as villagers roam outward to distant resources.
|
||||
// Towns keep their place on the compass instead of drifting to the
|
||||
// middle of the map where everyone's foraging paths overlap.
|
||||
return (sum / count).Lerp(TownAnchor(town), 0.7f);
|
||||
// Anchor-weighted (0.7 toward the seed) so a settlement keeps its
|
||||
// place as villagers roam, then wrapped back into the world.
|
||||
Vector2 mean = sum / count;
|
||||
return Toroidal.Wrap(anchor + mean * 0.3f);
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
@@ -221,7 +229,7 @@ public partial class GameManager : Node2D
|
||||
Grid.ForEachNear(a.Position, radius, b =>
|
||||
{
|
||||
if (b == a) return;
|
||||
float dist = a.Position.DistanceTo(b.Position);
|
||||
float dist = Toroidal.Distance(a.Position, b.Position);
|
||||
if (dist > radius) return;
|
||||
float falloff = 1f - dist / radius;
|
||||
float fromA = Mathf.Lerp(-strength, strength, a.Imprint.Total) * falloff;
|
||||
@@ -353,7 +361,7 @@ public partial class GameManager : Node2D
|
||||
foreach (var b in Buildings)
|
||||
{
|
||||
if (b.RestCapacity <= 0) continue;
|
||||
float d = npc.Position.DistanceSquaredTo(b.Site);
|
||||
float d = Toroidal.DistanceSquared(npc.Position, b.Site);
|
||||
if (d < best) { best = d; at = b; }
|
||||
}
|
||||
if (at == null) continue;
|
||||
@@ -404,7 +412,7 @@ public partial class GameManager : Node2D
|
||||
foreach (var b in Buildings)
|
||||
{
|
||||
if (b.RestCapacity <= 0) continue;
|
||||
float d = npc.Position.DistanceSquaredTo(b.Site);
|
||||
float d = Toroidal.DistanceSquared(npc.Position, b.Site);
|
||||
if (d < best) { best = d; at = b; }
|
||||
}
|
||||
if (at != null) at.Occupancy++;
|
||||
|
||||
Reference in New Issue
Block a user