mirror of
https://github.com/yairm210/Unciv.git
synced 2025-02-11 11:28:03 +07:00
chore: Split 'head towards enemy city' into subfunctions
Quite complex for a seemingly simple task, that's why it gets its own object
This commit is contained in:
parent
7503493d7d
commit
f812c828e5
@ -0,0 +1,145 @@
|
||||
package com.unciv.logic.automation.unit
|
||||
|
||||
import com.unciv.logic.automation.civilization.NextTurnAutomation
|
||||
import com.unciv.logic.battle.BattleDamage
|
||||
import com.unciv.logic.battle.CityCombatant
|
||||
import com.unciv.logic.battle.MapUnitCombatant
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.logic.map.mapunit.movement.PathsToTilesWithinTurn
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
|
||||
object HeadTowardsEnemyCityAutomation {
|
||||
|
||||
/** @returns whether the unit has taken this action */
|
||||
fun tryHeadTowardsEnemyCity(unit: MapUnit): Boolean {
|
||||
if (unit.civ.cities.isEmpty()) return false
|
||||
|
||||
// only focus on *attacking* 1 enemy at a time otherwise you'll lose on both fronts
|
||||
val closestReachableEnemyCity = getEnemyCitiesByPriority(unit)
|
||||
.firstOrNull { unit.movement.canReach(it.getCenterTile()) }
|
||||
?: return false // No enemy city reachable
|
||||
|
||||
return headTowardsEnemyCity(
|
||||
unit,
|
||||
closestReachableEnemyCity.getCenterTile(),
|
||||
// This should be cached after the `canReach` call above.
|
||||
unit.movement.getShortestPath(closestReachableEnemyCity.getCenterTile())
|
||||
)
|
||||
}
|
||||
|
||||
private fun getEnemyCitiesByPriority(unit: MapUnit):Sequence<City>{
|
||||
val enemies = unit.civ.getKnownCivs()
|
||||
.filter { unit.civ.isAtWarWith(it) && it.cities.isNotEmpty() }
|
||||
|
||||
val closestEnemyCity = enemies
|
||||
.mapNotNull { NextTurnAutomation.getClosestCities(unit.civ, it) }
|
||||
.minByOrNull { it.aerialDistance }?.city2
|
||||
?: return emptySequence() // no attackable cities found
|
||||
|
||||
// Our main attack target is the closest city, but we're fine with deviating from that a bit
|
||||
var enemyCitiesByPriority = closestEnemyCity.civ.cities
|
||||
.associateWith { it.getCenterTile().aerialDistanceTo(closestEnemyCity.getCenterTile()) }
|
||||
.asSequence().filterNot { it.value > 10 } // anything 10 tiles away from the target is irrelevant
|
||||
.sortedBy { it.value }.map { it.key } // sort the list by closeness to target - least is best!
|
||||
|
||||
if (unit.baseUnit.isRanged()) // ranged units don't harm capturable cities, waste of a turn
|
||||
enemyCitiesByPriority = enemyCitiesByPriority.filterNot { it.health == 1 }
|
||||
|
||||
return enemyCitiesByPriority
|
||||
}
|
||||
|
||||
|
||||
private const val maxDistanceFromCityToConsiderForLandingArea = 5
|
||||
private const val minDistanceFromCityToConsiderForLandingArea = 3
|
||||
|
||||
/** @returns whether the unit has taken this action */
|
||||
fun headTowardsEnemyCity(
|
||||
unit: MapUnit,
|
||||
closestReachableEnemyCity: Tile,
|
||||
shortestPath: List<Tile>
|
||||
): Boolean {
|
||||
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
||||
|
||||
val unitRange = unit.getRange()
|
||||
if (unitRange > 2) { // long-ranged unit, should never be in a bombardable position
|
||||
return headTowardsEnemyCityLongRange(closestReachableEnemyCity, unitDistanceToTiles, unitRange, unit)
|
||||
}
|
||||
|
||||
val nextTileInPath = shortestPath[0]
|
||||
|
||||
// None of the stuff below is relevant if we're still quite far away from the city, so we
|
||||
// short-circuit here for performance reasons.
|
||||
if (unit.currentTile.aerialDistanceTo(closestReachableEnemyCity) > maxDistanceFromCityToConsiderForLandingArea
|
||||
// Even in the worst case of only being able to move 1 tile per turn, we would still
|
||||
// not overshoot.
|
||||
&& shortestPath.size > minDistanceFromCityToConsiderForLandingArea ) {
|
||||
unit.movement.moveToTile(nextTileInPath)
|
||||
return true
|
||||
}
|
||||
|
||||
val ourUnitsAroundEnemyCity = closestReachableEnemyCity.getTilesInDistance(6)
|
||||
.flatMap { it.getUnits() }
|
||||
.filter { it.isMilitary() && it.civ == unit.civ }
|
||||
|
||||
val city = closestReachableEnemyCity.getCity()!!
|
||||
|
||||
if (cannotTakeCitySoon(ourUnitsAroundEnemyCity, city)){
|
||||
return headToLandingGrounds(closestReachableEnemyCity, unit)
|
||||
}
|
||||
|
||||
unit.movement.moveToTile(nextTileInPath) // go for it!
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/** Cannot take within 5 turns */
|
||||
private fun cannotTakeCitySoon(
|
||||
ourUnitsAroundEnemyCity: Sequence<MapUnit>,
|
||||
city: City
|
||||
): Boolean {
|
||||
val cityCombatant = CityCombatant(city)
|
||||
val expectedDamagePerTurn = ourUnitsAroundEnemyCity
|
||||
.sumOf { BattleDamage.calculateDamageToDefender(MapUnitCombatant(it), cityCombatant) }
|
||||
|
||||
val cityHealingPerTurn = 20
|
||||
return expectedDamagePerTurn < city.health && // Cannot take immediately
|
||||
(expectedDamagePerTurn <= cityHealingPerTurn // No lasting damage
|
||||
|| city.health / (expectedDamagePerTurn - cityHealingPerTurn) > 5) // Can damage, but will take more than 5 turns
|
||||
}
|
||||
|
||||
private fun headToLandingGrounds(closestReachableEnemyCity: Tile, unit: MapUnit): Boolean {
|
||||
// don't head straight to the city, try to head to landing grounds -
|
||||
// this is against tha AI's brilliant plan of having everyone embarked and attacking via sea when unnecessary.
|
||||
val tileToHeadTo = closestReachableEnemyCity.getTilesInDistanceRange(minDistanceFromCityToConsiderForLandingArea..maxDistanceFromCityToConsiderForLandingArea)
|
||||
.filter { it.isLand && unit.getDamageFromTerrain(it) <= 0 } // Don't head for hurty terrain
|
||||
.sortedBy { it.aerialDistanceTo(unit.currentTile) }
|
||||
.firstOrNull { (unit.movement.canMoveTo(it) || it == unit.currentTile) && unit.movement.canReach(it) }
|
||||
|
||||
if (tileToHeadTo != null) { // no need to worry, keep going as the movement alg. says
|
||||
unit.movement.headTowards(tileToHeadTo)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun headTowardsEnemyCityLongRange(
|
||||
closestReachableEnemyCity: Tile,
|
||||
unitDistanceToTiles: PathsToTilesWithinTurn,
|
||||
unitRange: Int,
|
||||
unit: MapUnit
|
||||
): Boolean {
|
||||
val tilesInBombardRange = closestReachableEnemyCity.getTilesInDistance(2).toSet()
|
||||
val tileToMoveTo =
|
||||
unitDistanceToTiles.asSequence()
|
||||
.filter {
|
||||
it.key.aerialDistanceTo(closestReachableEnemyCity) <=
|
||||
unitRange && it.key !in tilesInBombardRange
|
||||
&& unit.getDamageFromTerrain(it.key) <= 0 // Don't set up on a mountain
|
||||
}
|
||||
.minByOrNull { it.value.totalDistance }?.key ?: return false // return false if no tile to move to
|
||||
|
||||
// move into position far away enough that the bombard doesn't hurt
|
||||
unit.movement.headTowards(tileToMoveTo)
|
||||
return true
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ package com.unciv.logic.automation.unit
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.automation.Automation
|
||||
import com.unciv.logic.automation.civilization.NextTurnAutomation
|
||||
import com.unciv.logic.battle.Battle
|
||||
import com.unciv.logic.battle.BattleDamage
|
||||
import com.unciv.logic.battle.CityCombatant
|
||||
@ -206,7 +205,7 @@ object UnitAutomation {
|
||||
if (tryTakeBackCapturedCity(unit)) return
|
||||
|
||||
// Focus all units without a specific target on the enemy city closest to one of our cities
|
||||
if (tryHeadTowardsEnemyCity(unit)) return
|
||||
if (HeadTowardsEnemyCityAutomation.tryHeadTowardsEnemyCity(unit)) return
|
||||
|
||||
if (tryGarrisoningRangedLandUnit(unit)) return
|
||||
|
||||
@ -418,114 +417,6 @@ object UnitAutomation {
|
||||
return unit.currentMovement == 0f
|
||||
}
|
||||
|
||||
fun tryHeadTowardsEnemyCity(unit: MapUnit): Boolean {
|
||||
if (unit.civ.cities.isEmpty()) return false
|
||||
|
||||
// only focus on *attacking* 1 enemy at a time otherwise you'll lose on both fronts
|
||||
|
||||
val enemies = unit.civ.getKnownCivs()
|
||||
.filter { unit.civ.isAtWarWith(it) && it.cities.isNotEmpty() }
|
||||
|
||||
val closestEnemyCity = enemies
|
||||
.mapNotNull { NextTurnAutomation.getClosestCities(unit.civ, it) }
|
||||
.minByOrNull { it.aerialDistance }?.city2
|
||||
?: return false // no attackable cities found
|
||||
|
||||
// Our main attack target is the closest city, but we're fine with deviating from that a bit
|
||||
var enemyCitiesByPriority = closestEnemyCity.civ.cities
|
||||
.associateWith { it.getCenterTile().aerialDistanceTo(closestEnemyCity.getCenterTile()) }
|
||||
.asSequence().filterNot { it.value > 10 } // anything 10 tiles away from the target is irrelevant
|
||||
.sortedBy { it.value }.map { it.key } // sort the list by closeness to target - least is best!
|
||||
|
||||
if (unit.baseUnit.isRanged()) // ranged units don't harm capturable cities, waste of a turn
|
||||
enemyCitiesByPriority = enemyCitiesByPriority.filterNot { it.health == 1 }
|
||||
|
||||
val closestReachableEnemyCity = enemyCitiesByPriority
|
||||
.firstOrNull { unit.movement.canReach(it.getCenterTile()) }
|
||||
|
||||
if (closestReachableEnemyCity != null) {
|
||||
return headTowardsEnemyCity(
|
||||
unit,
|
||||
closestReachableEnemyCity.getCenterTile(),
|
||||
// This should be cached after the `canReach` call above.
|
||||
unit.movement.getShortestPath(closestReachableEnemyCity.getCenterTile())
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
private fun headTowardsEnemyCity(
|
||||
unit: MapUnit,
|
||||
closestReachableEnemyCity: Tile,
|
||||
shortestPath: List<Tile>
|
||||
): Boolean {
|
||||
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
||||
val unitRange = unit.getRange()
|
||||
|
||||
if (unitRange > 2) { // long-ranged unit, should never be in a bombardable position
|
||||
val tilesInBombardRange = closestReachableEnemyCity.getTilesInDistance(2).toSet()
|
||||
val tileToMoveTo =
|
||||
unitDistanceToTiles.asSequence()
|
||||
.filter {
|
||||
it.key.aerialDistanceTo(closestReachableEnemyCity) <=
|
||||
unitRange && it.key !in tilesInBombardRange
|
||||
&& unit.getDamageFromTerrain(it.key) <= 0 // Don't set up on a mountain
|
||||
}
|
||||
.minByOrNull { it.value.totalDistance }?.key
|
||||
|
||||
// move into position far away enough that the bombard doesn't hurt
|
||||
if (tileToMoveTo != null) {
|
||||
unit.movement.headTowards(tileToMoveTo)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// None of the stuff below is relevant if we're still quite far away from the city, so we
|
||||
// short-circuit here for performance reasons.
|
||||
val minDistanceFromCityToConsiderForLandingArea = 3
|
||||
val maxDistanceFromCityToConsiderForLandingArea = 5
|
||||
if (unit.currentTile.aerialDistanceTo(closestReachableEnemyCity) > maxDistanceFromCityToConsiderForLandingArea
|
||||
// Even in the worst case of only being able to move 1 tile per turn, we would still
|
||||
// not overshoot.
|
||||
&& shortestPath.size > minDistanceFromCityToConsiderForLandingArea ) {
|
||||
unit.movement.moveToTile(shortestPath[0])
|
||||
return true
|
||||
}
|
||||
|
||||
val ourUnitsAroundEnemyCity = closestReachableEnemyCity.getTilesInDistance(6)
|
||||
.flatMap { it.getUnits() }
|
||||
.filter { it.isMilitary() && it.civ == unit.civ }
|
||||
|
||||
val city = closestReachableEnemyCity.getCity()!!
|
||||
val cityCombatant = CityCombatant(city)
|
||||
|
||||
val expectedDamagePerTurn = ourUnitsAroundEnemyCity
|
||||
.map { BattleDamage.calculateDamageToDefender(MapUnitCombatant(it), cityCombatant) }
|
||||
.sum() // City heals 20 per turn
|
||||
|
||||
if (expectedDamagePerTurn < city.health && // If we can take immediately, go for it
|
||||
(expectedDamagePerTurn <= 20 || city.health / (expectedDamagePerTurn-20) > 5)){ // otherwise check if we can take within a couple of turns
|
||||
|
||||
// We won't be able to take this even with 5 turns of continuous damage!
|
||||
// don't head straight to the city, try to head to landing grounds -
|
||||
// this is against tha AI's brilliant plan of having everyone embarked and attacking via sea when unnecessary.
|
||||
val tileToHeadTo = closestReachableEnemyCity.getTilesInDistanceRange(3..5)
|
||||
.filter { it.isLand && unit.getDamageFromTerrain(it) <= 0 } // Don't head for hurty terrain
|
||||
.sortedBy { it.aerialDistanceTo(unit.currentTile) }
|
||||
.firstOrNull { (unit.movement.canMoveTo(it) || it == unit.currentTile) && unit.movement.canReach(it) }
|
||||
|
||||
if (tileToHeadTo != null) { // no need to worry, keep going as the movement alg. says
|
||||
unit.movement.headTowards(tileToHeadTo)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
unit.movement.moveToTile(shortestPath[0]) // go for it!
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun tryEnterOwnClosestCity(unit: MapUnit): Boolean {
|
||||
val closestCity = unit.civ.cities
|
||||
@ -590,7 +481,7 @@ object UnitAutomation {
|
||||
.firstOrNull { unit.movement.canReach(it) }
|
||||
|
||||
if (closestReachableCapturedCity != null) {
|
||||
return headTowardsEnemyCity(
|
||||
return HeadTowardsEnemyCityAutomation.headTowardsEnemyCity(
|
||||
unit,
|
||||
closestReachableCapturedCity,
|
||||
// This should be cached after the `canReach` call above.
|
||||
|
Loading…
Reference in New Issue
Block a user