using Theriapolis.Core.World.Polylines; namespace Theriapolis.Core.World; /// /// Phase 5 M5: per-chunk threat-tier index. Drives which template each /// instantiates: zone 0 = safest (footpads, /// pups), zone 4 = deepest wilds (captains, dire wolves, brown bears). /// /// Computed once per chunk at instantiation time from biome + distance to /// player-start + distance to nearest road + distance to nearest settlement. /// Stored on and folded into /// the chunk's hash so determinism tests catch any formula drift. /// public static class DangerZone { /// /// Compute the danger zone for a given world-tile center. Pass the /// world's player-start tile so distance-from-start is meaningful even /// before the actual spawns. /// public static int Compute(int worldTileX, int worldTileY, WorldState world, int startTileX, int startTileY) { int zone = 0; zone += DistanceFromStartZone(worldTileX, worldTileY, startTileX, startTileY); zone += DistanceFromRoadZone(worldTileX, worldTileY, world); zone += DistanceFromSettlementZone(worldTileX, worldTileY, world); zone += BiomeDangerBonus(in world.TileAt( System.Math.Clamp(worldTileX, 0, C.WORLD_WIDTH_TILES - 1), System.Math.Clamp(worldTileY, 0, C.WORLD_HEIGHT_TILES - 1))); return System.Math.Clamp(zone, C.DANGER_ZONE_MIN, C.DANGER_ZONE_MAX); } /// /// Convenience: pick the player-start tile from the world (Tier-1 /// settlement if any, else map centre) and compute the zone. /// public static int Compute(int worldTileX, int worldTileY, WorldState world) { var (sx, sy) = ResolveStartTile(world); return Compute(worldTileX, worldTileY, world, sx, sy); } private static int DistanceFromStartZone(int x, int y, int startX, int startY) { int dist = ChebyshevDistance(x, y, startX, startY); return dist / C.DANGER_DIST_FROM_START_PER_ZONE; } private static int DistanceFromRoadZone(int worldTileX, int worldTileY, WorldState world) { if (world.Roads.Count == 0) return 1; // no roads at all → treat as remote int worldPxX = worldTileX * C.WORLD_TILE_PIXELS + C.WORLD_TILE_PIXELS / 2; int worldPxY = worldTileY * C.WORLD_TILE_PIXELS + C.WORLD_TILE_PIXELS / 2; float minDistSq = float.MaxValue; foreach (var road in world.Roads) { foreach (var pt in road.Points) { float dx = pt.X - worldPxX; float dy = pt.Y - worldPxY; float d2 = dx * dx + dy * dy; if (d2 < minDistSq) minDistSq = d2; } } float distTiles = (float)System.Math.Sqrt(minDistSq) / C.WORLD_TILE_PIXELS; return distTiles > C.DANGER_DIST_FROM_ROAD_THRESHOLD ? 1 : 0; } private static int DistanceFromSettlementZone(int worldTileX, int worldTileY, WorldState world) { if (world.Settlements.Count == 0) return 1; int minDistTiles = int.MaxValue; foreach (var s in world.Settlements) { int d = ChebyshevDistance(worldTileX, worldTileY, s.TileX, s.TileY); if (d < minDistTiles) minDistTiles = d; } return minDistTiles > C.DANGER_DIST_FROM_SETTLE_THRESHOLD ? 1 : 0; } private static int BiomeDangerBonus(in WorldTile tile) => tile.Biome switch { // Settled / safe biomes BiomeId.TemperateGrassland => 0, BiomeId.TemperateDeciduous => 0, BiomeId.RiverValley => 0, BiomeId.Beach => 0, BiomeId.Coastal => 0, BiomeId.Ocean => 0, // not walkable but won't generate spawns either // Mid-danger biomes BiomeId.Boreal => 1, BiomeId.Wetland => 1, BiomeId.Tundra => 1, BiomeId.SubtropicalForest => 1, BiomeId.Scrubland => 1, BiomeId.MountainForested => 1, BiomeId.ForestEdge => 1, BiomeId.Foothills => 1, BiomeId.MarshEdge => 1, BiomeId.Mangrove => 1, // High-danger biomes BiomeId.MountainAlpine => 2, BiomeId.DesertCold => 2, BiomeId.TidalFlat => 1, BiomeId.Cliff => 2, _ => 1, }; private static int ChebyshevDistance(int x1, int y1, int x2, int y2) => System.Math.Max(System.Math.Abs(x1 - x2), System.Math.Abs(y1 - y2)); /// Tier-1 settlement tile if available, else map centre. public static (int x, int y) ResolveStartTile(WorldState world) { foreach (var s in world.Settlements) { if (s.Tier == 1 && !s.IsPoi) return (s.TileX, s.TileY); } return (C.WORLD_WIDTH_TILES / 2, C.WORLD_HEIGHT_TILES / 2); } }