Random Nations game starter - reactive UI (#9127)

* Random Nations game starter - reactive UI

* Random Nations game starter - reactive UI continued

* Random Nations game starter - reactive UI - patch
This commit is contained in:
SomeTroglodyte 2023-04-07 08:53:04 +02:00 committed by GitHub
parent 21510a8455
commit 36667d9d18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 396 additions and 295 deletions

View File

@ -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)
}

View File

@ -4,5 +4,6 @@ import com.unciv.logic.IsPartOfGameInfoSerialization
enum class PlayerType : IsPartOfGameInfoSerialization {
AI,
Human
Human;
fun toggle() = if (this == AI) Human else AI
}

View File

@ -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<Int>
var uniqueName = ""
override fun getUniqueTarget() = UniqueTarget.Nation
var uniqueText = ""
var innerColor: List<Int>? = null
var startBias = ArrayList<String>()
@ -52,6 +51,10 @@ import kotlin.math.pow
var favoredReligion: String? = null
var cities: ArrayList<String> = 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<String> = 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)
}

View File

@ -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,

View File

@ -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<Nation>{ it.isCityState }.thenBy(collator) { it.name.tr() })
.map { FormattedLine("[${it.name}] starting location", it.name, "Nation/${it.name}", size = 24) }

View File

@ -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<Int>,
maxField: KMutableProperty0<Int>,
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() {

View File

@ -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()
}
}

View File

@ -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<Nation>()
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<Nation>(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)
}

View File

@ -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