From 04083de7662deabaee6c550db165e8e242d95e6f Mon Sep 17 00:00:00 2001 From: Oskar Niesen Date: Sat, 24 Feb 2024 14:39:04 -0600 Subject: [PATCH] Added unit escorting formation!!! (#11057) * Added escort button * Added basic escort movement * Improved escort movement * Swapping breaks escorting * Added stop escorting button * Added link icon to unit * getDistanceToTiles() now automatically includes escorting * Multi-turn movement with different units works somewhat * Escorting units persist to escort across saves * Escorting units are only idle if their partner unit is idle as well * Fixed multi-turn escort movement where one unit has more movement points left over * Added basic tests * Added a test for formation idle units * Added some basic movement tests * Added some canMoveTo tests * getDistanceToTiles only caches when includeEscort is true * added getDistanceToTiles test * An entire commit to remove one line of white space just for you! And yes, there are no semi-colons; * Added translations * Added more stopEscorting() calls when the unit is removed * Added extra comments and refactoring * Refactored removeAllTilesNotInSet to use a mutableIterator * Refactored code based on review * Refactored removing tiles in PathsToTilesWithinTurn that aren't in another PathsToTilesWithinTurn --- .../jsons/translations/template.properties | 2 + .../com/unciv/logic/map/mapunit/MapUnit.kt | 43 +++- .../map/mapunit/movement/UnitMovement.kt | 59 +++++- core/src/com/unciv/models/UnitAction.kt | 4 + .../unciv/ui/components/widgets/UnitGroup.kt | 1 + .../worldscreen/unit/actions/UnitActions.kt | 33 ++- .../com/unciv/logic/map/UnitFomationTests.kt | 198 ++++++++++++++++++ 7 files changed, 330 insertions(+), 10 deletions(-) create mode 100644 tests/src/com/unciv/logic/map/UnitFomationTests.kt diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 0e23114dd9..c7517786fd 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -1107,6 +1107,8 @@ Sleep = Sleep until healed = Moving = Set up = +Escort formation = +Stop Escort formation = Paradrop = Air Sweep = Add in capital = diff --git a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt index 9f7005b619..990a8280a5 100644 --- a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt +++ b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt @@ -60,6 +60,8 @@ class MapUnit : IsPartOfGameInfoSerialization { // Connect roads implies automated is true. It is specified by the action type. var action: String? = null var automated: Boolean = false + // We can infer who we are escorting based on our tile + var escorting: Boolean = false var automatedRoadConnectionDestination: Vector2? = null var automatedRoadConnectionPath: List? = null @@ -177,6 +179,7 @@ class MapUnit : IsPartOfGameInfoSerialization { toReturn.health = health toReturn.action = action toReturn.automated = automated + toReturn.escorting = escorting toReturn.automatedRoadConnectionDestination = automatedRoadConnectionDestination toReturn.automatedRoadConnectionPath = automatedRoadConnectionPath toReturn.attacksThisTurn = attacksThisTurn @@ -231,13 +234,18 @@ class MapUnit : IsPartOfGameInfoSerialization { fun isPreparingAirSweep() = action == UnitActionType.AirSweep.value fun isSetUpForSiege() = action == UnitActionType.SetUp.value - fun isIdle(): Boolean { + /** + * @param includeOtherEscortUnit determines whether or not this method will also check if it's other escort unit is idle if it has one + * Leave it as default unless you know what [isIdle] does. + */ + fun isIdle(includeOtherEscortUnit: Boolean = true): Boolean { if (currentMovement == 0f) return false val tile = getTile() if (tile.improvementInProgress != null && canBuildImprovement(tile.getTileImprovementInProgress()!!) && !tile.isMarkedForCreatesOneImprovement() ) return false + if (includeOtherEscortUnit && isEscorting() && !getOtherEscortUnit()!!.isIdle(false)) return false return !(isFortified() || isExploring() || isSleeping() || isAutomated() || isMoving()) } @@ -567,6 +575,20 @@ class MapUnit : IsPartOfGameInfoSerialization { power /= 100 return power } + + fun getOtherEscortUnit(): MapUnit? { + if (isCivilian()) return getTile().militaryUnit + if (isMilitary()) return getTile().civilianUnit + return null + } + + fun isEscorting(): Boolean { + if (escorting) { + if (getOtherEscortUnit() != null) return true + escorting = false + } + return false + } fun threatensCiv(civInfo: Civilization): Boolean { if (getTile().getOwner() == civInfo) @@ -687,6 +709,7 @@ class MapUnit : IsPartOfGameInfoSerialization { fun doAction() { if (action == null) return if (currentMovement == 0f) return // We've already done stuff this turn, and can't do any more stuff + if (isEscorting() && getOtherEscortUnit()!!.currentMovement == 0f) return val enemyUnitsInWalkingDistance = movement.getDistanceToTiles().keys .filter { it.militaryUnit != null && civ.isAtWarWith(it.militaryUnit!!.civ) } @@ -730,6 +753,7 @@ class MapUnit : IsPartOfGameInfoSerialization { } fun destroy(destroyTransportedUnit: Boolean = true) { + stopEscorting() val currentPosition = Vector2(getTile().position) civ.attacksSinceTurnStart.addAll(attacksSinceTurnStart.asSequence().map { Civilization.HistoricalAttackMemory(this.name, currentPosition, it) }) currentMovement = 0f @@ -746,6 +770,7 @@ class MapUnit : IsPartOfGameInfoSerialization { } fun gift(recipient: Civilization) { + stopEscorting() civ.units.removeUnit(this) civ.cache.updateViewableTiles() // all transported units should be gift as well @@ -849,6 +874,22 @@ class MapUnit : IsPartOfGameInfoSerialization { moveThroughTile(tile) } + fun startEscorting() { + if (getOtherEscortUnit() != null) { + escorting = true + getOtherEscortUnit()!!.escorting = true + } else { + escorting = false + } + movement.clearPathfindingCache() + } + + fun stopEscorting() { + getOtherEscortUnit()?.escorting = false + escorting = false + movement.clearPathfindingCache() + } + private fun clearEncampment(tile: Tile) { tile.removeImprovement() 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 8aee7b3502..443c1b2cd9 100644 --- a/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt +++ b/core/src/com/unciv/logic/map/mapunit/movement/UnitMovement.kt @@ -238,8 +238,13 @@ class UnitMovement(val unit: MapUnit) { * @return The tile that we reached this turn */ fun headTowards(destination: Tile): Tile { + val escortUnit = if (unit.isEscorting()) unit.getOtherEscortUnit() else null + val startTile = unit.getTile() val destinationTileThisTurn = getTileToMoveToThisTurn(destination) moveToTile(destinationTileThisTurn) + if (startTile != unit.getTile() && escortUnit != null) { + escortUnit.movement.headTowards(unit.getTile()) + } return unit.currentTile } @@ -261,7 +266,11 @@ class UnitMovement(val unit: MapUnit) { return getDistanceToTiles().containsKey(destination) } - fun getReachableTilesInCurrentTurn(): Sequence { + /** + * @param includeOtherEscortUnit determines whether or not this method will also check its the other escort unit if it has one + * Leave it as default unless you know what [getReachableTilesInCurrentTurn] does. + */ + fun getReachableTilesInCurrentTurn(includeOtherEscortUnit: Boolean = true): Sequence { return when { unit.cache.cannotMove -> sequenceOf(unit.getTile()) unit.baseUnit.movesLikeAirUnits() -> @@ -269,8 +278,11 @@ class UnitMovement(val unit: MapUnit) { unit.isPreparingParadrop() -> unit.getTile().getTilesInDistance(unit.cache.paradropRange) .filter { unit.movement.canParadropOn(it) } - else -> - unit.movement.getDistanceToTiles().keys.asSequence() + includeOtherEscortUnit && unit.isEscorting() -> { + val otherUnitTiles = unit.getOtherEscortUnit()!!.movement.getReachableTilesInCurrentTurn(false).toSet() + unit.movement.getDistanceToTiles().filter { otherUnitTiles.contains(it.key) }.keys.asSequence() + } + else -> unit.movement.getDistanceToTiles().keys.asSequence() } } @@ -322,6 +334,7 @@ class UnitMovement(val unit: MapUnit) { * CAN DESTROY THE UNIT. */ fun teleportToClosestMoveableTile() { + unit.stopEscorting() if (unit.isTransported) return // handled when carrying unit is teleported var allowedTile: Tile? = null var distance = 0 @@ -370,6 +383,7 @@ class UnitMovement(val unit: MapUnit) { fun moveToTile(destination: Tile, considerZoneOfControl: Boolean = true) { if (destination == unit.getTile() || unit.isDestroyed) return // already here (or dead)! // Reset closestEnemy chache + val escortUnit = if (unit.isEscorting()) unit.getOtherEscortUnit()!! else null if (unit.baseUnit.movesLikeAirUnits()) { // air units move differently from all other units if (unit.action != UnitActionType.Automate.value) unit.action = null @@ -477,6 +491,10 @@ class UnitMovement(val unit: MapUnit) { payload.isTransported = true // restore the flag to not leave the payload in the city payload.mostRecentMoveType = UnitMovementMemoryType.UnitMoved } + if (escortUnit != null) { + escortUnit.movement.moveToTile(finalTileReached) + unit.startEscorting() // Need to re-apply this + } // Unit maintenance changed if (unit.canGarrison() @@ -498,6 +516,7 @@ class UnitMovement(val unit: MapUnit) { * Precondition: this unit can swap-move to the given tile, as determined by canUnitSwapTo */ fun swapMoveToTile(destination: Tile) { + unit.stopEscorting() val otherUnit = ( if (unit.isCivilian()) destination.civilianUnit @@ -548,8 +567,10 @@ class UnitMovement(val unit: MapUnit) { /** * Designates whether we can enter the tile - without attacking * DOES NOT designate whether we can reach that tile in the current turn + * @param includeOtherEscortUnit determines whether or not this method will also check if the other escort unit [canMoveTo] if it has one. + * Leave it as default unless you know what [canMoveTo] does. */ - fun canMoveTo(tile: Tile, assumeCanPassThrough: Boolean = false, canSwap: Boolean = false): Boolean { + fun canMoveTo(tile: Tile, assumeCanPassThrough: Boolean = false, canSwap: Boolean = false, includeOtherEscortUnit: Boolean = true): Boolean { if (unit.baseUnit.movesLikeAirUnits()) return canAirUnitMoveTo(tile, unit) @@ -560,6 +581,10 @@ class UnitMovement(val unit: MapUnit) { if (isCityCenterCannotEnter(tile)) return false + if (includeOtherEscortUnit && unit.isEscorting() + && !unit.getOtherEscortUnit()!!.movement.canMoveTo(tile, assumeCanPassThrough,canSwap, includeOtherEscortUnit = false)) + return false + return if (unit.isCivilian()) (tile.civilianUnit == null || (canSwap && tile.civilianUnit!!.owner == unit.owner)) && (tile.militaryUnit == null || tile.militaryUnit!!.owner == unit.owner) @@ -600,8 +625,10 @@ class UnitMovement(val unit: MapUnit) { * This is the most called function in the entire game, * so multiple callees of this function have been optimized, * because optimization on this function results in massive benefits! + * @param includeOtherEscortUnit determines whether or not this method will also check if the other escort unit [canPassThrough] if it has one. + * Leave it as default unless you know what [canPassThrough] does. */ - fun canPassThrough(tile: Tile): Boolean { + fun canPassThrough(tile: Tile, includeOtherEscortUnit: Boolean = true): Boolean { if (tile.isImpassible()) { // special exception - ice tiles are technically impassible, but some units can move through them anyway // helicopters can pass through impassable tiles like mountains @@ -649,15 +676,21 @@ class UnitMovement(val unit: MapUnit) { if (unit.civ.isAtWarWith(firstUnit.civ)) return false } - + if (includeOtherEscortUnit && unit.isEscorting() && !unit.getOtherEscortUnit()!!.movement.canPassThrough(tile,false)) + return false return true } + /** + * @param includeOtherEscortUnit determines whether or not this method will also check if the other escort units [getDistanceToTiles] if it has one. + * Leave it as default unless you know what [getDistanceToTiles] does. + */ fun getDistanceToTiles( considerZoneOfControl: Boolean = true, passThroughCache: HashMap = HashMap(), - movementCostCache: HashMap, Float> = HashMap()) + movementCostCache: HashMap, Float> = HashMap(), + includeOtherEscortUnit: Boolean = true) : PathsToTilesWithinTurn { val cacheResults = pathfindingCache.getDistanceToTiles(considerZoneOfControl) if (cacheResults != null) { @@ -671,7 +704,17 @@ class UnitMovement(val unit: MapUnit) { passThroughCache, movementCostCache ) - pathfindingCache.setDistanceToTiles(considerZoneOfControl, distanceToTiles) + + 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.removeIf { !escortDistanceToTiles.containsKey(it) } + } + } return distanceToTiles } diff --git a/core/src/com/unciv/models/UnitAction.kt b/core/src/com/unciv/models/UnitAction.kt index 3fccabd07a..dbae9870bc 100644 --- a/core/src/com/unciv/models/UnitAction.kt +++ b/core/src/com/unciv/models/UnitAction.kt @@ -97,6 +97,10 @@ enum class UnitActionType( /** UI "page" preference, 0-based - Dynamic overrides to this are in `UnitActions.actionTypeToPageGetter` */ val defaultPage: Int ) { + StopEscortFormation("Stop Escort formation", + { ImageGetter.getImage("OtherIcons/Stop") }, false, defaultPage = 1), + EscortFormation("Escort formation", + { ImageGetter.getImage("OtherIcons/Link") }, false, defaultPage = 1), SwapUnits("Swap units", { ImageGetter.getUnitActionPortrait("Swap") }, false, defaultPage = 1), Automate("Automate", diff --git a/core/src/com/unciv/ui/components/widgets/UnitGroup.kt b/core/src/com/unciv/ui/components/widgets/UnitGroup.kt index 5fc6d6fc41..a2ca7318e1 100644 --- a/core/src/com/unciv/ui/components/widgets/UnitGroup.kt +++ b/core/src/com/unciv/ui/components/widgets/UnitGroup.kt @@ -177,6 +177,7 @@ class UnitGroup(val unit: MapUnit, val size: Float) : Group() { private fun getActionImage(): Image? { return when { unit.isSleeping() -> ImageGetter.getImage("UnitActionIcons/Sleep") + unit.isEscorting() -> ImageGetter.getImage("OtherIcons/Link") unit.isMoving() -> ImageGetter.getImage("UnitActionIcons/MoveTo") unit.isExploring() -> ImageGetter.getImage("UnitActionIcons/Explore") unit.getTile().improvementInProgress!=null && unit.canBuildImprovement(unit.getTile().getTileImprovementInProgress()!!) -> diff --git a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt index 228e5471be..bdfb936d2f 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt @@ -158,11 +158,42 @@ object UnitActions { GUI.getMap().setCenterPosition(unit.getMovementDestination().position, true) }) } - + addEscortAction(unit) addSwapAction(unit) addDisbandAction(unit) } + private suspend fun SequenceScope.addEscortAction(unit: MapUnit) { + // Air units cannot escort + if (unit.baseUnit.movesLikeAirUnits()) return + + val worldScreen = GUI.getWorldScreen() + val selectedUnits = worldScreen.bottomUnitTable.selectedUnits + if (selectedUnits.size == 2) { + // We can still create a formation in the case that we have two units selected + // and they are on the same tile. We still have to manualy confirm they are on the same tile here. + val tile = selectedUnits.first().getTile() + if (selectedUnits.last().getTile() != tile) return + if (selectedUnits.any { it.baseUnit.movesLikeAirUnits() }) return + } else if (selectedUnits.size != 1) { + return + } + if (unit.getOtherEscortUnit() == null) return + if (!unit.isEscorting()) { + yield(UnitAction( + type = UnitActionType.EscortFormation, + action = { + unit.startEscorting() + })) + } else { + yield(UnitAction( + type = UnitActionType.StopEscortFormation, + action = { + unit.stopEscorting() + })) + } + } + private suspend fun SequenceScope.addSwapAction(unit: MapUnit) { // Air units cannot swap if (unit.baseUnit.movesLikeAirUnits()) return diff --git a/tests/src/com/unciv/logic/map/UnitFomationTests.kt b/tests/src/com/unciv/logic/map/UnitFomationTests.kt new file mode 100644 index 0000000000..a8fcee4352 --- /dev/null +++ b/tests/src/com/unciv/logic/map/UnitFomationTests.kt @@ -0,0 +1,198 @@ +package com.unciv.logic.map + +import com.badlogic.gdx.math.Vector2 +import com.unciv.Constants +import com.unciv.logic.civilization.Civilization +import com.unciv.testing.GdxTestRunner +import com.unciv.testing.TestGame +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(GdxTestRunner::class) +internal class UnitFormationTests { + private lateinit var civInfo: Civilization + + val testGame = TestGame() + fun setUp(size: Int, baseTerrain: String = Constants.desert) { + testGame.makeHexagonalMap(size) + civInfo = testGame.addCiv() + } + + @Test + fun `basic formation functionality civilian`() { + setUp(1) + val centerTile = testGame.getTile(Vector2(0f,0f)) + val civilianUnit = testGame.addUnit("Worker", civInfo, centerTile) + val militaryUnit = testGame.addUnit("Warrior", civInfo, centerTile) + assertTrue(civilianUnit.getOtherEscortUnit() != null) + assertFalse(civilianUnit.isEscorting()) + civilianUnit.startEscorting() + assertTrue(civilianUnit.isEscorting()) + assertTrue(militaryUnit.isEscorting()) + assertTrue(civilianUnit.getOtherEscortUnit() != null) + civilianUnit.stopEscorting() + assertFalse(civilianUnit.isEscorting()) + assertFalse(militaryUnit.isEscorting()) + assertTrue(civilianUnit.getOtherEscortUnit() != null) + } + + @Test + fun `basic formation functionality military`() { + setUp(1) + val centerTile = testGame.getTile(Vector2(0f,0f)) + val civilianUnit = testGame.addUnit("Worker", civInfo, centerTile) + val militaryUnit = testGame.addUnit("Warrior", civInfo, centerTile) + assertTrue(militaryUnit.getOtherEscortUnit() != null) + assertFalse(militaryUnit.isEscorting()) + militaryUnit.startEscorting() + assertTrue(militaryUnit.isEscorting()) + assertTrue(civilianUnit.isEscorting()) + assertTrue(militaryUnit.getOtherEscortUnit() != null) + militaryUnit.stopEscorting() + assertFalse(militaryUnit.isEscorting()) + assertFalse(civilianUnit.isEscorting()) + assertTrue(militaryUnit.getOtherEscortUnit() != null) + } + + @Test + fun `basic formation not available functionality`() { + setUp(1) + val centerTile = testGame.getTile(Vector2(0f,0f)) + val civilianUnit = testGame.addUnit("Worker", civInfo, centerTile) + assertFalse(civilianUnit.getOtherEscortUnit() != null) + assertFalse(civilianUnit.isEscorting()) + civilianUnit.startEscorting() + assertFalse(civilianUnit.isEscorting()) + civilianUnit.destroy() + val militaryUnit = testGame.addUnit("Warrior", civInfo, centerTile) + assertFalse(militaryUnit.getOtherEscortUnit() != null) + assertFalse(militaryUnit.isEscorting()) + militaryUnit.startEscorting() + assertFalse(militaryUnit.isEscorting()) + } + + @Test + fun `formation idle units`() { + setUp(1) + val centerTile = testGame.getTile(Vector2(0f,0f)) + val civilianUnit = testGame.addUnit("Worker", civInfo, centerTile) + val militaryUnit = testGame.addUnit("Warrior", civInfo, centerTile) + civilianUnit.startEscorting() + assertTrue(civilianUnit.isIdle()) + assertTrue(militaryUnit.isIdle()) + civilianUnit.currentMovement = 0f + assertFalse(civilianUnit.isIdle()) + assertFalse(militaryUnit.isIdle()) + civilianUnit.currentMovement = 2f + civInfo.tech.techsResearched.add(testGame.ruleset.tileImprovements["Farm"]!!.techRequired!!) + centerTile.startWorkingOnImprovement(testGame.ruleset.tileImprovements["Farm"]!!, civInfo, civilianUnit) + assertFalse(civilianUnit.isIdle()) + assertFalse(militaryUnit.isIdle()) + } + + @Test + fun `formation movement` () { + setUp(3) + val centerTile = testGame.getTile(Vector2(0f,0f)) + val civilianUnit = testGame.addUnit("Worker", civInfo, centerTile) + val militaryUnit = testGame.addUnit("Warrior", civInfo, centerTile) + civilianUnit.startEscorting() + val targetTile = testGame.getTile(Vector2(0f,2f)) + civilianUnit.movement.moveToTile(targetTile) + assert(civilianUnit.getTile() == targetTile) + assert(militaryUnit.getTile() == targetTile) + assertTrue(civilianUnit.isEscorting()) + assertTrue(militaryUnit.isEscorting()) + } + + @Test + fun `stop formation movement` () { + setUp(3) + val centerTile = testGame.getTile(Vector2(0f,0f)) + val civilianUnit = testGame.addUnit("Worker", civInfo, centerTile) + val militaryUnit = testGame.addUnit("Warrior", civInfo, centerTile) + civilianUnit.startEscorting() + civilianUnit.stopEscorting() + val targetTile = testGame.getTile(Vector2(0f,2f)) + civilianUnit.movement.moveToTile(targetTile) + assert(civilianUnit.getTile() == targetTile) + assert(militaryUnit.getTile() == centerTile) + assertFalse(civilianUnit.isEscorting()) + assertFalse(militaryUnit.isEscorting()) + } + + @Test + fun `formation canMoveTo` () { + setUp(3) + val centerTile = testGame.getTile(Vector2(0f,0f)) + val civilianUnit = testGame.addUnit("Worker", civInfo, centerTile) + val militaryUnit = testGame.addUnit("Warrior", civInfo, centerTile) + val targetTile = testGame.getTile(Vector2(0f,2f)) + val blockingCivilianUnit = testGame.addUnit("Worker", civInfo, targetTile) + assertFalse(civilianUnit.movement.canMoveTo(targetTile)) + assertTrue(militaryUnit.movement.canMoveTo(targetTile)) + civilianUnit.startEscorting() + assertFalse(militaryUnit.movement.canMoveTo(targetTile)) + } + + @Test + fun `formation canMoveTo water` () { + setUp(3, "Ocean") + val centerTile = testGame.getTile(Vector2(0f,0f)) + centerTile.baseTerrain = "Coast" + centerTile.isWater = true + centerTile.isLand = false + civInfo.tech.embarkedUnitsCanEnterOcean = true + civInfo.tech.addTechnology("Astronomy") + val civilianUnit = testGame.addUnit("Work Boats", civInfo, centerTile) // Can enter ocean + val militaryUnit = testGame.addUnit("Trireme", civInfo, centerTile) // Can't enter ocean + val targetTile = testGame.getTile(Vector2(0f,1f)) + targetTile.isWater = true + targetTile.isLand = false + targetTile.isOcean = true + assertFalse(militaryUnit.movement.canMoveTo(targetTile)) + assertTrue(civilianUnit.movement.canMoveTo(targetTile)) + civilianUnit.startEscorting() + assertFalse(civilianUnit.movement.canMoveTo(targetTile)) + } + + + @Test + fun `formation head towards with faster units` () { + setUp(5) + val centerTile = testGame.getTile(Vector2(0f,0f)) + val civilianUnit = testGame.addUnit("Worker", civInfo, centerTile) + val militaryUnit = testGame.addUnit("Horseman", civInfo, centerTile) // 4 movement + civilianUnit.startEscorting() + val targetTile = testGame.getTile(Vector2(0f,4f)) + val excpectedTile = testGame.getTile(Vector2(0f,2f)) + militaryUnit.movement.headTowards(targetTile) + assert(civilianUnit.getTile() == excpectedTile) + assert(militaryUnit.getTile() == excpectedTile) + assertTrue(civilianUnit.isEscorting()) + assertTrue(militaryUnit.isEscorting()) + assertTrue(militaryUnit.currentMovement == 2f) + assertFalse("The unit should not be idle if it's escort has no movement points",militaryUnit.isIdle()) + } + + @Test + fun `getDistanceToTiles when in formation`() { + setUp(5) + val centerTile = testGame.getTile(Vector2(0f,0f)) + val civilianUnit = testGame.addUnit("Worker", civInfo, centerTile) + val militaryUnit = testGame.addUnit("Horseman", civInfo, centerTile) // 4 movement + civilianUnit.startEscorting() + var civilianDistanceToTiles = civilianUnit.movement.getDistanceToTiles() + assertFalse(militaryUnit.movement.getDistanceToTiles().any { !civilianDistanceToTiles.contains(it.key) }) + + // Test again with caching + civilianUnit.stopEscorting() + militaryUnit.movement.getDistanceToTiles() + civilianUnit.movement.getDistanceToTiles() + civilianUnit.startEscorting() + civilianDistanceToTiles = civilianUnit.movement.getDistanceToTiles() + assertFalse(militaryUnit.movement.getDistanceToTiles().any { !civilianDistanceToTiles.contains(it.key) }) + } +}