mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-04 23:40:01 +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
|
||||
|
@ -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].
|
||||
*
|
||||
|
@ -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 ****** */
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user