Files
TheriapolisV3/Theriapolis.Core/World/DangerZone.cs
T
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

124 lines
5.0 KiB
C#

using Theriapolis.Core.World.Polylines;
namespace Theriapolis.Core.World;
/// <summary>
/// Phase 5 M5: per-chunk threat-tier index. Drives which template each
/// <see cref="Tactical.SpawnKind"/> 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 <see cref="Tactical.TacticalChunk.DangerZone"/> and folded into
/// the chunk's hash so determinism tests catch any formula drift.
/// </summary>
public static class DangerZone
{
/// <summary>
/// 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 <see cref="Entities.PlayerActor"/> spawns.
/// </summary>
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);
}
/// <summary>
/// Convenience: pick the player-start tile from the world (Tier-1
/// settlement if any, else map centre) and compute the zone.
/// </summary>
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));
/// <summary>Tier-1 settlement tile if available, else map centre.</summary>
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);
}
}