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:
SomeTroglodyte
2023-05-14 20:52:15 +02:00
committed by GitHub
parent e6135fa486
commit ae13f32f72
6 changed files with 210 additions and 113 deletions

View File

@ -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 {

View File

@ -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

View File

@ -96,8 +96,8 @@ class UncivTooltip <T: Actor>(
state = TipState.Showing
container.addAction(Actions.sequence(
Actions.parallel(
Actions.fadeIn(UncivSlider.tipAnimationDuration, Interpolation.fade),
Actions.scaleTo(1f, 1f, 0.2f, Interpolation.fade)
Actions.fadeIn(tipAnimationDuration, Interpolation.fade),
Actions.scaleTo(1f, 1f, tipAnimationDuration, Interpolation.fade)
),
Actions.run { if (state == TipState.Showing) state = TipState.Shown }
))
@ -117,8 +117,8 @@ class UncivTooltip <T: Actor>(
state = TipState.Hiding
container.addAction(Actions.sequence(
Actions.parallel(
Actions.alpha(0.2f, 0.2f, Interpolation.fade),
Actions.scaleTo(0.05f, 0.05f, 0.2f, Interpolation.fade)
Actions.alpha(0.2f, tipAnimationDuration, Interpolation.fade),
Actions.scaleTo(0.05f, 0.05f, tipAnimationDuration, Interpolation.fade)
),
Actions.removeActor(),
Actions.run { if (state == TipState.Hiding) state = TipState.Hidden }
@ -164,6 +164,9 @@ class UncivTooltip <T: Actor>(
//endregion
companion object {
/** Duration of the fade/zoom-in/out animations */
const val tipAnimationDuration = 0.2f
/**
* Add a [Label]-based Tooltip with a rounded-corner background to a [Table] or other [Group].
*

View File

@ -53,6 +53,7 @@ import com.unciv.ui.components.tilegroups.WorldTileGroup
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.basescreen.UncivStage
import com.unciv.ui.screens.worldscreen.bottombar.BattleTableHelpers.battleAnimation
import com.unciv.utils.Log
import com.unciv.utils.Concurrency
import com.unciv.utils.launchOnGLThread
@ -229,6 +230,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
.getAttackableEnemies(unit, unit.movement.getDistanceToTiles())
.firstOrNull { it.tileToAttack == tile }
@ -237,7 +239,9 @@ class WorldMapHolder(
val attacker = MapUnitCombatant(unit)
if (!Battle.movePreparingAttack(attacker, attackableTile)) return
SoundPlayer.play(attacker.getAttackSound())
Battle.attackOrNuke(attacker, attackableTile)
val (damageToDefender, damageToAttacker) = Battle.attackOrNuke(attacker, attackableTile)
if (attackableTile.combatant != null)
worldScreen.battleAnimation(attacker, damageToAttacker, attackableTile.combatant, damageToDefender)
localShouldUpdate = true
} else if (unit.movement.canReach(tile)) {
/** ****** Right-click Move ****** */

View File

@ -201,28 +201,9 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
val maxDamageToDefender = BattleDamage.calculateDamageToDefender(attacker, defender, tileToAttackFrom, 1f)
val minDamageToDefender = BattleDamage.calculateDamageToDefender(attacker, defender, tileToAttackFrom, 0f)
var expectedDamageToDefenderForHealthbar = (maxDamageToDefender + minDamageToDefender) / 2
val maxDamageToAttacker = BattleDamage.calculateDamageToAttacker(attacker, defender, tileToAttackFrom, 1f)
val minDamageToAttacker = BattleDamage.calculateDamageToAttacker(attacker, defender, tileToAttackFrom, 0f)
var expectedDamageToAttackerForHealthbar = (maxDamageToAttacker + minDamageToAttacker) / 2
if (expectedDamageToAttackerForHealthbar > attacker.getHealth() && expectedDamageToDefenderForHealthbar > defender.getHealth()) {
// when damage exceeds health, we don't want to show negative health numbers
// Also if both parties are supposed to die it's not indicative of who is more likely to win
// So we "normalize" the damages until one dies
if (expectedDamageToDefenderForHealthbar * attacker.getHealth() > expectedDamageToAttackerForHealthbar * defender.getHealth()) { // defender dies quicker ie first
// Both damages *= (defender.health/damageToDefender)
expectedDamageToDefenderForHealthbar = defender.getHealth()
expectedDamageToAttackerForHealthbar *= (defender.getHealth() / expectedDamageToDefenderForHealthbar.toFloat()).toInt()
} else { // attacker dies first
// Both damages *= (attacker.health/damageToAttacker)
expectedDamageToAttackerForHealthbar = attacker.getHealth()
expectedDamageToDefenderForHealthbar *= (attacker.getHealth() / expectedDamageToAttackerForHealthbar.toFloat()).toInt()
}
}
else if (expectedDamageToAttackerForHealthbar > attacker.getHealth()) expectedDamageToAttackerForHealthbar = attacker.getHealth()
else if (expectedDamageToDefenderForHealthbar > defender.getHealth()) expectedDamageToDefenderForHealthbar = defender.getHealth()
if (attacker.isMelee() &&
(defender.isCivilian() || defender is CityCombatant && defender.isDefeated())) {
@ -281,7 +262,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
attackButton.label.color = Color.GRAY
} else {
attackButton.onClick(UncivSound.Silent) { // onAttackButtonClicked will do the sound
onAttackButtonClicked(attacker, defender, attackableTile, expectedDamageToAttackerForHealthbar, expectedDamageToDefenderForHealthbar)
onAttackButtonClicked(attacker, defender, attackableTile)
}
}
@ -295,9 +276,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
private fun onAttackButtonClicked(
attacker: ICombatant,
defender: ICombatant,
attackableTile: AttackableTile,
damageToAttacker: Int,
damageToDefender: Int
attackableTile: AttackableTile
) {
val canStillAttack = Battle.movePreparingAttack(attacker, attackableTile)
worldScreen.mapHolder.removeUnitActionOverlay() // the overlay was one of attacking
@ -309,7 +288,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
if (!canStillAttack) return
SoundPlayer.play(attacker.getAttackSound())
Battle.attackOrNuke(attacker, attackableTile)
val (damageToDefender, damageToAttacker) = Battle.attackOrNuke(attacker, attackableTile)
worldScreen.battleAnimation(attacker, damageToAttacker, defender, damageToDefender)
}

View File

@ -5,40 +5,68 @@ import com.badlogic.gdx.math.Interpolation
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Group
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.actions.Actions
import com.badlogic.gdx.scenes.scene2d.actions.FloatAction
import com.badlogic.gdx.scenes.scene2d.actions.RelativeTemporalAction
import com.badlogic.gdx.scenes.scene2d.actions.RepeatAction
import com.badlogic.gdx.scenes.scene2d.actions.SequenceAction
import com.badlogic.gdx.scenes.scene2d.actions.TemporalAction
import com.badlogic.gdx.scenes.scene2d.ui.Image
import com.badlogic.gdx.scenes.scene2d.ui.Stack
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup
import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame
import com.unciv.logic.battle.ICombatant
import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.map.HexMath
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.tilegroups.TileSetStrings
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.worldscreen.WorldScreen
object BattleTableHelpers {
class FlashRedAction(start:Float, end:Float, private val actorsToOriginalColors:Map<Actor, Color>) : FloatAction(start, end, 0.2f, Interpolation.sine){
object BattleTableHelpers {
/** Duration of the red-tint transition, used once per direction */
private const val flashRedDuration = 0.2f
/** Duration of the attacker displacement, used once per direction */
private const val moveActorsDuration = 0.3f
/** Max distance of the attacker displacement, in world coords */
private const val moveActorsDisplacement = 10f
/** If a mod provides attack animations (e.g. swinging a sword), they're played with this duration per frame */
private const val attackAnimationFrameDuration = 0.1f
/** Duration a damage number label is visible */
private const val damageLabelDuration = 1.2f
/** Size of a damage number label - currently fixed independednt of map zoom */
private const val damageLabelFontSize = 40
/** Total distance a damage number label is displaced upwards during that time in world coords */
private const val damageLabelDisplacement = 90f
class FlashRedAction(
start: Float, end: Float,
private val actorsToOriginalColors: Map<Actor, Color>
) : FloatAction(start, end, flashRedDuration, Interpolation.sine) {
private fun updateRedPercent(percent: Float) {
for ((actor, color) in actorsToOriginalColors)
actor.color = color.cpy().lerp(Color.RED, start+percent*(end-start))
actor.color = color.cpy().lerp(Color.RED, start + percent * (end - start))
}
override fun update(percent: Float) = updateRedPercent(percent)
}
class MoveActorsAction(private val actorsToMove:List<Actor>, private val movementVector: Vector2) : RelativeTemporalAction(){
class MoveActorsAction(
private val actorsToMove: List<Actor>,
private val movementVector: Vector2
) : RelativeTemporalAction() {
init {
duration = 0.3f
duration = moveActorsDuration
interpolation = Interpolation.sine
}
override fun updateRelative(percentDelta: Float) {
for (actor in actorsToMove){
for (actor in actorsToMove) {
actor.moveBy(movementVector.x * percentDelta, movementVector.y * percentDelta)
}
}
@ -46,21 +74,21 @@ object BattleTableHelpers {
class AttackAnimationAction(
val attacker: ICombatant,
val defenderActors: List<Actor>,
val currentTileSetStrings: TileSetStrings
): SequenceAction(){
private val attacker: ICombatant,
defenderActors: List<Actor>,
private val currentTileSetStrings: TileSetStrings
): SequenceAction() {
init {
if (defenderActors.any()) {
val attackAnimationLocation = getAttackAnimationLocation()
if (attackAnimationLocation != null){
if (attackAnimationLocation != null) {
var i = 1
while (ImageGetter.imageExists(attackAnimationLocation+i)){
val image = ImageGetter.getImage(attackAnimationLocation+i)
while (ImageGetter.imageExists(attackAnimationLocation + i)){
val image = ImageGetter.getImage(attackAnimationLocation + i)
addAction(Actions.run {
defenderActors.first().parent.addActor(image)
})
addAction(Actions.delay(0.1f))
addAction(Actions.delay(attackAnimationFrameDuration))
addAction(Actions.removeActor(image))
i++
}
@ -68,25 +96,55 @@ object BattleTableHelpers {
}
}
private fun getAttackAnimationLocation(): String?{
private fun getAttackAnimationLocation(): String? {
fun TileSetStrings.getLocation(name: String) = getString(unitsLocation, name, "-attack-")
if (attacker is MapUnitCombatant) {
val unitSpecificAttackAnimationLocation =
currentTileSetStrings.getString(
currentTileSetStrings.unitsLocation,
attacker.getUnitType().name,
"-attack-"
)
if (ImageGetter.imageExists(unitSpecificAttackAnimationLocation+"1")) return unitSpecificAttackAnimationLocation
val unitSpecificAttackAnimationLocation = currentTileSetStrings.getLocation(attacker.getName())
if (ImageGetter.imageExists(unitSpecificAttackAnimationLocation + "1"))
return unitSpecificAttackAnimationLocation
}
val unitTypeAttackAnimationLocation =
currentTileSetStrings.getString(currentTileSetStrings.unitsLocation, attacker.getUnitType().name, "-attack-")
if (ImageGetter.imageExists(unitTypeAttackAnimationLocation+"1")) return unitTypeAttackAnimationLocation
val unitTypeAttackAnimationLocation = currentTileSetStrings.getLocation(attacker.getUnitType().name)
if (ImageGetter.imageExists(unitTypeAttackAnimationLocation + "1"))
return unitTypeAttackAnimationLocation
return null
}
}
/** The animation for the Damage labels */
private class DamageLabelAnimation(actor: WidgetGroup) : TemporalAction(damageLabelDuration) {
val startX = actor.x
val startY = actor.y
/* A tested version with smooth scale-out in addition to the alpha fade
val width = actor.width
val height = actor.height
init {
actor.isTransform = true
}
override fun update(percent: Float) {
actor.color.a = Interpolation.fade.apply(1f - percent)
val scale = Interpolation.smooth.apply(1f - percent)
actor.setScale(scale)
val x = startX + (1f - scale) * width / 2
val y = startY + (1f - scale) * height / 2 +
Interpolation.smooth.apply(percent) * damageLabelDisplacement
actor.setPosition(x, y)
}
*/
override fun update(percent: Float) {
actor.color.a = Interpolation.fade.apply(1f - percent)
actor.setPosition(startX, startY + percent * damageLabelDisplacement)
}
override fun end() {
actor.remove()
}
}
fun WorldScreen.battleAnimation(
attacker: ICombatant, damageToAttacker: Int,
defender: ICombatant, damageToDefender: Int
@ -97,8 +155,7 @@ object BattleTableHelpers {
if (combatant.isCity()) {
val icon = tileGroup.layerMisc.improvementIcon
if (icon != null) yield (icon)
}
else {
} else if (!combatant.isAirUnit()) {
val slot = if (combatant.isCivilian()) 0 else 1
yieldAll((tileGroup.layerUnitArt.getChild(slot) as Group).children)
}
@ -108,18 +165,28 @@ object BattleTableHelpers {
sequence {
if (damageToDefender != 0) yieldAll(getMapActorsForCombatant(defender))
if (damageToAttacker != 0) yieldAll(getMapActorsForCombatant(attacker))
}.mapTo(arrayListOf()) { it to it.color.cpy() }.toMap()
}.associateWith { it.color.cpy() }
val actorsToMove = getMapActorsForCombatant(attacker).toList()
val attackVectorHexCoords = defender.getTile().position.cpy().sub(attacker.getTile().position)
val attackVectorWorldCoords = HexMath.hex2WorldCoords(attackVectorHexCoords)
.nor() // normalize vector to length of "1"
.scl(10f) // we want 10 pixel movement
.scl(moveActorsDisplacement)
val attackerGroup = mapHolder.tileGroups[attacker.getTile()]!!
val defenderGroup = mapHolder.tileGroups[defender.getTile()]!!
val hideDefenderDamage = defender.isDefeated() &&
attacker.getTile().position == defender.getTile().position
stage.addAction(
Actions.sequence(
MoveActorsAction(actorsToMove, attackVectorWorldCoords),
Actions.run {
createDamageLabel(damageToAttacker, attackerGroup)
if (!hideDefenderDamage)
createDamageLabel(damageToDefender, defenderGroup)
},
Actions.parallel( // While the unit is moving back to its normal position, we flash the damages on both units
MoveActorsAction(actorsToMove, attackVectorWorldCoords.cpy().scl(-1f)),
AttackAnimationAction(attacker,
@ -137,8 +204,27 @@ object BattleTableHelpers {
)
)
))
}
private fun createDamageLabel(damage: Int, target: Actor) {
if (damage == 0) return
val label = (-damage).toString().toLabel(Color.RED, damageLabelFontSize, Align.topLeft, true)
label.touchable = Touchable.disabled
val shadow = (-damage).toString().toLabel(Color.BLACK, damageLabelFontSize, Align.bottomRight, true)
shadow.touchable = Touchable.disabled
val container = Stack(shadow, label)
container.touchable = Touchable.disabled
container.pack()
// The +1f is what displaces the shadow under the red label
container.setSize(container.width + 1f, container.height + 1f)
val targetRight = target.run { localToStageCoordinates(Vector2(width, height * 0.5f)) }
container.setPosition(targetRight.x, targetRight.y, Align.center)
target.stage.addActor(container)
container.addAction(DamageLabelAnimation(container))
}
fun getHealthBar(maxHealth: Int, currentHealth: Int, maxRemainingHealth: Int, minRemainingHealth: Int): Table {