mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-27 13:55:54 -04:00
Music controller with fade-over and mod capabilities. (#5273)
* Music controller with fade-over and mod capabilities. - Preparation for music following game situations - Minimal in-game hooks for now - Already allows mods providing music, will play randomly * Music controller - template
This commit is contained in:
parent
b7467d3467
commit
5e4aff90e9
@ -469,6 +469,7 @@ See online Readme =
|
|||||||
Turns between autosaves =
|
Turns between autosaves =
|
||||||
Sound effects volume =
|
Sound effects volume =
|
||||||
Music volume =
|
Music volume =
|
||||||
|
Pause between tracks =
|
||||||
Download music =
|
Download music =
|
||||||
Downloading... =
|
Downloading... =
|
||||||
Could not download music! =
|
Could not download music! =
|
||||||
|
@ -67,7 +67,6 @@ class MainMenuScreen: CameraStageBaseScreen() {
|
|||||||
// If we were in a mod, some of the resource images for the background map we're creating
|
// If we were in a mod, some of the resource images for the background map we're creating
|
||||||
// will not exist unless we reset the ruleset and images
|
// will not exist unless we reset the ruleset and images
|
||||||
ImageGetter.ruleset = RulesetCache.getBaseRuleset()
|
ImageGetter.ruleset = RulesetCache.getBaseRuleset()
|
||||||
//ImageGetter.refreshAtlas()
|
|
||||||
|
|
||||||
thread(name = "ShowMapBackground") {
|
thread(name = "ShowMapBackground") {
|
||||||
val newMap = MapGenerator(RulesetCache.getBaseRuleset())
|
val newMap = MapGenerator(RulesetCache.getBaseRuleset())
|
||||||
|
@ -4,7 +4,6 @@ import com.badlogic.gdx.Application
|
|||||||
import com.badlogic.gdx.Game
|
import com.badlogic.gdx.Game
|
||||||
import com.badlogic.gdx.Gdx
|
import com.badlogic.gdx.Gdx
|
||||||
import com.badlogic.gdx.Input
|
import com.badlogic.gdx.Input
|
||||||
import com.badlogic.gdx.audio.Music
|
|
||||||
import com.badlogic.gdx.scenes.scene2d.actions.Actions
|
import com.badlogic.gdx.scenes.scene2d.actions.Actions
|
||||||
import com.badlogic.gdx.utils.Align
|
import com.badlogic.gdx.utils.Align
|
||||||
import com.unciv.logic.GameInfo
|
import com.unciv.logic.GameInfo
|
||||||
@ -15,6 +14,7 @@ import com.unciv.models.ruleset.RulesetCache
|
|||||||
import com.unciv.models.tilesets.TileSetCache
|
import com.unciv.models.tilesets.TileSetCache
|
||||||
import com.unciv.models.translations.Translations
|
import com.unciv.models.translations.Translations
|
||||||
import com.unciv.ui.LanguagePickerScreen
|
import com.unciv.ui.LanguagePickerScreen
|
||||||
|
import com.unciv.ui.audio.MusicController
|
||||||
import com.unciv.ui.utils.*
|
import com.unciv.ui.utils.*
|
||||||
import com.unciv.ui.worldscreen.PlayerReadyScreen
|
import com.unciv.ui.worldscreen.PlayerReadyScreen
|
||||||
import com.unciv.ui.worldscreen.WorldScreen
|
import com.unciv.ui.worldscreen.WorldScreen
|
||||||
@ -37,6 +37,8 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
|||||||
fun isGameInfoInitialized() = this::gameInfo.isInitialized
|
fun isGameInfoInitialized() = this::gameInfo.isInitialized
|
||||||
lateinit var settings: GameSettings
|
lateinit var settings: GameSettings
|
||||||
lateinit var crashController: CrashController
|
lateinit var crashController: CrashController
|
||||||
|
lateinit var musicController: MusicController
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This exists so that when debugging we can see the entire map.
|
* This exists so that when debugging we can see the entire map.
|
||||||
* Remember to turn this to false before commit and upload!
|
* Remember to turn this to false before commit and upload!
|
||||||
@ -57,8 +59,6 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
|||||||
|
|
||||||
lateinit var worldScreen: WorldScreen
|
lateinit var worldScreen: WorldScreen
|
||||||
|
|
||||||
var music: Music? = null
|
|
||||||
val musicLocation = "music/thatched-villagers.mp3"
|
|
||||||
var isInitialized = false
|
var isInitialized = false
|
||||||
|
|
||||||
|
|
||||||
@ -86,6 +86,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
|||||||
*/
|
*/
|
||||||
settings = GameSaver.getGeneralSettings() // needed for the screen
|
settings = GameSaver.getGeneralSettings() // needed for the screen
|
||||||
screen = LoadingScreen() // NOT dependent on any atlas or skin
|
screen = LoadingScreen() // NOT dependent on any atlas or skin
|
||||||
|
musicController = MusicController() // early, but at this point does only copy volume from settings
|
||||||
|
|
||||||
ImageGetter.resetAtlases()
|
ImageGetter.resetAtlases()
|
||||||
ImageGetter.setNewRuleset(ImageGetter.ruleset) // This needs to come after the settings, since we may have default visual mods
|
ImageGetter.setNewRuleset(ImageGetter.ruleset) // This needs to come after the settings, since we may have default visual mods
|
||||||
@ -110,11 +111,10 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
|||||||
|
|
||||||
// This stuff needs to run on the main thread because it needs the GL context
|
// This stuff needs to run on the main thread because it needs the GL context
|
||||||
Gdx.app.postRunnable {
|
Gdx.app.postRunnable {
|
||||||
|
musicController.chooseTrack()
|
||||||
|
|
||||||
ImageGetter.ruleset = RulesetCache.getBaseRuleset() // so that we can enter the map editor without having to load a game first
|
ImageGetter.ruleset = RulesetCache.getBaseRuleset() // so that we can enter the map editor without having to load a game first
|
||||||
|
|
||||||
|
|
||||||
thread(name="Music") { startMusic() }
|
|
||||||
|
|
||||||
if (settings.isFreshlyCreated) {
|
if (settings.isFreshlyCreated) {
|
||||||
setScreen(LanguagePickerScreen())
|
setScreen(LanguagePickerScreen())
|
||||||
} else { setScreen(MainMenuScreen()) }
|
} else { setScreen(MainMenuScreen()) }
|
||||||
@ -127,6 +127,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
|||||||
fun loadGame(gameInfo: GameInfo) {
|
fun loadGame(gameInfo: GameInfo) {
|
||||||
this.gameInfo = gameInfo
|
this.gameInfo = gameInfo
|
||||||
ImageGetter.setNewRuleset(gameInfo.ruleSet)
|
ImageGetter.setNewRuleset(gameInfo.ruleSet)
|
||||||
|
musicController.setModList(gameInfo.gameParameters.mods)
|
||||||
Gdx.input.inputProcessor = null // Since we will set the world screen when we're ready,
|
Gdx.input.inputProcessor = null // Since we will set the world screen when we're ready,
|
||||||
if (gameInfo.civilizations.count { it.playerType == PlayerType.Human } > 1 && !gameInfo.gameParameters.isOnlineMultiplayer)
|
if (gameInfo.civilizations.count { it.playerType == PlayerType.Human } > 1 && !gameInfo.gameParameters.isOnlineMultiplayer)
|
||||||
setScreen(PlayerReadyScreen(gameInfo, gameInfo.getPlayerToViewAs()))
|
setScreen(PlayerReadyScreen(gameInfo, gameInfo.getPlayerToViewAs()))
|
||||||
@ -136,18 +137,6 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startMusic() {
|
|
||||||
if (settings.musicVolume < 0.01) return
|
|
||||||
|
|
||||||
val musicFile = Gdx.files.local(musicLocation)
|
|
||||||
if (musicFile.exists()) {
|
|
||||||
music = Gdx.audio.newMusic(musicFile)
|
|
||||||
music!!.isLooping = true
|
|
||||||
music!!.volume = 0.4f * settings.musicVolume
|
|
||||||
music!!.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setScreen(screen: CameraStageBaseScreen) {
|
fun setScreen(screen: CameraStageBaseScreen) {
|
||||||
Gdx.input.inputProcessor = screen.stage
|
Gdx.input.inputProcessor = screen.stage
|
||||||
super.setScreen(screen)
|
super.setScreen(screen)
|
||||||
@ -178,13 +167,14 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
|
|||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
cancelDiscordEvent?.invoke()
|
cancelDiscordEvent?.invoke()
|
||||||
Sounds.clearCache()
|
Sounds.clearCache()
|
||||||
|
if (::musicController.isInitialized) musicController.shutdown()
|
||||||
|
|
||||||
// Log still running threads (on desktop that should be only this one and "DestroyJavaVM")
|
// Log still running threads (on desktop that should be only this one and "DestroyJavaVM")
|
||||||
val numThreads = Thread.activeCount()
|
val numThreads = Thread.activeCount()
|
||||||
val threadList = Array(numThreads) { _ -> Thread() }
|
val threadList = Array(numThreads) { _ -> Thread() }
|
||||||
Thread.enumerate(threadList)
|
Thread.enumerate(threadList)
|
||||||
|
|
||||||
if (isGameInfoInitialized()){
|
if (isGameInfoInitialized()) {
|
||||||
val autoSaveThread = threadList.firstOrNull { it.name == "Autosave" }
|
val autoSaveThread = threadList.firstOrNull { it.name == "Autosave" }
|
||||||
if (autoSaveThread != null && autoSaveThread.isAlive) {
|
if (autoSaveThread != null && autoSaveThread.isAlive) {
|
||||||
// auto save is already in progress (e.g. started by onPause() event)
|
// auto save is already in progress (e.g. started by onPause() event)
|
||||||
@ -217,4 +207,3 @@ private class LoadingScreen : CameraStageBaseScreen() {
|
|||||||
stage.addActor(happinessImage)
|
stage.addActor(happinessImage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,8 +23,11 @@ class GameSettings {
|
|||||||
var tutorialsShown = HashSet<String>()
|
var tutorialsShown = HashSet<String>()
|
||||||
var tutorialTasksCompleted = HashSet<String>()
|
var tutorialTasksCompleted = HashSet<String>()
|
||||||
var hasCrashedRecently = false
|
var hasCrashedRecently = false
|
||||||
|
|
||||||
var soundEffectsVolume = 0.5f
|
var soundEffectsVolume = 0.5f
|
||||||
var musicVolume = 0.5f
|
var musicVolume = 0.5f
|
||||||
|
var pauseBetweenTracks = 10
|
||||||
|
|
||||||
var turnsBetweenAutosaves = 1
|
var turnsBetweenAutosaves = 1
|
||||||
var tileSet: String = "FantasyHex"
|
var tileSet: String = "FantasyHex"
|
||||||
var showTutorials: Boolean = true
|
var showTutorials: Boolean = true
|
||||||
|
335
core/src/com/unciv/ui/audio/MusicController.kt
Normal file
335
core/src/com/unciv/ui/audio/MusicController.kt
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
package com.unciv.ui.audio
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Gdx
|
||||||
|
import com.badlogic.gdx.Files.FileType
|
||||||
|
import com.badlogic.gdx.files.FileHandle
|
||||||
|
import com.unciv.UncivGame
|
||||||
|
import com.unciv.models.metadata.GameSettings
|
||||||
|
import com.unciv.ui.worldscreen.mainmenu.DropBox
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.concurrent.timer
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play, choose, fade-in/out and generally manage music track playback.
|
||||||
|
*
|
||||||
|
* Main methods: [chooseTrack], [pause], [resume], [setModList], [isPlaying], [gracefulShutdown]
|
||||||
|
*/
|
||||||
|
class MusicController {
|
||||||
|
companion object {
|
||||||
|
/** Mods live in Local - but this file prepares for music living in External just in case */
|
||||||
|
private val musicLocation = FileType.Local
|
||||||
|
const val musicPath = "music"
|
||||||
|
const val modPath = "mods"
|
||||||
|
const val musicFallbackLocation = "music/thatched-villagers.mp3"
|
||||||
|
const val maxVolume = 0.6f // baseVolume has range 0.0-1.0, which is multiplied by this for the API
|
||||||
|
private const val ticksPerSecond = 20 // Timer frequency defines smoothness of fade-in/out
|
||||||
|
private const val timerPeriod = 1000L / ticksPerSecond
|
||||||
|
const val defaultFadingStep = 0.08f // this means fade is 12 ticks or 0.6 seconds
|
||||||
|
private const val musicHistorySize = 8 // number of names to keep to avoid playing the same in short succession
|
||||||
|
private val fileExtensions = listOf("mp3", "ogg") // flac, opus, m4a... blocked by Gdx, `wav` we don't want
|
||||||
|
|
||||||
|
internal const val consoleLog = false
|
||||||
|
|
||||||
|
private fun getFile(path: String) =
|
||||||
|
if (musicLocation == FileType.External && Gdx.files.isExternalStorageAvailable)
|
||||||
|
Gdx.files.external(path)
|
||||||
|
else Gdx.files.local(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
//region Fields
|
||||||
|
/** mirrors [GameSettings.musicVolume] - use [setVolume] to update */
|
||||||
|
var baseVolume: Float = UncivGame.Current.settings.musicVolume
|
||||||
|
private set
|
||||||
|
|
||||||
|
/** Pause in seconds between tracks unless [chooseTrack] is called to force a track change */
|
||||||
|
var silenceLength: Float
|
||||||
|
get() = silenceLengthInTicks.toFloat() / ticksPerSecond
|
||||||
|
set(value) { silenceLengthInTicks = (ticksPerSecond * value).toInt() }
|
||||||
|
private var silenceLengthInTicks = UncivGame.Current.settings.pauseBetweenTracks * ticksPerSecond
|
||||||
|
|
||||||
|
private var mods = HashSet<String>()
|
||||||
|
|
||||||
|
private var state = ControllerState.Idle
|
||||||
|
|
||||||
|
private var ticksOfSilence: Int = 0
|
||||||
|
|
||||||
|
private var musicTimer: Timer? = null
|
||||||
|
|
||||||
|
private enum class ControllerState {
|
||||||
|
/** As the name says. Timer will stop itself if it encounters this state. */
|
||||||
|
Idle,
|
||||||
|
/** Play a track to its end, then silence for a while, then choose another track */
|
||||||
|
Playing,
|
||||||
|
/** Play a track to its end, then go [Idle] */
|
||||||
|
PlaySingle,
|
||||||
|
/** Wait for a while in silence to start next track */
|
||||||
|
Silence,
|
||||||
|
/** Music fades to pause or is paused. Continue with chooseTrack or resume. */
|
||||||
|
Pause,
|
||||||
|
/** Fade out then stop */
|
||||||
|
Shutdown
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Simple two-entry only queue, for smooth fade-overs from one track to another */
|
||||||
|
var current: MusicTrackController? = null
|
||||||
|
var next: MusicTrackController? = null
|
||||||
|
|
||||||
|
/** Keeps paths of recently played track to reduce repetition */
|
||||||
|
private val musicHistory = ArrayDeque<String>(musicHistorySize)
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
//region Pure functions
|
||||||
|
|
||||||
|
/** @return the path of the playing track or null if none playing */
|
||||||
|
fun currentlyPlaying() = if (state != ControllerState.Playing && state != ControllerState.PlaySingle) null
|
||||||
|
else musicHistory.peekLast()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether any music tracks are available for the options menu
|
||||||
|
*/
|
||||||
|
fun isMusicAvailable() = getAllMusicFiles().any()
|
||||||
|
|
||||||
|
/** @return `true` if there's a current music track and if it's actively playing */
|
||||||
|
fun isPlaying(): Boolean {
|
||||||
|
return current?.isPlaying() == true
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
//region Internal helpers
|
||||||
|
|
||||||
|
private fun clearCurrent() {
|
||||||
|
current?.clear()
|
||||||
|
current = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun musicTimerTask() {
|
||||||
|
// This ticks [ticksPerSecond] times per second
|
||||||
|
when (state) {
|
||||||
|
ControllerState.Playing, ControllerState.PlaySingle ->
|
||||||
|
if (current == null) {
|
||||||
|
if (next == null) {
|
||||||
|
// no music to play - begin silence or shut down
|
||||||
|
ticksOfSilence = 0
|
||||||
|
state = if (state == ControllerState.PlaySingle) ControllerState.Shutdown else ControllerState.Silence
|
||||||
|
} else if (next!!.state.canPlay) {
|
||||||
|
// Next track - if top slot empty and a next exists, move it to top and start
|
||||||
|
current = next
|
||||||
|
next = null
|
||||||
|
if (!current!!.play())
|
||||||
|
state = ControllerState.Shutdown
|
||||||
|
} // else wait for the thread of next.load() to finish
|
||||||
|
} else if (!current!!.isPlaying()) {
|
||||||
|
// normal end of track
|
||||||
|
clearCurrent()
|
||||||
|
// rest handled next tick
|
||||||
|
} else {
|
||||||
|
if (current?.timerTick() == MusicTrackController.State.Idle)
|
||||||
|
clearCurrent()
|
||||||
|
next?.timerTick()
|
||||||
|
}
|
||||||
|
ControllerState.Silence ->
|
||||||
|
if (++ticksOfSilence > silenceLengthInTicks) {
|
||||||
|
ticksOfSilence = 0
|
||||||
|
chooseTrack()
|
||||||
|
}
|
||||||
|
ControllerState.Shutdown, ControllerState.Idle -> {
|
||||||
|
state = ControllerState.Idle
|
||||||
|
shutdown()
|
||||||
|
}
|
||||||
|
ControllerState.Pause ->
|
||||||
|
current?.timerTick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get sequence of potential music locations */
|
||||||
|
private fun getMusicFolders() = sequence {
|
||||||
|
yieldAll(
|
||||||
|
(UncivGame.Current.settings.visualMods + mods).asSequence()
|
||||||
|
.map { getFile(modPath).child(it).child(musicPath) }
|
||||||
|
)
|
||||||
|
yield(getFile(musicPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get sequence of all existing music files */
|
||||||
|
private fun getAllMusicFiles() = getMusicFolders()
|
||||||
|
.filter { it.exists() && it.isDirectory }
|
||||||
|
.flatMap { it.list().asSequence() }
|
||||||
|
// ensure only normal files with common sound extension
|
||||||
|
.filter { it.exists() && !it.isDirectory && it.extension() in fileExtensions }
|
||||||
|
|
||||||
|
/** Choose adequate entry from [getAllMusicFiles] */
|
||||||
|
private fun chooseFile(prefix: String, suffix: String, flags: EnumSet<MusicTrackChooserFlags>): FileHandle? {
|
||||||
|
if (flags.contains(MusicTrackChooserFlags.PlayDefaultFile))
|
||||||
|
return getFile(musicFallbackLocation)
|
||||||
|
// Scan whole music folder and mods to find best match for desired prefix and/or suffix
|
||||||
|
// get a path list (as strings) of music folder candidates - existence unchecked
|
||||||
|
return getAllMusicFiles()
|
||||||
|
.filter {
|
||||||
|
(!flags.contains(MusicTrackChooserFlags.PrefixMustMatch) || it.nameWithoutExtension().startsWith(prefix))
|
||||||
|
&& (!flags.contains(MusicTrackChooserFlags.SuffixMustMatch) || it.nameWithoutExtension().endsWith(suffix))
|
||||||
|
}
|
||||||
|
// sort them by prefix match / suffix match / not last played / random
|
||||||
|
.sortedWith(compareBy(
|
||||||
|
{ if (it.nameWithoutExtension().startsWith(prefix)) 0 else 1 }
|
||||||
|
, { if (it.nameWithoutExtension().endsWith(suffix)) 0 else 1 }
|
||||||
|
, { if (it.path() in musicHistory) 1 else 0 }
|
||||||
|
, { Random().nextInt() }))
|
||||||
|
// Then just pick the first one. Not as wasteful as it looks - need to check all names anyway
|
||||||
|
.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
//region State changing methods
|
||||||
|
|
||||||
|
/** This tells the music controller about active mods - all are allowed to provide tracks */
|
||||||
|
fun setModList ( newMods: HashSet<String> ) {
|
||||||
|
//todo: Ensure this gets updated where appropriate.
|
||||||
|
// loadGame; newGame: Choose Map with Mods?; map editor...
|
||||||
|
// check against "ImageGetter.ruleset=" ?
|
||||||
|
mods = newMods
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses and plays a music track using an adaptable approach - for details see the wiki.
|
||||||
|
* Called without parameters it will choose a new ambient music track and start playing it with fade-in/out.
|
||||||
|
* Will do nothing when no music files exist.
|
||||||
|
*
|
||||||
|
* @param prefix file name prefix, meant to represent **Context** - in most cases a Civ name or default "Ambient"
|
||||||
|
* @param suffix file name suffix, meant to represent **Mood** - e.g. Peace, War, Theme...
|
||||||
|
* @param flags a set of optional flags to tune the choice and playback.
|
||||||
|
* @return `true` = success, `false` = no match, no playback change
|
||||||
|
*/
|
||||||
|
fun chooseTrack (
|
||||||
|
prefix: String = "",
|
||||||
|
suffix: String = "",
|
||||||
|
flags: EnumSet<MusicTrackChooserFlags> = EnumSet.noneOf(MusicTrackChooserFlags::class.java)
|
||||||
|
): Boolean {
|
||||||
|
val musicFile = chooseFile(prefix, suffix, flags)
|
||||||
|
|
||||||
|
if (musicFile == null) {
|
||||||
|
// MustMatch flags at work or Music folder empty
|
||||||
|
if (consoleLog)
|
||||||
|
println("No music found for prefix=$prefix, suffix=$suffix, flags=$flags")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (musicFile.path() == currentlyPlaying())
|
||||||
|
return true // picked file already playing
|
||||||
|
if (!musicFile.exists())
|
||||||
|
return false // Safety check - nothing to play found?
|
||||||
|
|
||||||
|
next?.clear()
|
||||||
|
next = MusicTrackController(baseVolume * maxVolume)
|
||||||
|
|
||||||
|
next!!.load(musicFile, onError = {
|
||||||
|
ticksOfSilence = 0
|
||||||
|
state = ControllerState.Silence // will retry after one silence period
|
||||||
|
next = null
|
||||||
|
}, onSuccess = {
|
||||||
|
if (consoleLog)
|
||||||
|
println("Music queued: ${musicFile.path()} for prefix=$prefix, suffix=$suffix, flags=$flags")
|
||||||
|
|
||||||
|
if (musicHistory.size >= musicHistorySize) musicHistory.removeFirst()
|
||||||
|
musicHistory.addLast(musicFile.path())
|
||||||
|
|
||||||
|
val fadingStep = defaultFadingStep / (if (flags.contains(MusicTrackChooserFlags.SlowFade)) 5 else 1)
|
||||||
|
it.startFade(MusicTrackController.State.FadeIn, fadingStep)
|
||||||
|
|
||||||
|
when (state) {
|
||||||
|
ControllerState.Playing, ControllerState.PlaySingle ->
|
||||||
|
current?.startFade(MusicTrackController.State.FadeOut, fadingStep)
|
||||||
|
ControllerState.Pause ->
|
||||||
|
if (current?.state == MusicTrackController.State.Idle) clearCurrent()
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Yes while the loader is doing its thing we wait for it in a Playing state
|
||||||
|
state = if (flags.contains(MusicTrackChooserFlags.PlaySingle)) ControllerState.PlaySingle else ControllerState.Playing
|
||||||
|
|
||||||
|
// Start background TimerTask which manages track changes
|
||||||
|
if (musicTimer == null)
|
||||||
|
musicTimer = timer("MusicTimer", true, 0, timerPeriod) {
|
||||||
|
musicTimerTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pause playback with fade-out
|
||||||
|
*
|
||||||
|
* @param speedFactor accelerate (>1) or slow down (<1) the fade-out. Clamped to 1/1000..1000.
|
||||||
|
*/
|
||||||
|
fun pause(speedFactor: Float = 1f) {
|
||||||
|
if ((state != ControllerState.Playing && state != ControllerState.PlaySingle) || current == null) return
|
||||||
|
val fadingStep = defaultFadingStep * speedFactor.coerceIn(0.001f..1000f)
|
||||||
|
current!!.startFade(MusicTrackController.State.FadeOut, fadingStep)
|
||||||
|
state = ControllerState.Pause
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resume playback with fade-in - from a pause will resume where playback left off,
|
||||||
|
* otherwise it will start a new ambient track choice.
|
||||||
|
*
|
||||||
|
* @param speedFactor accelerate (>1) or slow down (<1) the fade-in. Clamped to 1/1000..1000.
|
||||||
|
*/
|
||||||
|
fun resume(speedFactor: Float = 1f) {
|
||||||
|
if (state == ControllerState.Pause && current != null) {
|
||||||
|
val fadingStep = defaultFadingStep * speedFactor.coerceIn(0.001f..1000f)
|
||||||
|
current!!.startFade(MusicTrackController.State.FadeIn, fadingStep)
|
||||||
|
state = ControllerState.Playing // this may circumvent a PlaySingle, but, currently only the main menu resumes, and then it's perfect
|
||||||
|
current!!.play()
|
||||||
|
} else if (state == ControllerState.Idle) {
|
||||||
|
chooseTrack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fade out then shutdown with a given [duration] in seconds */
|
||||||
|
fun fadeoutToSilence(duration: Float = 4.0f) {
|
||||||
|
val fadingStep = 1f / ticksPerSecond / duration
|
||||||
|
current?.startFade(MusicTrackController.State.FadeOut, fadingStep)
|
||||||
|
next?.startFade(MusicTrackController.State.FadeOut, fadingStep)
|
||||||
|
state = ControllerState.Shutdown
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Update playback volume, to be called from options popup */
|
||||||
|
fun setVolume(volume: Float) {
|
||||||
|
baseVolume = volume
|
||||||
|
if ( volume < 0.01 ) shutdown()
|
||||||
|
else if (isPlaying()) current!!.setVolume(baseVolume * maxVolume)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Soft shutdown of music playback, with fadeout */
|
||||||
|
fun gracefulShutdown() {
|
||||||
|
if (state == ControllerState.Idle) shutdown()
|
||||||
|
else state = ControllerState.Shutdown
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Forceful shutdown of music playback and timers - see [gracefulShutdown] */
|
||||||
|
fun shutdown() {
|
||||||
|
state = ControllerState.Idle
|
||||||
|
if (musicTimer != null) {
|
||||||
|
musicTimer!!.cancel()
|
||||||
|
musicTimer = null
|
||||||
|
}
|
||||||
|
if (next != null) {
|
||||||
|
next!!.clear()
|
||||||
|
next = null
|
||||||
|
}
|
||||||
|
if (current != null) {
|
||||||
|
current!!.clear()
|
||||||
|
current = null
|
||||||
|
}
|
||||||
|
musicHistory.clear()
|
||||||
|
if (consoleLog)
|
||||||
|
println("MusicController shut down.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadDefaultFile() {
|
||||||
|
val file = DropBox.downloadFile(musicFallbackLocation)
|
||||||
|
getFile(musicFallbackLocation).write(file, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
}
|
14
core/src/com/unciv/ui/audio/MusicTrackChooserFlags.kt
Normal file
14
core/src/com/unciv/ui/audio/MusicTrackChooserFlags.kt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package com.unciv.ui.audio
|
||||||
|
|
||||||
|
enum class MusicTrackChooserFlags {
|
||||||
|
/** Makes prefix parameter a mandatory match */
|
||||||
|
PrefixMustMatch,
|
||||||
|
/** Makes suffix parameter a mandatory match */
|
||||||
|
SuffixMustMatch,
|
||||||
|
/** Extends fade duration by factor 5 */
|
||||||
|
SlowFade,
|
||||||
|
/** Lets music controller shut down after track ends instead of choosing a random next track */
|
||||||
|
PlaySingle,
|
||||||
|
/** directly choose the 'fallback' file for playback */
|
||||||
|
PlayDefaultFile,
|
||||||
|
}
|
152
core/src/com/unciv/ui/audio/MusicTrackController.kt
Normal file
152
core/src/com/unciv/ui/audio/MusicTrackController.kt
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
package com.unciv.ui.audio
|
||||||
|
|
||||||
|
import com.badlogic.gdx.Gdx
|
||||||
|
import com.badlogic.gdx.audio.Music
|
||||||
|
import com.badlogic.gdx.files.FileHandle
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
/** Wraps one Gdx Music instance and manages threaded loading, playback, fading and cleanup */
|
||||||
|
class MusicTrackController(private var volume: Float) {
|
||||||
|
|
||||||
|
/** Internal state of this Music track */
|
||||||
|
enum class State(val canPlay: Boolean) {
|
||||||
|
None(false),
|
||||||
|
Loading(false),
|
||||||
|
Idle(true),
|
||||||
|
FadeIn(true),
|
||||||
|
Playing(true),
|
||||||
|
FadeOut(true),
|
||||||
|
Error(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = State.None
|
||||||
|
private set
|
||||||
|
var music: Music? = null
|
||||||
|
private set
|
||||||
|
private var loaderThread: Thread? = null
|
||||||
|
private var fadeStep = MusicController.defaultFadingStep
|
||||||
|
private var fadeVolume: Float = 1f
|
||||||
|
|
||||||
|
/** Clean up and dispose resources */
|
||||||
|
fun clear() {
|
||||||
|
state = State.None
|
||||||
|
clearLoader()
|
||||||
|
clearMusic()
|
||||||
|
}
|
||||||
|
private fun clearLoader() {
|
||||||
|
if (loaderThread == null) return
|
||||||
|
loaderThread!!.interrupt()
|
||||||
|
loaderThread = null
|
||||||
|
}
|
||||||
|
private fun clearMusic() {
|
||||||
|
if (music == null) return
|
||||||
|
music!!.stop()
|
||||||
|
music!!.dispose()
|
||||||
|
music = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Loads [file] into this controller's [music] and optionally calls [onSuccess] when done.
|
||||||
|
* Failures are silently logged to console, and [onError] is called.
|
||||||
|
* Callbacks run on the background thread.
|
||||||
|
* @throws IllegalStateException if called in the wrong state (fresh or cleared instance only)
|
||||||
|
*/
|
||||||
|
fun load(
|
||||||
|
file: FileHandle,
|
||||||
|
onError: ((MusicTrackController)->Unit)? = null,
|
||||||
|
onSuccess: ((MusicTrackController)->Unit)? = null
|
||||||
|
) {
|
||||||
|
if (state != State.None || loaderThread != null || music != null)
|
||||||
|
throw IllegalStateException("MusicTrackController.load should only be called once")
|
||||||
|
loaderThread = thread(name = "MusicLoader") {
|
||||||
|
state = State.Loading
|
||||||
|
try {
|
||||||
|
music = Gdx.audio.newMusic(file)
|
||||||
|
if (state != State.Loading) { // in case clear was called in the meantime
|
||||||
|
clearMusic()
|
||||||
|
} else {
|
||||||
|
state = State.Idle
|
||||||
|
if (MusicController.consoleLog)
|
||||||
|
println("Music loaded: $file")
|
||||||
|
onSuccess?.invoke(this)
|
||||||
|
}
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
println("Exception loading $file: ${ex.message}")
|
||||||
|
if (MusicController.consoleLog)
|
||||||
|
ex.printStackTrace()
|
||||||
|
state = State.Error
|
||||||
|
onError?.invoke(this)
|
||||||
|
}
|
||||||
|
loaderThread = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called by the [MusicController] in its timer "tick" event handler, implements fading */
|
||||||
|
fun timerTick(): State {
|
||||||
|
if (state == State.FadeIn) fadeInStep()
|
||||||
|
if (state == State.FadeOut) fadeOutStep()
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
private fun fadeInStep() {
|
||||||
|
// fade-in: linearly ramp fadeVolume to 1.0, then continue playing
|
||||||
|
fadeVolume += fadeStep
|
||||||
|
if (fadeVolume < 1f && music != null && music!!.isPlaying) {
|
||||||
|
music!!.volume = volume * fadeVolume
|
||||||
|
return
|
||||||
|
}
|
||||||
|
music!!.volume = volume
|
||||||
|
fadeVolume = 1f
|
||||||
|
state = State.Playing
|
||||||
|
}
|
||||||
|
private fun fadeOutStep() {
|
||||||
|
// fade-out: linearly ramp fadeVolume to 0.0, then act according to Status (Playing->Silence/Pause/Shutdown)
|
||||||
|
fadeVolume -= fadeStep
|
||||||
|
if (fadeVolume >= 0.001f && music != null && music!!.isPlaying) {
|
||||||
|
music!!.volume = volume * fadeVolume
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fadeVolume = 0f
|
||||||
|
state = State.Idle
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Starts fadeIn or fadeOut.
|
||||||
|
*
|
||||||
|
* Note this does _not_ set the current fade "percentage" to allow smoothly changing direction mid-fade
|
||||||
|
* @param step Overrides current fade step only if >0
|
||||||
|
*/
|
||||||
|
fun startFade(fade: State, step: Float = 0f) {
|
||||||
|
if (!state.canPlay) return
|
||||||
|
if (fadeStep > 0f) fadeStep = step
|
||||||
|
state = fade
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return [Music.isPlaying] (Gdx music stream is playing) unless [state] says it won't make sense */
|
||||||
|
fun isPlaying() = state.canPlay && music?.isPlaying == true
|
||||||
|
|
||||||
|
/** Calls play() on the wrapped Gdx Music, catching exceptions to console.
|
||||||
|
* @return success
|
||||||
|
* @throws IllegalStateException if called on uninitialized instance
|
||||||
|
*/
|
||||||
|
fun play(): Boolean {
|
||||||
|
if (!state.canPlay || music == null) {
|
||||||
|
throw IllegalStateException("MusicTrackController.play called on uninitialized instance")
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
music!!.volume = volume
|
||||||
|
if (!music!!.isPlaying) // for fade-over this could be called by the end of the previous track
|
||||||
|
music!!.play()
|
||||||
|
true
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
println("Exception playing music: ${ex.message}")
|
||||||
|
if (MusicController.consoleLog)
|
||||||
|
ex.printStackTrace()
|
||||||
|
state = State.Error
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adjust master volume without affecting a fade-in/out */
|
||||||
|
fun setVolume(newVolume: Float) {
|
||||||
|
volume = newVolume
|
||||||
|
music?.volume = volume * fadeVolume
|
||||||
|
}
|
||||||
|
}
|
@ -170,6 +170,7 @@ class GameOptionsTable(
|
|||||||
ruleset.modOptions = newRuleset.modOptions
|
ruleset.modOptions = newRuleset.modOptions
|
||||||
|
|
||||||
ImageGetter.setNewRuleset(ruleset)
|
ImageGetter.setNewRuleset(ruleset)
|
||||||
|
UncivGame.Current.musicController.setModList(gameParameters.mods)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getModCheckboxes(isPortrait: Boolean = false): Table {
|
fun getModCheckboxes(isPortrait: Boolean = false): Table {
|
||||||
|
@ -226,6 +226,7 @@ class NewGameScreen(
|
|||||||
ruleset.clear()
|
ruleset.clear()
|
||||||
ruleset.add(RulesetCache.getComplexRuleset(gameSetupInfo.gameParameters.mods))
|
ruleset.add(RulesetCache.getComplexRuleset(gameSetupInfo.gameParameters.mods))
|
||||||
ImageGetter.setNewRuleset(ruleset)
|
ImageGetter.setNewRuleset(ruleset)
|
||||||
|
game.musicController.setModList(gameSetupInfo.gameParameters.mods)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun lockTables() {
|
fun lockTables() {
|
||||||
|
@ -21,6 +21,8 @@ import com.unciv.models.tilesets.TileSetCache
|
|||||||
import com.unciv.models.translations.TranslationFileWriter
|
import com.unciv.models.translations.TranslationFileWriter
|
||||||
import com.unciv.models.translations.Translations
|
import com.unciv.models.translations.Translations
|
||||||
import com.unciv.models.translations.tr
|
import com.unciv.models.translations.tr
|
||||||
|
import com.unciv.ui.audio.MusicController
|
||||||
|
import com.unciv.ui.audio.MusicTrackChooserFlags
|
||||||
import com.unciv.ui.civilopedia.FormattedLine
|
import com.unciv.ui.civilopedia.FormattedLine
|
||||||
import com.unciv.ui.civilopedia.MarkupRenderer
|
import com.unciv.ui.civilopedia.MarkupRenderer
|
||||||
import com.unciv.ui.civilopedia.SimpleCivilopediaText
|
import com.unciv.ui.civilopedia.SimpleCivilopediaText
|
||||||
@ -30,6 +32,8 @@ import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip
|
|||||||
import com.unciv.ui.worldscreen.WorldScreen
|
import com.unciv.ui.worldscreen.WorldScreen
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
|
import kotlin.math.floor
|
||||||
|
import kotlin.math.roundToInt
|
||||||
import com.badlogic.gdx.utils.Array as GdxArray
|
import com.badlogic.gdx.utils.Array as GdxArray
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -207,11 +211,12 @@ class OptionsPopup(val previousScreen: CameraStageBaseScreen) : Popup(previousSc
|
|||||||
|
|
||||||
addSoundEffectsVolumeSlider()
|
addSoundEffectsVolumeSlider()
|
||||||
|
|
||||||
val musicLocation = Gdx.files.local(previousScreen.game.musicLocation)
|
if (previousScreen.game.musicController.isMusicAvailable()) {
|
||||||
if (musicLocation.exists())
|
|
||||||
addMusicVolumeSlider()
|
addMusicVolumeSlider()
|
||||||
else
|
addMusicPauseSlider()
|
||||||
addDownloadMusic(musicLocation)
|
} else {
|
||||||
|
addDownloadMusic()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMultiplayerTab(): Table = Table(CameraStageBaseScreen.skin).apply {
|
private fun getMultiplayerTab(): Table = Table(CameraStageBaseScreen.skin).apply {
|
||||||
@ -247,7 +252,7 @@ class OptionsPopup(val previousScreen: CameraStageBaseScreen) : Popup(previousSc
|
|||||||
addYesNoRow("Enable portrait orientation", settings.allowAndroidPortrait) {
|
addYesNoRow("Enable portrait orientation", settings.allowAndroidPortrait) {
|
||||||
settings.allowAndroidPortrait = it
|
settings.allowAndroidPortrait = it
|
||||||
// Note the following might close the options screen indirectly and delayed
|
// Note the following might close the options screen indirectly and delayed
|
||||||
previousScreen.game.limitOrientationsHelper!!.allowPortrait(it)
|
previousScreen.game.limitOrientationsHelper.allowPortrait(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -392,7 +397,7 @@ class OptionsPopup(val previousScreen: CameraStageBaseScreen) : Popup(previousSc
|
|||||||
private fun Table.addSoundEffectsVolumeSlider() {
|
private fun Table.addSoundEffectsVolumeSlider() {
|
||||||
add("Sound effects volume".tr()).left().fillX()
|
add("Sound effects volume".tr()).left().fillX()
|
||||||
|
|
||||||
val soundEffectsVolumeSlider = UncivSlider(0f, 1.0f, 0.1f,
|
val soundEffectsVolumeSlider = UncivSlider(0f, 1.0f, 0.05f,
|
||||||
initial = settings.soundEffectsVolume
|
initial = settings.soundEffectsVolume
|
||||||
) {
|
) {
|
||||||
settings.soundEffectsVolume = it
|
settings.soundEffectsVolume = it
|
||||||
@ -404,24 +409,55 @@ class OptionsPopup(val previousScreen: CameraStageBaseScreen) : Popup(previousSc
|
|||||||
private fun Table.addMusicVolumeSlider() {
|
private fun Table.addMusicVolumeSlider() {
|
||||||
add("Music volume".tr()).left().fillX()
|
add("Music volume".tr()).left().fillX()
|
||||||
|
|
||||||
val musicVolumeSlider = UncivSlider(0f, 1.0f, 0.1f,
|
val musicVolumeSlider = UncivSlider(0f, 1.0f, 0.05f,
|
||||||
initial = settings.musicVolume,
|
initial = settings.musicVolume,
|
||||||
sound = UncivSound.Silent
|
sound = UncivSound.Silent
|
||||||
) {
|
) {
|
||||||
settings.musicVolume = it
|
settings.musicVolume = it
|
||||||
settings.save()
|
settings.save()
|
||||||
|
|
||||||
val music = previousScreen.game.music
|
val music = previousScreen.game.musicController
|
||||||
if (music == null) // restart music, if it was off at the app start
|
music.setVolume(it)
|
||||||
thread(name = "Music") { previousScreen.game.startMusic() }
|
if (!music.isPlaying())
|
||||||
|
music.chooseTrack(flags = EnumSet.of(MusicTrackChooserFlags.PlayDefaultFile, MusicTrackChooserFlags.PlaySingle))
|
||||||
music?.volume = 0.4f * it
|
|
||||||
}
|
}
|
||||||
musicVolumeSlider.value = settings.musicVolume
|
|
||||||
add(musicVolumeSlider).pad(5f).row()
|
add(musicVolumeSlider).pad(5f).row()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Table.addDownloadMusic(musicLocation: FileHandle) {
|
private fun Table.addMusicPauseSlider() {
|
||||||
|
val music = previousScreen.game.musicController
|
||||||
|
|
||||||
|
// map to/from 0-1-2..10-12-14..30-35-40..60-75-90-105-120
|
||||||
|
fun posToLength(pos: Float): Float = when (pos) {
|
||||||
|
in 0f..10f -> pos
|
||||||
|
in 11f..20f -> pos * 2f - 10f
|
||||||
|
in 21f..26f -> pos * 5f - 70f
|
||||||
|
else -> pos * 15f - 330f
|
||||||
|
}
|
||||||
|
fun lengthToPos(length: Float): Float = floor(when (length) {
|
||||||
|
in 0f..10f -> length
|
||||||
|
in 11f..30f -> (length + 10f) / 2f
|
||||||
|
in 31f..60f -> (length + 10f) / 5f
|
||||||
|
else -> (length + 330f) / 15f
|
||||||
|
})
|
||||||
|
val getTipText: (Float)->String = {
|
||||||
|
"%.0f".format(posToLength(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
add("Pause between tracks".tr()).left().fillX()
|
||||||
|
|
||||||
|
val pauseLengthSlider = UncivSlider(0f, 30f, 1f,
|
||||||
|
initial = lengthToPos(music.silenceLength),
|
||||||
|
sound = UncivSound.Silent,
|
||||||
|
getTipText = getTipText
|
||||||
|
) {
|
||||||
|
music.silenceLength = posToLength(it)
|
||||||
|
settings.pauseBetweenTracks = music.silenceLength.toInt()
|
||||||
|
}
|
||||||
|
add(pauseLengthSlider).pad(5f).row()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Table.addDownloadMusic() {
|
||||||
val downloadMusicButton = "Download music".toTextButton()
|
val downloadMusicButton = "Download music".toTextButton()
|
||||||
add(downloadMusicButton).colspan(2).row()
|
add(downloadMusicButton).colspan(2).row()
|
||||||
val errorTable = Table()
|
val errorTable = Table()
|
||||||
@ -435,11 +471,10 @@ class OptionsPopup(val previousScreen: CameraStageBaseScreen) : Popup(previousSc
|
|||||||
// So the whole game doesn't get stuck while downloading the file
|
// So the whole game doesn't get stuck while downloading the file
|
||||||
thread(name = "Music") {
|
thread(name = "Music") {
|
||||||
try {
|
try {
|
||||||
val file = DropBox.downloadFile("/Music/thatched-villagers.mp3")
|
previousScreen.game.musicController.downloadDefaultFile()
|
||||||
musicLocation.write(file, false)
|
|
||||||
Gdx.app.postRunnable {
|
Gdx.app.postRunnable {
|
||||||
tabs.replacePage("Sound", getSoundTab())
|
tabs.replacePage("Sound", getSoundTab())
|
||||||
previousScreen.game.startMusic()
|
previousScreen.game.musicController.chooseTrack(flags = EnumSet.of(MusicTrackChooserFlags.PlayDefaultFile, MusicTrackChooserFlags.PlaySingle))
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
Gdx.app.postRunnable {
|
Gdx.app.postRunnable {
|
||||||
|
@ -14,6 +14,8 @@ import com.unciv.ui.worldscreen.WorldScreen
|
|||||||
class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen) {
|
class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen) {
|
||||||
init {
|
init {
|
||||||
defaults().fillX()
|
defaults().fillX()
|
||||||
|
worldScreen.game.musicController.pause()
|
||||||
|
|
||||||
addButton("Main menu") { worldScreen.game.setScreen(MainMenuScreen()) }
|
addButton("Main menu") { worldScreen.game.setScreen(MainMenuScreen()) }
|
||||||
addButton("Civilopedia") { worldScreen.game.setScreen(CivilopediaScreen(worldScreen.gameInfo.ruleSet)) }
|
addButton("Civilopedia") { worldScreen.game.setScreen(CivilopediaScreen(worldScreen.gameInfo.ruleSet)) }
|
||||||
addButton("Save game") { worldScreen.game.setScreen(SaveGameScreen(worldScreen.gameInfo)) }
|
addButton("Save game") { worldScreen.game.setScreen(SaveGameScreen(worldScreen.gameInfo)) }
|
||||||
@ -30,8 +32,11 @@ class WorldScreenMenuPopup(val worldScreen: WorldScreen) : Popup(worldScreen) {
|
|||||||
addButton("Options") { worldScreen.openOptionsPopup() }
|
addButton("Options") { worldScreen.openOptionsPopup() }
|
||||||
addButton("Community") {
|
addButton("Community") {
|
||||||
close()
|
close()
|
||||||
WorldScreenCommunityPopup(worldScreen).open(force = true) }
|
WorldScreenCommunityPopup(worldScreen).open(force = true)
|
||||||
addCloseButton()
|
}
|
||||||
|
addCloseButton {
|
||||||
|
worldScreen.game.musicController.resume()
|
||||||
|
}
|
||||||
pack()
|
pack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user