Air unit automation improvement (#10991)

* Improved AirUnitAutomation

* UnitPriority now has special cases for air units

* Fighters now Air-sweep

* Added extra air sweep logic

* Moved airSweepDamagePercentBonus to AirUnitAutomation.kt
This commit is contained in:
Oskar Niesen 2024-01-25 15:28:51 -06:00 committed by GitHub
parent 5cbc04b63a
commit d216db5ced
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 61 additions and 8 deletions

View File

@ -390,9 +390,15 @@ object NextTurnAutomation {
for (unit in sortedUnits) UnitAutomation.automateUnitMoves(unit) for (unit in sortedUnits) UnitAutomation.automateUnitMoves(unit)
} }
/** Returns the priority of the unit, a lower value is higher priority **/
fun getUnitPriority(unit: MapUnit, isAtWar: Boolean): Int { fun getUnitPriority(unit: MapUnit, isAtWar: Boolean): Int {
if (unit.isCivilian() && !unit.isGreatPersonOfType("War")) return 1 // Civilian if (unit.isCivilian() && !unit.isGreatPersonOfType("War")) return 1 // Civilian
if (unit.baseUnit.isAirUnit()) return 2 if (unit.baseUnit.isAirUnit()) return when {
unit.canIntercept() -> 2 // Fighers first
unit.baseUnit.isNuclearWeapon() -> 3 // Then Nukes (area damage)
!unit.hasUnique(UniqueType.SelfDestructs) -> 4 // Then Bombers (reusable)
else -> 5 // Missiles
}
val distance = if (!isAtWar) 0 else unit.civ.threatManager.getDistanceToClosestEnemyUnit(unit.getTile(),6) val distance = if (!isAtWar) 0 else unit.civ.threatManager.getDistanceToClosestEnemyUnit(unit.getTile(),6)
// Lower health units should move earlier to swap with higher health units // Lower health units should move earlier to swap with higher health units
return distance + (unit.health / 10) + when { return distance + (unit.health / 10) + when {

View File

@ -1,20 +1,52 @@
package com.unciv.logic.automation.unit package com.unciv.logic.automation.unit
import com.unciv.logic.battle.AirInterception
import com.unciv.logic.battle.MapUnitCombatant import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.battle.Nuke import com.unciv.logic.battle.Nuke
import com.unciv.logic.battle.TargetHelper import com.unciv.logic.battle.TargetHelper
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.unique.UniqueType
object AirUnitAutomation { object AirUnitAutomation {
fun automateFighter(unit: MapUnit) { fun automateFighter(unit: MapUnit) {
if (unit.health <= 50 && !unit.hasUnique(UniqueType.HealsEvenAfterAction)) return // Wait and heal
val tilesWithEnemyUnitsInRange = unit.civ.threatManager.getTilesWithEnemyUnitsInDistance(unit.getTile(), unit.getRange()) val tilesWithEnemyUnitsInRange = unit.civ.threatManager.getTilesWithEnemyUnitsInDistance(unit.getTile(), unit.getRange())
// TODO: Optimize [friendlyAirUnitsInRange] by creating an alternate [ThreatManager.getTilesWithEnemyUnitsInDistance] that handles only friendly units
val friendlyAirUnitsInRange = unit.getTile().getTilesInDistance(unit.getRange()).flatMap { it.airUnits }.filter { it.civ == unit.civ }
// Find all visible enemy air units
val enemyAirUnitsInRange = tilesWithEnemyUnitsInRange val enemyAirUnitsInRange = tilesWithEnemyUnitsInRange
.flatMap { it.airUnits.asSequence() }.filter { it.civ.isAtWarWith(unit.civ) } .flatMap { it.airUnits.asSequence() }.filter { it.civ.isAtWarWith(unit.civ) }
val enemyFighters = enemyAirUnitsInRange.size / 2 // Assume half the planes are fighters
val friendlyUnusedFighterCount = friendlyAirUnitsInRange.count { it.health >= 50 && it.canAttack() }
val friendlyUsedFighterCount = friendlyAirUnitsInRange.count { it.health >= 50 && !it.canAttack() }
if (enemyAirUnitsInRange.any()) return // we need to be on standby in case they attack // We need to be on standby in case they attack
if (friendlyUnusedFighterCount < enemyFighters) return
if (friendlyUsedFighterCount <= enemyFighters) {
fun airSweepDamagePercentBonus(): Int {
return unit.getMatchingUniques(UniqueType.StrengthWhenAirsweep)
.sumOf { it.params[0].toInt() }
}
// If we are outnumbered, don't heal after attacking and don't have an Air Sweep bonus
// Then we shouldn't speed the air battle by killing our fighters, instead, focus on defending
if (friendlyUsedFighterCount + friendlyUnusedFighterCount < enemyFighters
&& !unit.hasUnique(UniqueType.HealsEvenAfterAction)
&& airSweepDamagePercentBonus() <= 0) {
return
} else {
if (tryAirSweep(unit, tilesWithEnemyUnitsInRange)) return
}
}
if (unit.health < 80) {
return // Wait and heal up, no point in moving closer to battle if we aren't healed
}
if (BattleHelper.tryAttackNearbyEnemy(unit)) return if (BattleHelper.tryAttackNearbyEnemy(unit)) return
@ -46,9 +78,25 @@ object AirUnitAutomation {
} }
private fun tryAirSweep(unit: MapUnit, tilesWithEnemyUnitsInRange: List<Tile>):Boolean {
val targetTile = tilesWithEnemyUnitsInRange.filter {
tile -> tile.getUnits().any { it.civ.isAtWarWith(unit.civ)
|| (tile.isCityCenter() && tile.getCity()!!.civ.isAtWarWith(unit.civ)) }
}.minByOrNull { it.aerialDistanceTo(unit.getTile()) } ?: return false
AirInterception.airSweep(MapUnitCombatant(unit),targetTile)
if (unit.currentMovement > 0) return false
return true
}
fun automateBomber(unit: MapUnit) { fun automateBomber(unit: MapUnit) {
if (unit.health <= 50 && !unit.hasUnique(UniqueType.HealsEvenAfterAction)) return // Wait and heal
if (BattleHelper.tryAttackNearbyEnemy(unit)) return if (BattleHelper.tryAttackNearbyEnemy(unit)) return
if (unit.health <= 90 || (unit.health < 100 && !unit.civ.isAtWar())) {
return // Wait and heal
}
if (tryRelocateToCitiesWithEnemyNearBy(unit)) return if (tryRelocateToCitiesWithEnemyNearBy(unit)) return
val pathsToCities = unit.movement.getAerialPathsToCities() val pathsToCities = unit.movement.getAerialPathsToCities()

View File

@ -15,8 +15,8 @@ object BattleHelper {
fun tryAttackNearbyEnemy(unit: MapUnit, stayOnTile: Boolean = false): Boolean { fun tryAttackNearbyEnemy(unit: MapUnit, stayOnTile: Boolean = false): Boolean {
if (unit.hasUnique(UniqueType.CannotAttack)) return false if (unit.hasUnique(UniqueType.CannotAttack)) return false
val attackableEnemies = TargetHelper.getAttackableEnemies(unit, unit.movement.getDistanceToTiles(), stayOnTile=stayOnTile) val attackableEnemies = TargetHelper.getAttackableEnemies(unit, unit.movement.getDistanceToTiles(), stayOnTile=stayOnTile)
// Only take enemies we can fight without dying // Only take enemies we can fight without dying or are made to die
.filter { .filter {unit.hasUnique(UniqueType.SelfDestructs) ||
BattleDamage.calculateDamageToAttacker( BattleDamage.calculateDamageToAttacker(
MapUnitCombatant(unit), MapUnitCombatant(unit),
Battle.getMapCombatantOfTile(it.tileToAttack)!! Battle.getMapCombatantOfTile(it.tileToAttack)!!
@ -92,7 +92,7 @@ object BattleHelper {
if (attacker.baseUnit.isMelee()) { if (attacker.baseUnit.isMelee()) {
val battleDamage = BattleDamage.calculateDamageToAttacker(attackerUnit, cityUnit) val battleDamage = BattleDamage.calculateDamageToAttacker(attackerUnit, cityUnit)
if (attacker.health - battleDamage * 2 <= 0) { if (attacker.health - battleDamage * 2 <= 0 && !attacker.hasUnique(UniqueType.SelfDestructs)) {
// The more fiendly units around the city, the more willing we should be to just attack the city // The more fiendly units around the city, the more willing we should be to just attack the city
val friendlyUnitsAroundCity = city.getCenterTile().getTilesInDistance(3).count { it.militaryUnit?.civ == attacker.civ } val friendlyUnitsAroundCity = city.getCenterTile().getTilesInDistance(3).count { it.militaryUnit?.civ == attacker.civ }
// If we have more than 4 other units around the city, go for it // If we have more than 4 other units around the city, go for it

View File

@ -197,15 +197,14 @@ object UnitAutomation {
if (unit.canIntercept()) if (unit.canIntercept())
return AirUnitAutomation.automateFighter(unit) return AirUnitAutomation.automateFighter(unit)
if (!unit.baseUnit.isNuclearWeapon())
return AirUnitAutomation.automateBomber(unit)
// Note that not all nukes have to be air units // Note that not all nukes have to be air units
if (unit.baseUnit.isNuclearWeapon()) if (unit.baseUnit.isNuclearWeapon())
return AirUnitAutomation.automateNukes(unit) return AirUnitAutomation.automateNukes(unit)
if (unit.hasUnique(UniqueType.SelfDestructs)) if (unit.hasUnique(UniqueType.SelfDestructs))
return AirUnitAutomation.automateMissile(unit) return AirUnitAutomation.automateMissile(unit)
return AirUnitAutomation.automateBomber(unit)
} }
if (tryGoToRuinAndEncampment(unit) && unit.currentMovement == 0f) return if (tryGoToRuinAndEncampment(unit) && unit.currentMovement == 0f) return