Files
TheriapolisV3/Theriapolis.Core/Entities/WorldTravelPlanner.cs
T

117 lines
4.6 KiB
C#
Raw Normal View History

using Theriapolis.Core.Util;
using Theriapolis.Core.World;
namespace Theriapolis.Core.Entities;
/// <summary>
/// Plans a continuous-time path between two world tiles. The result is a
/// list of world-pixel waypoints (tile centers); the controller animates
/// the player along it at <see cref="C.PLAYER_TRAVEL_PX_PER_SEC"/>.
///
/// Cost function:
/// • Ocean tiles (Biome == Ocean) are impassable.
/// • Mountain tiles cost 4× a grassland tile (slow but not blocked).
/// • Tiles carrying a road get a strong discount (matches the ROAD_SPEED_MULT idea).
/// • Diagonal moves are √2 like everywhere else in the codebase.
///
/// The clock-advance amount is computed afterwards by walking the path
/// and summing per-segment travel time — see <see cref="EstimateSecondsForLeg"/>.
/// </summary>
public sealed class WorldTravelPlanner
{
private readonly AStarPathfinder _astar = new();
private readonly WorldState _world;
public WorldTravelPlanner(WorldState world) { _world = world; }
/// <summary>
/// Returns null if no path exists. Returned waypoints are tile coordinates,
/// not world-pixel coordinates — convert with <see cref="TileCenterToWorldPixel"/>.
/// </summary>
public List<(int X, int Y)>? PlanTilePath(int sx, int sy, int gx, int gy)
{
if (!IsWalkable(sx, sy)) return null;
if (!IsWalkable(gx, gy)) return null;
return _astar.FindPath(sx, sy, gx, gy, CostFn);
}
private float CostFn(int fx, int fy, int tx, int ty, byte _)
{
if (!IsWalkable(tx, ty)) return float.PositiveInfinity;
ref var t = ref _world.TileAt(tx, ty);
float baseCost = TerrainCost(t);
// Strong incentive to follow roads — matches the EXISTING_ROAD_COST
// worldgen philosophy. Pure additive (not multiplicative) so it
// never goes negative and A* admissibility holds.
if ((t.Features & FeatureFlags.HasRoad) != 0) baseCost *= 0.25f;
return baseCost;
}
private bool IsWalkable(int x, int y)
{
if ((uint)x >= C.WORLD_WIDTH_TILES) return false;
if ((uint)y >= C.WORLD_HEIGHT_TILES) return false;
ref var t = ref _world.TileAt(x, y);
return t.Biome != BiomeId.Ocean;
}
private static float TerrainCost(in WorldTile t) => t.Biome switch
{
BiomeId.MountainAlpine => 4.0f,
BiomeId.MountainForested => 3.0f,
BiomeId.Wetland => 2.5f,
BiomeId.Foothills => 1.8f,
BiomeId.Boreal => 1.6f,
BiomeId.SubtropicalForest => 1.6f,
BiomeId.TemperateDeciduous => 1.5f,
BiomeId.Tundra => 1.5f,
BiomeId.DesertCold => 1.4f,
BiomeId.Mangrove => 1.4f,
BiomeId.MarshEdge => 1.4f,
BiomeId.Scrubland => 1.2f,
BiomeId.ForestEdge => 1.3f,
BiomeId.Cliff => 3.0f,
BiomeId.TemperateGrassland => 1.0f,
BiomeId.RiverValley => 1.0f,
BiomeId.Coastal => 1.0f,
BiomeId.Beach => 1.0f,
BiomeId.TidalFlat => 1.5f,
_ => 1.2f,
};
public static Vec2 TileCenterToWorldPixel(int x, int y)
=> new(x * C.WORLD_TILE_PIXELS + C.WORLD_TILE_PIXELS * 0.5f,
y * C.WORLD_TILE_PIXELS + C.WORLD_TILE_PIXELS * 0.5f);
/// <summary>
/// In-game seconds to traverse a single world-pixel between adjacent tiles.
/// Combines BASE_SEC_PER_WORLD_PIXEL with biome modifier and a road bonus.
/// </summary>
public float SecondsPerPixel(in WorldTile t)
{
float biomeMod = t.Biome switch
{
BiomeId.MountainAlpine => 3.0f,
BiomeId.MountainForested => 2.5f,
BiomeId.Wetland => 2.0f,
BiomeId.Foothills => 1.6f,
BiomeId.Boreal => 1.5f,
BiomeId.SubtropicalForest => 1.5f,
BiomeId.TemperateDeciduous => 1.4f,
BiomeId.Tundra => 1.5f,
_ => 1.0f,
};
float roadMod = ((t.Features & FeatureFlags.HasRoad) != 0) ? C.ROAD_SPEED_MULT : 1f;
return C.BASE_SEC_PER_WORLD_PIXEL * biomeMod * roadMod;
}
/// <summary>In-game seconds to walk between two adjacent tile centers.</summary>
public float EstimateSecondsForLeg(int fx, int fy, int tx, int ty)
{
ref var to = ref _world.TileAt(tx, ty);
float pixDist = Vec2.Dist(TileCenterToWorldPixel(fx, fy), TileCenterToWorldPixel(tx, ty));
return pixDist * SecondsPerPixel(to);
}
}