mirror of
https://github.com/yairm210/Unciv.git
synced 2025-01-24 18:06:04 +07:00
Better barbarian automation (#1560)
This commit is contained in:
parent
02ec64f14f
commit
725edc2a31
200
core/src/com/unciv/logic/automation/BarbarianAutomation.kt
Normal file
200
core/src/com/unciv/logic/automation/BarbarianAutomation.kt
Normal file
@ -0,0 +1,200 @@
|
||||
package com.unciv.logic.automation
|
||||
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.battle.Battle
|
||||
import com.unciv.logic.battle.BattleDamage
|
||||
import com.unciv.logic.battle.MapUnitCombatant
|
||||
import com.unciv.logic.civilization.CivilizationInfo
|
||||
import com.unciv.logic.map.MapUnit
|
||||
import com.unciv.logic.map.PathsToTilesWithinTurn
|
||||
import com.unciv.logic.map.TileInfo
|
||||
import com.unciv.models.AttackableTile
|
||||
import com.unciv.models.UnitAction
|
||||
import com.unciv.models.UnitActionType
|
||||
import com.unciv.models.ruleset.unit.UnitType
|
||||
import com.unciv.ui.worldscreen.unit.UnitActions
|
||||
|
||||
class BarbarianAutomation(val civInfo: CivilizationInfo) {
|
||||
|
||||
private val battleHelper = BattleHelper()
|
||||
private val battleDamage = BattleDamage()
|
||||
|
||||
fun automate() {
|
||||
// ranged go first, after melee and then everyone else
|
||||
civInfo.getCivUnits().filter { it.type.isRanged() }.forEach(::automateUnit)
|
||||
civInfo.getCivUnits().filter { it.type.isMelee() }.forEach(::automateUnit)
|
||||
civInfo.getCivUnits().filter { !it.type.isRanged() && !it.type.isMelee() }.forEach(::automateUnit)
|
||||
}
|
||||
|
||||
private fun automateUnit(unit: MapUnit) {
|
||||
when {
|
||||
unit.currentTile.improvement == Constants.barbarianEncampment -> automateEncampment(unit)
|
||||
unit.type == UnitType.Scout -> automateScout(unit)
|
||||
else -> automateCombatUnit(unit)
|
||||
}
|
||||
}
|
||||
|
||||
private fun automateEncampment(unit: MapUnit) {
|
||||
val unitActions = UnitActions().getUnitActions(unit, UncivGame.Current.worldScreen)
|
||||
|
||||
// 1 - trying to upgrade
|
||||
if (tryUpgradeUnit(unit, unitActions)) return
|
||||
|
||||
// 2 - trying to attack somebody
|
||||
if (battleHelper.tryAttackNearbyEnemy(unit)) return
|
||||
|
||||
// 3 - at least fortifying
|
||||
unit.fortifyIfCan()
|
||||
}
|
||||
|
||||
private fun automateCombatUnit(unit: MapUnit) {
|
||||
val unitActions = UnitActions().getUnitActions(unit, UncivGame.Current.worldScreen)
|
||||
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
||||
val nearEnemyTiles = battleHelper.getAttackableEnemies(unit, unitDistanceToTiles)
|
||||
|
||||
// 1 - heal or fortifying if death is near
|
||||
if (unit.health < 50) {
|
||||
val possibleDamage = nearEnemyTiles
|
||||
.map {
|
||||
battleDamage.calculateDamageToAttacker(MapUnitCombatant(unit),
|
||||
Battle.getMapCombatantOfTile(it.tileToAttack)!!)
|
||||
}
|
||||
.sum()
|
||||
val possibleHeal = unit.rankTileForHealing(unit.currentTile)
|
||||
if (possibleDamage > possibleHeal) {
|
||||
// run
|
||||
val furthestTile = findFurthestTile(unit, unitDistanceToTiles, nearEnemyTiles)
|
||||
unit.movement.moveToTile(furthestTile)
|
||||
} else {
|
||||
// heal
|
||||
unit.fortifyIfCan()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 2 - trying to upgrade
|
||||
if (tryUpgradeUnit(unit, unitActions)) return
|
||||
|
||||
// 3 - trying to attack enemy
|
||||
// if a embarked melee unit can land and attack next turn, do not attack from water.
|
||||
if (battleHelper.tryDisembarkUnitToAttackPosition(unit, unitDistanceToTiles)) return
|
||||
if (battleHelper.tryAttackNearbyEnemy(unit)) return
|
||||
|
||||
// 4 - trying to pillage tile or route
|
||||
if (tryPillageImprovement(unit, unitDistanceToTiles, unitActions)) return
|
||||
|
||||
// 5 - heal the unit if needed
|
||||
if (unit.health < 100) {
|
||||
healUnit(unit, unitDistanceToTiles)
|
||||
return
|
||||
}
|
||||
|
||||
// 6 - wander
|
||||
UnitAutomation().wander(unit, unitDistanceToTiles)
|
||||
}
|
||||
|
||||
private fun automateScout(unit: MapUnit) {
|
||||
val unitActions = UnitActions().getUnitActions(unit, UncivGame.Current.worldScreen)
|
||||
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
||||
val nearEnemyTiles = battleHelper.getAttackableEnemies(unit, unitDistanceToTiles)
|
||||
|
||||
// 1 - heal or run if death is near
|
||||
if (unit.health < 50) {
|
||||
if (nearEnemyTiles.isNotEmpty()) {
|
||||
// run
|
||||
val furthestTile = findFurthestTile(unit, unitDistanceToTiles, nearEnemyTiles)
|
||||
unit.movement.moveToTile(furthestTile)
|
||||
} else {
|
||||
// heal
|
||||
unit.fortifyIfCan()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 2 - trying to capture someone
|
||||
// TODO
|
||||
|
||||
// 3 - trying to pillage tile or trade route
|
||||
if (tryPillageImprovement(unit, unitDistanceToTiles, unitActions)) return
|
||||
|
||||
// 4 - heal the unit if needed
|
||||
if (unit.health < 100) {
|
||||
healUnit(unit, unitDistanceToTiles)
|
||||
return
|
||||
}
|
||||
|
||||
// 5 - wander
|
||||
UnitAutomation().wander(unit, unitDistanceToTiles)
|
||||
}
|
||||
|
||||
private fun findFurthestTile(
|
||||
unit: MapUnit,
|
||||
unitDistanceToTiles: PathsToTilesWithinTurn,
|
||||
nearEnemyTiles: List<AttackableTile>
|
||||
): TileInfo {
|
||||
val possibleTiles = unitDistanceToTiles.keys.filter { unit.movement.canMoveTo(it) }
|
||||
val enemies = nearEnemyTiles.mapNotNull { it.tileToAttack.militaryUnit }
|
||||
var furthestTile: Pair<TileInfo, Float> = possibleTiles.random() to 0f
|
||||
for (enemy in enemies) {
|
||||
for (tile in possibleTiles) {
|
||||
val distance = enemy.movement.getMovementCostBetweenAdjacentTiles(enemy.currentTile, tile, enemy.civInfo)
|
||||
if (distance > furthestTile.second) {
|
||||
furthestTile = tile to distance
|
||||
}
|
||||
}
|
||||
}
|
||||
return furthestTile.first
|
||||
}
|
||||
|
||||
private fun healUnit(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn) {
|
||||
val currentUnitTile = unit.getTile()
|
||||
val bestTilesForHealing = unitDistanceToTiles.keys
|
||||
.filter { unit.movement.canMoveTo(it) }
|
||||
.groupBy { unit.rankTileForHealing(it) }
|
||||
.maxBy { it.key }
|
||||
|
||||
// within the tiles with best healing rate, we'll prefer one which has the highest defensive bonuses
|
||||
val bestTileForHealing = bestTilesForHealing?.value?.maxBy { it.getDefensiveBonus() }
|
||||
if (bestTileForHealing != null
|
||||
&& currentUnitTile != bestTileForHealing
|
||||
&& unit.rankTileForHealing(bestTileForHealing) > unit.rankTileForHealing(currentUnitTile)) {
|
||||
unit.movement.moveToTile(bestTileForHealing)
|
||||
}
|
||||
|
||||
unit.fortifyIfCan()
|
||||
}
|
||||
|
||||
private fun tryUpgradeUnit(unit: MapUnit, unitActions: List<UnitAction>): Boolean {
|
||||
if (unit.baseUnit().upgradesTo != null) {
|
||||
val upgradedUnit = unit.civInfo.gameInfo.ruleSet.units[unit.baseUnit().upgradesTo!!]!!
|
||||
if (upgradedUnit.isBuildable(unit.civInfo)) {
|
||||
val upgradeAction = unitActions.firstOrNull { it.type == UnitActionType.Upgrade }
|
||||
if (upgradeAction != null && upgradeAction.canAct) {
|
||||
upgradeAction.action?.invoke()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun tryPillageImprovement(
|
||||
unit: MapUnit,
|
||||
unitDistanceToTiles: PathsToTilesWithinTurn,
|
||||
unitActions: List<UnitAction>
|
||||
): Boolean {
|
||||
val tilesThatCanWalkToAndThenPillage = unitDistanceToTiles
|
||||
.filter { it.value.totalDistance < unit.currentMovement }.keys
|
||||
.filter { unit.movement.canMoveTo(it) && UnitActions().canPillage(unit, it) }
|
||||
|
||||
if (tilesThatCanWalkToAndThenPillage.isEmpty()) return false
|
||||
val tileToPillage = tilesThatCanWalkToAndThenPillage.maxBy { it.getDefensiveBonus() }!!
|
||||
if (unit.getTile() != tileToPillage) {
|
||||
unit.movement.moveToTile(tileToPillage)
|
||||
}
|
||||
|
||||
unitActions.first { it.type == UnitActionType.Pillage }.action?.invoke()
|
||||
return true
|
||||
}
|
||||
}
|
146
core/src/com/unciv/logic/automation/BattleHelper.kt
Normal file
146
core/src/com/unciv/logic/automation/BattleHelper.kt
Normal file
@ -0,0 +1,146 @@
|
||||
package com.unciv.logic.automation
|
||||
|
||||
import com.unciv.Constants
|
||||
import com.unciv.logic.battle.Battle
|
||||
import com.unciv.logic.battle.BattleDamage
|
||||
import com.unciv.logic.battle.ICombatant
|
||||
import com.unciv.logic.battle.MapUnitCombatant
|
||||
import com.unciv.logic.map.MapUnit
|
||||
import com.unciv.logic.map.PathsToTilesWithinTurn
|
||||
import com.unciv.logic.map.TileInfo
|
||||
import com.unciv.models.AttackableTile
|
||||
|
||||
class BattleHelper {
|
||||
|
||||
fun tryAttackNearbyEnemy(unit: MapUnit): Boolean {
|
||||
val attackableEnemies = getAttackableEnemies(unit, unit.movement.getDistanceToTiles())
|
||||
// Only take enemies we can fight without dying
|
||||
.filter {
|
||||
BattleDamage().calculateDamageToAttacker(MapUnitCombatant(unit),
|
||||
Battle.getMapCombatantOfTile(it.tileToAttack)!!) < unit.health
|
||||
}
|
||||
|
||||
val enemyTileToAttack = chooseAttackTarget(unit, attackableEnemies)
|
||||
|
||||
if (enemyTileToAttack != null) {
|
||||
Battle.moveAndAttack(MapUnitCombatant(unit), enemyTileToAttack)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun getAttackableEnemies(
|
||||
unit: MapUnit,
|
||||
unitDistanceToTiles: PathsToTilesWithinTurn,
|
||||
tilesToCheck: List<TileInfo>? = null
|
||||
): ArrayList<AttackableTile> {
|
||||
val tilesWithEnemies = (tilesToCheck ?: unit.civInfo.viewableTiles)
|
||||
.filter { containsAttackableEnemy(it, MapUnitCombatant(unit)) }
|
||||
|
||||
val rangeOfAttack = unit.getRange()
|
||||
|
||||
val attackableTiles = ArrayList<AttackableTile>()
|
||||
// The >0.1 (instead of >0) solves a bug where you've moved 2/3 road tiles,
|
||||
// you come to move a third (distance is less that remaining movements),
|
||||
// and then later we round it off to a whole.
|
||||
// So the poor unit thought it could attack from the tile, but when it comes to do so it has no movement points!
|
||||
// Silly floats, basically
|
||||
|
||||
val unitMustBeSetUp = unit.hasUnique("Must set up to ranged attack")
|
||||
val tilesToAttackFrom = if (unit.type.isAirUnit()) sequenceOf(unit.currentTile)
|
||||
else
|
||||
unitDistanceToTiles.asSequence()
|
||||
.filter {
|
||||
val movementPointsToExpendAfterMovement = if (unitMustBeSetUp) 1 else 0
|
||||
val movementPointsToExpendHere = if (unitMustBeSetUp && unit.action != Constants.unitActionSetUp) 1 else 0
|
||||
val movementPointsToExpendBeforeAttack = if (it.key == unit.currentTile) movementPointsToExpendHere else movementPointsToExpendAfterMovement
|
||||
unit.currentMovement - it.value.totalDistance - movementPointsToExpendBeforeAttack > 0.1
|
||||
} // still got leftover movement points after all that, to attack (0.1 is because of Float nonsense, see MapUnit.moveToTile(...)
|
||||
.map { it.key }
|
||||
.filter { unit.movement.canMoveTo(it) || it == unit.getTile() }
|
||||
|
||||
for (reachableTile in tilesToAttackFrom) { // tiles we'll still have energy after we reach there
|
||||
val tilesInAttackRange =
|
||||
if (unit.hasUnique("Ranged attacks may be performed over obstacles") || unit.type.isAirUnit())
|
||||
reachableTile.getTilesInDistance(rangeOfAttack)
|
||||
else reachableTile.getViewableTiles(rangeOfAttack, unit.type.isWaterUnit())
|
||||
|
||||
attackableTiles += tilesInAttackRange.asSequence().filter { it in tilesWithEnemies }
|
||||
.map { AttackableTile(reachableTile, it) }
|
||||
}
|
||||
return attackableTiles
|
||||
}
|
||||
|
||||
fun containsAttackableEnemy(tile: TileInfo, combatant: ICombatant): Boolean {
|
||||
if (combatant is MapUnitCombatant) {
|
||||
if (combatant.unit.isEmbarked()) {
|
||||
if (tile.isWater) return false // can't attack water units while embarked, only land
|
||||
if (combatant.isRanged()) return false
|
||||
}
|
||||
if (combatant.unit.hasUnique("Can only attack water")) {
|
||||
if (tile.isLand) return false
|
||||
|
||||
// trying to attack lake-to-coast or vice versa
|
||||
if ((tile.baseTerrain == Constants.lakes) != (combatant.getTile().baseTerrain == Constants.lakes))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val tileCombatant = Battle.getMapCombatantOfTile(tile) ?: return false
|
||||
if (tileCombatant.getCivInfo() == combatant.getCivInfo()) return false
|
||||
if (!combatant.getCivInfo().isAtWarWith(tileCombatant.getCivInfo())) return false
|
||||
|
||||
//only submarine and destroyer can attack submarine
|
||||
//garrisoned submarine can be attacked by anyone, or the city will be in invincible
|
||||
if (tileCombatant.isInvisible() && !tile.isCityCenter()) {
|
||||
if (combatant is MapUnitCombatant
|
||||
&& combatant.unit.hasUnique("Can attack submarines")
|
||||
&& combatant.getCivInfo().viewableInvisibleUnitsTiles.map { it.position }.contains(tile.position)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun tryDisembarkUnitToAttackPosition(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
|
||||
if (!unit.type.isMelee() || !unit.type.isLandUnit() || !unit.isEmbarked()) return false
|
||||
|
||||
val attackableEnemiesNextTurn = getAttackableEnemies(unit, unitDistanceToTiles)
|
||||
// Only take enemies we can fight without dying
|
||||
.filter {
|
||||
BattleDamage().calculateDamageToAttacker(MapUnitCombatant(unit),
|
||||
Battle.getMapCombatantOfTile(it.tileToAttack)!!) < unit.health
|
||||
}
|
||||
.filter { it.tileToAttackFrom.isLand }
|
||||
|
||||
val enemyTileToAttackNextTurn = chooseAttackTarget(unit, attackableEnemiesNextTurn)
|
||||
|
||||
if (enemyTileToAttackNextTurn != null) {
|
||||
unit.movement.moveToTile(enemyTileToAttackNextTurn.tileToAttackFrom)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun chooseAttackTarget(unit: MapUnit, attackableEnemies: List<AttackableTile>): AttackableTile? {
|
||||
val cityTilesToAttack = attackableEnemies.filter { it.tileToAttack.isCityCenter() }
|
||||
val nonCityTilesToAttack = attackableEnemies.filter { !it.tileToAttack.isCityCenter() }
|
||||
|
||||
// todo For air units, prefer to attack tiles with lower intercept chance
|
||||
|
||||
var enemyTileToAttack: AttackableTile? = null
|
||||
val capturableCity = cityTilesToAttack.firstOrNull { it.tileToAttack.getCity()!!.health == 1 }
|
||||
val cityWithHealthLeft = cityTilesToAttack.filter { it.tileToAttack.getCity()!!.health != 1 } // don't want ranged units to attack defeated cities
|
||||
.minBy { it.tileToAttack.getCity()!!.health }
|
||||
|
||||
if (unit.type.isMelee() && capturableCity != null)
|
||||
enemyTileToAttack = capturableCity // enter it quickly, top priority!
|
||||
|
||||
else if (nonCityTilesToAttack.isNotEmpty()) // second priority, units
|
||||
enemyTileToAttack = nonCityTilesToAttack.minBy { Battle.getMapCombatantOfTile(it.tileToAttack)!!.getHealth() }
|
||||
else if (cityWithHealthLeft != null) enemyTileToAttack = cityWithHealthLeft // third priority, city
|
||||
|
||||
return enemyTileToAttack
|
||||
}
|
||||
}
|
@ -19,27 +19,32 @@ class NextTurnAutomation{
|
||||
|
||||
/** Top-level AI turn tasklist */
|
||||
fun automateCivMoves(civInfo: CivilizationInfo) {
|
||||
respondToDemands(civInfo)
|
||||
respondToTradeRequests(civInfo)
|
||||
|
||||
if(civInfo.isMajorCiv()) {
|
||||
offerPeaceTreaty(civInfo)
|
||||
exchangeTechs(civInfo)
|
||||
exchangeLuxuries(civInfo)
|
||||
issueRequests(civInfo)
|
||||
adoptPolicy(civInfo)
|
||||
if (civInfo.isBarbarian()) {
|
||||
BarbarianAutomation(civInfo).automate()
|
||||
} else {
|
||||
getFreeTechForCityStates(civInfo)
|
||||
respondToDemands(civInfo)
|
||||
respondToTradeRequests(civInfo)
|
||||
|
||||
if(civInfo.isMajorCiv()) {
|
||||
offerPeaceTreaty(civInfo)
|
||||
exchangeTechs(civInfo)
|
||||
exchangeLuxuries(civInfo)
|
||||
issueRequests(civInfo)
|
||||
adoptPolicy(civInfo)
|
||||
} else {
|
||||
getFreeTechForCityStates(civInfo)
|
||||
}
|
||||
|
||||
chooseTechToResearch(civInfo)
|
||||
updateDiplomaticRelationship(civInfo)
|
||||
declareWar(civInfo)
|
||||
automateCityBombardment(civInfo)
|
||||
useGold(civInfo)
|
||||
automateUnits(civInfo)
|
||||
reassignWorkedTiles(civInfo)
|
||||
trainSettler(civInfo)
|
||||
}
|
||||
|
||||
chooseTechToResearch(civInfo)
|
||||
updateDiplomaticRelationship(civInfo)
|
||||
declareWar(civInfo)
|
||||
automateCityBombardment(civInfo)
|
||||
useGold(civInfo)
|
||||
automateUnits(civInfo)
|
||||
reassignWorkedTiles(civInfo)
|
||||
trainSettler(civInfo)
|
||||
civInfo.popupAlerts.clear() // AIs don't care about popups.
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,8 @@ import com.unciv.ui.worldscreen.unit.UnitActions
|
||||
|
||||
class SpecificUnitAutomation{
|
||||
|
||||
private val battleHelper = BattleHelper()
|
||||
|
||||
private fun hasWorkableSeaResource(tileInfo: TileInfo, civInfo: CivilizationInfo): Boolean {
|
||||
return tileInfo.hasViewableResource(civInfo) && tileInfo.isWater && tileInfo.improvement==null
|
||||
}
|
||||
@ -186,14 +188,14 @@ class SpecificUnitAutomation{
|
||||
.flatMap { it.airUnits }.filter { it.civInfo.isAtWarWith(unit.civInfo) }
|
||||
|
||||
if(enemyAirUnitsInRange.isNotEmpty()) return // we need to be on standby in case they attack
|
||||
if(UnitAutomation().tryAttackNearbyEnemy(unit)) return
|
||||
if(battleHelper.tryAttackNearbyEnemy(unit)) return
|
||||
|
||||
val immediatelyReachableCities = tilesInRange
|
||||
.filter { it.isCityCenter() && it.getOwner()==unit.civInfo && unit.movement.canMoveTo(it)}
|
||||
|
||||
for(city in immediatelyReachableCities){
|
||||
if(city.getTilesInDistance(unit.getRange())
|
||||
.any { UnitAutomation().containsAttackableEnemy(it,MapUnitCombatant(unit)) }) {
|
||||
.any { battleHelper.containsAttackableEnemy(it,MapUnitCombatant(unit)) }) {
|
||||
unit.movement.moveToTile(city)
|
||||
return
|
||||
}
|
||||
@ -220,7 +222,7 @@ class SpecificUnitAutomation{
|
||||
}
|
||||
|
||||
fun automateBomber(unit: MapUnit) {
|
||||
if (UnitAutomation().tryAttackNearbyEnemy(unit)) return
|
||||
if (battleHelper.tryAttackNearbyEnemy(unit)) return
|
||||
|
||||
val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange())
|
||||
|
||||
@ -229,7 +231,7 @@ class SpecificUnitAutomation{
|
||||
|
||||
for (city in immediatelyReachableCities) {
|
||||
if (city.getTilesInDistance(unit.getRange())
|
||||
.any { UnitAutomation().containsAttackableEnemy(it, MapUnitCombatant(unit)) }) {
|
||||
.any { battleHelper.containsAttackableEnemy(it, MapUnitCombatant(unit)) }) {
|
||||
unit.movement.moveToTile(city)
|
||||
return
|
||||
}
|
||||
@ -245,7 +247,7 @@ class SpecificUnitAutomation{
|
||||
.filter {
|
||||
it != airUnit.currentTile
|
||||
&& it.getTilesInDistance(airUnit.getRange())
|
||||
.any { UnitAutomation().containsAttackableEnemy(it, MapUnitCombatant(airUnit)) }
|
||||
.any { battleHelper.containsAttackableEnemy(it, MapUnitCombatant(airUnit)) }
|
||||
}
|
||||
if (citiesThatCanAttackFrom.isEmpty()) return
|
||||
|
||||
@ -258,7 +260,7 @@ class SpecificUnitAutomation{
|
||||
|
||||
// This really needs to be changed, to have better targetting for missiles
|
||||
fun automateMissile(unit: MapUnit) {
|
||||
if (UnitAutomation().tryAttackNearbyEnemy(unit)) return
|
||||
if (battleHelper.tryAttackNearbyEnemy(unit)) return
|
||||
|
||||
val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange())
|
||||
|
||||
@ -267,7 +269,7 @@ class SpecificUnitAutomation{
|
||||
|
||||
for (city in immediatelyReachableCities) {
|
||||
if (city.getTilesInDistance(unit.getRange())
|
||||
.any { UnitAutomation().containsAttackableEnemy(it, MapUnitCombatant(unit)) }) {
|
||||
.any { battleHelper.containsAttackableEnemy(it, MapUnitCombatant(unit)) }) {
|
||||
unit.movement.moveToTile(city)
|
||||
return
|
||||
}
|
||||
|
@ -3,7 +3,10 @@ package com.unciv.logic.automation
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.battle.*
|
||||
import com.unciv.logic.battle.Battle
|
||||
import com.unciv.logic.battle.BattleDamage
|
||||
import com.unciv.logic.battle.CityCombatant
|
||||
import com.unciv.logic.battle.MapUnitCombatant
|
||||
import com.unciv.logic.city.CityInfo
|
||||
import com.unciv.logic.civilization.GreatPersonManager
|
||||
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
|
||||
@ -15,15 +18,19 @@ import com.unciv.models.UnitActionType
|
||||
import com.unciv.models.ruleset.unit.UnitType
|
||||
import com.unciv.ui.worldscreen.unit.UnitActions
|
||||
|
||||
|
||||
class UnitAutomation{
|
||||
class UnitAutomation {
|
||||
|
||||
companion object {
|
||||
const val CLOSE_ENEMY_TILES_AWAY_LIMIT = 5
|
||||
const val CLOSE_ENEMY_TURNS_AWAY_LIMIT = 3f
|
||||
}
|
||||
|
||||
private val battleHelper = BattleHelper()
|
||||
|
||||
fun automateUnitMoves(unit: MapUnit) {
|
||||
if (unit.civInfo.isBarbarian()) {
|
||||
throw IllegalStateException("Barbarians is not allowed here.")
|
||||
}
|
||||
|
||||
if (unit.name == Constants.settler) {
|
||||
return SpecificUnitAutomation().automateSettlerActions(unit)
|
||||
@ -33,38 +40,32 @@ class UnitAutomation{
|
||||
return WorkerAutomation(unit).automateWorkerAction()
|
||||
}
|
||||
|
||||
if(unit.name=="Work Boats"){
|
||||
if (unit.name == "Work Boats") {
|
||||
return SpecificUnitAutomation().automateWorkBoats(unit)
|
||||
}
|
||||
|
||||
if (unit.name == "Great General")
|
||||
return SpecificUnitAutomation().automateGreatGeneral(unit)
|
||||
|
||||
if(unit.type==UnitType.Fighter)
|
||||
if (unit.type == UnitType.Fighter)
|
||||
return SpecificUnitAutomation().automateFighter(unit)
|
||||
|
||||
if(unit.type==UnitType.Bomber)
|
||||
if (unit.type == UnitType.Bomber)
|
||||
return SpecificUnitAutomation().automateBomber(unit)
|
||||
|
||||
if(unit.type==UnitType.Missile)
|
||||
if (unit.type == UnitType.Missile)
|
||||
return SpecificUnitAutomation().automateMissile(unit)
|
||||
|
||||
if(unit.name.startsWith("Great")
|
||||
&& unit.name in GreatPersonManager().statToGreatPersonMapping.values){ // So "Great War Infantry" isn't caught here
|
||||
if (unit.name.startsWith("Great")
|
||||
&& unit.name in GreatPersonManager().statToGreatPersonMapping.values) { // So "Great War Infantry" isn't caught here
|
||||
return SpecificUnitAutomation().automateGreatPerson(unit)
|
||||
}
|
||||
|
||||
val unitActions = UnitActions().getUnitActions(unit,UncivGame.Current.worldScreen)
|
||||
val unitActions = UnitActions().getUnitActions(unit, UncivGame.Current.worldScreen)
|
||||
var unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
||||
|
||||
if(unit.civInfo.isBarbarian() &&
|
||||
unit.currentTile.improvement==Constants.barbarianEncampment && unit.type.isLandUnit()) {
|
||||
if(unit.canFortify()) unit.fortify()
|
||||
return // stay in the encampment
|
||||
}
|
||||
|
||||
if(tryGoToRuin(unit,unitDistanceToTiles)){
|
||||
if(unit.currentMovement==0f) return
|
||||
if (tryGoToRuin(unit, unitDistanceToTiles)) {
|
||||
if (unit.currentMovement == 0f) return
|
||||
unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
||||
}
|
||||
|
||||
@ -73,18 +74,13 @@ class UnitAutomation{
|
||||
// Accompany settlers
|
||||
if (tryAccompanySettlerOrGreatPerson(unit)) return
|
||||
|
||||
if (unit.health < 50 && tryHealUnit(unit,unitDistanceToTiles)) return // do nothing but heal
|
||||
if (unit.health < 50 && tryHealUnit(unit, unitDistanceToTiles)) return // do nothing but heal
|
||||
|
||||
// if a embarked melee unit can land and attack next turn, do not attack from water.
|
||||
if (unit.type.isLandUnit() && unit.type.isMelee() && unit.isEmbarked()) {
|
||||
if (tryDisembarkUnitToAttackPosition(unit,unitDistanceToTiles)) return
|
||||
}
|
||||
if (battleHelper.tryDisembarkUnitToAttackPosition(unit, unitDistanceToTiles)) return
|
||||
|
||||
// if there is an attackable unit in the vicinity, attack!
|
||||
if (tryAttackNearbyEnemy(unit)) return
|
||||
|
||||
// Barbarians try to pillage improvements if no targets reachable
|
||||
if (unit.civInfo.isBarbarian() && tryPillageImprovement(unit, unitDistanceToTiles)) return
|
||||
if (battleHelper.tryAttackNearbyEnemy(unit)) return
|
||||
|
||||
if (tryGarrisoningUnit(unit)) return
|
||||
|
||||
@ -98,47 +94,42 @@ class UnitAutomation{
|
||||
// Focus all units without a specific target on the enemy city closest to one of our cities
|
||||
if (tryHeadTowardsEnemyCity(unit)) return
|
||||
|
||||
if(tryHeadTowardsEncampment(unit)) return
|
||||
if (tryHeadTowardsEncampment(unit)) return
|
||||
|
||||
// else, try to go o unreached tiles
|
||||
if(tryExplore(unit,unitDistanceToTiles)) return
|
||||
|
||||
// Barbarians just wander all over the place
|
||||
if(unit.civInfo.isBarbarian())
|
||||
wander(unit,unitDistanceToTiles)
|
||||
if (tryExplore(unit, unitDistanceToTiles)) return
|
||||
}
|
||||
|
||||
private fun tryHeadTowardsEncampment(unit: MapUnit): Boolean {
|
||||
if(unit.civInfo.isBarbarian()) return false
|
||||
if(unit.type==UnitType.Missile) return false // don't use missiles against barbarians...
|
||||
if (unit.type == UnitType.Missile) return false // don't use missiles against barbarians...
|
||||
val knownEncampments = unit.civInfo.gameInfo.tileMap.values.asSequence()
|
||||
.filter { it.improvement==Constants.barbarianEncampment && unit.civInfo.exploredTiles.contains(it.position) }
|
||||
.filter { it.improvement == Constants.barbarianEncampment && unit.civInfo.exploredTiles.contains(it.position) }
|
||||
val cities = unit.civInfo.cities
|
||||
val encampmentsCloseToCities
|
||||
= knownEncampments.filter { cities.any { city -> city.getCenterTile().arialDistanceTo(it) < 6 } }
|
||||
val encampmentsCloseToCities = knownEncampments.filter { cities.any { city -> city.getCenterTile().arialDistanceTo(it) < 6 } }
|
||||
.sortedBy { it.arialDistanceTo(unit.currentTile) }
|
||||
val encampmentToHeadTowards = encampmentsCloseToCities.firstOrNull { unit.movement.canReach(it) }
|
||||
if(encampmentToHeadTowards==null) return false
|
||||
if (encampmentToHeadTowards == null) {
|
||||
return false
|
||||
}
|
||||
unit.movement.headTowards(encampmentToHeadTowards)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
fun tryHealUnit(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn):Boolean {
|
||||
private fun tryHealUnit(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
|
||||
val tilesInDistance = unitDistanceToTiles.keys.filter { unit.movement.canMoveTo(it) }
|
||||
if(unitDistanceToTiles.isEmpty()) return true // can't move, so...
|
||||
if (unitDistanceToTiles.isEmpty()) return true // can't move, so...
|
||||
val currentUnitTile = unit.getTile()
|
||||
|
||||
if (tryPillageImprovement(unit, unitDistanceToTiles)) return true
|
||||
|
||||
val tilesByHealingRate = tilesInDistance.groupBy { unit.rankTileForHealing(it) }
|
||||
|
||||
if(tilesByHealingRate.keys.none { it!=0 }){// We can't heal here at all! We're probably embarked
|
||||
if (tilesByHealingRate.keys.none { it != 0 }) { // We can't heal here at all! We're probably embarked
|
||||
val reachableCityTile = unit.civInfo.cities.map { it.getCenterTile() }
|
||||
.sortedBy { it.arialDistanceTo(unit.currentTile) }
|
||||
.firstOrNull{unit.movement.canReach(it)}
|
||||
if(reachableCityTile!=null) unit.movement.headTowards(reachableCityTile)
|
||||
else wander(unit,unitDistanceToTiles)
|
||||
.firstOrNull { unit.movement.canReach(it) }
|
||||
if (reachableCityTile != null) unit.movement.headTowards(reachableCityTile)
|
||||
else wander(unit, unitDistanceToTiles)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -147,23 +138,23 @@ class UnitAutomation{
|
||||
val bestTileForHealing = bestTilesForHealing.maxBy { it.getDefensiveBonus() }!!
|
||||
val bestTileForHealingRank = unit.rankTileForHealing(bestTileForHealing)
|
||||
|
||||
if(currentUnitTile!=bestTileForHealing
|
||||
&& bestTileForHealingRank > unit.rankTileForHealing(currentUnitTile))
|
||||
if (currentUnitTile != bestTileForHealing
|
||||
&& bestTileForHealingRank > unit.rankTileForHealing(currentUnitTile))
|
||||
unit.movement.moveToTile(bestTileForHealing)
|
||||
|
||||
if(unit.currentMovement>0 && unit.canFortify()) unit.fortify()
|
||||
if (unit.currentMovement > 0 && unit.canFortify()) unit.fortify()
|
||||
return true
|
||||
}
|
||||
|
||||
fun tryPillageImprovement(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn) : Boolean {
|
||||
if(unit.type.isCivilian()) return false
|
||||
private fun tryPillageImprovement(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
|
||||
if (unit.type.isCivilian()) return false
|
||||
val tilesThatCanWalkToAndThenPillage = unitDistanceToTiles
|
||||
.filter {it.value.totalDistance < unit.currentMovement}.keys
|
||||
.filter { unit.movement.canMoveTo(it) && UnitActions().canPillage(unit,it) }
|
||||
.filter { it.value.totalDistance < unit.currentMovement }.keys
|
||||
.filter { unit.movement.canMoveTo(it) && UnitActions().canPillage(unit, it) }
|
||||
|
||||
if (tilesThatCanWalkToAndThenPillage.isEmpty()) return false
|
||||
val tileToPillage = tilesThatCanWalkToAndThenPillage.maxBy { it.getDefensiveBonus() }!!
|
||||
if (unit.getTile()!=tileToPillage)
|
||||
if (unit.getTile() != tileToPillage)
|
||||
unit.movement.moveToTile(tileToPillage)
|
||||
|
||||
UnitActions().getUnitActions(unit, UncivGame.Current.worldScreen)
|
||||
@ -171,86 +162,9 @@ class UnitAutomation{
|
||||
return true
|
||||
}
|
||||
|
||||
fun containsAttackableEnemy(tile: TileInfo, combatant: ICombatant): Boolean {
|
||||
if(combatant is MapUnitCombatant) {
|
||||
if (combatant.unit.isEmbarked()) {
|
||||
if (tile.isWater) return false // can't attack water units while embarked, only land
|
||||
if (combatant.isRanged()) return false
|
||||
}
|
||||
if (combatant.unit.hasUnique("Can only attack water")) {
|
||||
if (tile.isLand) return false
|
||||
|
||||
// trying to attack lake-to-coast or vice versa
|
||||
if ((tile.baseTerrain == Constants.lakes) != (combatant.getTile().baseTerrain == Constants.lakes))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val tileCombatant = Battle.getMapCombatantOfTile(tile)
|
||||
if(tileCombatant==null) return false
|
||||
if(tileCombatant.getCivInfo()==combatant.getCivInfo() ) return false
|
||||
if(!combatant.getCivInfo().isAtWarWith(tileCombatant.getCivInfo())) return false
|
||||
|
||||
//only submarine and destroyer can attack submarine
|
||||
//garrisoned submarine can be attacked by anyone, or the city will be in invincible
|
||||
if (tileCombatant.isInvisible() && !tile.isCityCenter()) {
|
||||
if (combatant is MapUnitCombatant
|
||||
&& combatant.unit.hasUnique("Can attack submarines")
|
||||
&& combatant.getCivInfo().viewableInvisibleUnitsTiles.map { it.position }.contains(tile.position)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
class AttackableTile(val tileToAttackFrom:TileInfo, val tileToAttack:TileInfo)
|
||||
|
||||
fun getAttackableEnemies(
|
||||
unit: MapUnit,
|
||||
unitDistanceToTiles: PathsToTilesWithinTurn,
|
||||
tilesToCheck: List<TileInfo>? = null
|
||||
): ArrayList<AttackableTile> {
|
||||
val tilesWithEnemies = (tilesToCheck ?: unit.civInfo.viewableTiles)
|
||||
.filter { containsAttackableEnemy(it, MapUnitCombatant(unit)) }
|
||||
|
||||
val rangeOfAttack = unit.getRange()
|
||||
|
||||
val attackableTiles = ArrayList<AttackableTile>()
|
||||
// The >0.1 (instead of >0) solves a bug where you've moved 2/3 road tiles,
|
||||
// you come to move a third (distance is less that remaining movements),
|
||||
// and then later we round it off to a whole.
|
||||
// So the poor unit thought it could attack from the tile, but when it comes to do so it has no movement points!
|
||||
// Silly floats, basically
|
||||
|
||||
val unitMustBeSetUp = unit.hasUnique("Must set up to ranged attack")
|
||||
val tilesToAttackFrom = if (unit.type.isAirUnit()) sequenceOf(unit.currentTile)
|
||||
else
|
||||
unitDistanceToTiles.asSequence()
|
||||
.filter {
|
||||
val movementPointsToExpendAfterMovement = if (unitMustBeSetUp) 1 else 0
|
||||
val movementPointsToExpendHere = if (unitMustBeSetUp && unit.action != Constants.unitActionSetUp) 1 else 0
|
||||
val movementPointsToExpendBeforeAttack = if (it.key == unit.currentTile) movementPointsToExpendHere else movementPointsToExpendAfterMovement
|
||||
unit.currentMovement - it.value.totalDistance - movementPointsToExpendBeforeAttack > 0.1
|
||||
} // still got leftover movement points after all that, to attack (0.1 is because of Float nonsense, see MapUnit.moveToTile(...)
|
||||
.map { it.key }
|
||||
.filter { unit.movement.canMoveTo(it) || it == unit.getTile() }
|
||||
|
||||
for (reachableTile in tilesToAttackFrom) { // tiles we'll still have energy after we reach there
|
||||
val tilesInAttackRange =
|
||||
if (unit.hasUnique("Ranged attacks may be performed over obstacles") || unit.type.isAirUnit())
|
||||
reachableTile.getTilesInDistance(rangeOfAttack)
|
||||
else reachableTile.getViewableTiles(rangeOfAttack, unit.type.isWaterUnit())
|
||||
|
||||
attackableTiles += tilesInAttackRange.asSequence().filter { it in tilesWithEnemies }
|
||||
.map { AttackableTile(reachableTile, it) }
|
||||
}
|
||||
return attackableTiles
|
||||
}
|
||||
|
||||
fun getBombardTargets(city: CityInfo): List<TileInfo> {
|
||||
return city.getCenterTile().getViewableTiles(city.range,true)
|
||||
.filter { containsAttackableEnemy(it, CityCombatant(city)) }
|
||||
return city.getCenterTile().getViewableTiles(city.range, true)
|
||||
.filter { battleHelper.containsAttackableEnemy(it, CityCombatant(city)) }
|
||||
}
|
||||
|
||||
/** Move towards the closest attackable enemy of the [unit].
|
||||
@ -260,20 +174,21 @@ class UnitAutomation{
|
||||
private fun tryAdvanceTowardsCloseEnemy(unit: MapUnit): Boolean {
|
||||
// this can be sped up if we check each layer separately
|
||||
val unitDistanceToTiles = unit.movement.getDistanceToTilesWithinTurn(
|
||||
unit.getTile().position,
|
||||
unit.getMaxMovement() * CLOSE_ENEMY_TURNS_AWAY_LIMIT
|
||||
unit.getTile().position,
|
||||
unit.getMaxMovement() * CLOSE_ENEMY_TURNS_AWAY_LIMIT
|
||||
)
|
||||
var closeEnemies = getAttackableEnemies(
|
||||
unit,
|
||||
unitDistanceToTiles,
|
||||
tilesToCheck = unit.getTile().getTilesInDistance(CLOSE_ENEMY_TILES_AWAY_LIMIT)
|
||||
).filter { // Ignore units that would 1-shot you if you attacked
|
||||
var closeEnemies = battleHelper.getAttackableEnemies(
|
||||
unit,
|
||||
unitDistanceToTiles,
|
||||
tilesToCheck = unit.getTile().getTilesInDistance(CLOSE_ENEMY_TILES_AWAY_LIMIT)
|
||||
).filter {
|
||||
// Ignore units that would 1-shot you if you attacked
|
||||
BattleDamage().calculateDamageToAttacker(MapUnitCombatant(unit),
|
||||
Battle.getMapCombatantOfTile(it.tileToAttack)!!) < unit.health
|
||||
}
|
||||
|
||||
if(unit.type.isRanged())
|
||||
closeEnemies = closeEnemies.filterNot { it.tileToAttack.isCityCenter() && it.tileToAttack.getCity()!!.health==1 }
|
||||
if (unit.type.isRanged())
|
||||
closeEnemies = closeEnemies.filterNot { it.tileToAttack.isCityCenter() && it.tileToAttack.getCity()!!.health == 1 }
|
||||
|
||||
val closestEnemy = closeEnemies.minBy { it.tileToAttack.arialDistanceTo(unit.getTile()) }
|
||||
|
||||
@ -286,10 +201,12 @@ class UnitAutomation{
|
||||
|
||||
private fun tryAccompanySettlerOrGreatPerson(unit: MapUnit): Boolean {
|
||||
val settlerOrGreatPersonToAccompany = unit.civInfo.getCivUnits()
|
||||
.firstOrNull { val tile = it.currentTile
|
||||
(it.name== Constants.settler || it.name in GreatPersonManager().statToGreatPersonMapping.values)
|
||||
&& tile.militaryUnit==null && unit.movement.canMoveTo(tile) && unit.movement.canReach(tile) }
|
||||
if(settlerOrGreatPersonToAccompany==null) return false
|
||||
.firstOrNull {
|
||||
val tile = it.currentTile
|
||||
(it.name == Constants.settler || it.name in GreatPersonManager().statToGreatPersonMapping.values)
|
||||
&& tile.militaryUnit == null && unit.movement.canMoveTo(tile) && unit.movement.canReach(tile)
|
||||
}
|
||||
if (settlerOrGreatPersonToAccompany == null) return false
|
||||
unit.movement.headTowards(settlerOrGreatPersonToAccompany.currentTile)
|
||||
return true
|
||||
}
|
||||
@ -309,19 +226,20 @@ class UnitAutomation{
|
||||
}
|
||||
|
||||
private fun tryHeadTowardsEnemyCity(unit: MapUnit): Boolean {
|
||||
if(unit.civInfo.cities.isEmpty()) return false
|
||||
if (unit.civInfo.cities.isEmpty()) return false
|
||||
|
||||
var enemyCities = unit.civInfo.gameInfo.civilizations
|
||||
.filter { unit.civInfo.isAtWarWith(it) }
|
||||
.flatMap { it.cities }.asSequence()
|
||||
.filter { it.location in unit.civInfo.exploredTiles }
|
||||
|
||||
if(unit.type.isRanged()) // ranged units don't harm capturable cities, waste of a turn
|
||||
enemyCities = enemyCities.filterNot { it.health==1 }
|
||||
if (unit.type.isRanged()) // ranged units don't harm capturable cities, waste of a turn
|
||||
enemyCities = enemyCities.filterNot { it.health == 1 }
|
||||
|
||||
val closestReachableEnemyCity = enemyCities
|
||||
.asSequence().map { it.getCenterTile() }
|
||||
.sortedBy { cityCenterTile -> // sort enemy cities by closeness to our cities, and only then choose the first reachable - checking canReach is comparatively very time-intensive!
|
||||
.sortedBy { cityCenterTile ->
|
||||
// sort enemy cities by closeness to our cities, and only then choose the first reachable - checking canReach is comparatively very time-intensive!
|
||||
unit.civInfo.cities.asSequence().map { cityCenterTile.arialDistanceTo(it.getCenterTile()) }.min()!!
|
||||
}
|
||||
.firstOrNull { unit.movement.canReach(it) }
|
||||
@ -330,44 +248,38 @@ class UnitAutomation{
|
||||
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
||||
val tilesInBombardRange = closestReachableEnemyCity.getTilesInDistance(2)
|
||||
val reachableTilesNotInBombardRange = unitDistanceToTiles.keys.filter { it !in tilesInBombardRange }
|
||||
val canMoveIntoBombardRange = tilesInBombardRange.any { unitDistanceToTiles.containsKey(it)}
|
||||
|
||||
val suitableGatheringGroundTiles = closestReachableEnemyCity.getTilesAtDistance(4)
|
||||
.union(closestReachableEnemyCity.getTilesAtDistance(3))
|
||||
.filter { it.isLand }
|
||||
val closestReachableLandingGroundTile = suitableGatheringGroundTiles
|
||||
.sortedBy { it.arialDistanceTo(unit.currentTile) }
|
||||
.firstOrNull { unit.movement.canReach(it) }
|
||||
|
||||
// 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 = if(closestReachableLandingGroundTile!=null) closestReachableLandingGroundTile
|
||||
else closestReachableEnemyCity
|
||||
val tileToHeadTo = suitableGatheringGroundTiles
|
||||
.sortedBy { it.arialDistanceTo(unit.currentTile) }
|
||||
.firstOrNull { unit.movement.canReach(it) } ?: closestReachableEnemyCity
|
||||
|
||||
|
||||
if(tileToHeadTo !in tilesInBombardRange) // no need to worry, keep going as the movement alg. says
|
||||
if (tileToHeadTo !in tilesInBombardRange) // no need to worry, keep going as the movement alg. says
|
||||
unit.movement.headTowards(tileToHeadTo)
|
||||
|
||||
else{
|
||||
if(unit.getRange()>2){ // should never be in a bombardable position
|
||||
else {
|
||||
if (unit.getRange() > 2) { // should never be in a bombardable position
|
||||
val tilesCanAttackFromButNotInBombardRange =
|
||||
reachableTilesNotInBombardRange.filter{it.arialDistanceTo(closestReachableEnemyCity) <= unit.getRange()}
|
||||
reachableTilesNotInBombardRange.filter { it.arialDistanceTo(closestReachableEnemyCity) <= unit.getRange() }
|
||||
|
||||
// move into position far away enough that the bombard doesn't hurt
|
||||
if(tilesCanAttackFromButNotInBombardRange.any())
|
||||
if (tilesCanAttackFromButNotInBombardRange.any())
|
||||
unit.movement.headTowards(tilesCanAttackFromButNotInBombardRange.minBy { unitDistanceToTiles[it]!!.totalDistance }!!)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// calculate total damage of units in surrounding 4-spaces from enemy city (so we can attack a city from 2 directions at once)
|
||||
val militaryUnitsAroundEnemyCity = closestReachableEnemyCity.getTilesInDistance(3)
|
||||
.filter { it.militaryUnit!=null && it.militaryUnit!!.civInfo == unit.civInfo }
|
||||
.filter { it.militaryUnit != null && it.militaryUnit!!.civInfo == unit.civInfo }
|
||||
.map { it.militaryUnit!! }
|
||||
var totalAttackOnCityPerTurn = -20 // cities heal 20 per turn, so anything below that its useless
|
||||
val enemyCityCombatant = CityCombatant(closestReachableEnemyCity.getCity()!!)
|
||||
for(militaryUnit in militaryUnitsAroundEnemyCity){
|
||||
totalAttackOnCityPerTurn += BattleDamage().calculateDamageToDefender(MapUnitCombatant(militaryUnit), enemyCityCombatant)
|
||||
for (militaryUnit in militaryUnitsAroundEnemyCity) {
|
||||
totalAttackOnCityPerTurn += BattleDamage().calculateDamageToDefender(MapUnitCombatant(militaryUnit), enemyCityCombatant)
|
||||
}
|
||||
if(totalAttackOnCityPerTurn * 3 > closestReachableEnemyCity.getCity()!!.health) // if we can defeat it in 3 turns with the current units,
|
||||
if (totalAttackOnCityPerTurn * 3 > closestReachableEnemyCity.getCity()!!.health) // if we can defeat it in 3 turns with the current units,
|
||||
unit.movement.headTowards(closestReachableEnemyCity) // go for it!
|
||||
}
|
||||
}
|
||||
@ -377,42 +289,6 @@ class UnitAutomation{
|
||||
return false
|
||||
}
|
||||
|
||||
private fun tryDisembarkUnitToAttackPosition(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
|
||||
if (!unit.type.isMelee() || !unit.type.isLandUnit() || !unit.isEmbarked()) return false
|
||||
val attackableEnemiesNextTurn = getAttackableEnemies(unit, unitDistanceToTiles)
|
||||
// Only take enemies we can fight without dying
|
||||
.filter {
|
||||
BattleDamage().calculateDamageToAttacker(MapUnitCombatant(unit),
|
||||
Battle.getMapCombatantOfTile(it.tileToAttack)!!) < unit.health
|
||||
}
|
||||
.filter {it.tileToAttackFrom.isLand}
|
||||
|
||||
val enemyTileToAttackNextTurn = chooseAttackTarget(unit, attackableEnemiesNextTurn)
|
||||
|
||||
if (enemyTileToAttackNextTurn != null) {
|
||||
unit.movement.moveToTile(enemyTileToAttackNextTurn.tileToAttackFrom)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun tryAttackNearbyEnemy(unit: MapUnit): Boolean {
|
||||
val attackableEnemies = getAttackableEnemies(unit, unit.movement.getDistanceToTiles())
|
||||
// Only take enemies we can fight without dying
|
||||
.filter {
|
||||
BattleDamage().calculateDamageToAttacker(MapUnitCombatant(unit),
|
||||
Battle.getMapCombatantOfTile(it.tileToAttack)!!) < unit.health
|
||||
}
|
||||
|
||||
val enemyTileToAttack = chooseAttackTarget(unit, attackableEnemies)
|
||||
|
||||
if (enemyTileToAttack != null) {
|
||||
Battle.moveAndAttack(MapUnitCombatant(unit), enemyTileToAttack)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun tryBombardEnemy(city: CityInfo): Boolean {
|
||||
if (!city.attackedThisTurn) {
|
||||
val target = chooseBombardTarget(city)
|
||||
@ -424,48 +300,26 @@ class UnitAutomation{
|
||||
return false
|
||||
}
|
||||
|
||||
private fun chooseAttackTarget(unit: MapUnit, attackableEnemies: List<AttackableTile>): AttackableTile? {
|
||||
val cityTilesToAttack = attackableEnemies.filter { it.tileToAttack.isCityCenter() }
|
||||
val nonCityTilesToAttack = attackableEnemies.filter { !it.tileToAttack.isCityCenter() }
|
||||
|
||||
// todo For air units, prefer to attack tiles with lower intercept chance
|
||||
|
||||
var enemyTileToAttack: AttackableTile? = null
|
||||
val capturableCity = cityTilesToAttack.firstOrNull{it.tileToAttack.getCity()!!.health == 1}
|
||||
val cityWithHealthLeft = cityTilesToAttack.filter { it.tileToAttack.getCity()!!.health != 1 } // don't want ranged units to attack defeated cities
|
||||
.minBy { it.tileToAttack.getCity()!!.health }
|
||||
|
||||
if (unit.type.isMelee() && capturableCity!=null)
|
||||
enemyTileToAttack = capturableCity // enter it quickly, top priority!
|
||||
|
||||
else if (nonCityTilesToAttack.isNotEmpty()) // second priority, units
|
||||
enemyTileToAttack = nonCityTilesToAttack.minBy { Battle.getMapCombatantOfTile(it.tileToAttack)!!.getHealth() }
|
||||
|
||||
else if (cityWithHealthLeft!=null) enemyTileToAttack = cityWithHealthLeft// third priority, city
|
||||
|
||||
return enemyTileToAttack
|
||||
}
|
||||
|
||||
private fun chooseBombardTarget(city: CityInfo) : TileInfo? {
|
||||
private fun chooseBombardTarget(city: CityInfo): TileInfo? {
|
||||
var targets = getBombardTargets(city)
|
||||
if (targets.isEmpty()) return null
|
||||
val siegeUnits = targets
|
||||
.filter { Battle.getMapCombatantOfTile(it)!!.getUnitType()==UnitType.Siege }
|
||||
if(siegeUnits.any()) targets = siegeUnits
|
||||
else{
|
||||
.filter { Battle.getMapCombatantOfTile(it)!!.getUnitType() == UnitType.Siege }
|
||||
if (siegeUnits.any()) targets = siegeUnits
|
||||
else {
|
||||
val rangedUnits = targets
|
||||
.filter { Battle.getMapCombatantOfTile(it)!!.getUnitType().isRanged() }
|
||||
if(rangedUnits.any()) targets=rangedUnits
|
||||
if (rangedUnits.any()) targets = rangedUnits
|
||||
}
|
||||
return targets.minBy { Battle.getMapCombatantOfTile(it)!!.getHealth() }
|
||||
}
|
||||
|
||||
private fun tryGarrisoningUnit(unit: MapUnit): Boolean {
|
||||
if(unit.type.isMelee() || unit.type.isWaterUnit()) return false // don't garrison melee units, they're not that good at it
|
||||
if (unit.type.isMelee() || unit.type.isWaterUnit()) return false // don't garrison melee units, they're not that good at it
|
||||
val citiesWithoutGarrison = unit.civInfo.cities.filter {
|
||||
val centerTile = it.getCenterTile()
|
||||
centerTile.militaryUnit==null
|
||||
&& unit.movement.canMoveTo(centerTile)
|
||||
centerTile.militaryUnit == null
|
||||
&& unit.movement.canMoveTo(centerTile)
|
||||
}
|
||||
|
||||
fun isCityThatNeedsDefendingInWartime(city: CityInfo): Boolean {
|
||||
@ -473,69 +327,70 @@ class UnitAutomation{
|
||||
for (enemyCivCity in unit.civInfo.diplomacy.values
|
||||
.filter { it.diplomaticStatus == DiplomaticStatus.War }
|
||||
.map { it.otherCiv() }.flatMap { it.cities })
|
||||
if (city.getCenterTile().arialDistanceTo(enemyCivCity.getCenterTile()) <= 5) return true// this is an edge city that needs defending
|
||||
if (city.getCenterTile().arialDistanceTo(enemyCivCity.getCenterTile()) <= 5) return true // this is an edge city that needs defending
|
||||
return false
|
||||
}
|
||||
|
||||
val citiesToTry:Sequence<CityInfo>
|
||||
val citiesToTry: Sequence<CityInfo>
|
||||
|
||||
if (!unit.civInfo.isAtWar()) {
|
||||
if (unit.getTile().isCityCenter()) return true // It's always good to have a unit in the city center, so if you haven't found anyone around to attack, forget it.
|
||||
citiesToTry = citiesWithoutGarrison.asSequence()
|
||||
} else {
|
||||
if (unit.getTile().isCityCenter() &&
|
||||
isCityThatNeedsDefendingInWartime(unit.getTile().getCity()!!)) return true
|
||||
isCityThatNeedsDefendingInWartime(unit.getTile().getCity()!!)) return true
|
||||
|
||||
citiesToTry = citiesWithoutGarrison.asSequence()
|
||||
.filter { isCityThatNeedsDefendingInWartime(it) }
|
||||
}
|
||||
|
||||
val closestReachableCityNeedsDefending =citiesToTry
|
||||
.sortedBy{ it.getCenterTile().arialDistanceTo(unit.currentTile) }
|
||||
val closestReachableCityNeedsDefending = citiesToTry
|
||||
.sortedBy { it.getCenterTile().arialDistanceTo(unit.currentTile) }
|
||||
.firstOrNull { unit.movement.canReach(it.getCenterTile()) }
|
||||
if(closestReachableCityNeedsDefending==null) return false
|
||||
if (closestReachableCityNeedsDefending == null) return false
|
||||
unit.movement.headTowards(closestReachableCityNeedsDefending.getCenterTile())
|
||||
return true
|
||||
}
|
||||
|
||||
fun tryGoToRuin(unit:MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
|
||||
if(!unit.civInfo.isMajorCiv()) return false // barbs don't have anything to do in ruins
|
||||
private fun tryGoToRuin(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
|
||||
if (!unit.civInfo.isMajorCiv()) return false // barbs don't have anything to do in ruins
|
||||
val tileWithRuin = unitDistanceToTiles.keys
|
||||
.firstOrNull{ it.improvement == Constants.ancientRuins && unit.movement.canMoveTo(it) }
|
||||
if(tileWithRuin==null) return false
|
||||
.firstOrNull { it.improvement == Constants.ancientRuins && unit.movement.canMoveTo(it) }
|
||||
if (tileWithRuin == null) return false
|
||||
unit.movement.moveToTile(tileWithRuin)
|
||||
return true
|
||||
}
|
||||
|
||||
internal fun tryExplore(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
|
||||
if(tryGoToRuin(unit,unitDistanceToTiles))
|
||||
{
|
||||
if(unit.currentMovement==0f) return true
|
||||
if (tryGoToRuin(unit, unitDistanceToTiles)) {
|
||||
if (unit.currentMovement == 0f) return true
|
||||
}
|
||||
|
||||
for(tile in unit.currentTile.getTilesInDistance(5))
|
||||
if(unit.movement.canMoveTo(tile) && tile.position !in unit.civInfo.exploredTiles
|
||||
&& unit.movement.canReach(tile)){
|
||||
for (tile in unit.currentTile.getTilesInDistance(5))
|
||||
if (unit.movement.canMoveTo(tile) && tile.position !in unit.civInfo.exploredTiles
|
||||
&& unit.movement.canReach(tile)) {
|
||||
unit.movement.headTowards(tile)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun automatedExplore(unit:MapUnit){
|
||||
fun automatedExplore(unit: MapUnit) {
|
||||
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
||||
if(tryGoToRuin(unit, unitDistanceToTiles) && unit.currentMovement==0f) return
|
||||
if (tryGoToRuin(unit, unitDistanceToTiles) && unit.currentMovement == 0f) return
|
||||
|
||||
if (unit.health < 80) {
|
||||
tryHealUnit(unit,unitDistanceToTiles)
|
||||
tryHealUnit(unit, unitDistanceToTiles)
|
||||
return
|
||||
}
|
||||
|
||||
for(i in 1..10){
|
||||
for (i in 1..10) {
|
||||
val unexploredTilesAtDistance = unit.getTile().getTilesAtDistance(i)
|
||||
.filter { unit.movement.canMoveTo(it) && it.position !in unit.civInfo.exploredTiles
|
||||
&& unit.movement.canReach(it) }
|
||||
if(unexploredTilesAtDistance.isNotEmpty()){
|
||||
.filter {
|
||||
unit.movement.canMoveTo(it) && it.position !in unit.civInfo.exploredTiles
|
||||
&& unit.movement.canReach(it)
|
||||
}
|
||||
if (unexploredTilesAtDistance.isNotEmpty()) {
|
||||
unit.movement.headTowards(unexploredTilesAtDistance.random())
|
||||
return
|
||||
}
|
||||
@ -543,15 +398,12 @@ class UnitAutomation{
|
||||
unit.civInfo.addNotification("[${unit.name}] finished exploring.", unit.currentTile.position, Color.GRAY)
|
||||
}
|
||||
|
||||
|
||||
fun wander(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn) {
|
||||
val reachableTiles= unitDistanceToTiles
|
||||
val reachableTiles = unitDistanceToTiles
|
||||
.filter { unit.movement.canMoveTo(it.key) && unit.movement.canReach(it.key) }
|
||||
|
||||
val reachableTilesMaxWalkingDistance = reachableTiles.filter { it.value.totalDistance == unit.currentMovement }
|
||||
if (reachableTilesMaxWalkingDistance.any()) unit.movement.moveToTile(reachableTilesMaxWalkingDistance.toList().random().first)
|
||||
else if (reachableTiles.any()) unit.movement.moveToTile(reachableTiles.toList().random().first)
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -3,13 +3,13 @@ package com.unciv.logic.battle
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.automation.UnitAutomation
|
||||
import com.unciv.logic.city.CityInfo
|
||||
import com.unciv.logic.civilization.AlertType
|
||||
import com.unciv.logic.civilization.PopupAlert
|
||||
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
|
||||
import com.unciv.logic.map.RoadStatus
|
||||
import com.unciv.logic.map.TileInfo
|
||||
import com.unciv.models.AttackableTile
|
||||
import com.unciv.models.ruleset.unit.UnitType
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
@ -19,7 +19,7 @@ import kotlin.math.max
|
||||
*/
|
||||
object Battle {
|
||||
|
||||
fun moveAndAttack(attacker: ICombatant, attackableTile: UnitAutomation.AttackableTile){
|
||||
fun moveAndAttack(attacker: ICombatant, attackableTile: AttackableTile){
|
||||
if (attacker is MapUnitCombatant) {
|
||||
attacker.unit.movement.moveToTile(attackableTile.tileToAttackFrom)
|
||||
if (attacker.unit.hasUnique("Must set up to ranged attack") && attacker.unit.action != Constants.unitActionSetUp) {
|
||||
|
@ -262,7 +262,15 @@ class MapUnit {
|
||||
return true
|
||||
}
|
||||
|
||||
fun fortify(){ action = "Fortify 0"}
|
||||
fun fortify() {
|
||||
action = "Fortify 0"
|
||||
}
|
||||
|
||||
fun fortifyIfCan() {
|
||||
if (canFortify()) {
|
||||
fortify()
|
||||
}
|
||||
}
|
||||
|
||||
fun adjacentHealingBonus():Int{
|
||||
var healingBonus = 0
|
||||
|
@ -7,7 +7,7 @@ import com.unciv.logic.civilization.CivilizationInfo
|
||||
class UnitMovementAlgorithms(val unit:MapUnit) {
|
||||
|
||||
// This function is called ALL THE TIME and should be as time-optimal as possible!
|
||||
private fun getMovementCostBetweenAdjacentTiles(from: TileInfo, to: TileInfo, civInfo: CivilizationInfo): Float {
|
||||
fun getMovementCostBetweenAdjacentTiles(from: TileInfo, to: TileInfo, civInfo: CivilizationInfo): Float {
|
||||
|
||||
if ((from.isLand != to.isLand) && unit.type.isLandUnit())
|
||||
return 100f // this is embarkment or disembarkment, and will take the entire turn
|
||||
|
5
core/src/com/unciv/models/AttackableTile.kt
Normal file
5
core/src/com/unciv/models/AttackableTile.kt
Normal file
@ -0,0 +1,5 @@
|
||||
package com.unciv.models
|
||||
|
||||
import com.unciv.logic.map.TileInfo
|
||||
|
||||
class AttackableTile(val tileToAttackFrom: TileInfo, val tileToAttack: TileInfo)
|
@ -9,6 +9,7 @@ import com.badlogic.gdx.scenes.scene2d.actions.FloatAction
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.automation.BattleHelper
|
||||
import com.unciv.logic.automation.UnitAutomation
|
||||
import com.unciv.logic.city.CityInfo
|
||||
import com.unciv.logic.civilization.CivilizationInfo
|
||||
@ -244,7 +245,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
|
||||
val unitType = unit.type
|
||||
val attackableTiles: List<TileInfo> = if (unitType.isCivilian()) listOf()
|
||||
else {
|
||||
val tiles = UnitAutomation().getAttackableEnemies(unit, unit.movement.getDistanceToTiles()).map { it.tileToAttack }
|
||||
val tiles = BattleHelper().getAttackableEnemies(unit, unit.movement.getDistanceToTiles()).map { it.tileToAttack }
|
||||
tiles.filter { (UncivGame.Current.viewEntireMapForDebug || playerViewableTilePositions.contains(it.position)) }
|
||||
}
|
||||
|
||||
|
@ -9,8 +9,10 @@ import com.badlogic.gdx.scenes.scene2d.ui.Label
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Table
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.automation.BattleHelper
|
||||
import com.unciv.logic.automation.UnitAutomation
|
||||
import com.unciv.logic.battle.*
|
||||
import com.unciv.models.AttackableTile
|
||||
import com.unciv.models.translations.tr
|
||||
import com.unciv.models.ruleset.unit.UnitType
|
||||
import com.unciv.ui.utils.*
|
||||
@ -20,7 +22,9 @@ import kotlin.math.max
|
||||
|
||||
class BattleTable(val worldScreen: WorldScreen): Table() {
|
||||
|
||||
init{
|
||||
private val battleHelper = BattleHelper()
|
||||
|
||||
init {
|
||||
isVisible = false
|
||||
skin = CameraStageBaseScreen.skin
|
||||
background = ImageGetter.getBackground(ImageGetter.getBlue())
|
||||
@ -170,11 +174,11 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
|
||||
}
|
||||
val attackButton = TextButton(attackText.tr(), skin).apply { color= Color.RED }
|
||||
|
||||
var attackableEnemy : UnitAutomation.AttackableTile? = null
|
||||
var attackableEnemy : AttackableTile? = null
|
||||
|
||||
if (attacker.canAttack()) {
|
||||
if (attacker is MapUnitCombatant) {
|
||||
attackableEnemy = UnitAutomation()
|
||||
attackableEnemy = battleHelper
|
||||
.getAttackableEnemies(attacker.unit, attacker.unit.movement.getDistanceToTiles())
|
||||
.firstOrNull{ it.tileToAttack == defender.getTile()}
|
||||
}
|
||||
@ -182,7 +186,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
|
||||
{
|
||||
val canBombard = UnitAutomation().getBombardTargets(attacker.city).contains(defender.getTile())
|
||||
if (canBombard) {
|
||||
attackableEnemy = UnitAutomation.AttackableTile(attacker.getTile(), defender.getTile())
|
||||
attackableEnemy = AttackableTile(attacker.getTile(), defender.getTile())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user