From 057e218e0526b62f8ab7a532c9b2f3a6fec442c9 Mon Sep 17 00:00:00 2001 From: Arthur van der Staaij Date: Wed, 9 Jun 2021 02:00:12 +0200 Subject: [PATCH 1/6] Implemented Zone of Control --- core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt index 9df5e4b67b903..309271774ff1b 100644 --- a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt +++ b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt @@ -13,6 +13,14 @@ class UnitMovementAlgorithms(val unit:MapUnit) { if (from.isLand != to.isLand && !unit.civInfo.nation.embarkDisembarkCosts1 && unit.type.isLandUnit()) return 100f // this is embarkment or disembarkment, and will take the entire turn + // Zone of Control: https://civilization.fandom.com/wiki/Zone_of_control_(Civ5) + // If next to a tile with an enemy military unit, moving to another tile next to that + // military unit costs all movement points, except when moving into or out of a city center. + // We only need to check the two shared neighbors of [from] and [to]: the way of getting + // these two tiles can perhaps be optimized. + if (!from.isCityCenter() && !to.isCityCenter() && from.neighbors.any{to.neighbors.contains(it) && it.militaryUnit != null && civInfo.isAtWarWith(it.militaryUnit!!.civInfo)}) + return 100f + // land units will still spend all movement points to embark even with this unique if (unit.allTilesCosts1) return 1f From 5837d3ebfaf487d77c5f5396c2322c76dd0c8ec0 Mon Sep 17 00:00:00 2001 From: Arthur van der Staaij Date: Wed, 9 Jun 2021 03:16:13 +0200 Subject: [PATCH 2/6] Implemented "move after attacking" ZoC exception Units that can move after attacking are not affected by zone of control if they move because of defeating a unit. --- core/src/com/unciv/logic/battle/Battle.kt | 6 ++++- .../unciv/logic/map/UnitMovementAlgorithms.kt | 22 +++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index c5292373afdaf..438bcd8658c3c 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -225,7 +225,11 @@ object Battle { // we destroyed an enemy military unit and there was a civilian unit in the same tile as well if (attackedTile.civilianUnit != null && attackedTile.civilianUnit!!.civInfo != attacker.getCivInfo()) captureCivilianUnit(attacker, MapUnitCombatant(attackedTile.civilianUnit!!)) - attacker.unit.movement.moveToTile(attackedTile) + // Units that can move after attacking are not affected by zone of control if the + // movement is caused by killing a unit. Effectively, this means that attack movements + // are exempt from zone of control, since units that cannot move after attacking already + // lose all remaining movement points anyway. + attacker.unit.movement.moveToTile(attackedTile, considerZoneOfControl = false) } } diff --git a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt index 309271774ff1b..a7e9aa59e5281 100644 --- a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt +++ b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt @@ -8,7 +8,7 @@ import com.unciv.logic.civilization.CivilizationInfo class UnitMovementAlgorithms(val unit:MapUnit) { // This function is called ALL THE TIME and should be as time-optimal as possible! - fun getMovementCostBetweenAdjacentTiles(from: TileInfo, to: TileInfo, civInfo: CivilizationInfo): Float { + fun getMovementCostBetweenAdjacentTiles(from: TileInfo, to: TileInfo, civInfo: CivilizationInfo, considerZoneOfControl: Boolean = true): Float { if (from.isLand != to.isLand && !unit.civInfo.nation.embarkDisembarkCosts1 && unit.type.isLandUnit()) return 100f // this is embarkment or disembarkment, and will take the entire turn @@ -16,9 +16,17 @@ class UnitMovementAlgorithms(val unit:MapUnit) { // Zone of Control: https://civilization.fandom.com/wiki/Zone_of_control_(Civ5) // If next to a tile with an enemy military unit, moving to another tile next to that // military unit costs all movement points, except when moving into or out of a city center. + // There is another exception: units that can move after attacking are not affected by zone + // of control if the movement is caused by killing a unit. This case is handled in the + // movement-after-attacking code by using the [considerZoneOfControl] parameter. // We only need to check the two shared neighbors of [from] and [to]: the way of getting // these two tiles can perhaps be optimized. - if (!from.isCityCenter() && !to.isCityCenter() && from.neighbors.any{to.neighbors.contains(it) && it.militaryUnit != null && civInfo.isAtWarWith(it.militaryUnit!!.civInfo)}) + if ( + considerZoneOfControl && + !from.isCityCenter() && + !to.isCityCenter() && + from.neighbors.any{to.neighbors.contains(it) && it.militaryUnit != null && civInfo.isAtWarWith(it.militaryUnit!!.civInfo)} + ) return 100f // land units will still spend all movement points to embark even with this unique @@ -73,7 +81,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) { * Does not consider if tiles can actually be entered, use canMoveTo for that. * If a tile can be reached within the turn, but it cannot be passed through, the total distance to it is set to unitMovement */ - fun getDistanceToTilesWithinTurn(origin: Vector2, unitMovement: Float): PathsToTilesWithinTurn { + fun getDistanceToTilesWithinTurn(origin: Vector2, unitMovement: Float, considerZoneOfControl: Boolean = true): PathsToTilesWithinTurn { val distanceToTiles = PathsToTilesWithinTurn() if (unitMovement == 0f) return distanceToTiles @@ -97,7 +105,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) { // cities and units goes kaput. else { - val distanceBetweenTiles = getMovementCostBetweenAdjacentTiles(tileToCheck, neighbor, unit.civInfo) + val distanceBetweenTiles = getMovementCostBetweenAdjacentTiles(tileToCheck, neighbor, unit.civInfo, considerZoneOfControl) totalDistanceToTile = distanceToTiles[tileToCheck]!!.totalDistance + distanceBetweenTiles } } else totalDistanceToTile = distanceToTiles[tileToCheck]!!.totalDistance + 1f // If we don't know then we just guess it to be 1. @@ -316,7 +324,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) { unit.putInTile(allowedTile) } - fun moveToTile(destination: TileInfo) { + fun moveToTile(destination: TileInfo, considerZoneOfControl: Boolean = true) { if (destination == unit.getTile()) return // already here! if (unit.type.isAirUnit()) { // air units move differently from all other units @@ -343,7 +351,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) { return } - val distanceToTiles = getDistanceToTiles() + val distanceToTiles = getDistanceToTiles(considerZoneOfControl) val pathToDestination = distanceToTiles.getPathToTile(destination) val movableTiles = pathToDestination.takeWhile { canPassThrough(it) } val lastReachableTile = movableTiles.lastOrNull { canMoveTo(it) } @@ -493,7 +501,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) { } - fun getDistanceToTiles(): PathsToTilesWithinTurn = getDistanceToTilesWithinTurn(unit.currentTile.position, unit.currentMovement) + fun getDistanceToTiles(considerZoneOfControl: Boolean = true): PathsToTilesWithinTurn = getDistanceToTilesWithinTurn(unit.currentTile.position, unit.currentMovement, considerZoneOfControl) fun getAerialPathsToCities(): HashMap> { var tilesToCheck = ArrayList() From 88ee1774ac6b5579b02ffddf19a51bfd499a930b Mon Sep 17 00:00:00 2001 From: Arthur van der Staaij Date: Sun, 13 Jun 2021 04:35:25 +0200 Subject: [PATCH 3/6] Implemented all missing special ZoC cases As described in: https://forums.civfanatics.com/resources/understanding-the-zone-of-control-vanilla.25582/ --- .../unciv/logic/map/UnitMovementAlgorithms.kt | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt index a7e9aa59e5281..cb3e9699422db 100644 --- a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt +++ b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt @@ -13,20 +13,8 @@ class UnitMovementAlgorithms(val unit:MapUnit) { if (from.isLand != to.isLand && !unit.civInfo.nation.embarkDisembarkCosts1 && unit.type.isLandUnit()) return 100f // this is embarkment or disembarkment, and will take the entire turn - // Zone of Control: https://civilization.fandom.com/wiki/Zone_of_control_(Civ5) - // If next to a tile with an enemy military unit, moving to another tile next to that - // military unit costs all movement points, except when moving into or out of a city center. - // There is another exception: units that can move after attacking are not affected by zone - // of control if the movement is caused by killing a unit. This case is handled in the - // movement-after-attacking code by using the [considerZoneOfControl] parameter. - // We only need to check the two shared neighbors of [from] and [to]: the way of getting - // these two tiles can perhaps be optimized. - if ( - considerZoneOfControl && - !from.isCityCenter() && - !to.isCityCenter() && - from.neighbors.any{to.neighbors.contains(it) && it.militaryUnit != null && civInfo.isAtWarWith(it.militaryUnit!!.civInfo)} - ) + // If the movement is affected by a Zone of Control, all movement points are expended + if (considerZoneOfControl && isMovementAffectedByZoneOfControl(from, to, civInfo)) return 100f // land units will still spend all movement points to embark even with this unique @@ -73,6 +61,38 @@ class UnitMovementAlgorithms(val unit:MapUnit) { return to.getLastTerrain().movementCost.toFloat() + extraCost // no road } + /** Returns whether the movement between the adjacent tiles [from] and [to] is affected by Zone of Control */ + private fun isMovementAffectedByZoneOfControl(from: TileInfo, to: TileInfo, civInfo: CivilizationInfo): Boolean { + // Sources: + // - https://civilization.fandom.com/wiki/Zone_of_control_(Civ5) + // - https://forums.civfanatics.com/resources/understanding-the-zone-of-control-vanilla.25582/ + // + // Enemy military units exert a Zone of Control over the tiles surrounding them. Moving from + // one tile in the ZoC of an enemy unit to another tile in the same unit's ZoC expends all + // movement points. Land units only exert a ZoC against land units. Sea units exert a ZoC + // against both land and sea units. Cities exert a ZoC as well, and it also affects both + // land and sea units. Embarked land units do not exert a ZoC. Finally, units that can move + // after attacking are not affected by zone of control if the movement is caused by killing + // a unit. This last case is handled in the movement-after-attacking code instead of here. + // + // We only need to check the two shared neighbors of [from] and [to]: the way of getting + // these two tiles can perhaps be optimized. + return from.neighbors.any{ + to.neighbors.contains(it) && ( + ( + it.isCityCenter() && + civInfo.isAtWarWith(it.getOwner()!!) + ) + || + ( + it.militaryUnit != null && + civInfo.isAtWarWith(it.militaryUnit!!.civInfo) && + (it.militaryUnit!!.type.isWaterUnit() || (!it.militaryUnit!!.isEmbarked() && unit.type.isLandUnit())) + ) + ) + } + } + class ParentTileAndTotalDistance(val parentTile: TileInfo, val totalDistance: Float) fun isUnknownTileWeShouldAssumeToBePassable(tileInfo: TileInfo) = !unit.civInfo.exploredTiles.contains(tileInfo.position) From 3f868945f0e9193933cff650897b10ae50f6c19e Mon Sep 17 00:00:00 2001 From: Arthur van der Staaij Date: Sun, 13 Jun 2021 20:28:07 +0200 Subject: [PATCH 4/6] Slightly optimized ZoC logic --- core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt index cb3e9699422db..d83ed53f938df 100644 --- a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt +++ b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt @@ -78,7 +78,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) { // We only need to check the two shared neighbors of [from] and [to]: the way of getting // these two tiles can perhaps be optimized. return from.neighbors.any{ - to.neighbors.contains(it) && ( + ( ( it.isCityCenter() && civInfo.isAtWarWith(it.getOwner()!!) @@ -90,6 +90,8 @@ class UnitMovementAlgorithms(val unit:MapUnit) { (it.militaryUnit!!.type.isWaterUnit() || (!it.militaryUnit!!.isEmbarked() && unit.type.isLandUnit())) ) ) + && + to.neighbors.contains(it) } } From ff104c5a22f354eb64740f220ae83cc6633e1c24 Mon Sep 17 00:00:00 2001 From: Arthur van der Staaij Date: Fri, 13 Aug 2021 02:31:43 +0200 Subject: [PATCH 5/6] Modified the "possible optimization" comment Added the knowledge gained from SomeTroglodyte's tests. --- core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt index d83ed53f938df..53394fd9ed73e 100644 --- a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt +++ b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt @@ -76,7 +76,9 @@ class UnitMovementAlgorithms(val unit:MapUnit) { // a unit. This last case is handled in the movement-after-attacking code instead of here. // // We only need to check the two shared neighbors of [from] and [to]: the way of getting - // these two tiles can perhaps be optimized. + // these two tiles can perhaps be optimized. Using a hex-math-based "commonAdjacentTiles" + // function is surprisingly less efficient than the current neighbor-intersection approach. + // See #4085 for more details. return from.neighbors.any{ ( ( From 8721f3e91200bbe409a3067e0178daebdfa785c2 Mon Sep 17 00:00:00 2001 From: Arthur van der Staaij Date: Sun, 15 Aug 2021 17:25:55 +0200 Subject: [PATCH 6/6] Added "Ignores Zone of Control" unique Implemented the unique and gave it to the Helicopter Gunship. --- android/assets/jsons/Civ V - Vanilla/Units.json | 2 +- core/src/com/unciv/logic/map/MapUnit.kt | 4 ++++ .../unciv/logic/map/UnitMovementAlgorithms.kt | 16 +++++++++++++--- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/android/assets/jsons/Civ V - Vanilla/Units.json b/android/assets/jsons/Civ V - Vanilla/Units.json index 068f706674f37..5a56680c3d032 100644 --- a/android/assets/jsons/Civ V - Vanilla/Units.json +++ b/android/assets/jsons/Civ V - Vanilla/Units.json @@ -1376,7 +1376,7 @@ "requiredTech": "Computers", "requiredResource": "Aluminum", "uniques": ["+[100]% Strength vs [Armored]", "No defensive terrain bonus", "Can move after attacking", - "All tiles cost 1 movement", "Unable to capture cities"], + "All tiles cost 1 movement", "Ignores Zone of Control", "Unable to capture cities"], "attackSound": "machinegun" }, diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index 787bf89c9f7fa..60a18335dfe4b 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -44,6 +44,9 @@ class MapUnit { @Transient var ignoresTerrainCost = false + @Transient + var ignoresZoneOfControl = false + @Transient var allTilesCosts1 = false @@ -191,6 +194,7 @@ class MapUnit { allTilesCosts1 = hasUnique("All tiles cost 1 movement") || hasUnique("All tiles costs 1") canPassThroughImpassableTiles = hasUnique("Can pass through impassable tiles") ignoresTerrainCost = hasUnique("Ignores terrain cost") + ignoresZoneOfControl = hasUnique("Ignores Zone of Control") roughTerrainPenalty = hasUnique("Rough terrain penalty") doubleMovementInCoast = hasUnique("Double movement in coast") doubleMovementInForestAndJungle = diff --git a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt index 44cc51cc8b068..e855a7fd2d974 100644 --- a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt +++ b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt @@ -75,12 +75,12 @@ class UnitMovementAlgorithms(val unit:MapUnit) { // land and sea units. Embarked land units do not exert a ZoC. Finally, units that can move // after attacking are not affected by zone of control if the movement is caused by killing // a unit. This last case is handled in the movement-after-attacking code instead of here. - // + // We only need to check the two shared neighbors of [from] and [to]: the way of getting // these two tiles can perhaps be optimized. Using a hex-math-based "commonAdjacentTiles" // function is surprisingly less efficient than the current neighbor-intersection approach. // See #4085 for more details. - return from.neighbors.any{ + if (from.neighbors.none{ ( ( it.isCityCenter() && @@ -95,7 +95,17 @@ class UnitMovementAlgorithms(val unit:MapUnit) { ) && to.neighbors.contains(it) - } + }) + return false + + // Even though this is a very fast check, we perform it last. This is because very few units + // ignore zone of control, so the previous check has a much higher chance of yielding an + // early "false". If this function is going to return "true", the order doesn't matter + // anyway. + if (unit.ignoresZoneOfControl) + return false + + return true } class ParentTileAndTotalDistance(val parentTile: TileInfo, val totalDistance: Float)