Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zone of Control #4085

Merged
merged 7 commits into from
Aug 15, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion core/src/com/unciv/logic/battle/Battle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
48 changes: 42 additions & 6 deletions core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ 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

// 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
Expand Down Expand Up @@ -57,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) && (
avdstaaij marked this conversation as resolved.
Show resolved Hide resolved
(
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)
Expand All @@ -65,7 +101,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

Expand All @@ -89,7 +125,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.
Expand Down Expand Up @@ -308,7 +344,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
Expand All @@ -335,7 +371,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) }
Expand Down Expand Up @@ -485,7 +521,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>()
Expand Down