From d216db5ced702fde57f0c63c140b33543b422ab8 Mon Sep 17 00:00:00 2001 From: Oskar Niesen Date: Thu, 25 Jan 2024 15:28:51 -0600 Subject: [PATCH] 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 --- .../civilization/NextTurnAutomation.kt | 8 ++- .../automation/unit/AirUnitAutomation.kt | 50 ++++++++++++++++++- .../logic/automation/unit/BattleHelper.kt | 6 +-- .../logic/automation/unit/UnitAutomation.kt | 5 +- 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt index ec3e061db6..dd413cde8b 100644 --- a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt @@ -390,9 +390,15 @@ object NextTurnAutomation { 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 { 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) // Lower health units should move earlier to swap with higher health units return distance + (unit.health / 10) + when { diff --git a/core/src/com/unciv/logic/automation/unit/AirUnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/AirUnitAutomation.kt index b6e8aa874b..ceee4e8f7d 100644 --- a/core/src/com/unciv/logic/automation/unit/AirUnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/AirUnitAutomation.kt @@ -1,21 +1,53 @@ package com.unciv.logic.automation.unit +import com.unciv.logic.battle.AirInterception import com.unciv.logic.battle.MapUnitCombatant import com.unciv.logic.battle.Nuke import com.unciv.logic.battle.TargetHelper import com.unciv.logic.civilization.Civilization import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.tile.Tile +import com.unciv.models.ruleset.unique.UniqueType object AirUnitAutomation { 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()) + // 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 .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 (tryRelocateToCitiesWithEnemyNearBy(unit)) return @@ -46,9 +78,25 @@ object AirUnitAutomation { } + private fun tryAirSweep(unit: MapUnit, tilesWithEnemyUnitsInRange: List):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) { + if (unit.health <= 50 && !unit.hasUnique(UniqueType.HealsEvenAfterAction)) return // Wait and heal + if (BattleHelper.tryAttackNearbyEnemy(unit)) return + if (unit.health <= 90 || (unit.health < 100 && !unit.civ.isAtWar())) { + return // Wait and heal + } + if (tryRelocateToCitiesWithEnemyNearBy(unit)) return val pathsToCities = unit.movement.getAerialPathsToCities() diff --git a/core/src/com/unciv/logic/automation/unit/BattleHelper.kt b/core/src/com/unciv/logic/automation/unit/BattleHelper.kt index a182e588dd..8f57f7a6e9 100644 --- a/core/src/com/unciv/logic/automation/unit/BattleHelper.kt +++ b/core/src/com/unciv/logic/automation/unit/BattleHelper.kt @@ -15,8 +15,8 @@ object BattleHelper { fun tryAttackNearbyEnemy(unit: MapUnit, stayOnTile: Boolean = false): Boolean { if (unit.hasUnique(UniqueType.CannotAttack)) return false val attackableEnemies = TargetHelper.getAttackableEnemies(unit, unit.movement.getDistanceToTiles(), stayOnTile=stayOnTile) - // Only take enemies we can fight without dying - .filter { + // Only take enemies we can fight without dying or are made to die + .filter {unit.hasUnique(UniqueType.SelfDestructs) || BattleDamage.calculateDamageToAttacker( MapUnitCombatant(unit), Battle.getMapCombatantOfTile(it.tileToAttack)!! @@ -92,7 +92,7 @@ object BattleHelper { if (attacker.baseUnit.isMelee()) { 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 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 diff --git a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt index 7d8bae19b6..88a886abc8 100644 --- a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt @@ -197,15 +197,14 @@ object UnitAutomation { if (unit.canIntercept()) return AirUnitAutomation.automateFighter(unit) - if (!unit.baseUnit.isNuclearWeapon()) - return AirUnitAutomation.automateBomber(unit) - // Note that not all nukes have to be air units if (unit.baseUnit.isNuclearWeapon()) return AirUnitAutomation.automateNukes(unit) if (unit.hasUnique(UniqueType.SelfDestructs)) return AirUnitAutomation.automateMissile(unit) + + return AirUnitAutomation.automateBomber(unit) } if (tryGoToRuinAndEncampment(unit) && unit.currentMovement == 0f) return