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:
+33
-37
@@ -61,7 +61,7 @@ public class Npc
|
||||
public void SenseStructures(GameManager gm)
|
||||
{
|
||||
foreach (var b in gm.Buildings)
|
||||
if (Position.DistanceSquaredTo(b.Site) <= SensingRadius * SensingRadius)
|
||||
if (Toroidal.DistanceSquared(Position, b.Site) <= SensingRadius * SensingRadius)
|
||||
KnownStructures.Add(b);
|
||||
}
|
||||
|
||||
@@ -97,18 +97,14 @@ public class Npc
|
||||
private void Explore(GameManager gm)
|
||||
{
|
||||
Activity = "exploring for new ground";
|
||||
Vector2 anchor = gm.TownAnchor(HomeTown);
|
||||
Vector2 fromHome = Position - anchor;
|
||||
float distHome = fromHome.Length();
|
||||
|
||||
// Venture outward, unbounded — a town that must range far to eat
|
||||
// genuinely relocates over time (the South drifting inland after a
|
||||
// hard winter, an East scout wandering into a neighbor). Direction is
|
||||
// outward from home, or — when right at the anchor — a per-soul angle
|
||||
// from identity (NOT a fixed vector; the old default leaned hard +Y,
|
||||
// i.e. south, which funnelled every town's explorers into one corner).
|
||||
// Outward from home along the torus-shortest direction, unbounded — a
|
||||
// town that must range far to eat genuinely relocates over time, and
|
||||
// on a boundless globe an explorer can circle the whole world. When
|
||||
// right at the anchor there's no outward vector, so pick a per-soul
|
||||
// angle from identity (not a fixed vector — that once leaned south).
|
||||
Vector2 fromHome = Toroidal.Delta(gm.TownAnchor(HomeTown), Position);
|
||||
Vector2 dir;
|
||||
if (distHome > 1f)
|
||||
if (fromHome.LengthSquared() > 1f)
|
||||
dir = fromHome.Normalized();
|
||||
else
|
||||
{
|
||||
@@ -116,7 +112,7 @@ public class Npc
|
||||
dir = new Vector2(Mathf.Cos(ang), Mathf.Sin(ang));
|
||||
}
|
||||
dir = dir.Rotated(PersonalityModifier * 1.2f); // personality veer, so explorers fan out
|
||||
Position += dir * MoveSpeed;
|
||||
Position = Toroidal.Wrap(Position + dir * MoveSpeed);
|
||||
SenseNodes(gm.World);
|
||||
}
|
||||
|
||||
@@ -135,7 +131,7 @@ public class Npc
|
||||
if (node.Kind != MaterialKind.Water && node.RegenPerDay > 0f &&
|
||||
node.Amount <= node.MaxAmount * minFraction)
|
||||
continue;
|
||||
float d = Position.DistanceSquaredTo(node.Cell);
|
||||
float d = Toroidal.DistanceSquared(Position, node.Cell);
|
||||
if (d < bestDist) { bestDist = d; best = node; }
|
||||
}
|
||||
return best;
|
||||
@@ -380,7 +376,7 @@ public class Npc
|
||||
}
|
||||
|
||||
private void MoveToward(Vector2 target) =>
|
||||
Position = Position.MoveToward(target, MoveSpeed);
|
||||
Position = Toroidal.MoveToward(Position, target, MoveSpeed);
|
||||
|
||||
// --- Demand ----------------------------------------------------------
|
||||
|
||||
@@ -523,7 +519,7 @@ public class Npc
|
||||
{
|
||||
Activity = "fetching food from the granary";
|
||||
MoveToward(_foodSource.Site);
|
||||
if (Position.DistanceTo(_foodSource.Site) < ArriveDist)
|
||||
if (Toroidal.Distance(Position, _foodSource.Site) < ArriveDist)
|
||||
{
|
||||
float got = _foodSource.WithdrawFood(8f);
|
||||
Inventory.TryGetValue(MaterialKind.Food, out float haveF);
|
||||
@@ -536,7 +532,7 @@ public class Npc
|
||||
{
|
||||
Activity = $"asking {_giftSource.Name} for food";
|
||||
MoveToward(_giftSource.Position);
|
||||
if (Position.DistanceTo(_giftSource.Position) < ArriveDist)
|
||||
if (Toroidal.Distance(Position, _giftSource.Position) < ArriveDist)
|
||||
{
|
||||
TradeSystem.RequestGift(_giftSource, this);
|
||||
_giftSource = null;
|
||||
@@ -608,7 +604,7 @@ public class Npc
|
||||
float warmth = GetWarmth(other.Name);
|
||||
if (warmth < -0.3f) return;
|
||||
if (!other.Inventory.TryGetValue(MaterialKind.Food, out float f) || f <= 3f) return;
|
||||
float score = warmth * 40f - Position.DistanceTo(other.Position);
|
||||
float score = warmth * 40f - Toroidal.Distance(Position, other.Position);
|
||||
if (score > bestScore) { bestScore = score; giftFound = other; }
|
||||
});
|
||||
_giftSource = giftFound;
|
||||
@@ -626,7 +622,7 @@ public class Npc
|
||||
}
|
||||
|
||||
MoveToward(_targetNode.Cell);
|
||||
if (Position.DistanceTo(_targetNode.Cell) >= ArriveDist)
|
||||
if (Toroidal.Distance(Position, _targetNode.Cell) >= ArriveDist)
|
||||
{
|
||||
Activity = $"walking to {_targetNode.Kind.ToString().ToLower()}";
|
||||
}
|
||||
@@ -665,7 +661,7 @@ public class Npc
|
||||
{
|
||||
if (other == this) return;
|
||||
if (!TradeSystem.CanTrade(this, other, out _)) return;
|
||||
float d = Position.DistanceSquaredTo(other.Position);
|
||||
float d = Toroidal.DistanceSquared(Position, other.Position);
|
||||
if (d < bestDist) { bestDist = d; partner = other; }
|
||||
});
|
||||
_targetNpc = partner;
|
||||
@@ -674,7 +670,7 @@ public class Npc
|
||||
|
||||
Activity = $"bringing goods to {_targetNpc.Name}";
|
||||
MoveToward(_targetNpc.Position);
|
||||
if (Position.DistanceTo(_targetNpc.Position) < ArriveDist)
|
||||
if (Toroidal.Distance(Position, _targetNpc.Position) < ArriveDist)
|
||||
{
|
||||
if (TradeSystem.CanTrade(this, _targetNpc, out var material))
|
||||
TradeSystem.Execute(this, _targetNpc, material);
|
||||
@@ -704,7 +700,7 @@ public class Npc
|
||||
{
|
||||
if (b.Town != HomeTown || b.IsComplete || b.IsRuined ||
|
||||
b.Kind != BuildingKind.Shelter) continue;
|
||||
float d = Position.DistanceSquaredTo(b.Site);
|
||||
float d = Toroidal.DistanceSquared(Position, b.Site);
|
||||
if (d < best) { best = d; mine = b; }
|
||||
}
|
||||
return mine;
|
||||
@@ -751,7 +747,7 @@ public class Npc
|
||||
|
||||
Activity = "building my own home";
|
||||
MoveToward(mine.Site);
|
||||
if (Position.DistanceTo(mine.Site) < ArriveDist)
|
||||
if (Toroidal.Distance(Position, mine.Site) < ArriveDist)
|
||||
{
|
||||
mine.Deliver(this);
|
||||
if (mine.IsComplete)
|
||||
@@ -791,7 +787,7 @@ public class Npc
|
||||
foreach (var b in gm.Buildings)
|
||||
{
|
||||
if (b.Town != HomeTown || b.IsComplete) continue;
|
||||
float d = Position.DistanceSquaredTo(b.Site);
|
||||
float d = Toroidal.DistanceSquared(Position, b.Site);
|
||||
if (d < bestDist) { bestDist = d; site = b; }
|
||||
}
|
||||
if (site == null)
|
||||
@@ -799,7 +795,7 @@ public class Npc
|
||||
foreach (var b in gm.Buildings)
|
||||
{
|
||||
if (b.Town != HomeTown || !b.WantsUpkeepWood) continue;
|
||||
float d = Position.DistanceSquaredTo(b.Site);
|
||||
float d = Toroidal.DistanceSquared(Position, b.Site);
|
||||
if (d < bestDist) { bestDist = d; site = b; }
|
||||
}
|
||||
}
|
||||
@@ -812,7 +808,7 @@ public class Npc
|
||||
foreach (var b in gm.Buildings)
|
||||
{
|
||||
if (b.Town != HomeTown || !b.WantsFood) continue;
|
||||
float d = Position.DistanceSquaredTo(b.Site);
|
||||
float d = Toroidal.DistanceSquared(Position, b.Site);
|
||||
if (d < bestDist) { bestDist = d; site = b; }
|
||||
}
|
||||
}
|
||||
@@ -822,7 +818,7 @@ public class Npc
|
||||
foreach (var b in gm.Buildings)
|
||||
{
|
||||
if (b.Town != HomeTown || !b.WantsExpansion) continue;
|
||||
float d = Position.DistanceSquaredTo(b.Site);
|
||||
float d = Toroidal.DistanceSquared(Position, b.Site);
|
||||
if (d < bestDist) { bestDist = d; site = b; }
|
||||
}
|
||||
}
|
||||
@@ -875,7 +871,7 @@ public class Npc
|
||||
if (quarry == null) { Enter(NpcState.Socializing); return; }
|
||||
Activity = "returning stone to the quarry";
|
||||
MoveToward(quarry.Cell);
|
||||
if (Position.DistanceTo(quarry.Cell) < ArriveDist)
|
||||
if (Toroidal.Distance(Position, quarry.Cell) < ArriveDist)
|
||||
{
|
||||
quarry.Amount = Mathf.Min(quarry.MaxAmount, quarry.Amount + stoneCarried);
|
||||
Inventory[MaterialKind.Stone] = 0f;
|
||||
@@ -897,7 +893,7 @@ public class Npc
|
||||
: $"expanding {kindName}";
|
||||
|
||||
MoveToward(site.Site);
|
||||
if (Position.DistanceTo(site.Site) < ArriveDist)
|
||||
if (Toroidal.Distance(Position, site.Site) < ArriveDist)
|
||||
{
|
||||
bool delivered = site.Deliver(this);
|
||||
if (!delivered || site.IsComplete) Enter(NpcState.Socializing);
|
||||
@@ -915,10 +911,10 @@ public class Npc
|
||||
if (b.RestCapacity <= 0) continue;
|
||||
// Read the cached per-tick occupancy; +1 to leave room for us if
|
||||
// we're not already counted there.
|
||||
bool alreadyHere = Position.DistanceSquaredTo(b.Site) < 6.25f;
|
||||
bool alreadyHere = Toroidal.DistanceSquared(Position, b.Site) < 6.25f;
|
||||
int taken = alreadyHere ? b.Occupancy - 1 : b.Occupancy;
|
||||
if (taken >= b.RestCapacity) continue;
|
||||
float d = Position.DistanceSquaredTo(b.Site);
|
||||
float d = Toroidal.DistanceSquared(Position, b.Site);
|
||||
if (d < bestDist) { bestDist = d; shelter = b; }
|
||||
}
|
||||
|
||||
@@ -927,7 +923,7 @@ public class Npc
|
||||
bool sheltered = false;
|
||||
if (shelter != null)
|
||||
{
|
||||
if (Position.DistanceTo(shelter.Site) > 2.5f) MoveToward(shelter.Site);
|
||||
if (Toroidal.Distance(Position, shelter.Site) > 2.5f) MoveToward(shelter.Site);
|
||||
else sheltered = true; // we selected a shelter with a free bed
|
||||
}
|
||||
Activity = sheltered ? "resting at shelter"
|
||||
@@ -982,7 +978,7 @@ public class Npc
|
||||
});
|
||||
if (teacher != null)
|
||||
{
|
||||
if (Position.DistanceTo(teacher.Position) > 3f)
|
||||
if (Toroidal.Distance(Position, teacher.Position) > 3f)
|
||||
{
|
||||
Activity = $"seeking out {teacher.Name} to learn";
|
||||
MoveToward(teacher.Position);
|
||||
@@ -1001,7 +997,7 @@ public class Npc
|
||||
// relationships are a pull the work queue doesn't override.
|
||||
if (IsBonded && Partner!.Health > 0f)
|
||||
{
|
||||
if (Position.DistanceTo(Partner.Position) > 3f)
|
||||
if (Toroidal.Distance(Position, Partner.Position) > 3f)
|
||||
{
|
||||
Activity = $"with {Partner.Name}";
|
||||
MoveToward(Partner.Position);
|
||||
@@ -1022,7 +1018,7 @@ public class Npc
|
||||
gm.Grid.ForEachNear(Position, 30f, other =>
|
||||
{
|
||||
if (other == this) return;
|
||||
float d = Position.DistanceSquaredTo(other.Position);
|
||||
float d = Toroidal.DistanceSquared(Position, other.Position);
|
||||
if (d < fBest) { fBest = d; found = other; }
|
||||
});
|
||||
nearest = found;
|
||||
@@ -1035,7 +1031,7 @@ public class Npc
|
||||
// town can shack up across cultures — the first quiet thread of
|
||||
// contact between two peoples.
|
||||
if (!IsBonded && IsAdult && nearest is { IsBonded: false, IsAdult: true } n &&
|
||||
Position.DistanceTo(n.Position) < 4f &&
|
||||
Toroidal.Distance(Position, n.Position) < 4f &&
|
||||
GetWarmth(n.Name) > 0.4f && n.GetWarmth(Name) > 0.4f)
|
||||
{
|
||||
Partner = n;
|
||||
@@ -1043,7 +1039,7 @@ public class Npc
|
||||
string kind = n.HomeTown == HomeTown ? HomeTown : $"{HomeTown}+{n.HomeTown}";
|
||||
Godot.GD.Print($"[Bond] day {gm.Day}: {Name} and {n.Name} ({kind}) pair up.");
|
||||
}
|
||||
if (nearest != null && Position.DistanceTo(nearest.Position) > 3f)
|
||||
if (nearest != null && Toroidal.Distance(Position, nearest.Position) > 3f)
|
||||
{
|
||||
Activity = $"walking over to {nearest.Name}";
|
||||
MoveToward(nearest.Position);
|
||||
|
||||
Reference in New Issue
Block a user