Military unit healing improvement (#11195)

* Military units prioritize healing more

* Military units try to pillage tiles when attacking

* tryHeal() now tries to pillage multiple times if a unit have more movement

* Barbarians now pillage more

* Units stay healing on its tile if it can heal in two turns

* Units will heal when no enemies are around in their territory

* TryHealUnit returns false after pillaging to full health

* Refactored canUnitHealInTurnsOnCurrentTile

* Refactored movePreparingAttack pillaging
This commit is contained in:
Oskar Niesen 2024-02-28 15:45:35 -06:00 committed by GitHub
parent c8f9f38d96
commit a5f1ba0401
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 54 additions and 10 deletions

View File

@ -49,7 +49,7 @@ class BarbarianAutomation(val civInfo: Civilization) {
private fun automateCombatUnit(unit: MapUnit) {
// 1 - Try pillaging to restore health (barbs don't auto-heal)
if (unit.health < 50 && UnitAutomation.tryPillageImprovement(unit)) return
if (unit.health < 50 && UnitAutomation.tryPillageImprovement(unit, true) && unit.currentMovement == 0f) return
// 2 - trying to upgrade
if (UnitAutomation.tryUpgradeUnit(unit)) return
@ -60,7 +60,9 @@ class BarbarianAutomation(val civInfo: Civilization) {
if (!unit.isCivilian() && BattleHelper.tryAttackNearbyEnemy(unit)) return
// 4 - trying to pillage tile or route
if (UnitAutomation.tryPillageImprovement(unit)) return
while (UnitAutomation.tryPillageImprovement(unit)) {
if (unit.currentMovement == 0f) return
}
// 6 - wander
UnitAutomation.wander(unit)

View File

@ -213,13 +213,16 @@ object UnitAutomation {
if (tryUpgradeUnit(unit)) return
if (unit.health < 50 && (trySwapRetreat(unit) || tryHealUnit(unit))) return // do nothing but heal
// If there are no enemies nearby and we can heal here, wait until we are at full health
if (unit.health < 100 && canUnitHealInTurnsOnCurrentTile(unit,2, 4)) return
// Accompany settlers
if (tryAccompanySettlerOrGreatPerson(unit)) return
if (tryHeadTowardsOurSiegedCity(unit)) return
if (unit.health < 50 && (trySwapRetreat(unit) || tryHealUnit(unit))) return // do nothing but heal
// if a embarked melee unit can land and attack next turn, do not attack from water.
if (BattleHelper.tryDisembarkUnitToAttackPosition(unit)) return
@ -314,10 +317,18 @@ object UnitAutomation {
if (unit.baseUnit.isRanged() && unit.hasUnique(UniqueType.HealsEvenAfterAction))
return false // will heal anyway, and attacks don't hurt
if (tryPillageImprovement(unit)) return true
// Try pillage improvements until healed
while(tryPillageImprovement(unit, false)) {
// If we are fully healed and can still do things, lets keep on going by returning false
if (unit.currentMovement == 0f || unit.health == 100) return unit.currentMovement == 0f
}
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
if (unitDistanceToTiles.isEmpty()) return true // can't move, so...
// If the unit can heal on this tile in two turns, just heal here
if (canUnitHealInTurnsOnCurrentTile(unit,3,)) return true
val currentUnitTile = unit.getTile()
val dangerousTiles = unit.civ.threatManager.getDangerousTiles(unit)
@ -366,6 +377,19 @@ object UnitAutomation {
return true
}
/**
* @return true if the tile is safe and the unit can heal to full within [turns]
*/
private fun canUnitHealInTurnsOnCurrentTile(unit: MapUnit, turns: Int, noEnemyDistance: Int = 3): Boolean {
if (unit.hasUnique(UniqueType.HealsEvenAfterAction)) return false // We can keep on moving
// Check if we are not in a safe city and there is an enemy nearby this isn't a good tile to heal on
if (!(unit.getTile().isCityCenter() && unit.getTile().getCity()!!.health > 50)
&& unit.civ.threatManager.getDistanceToClosestEnemyUnit(unit.getTile(), noEnemyDistance) <= noEnemyDistance) return false
val healthRequiredPerTurn = (100 - unit.health) / turns
return healthRequiredPerTurn <= unit.rankTileForHealing(unit.getTile())
}
private fun getDangerousTiles(unit: MapUnit): HashSet<Tile> {
val nearbyRangedEnemyUnits = unit.currentTile.getTilesInDistance(3)
.flatMap { tile -> tile.getUnits().filter { unit.civ.isAtWarWith(it.civ) } }
@ -383,14 +407,17 @@ object UnitAutomation {
return (tilesInRangeOfAttack + tilesWithinBombardmentRange + tilesWithTerrainDamage).toHashSet()
}
fun tryPillageImprovement(unit: MapUnit): Boolean {
/**
* @return true if the unit was able to pillage a tile, false otherwise
*/
fun tryPillageImprovement(unit: MapUnit, onlyPillageToHeal: Boolean = false): Boolean {
if (unit.isCivilian()) return false
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
val tilesThatCanWalkToAndThenPillage = unitDistanceToTiles
.filter { it.value.totalDistance < unit.currentMovement }.keys
.filter { unit.movement.canMoveTo(it) && UnitActionsPillage.canPillage(unit, it)
&& (it.canPillageTileImprovement()
|| (it.canPillageRoad() && it.getRoadOwner() != null && unit.civ.isAtWarWith(it.getRoadOwner()!!))) }
|| (!onlyPillageToHeal && it.canPillageRoad() && it.getRoadOwner() != null && unit.civ.isAtWarWith(it.getRoadOwner()!!))) }
if (tilesThatCanWalkToAndThenPillage.isEmpty()) return false
val tileToPillage = tilesThatCanWalkToAndThenPillage.maxByOrNull { it.getDefensiveBonus(false) }!!
@ -400,7 +427,7 @@ object UnitAutomation {
// We CANNOT use invokeUnitAction, since the default unit action contains a popup, which - when automated -
// runs a UI action on a side thread leading to crash!
UnitActionsPillage.getPillageAction(unit, unit.currentTile)?.action?.invoke()
return unit.currentMovement == 0f
return true
}
/** Move towards the closest attackable enemy of the [unit].

View File

@ -22,6 +22,7 @@ import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats
import com.unciv.ui.components.UnitMovementMemoryType
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsPillage
import com.unciv.utils.debug
import kotlin.math.max
import kotlin.math.min
@ -39,7 +40,7 @@ object Battle {
* Currently not used by UI, only by automation via [BattleHelper.tryAttackNearbyEnemy][com.unciv.logic.automation.unit.BattleHelper.tryAttackNearbyEnemy]
*/
fun moveAndAttack(attacker: ICombatant, attackableTile: AttackableTile) {
if (!movePreparingAttack(attacker, attackableTile)) return
if (!movePreparingAttack(attacker, attackableTile, true)) return
attackOrNuke(attacker, attackableTile)
}
@ -48,8 +49,9 @@ object Battle {
*
* This is a logic function, not UI, so e.g. sound needs to be handled after calling this.
*/
fun movePreparingAttack(attacker: ICombatant, attackableTile: AttackableTile): Boolean {
fun movePreparingAttack(attacker: ICombatant, attackableTile: AttackableTile, tryHealPillage: Boolean = false): Boolean {
if (attacker !is MapUnitCombatant) return true
val tilesMovedThrough = attacker.unit.movement.getDistanceToTiles().getPathToTile(attackableTile.tileToAttackFrom)
attacker.unit.movement.moveToTile(attackableTile.tileToAttackFrom)
/**
* When calculating movement distance, we assume that a hidden tile is 1 movement point,
@ -74,6 +76,19 @@ object Battle {
attacker.unit.action = UnitActionType.SetUp.value
attacker.unit.useMovementPoints(1f)
}
if (tryHealPillage) {
// Now lets retroactively see if we can pillage any improvement on the path improvement to heal
// while still being able to attack
for (tileToPillage in tilesMovedThrough) {
if (attacker.unit.currentMovement <= 1f || attacker.unit.health > 90) break // We are done pillaging
if (UnitActionsPillage.canPillage(attacker.unit, tileToPillage)
&& tileToPillage.canPillageTileImprovement()) {
UnitActionsPillage.getPillageAction(attacker.unit, tileToPillage)?.action?.invoke()
}
}
}
return (attacker.unit.currentMovement > 0f)
}