diff --git a/android/assets/jsons/Civ V - Vanilla/Units.json b/android/assets/jsons/Civ V - Vanilla/Units.json index 4273420fd7..a42d0aa759 100644 --- a/android/assets/jsons/Civ V - Vanilla/Units.json +++ b/android/assets/jsons/Civ V - Vanilla/Units.json @@ -1076,7 +1076,8 @@ "cost": 1000, "requiredTech": "Rocketry", "requiredResource": "Uranium", - "uniques": ["Self-destructs when attacking", "Nuclear weapon", "Requires [Manhattan Project]"] + "uniques": ["Self-destructs when attacking", "Nuclear weapon", "Requires [Manhattan Project]"], + "attackSound": "nuke" }, { "name": "Landship", diff --git a/android/assets/sounds/bombard.mp3 b/android/assets/sounds/bombard.mp3 new file mode 100644 index 0000000000..86e5eb13c5 Binary files /dev/null and b/android/assets/sounds/bombard.mp3 differ diff --git a/android/assets/sounds/nuke.mp3 b/android/assets/sounds/nuke.mp3 new file mode 100644 index 0000000000..62306230c0 Binary files /dev/null and b/android/assets/sounds/nuke.mp3 differ diff --git a/android/assets/sounds/slider.mp3 b/android/assets/sounds/slider.mp3 new file mode 100644 index 0000000000..a0f57f1c93 Binary files /dev/null and b/android/assets/sounds/slider.mp3 differ diff --git a/core/src/com/unciv/logic/battle/CityCombatant.kt b/core/src/com/unciv/logic/battle/CityCombatant.kt index d4b50b125c..47cbdc14e1 100644 --- a/core/src/com/unciv/logic/battle/CityCombatant.kt +++ b/core/src/com/unciv/logic/battle/CityCombatant.kt @@ -3,6 +3,7 @@ package com.unciv.logic.battle import com.unciv.logic.city.CityInfo import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.TileInfo +import com.unciv.models.UncivSound import com.unciv.models.ruleset.unit.UnitType import kotlin.math.pow import kotlin.math.roundToInt @@ -20,6 +21,7 @@ class CityCombatant(val city: CityInfo) : ICombatant { override fun isInvisible(): Boolean = false override fun canAttack(): Boolean = (!city.attackedThisTurn) override fun matchesCategory(category: String) = category == "City" + override fun getAttackSound() = UncivSound.Bombard override fun takeDamage(damage: Int) { city.health -= damage diff --git a/core/src/com/unciv/logic/battle/ICombatant.kt b/core/src/com/unciv/logic/battle/ICombatant.kt index da6d183946..efa74bafdc 100644 --- a/core/src/com/unciv/logic/battle/ICombatant.kt +++ b/core/src/com/unciv/logic/battle/ICombatant.kt @@ -2,6 +2,7 @@ package com.unciv.logic.battle import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.TileInfo +import com.unciv.models.UncivSound import com.unciv.models.ruleset.unit.UnitType interface ICombatant{ @@ -18,6 +19,7 @@ interface ICombatant{ fun isInvisible(): Boolean fun canAttack(): Boolean fun matchesCategory(category:String): Boolean + fun getAttackSound(): UncivSound fun isMelee(): Boolean { return getUnitType().isMelee() diff --git a/core/src/com/unciv/logic/battle/MapUnitCombatant.kt b/core/src/com/unciv/logic/battle/MapUnitCombatant.kt index 12ee64d754..79241548a5 100644 --- a/core/src/com/unciv/logic/battle/MapUnitCombatant.kt +++ b/core/src/com/unciv/logic/battle/MapUnitCombatant.kt @@ -3,6 +3,7 @@ package com.unciv.logic.battle import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.MapUnit import com.unciv.logic.map.TileInfo +import com.unciv.models.UncivSound import com.unciv.models.ruleset.unit.UnitType class MapUnitCombatant(val unit: MapUnit) : ICombatant { @@ -15,6 +16,9 @@ class MapUnitCombatant(val unit: MapUnit) : ICombatant { override fun isInvisible(): Boolean = unit.isInvisible() override fun canAttack(): Boolean = unit.canAttack() override fun matchesCategory(category:String) = unit.matchesFilter(category) + override fun getAttackSound() = unit.baseUnit.attackSound.let { + if (it==null) UncivSound.Click else UncivSound.custom(it) + } override fun takeDamage(damage: Int) { unit.health -= damage diff --git a/core/src/com/unciv/models/UncivSound.kt b/core/src/com/unciv/models/UncivSound.kt index a992885e59..6cef41c3d1 100644 --- a/core/src/com/unciv/models/UncivSound.kt +++ b/core/src/com/unciv/models/UncivSound.kt @@ -1,6 +1,6 @@ package com.unciv.models -enum class UncivSound(val value: String) { +private enum class UncivSoundConstants (val value: String) { Click("click"), Fortify("fortify"), Promote("promote"), @@ -12,5 +12,62 @@ enum class UncivSound(val value: String) { Policy("policy"), Paper("paper"), Whoosh("whoosh"), - Silent("") + Bombard("bombard"), + Slider("slider"), + Silent(""), + Custom("") +} + +/** + * Represents an Unciv Sound, either from a predefined set or custom with a specified filename. + */ +class UncivSound private constructor ( + private val type: UncivSoundConstants, + filename: String? = null +) { + /** The base filename without extension. */ + val value: String = filename ?: type.value + +/* + init { + // Checking contract "use non-custom *w/o* filename OR custom *with* one + // Removed due to private constructor + if ((type == UncivSoundConstants.Custom) == filename.isNullOrEmpty()) { + throw IllegalArgumentException("Invalid UncivSound constructor arguments") + } + } +*/ + + companion object { + val Click = UncivSound(UncivSoundConstants.Click) + val Fortify = UncivSound(UncivSoundConstants.Fortify) + val Promote = UncivSound(UncivSoundConstants.Promote) + val Upgrade = UncivSound(UncivSoundConstants.Upgrade) + val Setup = UncivSound(UncivSoundConstants.Setup) + val Chimes = UncivSound(UncivSoundConstants.Chimes) + val Coin = UncivSound(UncivSoundConstants.Coin) + val Choir = UncivSound(UncivSoundConstants.Choir) + val Policy = UncivSound(UncivSoundConstants.Policy) + val Paper = UncivSound(UncivSoundConstants.Paper) + val Whoosh = UncivSound(UncivSoundConstants.Whoosh) + val Bombard = UncivSound(UncivSoundConstants.Bombard) + val Slider = UncivSound(UncivSoundConstants.Slider) + val Silent = UncivSound(UncivSoundConstants.Silent) + /** Creates an UncivSound instance for a custom sound. + * @param filename The base filename without extension. + */ + fun custom(filename: String) = UncivSound(UncivSoundConstants.Custom, filename) + } + + // overrides ensure usability as hash key + override fun hashCode(): Int { + return type.hashCode() xor value.hashCode() + } + override fun equals(other: Any?): Boolean { + if (other == null || other !is UncivSound) return false + if (type != other.type) return false + return type != UncivSoundConstants.Custom || value == other.value + } + + override fun toString(): String = value } \ No newline at end of file diff --git a/core/src/com/unciv/ui/utils/Sounds.kt b/core/src/com/unciv/ui/utils/Sounds.kt index 24a9e2a136..fd19edb897 100644 --- a/core/src/com/unciv/ui/utils/Sounds.kt +++ b/core/src/com/unciv/ui/utils/Sounds.kt @@ -2,22 +2,64 @@ package com.unciv.ui.utils import com.badlogic.gdx.Gdx import com.badlogic.gdx.audio.Sound +import com.badlogic.gdx.files.FileHandle import com.unciv.UncivGame import com.unciv.models.UncivSound +import java.io.File +/** + * Generates Gdx [Sound] objects from [UncivSound] ones on demand, only once per key + * (two UncivSound custom instances with the same filename are considered equal). + * + * Gdx asks Sound usage to respect the Disposable contract, but since we're only caching + * a handful of them in memory we should be able to get away with keeping them alive for the + * app lifetime. + */ object Sounds { - private val soundMap = HashMap() - - fun get(sound: UncivSound): Sound { - if (!soundMap.containsKey(sound)) { - soundMap[sound] = Gdx.audio.newSound(Gdx.files.internal("sounds/${sound.value}.mp3")) + private val soundMap = HashMap() + + private val separator = File.separator // just a shorthand for readability + + private var modListHash = Int.MIN_VALUE + /** Ensure cache is not outdated _and_ build list of folders to look for sounds */ + private fun getFolders(): Sequence { + if (!UncivGame.isCurrentInitialized() || !UncivGame.Current.isGameInfoInitialized()) // Allow sounds from main menu + return sequenceOf("") + // Allow mod sounds - preferentially so they can override built-in sounds + val modList = UncivGame.Current.gameInfo.ruleSet.mods + val newHash = modList.hashCode() + if (modListHash == Int.MIN_VALUE || modListHash != newHash) { + // Seems the mod list has changed - start over + for (sound in soundMap.values) sound?.dispose() + soundMap.clear() + modListHash = newHash } - return soundMap[sound]!! + // Should we also look in UncivGame.Current.settings.visualMods? + return modList.asSequence() + .map { "mods$separator$it$separator" } + + sequenceOf("") + } + + fun get(sound: UncivSound): Sound? { + if (sound in soundMap) return soundMap[sound] + val fileName = sound.value + var file: FileHandle? = null + for (modFolder in getFolders()) { + val path = "${modFolder}sounds$separator$fileName.mp3" + file = Gdx.files.internal(path) + if (file.exists()) break + } + val newSound = + if (file == null || !file.exists()) null + else Gdx.audio.newSound(file) + // Store Sound for reuse or remember that the actual file is missing + soundMap[sound] = newSound + return newSound } fun play(sound: UncivSound) { val volume = UncivGame.Current.settings.soundEffectsVolume if (sound == UncivSound.Silent || volume < 0.01) return - get(sound).play(volume) + get(sound)?.play(volume) } } \ No newline at end of file diff --git a/core/src/com/unciv/ui/worldscreen/bottombar/BattleTable.kt b/core/src/com/unciv/ui/worldscreen/bottombar/BattleTable.kt index 71343c3eed..9d96c654a1 100644 --- a/core/src/com/unciv/ui/worldscreen/bottombar/BattleTable.kt +++ b/core/src/com/unciv/ui/worldscreen/bottombar/BattleTable.kt @@ -207,7 +207,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() { } else { - attackButton.onClick { + attackButton.onClick(attacker.getAttackSound()) { Battle.moveAndAttack(attacker, attackableTile) worldScreen.mapHolder.removeUnitActionOverlay() // the overlay was one of attacking worldScreen.shouldUpdate = true @@ -278,7 +278,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() { attackButton.label.color = Color.GRAY } else { - attackButton.onClick { + attackButton.onClick(attacker.getAttackSound()) { Battle.nuke(attacker, targetTile) worldScreen.mapHolder.removeUnitActionOverlay() // the overlay was one of attacking worldScreen.shouldUpdate = true diff --git a/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt b/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt index d53406709b..92a0a49b59 100644 --- a/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt +++ b/core/src/com/unciv/ui/worldscreen/mainmenu/OptionsPopup.kt @@ -181,7 +181,7 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr settings.minimapSize = size } settings.save() - Sounds.play(UncivSound.Click) + Sounds.play(UncivSound.Slider) if (previousScreen is WorldScreen) previousScreen.shouldUpdate = true } @@ -276,7 +276,7 @@ class OptionsPopup(val previousScreen:CameraStageBaseScreen) : Popup(previousScr soundEffectsVolumeSlider.onChange { settings.soundEffectsVolume = soundEffectsVolumeSlider.value settings.save() - Sounds.play(UncivSound.Click) + Sounds.play(UncivSound.Slider) } optionsTable.add(soundEffectsVolumeSlider).pad(5f).row() } diff --git a/docs/Credits.md b/docs/Credits.md index ae968a91ce..7851ad7981 100644 --- a/docs/Credits.md +++ b/docs/Credits.md @@ -562,7 +562,10 @@ Sounds are from FreeSound.org and are either Creative Commons or Public Domain * [Horse Neigh 2](https://freesound.org/people/GoodListener/sounds/322450/) By GoodListener as 'horse' for cavalry attack sounds * [machine gun 001 - loop](https://freesound.org/people/pgi/sounds/212602/) By pgi as 'machinegun' for machine gun attack sound * [uzzi_full_single](https://freesound.org/people/Deganoth/sounds/348685/) By Deganoth as 'shot' for bullet attacks - +* [Grenade Launcher 2](https://soundbible.com/2140-Grenade-Launcher-2.html) By Daniel Simon as city bombard sound (CC Attribution 3.0 license) +* [Woosh](https://soundbible.com/2068-Woosh.html) by Mark DiAngelo as 'slider' sound (CC Attribution 3.0 license) +* [Tornado-Siren-II](https://soundbible.com/1937-Tornado-Siren-II.html) by Delilah as part of 'nuke' sound (CC Attribution 3.0 license) +* [Explosion-Ultra-Bass](https://soundbible.com/1807-Explosion-Ultra-Bass.html) by Mark DiAngelo as part of 'nuke' sound (CC Attribution 3.0 license) # Music The following music is from https://filmmusic.io