using Theriapolis.Core.Util; using Theriapolis.Core.World; namespace Theriapolis.Core.Entities; /// /// 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 . /// /// 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 . /// public sealed class WorldTravelPlanner { private readonly AStarPathfinder _astar = new(); private readonly WorldState _world; public WorldTravelPlanner(WorldState world) { _world = world; } /// /// Returns null if no path exists. Returned waypoints are tile coordinates, /// not world-pixel coordinates — convert with . /// 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); /// /// 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. /// 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; } /// In-game seconds to walk between two adjacent tile centers. 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); } }