b451f83174
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>
124 lines
5.0 KiB
C#
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);
|
|
}
|
|
}
|