mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-25 04:43:05 -04:00
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:
parent
f46b3fc62b
commit
f4b7822728
190
core/src/com/unciv/logic/automation/unit/AirUnitAutomation.kt
Normal file
190
core/src/com/unciv/logic/automation/unit/AirUnitAutomation.kt
Normal 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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user