using System.Collections.Generic; using Godot; namespace WorldSim; /// /// A coarse spatial hash over NPC positions so proximity queries stop being /// O(n²). Rebuilt once per tick from the live NPC list; queries return only /// the buckets within range. At ~80 NPCs this turns every soul-pressure, /// neighbor, and teacher scan from "look at everyone" into "look at the few /// nearby" — the difference between a live view that stutters and one that /// doesn't. /// public class SpatialGrid { private readonly float _cell; private readonly Dictionary<(int, int), List> _buckets = new(); public SpatialGrid(float cellSize) => _cell = cellSize; private (int, int) Key(Vector2 p) => (Mathf.FloorToInt(p.X / _cell), Mathf.FloorToInt(p.Y / _cell)); public void Rebuild(IReadOnlyList npcs) { foreach (var list in _buckets.Values) list.Clear(); foreach (var npc in npcs) { var key = Key(npc.Position); if (!_buckets.TryGetValue(key, out var list)) _buckets[key] = list = new List(); list.Add(npc); } } /// /// Invoke for every NPC within of (excluding none — caller /// filters). Scans only the buckets the radius touches. /// public void ForEachNear(Vector2 center, float radius, System.Action action) { int reach = Mathf.CeilToInt(radius / _cell); int cols = Mathf.CeilToInt(Toroidal.Size / _cell); // buckets per axis var (cx, cy) = Key(center); // Wrap bucket coordinates so a search near the seam also scans the // buckets on the far edge — the world is a torus. for (int dx = -reach; dx <= reach; dx++) for (int dy = -reach; dy <= reach; dy++) { int gx = ((cx + dx) % cols + cols) % cols; int gy = ((cy + dy) % cols + cols) % cols; if (_buckets.TryGetValue((gx, gy), out var list)) foreach (var npc in list) action(npc); } } }