From 1fa4390ca1cc6016407a2674584a929ea31a942a Mon Sep 17 00:00:00 2001 From: MPD Date: Wed, 17 Sep 2025 22:19:24 -0700 Subject: [PATCH] getRoadPath takes a civ instead of a unit Therefore, RoadBetweenCitiesAutomation no longer needs to hallucinate a worker to plan where to build roads Signed-off-by: MPD --- .../unit/RoadBetweenCitiesAutomation.kt | 14 +++------ .../logic/automation/unit/RoadToAutomation.kt | 2 +- core/src/com/unciv/logic/map/MapPathing.kt | 31 ++++++++++--------- .../worldscreen/worldmap/WorldMapHolder.kt | 2 +- .../worldmap/WorldMapTileUpdater.kt | 2 +- 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/core/src/com/unciv/logic/automation/unit/RoadBetweenCitiesAutomation.kt b/core/src/com/unciv/logic/automation/unit/RoadBetweenCitiesAutomation.kt index 0ddd8d54c3..ce69b40ef0 100644 --- a/core/src/com/unciv/logic/automation/unit/RoadBetweenCitiesAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/RoadBetweenCitiesAutomation.kt @@ -107,10 +107,6 @@ class RoadBetweenCitiesAutomation(val civInfo: Civilization, private val cachedF if (roadsToBuildByCitiesCache.containsKey(city)) return roadsToBuildByCitiesCache[city]!! - // TODO: some better worker representative needs to be used here - val workerUnit = civInfo.gameInfo.ruleset.units.map { it.value }.firstOrNull { it.hasUnique(UniqueType.BuildImprovements) } - // This is a temporary unit only for AI purposes so it doesn't get a unique ID - ?.newMapUnit(civInfo, Constants.NO_ID) ?: return listOf() val roadToCapitalStatus = city.cityStats.getRoadTypeOfConnectionToCapital() /** @return Rank 0, 1 or 2 of how important it is to build best available road to capital */ @@ -144,8 +140,8 @@ class RoadBetweenCitiesAutomation(val civInfo: Civilization, private val cachedF // Try to build a plan for the road to the city // TODO: May return inconsistent paths across turns due to worker position, this makes it impossible to plan an exact road resulting in excessive roads built - val roadPath = if (civInfo.cities.indexOf(city) < civInfo.cities.indexOf(closeCity)) MapPathing.getRoadPath(workerUnit, city.getCenterTile(), closeCity.getCenterTile()) ?: continue - else MapPathing.getRoadPath(workerUnit, closeCity.getCenterTile(), city.getCenterTile()) ?: continue + val roadPath = if (civInfo.cities.indexOf(city) < civInfo.cities.indexOf(closeCity)) MapPathing.getRoadPath(civInfo, city.getCenterTile(), closeCity.getCenterTile()) ?: continue + else MapPathing.getRoadPath(civInfo, closeCity.getCenterTile(), city.getCenterTile()) ?: continue val worstRoadStatus = getWorstRoadTypeInPath(roadPath) if (worstRoadStatus == bestRoadAvailable) continue @@ -171,7 +167,7 @@ class RoadBetweenCitiesAutomation(val civInfo: Civilization, private val cachedF // If and only if we have no roads to build to close-by cities then we check for a road to build to the capital // The condition !city.isConnectedToCapital() is to avoid BFS for cities connected to capital with roads when railroads are unlocked else if (roadPlans.isEmpty() && (roadToCapitalStatus < bestRoadAvailable) && !city.isConnectedToCapital()) { - val roadToCapital = getRoadToConnectCityToCapital(workerUnit, city) + val roadToCapital = getRoadToConnectCityToCapital(city) if (roadToCapital != null) { val worstRoadStatus = getWorstRoadTypeInPath(roadToCapital.second) @@ -243,10 +239,10 @@ class RoadBetweenCitiesAutomation(val civInfo: Civilization, private val cachedF * @return a pair containing a list of tiles that resemble the road to build and the city that the road will connect to */ @Readonly - private fun getRoadToConnectCityToCapital(unit: MapUnit, city: City): Pair>? { + private fun getRoadToConnectCityToCapital(city: City): Pair>? { if (tilesOfConnectedCities.isEmpty()) return null // In mods with no capital city indicator, there are no connected cities - val isCandidateTilePredicate: (Tile) -> Boolean = { it.isLand && unit.movement.canPassThrough(it) } + val isCandidateTilePredicate: (Tile) -> Boolean = { it.isLand && MapPathing.isValidRoadPathTile(city.civ, it) } val toConnectTile = city.getCenterTile() @LocalState val bfs: BFS = bfsCache[toConnectTile.position] ?: run { val bfs = BFS(toConnectTile, isCandidateTilePredicate) diff --git a/core/src/com/unciv/logic/automation/unit/RoadToAutomation.kt b/core/src/com/unciv/logic/automation/unit/RoadToAutomation.kt index 95de78198f..53aca97486 100644 --- a/core/src/com/unciv/logic/automation/unit/RoadToAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/RoadToAutomation.kt @@ -52,7 +52,7 @@ class RoadToAutomation(val civInfo: Civilization) { // The path does not exist, create it if (pathToDest == null) { - val foundPath: List? = MapPathing.getRoadPath(unit, currentTile, destinationTile) + val foundPath: List? = MapPathing.getRoadPath(unit.civ, unit.getTile(), destinationTile) if (foundPath == null) { Log.debug("WorkerAutomation: $unit -> connect road failed") stopAndCleanAutomation(unit) diff --git a/core/src/com/unciv/logic/map/MapPathing.kt b/core/src/com/unciv/logic/map/MapPathing.kt index 47812dd6ae..7cf15eb5d0 100644 --- a/core/src/com/unciv/logic/map/MapPathing.kt +++ b/core/src/com/unciv/logic/map/MapPathing.kt @@ -16,29 +16,29 @@ object MapPathing { */ @Suppress("UNUSED_PARAMETER") // While `from` is unused, this function should stay close to the signatures expected by the AStar and getPath `heuristic` parameter. @Readonly - private fun roadPreferredMovementCost(unit: MapUnit, from: Tile, to: Tile): Float{ + private fun roadPreferredMovementCost(civ: Civilization, from: Tile, to: Tile): Float{ // hasRoadConnection accounts for civs that treat jungle/forest as roads // Ignore road over river penalties. - if ((to.hasRoadConnection(unit.civ, false) || to.hasRailroadConnection(false))) + if ((to.hasRoadConnection(civ, false) || to.hasRailroadConnection(false))) return .5f return 1f } @Readonly - fun isValidRoadPathTile(unit: MapUnit, tile: Tile): Boolean { + fun isValidRoadPathTile(civ: Civilization, tile: Tile): Boolean { val roadImprovement = tile.ruleset.roadImprovement val railRoadImprovement = tile.ruleset.railroadImprovement if (tile.isWater) return false if (tile.isImpassible()) return false - if (!unit.civ.hasExplored(tile)) return false - if (!tile.canCivPassThrough(unit.civ)) return false + if (!civ.hasExplored(tile)) return false + if (!tile.canCivPassThrough(civ)) return false - return tile.hasRoadConnection(unit.civ, false) + return tile.hasRoadConnection(civ, false) || tile.hasRailroadConnection(false) - || roadImprovement != null && tile.improvementFunctions.canBuildImprovement(roadImprovement, unit.cache.state) - || railRoadImprovement != null && tile.improvementFunctions.canBuildImprovement(railRoadImprovement, unit.cache.state) + || roadImprovement != null && tile.improvementFunctions.canBuildImprovement(roadImprovement, civ.state) + || railRoadImprovement != null && tile.improvementFunctions.canBuildImprovement(railRoadImprovement,civ.state) } /** @@ -46,14 +46,14 @@ object MapPathing { * * This function uses the A* search algorithm to find an optimal path for road construction between two specified tiles. * - * @param unit The unit that will construct the road. + * @param civ The civlization that will construct the road. * @param startTile The starting tile of the path. * @param endTile The destination tile of the path. * @return A sequence of tiles representing the path from startTile to endTile, or null if no valid path is found. */ @Readonly - fun getRoadPath(unit: MapUnit, startTile: Tile, endTile: Tile): List?{ - return getPath(unit, + fun getRoadPath(civ: Civilization, startTile: Tile, endTile: Tile): List? { + return getConnection(civ, startTile, endTile, ::isValidRoadPathTile, @@ -112,13 +112,15 @@ object MapPathing { fun getConnection(civ: Civilization, startTile: Tile, endTile: Tile, - predicate: (Civilization, Tile) -> Boolean + predicate: (Civilization, Tile) -> Boolean, + cost: (Civilization, Tile, Tile) -> Float = { _, _, _ -> 1f }, + heuristic: (Civilization, Tile, Tile) -> Float = { _, from, to -> from.aerialDistanceTo(to).toFloat() } ): List? { val astar = AStar( startTile, predicate = { tile -> predicate(civ, tile) }, - cost = { _, _ -> 1f }, - heuristic = { from, to -> from.aerialDistanceTo(to).toFloat() } + cost = { from, to -> cost(civ, from, to) }, + heuristic = { from, to -> heuristic(civ, from, to) } ) while (true) { if (astar.hasEnded()) { @@ -136,4 +138,5 @@ object MapPathing { .reversed() } } + } diff --git a/core/src/com/unciv/ui/screens/worldscreen/worldmap/WorldMapHolder.kt b/core/src/com/unciv/ui/screens/worldscreen/worldmap/WorldMapHolder.kt index 4b26615b03..a731929283 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/worldmap/WorldMapHolder.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/worldmap/WorldMapHolder.kt @@ -468,7 +468,7 @@ class WorldMapHolder( selectedUnit.civ.hasExplored(tile) if (validTile) { - val roadPath: List? = MapPathing.getRoadPath(selectedUnit, selectedUnit.currentTile, tile) + val roadPath: List? = MapPathing.getRoadPath(selectedUnit.civ, selectedUnit.getTile(), tile) launchOnGLThread { if (roadPath == null) { // give the regular tile overlays with no road connection addTileOverlays(tile) diff --git a/core/src/com/unciv/ui/screens/worldscreen/worldmap/WorldMapTileUpdater.kt b/core/src/com/unciv/ui/screens/worldscreen/worldmap/WorldMapTileUpdater.kt index 349d52a8c5..bc417b43cf 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/worldmap/WorldMapTileUpdater.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/worldmap/WorldMapTileUpdater.kt @@ -104,7 +104,7 @@ object WorldMapTileUpdater { if (worldScreen.bottomUnitTable.selectedUnitIsConnectingRoad) { if (unit.currentTile.ruleset.roadImprovement == null) return val validTiles = unit.civ.gameInfo.tileMap.tileList.filter { - MapPathing.isValidRoadPathTile(unit, it) + MapPathing.isValidRoadPathTile(unit.civ, it) } val connectRoadTileOverlayColor = Color.RED for (tile in validTiles) {