Refactor: Move checking targets from automation to logic (#9945)

* Move checking targets from automation to logic

* Ending newline, move attackable tile

* move getBombardableTiles for similar reasons

* fix package name

* remove import
This commit is contained in:
SeventhM 2023-08-19 23:18:47 -07:00 committed by GitHub
parent 7cb39dcecf
commit 08a04d3575
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 158 additions and 152 deletions

View File

@ -1,21 +1,18 @@
package com.unciv.logic.automation.unit
import com.unciv.Constants
import com.unciv.logic.battle.AttackableTile
import com.unciv.logic.battle.Battle
import com.unciv.logic.battle.BattleDamage
import com.unciv.logic.battle.CityCombatant
import com.unciv.logic.battle.ICombatant
import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.battle.TargetHelper
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.mapunit.PathsToTilesWithinTurn
import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.unique.UniqueType
object BattleHelper {
fun tryAttackNearbyEnemy(unit: MapUnit, stayOnTile: Boolean = false): Boolean {
if (unit.hasUnique(UniqueType.CannotAttack)) return false
val attackableEnemies = getAttackableEnemies(unit, unit.movement.getDistanceToTiles(), stayOnTile=stayOnTile)
val attackableEnemies = TargetHelper.getAttackableEnemies(unit, unit.movement.getDistanceToTiles(), stayOnTile=stayOnTile)
// Only take enemies we can fight without dying
.filter {
BattleDamage.calculateDamageToAttacker(
@ -32,131 +29,11 @@ object BattleHelper {
return unit.currentMovement == 0f
}
fun getAttackableEnemies(
unit: MapUnit,
unitDistanceToTiles: PathsToTilesWithinTurn,
tilesToCheck: List<Tile>? = null,
stayOnTile: Boolean = false
): ArrayList<AttackableTile> {
val rangeOfAttack = unit.getRange()
val attackableTiles = ArrayList<AttackableTile>()
val unitMustBeSetUp = unit.hasUnique(UniqueType.MustSetUp)
val tilesToAttackFrom = if (stayOnTile || unit.baseUnit.movesLikeAirUnits())
sequenceOf(Pair(unit.currentTile, unit.currentMovement))
else
unitDistanceToTiles.asSequence()
.map { (tile, distance) ->
val movementPointsToExpendAfterMovement = if (unitMustBeSetUp) 1 else 0
val movementPointsToExpendHere =
if (unitMustBeSetUp && !unit.isSetUpForSiege()) 1 else 0
val movementPointsToExpendBeforeAttack =
if (tile == unit.currentTile) movementPointsToExpendHere else movementPointsToExpendAfterMovement
val movementLeft =
unit.currentMovement - distance.totalDistance - movementPointsToExpendBeforeAttack
Pair(tile, movementLeft)
}
// still got leftover movement points after all that, to attack
.filter { it.second > Constants.minimumMovementEpsilon }
.filter {
it.first == unit.getTile() || unit.movement.canMoveTo(it.first)
}
val tilesWithEnemies: HashSet<Tile> = HashSet()
val tilesWithoutEnemies: HashSet<Tile> = HashSet()
for ((reachableTile, movementLeft) in tilesToAttackFrom) { // tiles we'll still have energy after we reach there
val tilesInAttackRange =
if (unit.hasUnique(UniqueType.IndirectFire) || unit.baseUnit.movesLikeAirUnits())
reachableTile.getTilesInDistance(rangeOfAttack)
else reachableTile.tileMap.getViewableTiles(reachableTile.position, rangeOfAttack, true).asSequence()
for (tile in tilesInAttackRange) {
// Since military units can technically enter tiles with enemy civilians,
// some try to move to to the tile and then attack the unit it contains, which is silly
if (tile == reachableTile) continue
if (tile in tilesWithEnemies) attackableTiles += AttackableTile(
reachableTile,
tile,
movementLeft,
Battle.getMapCombatantOfTile(tile)
)
else if (tile in tilesWithoutEnemies) continue // avoid checking the same empty tile multiple times
else if (tileContainsAttackableEnemy(unit, tile, tilesToCheck)) {
tilesWithEnemies += tile
attackableTiles += AttackableTile(
reachableTile, tile, movementLeft,
Battle.getMapCombatantOfTile(tile)
)
} else if (unit.isPreparingAirSweep()) {
tilesWithEnemies += tile
attackableTiles += AttackableTile(
reachableTile, tile, movementLeft,
Battle.getMapCombatantOfTile(tile)
)
} else tilesWithoutEnemies += tile
}
}
return attackableTiles
}
private fun tileContainsAttackableEnemy(unit: MapUnit, tile: Tile, tilesToCheck: List<Tile>?): Boolean {
if (!containsAttackableEnemy(tile, MapUnitCombatant(unit))) return false
if (tile !in (tilesToCheck ?: unit.civ.viewableTiles)) return false
val mapCombatant = Battle.getMapCombatantOfTile(tile)
return (!unit.baseUnit.isMelee() || mapCombatant !is MapUnitCombatant || !mapCombatant.unit.isCivilian() || unit.movement.canPassThrough(tile))
}
fun containsAttackableEnemy(tile: Tile, combatant: ICombatant): Boolean {
if (combatant is MapUnitCombatant && combatant.unit.isEmbarked() && !combatant.hasUnique(UniqueType.AttackOnSea)) {
// Can't attack water units while embarked, only land
if (tile.isWater || combatant.isRanged())
return false
}
val tileCombatant = Battle.getMapCombatantOfTile(tile) ?: return false
if (tileCombatant.getCivInfo() == combatant.getCivInfo()) return false
// If the user automates units, one may capture the city before the user had a chance to decide what to do with it,
// and then the next unit should not attack that city
if (tileCombatant is CityCombatant && tileCombatant.city.hasJustBeenConquered) return false
if (!combatant.getCivInfo().isAtWarWith(tileCombatant.getCivInfo())) return false
if (combatant is MapUnitCombatant && combatant.isLandUnit() && combatant.isMelee() && tile.isWater &&
!combatant.getCivInfo().tech.unitsCanEmbark && !combatant.unit.cache.canMoveOnWater
)
return false
if (combatant is MapUnitCombatant && combatant.hasUnique(UniqueType.CannotAttack))
return false
if (combatant is MapUnitCombatant &&
combatant.unit.getMatchingUniques(UniqueType.CanOnlyAttackUnits).run {
any() && none { tileCombatant.matchesCategory(it.params[0]) }
}
)
return false
if (combatant is MapUnitCombatant &&
combatant.unit.getMatchingUniques(UniqueType.CanOnlyAttackTiles).run {
any() && none { tile.matchesFilter(it.params[0]) }
}
)
return false
// Only units with the right unique can view submarines (or other invisible units) from more then one tile away.
// Garrisoned invisible units can be attacked by anyone, as else the city will be in invincible.
if (tileCombatant.isInvisible(combatant.getCivInfo()) && !tile.isCityCenter()) {
return combatant is MapUnitCombatant
&& combatant.getCivInfo().viewableInvisibleUnitsTiles.map { it.position }.contains(tile.position)
}
return true
}
fun tryDisembarkUnitToAttackPosition(unit: MapUnit): Boolean {
if (!unit.baseUnit.isMelee() || !unit.baseUnit.isLandUnit() || !unit.isEmbarked()) return false
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
val attackableEnemiesNextTurn = getAttackableEnemies(unit, unitDistanceToTiles)
val attackableEnemiesNextTurn = TargetHelper.getAttackableEnemies(unit, unitDistanceToTiles)
// Only take enemies we can fight without dying
.filter {
BattleDamage.calculateDamageToAttacker(

View File

@ -5,6 +5,7 @@ import com.unciv.logic.automation.Automation
import com.unciv.logic.battle.Battle
import com.unciv.logic.battle.GreatGeneralImplementation
import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.battle.TargetHelper
import com.unciv.logic.city.City
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.logic.map.mapunit.MapUnit
@ -480,7 +481,7 @@ object SpecificUnitAutomation {
.filter { destinationCity ->
destinationCity != airUnit.currentTile
&& destinationCity.getTilesInDistance(airUnit.getRange())
.any { BattleHelper.containsAttackableEnemy(it, MapUnitCombatant(airUnit)) }
.any { TargetHelper.containsAttackableEnemy(it, MapUnitCombatant(airUnit)) }
}
if (citiesThatCanAttackFrom.isEmpty()) return
@ -547,7 +548,7 @@ object SpecificUnitAutomation {
if (city.getTilesInDistance(unit.getRange())
.any {
it.isVisible(unit.civ) &&
BattleHelper.containsAttackableEnemy(
TargetHelper.containsAttackableEnemy(
it,
MapUnitCombatant(unit)
)

View File

@ -8,6 +8,7 @@ import com.unciv.logic.battle.BattleDamage
import com.unciv.logic.battle.CityCombatant
import com.unciv.logic.battle.ICombatant
import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.battle.TargetHelper
import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.NotificationCategory
@ -467,11 +468,6 @@ object UnitAutomation {
return unit.currentMovement == 0f
}
/** Get a list of visible tiles which have something attackable */
fun getBombardableTiles(city: City): Sequence<Tile> =
city.getCenterTile().getTilesInDistance(city.range)
.filter { it.isVisible(city.civ) && BattleHelper.containsAttackableEnemy(it, CityCombatant(city)) }
/** Move towards the closest attackable enemy of the [unit].
*
* Limited by [CLOSE_ENEMY_TURNS_AWAY_LIMIT] and [CLOSE_ENEMY_TILES_AWAY_LIMIT].
@ -482,7 +478,7 @@ object UnitAutomation {
unit.getTile().position,
unit.getMaxMovement() * CLOSE_ENEMY_TURNS_AWAY_LIMIT
)
var closeEnemies = BattleHelper.getAttackableEnemies(
var closeEnemies = TargetHelper.getAttackableEnemies(
unit,
unitDistanceToTiles,
tilesToCheck = unit.getTile().getTilesInDistance(CLOSE_ENEMY_TILES_AWAY_LIMIT).toList()
@ -674,7 +670,7 @@ object UnitAutomation {
}
private fun chooseBombardTarget(city: City): ICombatant? {
var targets = getBombardableTiles(city).map { Battle.getMapCombatantOfTile(it)!! }
var targets = TargetHelper.getBombardableTiles(city).map { Battle.getMapCombatantOfTile(it)!! }
if (targets.none()) return null
val siegeUnits = targets

View File

@ -1,4 +1,4 @@
package com.unciv.logic.automation.unit
package com.unciv.logic.battle
import com.unciv.logic.battle.ICombatant
import com.unciv.logic.map.tile.Tile

View File

@ -4,7 +4,6 @@ import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.automation.civilization.NextTurnAutomation
import com.unciv.logic.automation.unit.AttackableTile
import com.unciv.logic.automation.unit.SpecificUnitAutomation
import com.unciv.logic.city.City
import com.unciv.logic.civilization.AlertType

View File

@ -1,6 +1,5 @@
package com.unciv.logic.battle
import com.unciv.logic.automation.unit.BattleHelper
import com.unciv.logic.automation.unit.SpecificUnitAutomation
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
@ -95,7 +94,7 @@ object GreatGeneralImplementation {
unitTile.getTilesInDistance(unitBonusRadius).sumOf { auraTile ->
val militaryUnit = auraTile.militaryUnit
if (militaryUnit == null || militaryUnit.civ != general.civ || militaryUnit.isEmbarked()) 0
else if (BattleHelper.getAttackableEnemies(militaryUnit, militaryUnit.movement.getDistanceToTiles()).isEmpty()) 0
else if (TargetHelper.getAttackableEnemies(militaryUnit, militaryUnit.movement.getDistanceToTiles()).isEmpty()) 0
else generalBonusData.firstOrNull {
// "Military" as commented above only a small optimization
auraTile.aerialDistanceTo(unitTile) <= it.radius

View File

@ -0,0 +1,136 @@
package com.unciv.logic.battle
import com.unciv.Constants
import com.unciv.logic.city.City
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.mapunit.PathsToTilesWithinTurn
import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.unique.UniqueType
object TargetHelper {
fun getAttackableEnemies(
unit: MapUnit,
unitDistanceToTiles: PathsToTilesWithinTurn,
tilesToCheck: List<Tile>? = null,
stayOnTile: Boolean = false
): ArrayList<AttackableTile> {
val rangeOfAttack = unit.getRange()
val attackableTiles = ArrayList<AttackableTile>()
val unitMustBeSetUp = unit.hasUnique(UniqueType.MustSetUp)
val tilesToAttackFrom = if (stayOnTile || unit.baseUnit.movesLikeAirUnits())
sequenceOf(Pair(unit.currentTile, unit.currentMovement))
else
unitDistanceToTiles.asSequence()
.map { (tile, distance) ->
val movementPointsToExpendAfterMovement = if (unitMustBeSetUp) 1 else 0
val movementPointsToExpendHere =
if (unitMustBeSetUp && !unit.isSetUpForSiege()) 1 else 0
val movementPointsToExpendBeforeAttack =
if (tile == unit.currentTile) movementPointsToExpendHere else movementPointsToExpendAfterMovement
val movementLeft =
unit.currentMovement - distance.totalDistance - movementPointsToExpendBeforeAttack
Pair(tile, movementLeft)
}
// still got leftover movement points after all that, to attack
.filter { it.second > Constants.minimumMovementEpsilon }
.filter {
it.first == unit.getTile() || unit.movement.canMoveTo(it.first)
}
val tilesWithEnemies: HashSet<Tile> = HashSet()
val tilesWithoutEnemies: HashSet<Tile> = HashSet()
for ((reachableTile, movementLeft) in tilesToAttackFrom) { // tiles we'll still have energy after we reach there
val tilesInAttackRange =
if (unit.hasUnique(UniqueType.IndirectFire) || unit.baseUnit.movesLikeAirUnits())
reachableTile.getTilesInDistance(rangeOfAttack)
else reachableTile.tileMap.getViewableTiles(reachableTile.position, rangeOfAttack, true).asSequence()
for (tile in tilesInAttackRange) {
// Since military units can technically enter tiles with enemy civilians,
// some try to move to to the tile and then attack the unit it contains, which is silly
if (tile == reachableTile) continue
if (tile in tilesWithEnemies) attackableTiles += AttackableTile(
reachableTile,
tile,
movementLeft,
Battle.getMapCombatantOfTile(tile)
)
else if (tile in tilesWithoutEnemies) continue // avoid checking the same empty tile multiple times
else if (tileContainsAttackableEnemy(unit, tile, tilesToCheck)) {
tilesWithEnemies += tile
attackableTiles += AttackableTile(
reachableTile, tile, movementLeft,
Battle.getMapCombatantOfTile(tile)
)
} else if (unit.isPreparingAirSweep()) {
tilesWithEnemies += tile
attackableTiles += AttackableTile(
reachableTile, tile, movementLeft,
Battle.getMapCombatantOfTile(tile)
)
} else tilesWithoutEnemies += tile
}
}
return attackableTiles
}
private fun tileContainsAttackableEnemy(unit: MapUnit, tile: Tile, tilesToCheck: List<Tile>?): Boolean {
if (!containsAttackableEnemy(tile, MapUnitCombatant(unit))) return false
if (tile !in (tilesToCheck ?: unit.civ.viewableTiles)) return false
val mapCombatant = Battle.getMapCombatantOfTile(tile)
return (!unit.baseUnit.isMelee() || mapCombatant !is MapUnitCombatant || !mapCombatant.unit.isCivilian() || unit.movement.canPassThrough(tile))
}
fun containsAttackableEnemy(tile: Tile, combatant: ICombatant): Boolean {
if (combatant is MapUnitCombatant && combatant.unit.isEmbarked() && !combatant.hasUnique(UniqueType.AttackOnSea)) {
// Can't attack water units while embarked, only land
if (tile.isWater || combatant.isRanged())
return false
}
val tileCombatant = Battle.getMapCombatantOfTile(tile) ?: return false
if (tileCombatant.getCivInfo() == combatant.getCivInfo()) return false
// If the user automates units, one may capture the city before the user had a chance to decide what to do with it,
// and then the next unit should not attack that city
if (tileCombatant is CityCombatant && tileCombatant.city.hasJustBeenConquered) return false
if (!combatant.getCivInfo().isAtWarWith(tileCombatant.getCivInfo())) return false
if (combatant is MapUnitCombatant && combatant.isLandUnit() && combatant.isMelee() && tile.isWater &&
!combatant.getCivInfo().tech.unitsCanEmbark && !combatant.unit.cache.canMoveOnWater
)
return false
if (combatant is MapUnitCombatant && combatant.hasUnique(UniqueType.CannotAttack))
return false
if (combatant is MapUnitCombatant &&
combatant.unit.getMatchingUniques(UniqueType.CanOnlyAttackUnits).run {
any() && none { tileCombatant.matchesCategory(it.params[0]) }
}
)
return false
if (combatant is MapUnitCombatant &&
combatant.unit.getMatchingUniques(UniqueType.CanOnlyAttackTiles).run {
any() && none { tile.matchesFilter(it.params[0]) }
}
)
return false
// Only units with the right unique can view submarines (or other invisible units) from more then one tile away.
// Garrisoned invisible units can be attacked by anyone, as else the city will be in invincible.
if (tileCombatant.isInvisible(combatant.getCivInfo()) && !tile.isCityCenter()) {
return combatant is MapUnitCombatant
&& combatant.getCivInfo().viewableInvisibleUnitsTiles.map { it.position }.contains(tile.position)
}
return true
}
/** Get a list of visible tiles which have something attackable */
fun getBombardableTiles(city: City): Sequence<Tile> =
city.getCenterTile().getTilesInDistance(city.range)
.filter { it.isVisible(city.civ) && containsAttackableEnemy(it, CityCombatant(city)) }
}

View File

@ -15,12 +15,11 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.automation.unit.AttackableTile
import com.unciv.logic.automation.unit.BattleHelper
import com.unciv.logic.automation.unit.CityLocationTileRanker
import com.unciv.logic.automation.unit.UnitAutomation
import com.unciv.logic.battle.AttackableTile
import com.unciv.logic.battle.Battle
import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.battle.TargetHelper
import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.TileMap
@ -224,7 +223,7 @@ class WorldMapHolder(
/** If we are in unit-swapping mode and didn't find a swap partner, we don't want to move or attack */
} else {
// This seems inefficient as the tileToAttack is already known - but the method also calculates tileToAttackFrom
val attackableTile = BattleHelper
val attackableTile = TargetHelper
.getAttackableEnemies(unit, unit.movement.getDistanceToTiles())
.firstOrNull { it.tileToAttack == tile }
if (unit.canAttack() && attackableTile != null) {
@ -700,7 +699,7 @@ class WorldMapHolder(
|| (targetTile.isCityCenter() && unit.civ.hasExplored(targetTile)) }
.map { AttackableTile(unit.getTile(), it, 1f, null) }
.toList()
else BattleHelper.getAttackableEnemies(unit, unit.movement.getDistanceToTiles())
else TargetHelper.getAttackableEnemies(unit, unit.movement.getDistanceToTiles())
.filter { it.tileToAttack.isVisible(unit.civ) }
.distinctBy { it.tileToAttack }
@ -730,7 +729,7 @@ class WorldMapHolder(
private fun updateBombardableTilesForSelectedCity(city: City) {
if (!city.canBombard()) return
for (attackableTile in UnitAutomation.getBombardableTiles(city)) {
for (attackableTile in TargetHelper.getBombardableTiles(city)) {
val group = tileGroups[attackableTile]!!
group.layerOverlay.showHighlight(colorFromRGB(237, 41, 57))
group.layerOverlay.showCrosshair()

View File

@ -4,14 +4,13 @@ import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.logic.automation.unit.AttackableTile
import com.unciv.logic.automation.unit.BattleHelper
import com.unciv.logic.automation.unit.UnitAutomation
import com.unciv.logic.battle.AttackableTile
import com.unciv.logic.battle.Battle
import com.unciv.logic.battle.BattleDamage
import com.unciv.logic.battle.CityCombatant
import com.unciv.logic.battle.ICombatant
import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.battle.TargetHelper
import com.unciv.logic.map.tile.Tile
import com.unciv.models.UncivSound
import com.unciv.models.ruleset.unique.UniqueType
@ -73,7 +72,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
val defender = tryGetDefender() ?: return hide()
if (attacker is CityCombatant && defender is CityCombatant) return hide()
val tileToAttackFrom = if (attacker is MapUnitCombatant)
BattleHelper.getAttackableEnemies(
TargetHelper.getAttackableEnemies(
attacker.unit,
attacker.unit.movement.getDistanceToTiles()
)
@ -247,11 +246,11 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
if (attacker.canAttack()) {
if (attacker is MapUnitCombatant) {
attackableTile = BattleHelper
attackableTile = TargetHelper
.getAttackableEnemies(attacker.unit, attacker.unit.movement.getDistanceToTiles())
.firstOrNull{ it.tileToAttack == defender.getTile()}
} else if (attacker is CityCombatant) {
val canBombard = UnitAutomation.getBombardableTiles(attacker.city).contains(defender.getTile())
val canBombard = TargetHelper.getBombardableTiles(attacker.city).contains(defender.getTile())
if (canBombard) {
attackableTile = AttackableTile(attacker.getTile(), defender.getTile(), 0f, defender)
}