mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-06 00:09:23 +07:00
Zone of Control (#4085)
* Implemented Zone of Control * 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. * Implemented all missing special ZoC cases As described in: https://forums.civfanatics.com/resources/understanding-the-zone-of-control-vanilla.25582/ * Slightly optimized ZoC logic * Modified the "possible optimization" comment Added the knowledge gained from SomeTroglodyte's tests. * Added "Ignores Zone of Control" unique Implemented the unique and gave it to the Helicopter Gunship.
This commit is contained in:

committed by
GitHub

parent
377cce3348
commit
201648a680
@ -1376,7 +1376,7 @@
|
|||||||
"requiredTech": "Computers",
|
"requiredTech": "Computers",
|
||||||
"requiredResource": "Aluminum",
|
"requiredResource": "Aluminum",
|
||||||
"uniques": ["+[100]% Strength vs [Armored]", "No defensive terrain bonus", "Can move after attacking",
|
"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"
|
"attackSound": "machinegun"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -325,7 +325,11 @@ object Battle {
|
|||||||
// we destroyed an enemy military unit and there was a civilian unit in the same tile as well
|
// 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())
|
if (attackedTile.civilianUnit != null && attackedTile.civilianUnit!!.civInfo != attacker.getCivInfo())
|
||||||
captureCivilianUnit(attacker, MapUnitCombatant(attackedTile.civilianUnit!!))
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +44,9 @@ class MapUnit {
|
|||||||
@Transient
|
@Transient
|
||||||
var ignoresTerrainCost = false
|
var ignoresTerrainCost = false
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
var ignoresZoneOfControl = false
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
var allTilesCosts1 = false
|
var allTilesCosts1 = false
|
||||||
|
|
||||||
@ -196,6 +199,7 @@ class MapUnit {
|
|||||||
allTilesCosts1 = hasUnique("All tiles cost 1 movement") || hasUnique("All tiles costs 1")
|
allTilesCosts1 = hasUnique("All tiles cost 1 movement") || hasUnique("All tiles costs 1")
|
||||||
canPassThroughImpassableTiles = hasUnique("Can pass through impassable tiles")
|
canPassThroughImpassableTiles = hasUnique("Can pass through impassable tiles")
|
||||||
ignoresTerrainCost = hasUnique("Ignores terrain cost")
|
ignoresTerrainCost = hasUnique("Ignores terrain cost")
|
||||||
|
ignoresZoneOfControl = hasUnique("Ignores Zone of Control")
|
||||||
roughTerrainPenalty = hasUnique("Rough terrain penalty")
|
roughTerrainPenalty = hasUnique("Rough terrain penalty")
|
||||||
doubleMovementInCoast = hasUnique("Double movement in coast")
|
doubleMovementInCoast = hasUnique("Double movement in coast")
|
||||||
doubleMovementInForestAndJungle = hasUnique("Double movement rate through Forest and Jungle")
|
doubleMovementInForestAndJungle = hasUnique("Double movement rate through Forest and Jungle")
|
||||||
|
@ -8,12 +8,16 @@ import com.unciv.logic.civilization.CivilizationInfo
|
|||||||
class UnitMovementAlgorithms(val unit:MapUnit) {
|
class UnitMovementAlgorithms(val unit:MapUnit) {
|
||||||
|
|
||||||
// This function is called ALL THE TIME and should be as time-optimal as possible!
|
// 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.baseUnit.isLandUnit())
|
if (from.isLand != to.isLand && unit.baseUnit.isLandUnit())
|
||||||
if (unit.civInfo.nation.disembarkCosts1 && from.isWater && to.isLand) return 1f
|
if (unit.civInfo.nation.disembarkCosts1 && from.isWater && to.isLand) return 1f
|
||||||
else return 100f // this is embarkment or disembarkment, and will take the entire turn
|
else return 100f // this is embarkment or disembarkment, and will take the entire turn
|
||||||
|
|
||||||
|
// 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
|
// land units will still spend all movement points to embark even with this unique
|
||||||
if (unit.allTilesCosts1)
|
if (unit.allTilesCosts1)
|
||||||
return 1f
|
return 1f
|
||||||
@ -58,6 +62,52 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
|
|||||||
return to.getLastTerrain().movementCost.toFloat() + extraCost // no road
|
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. Using a hex-math-based "commonAdjacentTiles"
|
||||||
|
// function is surprisingly less efficient than the current neighbor-intersection approach.
|
||||||
|
// See #4085 for more details.
|
||||||
|
if (from.neighbors.none{
|
||||||
|
(
|
||||||
|
(
|
||||||
|
it.isCityCenter() &&
|
||||||
|
civInfo.isAtWarWith(it.getOwner()!!)
|
||||||
|
)
|
||||||
|
||
|
||||||
|
(
|
||||||
|
it.militaryUnit != null &&
|
||||||
|
civInfo.isAtWarWith(it.militaryUnit!!.civInfo) &&
|
||||||
|
(it.militaryUnit!!.type.isWaterUnit() || (!it.militaryUnit!!.isEmbarked() && unit.type.isLandUnit()))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
&&
|
||||||
|
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)
|
class ParentTileAndTotalDistance(val parentTile: TileInfo, val totalDistance: Float)
|
||||||
|
|
||||||
fun isUnknownTileWeShouldAssumeToBePassable(tileInfo: TileInfo) = !unit.civInfo.exploredTiles.contains(tileInfo.position)
|
fun isUnknownTileWeShouldAssumeToBePassable(tileInfo: TileInfo) = !unit.civInfo.exploredTiles.contains(tileInfo.position)
|
||||||
@ -66,7 +116,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
|
|||||||
* Does not consider if tiles can actually be entered, use canMoveTo for that.
|
* 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
|
* 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()
|
val distanceToTiles = PathsToTilesWithinTurn()
|
||||||
if (unitMovement == 0f) return distanceToTiles
|
if (unitMovement == 0f) return distanceToTiles
|
||||||
|
|
||||||
@ -90,7 +140,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
|
|||||||
// cities and units goes kaput.
|
// cities and units goes kaput.
|
||||||
|
|
||||||
else {
|
else {
|
||||||
val distanceBetweenTiles = getMovementCostBetweenAdjacentTiles(tileToCheck, neighbor, unit.civInfo)
|
val distanceBetweenTiles = getMovementCostBetweenAdjacentTiles(tileToCheck, neighbor, unit.civInfo, considerZoneOfControl)
|
||||||
totalDistanceToTile = distanceToTiles[tileToCheck]!!.totalDistance + distanceBetweenTiles
|
totalDistanceToTile = distanceToTiles[tileToCheck]!!.totalDistance + distanceBetweenTiles
|
||||||
}
|
}
|
||||||
} else totalDistanceToTile = distanceToTiles[tileToCheck]!!.totalDistance + 1f // If we don't know then we just guess it to be 1.
|
} else totalDistanceToTile = distanceToTiles[tileToCheck]!!.totalDistance + 1f // If we don't know then we just guess it to be 1.
|
||||||
@ -323,7 +373,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
|
|||||||
else unit.destroy()
|
else unit.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun moveToTile(destination: TileInfo) {
|
fun moveToTile(destination: TileInfo, considerZoneOfControl: Boolean = true) {
|
||||||
if (destination == unit.getTile()) return // already here!
|
if (destination == unit.getTile()) return // already here!
|
||||||
|
|
||||||
if (unit.baseUnit.movesLikeAirUnits()) { // air units move differently from all other units
|
if (unit.baseUnit.movesLikeAirUnits()) { // air units move differently from all other units
|
||||||
@ -350,7 +400,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val distanceToTiles = getDistanceToTiles()
|
val distanceToTiles = getDistanceToTiles(considerZoneOfControl)
|
||||||
val pathToDestination = distanceToTiles.getPathToTile(destination)
|
val pathToDestination = distanceToTiles.getPathToTile(destination)
|
||||||
val movableTiles = pathToDestination.takeWhile { canPassThrough(it) }
|
val movableTiles = pathToDestination.takeWhile { canPassThrough(it) }
|
||||||
val lastReachableTile = movableTiles.lastOrNull { canMoveTo(it) }
|
val lastReachableTile = movableTiles.lastOrNull { canMoveTo(it) }
|
||||||
@ -509,7 +559,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<TileInfo, ArrayList<TileInfo>> {
|
fun getAerialPathsToCities(): HashMap<TileInfo, ArrayList<TileInfo>> {
|
||||||
var tilesToCheck = ArrayList<TileInfo>()
|
var tilesToCheck = ArrayList<TileInfo>()
|
||||||
|
Reference in New Issue
Block a user