mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-09 23:39:40 +07:00
Animate battle damage numbers (#9387)
* Animated battle damage numbers 001 * Animated battle damage numbers 002 * Animated battle damage numbers 003 * Animated battle damage numbers 004
This commit is contained in:
@ -40,6 +40,8 @@ object Battle {
|
||||
/**
|
||||
* Moves [attacker] to [attackableTile], handles siege setup then attacks if still possible
|
||||
* (by calling [attack] or [NUKE]). Does _not_ play the attack sound!
|
||||
*
|
||||
* 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
|
||||
@ -83,14 +85,16 @@ object Battle {
|
||||
/**
|
||||
* This is meant to be called only after all prerequisite checks have been done.
|
||||
*/
|
||||
fun attackOrNuke(attacker: ICombatant, attackableTile: AttackableTile) {
|
||||
if (attacker is MapUnitCombatant && attacker.unit.baseUnit.isNuclearWeapon())
|
||||
fun attackOrNuke(attacker: ICombatant, attackableTile: AttackableTile): DamageDealt {
|
||||
return if (attacker is MapUnitCombatant && attacker.unit.baseUnit.isNuclearWeapon()) {
|
||||
NUKE(attacker, attackableTile.tileToAttack)
|
||||
else
|
||||
DamageDealt.None
|
||||
} else {
|
||||
attack(attacker, getMapCombatantOfTile(attackableTile.tileToAttack)!!)
|
||||
}
|
||||
}
|
||||
|
||||
fun attack(attacker: ICombatant, defender: ICombatant) {
|
||||
fun attack(attacker: ICombatant, defender: ICombatant): DamageDealt {
|
||||
debug("%s %s attacked %s %s", attacker.getCivInfo().civName, attacker.getName(), defender.getCivInfo().civName, defender.getName())
|
||||
val attackedTile = defender.getTile()
|
||||
if (attacker is MapUnitCombatant) {
|
||||
@ -103,10 +107,11 @@ object Battle {
|
||||
))
|
||||
}
|
||||
|
||||
val interceptDamage: DamageDealt
|
||||
if (attacker is MapUnitCombatant && attacker.unit.baseUnit.isAirUnit()) {
|
||||
tryInterceptAirAttack(attacker, attackedTile, defender.getCivInfo(), defender)
|
||||
if (attacker.isDefeated()) return
|
||||
}
|
||||
interceptDamage = tryInterceptAirAttack(attacker, attackedTile, defender.getCivInfo(), defender)
|
||||
if (attacker.isDefeated()) return interceptDamage
|
||||
} else interceptDamage = DamageDealt.None
|
||||
|
||||
// Withdraw from melee ability
|
||||
if (attacker is MapUnitCombatant && attacker.isMelee() && defender is MapUnitCombatant) {
|
||||
@ -114,7 +119,8 @@ object Battle {
|
||||
val combinedProbabilityToStayPut = withdrawUniques.fold(100) { probabilityToStayPut, unique -> probabilityToStayPut * (100-unique.params[0].toInt()) / 100 }
|
||||
val baseWithdrawChance = 100 - combinedProbabilityToStayPut
|
||||
// If a mod allows multiple withdraw properties, they stack multiplicatively
|
||||
if (baseWithdrawChance != 0 && doWithdrawFromMeleeAbility(attacker, defender, baseWithdrawChance)) return
|
||||
if (baseWithdrawChance != 0 && doWithdrawFromMeleeAbility(attacker, defender, baseWithdrawChance))
|
||||
return DamageDealt.None
|
||||
}
|
||||
|
||||
val isAlreadyDefeatedCity = defender is CityCombatant && defender.isDefeated()
|
||||
@ -158,7 +164,6 @@ object Battle {
|
||||
UniqueTriggerActivation.triggerUnitwideUnique(unique, ourUnit.unit, triggerNotificationText = "due to our [${ourUnit.getName()}] defeating a [${enemy.getName()}]")
|
||||
}
|
||||
|
||||
|
||||
// Add culture when defeating a barbarian when Honor policy is adopted, gold from enemy killed when honor is complete
|
||||
// or any enemy military unit with Sacrificial captives unique (can be either attacker or defender!)
|
||||
if (defender.isDefeated() && defender is MapUnitCombatant && !defender.unit.isCivilian()) {
|
||||
@ -197,6 +202,8 @@ object Battle {
|
||||
.firstOrNull { it.text == "Your city [${attacker.getName()}] can bombard the enemy!" }
|
||||
attacker.getCivInfo().notifications.remove(cityCanBombardNotification)
|
||||
}
|
||||
|
||||
return damageDealt + interceptDamage
|
||||
}
|
||||
|
||||
private fun triggerDefeatUniques(ourUnit: MapUnitCombatant, enemy: ICombatant, attackedTile: Tile){
|
||||
@ -340,7 +347,17 @@ object Battle {
|
||||
return true
|
||||
}
|
||||
|
||||
private data class DamageDealt(val attackerDealt: Int, val defenderDealt: Int)
|
||||
/** Holder for battle result - actual damage.
|
||||
* @param attackerDealt Damage done by attacker to defender
|
||||
* @param defenderDealt Damage done by defender to attacker
|
||||
*/
|
||||
data class DamageDealt(val attackerDealt: Int, val defenderDealt: Int) {
|
||||
operator fun plus(other: DamageDealt) =
|
||||
DamageDealt(attackerDealt + other.attackerDealt, defenderDealt + other.defenderDealt)
|
||||
companion object {
|
||||
val None = DamageDealt(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun takeDamage(attacker: ICombatant, defender: ICombatant): DamageDealt {
|
||||
var potentialDamageToDefender = BattleDamage.calculateDamageToDefender(attacker, defender)
|
||||
@ -1014,62 +1031,70 @@ object Battle {
|
||||
attacker.unit.action = null
|
||||
}
|
||||
|
||||
private fun tryInterceptAirAttack(attacker: MapUnitCombatant, attackedTile: Tile, interceptingCiv: Civilization, defender: ICombatant?) {
|
||||
private fun tryInterceptAirAttack(
|
||||
attacker: MapUnitCombatant,
|
||||
attackedTile: Tile,
|
||||
interceptingCiv: Civilization,
|
||||
defender: ICombatant?
|
||||
): DamageDealt {
|
||||
if (attacker.unit.hasUnique(UniqueType.CannotBeIntercepted, StateForConditionals(attacker.getCivInfo(), ourCombatant = attacker, theirCombatant = defender, attackedTile = attackedTile)))
|
||||
return
|
||||
return DamageDealt.None
|
||||
|
||||
// Pick highest chance interceptor
|
||||
for (interceptor in interceptingCiv.units.getCivUnits()
|
||||
val interceptor = interceptingCiv.units.getCivUnits()
|
||||
.filter { it.canIntercept(attackedTile) }
|
||||
.sortedByDescending { it.interceptChance() }
|
||||
) {
|
||||
// Can't intercept if we have a unique preventing it
|
||||
val conditionalState = StateForConditionals(interceptingCiv, ourCombatant = MapUnitCombatant(interceptor), theirCombatant = attacker, combatAction = CombatAction.Intercept, attackedTile = attackedTile)
|
||||
if (interceptor.getMatchingUniques(UniqueType.CannotInterceptUnits, conditionalState)
|
||||
.any { attacker.matchesCategory(it.params[0]) }
|
||||
) continue
|
||||
.firstOrNull { unit ->
|
||||
// Can't intercept if we have a unique preventing it
|
||||
val conditionalState = StateForConditionals(interceptingCiv, ourCombatant = MapUnitCombatant(unit), theirCombatant = attacker, combatAction = CombatAction.Intercept, attackedTile = attackedTile)
|
||||
unit.getMatchingUniques(UniqueType.CannotInterceptUnits, conditionalState)
|
||||
.none { attacker.matchesCategory(it.params[0]) }
|
||||
// Defender can't intercept either
|
||||
&& unit != (defender as? MapUnitCombatant)?.unit
|
||||
}
|
||||
?: return DamageDealt.None
|
||||
|
||||
// Defender can't intercept either
|
||||
if (defender != null && defender is MapUnitCombatant && interceptor == defender.unit) continue
|
||||
interceptor.attacksThisTurn++ // even if you miss, you took the shot
|
||||
// Does Intercept happen? If not, exit
|
||||
if (Random.Default.nextFloat() > interceptor.interceptChance() / 100f) return
|
||||
interceptor.attacksThisTurn++ // even if you miss, you took the shot
|
||||
// Does Intercept happen? If not, exit
|
||||
if (Random.Default.nextFloat() > interceptor.interceptChance() / 100f)
|
||||
return DamageDealt.None
|
||||
|
||||
var damage = BattleDamage.calculateDamageToDefender(
|
||||
MapUnitCombatant(interceptor),
|
||||
attacker
|
||||
)
|
||||
var damage = BattleDamage.calculateDamageToDefender(
|
||||
MapUnitCombatant(interceptor),
|
||||
attacker
|
||||
)
|
||||
|
||||
var damageFactor = 1f + interceptor.interceptDamagePercentBonus().toFloat() / 100f
|
||||
damageFactor *= attacker.unit.receivedInterceptDamageFactor()
|
||||
var damageFactor = 1f + interceptor.interceptDamagePercentBonus().toFloat() / 100f
|
||||
damageFactor *= attacker.unit.receivedInterceptDamageFactor()
|
||||
|
||||
damage = (damage.toFloat() * damageFactor).toInt()
|
||||
damage = (damage.toFloat() * damageFactor).toInt().coerceAtMost(attacker.unit.health)
|
||||
|
||||
attacker.takeDamage(damage)
|
||||
if (damage > 0)
|
||||
addXp(MapUnitCombatant(interceptor), 2, attacker)
|
||||
attacker.takeDamage(damage)
|
||||
if (damage > 0)
|
||||
addXp(MapUnitCombatant(interceptor), 2, attacker)
|
||||
|
||||
val attackerName = attacker.getName()
|
||||
val interceptorName = interceptor.name
|
||||
val locations = LocationAction(interceptor.currentTile.position, attacker.unit.currentTile.position)
|
||||
val attackerName = attacker.getName()
|
||||
val interceptorName = interceptor.name
|
||||
val locations = LocationAction(interceptor.currentTile.position, attacker.unit.currentTile.position)
|
||||
|
||||
val attackerText = if (!attacker.isDefeated())
|
||||
"Our [$attackerName] ([-$damage] HP) was attacked by an intercepting [$interceptorName] ([-0] HP)"
|
||||
else if (interceptor.getTile() in attacker.getCivInfo().viewableTiles)
|
||||
"Our [$attackerName] ([-$damage] HP) was destroyed by an intercepting [$interceptorName] ([-0] HP)"
|
||||
else "Our [$attackerName] ([-$damage] HP) was destroyed by an unknown interceptor"
|
||||
val attackerText = if (!attacker.isDefeated())
|
||||
"Our [$attackerName] ([-$damage] HP) was attacked by an intercepting [$interceptorName] ([-0] HP)"
|
||||
else if (interceptor.getTile() in attacker.getCivInfo().viewableTiles)
|
||||
"Our [$attackerName] ([-$damage] HP) was destroyed by an intercepting [$interceptorName] ([-0] HP)"
|
||||
else "Our [$attackerName] ([-$damage] HP) was destroyed by an unknown interceptor"
|
||||
|
||||
attacker.getCivInfo().addNotification(
|
||||
attackerText, interceptor.currentTile.position, NotificationCategory.War,
|
||||
attackerName, NotificationIcon.War, interceptorName
|
||||
)
|
||||
attacker.getCivInfo().addNotification(
|
||||
attackerText, interceptor.currentTile.position, NotificationCategory.War,
|
||||
attackerName, NotificationIcon.War, interceptorName
|
||||
)
|
||||
|
||||
val interceptorText = if (attacker.isDefeated())
|
||||
"Our [$interceptorName] ([-0] HP) intercepted and destroyed an enemy [$attackerName] ([-$damage] HP)"
|
||||
else "Our [$interceptorName] ([-0] HP) intercepted and attacked an enemy [$attackerName] ([-$damage] HP)"
|
||||
interceptingCiv.addNotification(interceptorText, locations, NotificationCategory.War,
|
||||
interceptorName, NotificationIcon.War, attackerName)
|
||||
return
|
||||
}
|
||||
val interceptorText = if (attacker.isDefeated())
|
||||
"Our [$interceptorName] ([-0] HP) intercepted and destroyed an enemy [$attackerName] ([-$damage] HP)"
|
||||
else "Our [$interceptorName] ([-0] HP) intercepted and attacked an enemy [$attackerName] ([-$damage] HP)"
|
||||
interceptingCiv.addNotification(interceptorText, locations, NotificationCategory.War,
|
||||
interceptorName, NotificationIcon.War, attackerName)
|
||||
|
||||
return DamageDealt(0, damage)
|
||||
}
|
||||
|
||||
private fun doWithdrawFromMeleeAbility(attacker: ICombatant, defender: ICombatant, baseWithdrawChance: Int): Boolean {
|
||||
|
@ -261,7 +261,7 @@ object BattleDamage {
|
||||
defender: ICombatant,
|
||||
tileToAttackFrom: Tile = defender.getTile(),
|
||||
/** Between 0 and 1. Defaults to turn and location-based random to avoid save scumming */
|
||||
randomnessFactor: Float = Random(attacker.getCivInfo().gameInfo.turns * attacker.getTile().position.hashCode().toLong()).nextFloat()
|
||||
randomnessFactor: Float = Random(defender.getCivInfo().gameInfo.turns * defender.getTile().position.hashCode().toLong()).nextFloat()
|
||||
,
|
||||
): Int {
|
||||
if (defender.isCivilian()) return 40
|
||||
|
Reference in New Issue
Block a user