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>
117 lines
4.6 KiB
C#
117 lines
4.6 KiB
C#
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);
|
||
}
|
||
}
|