mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-27 13:55:54 -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,27 +19,32 @@ class NextTurnAutomation{
|
|||||||
|
|
||||||
/** Top-level AI turn tasklist */
|
/** Top-level AI turn tasklist */
|
||||||
fun automateCivMoves(civInfo: CivilizationInfo) {
|
fun automateCivMoves(civInfo: CivilizationInfo) {
|
||||||
respondToDemands(civInfo)
|
if (civInfo.isBarbarian()) {
|
||||||
respondToTradeRequests(civInfo)
|
BarbarianAutomation(civInfo).automate()
|
||||||
|
|
||||||
if(civInfo.isMajorCiv()) {
|
|
||||||
offerPeaceTreaty(civInfo)
|
|
||||||
exchangeTechs(civInfo)
|
|
||||||
exchangeLuxuries(civInfo)
|
|
||||||
issueRequests(civInfo)
|
|
||||||
adoptPolicy(civInfo)
|
|
||||||
} else {
|
} 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.
|
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,15 +18,19 @@ 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 {
|
||||||
const val CLOSE_ENEMY_TILES_AWAY_LIMIT = 5
|
const val CLOSE_ENEMY_TILES_AWAY_LIMIT = 5
|
||||||
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)
|
||||||
@ -33,38 +40,32 @@ class UnitAutomation{
|
|||||||
return WorkerAutomation(unit).automateWorkerAction()
|
return WorkerAutomation(unit).automateWorkerAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(unit.name=="Work Boats"){
|
if (unit.name == "Work Boats") {
|
||||||
return SpecificUnitAutomation().automateWorkBoats(unit)
|
return SpecificUnitAutomation().automateWorkBoats(unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unit.name == "Great General")
|
if (unit.name == "Great General")
|
||||||
return SpecificUnitAutomation().automateGreatGeneral(unit)
|
return SpecificUnitAutomation().automateGreatGeneral(unit)
|
||||||
|
|
||||||
if(unit.type==UnitType.Fighter)
|
if (unit.type == UnitType.Fighter)
|
||||||
return SpecificUnitAutomation().automateFighter(unit)
|
return SpecificUnitAutomation().automateFighter(unit)
|
||||||
|
|
||||||
if(unit.type==UnitType.Bomber)
|
if (unit.type == UnitType.Bomber)
|
||||||
return SpecificUnitAutomation().automateBomber(unit)
|
return SpecificUnitAutomation().automateBomber(unit)
|
||||||
|
|
||||||
if(unit.type==UnitType.Missile)
|
if (unit.type == UnitType.Missile)
|
||||||
return SpecificUnitAutomation().automateMissile(unit)
|
return SpecificUnitAutomation().automateMissile(unit)
|
||||||
|
|
||||||
if(unit.name.startsWith("Great")
|
if (unit.name.startsWith("Great")
|
||||||
&& unit.name in GreatPersonManager().statToGreatPersonMapping.values){ // So "Great War Infantry" isn't caught here
|
&& unit.name in GreatPersonManager().statToGreatPersonMapping.values) { // So "Great War Infantry" isn't caught here
|
||||||
return SpecificUnitAutomation().automateGreatPerson(unit)
|
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()
|
var unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
||||||
|
|
||||||
if(unit.civInfo.isBarbarian() &&
|
if (tryGoToRuin(unit, unitDistanceToTiles)) {
|
||||||
unit.currentTile.improvement==Constants.barbarianEncampment && unit.type.isLandUnit()) {
|
if (unit.currentMovement == 0f) return
|
||||||
if(unit.canFortify()) unit.fortify()
|
|
||||||
return // stay in the encampment
|
|
||||||
}
|
|
||||||
|
|
||||||
if(tryGoToRuin(unit,unitDistanceToTiles)){
|
|
||||||
if(unit.currentMovement==0f) return
|
|
||||||
unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,18 +74,13 @@ class UnitAutomation{
|
|||||||
// Accompany settlers
|
// Accompany settlers
|
||||||
if (tryAccompanySettlerOrGreatPerson(unit)) return
|
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 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
|
||||||
|
|
||||||
@ -98,47 +94,42 @@ class UnitAutomation{
|
|||||||
// Focus all units without a specific target on the enemy city closest to one of our cities
|
// Focus all units without a specific target on the enemy city closest to one of our cities
|
||||||
if (tryHeadTowardsEnemyCity(unit)) return
|
if (tryHeadTowardsEnemyCity(unit)) return
|
||||||
|
|
||||||
if(tryHeadTowardsEncampment(unit)) return
|
if (tryHeadTowardsEncampment(unit)) return
|
||||||
|
|
||||||
// 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()
|
||||||
|
|
||||||
if (tryPillageImprovement(unit, unitDistanceToTiles)) return true
|
if (tryPillageImprovement(unit, unitDistanceToTiles)) return true
|
||||||
|
|
||||||
val tilesByHealingRate = tilesInDistance.groupBy { unit.rankTileForHealing(it) }
|
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() }
|
val reachableCityTile = unit.civInfo.cities.map { it.getCenterTile() }
|
||||||
.sortedBy { it.arialDistanceTo(unit.currentTile) }
|
.sortedBy { it.arialDistanceTo(unit.currentTile) }
|
||||||
.firstOrNull{unit.movement.canReach(it)}
|
.firstOrNull { unit.movement.canReach(it) }
|
||||||
if(reachableCityTile!=null) unit.movement.headTowards(reachableCityTile)
|
if (reachableCityTile != null) unit.movement.headTowards(reachableCityTile)
|
||||||
else wander(unit,unitDistanceToTiles)
|
else wander(unit, unitDistanceToTiles)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,23 +138,23 @@ class UnitAutomation{
|
|||||||
val bestTileForHealing = bestTilesForHealing.maxBy { it.getDefensiveBonus() }!!
|
val bestTileForHealing = bestTilesForHealing.maxBy { it.getDefensiveBonus() }!!
|
||||||
val bestTileForHealingRank = unit.rankTileForHealing(bestTileForHealing)
|
val bestTileForHealingRank = unit.rankTileForHealing(bestTileForHealing)
|
||||||
|
|
||||||
if(currentUnitTile!=bestTileForHealing
|
if (currentUnitTile != bestTileForHealing
|
||||||
&& bestTileForHealingRank > unit.rankTileForHealing(currentUnitTile))
|
&& bestTileForHealingRank > unit.rankTileForHealing(currentUnitTile))
|
||||||
unit.movement.moveToTile(bestTileForHealing)
|
unit.movement.moveToTile(bestTileForHealing)
|
||||||
|
|
||||||
if(unit.currentMovement>0 && unit.canFortify()) unit.fortify()
|
if (unit.currentMovement > 0 && unit.canFortify()) unit.fortify()
|
||||||
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
|
||||||
.filter { unit.movement.canMoveTo(it) && UnitActions().canPillage(unit,it) }
|
.filter { unit.movement.canMoveTo(it) && UnitActions().canPillage(unit, it) }
|
||||||
|
|
||||||
if (tilesThatCanWalkToAndThenPillage.isEmpty()) return false
|
if (tilesThatCanWalkToAndThenPillage.isEmpty()) return false
|
||||||
val tileToPillage = tilesThatCanWalkToAndThenPillage.maxBy { it.getDefensiveBonus() }!!
|
val tileToPillage = tilesThatCanWalkToAndThenPillage.maxBy { it.getDefensiveBonus() }!!
|
||||||
if (unit.getTile()!=tileToPillage)
|
if (unit.getTile() != tileToPillage)
|
||||||
unit.movement.moveToTile(tileToPillage)
|
unit.movement.moveToTile(tileToPillage)
|
||||||
|
|
||||||
UnitActions().getUnitActions(unit, UncivGame.Current.worldScreen)
|
UnitActions().getUnitActions(unit, UncivGame.Current.worldScreen)
|
||||||
@ -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].
|
||||||
@ -260,20 +174,21 @@ class UnitAutomation{
|
|||||||
private fun tryAdvanceTowardsCloseEnemy(unit: MapUnit): Boolean {
|
private fun tryAdvanceTowardsCloseEnemy(unit: MapUnit): Boolean {
|
||||||
// this can be sped up if we check each layer separately
|
// this can be sped up if we check each layer separately
|
||||||
val unitDistanceToTiles = unit.movement.getDistanceToTilesWithinTurn(
|
val unitDistanceToTiles = unit.movement.getDistanceToTilesWithinTurn(
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
if(unit.type.isRanged())
|
if (unit.type.isRanged())
|
||||||
closeEnemies = closeEnemies.filterNot { it.tileToAttack.isCityCenter() && it.tileToAttack.getCity()!!.health==1 }
|
closeEnemies = closeEnemies.filterNot { it.tileToAttack.isCityCenter() && it.tileToAttack.getCity()!!.health == 1 }
|
||||||
|
|
||||||
val closestEnemy = closeEnemies.minBy { it.tileToAttack.arialDistanceTo(unit.getTile()) }
|
val closestEnemy = closeEnemies.minBy { it.tileToAttack.arialDistanceTo(unit.getTile()) }
|
||||||
|
|
||||||
@ -286,10 +201,12 @@ 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 {
|
||||||
(it.name== Constants.settler || it.name in GreatPersonManager().statToGreatPersonMapping.values)
|
val tile = it.currentTile
|
||||||
&& tile.militaryUnit==null && unit.movement.canMoveTo(tile) && unit.movement.canReach(tile) }
|
(it.name == Constants.settler || it.name in GreatPersonManager().statToGreatPersonMapping.values)
|
||||||
if(settlerOrGreatPersonToAccompany==null) return false
|
&& tile.militaryUnit == null && unit.movement.canMoveTo(tile) && unit.movement.canReach(tile)
|
||||||
|
}
|
||||||
|
if (settlerOrGreatPersonToAccompany == null) return false
|
||||||
unit.movement.headTowards(settlerOrGreatPersonToAccompany.currentTile)
|
unit.movement.headTowards(settlerOrGreatPersonToAccompany.currentTile)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -309,19 +226,20 @@ class UnitAutomation{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun tryHeadTowardsEnemyCity(unit: MapUnit): Boolean {
|
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
|
var enemyCities = unit.civInfo.gameInfo.civilizations
|
||||||
.filter { unit.civInfo.isAtWarWith(it) }
|
.filter { unit.civInfo.isAtWarWith(it) }
|
||||||
.flatMap { it.cities }.asSequence()
|
.flatMap { it.cities }.asSequence()
|
||||||
.filter { it.location in unit.civInfo.exploredTiles }
|
.filter { it.location in unit.civInfo.exploredTiles }
|
||||||
|
|
||||||
if(unit.type.isRanged()) // ranged units don't harm capturable cities, waste of a turn
|
if (unit.type.isRanged()) // ranged units don't harm capturable cities, waste of a turn
|
||||||
enemyCities = enemyCities.filterNot { it.health==1 }
|
enemyCities = enemyCities.filterNot { it.health == 1 }
|
||||||
|
|
||||||
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,44 +248,38 @@ 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 =
|
||||||
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
|
// 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 }
|
||||||
.map { it.militaryUnit!! }
|
.map { it.militaryUnit!! }
|
||||||
var totalAttackOnCityPerTurn = -20 // cities heal 20 per turn, so anything below that its useless
|
var totalAttackOnCityPerTurn = -20 // cities heal 20 per turn, so anything below that its useless
|
||||||
val enemyCityCombatant = CityCombatant(closestReachableEnemyCity.getCity()!!)
|
val enemyCityCombatant = CityCombatant(closestReachableEnemyCity.getCity()!!)
|
||||||
for(militaryUnit in militaryUnitsAroundEnemyCity){
|
for (militaryUnit in militaryUnitsAroundEnemyCity) {
|
||||||
totalAttackOnCityPerTurn += BattleDamage().calculateDamageToDefender(MapUnitCombatant(militaryUnit), enemyCityCombatant)
|
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!
|
unit.movement.headTowards(closestReachableEnemyCity) // go for it!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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,48 +300,26 @@ class UnitAutomation{
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun chooseAttackTarget(unit: MapUnit, attackableEnemies: List<AttackableTile>): AttackableTile? {
|
private fun chooseBombardTarget(city: CityInfo): TileInfo? {
|
||||||
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? {
|
|
||||||
var targets = getBombardTargets(city)
|
var targets = getBombardTargets(city)
|
||||||
if (targets.isEmpty()) return null
|
if (targets.isEmpty()) return null
|
||||||
val siegeUnits = targets
|
val siegeUnits = targets
|
||||||
.filter { Battle.getMapCombatantOfTile(it)!!.getUnitType()==UnitType.Siege }
|
.filter { Battle.getMapCombatantOfTile(it)!!.getUnitType() == UnitType.Siege }
|
||||||
if(siegeUnits.any()) targets = siegeUnits
|
if (siegeUnits.any()) targets = siegeUnits
|
||||||
else{
|
else {
|
||||||
val rangedUnits = targets
|
val rangedUnits = targets
|
||||||
.filter { Battle.getMapCombatantOfTile(it)!!.getUnitType().isRanged() }
|
.filter { Battle.getMapCombatantOfTile(it)!!.getUnitType().isRanged() }
|
||||||
if(rangedUnits.any()) targets=rangedUnits
|
if (rangedUnits.any()) targets = rangedUnits
|
||||||
}
|
}
|
||||||
return targets.minBy { Battle.getMapCombatantOfTile(it)!!.getHealth() }
|
return targets.minBy { Battle.getMapCombatantOfTile(it)!!.getHealth() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryGarrisoningUnit(unit: MapUnit): Boolean {
|
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 citiesWithoutGarrison = unit.civInfo.cities.filter {
|
||||||
val centerTile = it.getCenterTile()
|
val centerTile = it.getCenterTile()
|
||||||
centerTile.militaryUnit==null
|
centerTile.militaryUnit == null
|
||||||
&& unit.movement.canMoveTo(centerTile)
|
&& unit.movement.canMoveTo(centerTile)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isCityThatNeedsDefendingInWartime(city: CityInfo): Boolean {
|
fun isCityThatNeedsDefendingInWartime(city: CityInfo): Boolean {
|
||||||
@ -473,69 +327,70 @@ class UnitAutomation{
|
|||||||
for (enemyCivCity in unit.civInfo.diplomacy.values
|
for (enemyCivCity in unit.civInfo.diplomacy.values
|
||||||
.filter { it.diplomaticStatus == DiplomaticStatus.War }
|
.filter { it.diplomaticStatus == DiplomaticStatus.War }
|
||||||
.map { it.otherCiv() }.flatMap { it.cities })
|
.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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val citiesToTry:Sequence<CityInfo>
|
val citiesToTry: Sequence<CityInfo>
|
||||||
|
|
||||||
if (!unit.civInfo.isAtWar()) {
|
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.
|
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()
|
citiesToTry = citiesWithoutGarrison.asSequence()
|
||||||
} else {
|
} else {
|
||||||
if (unit.getTile().isCityCenter() &&
|
if (unit.getTile().isCityCenter() &&
|
||||||
isCityThatNeedsDefendingInWartime(unit.getTile().getCity()!!)) return true
|
isCityThatNeedsDefendingInWartime(unit.getTile().getCity()!!)) return true
|
||||||
|
|
||||||
citiesToTry = citiesWithoutGarrison.asSequence()
|
citiesToTry = citiesWithoutGarrison.asSequence()
|
||||||
.filter { isCityThatNeedsDefendingInWartime(it) }
|
.filter { isCityThatNeedsDefendingInWartime(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val closestReachableCityNeedsDefending =citiesToTry
|
val closestReachableCityNeedsDefending = citiesToTry
|
||||||
.sortedBy{ it.getCenterTile().arialDistanceTo(unit.currentTile) }
|
.sortedBy { it.getCenterTile().arialDistanceTo(unit.currentTile) }
|
||||||
.firstOrNull { unit.movement.canReach(it.getCenterTile()) }
|
.firstOrNull { unit.movement.canReach(it.getCenterTile()) }
|
||||||
if(closestReachableCityNeedsDefending==null) return false
|
if (closestReachableCityNeedsDefending == null) return false
|
||||||
unit.movement.headTowards(closestReachableCityNeedsDefending.getCenterTile())
|
unit.movement.headTowards(closestReachableCityNeedsDefending.getCenterTile())
|
||||||
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) }
|
||||||
if(tileWithRuin==null) return false
|
if (tileWithRuin == null) return false
|
||||||
unit.movement.moveToTile(tileWithRuin)
|
unit.movement.moveToTile(tileWithRuin)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for(tile in unit.currentTile.getTilesInDistance(5))
|
for (tile in unit.currentTile.getTilesInDistance(5))
|
||||||
if(unit.movement.canMoveTo(tile) && tile.position !in unit.civInfo.exploredTiles
|
if (unit.movement.canMoveTo(tile) && tile.position !in unit.civInfo.exploredTiles
|
||||||
&& unit.movement.canReach(tile)){
|
&& unit.movement.canReach(tile)) {
|
||||||
unit.movement.headTowards(tile)
|
unit.movement.headTowards(tile)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun automatedExplore(unit:MapUnit){
|
fun automatedExplore(unit: MapUnit) {
|
||||||
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
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) {
|
if (unit.health < 80) {
|
||||||
tryHealUnit(unit,unitDistanceToTiles)
|
tryHealUnit(unit, unitDistanceToTiles)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
if(unexploredTilesAtDistance.isNotEmpty()){
|
&& unit.movement.canReach(it)
|
||||||
|
}
|
||||||
|
if (unexploredTilesAtDistance.isNotEmpty()) {
|
||||||
unit.movement.headTowards(unexploredTilesAtDistance.random())
|
unit.movement.headTowards(unexploredTilesAtDistance.random())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -543,15 +398,12 @@ 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) }
|
||||||
|
|
||||||
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,7 +22,9 @@ import kotlin.math.max
|
|||||||
|
|
||||||
class BattleTable(val worldScreen: WorldScreen): Table() {
|
class BattleTable(val worldScreen: WorldScreen): Table() {
|
||||||
|
|
||||||
init{
|
private val battleHelper = BattleHelper()
|
||||||
|
|
||||||
|
init {
|
||||||
isVisible = false
|
isVisible = false
|
||||||
skin = CameraStageBaseScreen.skin
|
skin = CameraStageBaseScreen.skin
|
||||||
background = ImageGetter.getBackground(ImageGetter.getBlue())
|
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 }
|
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