using System; using System.Collections.Generic; using Godot; namespace WorldSim; public enum MaterialKind { Wood, Stone, Clay, Water, Food, Ore, Herb } /// /// A harvestable resource node on the grid (SPEC §6). /// Wood regenerates; stone is static; clay sits near water; water is unlimited. /// public class ResourceNode { public MaterialKind Kind; public Vector2I Cell; public float Amount; public float MaxAmount; public float RegenPerDay; // 0 for stone; water ignores Amount entirely public bool IsDepleted => Kind != MaterialKind.Water && Amount <= 0f; } /// /// World generation, resource placement, and the shared resource pool (SPEC §9). /// Grid is 256×256 (SPEC §9 performance target). No scripted town layout — /// NPCs are dropped in and the community forms where it forms. /// public class World { public const int GridSize = 1280; public List Nodes { get; } = new(); /// Units harvested since the last daily stats snapshot. public float HarvestedToday; private readonly Random _rng; // Node cell index (built once — nodes never move). Only the sensing sweep // uses it, but that runs per-NPC constantly, so it's the one place a node // index genuinely pays (unlike nearest-of-kind, which stays a linear scan). private const int NodeCell = 32; private readonly Dictionary<(int, int), List> _nodeCells = new(); private void IndexNode(ResourceNode n) { var key = ((int)(n.Cell.X / NodeCell), (int)(n.Cell.Y / NodeCell)); if (!_nodeCells.TryGetValue(key, out var list)) _nodeCells[key] = list = new List(); list.Add(n); } public World(int seed) { _rng = new Random(seed); } /// /// Place resources: water source tiles first, clay near water, /// wood in clusters (groves), stone in a few deposits. /// TODO(Week 1): replace uniform scatter with noise-based clustering. /// public void Generate() { // Resource counts scale with area (1280² ≈ 1.23× the 1152² world) so // density holds across the boundless globe. PlaceMany(MaterialKind.Water, count: 110, amount: float.PositiveInfinity, regen: 0f); PlaceMany(MaterialKind.Clay, count: 220, amount: 40f, regen: 0.5f); PlaceMany(MaterialKind.Wood, count: 1330, amount: 30f, regen: 2f); PlaceMany(MaterialKind.Stone, count: 390, amount: 80f, regen: 0f); PlaceMany(MaterialKind.Food, count: 1000, amount: 20f, regen: 4f); PlaceMany(MaterialKind.Ore, count: 110, amount: 60f, regen: 0f); PlaceMany(MaterialKind.Herb, count: 330, amount: 15f, regen: 3f); } private void PlaceMany(MaterialKind kind, int count, float amount, float regen) { for (int i = 0; i < count; i++) { var node = new ResourceNode { Kind = kind, Cell = new Vector2I(_rng.Next(GridSize), _rng.Next(GridSize)), Amount = amount, MaxAmount = amount, RegenPerDay = regen, }; Nodes.Add(node); IndexNode(node); } } /// /// Harvest respecting the harvester's sustainability weight: /// a South soul stops at ~50% of the node (SPEC §10) so nodes stay healthy. /// Taking from below the commons line marks the taker's soul — it is /// taking from everyone, whether anyone sees it or not. /// Returns the amount actually taken. /// public float Harvest(ResourceNode node, float requested, Npc npc) { if (node.Kind == MaterialKind.Water) return requested; // unlimited at source // Reciprocity is with the living land: floors and the commons line // apply only to what regrows. Minerals quarry to depletion, for // every soul equally, with no mark on anyone. bool living = node.RegenPerDay > 0f; float floorFraction = living ? npc.Soul.Sustainability * 0.5f : 0f; float takeable = Math.Max(0f, node.Amount - node.MaxAmount * floorFraction); float taken = Math.Min(requested, takeable); node.Amount -= taken; HarvestedToday += taken; if (living) { // The moral line sits below any respectful harvest floor (nature // souls stop at 45%) — ordinary sustainable taking never grazes // it, while strip-harvesting (selfish ~10%) lives deep inside it. float commons = node.MaxAmount * 0.4f; if (node.Amount < commons) { float takenBelow = Math.Min(taken, commons - node.Amount); npc.Imprint.OthersRegard = Math.Clamp(npc.Imprint.OthersRegard + takenBelow * 0.004f, 0f, 1f); } } return taken; } /// Invoke action for every node within radius — used by NPCs to /// discover nearby nodes (addendum §4 sensing). Cell-indexed: scans only /// the buckets the radius touches, not all ~1,260 nodes. public void ForEachNodeNear(Vector2 position, float radius, System.Action action) { float r2 = radius * radius; int reach = (int)(radius / NodeCell) + 1; int cols = (GridSize + NodeCell - 1) / NodeCell; // cells per axis int cx = (int)(position.X / NodeCell); int cy = (int)(position.Y / NodeCell); for (int dx = -reach; dx <= reach; dx++) for (int dy = -reach; dy <= reach; dy++) { int gx = ((cx + dx) % cols + cols) % cols; // wrap at the seam int gy = ((cy + dy) % cols + cols) % cols; if (!_nodeCells.TryGetValue((gx, gy), out var list)) continue; foreach (var node in list) if (Toroidal.DistanceSquared(position, node.Cell) <= r2) action(node); } } /// Seed a town's founders with the resources around their home, /// so they begin with a working local map instead of blind. public IEnumerable NodesWithin(Vector2 center, float radius) { float r2 = radius * radius; foreach (var node in Nodes) if (Toroidal.DistanceSquared(center, node.Cell) <= r2) yield return node; } /// Nearest node of a kind regardless of amount — for returning /// unneeded minerals to the quarry rather than carrying them forever. public ResourceNode? FindNearestAny(MaterialKind kind, Vector2 position) { ResourceNode? best = null; float bestDist = float.MaxValue; foreach (var node in Nodes) { if (node.Kind != kind) continue; float d = Toroidal.DistanceSquared(position, node.Cell); if (d < bestDist) { best = node; bestDist = d; } } return best; } /// /// Called once per in-game day by GameManager. Season multipliers throttle /// regeneration — winter is when the granary earns its clay. /// public void RegenerateDaily(float foodMult = 1f, float woodMult = 1f) { bool frost = foodMult < 0.1f; // deep winter foreach (var node in Nodes) { // Frost kills standing food — the wild larder rots on the stem. // Without this, the sustainability floors act as a huge hidden // savings account and winter never reaches an actual villager. if (frost && node.Kind == MaterialKind.Food) { node.Amount = Math.Max(node.Amount * 0.93f, node.MaxAmount * 0.15f); continue; } if (node.RegenPerDay <= 0f) continue; float mult = node.Kind switch { MaterialKind.Food => foodMult, MaterialKind.Wood => woodMult, _ => 1f, }; node.Amount = Math.Min(node.MaxAmount, node.Amount + node.RegenPerDay * mult); } } /// /// Nearest node this particular soul considers harvestable. What counts /// as "worth taking" is a moral judgment, not a world fact: a nature /// soul walks past a node at half strength; a selfish soul strips it to /// the stem. The commons' savings account only exists for those who /// honor it. /// public ResourceNode? FindNearest(MaterialKind kind, Vector2 position, float minFraction = 0.5f) { ResourceNode? best = null; float bestDist = float.MaxValue; foreach (var node in Nodes) { if (node.Kind != kind || node.IsDepleted) continue; // The harvest floor is an ethic of regrowth — it applies only to // living, regenerating things. Leaving half a berry bush lets it // live; leaving half a stone deposit protects nobody, and once // stranded it homelessly, we learned that the hard way (run 9). if (node.Kind != MaterialKind.Water && node.RegenPerDay > 0f && node.Amount <= node.MaxAmount * minFraction) continue; float d = Toroidal.DistanceSquared(position, node.Cell); if (d < bestDist) { best = node; bestDist = d; } } return best; } }