Files
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

117 lines
4.6 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}