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>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user