mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-26 13:27:22 -04:00
Fix Right-Click attacks made no sound (#6906)
* Fix Right-Click attacks made no sound * Fix Right-Click attacks made no sound - no UI in logic * Fix Right-Click attacks made no sound - comments * Fix Right-Click attacks made no sound - comments
This commit is contained in:
parent
e927ef6f64
commit
0461d9d7fd
@ -94,7 +94,7 @@ object BattleHelper {
|
|||||||
fun containsAttackableEnemy(tile: TileInfo, combatant: ICombatant): Boolean {
|
fun containsAttackableEnemy(tile: TileInfo, combatant: ICombatant): Boolean {
|
||||||
if (combatant is MapUnitCombatant && combatant.unit.isEmbarked() && !combatant.hasUnique(UniqueType.AttackOnSea)) {
|
if (combatant is MapUnitCombatant && combatant.unit.isEmbarked() && !combatant.hasUnique(UniqueType.AttackOnSea)) {
|
||||||
// Can't attack water units while embarked, only land
|
// Can't attack water units while embarked, only land
|
||||||
if (tile.isWater || combatant.isRanged())
|
if (tile.isWater || combatant.isRanged())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ object BattleHelper {
|
|||||||
if (tileCombatant.getCivInfo() == combatant.getCivInfo()) return false
|
if (tileCombatant.getCivInfo() == combatant.getCivInfo()) return false
|
||||||
if (!combatant.getCivInfo().isAtWarWith(tileCombatant.getCivInfo())) return false
|
if (!combatant.getCivInfo().isAtWarWith(tileCombatant.getCivInfo())) return false
|
||||||
|
|
||||||
if (combatant is MapUnitCombatant &&
|
if (combatant is MapUnitCombatant &&
|
||||||
combatant.unit.hasUnique(UniqueType.CanOnlyAttackUnits) &&
|
combatant.unit.hasUnique(UniqueType.CanOnlyAttackUnits) &&
|
||||||
combatant.unit.getMatchingUniques(UniqueType.CanOnlyAttackUnits).none { tileCombatant.matchesCategory(it.params[0]) }
|
combatant.unit.getMatchingUniques(UniqueType.CanOnlyAttackUnits).none { tileCombatant.matchesCategory(it.params[0]) }
|
||||||
)
|
)
|
||||||
@ -117,7 +117,7 @@ object BattleHelper {
|
|||||||
// Only units with the right unique can view submarines (or other invisible units) from more then one tile away.
|
// 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.
|
// Garrisoned invisible units can be attacked by anyone, as else the city will be in invincible.
|
||||||
if (tileCombatant.isInvisible(combatant.getCivInfo()) && !tile.isCityCenter()) {
|
if (tileCombatant.isInvisible(combatant.getCivInfo()) && !tile.isCityCenter()) {
|
||||||
return combatant is MapUnitCombatant
|
return combatant is MapUnitCombatant
|
||||||
&& combatant.getCivInfo().viewableInvisibleUnitsTiles.map { it.position }.contains(tile.position)
|
&& combatant.getCivInfo().viewableInvisibleUnitsTiles.map { it.position }.contains(tile.position)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@ -169,4 +169,4 @@ object BattleHelper {
|
|||||||
|
|
||||||
return enemyTileToAttack
|
return enemyTileToAttack
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,30 +27,51 @@ import kotlin.math.min
|
|||||||
*/
|
*/
|
||||||
object Battle {
|
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!
|
||||||
|
*/
|
||||||
fun moveAndAttack(attacker: ICombatant, attackableTile: AttackableTile) {
|
fun moveAndAttack(attacker: ICombatant, attackableTile: AttackableTile) {
|
||||||
if (attacker is MapUnitCombatant) {
|
if (!movePreparingAttack(attacker, attackableTile)) return
|
||||||
attacker.unit.movement.moveToTile(attackableTile.tileToAttackFrom)
|
attackOrNuke(attacker, attackableTile)
|
||||||
/**
|
}
|
||||||
* When calculating movement distance, we assume that a hidden tile is 1 movement point,
|
|
||||||
* which can lead to EXCEEDINGLY RARE edge cases where you think
|
|
||||||
* that you can attack a tile by passing through a HIDDEN TILE,
|
|
||||||
* but the hidden tile is actually IMPASSIBLE so you stop halfway!
|
|
||||||
*/
|
|
||||||
if (attacker.getTile() != attackableTile.tileToAttackFrom) return
|
|
||||||
/** Alternatively, maybe we DID reach that tile, but it turned out to be a hill or something,
|
|
||||||
* so we expended all of our movement points!
|
|
||||||
*/
|
|
||||||
if (attacker.unit.currentMovement == 0f)
|
|
||||||
return
|
|
||||||
if (attacker.hasUnique(UniqueType.MustSetUp) && !attacker.unit.isSetUpForSiege()) {
|
|
||||||
attacker.unit.action = UnitActionType.SetUp.value
|
|
||||||
attacker.unit.useMovementPoints(1f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves [attacker] to [attackableTile], handles siege setup and returns `true` if an attack is still possible.
|
||||||
|
*
|
||||||
|
* 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 {
|
||||||
|
if (attacker !is MapUnitCombatant) return true
|
||||||
|
attacker.unit.movement.moveToTile(attackableTile.tileToAttackFrom)
|
||||||
|
/**
|
||||||
|
* When calculating movement distance, we assume that a hidden tile is 1 movement point,
|
||||||
|
* which can lead to EXCEEDINGLY RARE edge cases where you think
|
||||||
|
* that you can attack a tile by passing through a HIDDEN TILE,
|
||||||
|
* but the hidden tile is actually IMPASSIBLE so you stop halfway!
|
||||||
|
*/
|
||||||
|
if (attacker.getTile() != attackableTile.tileToAttackFrom) return false
|
||||||
|
/** Alternatively, maybe we DID reach that tile, but it turned out to be a hill or something,
|
||||||
|
* so we expended all of our movement points!
|
||||||
|
*/
|
||||||
|
if (attacker.hasUnique(UniqueType.MustSetUp)
|
||||||
|
&& !attacker.unit.isSetUpForSiege()
|
||||||
|
&& attacker.unit.currentMovement > 0f
|
||||||
|
) {
|
||||||
|
attacker.unit.action = UnitActionType.SetUp.value
|
||||||
|
attacker.unit.useMovementPoints(1f)
|
||||||
|
}
|
||||||
|
return (attacker.unit.currentMovement > 0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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())
|
if (attacker is MapUnitCombatant && attacker.unit.baseUnit.isNuclearWeapon())
|
||||||
return NUKE(attacker, attackableTile.tileToAttack)
|
NUKE(attacker, attackableTile.tileToAttack)
|
||||||
attack(attacker, getMapCombatantOfTile(attackableTile.tileToAttack)!!)
|
else
|
||||||
|
attack(attacker, getMapCombatantOfTile(attackableTile.tileToAttack)!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun attack(attacker: ICombatant, defender: ICombatant) {
|
fun attack(attacker: ICombatant, defender: ICombatant) {
|
||||||
@ -98,9 +119,9 @@ object Battle {
|
|||||||
|
|
||||||
if (defender.getCivInfo().isBarbarian() && attackedTile.improvement == Constants.barbarianEncampment)
|
if (defender.getCivInfo().isBarbarian() && attackedTile.improvement == Constants.barbarianEncampment)
|
||||||
defender.getCivInfo().gameInfo.barbarians.campAttacked(attackedTile.position)
|
defender.getCivInfo().gameInfo.barbarians.campAttacked(attackedTile.position)
|
||||||
|
|
||||||
postBattleNationUniques(defender, attackedTile, attacker)
|
postBattleNationUniques(defender, attackedTile, attacker)
|
||||||
|
|
||||||
// This needs to come BEFORE the move-to-tile, because if we haven't conquered it we can't move there =)
|
// This needs to come BEFORE the move-to-tile, because if we haven't conquered it we can't move there =)
|
||||||
if (defender.isDefeated() && defender is CityCombatant && attacker is MapUnitCombatant
|
if (defender.isDefeated() && defender is CityCombatant && attacker is MapUnitCombatant
|
||||||
&& attacker.isMelee() && !attacker.unit.hasUnique(UniqueType.CannotCaptureCities)) {
|
&& attacker.isMelee() && !attacker.unit.hasUnique(UniqueType.CannotCaptureCities)) {
|
||||||
@ -316,7 +337,7 @@ object Battle {
|
|||||||
attacker.unit.healBy(amountToHeal)
|
attacker.unit.healBy(amountToHeal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Places a [unitName] unit near [tile] after being attacked by [attacker].
|
/** Places a [unitName] unit near [tile] after being attacked by [attacker].
|
||||||
* Adds a notification to [attacker]'s civInfo and returns whether the captured unit could be placed */
|
* Adds a notification to [attacker]'s civInfo and returns whether the captured unit could be placed */
|
||||||
private fun spawnCapturedUnit(unitName: String, attacker: ICombatant, tile: TileInfo, notification: String): Boolean {
|
private fun spawnCapturedUnit(unitName: String, attacker: ICombatant, tile: TileInfo, notification: String): Boolean {
|
||||||
@ -332,19 +353,19 @@ object Battle {
|
|||||||
|
|
||||||
private fun postBattleNationUniques(defender: ICombatant, attackedTile: TileInfo, attacker: ICombatant) {
|
private fun postBattleNationUniques(defender: ICombatant, attackedTile: TileInfo, attacker: ICombatant) {
|
||||||
if (!defender.isDefeated()) return
|
if (!defender.isDefeated()) return
|
||||||
|
|
||||||
// Barbarians reduce spawn countdown after their camp was attacked "kicking the hornet's nest"
|
// Barbarians reduce spawn countdown after their camp was attacked "kicking the hornet's nest"
|
||||||
if (defender.getCivInfo().isBarbarian() && attackedTile.improvement == Constants.barbarianEncampment) {
|
if (defender.getCivInfo().isBarbarian() && attackedTile.improvement == Constants.barbarianEncampment) {
|
||||||
var unitPlaced = false
|
var unitPlaced = false
|
||||||
// German unique - needs to be checked before we try to move to the enemy tile, since the encampment disappears after we move in
|
// German unique - needs to be checked before we try to move to the enemy tile, since the encampment disappears after we move in
|
||||||
// Deprecated as of 4.0.3
|
// Deprecated as of 4.0.3
|
||||||
if (attacker.getCivInfo().hasUnique(UniqueType.ChanceToRecruitBarbarianFromEncampment)
|
if (attacker.getCivInfo().hasUnique(UniqueType.ChanceToRecruitBarbarianFromEncampment)
|
||||||
&& Random().nextDouble() < 0.67
|
&& Random().nextDouble() < 0.67
|
||||||
) {
|
) {
|
||||||
attacker.getCivInfo().addGold(25)
|
attacker.getCivInfo().addGold(25)
|
||||||
unitPlaced = spawnCapturedUnit(defender.getName(), attacker, attackedTile,"A barbarian [${defender.getName()}] has joined us!")
|
unitPlaced = spawnCapturedUnit(defender.getName(), attacker, attackedTile,"A barbarian [${defender.getName()}] has joined us!")
|
||||||
}
|
}
|
||||||
|
|
||||||
// New version of unique
|
// New version of unique
|
||||||
//
|
//
|
||||||
for (unique in attacker.getCivInfo().getMatchingUniques(UniqueType.GainFromEncampment)) {
|
for (unique in attacker.getCivInfo().getMatchingUniques(UniqueType.GainFromEncampment)) {
|
||||||
@ -353,7 +374,7 @@ object Battle {
|
|||||||
unitPlaced = spawnCapturedUnit(defender.getName(), attacker, attackedTile,"A barbarian [${defender.getName()}] has joined us!")
|
unitPlaced = spawnCapturedUnit(defender.getName(), attacker, attackedTile,"A barbarian [${defender.getName()}] has joined us!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Similarly, Ottoman unique
|
// Similarly, Ottoman unique
|
||||||
// Deprecated as of 4.0.3
|
// Deprecated as of 4.0.3
|
||||||
if (attacker.getCivInfo().hasUnique(UniqueType.ChanceToRecruitNavalBarbarian)
|
if (attacker.getCivInfo().hasUnique(UniqueType.ChanceToRecruitNavalBarbarian)
|
||||||
@ -449,7 +470,7 @@ object Battle {
|
|||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val stateForConditionals = StateForConditionals(civInfo = thisCombatant.getCivInfo(), ourCombatant = thisCombatant, theirCombatant = otherCombatant)
|
val stateForConditionals = StateForConditionals(civInfo = thisCombatant.getCivInfo(), ourCombatant = thisCombatant, theirCombatant = otherCombatant)
|
||||||
|
|
||||||
for (unique in thisCombatant.getMatchingUniques(UniqueType.FlatXPGain, stateForConditionals, true))
|
for (unique in thisCombatant.getMatchingUniques(UniqueType.FlatXPGain, stateForConditionals, true))
|
||||||
@ -459,7 +480,7 @@ object Battle {
|
|||||||
|
|
||||||
for (unique in thisCombatant.getMatchingUniques(UniqueType.PercentageXPGain, stateForConditionals, true))
|
for (unique in thisCombatant.getMatchingUniques(UniqueType.PercentageXPGain, stateForConditionals, true))
|
||||||
xpModifier += unique.params[0].toFloat() / 100
|
xpModifier += unique.params[0].toFloat() / 100
|
||||||
|
|
||||||
val xpGained = (baseXP * xpModifier).toInt()
|
val xpGained = (baseXP * xpModifier).toInt()
|
||||||
thisCombatant.unit.promotions.XP += xpGained
|
thisCombatant.unit.promotions.XP += xpGained
|
||||||
|
|
||||||
@ -481,8 +502,8 @@ object Battle {
|
|||||||
|
|
||||||
private fun conquerCity(city: CityInfo, attacker: MapUnitCombatant) {
|
private fun conquerCity(city: CityInfo, attacker: MapUnitCombatant) {
|
||||||
val attackerCiv = attacker.getCivInfo()
|
val attackerCiv = attacker.getCivInfo()
|
||||||
|
|
||||||
|
|
||||||
attackerCiv.addNotification("We have conquered the city of [${city.name}]!", city.location, NotificationIcon.War)
|
attackerCiv.addNotification("We have conquered the city of [${city.name}]!", city.location, NotificationIcon.War)
|
||||||
|
|
||||||
city.hasJustBeenConquered = true
|
city.hasJustBeenConquered = true
|
||||||
@ -610,13 +631,13 @@ object Battle {
|
|||||||
attacker.popupAlerts.add(PopupAlert(AlertType.Defeated, attackedCiv.civName))
|
attacker.popupAlerts.add(PopupAlert(AlertType.Defeated, attackedCiv.civName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mayUseNuke(nuke: MapUnitCombatant, targetTile: TileInfo): Boolean {
|
fun mayUseNuke(nuke: MapUnitCombatant, targetTile: TileInfo): Boolean {
|
||||||
val blastRadius =
|
val blastRadius =
|
||||||
if (!nuke.hasUnique(UniqueType.BlastRadius)) 2
|
if (!nuke.hasUnique(UniqueType.BlastRadius)) 2
|
||||||
// Don't check conditionals as these are not supported
|
// Don't check conditionals as these are not supported
|
||||||
else nuke.unit.getMatchingUniques(UniqueType.BlastRadius).first().params[0].toInt()
|
else nuke.unit.getMatchingUniques(UniqueType.BlastRadius).first().params[0].toInt()
|
||||||
|
|
||||||
var canNuke = true
|
var canNuke = true
|
||||||
val attackerCiv = nuke.getCivInfo()
|
val attackerCiv = nuke.getCivInfo()
|
||||||
for (tile in targetTile.getTilesInDistance(blastRadius)) {
|
for (tile in targetTile.getTilesInDistance(blastRadius)) {
|
||||||
@ -761,7 +782,7 @@ object Battle {
|
|||||||
tile.addTerrainFeature("Fallout")
|
tile.addTerrainFeature("Fallout")
|
||||||
}
|
}
|
||||||
if (!tile.terrainHasUnique(UniqueType.DestroyableByNukes)) return
|
if (!tile.terrainHasUnique(UniqueType.DestroyableByNukes)) return
|
||||||
|
|
||||||
// Deprecated as of 3.19.19 -- If removed, the two successive `if`s above should be merged
|
// Deprecated as of 3.19.19 -- If removed, the two successive `if`s above should be merged
|
||||||
val destructionChance = if (tile.terrainHasUnique(UniqueType.ResistsNukes)) 0.25f
|
val destructionChance = if (tile.terrainHasUnique(UniqueType.ResistsNukes)) 0.25f
|
||||||
else 0.5f
|
else 0.5f
|
||||||
@ -797,13 +818,13 @@ object Battle {
|
|||||||
targetedCity.population.addPopulation(-populationLoss.toInt())
|
targetedCity.population.addPopulation(-populationLoss.toInt())
|
||||||
if (targetedCity.population.population < 1) targetedCity.population.setPopulation(1)
|
if (targetedCity.population.population < 1) targetedCity.population.setPopulation(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryInterceptAirAttack(attacker: MapUnitCombatant, attackedTile: TileInfo, interceptingCiv: CivilizationInfo, defender: ICombatant?) {
|
private fun tryInterceptAirAttack(attacker: MapUnitCombatant, attackedTile: TileInfo, interceptingCiv: CivilizationInfo, defender: ICombatant?) {
|
||||||
if (attacker.unit.hasUnique(UniqueType.CannotBeIntercepted)) return
|
if (attacker.unit.hasUnique(UniqueType.CannotBeIntercepted)) return
|
||||||
// Pick highest chance interceptor
|
// Pick highest chance interceptor
|
||||||
for (interceptor in interceptingCiv.getCivUnits()
|
for (interceptor in interceptingCiv.getCivUnits()
|
||||||
.filter { it.canIntercept(attackedTile) }
|
.filter { it.canIntercept(attackedTile) }
|
||||||
.sortedByDescending { it.interceptChance() }) {
|
.sortedByDescending { it.interceptChance() }) {
|
||||||
// defender can't also intercept
|
// defender can't also intercept
|
||||||
if (defender != null && defender is MapUnitCombatant && interceptor == defender.unit) continue
|
if (defender != null && defender is MapUnitCombatant && interceptor == defender.unit) continue
|
||||||
// Does Intercept happen? If not, exit
|
// Does Intercept happen? If not, exit
|
||||||
|
@ -12,7 +12,7 @@ import java.io.File
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Problems on Android
|
* Problems on Android
|
||||||
*
|
*
|
||||||
* Essentially the freshly created Gdx Sound object from newSound() is not immediately usable, it
|
* Essentially the freshly created Gdx Sound object from newSound() is not immediately usable, it
|
||||||
* needs some preparation time - buffering, decoding, whatever. Calling play() immediately will result
|
* needs some preparation time - buffering, decoding, whatever. Calling play() immediately will result
|
||||||
* in no sound, a logcat warning (not ready), and nothing else - specifically no exceptions. Also,
|
* in no sound, a logcat warning (not ready), and nothing else - specifically no exceptions. Also,
|
||||||
@ -20,14 +20,14 @@ import java.io.File
|
|||||||
* (resource failed to clean up). Also, Gdx will attempt fast track, which will cause logcat entries,
|
* (resource failed to clean up). Also, Gdx will attempt fast track, which will cause logcat entries,
|
||||||
* and these will be warnings if the sound file's sample rate (not bitrate) does not match the device's
|
* and these will be warnings if the sound file's sample rate (not bitrate) does not match the device's
|
||||||
* hardware preferred bitrate. On a Xiaomi Mi8 that is 48kHz, not 44.1kHz. Channel count also must match.
|
* hardware preferred bitrate. On a Xiaomi Mi8 that is 48kHz, not 44.1kHz. Channel count also must match.
|
||||||
*
|
*
|
||||||
* @see "https://github.com/libgdx/libgdx/issues/1775"
|
* @see "https://github.com/libgdx/libgdx/issues/1775"
|
||||||
* logcat entry "W/System: A resource failed to call end.":
|
* logcat entry "W/System: A resource failed to call end.":
|
||||||
* unavoidable as long as we cache Gdx Sound objects loaded from assets
|
* unavoidable as long as we cache Gdx Sound objects loaded from assets
|
||||||
* logcat entry "W/SoundPool: sample X not READY":
|
* logcat entry "W/SoundPool: sample X not READY":
|
||||||
* could be avoided by preloading the 'cache' or otherwise ensuring a minimum delay between
|
* could be avoided by preloading the 'cache' or otherwise ensuring a minimum delay between
|
||||||
* newSound() and play() - there's no test function that does not trigger logcat warnings.
|
* newSound() and play() - there's no test function that does not trigger logcat warnings.
|
||||||
*
|
*
|
||||||
* Current approach: Cache on demand as before, catch stream not ready and retry. This maximizes
|
* Current approach: Cache on demand as before, catch stream not ready and retry. This maximizes
|
||||||
* logcat messages but user experience is acceptable. Empiric delay needed was measured a 40ms
|
* logcat messages but user experience is acceptable. Empiric delay needed was measured a 40ms
|
||||||
* so that is the minimum wait before attempting play when we know the sound is freshly cached
|
* so that is the minimum wait before attempting play when we know the sound is freshly cached
|
||||||
@ -47,11 +47,11 @@ object Sounds {
|
|||||||
|
|
||||||
@Suppress("EnumEntryName")
|
@Suppress("EnumEntryName")
|
||||||
private enum class SupportedExtensions { mp3, ogg, wav } // Per Gdx docs, no aac/m4a
|
private enum class SupportedExtensions { mp3, ogg, wav } // Per Gdx docs, no aac/m4a
|
||||||
|
|
||||||
private val soundMap = HashMap<UncivSound, Sound?>()
|
private val soundMap = HashMap<UncivSound, Sound?>()
|
||||||
|
|
||||||
private val separator = File.separator // just a shorthand for readability
|
private val separator = File.separator // just a shorthand for readability
|
||||||
|
|
||||||
private var modListHash = Int.MIN_VALUE
|
private var modListHash = Int.MIN_VALUE
|
||||||
|
|
||||||
/** Ensure cache is not outdated */
|
/** Ensure cache is not outdated */
|
||||||
@ -101,7 +101,7 @@ object Sounds {
|
|||||||
return modList.asSequence()
|
return modList.asSequence()
|
||||||
.map { "mods$separator$it$separator" } +
|
.map { "mods$separator$it$separator" } +
|
||||||
sequenceOf("")
|
sequenceOf("")
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Holds a Gdx Sound and a flag indicating the sound is freshly loaded and not from cache */
|
/** Holds a Gdx Sound and a flag indicating the sound is freshly loaded and not from cache */
|
||||||
private data class GetSoundResult(val resource: Sound, val isFresh: Boolean)
|
private data class GetSoundResult(val resource: Sound, val isFresh: Boolean)
|
||||||
@ -113,7 +113,7 @@ object Sounds {
|
|||||||
private fun get(sound: UncivSound): GetSoundResult? {
|
private fun get(sound: UncivSound): GetSoundResult? {
|
||||||
checkCache()
|
checkCache()
|
||||||
// Look for cached sound
|
// Look for cached sound
|
||||||
if (sound in soundMap)
|
if (sound in soundMap)
|
||||||
return if(soundMap[sound] == null) null
|
return if(soundMap[sound] == null) null
|
||||||
else GetSoundResult(soundMap[sound]!!, false)
|
else GetSoundResult(soundMap[sound]!!, false)
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ object Sounds {
|
|||||||
// This is essentially a cross join. To operate on all combinations, we pack both lambda
|
// This is essentially a cross join. To operate on all combinations, we pack both lambda
|
||||||
// parameters into a Pair (using `to`) and unwrap that in the loop using automatic data
|
// parameters into a Pair (using `to`) and unwrap that in the loop using automatic data
|
||||||
// class deconstruction `(,)`. All this avoids a double break when a match is found.
|
// class deconstruction `(,)`. All this avoids a double break when a match is found.
|
||||||
folder -> SupportedExtensions.values().asSequence().map { folder to it }
|
folder -> SupportedExtensions.values().asSequence().map { folder to it }
|
||||||
} ) {
|
} ) {
|
||||||
val path = "${modFolder}sounds$separator$fileName.${extension.name}"
|
val path = "${modFolder}sounds$separator$fileName.${extension.name}"
|
||||||
file = Gdx.files.local(path)
|
file = Gdx.files.local(path)
|
||||||
@ -152,10 +152,10 @@ object Sounds {
|
|||||||
*
|
*
|
||||||
* Sources are mods from a loaded game, then mods marked as permanent audiovisual,
|
* Sources are mods from a loaded game, then mods marked as permanent audiovisual,
|
||||||
* and lastly Unciv's own assets/sounds. Will fail silently if the sound file cannot be found.
|
* and lastly Unciv's own assets/sounds. Will fail silently if the sound file cannot be found.
|
||||||
*
|
*
|
||||||
* This will wait for the Stream to become ready (Android issue) if necessary, and do so on a
|
* This will wait for the Stream to become ready (Android issue) if necessary, and do so on a
|
||||||
* separate thread. No new thread is created if the sound can be played immediately.
|
* separate thread. No new thread is created if the sound can be played immediately.
|
||||||
*
|
*
|
||||||
* @param sound The sound to play
|
* @param sound The sound to play
|
||||||
*/
|
*/
|
||||||
fun play(sound: UncivSound) {
|
fun play(sound: UncivSound) {
|
||||||
|
@ -116,29 +116,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
|
|||||||
val unit = worldScreen.bottomUnitTable.selectedUnit
|
val unit = worldScreen.bottomUnitTable.selectedUnit
|
||||||
?: return
|
?: return
|
||||||
launchCrashHandling("WorldScreenClick") {
|
launchCrashHandling("WorldScreenClick") {
|
||||||
val tile = tileGroup.tileInfo
|
onTileRightClicked(unit, tileGroup.tileInfo)
|
||||||
|
|
||||||
if (worldScreen.bottomUnitTable.selectedUnitIsSwapping) {
|
|
||||||
if (unit.movement.canUnitSwapTo(tile)) {
|
|
||||||
swapMoveUnitToTargetTile(unit, tile)
|
|
||||||
}
|
|
||||||
// If we are in unit-swapping mode, we don't want to move or attack
|
|
||||||
return@launchCrashHandling
|
|
||||||
}
|
|
||||||
|
|
||||||
val attackableTile = BattleHelper.getAttackableEnemies(unit, unit.movement.getDistanceToTiles())
|
|
||||||
.firstOrNull { it.tileToAttack == tileGroup.tileInfo }
|
|
||||||
if (unit.canAttack() && attackableTile != null) {
|
|
||||||
Battle.moveAndAttack(MapUnitCombatant(unit), attackableTile)
|
|
||||||
worldScreen.shouldUpdate = true
|
|
||||||
return@launchCrashHandling
|
|
||||||
}
|
|
||||||
|
|
||||||
val canUnitReachTile = unit.movement.canReach(tile)
|
|
||||||
if (canUnitReachTile) {
|
|
||||||
moveUnitToTargetTile(listOf(unit), tile)
|
|
||||||
return@launchCrashHandling
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -202,6 +180,32 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
|
|||||||
worldScreen.shouldUpdate = true
|
worldScreen.shouldUpdate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onTileRightClicked(unit: MapUnit, tile: TileInfo) {
|
||||||
|
if (worldScreen.bottomUnitTable.selectedUnitIsSwapping) {
|
||||||
|
if (unit.movement.canUnitSwapTo(tile)) {
|
||||||
|
swapMoveUnitToTargetTile(unit, tile)
|
||||||
|
}
|
||||||
|
// If we are in unit-swapping mode, we don't want to move or attack
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val attackableTile = BattleHelper.getAttackableEnemies(unit, unit.movement.getDistanceToTiles())
|
||||||
|
.firstOrNull { it.tileToAttack == tile }
|
||||||
|
if (unit.canAttack() && attackableTile != null) {
|
||||||
|
worldScreen.shouldUpdate = true
|
||||||
|
val attacker = MapUnitCombatant(unit)
|
||||||
|
if (!Battle.movePreparingAttack(attacker, attackableTile)) return
|
||||||
|
Sounds.play(attacker.getAttackSound())
|
||||||
|
Battle.attackOrNuke(attacker, attackableTile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val canUnitReachTile = unit.movement.canReach(tile)
|
||||||
|
if (canUnitReachTile) {
|
||||||
|
moveUnitToTargetTile(listOf(unit), tile)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun moveUnitToTargetTile(selectedUnits: List<MapUnit>, targetTile: TileInfo) {
|
private fun moveUnitToTargetTile(selectedUnits: List<MapUnit>, targetTile: TileInfo) {
|
||||||
// this can take a long time, because of the unit-to-tile calculation needed, so we put it in a different thread
|
// this can take a long time, because of the unit-to-tile calculation needed, so we put it in a different thread
|
||||||
@ -550,7 +554,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Same as below - randomly, tileGroups doesn't seem to contain the selected tile, and this doesn't seem duplicatable
|
// Same as below - randomly, tileGroups doesn't seem to contain the selected tile, and this doesn't seem reproducible
|
||||||
val worldTileGroupsForSelectedTile = tileGroups[selectedTile]
|
val worldTileGroupsForSelectedTile = tileGroups[selectedTile]
|
||||||
if (worldTileGroupsForSelectedTile != null)
|
if (worldTileGroupsForSelectedTile != null)
|
||||||
for (group in worldTileGroupsForSelectedTile)
|
for (group in worldTileGroupsForSelectedTile)
|
||||||
@ -681,7 +685,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
|
|||||||
val originalScrollX = scrollX
|
val originalScrollX = scrollX
|
||||||
val originalScrollY = scrollY
|
val originalScrollY = scrollY
|
||||||
|
|
||||||
// We want to center on the middle of the tilegroup (TG.getX()+TG.getWidth()/2)
|
// We want to center on the middle of the TileGroup (TG.getX()+TG.getWidth()/2)
|
||||||
// and so the scroll position (== filter the screen starts) needs to be half the ScrollMap away
|
// and so the scroll position (== filter the screen starts) needs to be half the ScrollMap away
|
||||||
val finalScrollX = tileGroup.x + tileGroup.width / 2 - width / 2
|
val finalScrollX = tileGroup.x + tileGroup.width / 2 - width / 2
|
||||||
|
|
||||||
|
@ -407,7 +407,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
|||||||
// This is private so that we will set the shouldUpdate to true instead.
|
// This is private so that we will set the shouldUpdate to true instead.
|
||||||
// That way, not only do we save a lot of unnecessary updates, we also ensure that all updates are called from the main GL thread
|
// That way, not only do we save a lot of unnecessary updates, we also ensure that all updates are called from the main GL thread
|
||||||
// and we don't get any silly concurrency problems!
|
// and we don't get any silly concurrency problems!
|
||||||
internal fun update() {
|
private fun update() {
|
||||||
|
|
||||||
displayTutorialsOnUpdate()
|
displayTutorialsOnUpdate()
|
||||||
|
|
||||||
|
@ -16,8 +16,10 @@ import com.unciv.logic.automation.UnitAutomation
|
|||||||
import com.unciv.logic.battle.*
|
import com.unciv.logic.battle.*
|
||||||
import com.unciv.logic.map.TileInfo
|
import com.unciv.logic.map.TileInfo
|
||||||
import com.unciv.models.AttackableTile
|
import com.unciv.models.AttackableTile
|
||||||
|
import com.unciv.models.UncivSound
|
||||||
import com.unciv.models.ruleset.unique.UniqueType
|
import com.unciv.models.ruleset.unique.UniqueType
|
||||||
import com.unciv.models.translations.tr
|
import com.unciv.models.translations.tr
|
||||||
|
import com.unciv.ui.audio.Sounds
|
||||||
import com.unciv.ui.images.ImageGetter
|
import com.unciv.ui.images.ImageGetter
|
||||||
import com.unciv.ui.utils.*
|
import com.unciv.ui.utils.*
|
||||||
import com.unciv.ui.worldscreen.WorldScreen
|
import com.unciv.ui.worldscreen.WorldScreen
|
||||||
@ -84,7 +86,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
|
|||||||
if (defender == null || (!includeFriendly && defender.getCivInfo() == attackerCiv))
|
if (defender == null || (!includeFriendly && defender.getCivInfo() == attackerCiv))
|
||||||
return null // no enemy combatant in tile
|
return null // no enemy combatant in tile
|
||||||
|
|
||||||
val canSeeDefender =
|
val canSeeDefender =
|
||||||
if (UncivGame.Current.viewEntireMapForDebug) true
|
if (UncivGame.Current.viewEntireMapForDebug) true
|
||||||
else {
|
else {
|
||||||
when {
|
when {
|
||||||
@ -251,29 +253,8 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
attackButton.onClick(attacker.getAttackSound()) {
|
attackButton.onClick(UncivSound.Silent) { // onAttackButtonClicked will do the sound
|
||||||
Battle.moveAndAttack(attacker, attackableTile)
|
onAttackButtonClicked(attacker, defender, attackableTile, damageToAttacker, damageToDefender)
|
||||||
worldScreen.mapHolder.removeUnitActionOverlay() // the overlay was one of attacking
|
|
||||||
worldScreen.update()
|
|
||||||
|
|
||||||
val actorsToFlashRed = arrayListOf<Actor>()
|
|
||||||
|
|
||||||
if (damageToDefender != 0)
|
|
||||||
actorsToFlashRed.addAll(getMapActorsForCombatant(defender))
|
|
||||||
if (damageToAttacker != 0)
|
|
||||||
actorsToFlashRed.addAll(getMapActorsForCombatant(attacker))
|
|
||||||
fun updateRedPercent(percent: Float) {
|
|
||||||
for (actor in actorsToFlashRed)
|
|
||||||
actor.color = Color.WHITE.cpy().lerp(Color.RED, percent)
|
|
||||||
}
|
|
||||||
worldScreen.stage.addAction(Actions.sequence(
|
|
||||||
object : FloatAction(0f, 1f, 0.3f, Interpolation.sine) {
|
|
||||||
override fun update(percent: Float) = updateRedPercent(percent)
|
|
||||||
},
|
|
||||||
object : FloatAction(0f, 1f, 0.3f, Interpolation.sine) {
|
|
||||||
override fun update(percent: Float) = updateRedPercent(1 - percent)
|
|
||||||
}
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,6 +265,43 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
|
|||||||
setPosition(worldScreen.stage.width/2-width/2, 5f)
|
setPosition(worldScreen.stage.width/2-width/2, 5f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onAttackButtonClicked(
|
||||||
|
attacker: ICombatant,
|
||||||
|
defender: ICombatant,
|
||||||
|
attackableTile: AttackableTile,
|
||||||
|
damageToAttacker: Int,
|
||||||
|
damageToDefender: Int
|
||||||
|
) {
|
||||||
|
val canStillAttack = Battle.movePreparingAttack(attacker, attackableTile)
|
||||||
|
worldScreen.mapHolder.removeUnitActionOverlay() // the overlay was one of attacking
|
||||||
|
// There was a direct worldScreen.update() call here, removing its 'private' but not the comment justifying the modifier.
|
||||||
|
// My tests (desktop only) show the red-flash animations look just fine without.
|
||||||
|
worldScreen.shouldUpdate = true
|
||||||
|
//Gdx.graphics.requestRendering() // Use this if immediate rendering is required
|
||||||
|
|
||||||
|
if (!canStillAttack) return
|
||||||
|
Sounds.play(attacker.getAttackSound())
|
||||||
|
Battle.attackOrNuke(attacker, attackableTile)
|
||||||
|
|
||||||
|
val actorsToFlashRed = arrayListOf<Actor>()
|
||||||
|
|
||||||
|
if (damageToDefender != 0)
|
||||||
|
actorsToFlashRed.addAll(getMapActorsForCombatant(defender))
|
||||||
|
if (damageToAttacker != 0)
|
||||||
|
actorsToFlashRed.addAll(getMapActorsForCombatant(attacker))
|
||||||
|
fun updateRedPercent(percent: Float) {
|
||||||
|
for (actor in actorsToFlashRed)
|
||||||
|
actor.color = Color.WHITE.cpy().lerp(Color.RED, percent)
|
||||||
|
}
|
||||||
|
worldScreen.stage.addAction(Actions.sequence(
|
||||||
|
object : FloatAction(0f, 1f, 0.3f, Interpolation.sine) {
|
||||||
|
override fun update(percent: Float) = updateRedPercent(percent)
|
||||||
|
},
|
||||||
|
object : FloatAction(0f, 1f, 0.3f, Interpolation.sine) {
|
||||||
|
override fun update(percent: Float) = updateRedPercent(1 - percent)
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fun getMapActorsForCombatant(combatant: ICombatant):Sequence<Actor> =
|
fun getMapActorsForCombatant(combatant: ICombatant):Sequence<Actor> =
|
||||||
sequence {
|
sequence {
|
||||||
@ -314,17 +332,17 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
|
|||||||
attackerNameWrapper.add(getIcon(attacker)).padRight(5f)
|
attackerNameWrapper.add(getIcon(attacker)).padRight(5f)
|
||||||
attackerNameWrapper.add(attackerLabel)
|
attackerNameWrapper.add(attackerLabel)
|
||||||
add(attackerNameWrapper)
|
add(attackerNameWrapper)
|
||||||
|
|
||||||
val canNuke = Battle.mayUseNuke(attacker, targetTile)
|
val canNuke = Battle.mayUseNuke(attacker, targetTile)
|
||||||
|
|
||||||
val blastRadius =
|
val blastRadius =
|
||||||
if (!attacker.unit.hasUnique(UniqueType.BlastRadius)) 2
|
if (!attacker.unit.hasUnique(UniqueType.BlastRadius)) 2
|
||||||
else attacker.unit.getMatchingUniques(UniqueType.BlastRadius).first().params[0].toInt()
|
else attacker.unit.getMatchingUniques(UniqueType.BlastRadius).first().params[0].toInt()
|
||||||
|
|
||||||
val defenderNameWrapper = Table()
|
val defenderNameWrapper = Table()
|
||||||
for (tile in targetTile.getTilesInDistance(blastRadius)) {
|
for (tile in targetTile.getTilesInDistance(blastRadius)) {
|
||||||
val defender = tryGetDefenderAtTile(tile, true) ?: continue
|
val defender = tryGetDefenderAtTile(tile, true) ?: continue
|
||||||
|
|
||||||
val defenderLabel = Label(defender.getName().tr(), skin)
|
val defenderLabel = Label(defender.getName().tr(), skin)
|
||||||
defenderNameWrapper.add(getIcon(defender)).padRight(5f)
|
defenderNameWrapper.add(getIcon(defender)).padRight(5f)
|
||||||
defenderNameWrapper.add(defenderLabel).row()
|
defenderNameWrapper.add(defenderLabel).row()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user