diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index c9c5f833cd..e91eb7a002 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -344,14 +344,18 @@ object GameStarter { for (player in chosenPlayers) { val civ = Civilization(player.chosenCiv) - if (player.chosenCiv in usedMajorCivs) { - for (tech in startingTechs) - civ.tech.techsResearched.add(tech.name) // can't be .addTechnology because the civInfo isn't assigned yet - civ.playerType = player.playerType - civ.playerId = player.playerId - } else { - if (!civ.cityStateFunctions.initCityState(ruleset, newGameParameters.startingEra, unusedMajorCivs)) - continue + when (player.chosenCiv) { + Constants.spectator -> + civ.playerType = player.playerType + in usedMajorCivs -> { + for (tech in startingTechs) + civ.tech.techsResearched.add(tech.name) // can't be .addTechnology because the civInfo isn't assigned yet + civ.playerType = player.playerType + civ.playerId = player.playerId + } + else -> + if (!civ.cityStateFunctions.initCityState(ruleset, newGameParameters.startingEra, unusedMajorCivs)) + continue } gameInfo.civilizations.add(civ) } diff --git a/core/src/com/unciv/logic/civilization/PlayerType.kt b/core/src/com/unciv/logic/civilization/PlayerType.kt index 7a034fb896..b8c20a0aa6 100644 --- a/core/src/com/unciv/logic/civilization/PlayerType.kt +++ b/core/src/com/unciv/logic/civilization/PlayerType.kt @@ -4,5 +4,6 @@ import com.unciv.logic.IsPartOfGameInfoSerialization enum class PlayerType : IsPartOfGameInfoSerialization { AI, - Human + Human; + fun toggle() = if (this == AI) Human else AI } diff --git a/core/src/com/unciv/models/ruleset/nation/Nation.kt b/core/src/com/unciv/models/ruleset/nation/Nation.kt index d26beee37c..ee8fbb6b05 100644 --- a/core/src/com/unciv/models/ruleset/nation/Nation.kt +++ b/core/src/com/unciv/models/ruleset/nation/Nation.kt @@ -1,4 +1,4 @@ - package com.unciv.models.ruleset.nation +package com.unciv.models.ruleset.nation import com.badlogic.gdx.graphics.Color import com.unciv.Constants @@ -15,10 +15,10 @@ import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen.Companion.showRe import com.unciv.ui.screens.civilopediascreen.FormattedLine import kotlin.math.pow - class Nation : RulesetObject() { +class Nation : RulesetObject() { var leaderName = "" - fun getLeaderDisplayName() = if (isCityState) name - else "[$leaderName] of [$name]" + fun getLeaderDisplayName() = if (isCityState || isSpectator) name + else "[$leaderName] of [$name]" val style = "" fun getStyleOrCivName() = style.ifEmpty { name } @@ -36,7 +36,6 @@ import kotlin.math.pow lateinit var outerColor: List var uniqueName = "" - override fun getUniqueTarget() = UniqueTarget.Nation var uniqueText = "" var innerColor: List? = null var startBias = ArrayList() @@ -52,6 +51,10 @@ import kotlin.math.pow var favoredReligion: String? = null + var cities: ArrayList = arrayListOf() + + override fun getUniqueTarget() = UniqueTarget.Nation + @Transient private lateinit var outerColorObject: Color fun getOuterColor(): Color = outerColorObject @@ -84,8 +87,6 @@ import kotlin.math.pow ignoreHillMovementCost = uniques.contains("Units ignore terrain costs when moving into any tile with Hills") } - var cities: ArrayList = arrayListOf() - override fun makeLink() = "Nation/$name" override fun getSortGroup(ruleset: Ruleset) = when { @@ -294,38 +295,39 @@ import kotlin.math.pow } } - fun getContrastRatio() = getContrastRatio(getInnerColor(), getOuterColor()) + fun getContrastRatio() = getContrastRatio(getInnerColor(), getOuterColor()) - fun matchesFilter(filter: String): Boolean { - return when (filter) { - "All" -> true - name -> true - "Major" -> isMajorCiv - "CityState" -> isCityState - else -> uniques.contains(filter) - } - } - } + fun matchesFilter(filter: String): Boolean { + return when (filter) { + "All" -> true + name -> true + "Major" -> isMajorCiv + "CityState" -> isCityState + else -> uniques.contains(filter) + } + } +} - /** All defined by https://www.w3.org/TR/WCAG20/#relativeluminancedef */ - fun getRelativeLuminance(color:Color):Double{ - fun getRelativeChannelLuminance(channel:Float):Double = - if (channel < 0.03928) channel / 12.92 - else ((channel + 0.055) / 1.055).pow(2.4) +/** All defined by https://www.w3.org/TR/WCAG20/#relativeluminancedef */ +fun getRelativeLuminance(color: Color): Double { + fun getRelativeChannelLuminance(channel: Float): Double = + if (channel < 0.03928) channel / 12.92 + else ((channel + 0.055) / 1.055).pow(2.4) - val R = getRelativeChannelLuminance(color.r) - val G = getRelativeChannelLuminance(color.g) - val B = getRelativeChannelLuminance(color.b) + val R = getRelativeChannelLuminance(color.r) + val G = getRelativeChannelLuminance(color.g) + val B = getRelativeChannelLuminance(color.b) - return 0.2126 * R + 0.7152 * G + 0.0722 * B - } + return 0.2126 * R + 0.7152 * G + 0.0722 * B +} - /** https://www.w3.org/TR/WCAG20/#contrast-ratiodef */ - fun getContrastRatio(color1:Color, color2:Color): Double { // ratio can range from 1 to 21 - val innerColorLuminance = getRelativeLuminance(color1) - val outerColorLuminance = getRelativeLuminance(color2) +/** https://www.w3.org/TR/WCAG20/#contrast-ratiodef */ +fun getContrastRatio(color1: Color, color2: Color): Double { // ratio can range from 1 to 21 + val innerColorLuminance = getRelativeLuminance(color1) + val outerColorLuminance = getRelativeLuminance(color2) - return if (innerColorLuminance > outerColorLuminance) (innerColorLuminance + 0.05) / (outerColorLuminance + 0.05) - else (outerColorLuminance + 0.05) / (innerColorLuminance + 0.05) - } + return if (innerColorLuminance > outerColorLuminance) + (innerColorLuminance + 0.05) / (outerColorLuminance + 0.05) + else (outerColorLuminance + 0.05) / (innerColorLuminance + 0.05) +} diff --git a/core/src/com/unciv/ui/components/extensions/Scene2dExtensions.kt b/core/src/com/unciv/ui/components/extensions/Scene2dExtensions.kt index caf7f3d71e..ac7f77515b 100644 --- a/core/src/com/unciv/ui/components/extensions/Scene2dExtensions.kt +++ b/core/src/com/unciv/ui/components/extensions/Scene2dExtensions.kt @@ -48,15 +48,15 @@ private class RestorableTextButtonStyle( val restoreStyle: ButtonStyle ) : TextButtonStyle(baseStyle) -/** Disable a [Button] by setting its [touchable][Button.touchable] and [color][Button.color] properties. */ +/** Disable a [Button] by setting its [touchable][Button.touchable] and [style][Button.style] properties. */ fun Button.disable() { touchable = Touchable.disabled val oldStyle = style + if (oldStyle is RestorableTextButtonStyle) return val disabledStyle = BaseScreen.skin.get("disabled", TextButtonStyle::class.java) - if (oldStyle !is RestorableTextButtonStyle) - style = RestorableTextButtonStyle(disabledStyle, oldStyle) + style = RestorableTextButtonStyle(disabledStyle, oldStyle) } -/** Enable a [Button] by setting its [touchable][Button.touchable] and [color][Button.color] properties. */ +/** Enable a [Button] by setting its [touchable][Button.touchable] and [style][Button.style] properties. */ fun Button.enable() { val oldStyle = style if (oldStyle is RestorableTextButtonStyle) { @@ -64,7 +64,7 @@ fun Button.enable() { } touchable = Touchable.enabled } -/** Enable or disable a [Button] by setting its [touchable][Button.touchable] and [color][Button.color] properties, +/** Enable or disable a [Button] by setting its [touchable][Button.touchable] and [style][Button.style] properties, * or returns the corresponding state. * * Do not confuse with Gdx' builtin [isDisabled][Button.isDisabled] property, diff --git a/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorEditSubTabs.kt b/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorEditSubTabs.kt index 65656545ae..c89e8c5254 100644 --- a/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorEditSubTabs.kt +++ b/core/src/com/unciv/ui/screens/mapeditorscreen/tabs/MapEditorEditSubTabs.kt @@ -299,7 +299,7 @@ class MapEditorEditStartsTab( } private fun allowedNations() = ruleset.nations.values.asSequence() - .filter { it.name !in disallowNations } + .filter { it.name !in disallowNations && !it.hasUnique(UniqueType.CityStateDeprecated) } private fun getNations() = allowedNations() .sortedWith(compareBy{ it.isCityState }.thenBy(collator) { it.name.tr() }) .map { FormattedLine("[${it.name}] starting location", it.name, "Nation/${it.name}", size = 24) } diff --git a/core/src/com/unciv/ui/screens/newgamescreen/GameOptionsTable.kt b/core/src/com/unciv/ui/screens/newgamescreen/GameOptionsTable.kt index 2552b530ae..d44443a24e 100644 --- a/core/src/com/unciv/ui/screens/newgamescreen/GameOptionsTable.kt +++ b/core/src/com/unciv/ui/screens/newgamescreen/GameOptionsTable.kt @@ -5,8 +5,11 @@ import com.badlogic.gdx.scenes.scene2d.Touchable import com.badlogic.gdx.scenes.scene2d.ui.CheckBox import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.utils.Align +import com.unciv.Constants import com.unciv.UncivGame +import com.unciv.logic.civilization.PlayerType import com.unciv.models.metadata.GameParameters +import com.unciv.models.metadata.Player import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.nation.Nation import com.unciv.models.ruleset.unique.UniqueType @@ -31,11 +34,13 @@ import com.unciv.ui.popups.Popup import com.unciv.ui.popups.ToastPopup import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.multiplayerscreens.MultiplayerHelpers +import kotlin.reflect.KMutableProperty0 class GameOptionsTable( private val previousScreen: IPreviousScreen, private val isPortrait: Boolean = false, - private val updatePlayerPickerTable: (desiredCiv:String)->Unit + private val updatePlayerPickerTable: (desiredCiv: String) -> Unit, + private val updatePlayerPickerRandomLabel: () -> Unit ) : Table(BaseScreen.skin) { var gameParameters = previousScreen.gameSetupInfo.gameParameters val ruleset = previousScreen.ruleset @@ -77,12 +82,10 @@ class GameOptionsTable( if (turnSlider != null) add(turnSlider).padTop(10f).row() if (gameParameters.randomNumberOfPlayers) { - addMinPlayersSlider() - addMaxPlayersSlider() + addMinMaxPlayersSliders() } if (gameParameters.randomNumberOfCityStates) { - addMinCityStatesSlider() - addMaxCityStatesSlider() + addMinMaxCityStatesSliders() } else { addCityStatesSlider() } @@ -204,7 +207,7 @@ class GameOptionsTable( add(button) } - private fun numberOfPlayable() = ruleset.nations.values.count { + private fun numberOfMajorCivs() = ruleset.nations.values.count { it.isMajorCiv } @@ -218,64 +221,101 @@ class GameOptionsTable( private fun Table.addRandomPlayersCheckbox() = addCheckbox("Random number of Civilizations", gameParameters.randomNumberOfPlayers) - { - gameParameters.randomNumberOfPlayers = it + {newRandomNumberOfPlayers -> + gameParameters.randomNumberOfPlayers = newRandomNumberOfPlayers + if (newRandomNumberOfPlayers) { + // remove all random AI from player picker + val newPlayers = gameParameters.players.asSequence() + .filterNot { it.playerType == PlayerType.AI && it.chosenCiv == Constants.random } + .toCollection(ArrayList(gameParameters.players.size)) + if (newPlayers.size != gameParameters.players.size) { + gameParameters.players = newPlayers + updatePlayerPickerTable("") + } + } else { + // Fill up player picker with random AI until previously active min reached + val additionalRandom = gameParameters.minNumberOfPlayers - gameParameters.players.size + if (additionalRandom > 0) { + repeat(additionalRandom) { + gameParameters.players.add(Player(Constants.random)) + } + updatePlayerPickerTable("") + } + } update() // To see the new sliders } private fun Table.addRandomCityStatesCheckbox() = addCheckbox("Random number of City-States", gameParameters.randomNumberOfCityStates) { - gameParameters.randomNumberOfCityStates = it + gameParameters.run { + randomNumberOfCityStates = it + if (it) { + if (numberOfCityStates > maxNumberOfCityStates) + maxNumberOfCityStates = numberOfCityStates + if (numberOfCityStates < minNumberOfCityStates) + minNumberOfCityStates = numberOfCityStates + } else { + if (numberOfCityStates > maxNumberOfCityStates) + numberOfCityStates = maxNumberOfCityStates + if (numberOfCityStates < minNumberOfCityStates) + numberOfCityStates = minNumberOfCityStates + } + } update() // To see the changed sliders } - private fun Table.addMinPlayersSlider() { - val playableAvailable = numberOfPlayable() - if (playableAvailable == 0) return + private fun Table.addLinkedMinMaxSliders( + minValue: Int, maxValue: Int, + minText: String, maxText: String, + minField: KMutableProperty0, + maxField: KMutableProperty0, + onChangeCallback: (() -> Unit)? = null + ) { + if (maxValue < minValue) return - add("{Min number of Civilizations}:".toLabel()).left().expandX() - val slider = UncivSlider(2f, playableAvailable.toFloat(), 1f, initial = gameParameters.minNumberOfPlayers.toFloat()) { - gameParameters.minNumberOfPlayers = it.toInt() + @Suppress("JoinDeclarationAndAssignment") // it's a forward declaration! + lateinit var maxSlider: UncivSlider // lateinit safe because the closure won't use it until the user operates a slider + val minSlider = UncivSlider(minValue.toFloat(), maxValue.toFloat(), 1f, initial = minField.get().toFloat()) { + val newMin = it.toInt() + minField.set(newMin) + if (newMin > maxSlider.value.toInt()) { + maxSlider.value = it + maxField.set(newMin) + } + onChangeCallback?.invoke() } - slider.isDisabled = locked - add(slider).padTop(10f).row() + minSlider.isDisabled = locked + maxSlider = UncivSlider(minValue.toFloat(), maxValue.toFloat(), 1f, initial = maxField.get().toFloat()) { + val newMax = it.toInt() + maxField.set(newMax) + if (newMax < minSlider.value.toInt()) { + minSlider.value = it + minField.set(newMax) + } + onChangeCallback?.invoke() + } + maxSlider.isDisabled = locked + + add(minText.toLabel()).left().expandX() + add(minSlider).padTop(10f).row() + add(maxText.toLabel()).left().expandX() + add(maxSlider).padTop(10f).row() } - private fun Table.addMaxPlayersSlider() { - val playableAvailable = numberOfPlayable() - if (playableAvailable == 0) return - - add("{Max number of Civilizations}:".toLabel()).left().expandX() - val slider = UncivSlider(2f, playableAvailable.toFloat(), 1f, initial = gameParameters.maxNumberOfPlayers.toFloat()) { - gameParameters.maxNumberOfPlayers = it.toInt() - } - slider.isDisabled = locked - add(slider).padTop(10f).row() + private fun Table.addMinMaxPlayersSliders() { + addLinkedMinMaxSliders(2, numberOfMajorCivs(), + "{Min number of Civilizations}:", "{Max number of Civilizations}:", + gameParameters::minNumberOfPlayers, gameParameters::maxNumberOfPlayers, + updatePlayerPickerRandomLabel + ) } - private fun Table.addMinCityStatesSlider() { - val cityStatesAvailable = numberOfCityStates() - if (cityStatesAvailable == 0) return - - add("{Min number of City-States}:".toLabel()).left().expandX() - val slider = UncivSlider(0f, cityStatesAvailable.toFloat(), 1f, initial = gameParameters.minNumberOfCityStates.toFloat()) { - gameParameters.minNumberOfCityStates = it.toInt() - } - slider.isDisabled = locked - add(slider).padTop(10f).row() - } - - private fun Table.addMaxCityStatesSlider() { - val cityStatesAvailable = numberOfCityStates() - if (cityStatesAvailable == 0) return - - add("{Max number of City-States}:".toLabel()).left().expandX() - val slider = UncivSlider(0f, cityStatesAvailable.toFloat(), 1f, initial = gameParameters.maxNumberOfCityStates.toFloat()) { - gameParameters.maxNumberOfCityStates = it.toInt() - } - slider.isDisabled = locked - add(slider).padTop(10f).row() + private fun Table.addMinMaxCityStatesSliders() { + addLinkedMinMaxSliders( 0, numberOfCityStates(), + "{Min number of City-States}:", "{Max number of City-States}:", + gameParameters::minNumberOfCityStates, gameParameters::maxNumberOfCityStates + ) } private fun Table.addCityStatesSlider() { diff --git a/core/src/com/unciv/ui/screens/newgamescreen/NewGameScreen.kt b/core/src/com/unciv/ui/screens/newgamescreen/NewGameScreen.kt index e886abedcd..e95fbbdaf4 100644 --- a/core/src/com/unciv/ui/screens/newgamescreen/NewGameScreen.kt +++ b/core/src/com/unciv/ui/screens/newgamescreen/NewGameScreen.kt @@ -11,22 +11,15 @@ import com.unciv.UncivGame import com.unciv.logic.GameInfo import com.unciv.logic.GameStarter import com.unciv.logic.IdChecker -import com.unciv.logic.files.MapSaver import com.unciv.logic.civilization.PlayerType +import com.unciv.logic.files.MapSaver import com.unciv.logic.map.MapGeneratedMainType import com.unciv.logic.multiplayer.OnlineMultiplayer import com.unciv.logic.multiplayer.storage.FileStorageRateLimitReached import com.unciv.models.metadata.GameSetupInfo import com.unciv.models.ruleset.RulesetCache import com.unciv.models.translations.tr -import com.unciv.ui.images.ImageGetter -import com.unciv.ui.screens.pickerscreens.PickerScreen -import com.unciv.ui.popups.ConfirmPopup -import com.unciv.ui.popups.Popup -import com.unciv.ui.popups.ToastPopup -import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.components.ExpanderTab -import com.unciv.ui.screens.basescreen.RecreateOnResize import com.unciv.ui.components.extensions.addSeparator import com.unciv.ui.components.extensions.addSeparatorVertical import com.unciv.ui.components.extensions.disable @@ -35,12 +28,19 @@ import com.unciv.ui.components.extensions.onClick import com.unciv.ui.components.extensions.pad import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toTextButton +import com.unciv.ui.images.ImageGetter +import com.unciv.ui.popups.ConfirmPopup +import com.unciv.ui.popups.Popup +import com.unciv.ui.popups.ToastPopup +import com.unciv.ui.screens.basescreen.BaseScreen +import com.unciv.ui.screens.basescreen.RecreateOnResize +import com.unciv.ui.screens.pickerscreens.PickerScreen import com.unciv.utils.Log import com.unciv.utils.concurrency.Concurrency import com.unciv.utils.concurrency.launchOnGLThread import kotlinx.coroutines.coroutineScope import java.net.URL -import java.util.* +import java.util.UUID import com.unciv.ui.components.AutoScrollPane as ScrollPane class NewGameScreen( @@ -54,6 +54,8 @@ class NewGameScreen( private val mapOptionsTable: MapOptionsTable init { + val isPortrait = isNarrowerThan4to3() + updateRuleset() // must come before playerPickerTable so mod nations from fromSettings // Has to be initialized before the mapOptionsTable, since the mapOptionsTable refers to it on init @@ -65,13 +67,17 @@ class NewGameScreen( playerPickerTable = PlayerPickerTable( this, gameSetupInfo.gameParameters, - if (isNarrowerThan4to3()) stage.width - 20f else 0f + if (isPortrait) stage.width - 20f else 0f + ) + newGameOptionsTable = GameOptionsTable( + this, isPortrait, + updatePlayerPickerTable = { desiredCiv -> playerPickerTable.update(desiredCiv) }, + updatePlayerPickerRandomLabel = { playerPickerTable.updateRandomNumberLabel() } ) - newGameOptionsTable = GameOptionsTable(this, isNarrowerThan4to3()) { desiredCiv: String -> playerPickerTable.update(desiredCiv) } mapOptionsTable = MapOptionsTable(this) setDefaultCloseAction() - if (isNarrowerThan4to3()) initPortrait() + if (isPortrait) initPortrait() else initLandscape() pickerPane.bottomTable.background = skinStrings.getUiBackground("NewGameScreen/BottomTable", tintColor = skinStrings.skinConfig.clearColor) @@ -94,107 +100,109 @@ class NewGameScreen( rightSideButton.enable() rightSideButton.setText("Start game!".tr()) - rightSideButton.onClick { - if (gameSetupInfo.gameParameters.isOnlineMultiplayer) { - if (!checkConnectionToMultiplayerServer()) { - val noInternetConnectionPopup = Popup(this) - val label = if (OnlineMultiplayer.usesCustomServer()) "Couldn't connect to Multiplayer Server!" else "Couldn't connect to Dropbox!" - noInternetConnectionPopup.addGoodSizedLabel(label.tr()).row() - noInternetConnectionPopup.addCloseButton() - noInternetConnectionPopup.open() - return@onClick - } + rightSideButton.onClick(this::onStartGameClicked) + } - for (player in gameSetupInfo.gameParameters.players.filter { it.playerType == PlayerType.Human }) { - try { - UUID.fromString(IdChecker.checkAndReturnPlayerUuid(player.playerId)) - } catch (ex: Exception) { - val invalidPlayerIdPopup = Popup(this) - invalidPlayerIdPopup.addGoodSizedLabel("Invalid player ID!".tr()).row() - invalidPlayerIdPopup.addCloseButton() - invalidPlayerIdPopup.open() - return@onClick - } - } + private fun onStartGameClicked() { + if (gameSetupInfo.gameParameters.isOnlineMultiplayer) { + if (!checkConnectionToMultiplayerServer()) { + val noInternetConnectionPopup = Popup(this) + val label = if (OnlineMultiplayer.usesCustomServer()) "Couldn't connect to Multiplayer Server!" else "Couldn't connect to Dropbox!" + noInternetConnectionPopup.addGoodSizedLabel(label.tr()).row() + noInternetConnectionPopup.addCloseButton() + noInternetConnectionPopup.open() + return + } - if (!gameSetupInfo.gameParameters.anyoneCanSpectate) { - if (gameSetupInfo.gameParameters.players.none { it.playerId == UncivGame.Current.settings.multiplayer.userId }) { - val notAllowedToSpectate = Popup(this) - notAllowedToSpectate.addGoodSizedLabel("You are not allowed to spectate!".tr()).row() - notAllowedToSpectate.addCloseButton() - notAllowedToSpectate.open() - return@onClick - } + for (player in gameSetupInfo.gameParameters.players.filter { it.playerType == PlayerType.Human }) { + try { + UUID.fromString(IdChecker.checkAndReturnPlayerUuid(player.playerId)) + } catch (ex: Exception) { + val invalidPlayerIdPopup = Popup(this) + invalidPlayerIdPopup.addGoodSizedLabel("Invalid player ID!".tr()).row() + invalidPlayerIdPopup.addCloseButton() + invalidPlayerIdPopup.open() + return } } - if (gameSetupInfo.gameParameters.players.none { - it.playerType == PlayerType.Human && - // do not allow multiplayer with only remote spectator(s) and AI(s) - non-MP that works - !(it.chosenCiv == Constants.spectator && gameSetupInfo.gameParameters.isOnlineMultiplayer && - it.playerId != UncivGame.Current.settings.multiplayer.userId) - }) { - val noHumanPlayersPopup = Popup(this) - noHumanPlayersPopup.addGoodSizedLabel("No human players selected!".tr()).row() - noHumanPlayersPopup.addCloseButton() - noHumanPlayersPopup.open() - return@onClick - } - - if (gameSetupInfo.gameParameters.victoryTypes.isEmpty()) { - val noVictoryTypesPopup = Popup(this) - noVictoryTypesPopup.addGoodSizedLabel("No victory conditions were selected!".tr()).row() - noVictoryTypesPopup.addCloseButton() - noVictoryTypesPopup.open() - return@onClick - } - - Gdx.input.inputProcessor = null // remove input processing - nothing will be clicked! - - if (mapOptionsTable.mapTypeSelectBox.selected.value == MapGeneratedMainType.custom) { - val map = try { - MapSaver.loadMap(gameSetupInfo.mapFile!!) - } catch (ex: Throwable) { - Gdx.input.inputProcessor = stage - ToastPopup("Could not load map!", this) - return@onClick - } - - val rulesetIncompatibilities = map.getRulesetIncompatibility(ruleset) - if (rulesetIncompatibilities.isNotEmpty()) { - val incompatibleMap = Popup(this) - incompatibleMap.addGoodSizedLabel("Map is incompatible with the chosen ruleset!".tr()).row() - for(incompatibility in rulesetIncompatibilities) - incompatibleMap.addGoodSizedLabel(incompatibility).row() - incompatibleMap.addCloseButton() - incompatibleMap.open() - Gdx.input.inputProcessor = stage - return@onClick - } - } else { - // Generated map - check for sensible dimensions and if exceeded correct them and notify user - val mapSize = gameSetupInfo.mapParameters.mapSize - val message = mapSize.fixUndesiredSizes(gameSetupInfo.mapParameters.worldWrap) - if (message != null) { - ToastPopup( message, UncivGame.Current.screen!!, 4000 ) - with (mapOptionsTable.generatedMapOptionsTable) { - customMapSizeRadius.text = mapSize.radius.toString() - customMapWidth.text = mapSize.width.toString() - customMapHeight.text = mapSize.height.toString() - } - Gdx.input.inputProcessor = stage - return@onClick + if (!gameSetupInfo.gameParameters.anyoneCanSpectate) { + if (gameSetupInfo.gameParameters.players.none { it.playerId == UncivGame.Current.settings.multiplayer.userId }) { + val notAllowedToSpectate = Popup(this) + notAllowedToSpectate.addGoodSizedLabel("You are not allowed to spectate!".tr()).row() + notAllowedToSpectate.addCloseButton() + notAllowedToSpectate.open() + return } } + } - rightSideButton.disable() - rightSideButton.setText("Working...".tr()) + if (gameSetupInfo.gameParameters.players.none { + it.playerType == PlayerType.Human && + // do not allow multiplayer with only remote spectator(s) and AI(s) - non-MP that works + !(it.chosenCiv == Constants.spectator && gameSetupInfo.gameParameters.isOnlineMultiplayer && + it.playerId != UncivGame.Current.settings.multiplayer.userId) + }) { + val noHumanPlayersPopup = Popup(this) + noHumanPlayersPopup.addGoodSizedLabel("No human players selected!".tr()).row() + noHumanPlayersPopup.addCloseButton() + noHumanPlayersPopup.open() + return + } - setSkin() - // Creating a new game can take a while and we don't want ANRs - Concurrency.runOnNonDaemonThreadPool("NewGame") { - startNewGame() + if (gameSetupInfo.gameParameters.victoryTypes.isEmpty()) { + val noVictoryTypesPopup = Popup(this) + noVictoryTypesPopup.addGoodSizedLabel("No victory conditions were selected!".tr()).row() + noVictoryTypesPopup.addCloseButton() + noVictoryTypesPopup.open() + return + } + + Gdx.input.inputProcessor = null // remove input processing - nothing will be clicked! + + if (mapOptionsTable.mapTypeSelectBox.selected.value == MapGeneratedMainType.custom) { + val map = try { + MapSaver.loadMap(gameSetupInfo.mapFile!!) + } catch (ex: Throwable) { + Gdx.input.inputProcessor = stage + ToastPopup("Could not load map!", this) + return } + + val rulesetIncompatibilities = map.getRulesetIncompatibility(ruleset) + if (rulesetIncompatibilities.isNotEmpty()) { + val incompatibleMap = Popup(this) + incompatibleMap.addGoodSizedLabel("Map is incompatible with the chosen ruleset!".tr()).row() + for(incompatibility in rulesetIncompatibilities) + incompatibleMap.addGoodSizedLabel(incompatibility).row() + incompatibleMap.addCloseButton() + incompatibleMap.open() + Gdx.input.inputProcessor = stage + return + } + } else { + // Generated map - check for sensible dimensions and if exceeded correct them and notify user + val mapSize = gameSetupInfo.mapParameters.mapSize + val message = mapSize.fixUndesiredSizes(gameSetupInfo.mapParameters.worldWrap) + if (message != null) { + ToastPopup( message, UncivGame.Current.screen!!, 4000 ) + with (mapOptionsTable.generatedMapOptionsTable) { + customMapSizeRadius.text = mapSize.radius.toString() + customMapWidth.text = mapSize.width.toString() + customMapHeight.text = mapSize.height.toString() + } + Gdx.input.inputProcessor = stage + return + } + } + + rightSideButton.disable() + rightSideButton.setText("Working...".tr()) + + setSkin() + // Creating a new game can take a while and we don't want ANRs + Concurrency.runOnNonDaemonThreadPool("NewGame") { + startNewGame() } } diff --git a/core/src/com/unciv/ui/screens/newgamescreen/PlayerPickerTable.kt b/core/src/com/unciv/ui/screens/newgamescreen/PlayerPickerTable.kt index 81bd8d450d..d1878fafa7 100644 --- a/core/src/com/unciv/ui/screens/newgamescreen/PlayerPickerTable.kt +++ b/core/src/com/unciv/ui/screens/newgamescreen/PlayerPickerTable.kt @@ -11,6 +11,7 @@ import com.unciv.logic.IdChecker import com.unciv.logic.civilization.PlayerType import com.unciv.logic.multiplayer.FriendList import com.unciv.models.metadata.GameParameters +import com.unciv.models.metadata.GameSetupInfo import com.unciv.models.metadata.Player import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.nation.Nation @@ -19,6 +20,7 @@ import com.unciv.ui.audio.MusicMood import com.unciv.ui.audio.MusicTrackChooserFlags import com.unciv.ui.components.KeyCharAndCode import com.unciv.ui.components.UncivTextField +import com.unciv.ui.components.WrappableLabel import com.unciv.ui.components.extensions.* import com.unciv.ui.images.ImageGetter import com.unciv.ui.popups.Popup @@ -44,12 +46,13 @@ class PlayerPickerTable( blockWidth: Float = 0f ): Table() { val playerListTable = Table() - val civBlocksWidth = if(blockWidth <= 10f) previousScreen.stage.width / 3 - 5f else blockWidth + val civBlocksWidth = if (blockWidth <= 10f) previousScreen.stage.width / 3 - 5f else blockWidth + private var randomNumberLabel: WrappableLabel? = null - /** Locks player table for editing, currently unused, was previously used for scenarios and could be useful in the future.*/ + /** Locks player table for editing, currently unused, was previously used for scenarios and could be useful in the future. */ var locked = false - /** No random civilization is available, used during map editing.*/ + /** No random civilization is available, potentially used in the future during map editing. */ var noRandom = false private val friendList = FriendList() @@ -82,27 +85,47 @@ class PlayerPickerTable( for (player in gameParameters.players) { playerListTable.add(getPlayerTable(player)).width(civBlocksWidth).padBottom(20f).row() } + + val isRandomNumberOfPlayers = gameParameters.randomNumberOfPlayers + if (isRandomNumberOfPlayers) { + randomNumberLabel = WrappableLabel("", civBlocksWidth - 20f, Color.GOLD) + playerListTable.add(randomNumberLabel).fillX().pad(0f, 10f, 20f, 10f).row() + updateRandomNumberLabel() + } + if (!locked && gameParameters.players.size < gameBasics.nations.values.count { it.isMajorCiv }) { val addPlayerButton = "+".toLabel(Color.BLACK, 30) .apply { this.setAlignment(Align.center) } .surroundWithCircle(50f) .onClick { - var player = Player() // no random mode - add first not spectator civ if still available - if (noRandom) { + val player = if (noRandom || isRandomNumberOfPlayers) { val availableCiv = getAvailablePlayerCivs().firstOrNull() - if (availableCiv != null) player = Player(availableCiv.name) - // Spectators only Humans - else player = Player(Constants.spectator, PlayerType.Human) - } + if (availableCiv != null) Player(availableCiv.name) + // Spectators can only be Humans + else Player(Constants.spectator, PlayerType.Human) + } else Player() // normal: add random AI gameParameters.players.add(player) update() } playerListTable.add(addPlayerButton).pad(10f) } - // enable start game when more than 1 active player - val moreThanOnePlayer = 1 < gameParameters.players.count { it.chosenCiv != Constants.spectator } - (previousScreen as? PickerScreen)?.setRightSideButtonEnabled(moreThanOnePlayer) + + // enable start game when at least one human player and they're not alone + val humanPlayerCount = gameParameters.players.count { it.playerType == PlayerType.Human } + val isValid = humanPlayerCount >= 2 || humanPlayerCount >= 1 && isRandomNumberOfPlayers + (previousScreen as? PickerScreen)?.setRightSideButtonEnabled(isValid) + } + + fun updateRandomNumberLabel() { + randomNumberLabel?.run { + val text = "These [${gameParameters.players.size}] players will be adjusted to [${gameParameters.minNumberOfPlayers}" + + "]-[${gameParameters.maxNumberOfPlayers}] actual players by adding random AI's or by randomly omitting AI's." + wrap = false + align(Align.center) + setText(text.tr()) + wrap = true + } } /** @@ -145,88 +168,109 @@ class PlayerPickerTable( playerTable.add(nationTable).left() val playerTypeTextButton = player.playerType.name.toTextButton() + playerTable.add(playerTypeTextButton).width(100f).pad(5f).right() + fun updatePlayerTypeButtonEnabled() { + // This could be written much shorter with logical operators - I think this is readable + playerTypeTextButton.isEnabled = when { + // Can always change AI to Human + player.playerType == PlayerType.AI -> true + // we cannot change Spectator player to AI type, robots not allowed to spectate :( + player.chosenCiv == Constants.spectator -> false + // In randomNumberOfPlayers mode, don't let the user choose random AI's + gameParameters.randomNumberOfPlayers && player.chosenCiv == Constants.random -> false + else -> true + } + } + updatePlayerTypeButtonEnabled() + + nationTable.onClick { + if (locked) return@onClick + val noRandom = noRandom || + gameParameters.randomNumberOfPlayers && player.playerType == PlayerType.AI + popupNationPicker(player, noRandom) + updatePlayerTypeButtonEnabled() + } playerTypeTextButton.onClick { - if (player.playerType == PlayerType.AI) - player.playerType = PlayerType.Human - // we cannot change Spectator player to AI type, robots not allowed to spectate :( - else if (player.chosenCiv != Constants.spectator) - player.playerType = PlayerType.AI + player.playerType = player.playerType.toggle() update() } - playerTable.add(playerTypeTextButton).width(100f).pad(5f).right() + if (!locked) { - playerTable.add("-".toLabel(Color.BLACK, 30).apply { this.setAlignment(Align.center) } - .surroundWithCircle(40f) - .onClick { - gameParameters.players.remove(player) - update() - }).pad(5f).right().row() - } - if (gameParameters.isOnlineMultiplayer && player.playerType == PlayerType.Human) { - - val playerIdTextField = UncivTextField.create("Please input Player ID!", player.playerId) - playerTable.add(playerIdTextField).colspan(2).fillX().pad(5f) - val errorLabel = "✘".toLabel(Color.RED) - playerTable.add(errorLabel).pad(5f).row() - - fun onPlayerIdTextUpdated() { - try { - UUID.fromString(IdChecker.checkAndReturnPlayerUuid(playerIdTextField.text)) - player.playerId = playerIdTextField.text.trim() - errorLabel.apply { setText("✔");setFontColor(Color.GREEN) } - } catch (ex: Exception) { - errorLabel.apply { setText("✘");setFontColor(Color.RED) } + playerTable.add("-".toLabel(Color.BLACK, 30, Align.center) + .surroundWithCircle(40f) + .onClick { + gameParameters.players.remove(player) + update() } - } - onPlayerIdTextUpdated() - - playerIdTextField.addListener { onPlayerIdTextUpdated(); true } - val currentUserId = UncivGame.Current.settings.multiplayer.userId - val setCurrentUserButton = "Set current user".toTextButton() - setCurrentUserButton.onClick { - playerIdTextField.text = currentUserId - onPlayerIdTextUpdated() - } - playerTable.add(setCurrentUserButton).colspan(3).fillX().pad(5f).row() - - val copyFromClipboardButton = "Player ID from clipboard".toTextButton() - copyFromClipboardButton.onClick { - playerIdTextField.text = Gdx.app.clipboard.contents - onPlayerIdTextUpdated() - } - playerTable.add(copyFromClipboardButton).right().colspan(3).fillX().pad(5f).row() - - //check if friends list is empty before adding the select friend button - if (friendList.friendList.isNotEmpty()) { - val selectPlayerFromFriendsList = "Player ID from friends list".toTextButton() - selectPlayerFromFriendsList.onClick { - popupFriendPicker(player) - } - playerTable.add(selectPlayerFromFriendsList).left().colspan(3).fillX().pad(5f) - } + ).pad(5f).right() } + if (gameParameters.isOnlineMultiplayer && player.playerType == PlayerType.Human) + playerTable.addPlayerTableMultiplayerControls(player) + return playerTable } + private fun Table.addPlayerTableMultiplayerControls(player: Player) { + row() + + val playerIdTextField = UncivTextField.create("Please input Player ID!", player.playerId) + add(playerIdTextField).colspan(2).fillX().pad(5f) + val errorLabel = "✘".toLabel(Color.RED) + add(errorLabel).pad(5f).row() + + fun onPlayerIdTextUpdated() { + try { + UUID.fromString(IdChecker.checkAndReturnPlayerUuid(playerIdTextField.text)) + player.playerId = playerIdTextField.text.trim() + errorLabel.apply { setText("✔");setFontColor(Color.GREEN) } + } catch (ex: Exception) { + errorLabel.apply { setText("✘");setFontColor(Color.RED) } + } + } + onPlayerIdTextUpdated() + playerIdTextField.addListener { onPlayerIdTextUpdated(); true } + + val currentUserId = UncivGame.Current.settings.multiplayer.userId + val setCurrentUserButton = "Set current user".toTextButton() + setCurrentUserButton.onClick { + playerIdTextField.text = currentUserId + onPlayerIdTextUpdated() + } + add(setCurrentUserButton).colspan(3).fillX().pad(5f).row() + + val copyFromClipboardButton = "Player ID from clipboard".toTextButton() + copyFromClipboardButton.onClick { + playerIdTextField.text = Gdx.app.clipboard.contents + onPlayerIdTextUpdated() + } + add(copyFromClipboardButton).right().colspan(3).fillX().pad(5f).row() + + //check if friends list is empty before adding the select friend button + if (friendList.friendList.isNotEmpty()) { + val selectPlayerFromFriendsList = "Player ID from friends list".toTextButton() + selectPlayerFromFriendsList.onClick { + popupFriendPicker(player) + } + add(selectPlayerFromFriendsList).left().colspan(3).fillX().pad(5f) + } + } + /** - * Creates clickable icon and nation name for some [Player] - * as a [Table]. Clicking creates [popupNationPicker] to choose new nation. + * Creates clickable icon and nation name for some [Player]. * @param player [Player] for which generated * @return [Table] containing nation icon and name */ private fun getNationTable(player: Player): Table { val nationTable = Table() + val nationImageName = previousScreen.ruleset.nations[player.chosenCiv] val nationImage = - if (player.chosenCiv == Constants.random) + if (nationImageName == null) ImageGetter.getRandomNationPortrait(40f) - else ImageGetter.getNationPortrait(previousScreen.ruleset.nations[player.chosenCiv]!!, 40f) + else ImageGetter.getNationPortrait(nationImageName, 40f) nationTable.add(nationImage).pad(5f) nationTable.add(player.chosenCiv.toLabel()).pad(5f) nationTable.touchable = Touchable.enabled - nationTable.onClick { - if (!locked) popupNationPicker(player) - } return nationTable } @@ -247,8 +291,8 @@ class PlayerPickerTable( * ruleset and other players nation choice. * @param player current player */ - private fun popupNationPicker(player: Player) { - NationPickerPopup(this, player).open() + private fun popupNationPicker(player: Player, noRandom: Boolean) { + NationPickerPopup(this, player, noRandom).open() update() } @@ -288,7 +332,7 @@ class FriendSelectionPopup( screen: BaseScreen, ) : Popup(screen) { - val pickerPane = PickerPane() + private val pickerPane = PickerPane() private var selectedFriendId: String? = null init { @@ -327,7 +371,8 @@ class FriendSelectionPopup( private class NationPickerPopup( private val playerPicker: PlayerPickerTable, - private val player: Player + private val player: Player, + noRandom: Boolean ) : Popup(playerPicker.previousScreen as BaseScreen) { companion object { // These are used for the Close/OK buttons in the lower left/right corners: @@ -357,26 +402,23 @@ private class NationPickerPopup( nationDetailsScroll.setOverscroll(false, false) add(nationDetailsScroll).size(civBlocksWidth + 10f, partHeight) // Same here, see above - val randomNation = Nation().apply { - name = Constants.random - innerColor = listOf(255, 255, 255) - outerColor = listOf(0, 0, 0) - setTransients() - } - val nations = ArrayList() - if (!playerPicker.noRandom) nations += randomNation - val spectator = previousScreen.ruleset.nations[Constants.spectator] - if (spectator != null) nations += spectator - - nations += playerPicker.getAvailablePlayerCivs(player.chosenCiv) + val nationSequence = sequence { + if (!noRandom) yield(Nation().apply { + name = Constants.random + innerColor = listOf(255, 255, 255) + outerColor = listOf(0, 0, 0) + setTransients() + }) + val spectator = previousScreen.ruleset.nations[Constants.spectator] + if (spectator != null && player.playerType != PlayerType.AI) // only humans can spectate, sorry robots + yield(spectator) + } + playerPicker.getAvailablePlayerCivs(player.chosenCiv) .sortedWith(compareBy(UncivGame.Current.settings.getCollatorFromLocale()) { it.name.tr() }) + val nations = nationSequence.toCollection(ArrayList(previousScreen.ruleset.nations.size)) var nationListScrollY = 0f var currentY = 0f for (nation in nations) { - // only humans can spectate, sorry robots - if (player.playerType == PlayerType.AI && nation.isSpectator) - continue if (player.chosenCiv == nation.name) nationListScrollY = currentY val nationTable = NationTable(nation, civBlocksWidth, 0f) // no need for min height @@ -386,6 +428,10 @@ private class NationPickerPopup( nationTable.onClick { setNationDetails(nation) } + nationTable.onDoubleClick { + selectedNation = nation + returnSelected() + } if (player.chosenCiv == nation.name) setNationDetails(nation) } diff --git a/docs/Other/Civilization-related-JSON-files.md b/docs/Other/Civilization-related-JSON-files.md index 1d2938d33c..7aabd15eef 100644 --- a/docs/Other/Civilization-related-JSON-files.md +++ b/docs/Other/Civilization-related-JSON-files.md @@ -169,7 +169,7 @@ Each specialist can have the following attributes: | science | Integer | defaults to 0 | | faith | Integer | defaults to 0 | | color | List of 3 Integers | required | Color of the image for this specialist | -| greatPersonPoints | Object | defaults to none | Great person points generated by this specialist. Valid keys are the names of the great person(Great Scientist, Great Merachant, etc.), valid values are Integers (≥0) | +| greatPersonPoints | Object | defaults to none | Great person points generated by this specialist. Valid keys are the names of the great person(Great Scientist, Great Merachant, etc.), valid values are Integers (≥0) | ## Techs.json