mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-28 14:24:43 -04: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,6 +19,9 @@ class NextTurnAutomation{
|
|||||||
|
|
||||||
/** Top-level AI turn tasklist */
|
/** Top-level AI turn tasklist */
|
||||||
fun automateCivMoves(civInfo: CivilizationInfo) {
|
fun automateCivMoves(civInfo: CivilizationInfo) {
|
||||||
|
if (civInfo.isBarbarian()) {
|
||||||
|
BarbarianAutomation(civInfo).automate()
|
||||||
|
} else {
|
||||||
respondToDemands(civInfo)
|
respondToDemands(civInfo)
|
||||||
respondToTradeRequests(civInfo)
|
respondToTradeRequests(civInfo)
|
||||||
|
|
||||||
@ -40,6 +43,8 @@ class NextTurnAutomation{
|
|||||||
automateUnits(civInfo)
|
automateUnits(civInfo)
|
||||||
reassignWorkedTiles(civInfo)
|
reassignWorkedTiles(civInfo)
|
||||||
trainSettler(civInfo)
|
trainSettler(civInfo)
|
||||||
|
}
|
||||||
|
|
||||||
civInfo.popupAlerts.clear() // AIs don't care about popups.
|
civInfo.popupAlerts.clear() // AIs don't care about popups.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@ import com.unciv.ui.worldscreen.unit.UnitActions
|
|||||||
|
|
||||||
class SpecificUnitAutomation{
|
class SpecificUnitAutomation{
|
||||||
|
|
||||||
|
private val battleHelper = BattleHelper()
|
||||||
|
|
||||||
private fun hasWorkableSeaResource(tileInfo: TileInfo, civInfo: CivilizationInfo): Boolean {
|
private fun hasWorkableSeaResource(tileInfo: TileInfo, civInfo: CivilizationInfo): Boolean {
|
||||||
return tileInfo.hasViewableResource(civInfo) && tileInfo.isWater && tileInfo.improvement==null
|
return tileInfo.hasViewableResource(civInfo) && tileInfo.isWater && tileInfo.improvement==null
|
||||||
}
|
}
|
||||||
@ -186,14 +188,14 @@ class SpecificUnitAutomation{
|
|||||||
.flatMap { it.airUnits }.filter { it.civInfo.isAtWarWith(unit.civInfo) }
|
.flatMap { it.airUnits }.filter { it.civInfo.isAtWarWith(unit.civInfo) }
|
||||||
|
|
||||||
if(enemyAirUnitsInRange.isNotEmpty()) return // we need to be on standby in case they attack
|
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
|
val immediatelyReachableCities = tilesInRange
|
||||||
.filter { it.isCityCenter() && it.getOwner()==unit.civInfo && unit.movement.canMoveTo(it)}
|
.filter { it.isCityCenter() && it.getOwner()==unit.civInfo && unit.movement.canMoveTo(it)}
|
||||||
|
|
||||||
for(city in immediatelyReachableCities){
|
for(city in immediatelyReachableCities){
|
||||||
if(city.getTilesInDistance(unit.getRange())
|
if(city.getTilesInDistance(unit.getRange())
|
||||||
.any { UnitAutomation().containsAttackableEnemy(it,MapUnitCombatant(unit)) }) {
|
.any { battleHelper.containsAttackableEnemy(it,MapUnitCombatant(unit)) }) {
|
||||||
unit.movement.moveToTile(city)
|
unit.movement.moveToTile(city)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -220,7 +222,7 @@ class SpecificUnitAutomation{
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun automateBomber(unit: MapUnit) {
|
fun automateBomber(unit: MapUnit) {
|
||||||
if (UnitAutomation().tryAttackNearbyEnemy(unit)) return
|
if (battleHelper.tryAttackNearbyEnemy(unit)) return
|
||||||
|
|
||||||
val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange())
|
val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange())
|
||||||
|
|
||||||
@ -229,7 +231,7 @@ class SpecificUnitAutomation{
|
|||||||
|
|
||||||
for (city in immediatelyReachableCities) {
|
for (city in immediatelyReachableCities) {
|
||||||
if (city.getTilesInDistance(unit.getRange())
|
if (city.getTilesInDistance(unit.getRange())
|
||||||
.any { UnitAutomation().containsAttackableEnemy(it, MapUnitCombatant(unit)) }) {
|
.any { battleHelper.containsAttackableEnemy(it, MapUnitCombatant(unit)) }) {
|
||||||
unit.movement.moveToTile(city)
|
unit.movement.moveToTile(city)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -245,7 +247,7 @@ class SpecificUnitAutomation{
|
|||||||
.filter {
|
.filter {
|
||||||
it != airUnit.currentTile
|
it != airUnit.currentTile
|
||||||
&& it.getTilesInDistance(airUnit.getRange())
|
&& it.getTilesInDistance(airUnit.getRange())
|
||||||
.any { UnitAutomation().containsAttackableEnemy(it, MapUnitCombatant(airUnit)) }
|
.any { battleHelper.containsAttackableEnemy(it, MapUnitCombatant(airUnit)) }
|
||||||
}
|
}
|
||||||
if (citiesThatCanAttackFrom.isEmpty()) return
|
if (citiesThatCanAttackFrom.isEmpty()) return
|
||||||
|
|
||||||
@ -258,7 +260,7 @@ class SpecificUnitAutomation{
|
|||||||
|
|
||||||
// This really needs to be changed, to have better targetting for missiles
|
// This really needs to be changed, to have better targetting for missiles
|
||||||
fun automateMissile(unit: MapUnit) {
|
fun automateMissile(unit: MapUnit) {
|
||||||
if (UnitAutomation().tryAttackNearbyEnemy(unit)) return
|
if (battleHelper.tryAttackNearbyEnemy(unit)) return
|
||||||
|
|
||||||
val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange())
|
val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange())
|
||||||
|
|
||||||
@ -267,7 +269,7 @@ class SpecificUnitAutomation{
|
|||||||
|
|
||||||
for (city in immediatelyReachableCities) {
|
for (city in immediatelyReachableCities) {
|
||||||
if (city.getTilesInDistance(unit.getRange())
|
if (city.getTilesInDistance(unit.getRange())
|
||||||
.any { UnitAutomation().containsAttackableEnemy(it, MapUnitCombatant(unit)) }) {
|
.any { battleHelper.containsAttackableEnemy(it, MapUnitCombatant(unit)) }) {
|
||||||
unit.movement.moveToTile(city)
|
unit.movement.moveToTile(city)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,10 @@ package com.unciv.logic.automation
|
|||||||
import com.badlogic.gdx.graphics.Color
|
import com.badlogic.gdx.graphics.Color
|
||||||
import com.unciv.Constants
|
import com.unciv.Constants
|
||||||
import com.unciv.UncivGame
|
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.city.CityInfo
|
||||||
import com.unciv.logic.civilization.GreatPersonManager
|
import com.unciv.logic.civilization.GreatPersonManager
|
||||||
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
|
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
|
||||||
@ -15,7 +18,6 @@ import com.unciv.models.UnitActionType
|
|||||||
import com.unciv.models.ruleset.unit.UnitType
|
import com.unciv.models.ruleset.unit.UnitType
|
||||||
import com.unciv.ui.worldscreen.unit.UnitActions
|
import com.unciv.ui.worldscreen.unit.UnitActions
|
||||||
|
|
||||||
|
|
||||||
class UnitAutomation {
|
class UnitAutomation {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -23,7 +25,12 @@ class UnitAutomation{
|
|||||||
const val CLOSE_ENEMY_TURNS_AWAY_LIMIT = 3f
|
const val CLOSE_ENEMY_TURNS_AWAY_LIMIT = 3f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val battleHelper = BattleHelper()
|
||||||
|
|
||||||
fun automateUnitMoves(unit: MapUnit) {
|
fun automateUnitMoves(unit: MapUnit) {
|
||||||
|
if (unit.civInfo.isBarbarian()) {
|
||||||
|
throw IllegalStateException("Barbarians is not allowed here.")
|
||||||
|
}
|
||||||
|
|
||||||
if (unit.name == Constants.settler) {
|
if (unit.name == Constants.settler) {
|
||||||
return SpecificUnitAutomation().automateSettlerActions(unit)
|
return SpecificUnitAutomation().automateSettlerActions(unit)
|
||||||
@ -57,12 +64,6 @@ class UnitAutomation{
|
|||||||
val unitActions = UnitActions().getUnitActions(unit, UncivGame.Current.worldScreen)
|
val unitActions = UnitActions().getUnitActions(unit, UncivGame.Current.worldScreen)
|
||||||
var unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
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 (tryGoToRuin(unit, unitDistanceToTiles)) {
|
||||||
if (unit.currentMovement == 0f) return
|
if (unit.currentMovement == 0f) return
|
||||||
unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
||||||
@ -76,15 +77,10 @@ class UnitAutomation{
|
|||||||
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 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 (battleHelper.tryDisembarkUnitToAttackPosition(unit, unitDistanceToTiles)) return
|
||||||
if (tryDisembarkUnitToAttackPosition(unit,unitDistanceToTiles)) return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is an attackable unit in the vicinity, attack!
|
// if there is an attackable unit in the vicinity, attack!
|
||||||
if (tryAttackNearbyEnemy(unit)) return
|
if (battleHelper.tryAttackNearbyEnemy(unit)) return
|
||||||
|
|
||||||
// Barbarians try to pillage improvements if no targets reachable
|
|
||||||
if (unit.civInfo.isBarbarian() && tryPillageImprovement(unit, unitDistanceToTiles)) return
|
|
||||||
|
|
||||||
if (tryGarrisoningUnit(unit)) return
|
if (tryGarrisoningUnit(unit)) return
|
||||||
|
|
||||||
@ -102,29 +98,24 @@ class UnitAutomation{
|
|||||||
|
|
||||||
// else, try to go o unreached tiles
|
// else, try to go o unreached tiles
|
||||||
if (tryExplore(unit, unitDistanceToTiles)) return
|
if (tryExplore(unit, unitDistanceToTiles)) return
|
||||||
|
|
||||||
// Barbarians just wander all over the place
|
|
||||||
if(unit.civInfo.isBarbarian())
|
|
||||||
wander(unit,unitDistanceToTiles)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryHeadTowardsEncampment(unit: MapUnit): Boolean {
|
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()
|
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 cities = unit.civInfo.cities
|
||||||
val encampmentsCloseToCities
|
val encampmentsCloseToCities = knownEncampments.filter { cities.any { city -> city.getCenterTile().arialDistanceTo(it) < 6 } }
|
||||||
= knownEncampments.filter { cities.any { city -> city.getCenterTile().arialDistanceTo(it) < 6 } }
|
|
||||||
.sortedBy { it.arialDistanceTo(unit.currentTile) }
|
.sortedBy { it.arialDistanceTo(unit.currentTile) }
|
||||||
val encampmentToHeadTowards = encampmentsCloseToCities.firstOrNull { unit.movement.canReach(it) }
|
val encampmentToHeadTowards = encampmentsCloseToCities.firstOrNull { unit.movement.canReach(it) }
|
||||||
if(encampmentToHeadTowards==null) return false
|
if (encampmentToHeadTowards == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
unit.movement.headTowards(encampmentToHeadTowards)
|
unit.movement.headTowards(encampmentToHeadTowards)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun tryHealUnit(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
|
||||||
fun tryHealUnit(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn):Boolean {
|
|
||||||
val tilesInDistance = unitDistanceToTiles.keys.filter { unit.movement.canMoveTo(it) }
|
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()
|
val currentUnitTile = unit.getTile()
|
||||||
@ -155,7 +146,7 @@ class UnitAutomation{
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tryPillageImprovement(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn) : Boolean {
|
private fun tryPillageImprovement(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
|
||||||
if (unit.type.isCivilian()) return false
|
if (unit.type.isCivilian()) return false
|
||||||
val tilesThatCanWalkToAndThenPillage = unitDistanceToTiles
|
val tilesThatCanWalkToAndThenPillage = unitDistanceToTiles
|
||||||
.filter { it.value.totalDistance < unit.currentMovement }.keys
|
.filter { it.value.totalDistance < unit.currentMovement }.keys
|
||||||
@ -171,86 +162,9 @@ class UnitAutomation{
|
|||||||
return true
|
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> {
|
fun getBombardTargets(city: CityInfo): List<TileInfo> {
|
||||||
return city.getCenterTile().getViewableTiles(city.range, true)
|
return city.getCenterTile().getViewableTiles(city.range, true)
|
||||||
.filter { containsAttackableEnemy(it, CityCombatant(city)) }
|
.filter { battleHelper.containsAttackableEnemy(it, CityCombatant(city)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Move towards the closest attackable enemy of the [unit].
|
/** Move towards the closest attackable enemy of the [unit].
|
||||||
@ -263,11 +177,12 @@ class UnitAutomation{
|
|||||||
unit.getTile().position,
|
unit.getTile().position,
|
||||||
unit.getMaxMovement() * CLOSE_ENEMY_TURNS_AWAY_LIMIT
|
unit.getMaxMovement() * CLOSE_ENEMY_TURNS_AWAY_LIMIT
|
||||||
)
|
)
|
||||||
var closeEnemies = getAttackableEnemies(
|
var closeEnemies = battleHelper.getAttackableEnemies(
|
||||||
unit,
|
unit,
|
||||||
unitDistanceToTiles,
|
unitDistanceToTiles,
|
||||||
tilesToCheck = unit.getTile().getTilesInDistance(CLOSE_ENEMY_TILES_AWAY_LIMIT)
|
tilesToCheck = unit.getTile().getTilesInDistance(CLOSE_ENEMY_TILES_AWAY_LIMIT)
|
||||||
).filter { // Ignore units that would 1-shot you if you attacked
|
).filter {
|
||||||
|
// Ignore units that would 1-shot you if you attacked
|
||||||
BattleDamage().calculateDamageToAttacker(MapUnitCombatant(unit),
|
BattleDamage().calculateDamageToAttacker(MapUnitCombatant(unit),
|
||||||
Battle.getMapCombatantOfTile(it.tileToAttack)!!) < unit.health
|
Battle.getMapCombatantOfTile(it.tileToAttack)!!) < unit.health
|
||||||
}
|
}
|
||||||
@ -286,9 +201,11 @@ class UnitAutomation{
|
|||||||
|
|
||||||
private fun tryAccompanySettlerOrGreatPerson(unit: MapUnit): Boolean {
|
private fun tryAccompanySettlerOrGreatPerson(unit: MapUnit): Boolean {
|
||||||
val settlerOrGreatPersonToAccompany = unit.civInfo.getCivUnits()
|
val settlerOrGreatPersonToAccompany = unit.civInfo.getCivUnits()
|
||||||
.firstOrNull { val tile = it.currentTile
|
.firstOrNull {
|
||||||
|
val tile = it.currentTile
|
||||||
(it.name == Constants.settler || it.name in GreatPersonManager().statToGreatPersonMapping.values)
|
(it.name == Constants.settler || it.name in GreatPersonManager().statToGreatPersonMapping.values)
|
||||||
&& tile.militaryUnit==null && unit.movement.canMoveTo(tile) && unit.movement.canReach(tile) }
|
&& tile.militaryUnit == null && unit.movement.canMoveTo(tile) && unit.movement.canReach(tile)
|
||||||
|
}
|
||||||
if (settlerOrGreatPersonToAccompany == null) return false
|
if (settlerOrGreatPersonToAccompany == null) return false
|
||||||
unit.movement.headTowards(settlerOrGreatPersonToAccompany.currentTile)
|
unit.movement.headTowards(settlerOrGreatPersonToAccompany.currentTile)
|
||||||
return true
|
return true
|
||||||
@ -321,7 +238,8 @@ class UnitAutomation{
|
|||||||
|
|
||||||
val closestReachableEnemyCity = enemyCities
|
val closestReachableEnemyCity = enemyCities
|
||||||
.asSequence().map { it.getCenterTile() }
|
.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()!!
|
unit.civInfo.cities.asSequence().map { cityCenterTile.arialDistanceTo(it.getCenterTile()) }.min()!!
|
||||||
}
|
}
|
||||||
.firstOrNull { unit.movement.canReach(it) }
|
.firstOrNull { unit.movement.canReach(it) }
|
||||||
@ -330,24 +248,19 @@ class UnitAutomation{
|
|||||||
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
||||||
val tilesInBombardRange = closestReachableEnemyCity.getTilesInDistance(2)
|
val tilesInBombardRange = closestReachableEnemyCity.getTilesInDistance(2)
|
||||||
val reachableTilesNotInBombardRange = unitDistanceToTiles.keys.filter { it !in tilesInBombardRange }
|
val reachableTilesNotInBombardRange = unitDistanceToTiles.keys.filter { it !in tilesInBombardRange }
|
||||||
val canMoveIntoBombardRange = tilesInBombardRange.any { unitDistanceToTiles.containsKey(it)}
|
|
||||||
|
|
||||||
val suitableGatheringGroundTiles = closestReachableEnemyCity.getTilesAtDistance(4)
|
val suitableGatheringGroundTiles = closestReachableEnemyCity.getTilesAtDistance(4)
|
||||||
.union(closestReachableEnemyCity.getTilesAtDistance(3))
|
.union(closestReachableEnemyCity.getTilesAtDistance(3))
|
||||||
.filter { it.isLand }
|
.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 -
|
// 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.
|
// this is against tha AI's brilliant plan of having everyone embarked and attacking via sea when unnecessary.
|
||||||
val tileToHeadTo = if(closestReachableLandingGroundTile!=null) closestReachableLandingGroundTile
|
val tileToHeadTo = suitableGatheringGroundTiles
|
||||||
else closestReachableEnemyCity
|
.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)
|
unit.movement.headTowards(tileToHeadTo)
|
||||||
|
|
||||||
else {
|
else {
|
||||||
if (unit.getRange() > 2) { // should never be in a bombardable position
|
if (unit.getRange() > 2) { // should never be in a bombardable position
|
||||||
val tilesCanAttackFromButNotInBombardRange =
|
val tilesCanAttackFromButNotInBombardRange =
|
||||||
@ -356,8 +269,7 @@ class UnitAutomation{
|
|||||||
// move into position far away enough that the bombard doesn't hurt
|
// 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 }!!)
|
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)
|
// 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)
|
val militaryUnitsAroundEnemyCity = closestReachableEnemyCity.getTilesInDistance(3)
|
||||||
.filter { it.militaryUnit != null && it.militaryUnit!!.civInfo == unit.civInfo }
|
.filter { it.militaryUnit != null && it.militaryUnit!!.civInfo == unit.civInfo }
|
||||||
@ -377,42 +289,6 @@ class UnitAutomation{
|
|||||||
return false
|
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 {
|
fun tryBombardEnemy(city: CityInfo): Boolean {
|
||||||
if (!city.attackedThisTurn) {
|
if (!city.attackedThisTurn) {
|
||||||
val target = chooseBombardTarget(city)
|
val target = chooseBombardTarget(city)
|
||||||
@ -424,28 +300,6 @@ class UnitAutomation{
|
|||||||
return false
|
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)
|
var targets = getBombardTargets(city)
|
||||||
if (targets.isEmpty()) return null
|
if (targets.isEmpty()) return null
|
||||||
@ -498,7 +352,7 @@ class UnitAutomation{
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tryGoToRuin(unit:MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
|
private fun tryGoToRuin(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
|
||||||
if (!unit.civInfo.isMajorCiv()) return false // barbs don't have anything to do in ruins
|
if (!unit.civInfo.isMajorCiv()) return false // barbs don't have anything to do in ruins
|
||||||
val tileWithRuin = unitDistanceToTiles.keys
|
val tileWithRuin = unitDistanceToTiles.keys
|
||||||
.firstOrNull { it.improvement == Constants.ancientRuins && unit.movement.canMoveTo(it) }
|
.firstOrNull { it.improvement == Constants.ancientRuins && unit.movement.canMoveTo(it) }
|
||||||
@ -508,8 +362,7 @@ class UnitAutomation{
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal fun tryExplore(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
|
internal fun tryExplore(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
|
||||||
if(tryGoToRuin(unit,unitDistanceToTiles))
|
if (tryGoToRuin(unit, unitDistanceToTiles)) {
|
||||||
{
|
|
||||||
if (unit.currentMovement == 0f) return true
|
if (unit.currentMovement == 0f) return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -533,8 +386,10 @@ class UnitAutomation{
|
|||||||
|
|
||||||
for (i in 1..10) {
|
for (i in 1..10) {
|
||||||
val unexploredTilesAtDistance = unit.getTile().getTilesAtDistance(i)
|
val unexploredTilesAtDistance = unit.getTile().getTilesAtDistance(i)
|
||||||
.filter { unit.movement.canMoveTo(it) && it.position !in unit.civInfo.exploredTiles
|
.filter {
|
||||||
&& unit.movement.canReach(it) }
|
unit.movement.canMoveTo(it) && it.position !in unit.civInfo.exploredTiles
|
||||||
|
&& unit.movement.canReach(it)
|
||||||
|
}
|
||||||
if (unexploredTilesAtDistance.isNotEmpty()) {
|
if (unexploredTilesAtDistance.isNotEmpty()) {
|
||||||
unit.movement.headTowards(unexploredTilesAtDistance.random())
|
unit.movement.headTowards(unexploredTilesAtDistance.random())
|
||||||
return
|
return
|
||||||
@ -543,7 +398,6 @@ class UnitAutomation{
|
|||||||
unit.civInfo.addNotification("[${unit.name}] finished exploring.", unit.currentTile.position, Color.GRAY)
|
unit.civInfo.addNotification("[${unit.name}] finished exploring.", unit.currentTile.position, Color.GRAY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun wander(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn) {
|
fun wander(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn) {
|
||||||
val reachableTiles = unitDistanceToTiles
|
val reachableTiles = unitDistanceToTiles
|
||||||
.filter { unit.movement.canMoveTo(it.key) && unit.movement.canReach(it.key) }
|
.filter { unit.movement.canMoveTo(it.key) && unit.movement.canReach(it.key) }
|
||||||
@ -551,7 +405,5 @@ class UnitAutomation{
|
|||||||
val reachableTilesMaxWalkingDistance = reachableTiles.filter { it.value.totalDistance == unit.currentMovement }
|
val reachableTilesMaxWalkingDistance = reachableTiles.filter { it.value.totalDistance == unit.currentMovement }
|
||||||
if (reachableTilesMaxWalkingDistance.any()) unit.movement.moveToTile(reachableTilesMaxWalkingDistance.toList().random().first)
|
if (reachableTilesMaxWalkingDistance.any()) unit.movement.moveToTile(reachableTilesMaxWalkingDistance.toList().random().first)
|
||||||
else if (reachableTiles.any()) unit.movement.moveToTile(reachableTiles.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.badlogic.gdx.graphics.Color
|
||||||
import com.unciv.Constants
|
import com.unciv.Constants
|
||||||
import com.unciv.UncivGame
|
import com.unciv.UncivGame
|
||||||
import com.unciv.logic.automation.UnitAutomation
|
|
||||||
import com.unciv.logic.city.CityInfo
|
import com.unciv.logic.city.CityInfo
|
||||||
import com.unciv.logic.civilization.AlertType
|
import com.unciv.logic.civilization.AlertType
|
||||||
import com.unciv.logic.civilization.PopupAlert
|
import com.unciv.logic.civilization.PopupAlert
|
||||||
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
|
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
|
||||||
import com.unciv.logic.map.RoadStatus
|
import com.unciv.logic.map.RoadStatus
|
||||||
import com.unciv.logic.map.TileInfo
|
import com.unciv.logic.map.TileInfo
|
||||||
|
import com.unciv.models.AttackableTile
|
||||||
import com.unciv.models.ruleset.unit.UnitType
|
import com.unciv.models.ruleset.unit.UnitType
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
@ -19,7 +19,7 @@ import kotlin.math.max
|
|||||||
*/
|
*/
|
||||||
object Battle {
|
object Battle {
|
||||||
|
|
||||||
fun moveAndAttack(attacker: ICombatant, attackableTile: UnitAutomation.AttackableTile){
|
fun moveAndAttack(attacker: ICombatant, attackableTile: AttackableTile){
|
||||||
if (attacker is MapUnitCombatant) {
|
if (attacker is MapUnitCombatant) {
|
||||||
attacker.unit.movement.moveToTile(attackableTile.tileToAttackFrom)
|
attacker.unit.movement.moveToTile(attackableTile.tileToAttackFrom)
|
||||||
if (attacker.unit.hasUnique("Must set up to ranged attack") && attacker.unit.action != Constants.unitActionSetUp) {
|
if (attacker.unit.hasUnique("Must set up to ranged attack") && attacker.unit.action != Constants.unitActionSetUp) {
|
||||||
|
@ -262,7 +262,15 @@ class MapUnit {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fortify(){ action = "Fortify 0"}
|
fun fortify() {
|
||||||
|
action = "Fortify 0"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fortifyIfCan() {
|
||||||
|
if (canFortify()) {
|
||||||
|
fortify()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun adjacentHealingBonus():Int{
|
fun adjacentHealingBonus():Int{
|
||||||
var healingBonus = 0
|
var healingBonus = 0
|
||||||
|
@ -7,7 +7,7 @@ import com.unciv.logic.civilization.CivilizationInfo
|
|||||||
class UnitMovementAlgorithms(val unit:MapUnit) {
|
class UnitMovementAlgorithms(val unit:MapUnit) {
|
||||||
|
|
||||||
// This function is called ALL THE TIME and should be as time-optimal as possible!
|
// 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())
|
if ((from.isLand != to.isLand) && unit.type.isLandUnit())
|
||||||
return 100f // this is embarkment or disembarkment, and will take the entire turn
|
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.badlogic.gdx.scenes.scene2d.ui.Table
|
||||||
import com.unciv.Constants
|
import com.unciv.Constants
|
||||||
import com.unciv.UncivGame
|
import com.unciv.UncivGame
|
||||||
|
import com.unciv.logic.automation.BattleHelper
|
||||||
import com.unciv.logic.automation.UnitAutomation
|
import com.unciv.logic.automation.UnitAutomation
|
||||||
import com.unciv.logic.city.CityInfo
|
import com.unciv.logic.city.CityInfo
|
||||||
import com.unciv.logic.civilization.CivilizationInfo
|
import com.unciv.logic.civilization.CivilizationInfo
|
||||||
@ -244,7 +245,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
|
|||||||
val unitType = unit.type
|
val unitType = unit.type
|
||||||
val attackableTiles: List<TileInfo> = if (unitType.isCivilian()) listOf()
|
val attackableTiles: List<TileInfo> = if (unitType.isCivilian()) listOf()
|
||||||
else {
|
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)) }
|
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.Table
|
||||||
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
|
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
|
||||||
import com.unciv.UncivGame
|
import com.unciv.UncivGame
|
||||||
|
import com.unciv.logic.automation.BattleHelper
|
||||||
import com.unciv.logic.automation.UnitAutomation
|
import com.unciv.logic.automation.UnitAutomation
|
||||||
import com.unciv.logic.battle.*
|
import com.unciv.logic.battle.*
|
||||||
|
import com.unciv.models.AttackableTile
|
||||||
import com.unciv.models.translations.tr
|
import com.unciv.models.translations.tr
|
||||||
import com.unciv.models.ruleset.unit.UnitType
|
import com.unciv.models.ruleset.unit.UnitType
|
||||||
import com.unciv.ui.utils.*
|
import com.unciv.ui.utils.*
|
||||||
@ -20,6 +22,8 @@ import kotlin.math.max
|
|||||||
|
|
||||||
class BattleTable(val worldScreen: WorldScreen): Table() {
|
class BattleTable(val worldScreen: WorldScreen): Table() {
|
||||||
|
|
||||||
|
private val battleHelper = BattleHelper()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
isVisible = false
|
isVisible = false
|
||||||
skin = CameraStageBaseScreen.skin
|
skin = CameraStageBaseScreen.skin
|
||||||
@ -170,11 +174,11 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
|
|||||||
}
|
}
|
||||||
val attackButton = TextButton(attackText.tr(), skin).apply { color= Color.RED }
|
val attackButton = TextButton(attackText.tr(), skin).apply { color= Color.RED }
|
||||||
|
|
||||||
var attackableEnemy : UnitAutomation.AttackableTile? = null
|
var attackableEnemy : AttackableTile? = null
|
||||||
|
|
||||||
if (attacker.canAttack()) {
|
if (attacker.canAttack()) {
|
||||||
if (attacker is MapUnitCombatant) {
|
if (attacker is MapUnitCombatant) {
|
||||||
attackableEnemy = UnitAutomation()
|
attackableEnemy = battleHelper
|
||||||
.getAttackableEnemies(attacker.unit, attacker.unit.movement.getDistanceToTiles())
|
.getAttackableEnemies(attacker.unit, attacker.unit.movement.getDistanceToTiles())
|
||||||
.firstOrNull{ it.tileToAttack == defender.getTile()}
|
.firstOrNull{ it.tileToAttack == defender.getTile()}
|
||||||
}
|
}
|
||||||
@ -182,7 +186,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
|
|||||||
{
|
{
|
||||||
val canBombard = UnitAutomation().getBombardTargets(attacker.city).contains(defender.getTile())
|
val canBombard = UnitAutomation().getBombardTargets(attacker.city).contains(defender.getTile())
|
||||||
if (canBombard) {
|
if (canBombard) {
|
||||||
attackableEnemy = UnitAutomation.AttackableTile(attacker.getTile(), defender.getTile())
|
attackableEnemy = AttackableTile(attacker.getTile(), defender.getTile())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user