chore: Split SpecificUnitAutomation into air units and religious units, with the remainder being mostly great person automation but also a few 'others' so I'm not renaming it

This commit is contained in:
Yair Morgenstern 2023-10-04 22:39:25 +03:00
parent f46b3fc62b
commit f4b7822728
5 changed files with 352 additions and 333 deletions

View File

@ -0,0 +1,190 @@
package com.unciv.logic.automation.unit
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
object AirUnitAutomation {
fun automateFighter(unit: MapUnit) {
val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange())
val enemyAirUnitsInRange = tilesInRange
.flatMap { it.airUnits.asSequence() }.filter { it.civ.isAtWarWith(unit.civ) }
if (enemyAirUnitsInRange.any()) return // we need to be on standby in case they attack
if (BattleHelper.tryAttackNearbyEnemy(unit)) return
if (tryRelocateToCitiesWithEnemyNearBy(unit)) return
val pathsToCities = unit.movement.getAerialPathsToCities()
if (pathsToCities.isEmpty()) return // can't actually move anywhere else
val citiesByNearbyAirUnits = pathsToCities.keys
.groupBy { key ->
key.getTilesInDistance(unit.getMaxMovementForAirUnits())
.count {
val firstAirUnit = it.airUnits.firstOrNull()
firstAirUnit != null && firstAirUnit.civ.isAtWarWith(unit.civ)
}
}
if (citiesByNearbyAirUnits.keys.any { it != 0 }) {
val citiesWithMostNeedOfAirUnits = citiesByNearbyAirUnits.maxByOrNull { it.key }!!.value
//todo: maybe group by size and choose highest priority within the same size turns
val chosenCity = citiesWithMostNeedOfAirUnits.minByOrNull { pathsToCities.getValue(it).size }!! // city with min path = least turns to get there
val firstStepInPath = pathsToCities.getValue(chosenCity).first()
unit.movement.moveToTile(firstStepInPath)
return
}
// no city needs fighters to defend, so let's attack stuff from the closest possible location
tryMoveToCitiesToAerialAttackFrom(pathsToCities, unit)
}
fun automateBomber(unit: MapUnit) {
if (BattleHelper.tryAttackNearbyEnemy(unit)) return
if (tryRelocateToCitiesWithEnemyNearBy(unit)) return
val pathsToCities = unit.movement.getAerialPathsToCities()
if (pathsToCities.isEmpty()) return // can't actually move anywhere else
tryMoveToCitiesToAerialAttackFrom(pathsToCities, unit)
}
private fun tryMoveToCitiesToAerialAttackFrom(pathsToCities: HashMap<Tile, ArrayList<Tile>>, airUnit: MapUnit) {
val citiesThatCanAttackFrom = pathsToCities.keys
.filter { destinationCity ->
destinationCity != airUnit.currentTile
&& destinationCity.getTilesInDistance(airUnit.getRange())
.any { TargetHelper.containsAttackableEnemy(it, MapUnitCombatant(airUnit)) }
}
if (citiesThatCanAttackFrom.isEmpty()) return
//todo: this logic looks similar to some parts of automateFighter, maybe pull out common code
//todo: maybe group by size and choose highest priority within the same size turns
val closestCityThatCanAttackFrom =
citiesThatCanAttackFrom.minByOrNull { pathsToCities[it]!!.size }!!
val firstStepInPath = pathsToCities[closestCityThatCanAttackFrom]!!.first()
airUnit.movement.moveToTile(firstStepInPath)
}
fun automateNukes(unit: MapUnit) {
if (!unit.civ.isAtWar()) return
// We should *Almost* never want to nuke our own city, so don't consider it
val tilesInRange = unit.currentTile.getTilesInDistanceRange(2..unit.getRange())
var highestTileNukeValue = 0
var tileToNuke: Tile? = null
tilesInRange.forEach {
val value = getNukeLocationValue(unit, it)
if (value > highestTileNukeValue) {
highestTileNukeValue = value
tileToNuke = it
}
}
if (highestTileNukeValue > 0) {
Nuke.NUKE(MapUnitCombatant(unit), tileToNuke!!)
}
tryRelocateMissileToNearbyAttackableCities(unit)
}
/**
* Ranks the tile to nuke based off of all tiles in it's blast radius
* By default the value is -500 to prevent inefficient nuking.
*/
private fun getNukeLocationValue(nuke: MapUnit, tile: Tile): Int {
val civ = nuke.civ
if (!Nuke.mayUseNuke(MapUnitCombatant(nuke), tile)) return Int.MIN_VALUE
val blastRadius = nuke.getNukeBlastRadius()
val tilesInBlastRadius = tile.getTilesInDistance(blastRadius)
val civsInBlastRadius = tilesInBlastRadius.mapNotNull { it.getOwner() } +
tilesInBlastRadius.mapNotNull { it.getFirstUnit()?.civ }
// Don't nuke if it means we will be declaring war on someone!
if (civsInBlastRadius.any { it != civ && !it.isAtWarWith(civ) }) return -100000
// If there are no enemies to hit, don't nuke
if (!civsInBlastRadius.any { it.isAtWarWith(civ) }) return -100000
// Launching a Nuke uses resources, therefore don't launch it by default
var explosionValue = -500
// Returns either ourValue or thierValue depending on if the input Civ matches the Nuke's Civ
fun evaluateCivValue(targetCiv: Civilization, ourValue: Int, theirValue: Int): Int {
if (targetCiv == civ) // We are nuking something that we own!
return ourValue
return theirValue // We are nuking an enemy!
}
for (targetTile in tilesInBlastRadius) {
// We can only account for visible units
if (tile.isVisible(civ)) {
if (targetTile.militaryUnit != null && !targetTile.militaryUnit!!.isInvisible(civ))
explosionValue += evaluateCivValue(targetTile.militaryUnit?.civ!!, -150, 50)
if (targetTile.civilianUnit != null && !targetTile.civilianUnit!!.isInvisible(civ))
explosionValue += evaluateCivValue(targetTile.civilianUnit?.civ!!, -100, 25)
}
// Never nuke our own Civ, don't nuke single enemy civs as well
if (targetTile.isCityCenter()
&& !(targetTile.getCity()!!.health <= 50f
&& targetTile.neighbors.any {it.militaryUnit?.civ == civ})) // Prefer not to nuke cities that we are about to take
explosionValue += evaluateCivValue(targetTile.getCity()?.civ!!, -100000, 250)
else if (targetTile.owningCity != null) {
val owningCiv = targetTile.owningCity?.civ!!
// If there is a tile to add fallout to there is a 50% chance it will get fallout
if (!(tile.isWater || tile.isImpassible() || targetTile.terrainFeatures.any { it == "Fallout" }))
explosionValue += evaluateCivValue(owningCiv, -40, 10)
// If there is an improvment to pillage
if (targetTile.improvement != null && !targetTile.improvementIsPillaged)
explosionValue += evaluateCivValue(owningCiv, -40, 20)
}
// If the value is too low end the search early
if (explosionValue < -1000) return explosionValue
}
return explosionValue
}
// This really needs to be changed, to have better targeting for missiles
fun automateMissile(unit: MapUnit) {
if (BattleHelper.tryAttackNearbyEnemy(unit)) return
tryRelocateMissileToNearbyAttackableCities(unit)
}
private fun tryRelocateMissileToNearbyAttackableCities(unit: MapUnit) {
val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange())
val immediatelyReachableCities = tilesInRange
.filter { unit.movement.canMoveTo(it) }
for (city in immediatelyReachableCities) if (city.getTilesInDistance(unit.getRange())
.any { it.isCityCenter() && it.getOwner()!!.isAtWarWith(unit.civ) }
) {
unit.movement.moveToTile(city)
return
}
if (unit.baseUnit.isAirUnit()) {
val pathsToCities = unit.movement.getAerialPathsToCities()
if (pathsToCities.isEmpty()) return // can't actually move anywhere else
tryMoveToCitiesToAerialAttackFrom(pathsToCities, unit)
} else UnitAutomation.tryHeadTowardsEnemyCity(unit)
}
private fun tryRelocateToCitiesWithEnemyNearBy(unit: MapUnit): Boolean {
val immediatelyReachableCitiesAndCarriers = unit.currentTile
.getTilesInDistance(unit.getMaxMovementForAirUnits()).filter { unit.movement.canMoveTo(it) }
for (city in immediatelyReachableCitiesAndCarriers) {
if (city.getTilesInDistance(unit.getRange())
.any {
it.isVisible(unit.civ) &&
TargetHelper.containsAttackableEnemy(it,MapUnitCombatant(unit))
}) {
unit.movement.moveToTile(city)
return true
}
}
return false
}
}

View File

@ -0,0 +1,151 @@
package com.unciv.logic.automation.unit
import com.unciv.Constants
import com.unciv.logic.city.City
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
import com.unciv.models.UnitActionType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
object ReligiousUnitAutomation {
fun automateMissionary(unit: MapUnit) {
if (unit.religion != unit.civ.religionManager.religion?.name || unit.religion == null)
return unit.disband()
val ourCitiesWithoutReligion = unit.civ.cities.filter {
it.religion.getMajorityReligion() != unit.civ.religionManager.religion
}
val city =
if (ourCitiesWithoutReligion.any())
ourCitiesWithoutReligion.minByOrNull { it.getCenterTile().aerialDistanceTo(unit.getTile()) }
else unit.civ.gameInfo.getCities()
.filter { it.religion.getMajorityReligion() != unit.civ.religionManager.religion }
.filter { it.civ.knows(unit.civ) && !it.civ.isAtWarWith(unit.civ) }
.filterNot { it.religion.isProtectedByInquisitor(unit.religion) }
.minByOrNull { it.getCenterTile().aerialDistanceTo(unit.getTile()) }
if (city == null) return
val destination = city.getTiles()
.filter { unit.movement.canMoveTo(it) || it == unit.getTile() }
.sortedBy { it.aerialDistanceTo(unit.getTile()) }
.firstOrNull { unit.movement.canReach(it) } ?: return
unit.movement.headTowards(destination)
if (unit.getTile() in city.getTiles() && unit.civ.religionManager.maySpreadReligionNow(unit)) {
UnitActions.invokeUnitAction(unit, UnitActionType.SpreadReligion)
}
}
fun automateInquisitor(unit: MapUnit) {
val civReligion = unit.civ.religionManager.religion
if (unit.religion != civReligion?.name || unit.religion == null)
return unit.disband() // No need to keep a unit we can't use, as it only blocks religion spreads of religions other that its own
val holyCity = unit.civ.religionManager.getHolyCity()
val cityToConvert = determineBestInquisitorCityToConvert(unit) // Also returns null if the inquisitor can't convert cities
val pressureDeficit =
if (cityToConvert == null) 0
else cityToConvert.religion.getPressureDeficit(civReligion?.name)
val citiesToProtect = unit.civ.cities.asSequence()
.filter { it.religion.getMajorityReligion() == civReligion }
// We only look at cities that are not currently protected or are protected by us
.filter { !it.religion.isProtectedByInquisitor() || unit.getTile() in it.getCenterTile().getTilesInDistance(1) }
// cities with most populations will be prioritized by the AI
val cityToProtect = citiesToProtect.maxByOrNull { it.population.population }
var destination: Tile?
destination = when {
cityToConvert != null
&& (cityToConvert == holyCity
|| pressureDeficit > Constants.aiPreferInquisitorOverMissionaryPressureDifference
|| cityToConvert.religion.isBlockedHolyCity && cityToConvert.religion.religionThisIsTheHolyCityOf == civReligion?.name
) && unit.canDoLimitedAction(Constants.removeHeresy) -> {
cityToConvert.getCenterTile()
}
cityToProtect != null && unit.hasUnique(UniqueType.PreventSpreadingReligion) -> {
if (holyCity != null && !holyCity.religion.isProtectedByInquisitor())
holyCity.getCenterTile()
else cityToProtect.getCenterTile()
}
cityToConvert != null -> cityToConvert.getCenterTile()
else -> null
}
if (destination == null) return
if (!unit.movement.canReach(destination)) {
destination = destination.neighbors
.filter { unit.movement.canMoveTo(it) || it == unit.getTile() }
.sortedBy { it.aerialDistanceTo(unit.currentTile) }
.firstOrNull { unit.movement.canReach(it) }
?: return
}
unit.movement.headTowards(destination)
if (cityToConvert != null && unit.getTile().getCity() == destination.getCity()) {
UnitActions.invokeUnitAction(unit, UnitActionType.RemoveHeresy)
}
}
private fun determineBestInquisitorCityToConvert(
unit: MapUnit,
): City? {
if (unit.religion != unit.civ.religionManager.religion?.name || !unit.canDoLimitedAction(Constants.removeHeresy))
return null
val holyCity = unit.civ.religionManager.getHolyCity()
if (holyCity != null && holyCity.religion.getMajorityReligion() != unit.civ.religionManager.religion!!)
return holyCity
val blockedHolyCity = unit.civ.cities.firstOrNull { it.religion.isBlockedHolyCity && it.religion.religionThisIsTheHolyCityOf == unit.religion }
if (blockedHolyCity != null)
return blockedHolyCity
return unit.civ.cities.asSequence()
.filter { it.religion.getMajorityReligion() != null }
.filter { it.religion.getMajorityReligion()!! != unit.civ.religionManager.religion }
// Don't go if it takes too long
.filter { it.getCenterTile().aerialDistanceTo(unit.currentTile) <= 20 }
.maxByOrNull { it.religion.getPressureDeficit(unit.civ.religionManager.religion?.name) }
}
fun foundReligion(unit: MapUnit) {
val cityToFoundReligionAt =
if (unit.getTile().isCityCenter() && !unit.getTile().owningCity!!.isHolyCity()) unit.getTile().owningCity
else unit.civ.cities.firstOrNull {
!it.isHolyCity()
&& unit.movement.canMoveTo(it.getCenterTile())
&& unit.movement.canReach(it.getCenterTile())
}
if (cityToFoundReligionAt == null) return
if (unit.getTile() != cityToFoundReligionAt.getCenterTile()) {
unit.movement.headTowards(cityToFoundReligionAt.getCenterTile())
return
}
UnitActions.invokeUnitAction(unit, UnitActionType.FoundReligion)
}
fun enhanceReligion(unit: MapUnit) {
// Try go to a nearby city
if (!unit.getTile().isCityCenter())
UnitAutomation.tryEnterOwnClosestCity(unit)
// If we were unable to go there this turn, unable to do anything else
if (!unit.getTile().isCityCenter())
return
UnitActions.invokeUnitAction(unit, UnitActionType.EnhanceReligion)
}
}

View File

@ -1,13 +1,8 @@
package com.unciv.logic.automation.unit
import com.unciv.Constants
import com.unciv.logic.automation.Automation
import com.unciv.logic.battle.GreatGeneralImplementation
import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.battle.Nuke
import com.unciv.logic.battle.TargetHelper
import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
@ -310,323 +305,4 @@ object SpecificUnitAutomation {
}
}
fun automateMissionary(unit: MapUnit) {
if (unit.religion != unit.civ.religionManager.religion?.name || unit.religion == null)
return unit.disband()
val ourCitiesWithoutReligion = unit.civ.cities.filter {
it.religion.getMajorityReligion() != unit.civ.religionManager.religion
}
val city =
if (ourCitiesWithoutReligion.any())
ourCitiesWithoutReligion.minByOrNull { it.getCenterTile().aerialDistanceTo(unit.getTile()) }
else unit.civ.gameInfo.getCities()
.filter { it.religion.getMajorityReligion() != unit.civ.religionManager.religion }
.filter { it.civ.knows(unit.civ) && !it.civ.isAtWarWith(unit.civ) }
.filterNot { it.religion.isProtectedByInquisitor(unit.religion) }
.minByOrNull { it.getCenterTile().aerialDistanceTo(unit.getTile()) }
if (city == null) return
val destination = city.getTiles()
.filter { unit.movement.canMoveTo(it) || it == unit.getTile() }
.sortedBy { it.aerialDistanceTo(unit.getTile()) }
.firstOrNull { unit.movement.canReach(it) } ?: return
unit.movement.headTowards(destination)
if (unit.getTile() in city.getTiles() && unit.civ.religionManager.maySpreadReligionNow(unit)) {
UnitActions.invokeUnitAction(unit, UnitActionType.SpreadReligion)
}
}
fun automateInquisitor(unit: MapUnit) {
val civReligion = unit.civ.religionManager.religion
if (unit.religion != civReligion?.name || unit.religion == null)
return unit.disband() // No need to keep a unit we can't use, as it only blocks religion spreads of religions other that its own
val holyCity = unit.civ.religionManager.getHolyCity()
val cityToConvert = determineBestInquisitorCityToConvert(unit) // Also returns null if the inquisitor can't convert cities
val pressureDeficit =
if (cityToConvert == null) 0
else cityToConvert.religion.getPressureDeficit(civReligion?.name)
val citiesToProtect = unit.civ.cities.asSequence()
.filter { it.religion.getMajorityReligion() == civReligion }
// We only look at cities that are not currently protected or are protected by us
.filter { !it.religion.isProtectedByInquisitor() || unit.getTile() in it.getCenterTile().getTilesInDistance(1) }
// cities with most populations will be prioritized by the AI
val cityToProtect = citiesToProtect.maxByOrNull { it.population.population }
var destination: Tile?
destination = when {
cityToConvert != null
&& (cityToConvert == holyCity
|| pressureDeficit > Constants.aiPreferInquisitorOverMissionaryPressureDifference
|| cityToConvert.religion.isBlockedHolyCity && cityToConvert.religion.religionThisIsTheHolyCityOf == civReligion?.name
) && unit.canDoLimitedAction(Constants.removeHeresy) -> {
cityToConvert.getCenterTile()
}
cityToProtect != null && unit.hasUnique(UniqueType.PreventSpreadingReligion) -> {
if (holyCity != null && !holyCity.religion.isProtectedByInquisitor())
holyCity.getCenterTile()
else cityToProtect.getCenterTile()
}
cityToConvert != null -> cityToConvert.getCenterTile()
else -> null
}
if (destination == null) return
if (!unit.movement.canReach(destination)) {
destination = destination.neighbors
.filter { unit.movement.canMoveTo(it) || it == unit.getTile() }
.sortedBy { it.aerialDistanceTo(unit.currentTile) }
.firstOrNull { unit.movement.canReach(it) }
?: return
}
unit.movement.headTowards(destination)
if (cityToConvert != null && unit.getTile().getCity() == destination.getCity()) {
UnitActions.invokeUnitAction(unit, UnitActionType.RemoveHeresy)
}
}
private fun determineBestInquisitorCityToConvert(
unit: MapUnit,
): City? {
if (unit.religion != unit.civ.religionManager.religion?.name || !unit.canDoLimitedAction(Constants.removeHeresy))
return null
val holyCity = unit.civ.religionManager.getHolyCity()
if (holyCity != null && holyCity.religion.getMajorityReligion() != unit.civ.religionManager.religion!!)
return holyCity
val blockedHolyCity = unit.civ.cities.firstOrNull { it.religion.isBlockedHolyCity && it.religion.religionThisIsTheHolyCityOf == unit.religion }
if (blockedHolyCity != null)
return blockedHolyCity
return unit.civ.cities.asSequence()
.filter { it.religion.getMajorityReligion() != null }
.filter { it.religion.getMajorityReligion()!! != unit.civ.religionManager.religion }
// Don't go if it takes too long
.filter { it.getCenterTile().aerialDistanceTo(unit.currentTile) <= 20 }
.maxByOrNull { it.religion.getPressureDeficit(unit.civ.religionManager.religion?.name) }
}
fun automateFighter(unit: MapUnit) {
val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange())
val enemyAirUnitsInRange = tilesInRange
.flatMap { it.airUnits.asSequence() }.filter { it.civ.isAtWarWith(unit.civ) }
if (enemyAirUnitsInRange.any()) return // we need to be on standby in case they attack
if (BattleHelper.tryAttackNearbyEnemy(unit)) return
if (tryRelocateToCitiesWithEnemyNearBy(unit)) return
val pathsToCities = unit.movement.getAerialPathsToCities()
if (pathsToCities.isEmpty()) return // can't actually move anywhere else
val citiesByNearbyAirUnits = pathsToCities.keys
.groupBy { key ->
key.getTilesInDistance(unit.getMaxMovementForAirUnits())
.count {
val firstAirUnit = it.airUnits.firstOrNull()
firstAirUnit != null && firstAirUnit.civ.isAtWarWith(unit.civ)
}
}
if (citiesByNearbyAirUnits.keys.any { it != 0 }) {
val citiesWithMostNeedOfAirUnits = citiesByNearbyAirUnits.maxByOrNull { it.key }!!.value
//todo: maybe group by size and choose highest priority within the same size turns
val chosenCity = citiesWithMostNeedOfAirUnits.minByOrNull { pathsToCities.getValue(it).size }!! // city with min path = least turns to get there
val firstStepInPath = pathsToCities.getValue(chosenCity).first()
unit.movement.moveToTile(firstStepInPath)
return
}
// no city needs fighters to defend, so let's attack stuff from the closest possible location
tryMoveToCitiesToAerialAttackFrom(pathsToCities, unit)
}
fun automateBomber(unit: MapUnit) {
if (BattleHelper.tryAttackNearbyEnemy(unit)) return
if (tryRelocateToCitiesWithEnemyNearBy(unit)) return
val pathsToCities = unit.movement.getAerialPathsToCities()
if (pathsToCities.isEmpty()) return // can't actually move anywhere else
tryMoveToCitiesToAerialAttackFrom(pathsToCities, unit)
}
private fun tryMoveToCitiesToAerialAttackFrom(pathsToCities: HashMap<Tile, ArrayList<Tile>>, airUnit: MapUnit) {
val citiesThatCanAttackFrom = pathsToCities.keys
.filter { destinationCity ->
destinationCity != airUnit.currentTile
&& destinationCity.getTilesInDistance(airUnit.getRange())
.any { TargetHelper.containsAttackableEnemy(it, MapUnitCombatant(airUnit)) }
}
if (citiesThatCanAttackFrom.isEmpty()) return
//todo: this logic looks similar to some parts of automateFighter, maybe pull out common code
//todo: maybe group by size and choose highest priority within the same size turns
val closestCityThatCanAttackFrom =
citiesThatCanAttackFrom.minByOrNull { pathsToCities[it]!!.size }!!
val firstStepInPath = pathsToCities[closestCityThatCanAttackFrom]!!.first()
airUnit.movement.moveToTile(firstStepInPath)
}
fun automateNukes(unit: MapUnit) {
if (!unit.civ.isAtWar()) return
// We should *Almost* never want to nuke our own city, so don't consider it
val tilesInRange = unit.currentTile.getTilesInDistanceRange(2..unit.getRange())
var highestTileNukeValue = 0
var tileToNuke: Tile? = null
tilesInRange.forEach {
val value = getNukeLocationValue(unit, it)
if (value > highestTileNukeValue) {
highestTileNukeValue = value
tileToNuke = it
}
}
if (highestTileNukeValue > 0) {
Nuke.NUKE(MapUnitCombatant(unit), tileToNuke!!)
}
tryRelocateToNearbyAttackableCities(unit)
}
/**
* Ranks the tile to nuke based off of all tiles in it's blast radius
* By default the value is -500 to prevent inefficient nuking.
*/
fun getNukeLocationValue(nuke: MapUnit, tile: Tile): Int {
val civ = nuke.civ
if (!Nuke.mayUseNuke(MapUnitCombatant(nuke), tile)) return Int.MIN_VALUE
val blastRadius = nuke.getNukeBlastRadius()
val tilesInBlastRadius = tile.getTilesInDistance(blastRadius)
val civsInBlastRadius = tilesInBlastRadius.mapNotNull { it.getOwner() } +
tilesInBlastRadius.mapNotNull { it.getFirstUnit()?.civ }
// Don't nuke if it means we will be declaring war on someone!
if (civsInBlastRadius.any { it != civ && !it.isAtWarWith(civ) }) return -100000
// If there are no enemies to hit, don't nuke
if (!civsInBlastRadius.any { it.isAtWarWith(civ) }) return -100000
// Launching a Nuke uses resources, therefore don't launch it by default
var explosionValue = -500
// Returns either ourValue or thierValue depending on if the input Civ matches the Nuke's Civ
fun evaluateCivValue(targetCiv: Civilization, ourValue: Int, theirValue: Int): Int {
if (targetCiv == civ) // We are nuking something that we own!
return ourValue
return theirValue // We are nuking an enemy!
}
for (targetTile in tilesInBlastRadius) {
// We can only account for visible units
if (tile.isVisible(civ)) {
if (targetTile.militaryUnit != null && !targetTile.militaryUnit!!.isInvisible(civ))
explosionValue += evaluateCivValue(targetTile.militaryUnit?.civ!!, -150, 50)
if (targetTile.civilianUnit != null && !targetTile.civilianUnit!!.isInvisible(civ))
explosionValue += evaluateCivValue(targetTile.civilianUnit?.civ!!, -100, 25)
}
// Never nuke our own Civ, don't nuke single enemy civs as well
if (targetTile.isCityCenter()
&& !(targetTile.getCity()!!.health <= 50f
&& targetTile.neighbors.any {it.militaryUnit?.civ == civ})) // Prefer not to nuke cities that we are about to take
explosionValue += evaluateCivValue(targetTile.getCity()?.civ!!, -100000, 250)
else if (targetTile.owningCity != null) {
val owningCiv = targetTile.owningCity?.civ!!
// If there is a tile to add fallout to there is a 50% chance it will get fallout
if (!(tile.isWater || tile.isImpassible() || targetTile.terrainFeatures.any { it == "Fallout" }))
explosionValue += evaluateCivValue(owningCiv, -40, 10)
// If there is an improvment to pillage
if (targetTile.improvement != null && !targetTile.improvementIsPillaged)
explosionValue += evaluateCivValue(owningCiv, -40, 20)
}
// If the value is too low end the search early
if (explosionValue < -1000) return explosionValue
}
return explosionValue
}
// This really needs to be changed, to have better targeting for missiles
fun automateMissile(unit: MapUnit) {
if (BattleHelper.tryAttackNearbyEnemy(unit)) return
tryRelocateToNearbyAttackableCities(unit)
}
private fun tryRelocateToNearbyAttackableCities(unit: MapUnit) {
val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange())
val immediatelyReachableCities = tilesInRange
.filter { unit.movement.canMoveTo(it) }
for (city in immediatelyReachableCities) if (city.getTilesInDistance(unit.getRange())
.any { it.isCityCenter() && it.getOwner()!!.isAtWarWith(unit.civ) }
) {
unit.movement.moveToTile(city)
return
}
if (unit.baseUnit.isAirUnit()) {
val pathsToCities = unit.movement.getAerialPathsToCities()
if (pathsToCities.isEmpty()) return // can't actually move anywhere else
tryMoveToCitiesToAerialAttackFrom(pathsToCities, unit)
} else UnitAutomation.tryHeadTowardsEnemyCity(unit)
}
private fun tryRelocateToCitiesWithEnemyNearBy(unit: MapUnit): Boolean {
val immediatelyReachableCitiesAndCarriers = unit.currentTile
.getTilesInDistance(unit.getMaxMovementForAirUnits()).filter { unit.movement.canMoveTo(it) }
for (city in immediatelyReachableCitiesAndCarriers) {
if (city.getTilesInDistance(unit.getRange())
.any {
it.isVisible(unit.civ) &&
TargetHelper.containsAttackableEnemy(
it,
MapUnitCombatant(unit)
)
}) {
unit.movement.moveToTile(city)
return true
}
}
return false
}
fun foundReligion(unit: MapUnit) {
val cityToFoundReligionAt =
if (unit.getTile().isCityCenter() && !unit.getTile().owningCity!!.isHolyCity()) unit.getTile().owningCity
else unit.civ.cities.firstOrNull {
!it.isHolyCity()
&& unit.movement.canMoveTo(it.getCenterTile())
&& unit.movement.canReach(it.getCenterTile())
}
if (cityToFoundReligionAt == null) return
if (unit.getTile() != cityToFoundReligionAt.getCenterTile()) {
unit.movement.headTowards(cityToFoundReligionAt.getCenterTile())
return
}
UnitActions.invokeUnitAction(unit, UnitActionType.FoundReligion)
}
fun enhanceReligion(unit: MapUnit) {
// Try go to a nearby city
if (!unit.getTile().isCityCenter())
UnitAutomation.tryEnterOwnClosestCity(unit)
// If we were unable to go there this turn, unable to do anything else
if (!unit.getTile().isCityCenter())
return
UnitActions.invokeUnitAction(unit, UnitActionType.EnhanceReligion)
}
}

View File

@ -179,16 +179,16 @@ object UnitAutomation {
}
if (unit.baseUnit.isAirUnit() && unit.canIntercept())
return SpecificUnitAutomation.automateFighter(unit)
return AirUnitAutomation.automateFighter(unit)
if (unit.baseUnit.isAirUnit() && !unit.baseUnit.isNuclearWeapon())
return SpecificUnitAutomation.automateBomber(unit)
return AirUnitAutomation.automateBomber(unit)
if (unit.baseUnit.isNuclearWeapon())
return SpecificUnitAutomation.automateNukes(unit)
return AirUnitAutomation.automateNukes(unit)
if (unit.hasUnique(UniqueType.SelfDestructs))
return SpecificUnitAutomation.automateMissile(unit)
return AirUnitAutomation.automateMissile(unit)
if (tryGoToRuinAndEncampment(unit) && unit.currentMovement == 0f) return
@ -271,13 +271,13 @@ object UnitAutomation {
&& unit.civ.religionManager.religionState < ReligionState.Religion
&& unit.civ.religionManager.mayFoundReligionAtAll()
)
return SpecificUnitAutomation.foundReligion(unit)
return ReligiousUnitAutomation.foundReligion(unit)
if (unit.hasUnique(UniqueType.MayEnhanceReligion)
&& unit.civ.religionManager.religionState < ReligionState.EnhancedReligion
&& unit.civ.religionManager.mayEnhanceReligionAtAll(unit)
)
return SpecificUnitAutomation.enhanceReligion(unit)
return ReligiousUnitAutomation.enhanceReligion(unit)
// We try to add any unit in the capital we can, though that might not always be desirable
// For now its a simple option to allow AI to win a science victory again
@ -295,10 +295,10 @@ object UnitAutomation {
return SpecificUnitAutomation.automateGreatGeneralFallback(unit)
if (unit.civ.religionManager.maySpreadReligionAtAll(unit))
return SpecificUnitAutomation.automateMissionary(unit)
return ReligiousUnitAutomation.automateMissionary(unit)
if (unit.hasUnique(UniqueType.PreventSpreadingReligion) || unit.canDoLimitedAction(Constants.removeHeresy))
return SpecificUnitAutomation.automateInquisitor(unit)
return ReligiousUnitAutomation.automateInquisitor(unit)
val isLateGame = isLateGame(unit.civ)
// Great scientist -> Hurry research if late game

View File

@ -380,7 +380,9 @@ class UnitMovement(val unit: MapUnit) {
unit.currentMovement = 0f
unit.mostRecentMoveType = UnitMovementMemoryType.UnitTeleported
return
} else if (unit.isPreparingParadrop()) { // paradropping units move differently
}
if (unit.isPreparingParadrop()) { // paradropping units move differently
unit.action = null
unit.removeFromTile()
unit.putInTile(destination)