mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-28 22:37:02 -04: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:
parent
377cce3348
commit
201648a680
@ -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"
|
||||
},
|
||||
|
||||
|
@ -325,7 +325,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,9 @@ class MapUnit {
|
||||
@Transient
|
||||
var ignoresTerrainCost = false
|
||||
|
||||
@Transient
|
||||
var ignoresZoneOfControl = false
|
||||
|
||||
@Transient
|
||||
var allTilesCosts1 = false
|
||||
|
||||
@ -196,6 +199,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 = hasUnique("Double movement rate through Forest and Jungle")
|
||||
|
@ -8,12 +8,16 @@ 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.baseUnit.isLandUnit())
|
||||
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
|
||||
|
||||
// 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
|
||||
if (unit.allTilesCosts1)
|
||||
return 1f
|
||||
@ -58,6 +62,52 @@ 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. 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)
|
||||
|
||||
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.
|
||||
* 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
|
||||
|
||||
@ -90,7 +140,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.
|
||||
@ -323,7 +373,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
|
||||
else unit.destroy()
|
||||
}
|
||||
|
||||
fun moveToTile(destination: TileInfo) {
|
||||
fun moveToTile(destination: TileInfo, considerZoneOfControl: Boolean = true) {
|
||||
if (destination == unit.getTile()) return // already here!
|
||||
|
||||
if (unit.baseUnit.movesLikeAirUnits()) { // air units move differently from all other units
|
||||
@ -350,7 +400,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) }
|
||||
@ -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>> {
|
||||
var tilesToCheck = ArrayList<TileInfo>()
|
||||
|
Loading…
x
Reference in New Issue
Block a user