diff --git a/core/src/com/unciv/logic/map/mapunit/movement/MovementCost.kt b/core/src/com/unciv/logic/map/mapunit/movement/MovementCost.kt index d221b6f3cf..452d3ccc10 100644 --- a/core/src/com/unciv/logic/map/mapunit/movement/MovementCost.kt +++ b/core/src/com/unciv/logic/map/mapunit/movement/MovementCost.kt @@ -11,12 +11,31 @@ import com.unciv.models.ruleset.unique.UniqueType object MovementCost { + fun getMovementCostBetweenAdjacentTilesEscort( + unit: MapUnit, + from: Tile, + to: Tile, + considerZoneOfControl: Boolean = true, + includeEscortUnit: Boolean = true, + ): Float { + if (includeEscortUnit && unit.isEscorting()) { + return maxOf(getMovementCostBetweenAdjacentTiles(unit, from, to, considerZoneOfControl), + getMovementCostBetweenAdjacentTiles(unit.getOtherEscortUnit()!!, from, to, considerZoneOfControl)) + } else { + return getMovementCostBetweenAdjacentTiles(unit, from, to, considerZoneOfControl) + } + } + // This function is called ALL THE TIME and should be as time-optimal as possible! + /** + * Does not include escort unit + * @return The cost of movment for the unit between two tiles + */ fun getMovementCostBetweenAdjacentTiles( unit: MapUnit, from: Tile, to: Tile, - considerZoneOfControl: Boolean = true + considerZoneOfControl: Boolean = true, ): Float { val civ = unit.civ diff --git a/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt b/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt index 761ac64d5a..32bf71a152 100644 --- a/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt +++ b/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt @@ -31,7 +31,8 @@ class UnitMovement(val unit: MapUnit) { considerZoneOfControl: Boolean = true, tilesToIgnore: HashSet? = null, passThroughCache: HashMap = HashMap(), - movementCostCache: HashMap, Float> = HashMap() + movementCostCache: HashMap, Float> = HashMap(), + includeOtherEscortUnit: Boolean = true ): PathsToTilesWithinTurn { val distanceToTiles = PathsToTilesWithinTurn() @@ -59,19 +60,22 @@ class UnitMovement(val unit: MapUnit) { // cities and units goes kaput. else -> { val key = Pair(tileToCheck, neighbor) - val movementCost = - movementCostCache.getOrPut(key) { - MovementCost.getMovementCostBetweenAdjacentTiles(unit, tileToCheck, neighbor, considerZoneOfControl) - } + val movementCost = movementCostCache.getOrPut(key) { + MovementCost.getMovementCostBetweenAdjacentTilesEscort(unit, tileToCheck, neighbor, considerZoneOfControl, includeOtherEscortUnit) + } distanceToTiles[tileToCheck]!!.totalDistance + movementCost } } if (!distanceToTiles.containsKey(neighbor) || distanceToTiles[neighbor]!!.totalDistance > totalDistanceToTile) { // this is the new best path - if (totalDistanceToTile < unitMovement - Constants.minimumMovementEpsilon) // We can still keep moving from here! + val usableMovement = if (includeOtherEscortUnit && unit.isEscorting()) + minOf(unitMovement, unit.getOtherEscortUnit()!!.currentMovement) + else unitMovement + + if (totalDistanceToTile < usableMovement - Constants.minimumMovementEpsilon) // We can still keep moving from here! updatedTiles += neighbor else - totalDistanceToTile = unitMovement + totalDistanceToTile = usableMovement // In Civ V, you can always travel between adjacent tiles, even if you don't technically // have enough movement points - it simply depletes what you have @@ -707,19 +711,12 @@ class UnitMovement(val unit: MapUnit) { considerZoneOfControl, null, passThroughCache, - movementCostCache + movementCostCache, + includeOtherEscortUnit ) - if (includeOtherEscortUnit) { - // Only save to cache only if we are the original call and not the subsequent escort unit call - pathfindingCache.setDistanceToTiles(considerZoneOfControl, distanceToTiles) - if (unit.isEscorting()) { - // We should only be able to move to tiles that our escort can also move to - val escortDistanceToTiles = unit.getOtherEscortUnit()!!.movement - .getDistanceToTiles(considerZoneOfControl, includeOtherEscortUnit = false) - distanceToTiles.keys.removeAll { !escortDistanceToTiles.containsKey(it) } - } - } + pathfindingCache.setDistanceToTiles(considerZoneOfControl, distanceToTiles) + return distanceToTiles } diff --git a/tests/src/com/unciv/logic/map/UnitFomationTests.kt b/tests/src/com/unciv/logic/map/UnitFomationTests.kt index 6d58975e8d..1ce9146289 100644 --- a/tests/src/com/unciv/logic/map/UnitFomationTests.kt +++ b/tests/src/com/unciv/logic/map/UnitFomationTests.kt @@ -278,4 +278,58 @@ internal class UnitFormationTests { assertTrue(civilianUnit.isEscorting()) assertTrue(TargetHelper.getAttackableEnemies(scout, scout.movement.getDistanceToTiles()).isEmpty()) } + + @Test + fun `test escort path with hills one turn civilian`() { + setUp(3) + val centerTile = testGame.getTile(Vector2(0f,0f)) + val hillTile = testGame.getTile(Vector2(1f,1f)) + val destinationTile = testGame.getTile(Vector2(1f,2f)) + val militaryUnit = testGame.addUnit("Mechanized Infantry", civInfo, centerTile) + val civilianUnit = testGame.addUnit("Worker", civInfo, centerTile) + hillTile.addTerrainFeature("Hill") + destinationTile.addTerrainFeature("Hill") + civilianUnit.startEscorting() + civilianUnit.movement.moveToTile(destinationTile) + assertEquals(civilianUnit.getTile(), destinationTile) + assertEquals(militaryUnit.getTile(), destinationTile) + } + + @Test + fun `test escort path with hills one turn military`() { + setUp(3) + val centerTile = testGame.getTile(Vector2(0f,0f)) + val hillTile = testGame.getTile(Vector2(1f,1f)) + val destinationTile = testGame.getTile(Vector2(1f,2f)) + val militaryUnit = testGame.addUnit("Mechanized Infantry", civInfo, centerTile) + val civilianUnit = testGame.addUnit("Worker", civInfo, centerTile) + hillTile.addTerrainFeature("Hill") + destinationTile.addTerrainFeature("Hill") + militaryUnit.startEscorting() + militaryUnit.movement.moveToTile(destinationTile) + assertEquals(civilianUnit.getTile(), destinationTile) + assertEquals(militaryUnit.getTile(), destinationTile) + } + + @Test + fun `test escort with ignore terrain cost unit`() { + setUp(5) + val centerTile = testGame.getTile(Vector2(0f,0f)) + val marsh = testGame.getTile(Vector2(1f,1f)) + marsh.addTerrainFeature("Marsh") + val jungle = testGame.getTile(Vector2(2f,2f)) + jungle.addTerrainFeature("Jungle") + testGame.getTile(Vector2(3f,3f)).addTerrainFeature("Hill") + testGame.getTile(Vector2(3f,4f)).addTerrainFeature("Hill") + val destinationTile = testGame.getTile(Vector2(4f,5f)) + val tileReached = testGame.getTile(Vector2(1f,2f)); + val militaryUnit = testGame.addUnit("Scout", civInfo, centerTile) + val civilianUnit = testGame.addUnit("Worker", civInfo, centerTile) + militaryUnit.startEscorting() + val shortestPath = militaryUnit.movement.getShortestPath(destinationTile) + assertEquals(true, shortestPath.count() == 3) + assertEquals(false, shortestPath.contains(jungle)) + assertEquals(false, shortestPath.contains(marsh)) + } + }