From eef5cdb584671568c3fb51f28616182c474aaf8f Mon Sep 17 00:00:00 2001 From: Alexander Korolyov <49795502+alkorolyov@users.noreply.github.com> Date: Sat, 4 Jul 2020 22:29:20 +0200 Subject: [PATCH] Basic spectator functionality. (#2765) * Basic spectator functionality. Added as a separate nation with full visibility. * Robots not allowed to spectate :( * Allow multiple human spectators * Multiple spectators allowed. Fix crash when max players in map editor. * At least one active (not spectator) human player needed * - Invisible tiles vision for spectators. - Minimum 1 active player (Human or AI) to start the game. - temporary only 1 spectator in game * Revert "At least one active (not spectator) human player needed" This reverts commit d843bebe * Add all tech to spectators for resource view * Spectators couldn't be rolled out from "Random" civ * Spectator is not a MajorCiv. corrected random pick * Small refactor * No diplomacy button for Specators * Add fixes from yair210 review. --- .../assets/jsons/Civ V - Vanilla/Nations.json | 9 +++++++- core/src/com/unciv/Constants.kt | 1 + core/src/com/unciv/logic/GameInfo.kt | 1 + core/src/com/unciv/logic/GameStarter.kt | 13 +++++++++--- .../civilization/CivInfoTransientUpdater.kt | 8 +++++++ .../logic/civilization/CivilizationInfo.kt | 2 ++ core/src/com/unciv/models/ruleset/Nation.kt | 3 ++- .../unciv/ui/newgamescreen/NewGameScreen.kt | 9 ++++++++ .../ui/newgamescreen/PlayerPickerTable.kt | 21 +++++++++++++++---- .../com/unciv/ui/worldscreen/WorldScreen.kt | 2 +- 10 files changed, 59 insertions(+), 10 deletions(-) diff --git a/android/assets/jsons/Civ V - Vanilla/Nations.json b/android/assets/jsons/Civ V - Vanilla/Nations.json index 0e9da52b93..891340f9a9 100644 --- a/android/assets/jsons/Civ V - Vanilla/Nations.json +++ b/android/assets/jsons/Civ V - Vanilla/Nations.json @@ -1,4 +1,11 @@ [ + //Spectator + { + "name": "Spectator", + "outerColor": [255,255,255] +// "innerColor": [255,255,255] + }, + { //nations "name": "Babylon", @@ -1015,5 +1022,5 @@ "outerColor": [0,0,0], "innerColor": [182,0,0] } - + ] diff --git a/core/src/com/unciv/Constants.kt b/core/src/com/unciv/Constants.kt index 7634b51686..e28b9d9645 100644 --- a/core/src/com/unciv/Constants.kt +++ b/core/src/com/unciv/Constants.kt @@ -77,4 +77,5 @@ object Constants { const val informationEra = "Information era" const val futureEra = "Future era" const val barbarians = "Barbarians" + const val spectator = "Spectator" } \ No newline at end of file diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index 80b0d6c31b..e1e443bb9b 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -112,6 +112,7 @@ class GameInfo { currentPlayer = thisPlayer.civName currentPlayerCiv = getCivilization(currentPlayer) + if (currentPlayerCiv.isSpectator()) currentPlayerCiv.popupAlerts.clear() // no popups for spectators // Start our turn immediately before the player can made decisions - affects whether our units can commit automated actions and then be attacked immediately etc. diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index 7f4a9452fc..6dd6ff07ef 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -52,6 +52,12 @@ object GameStarter { for (tech in gameInfo.getDifficulty().aiFreeTechs) civInfo.tech.addTechnology(tech) + // add all techs to spectators + if (civInfo.isSpectator()) + for (tech in ruleset.technologies.values) + if (!civInfo.tech.isResearched(tech.name)) + civInfo.tech.addTechnology(tech.name) + for (tech in ruleset.technologies.values .filter { ruleset.getEraNumber(it.era()) < ruleset.getEraNumber(gameSetupInfo.gameParameters.startingEra) }) if (!civInfo.tech.isResearched(tech.name)) @@ -68,7 +74,8 @@ object GameStarter { private fun addCivilizations(newGameParameters: GameParameters, gameInfo: GameInfo, ruleset: Ruleset) { val availableCivNames = Stack() - availableCivNames.addAll(ruleset.nations.filter { !it.value.isCityState() }.keys.shuffled()) + // CityState or Spectator civs are not available for Random pick + availableCivNames.addAll(ruleset.nations.filter { it.value.isMajorCiv() }.keys.shuffled()) availableCivNames.removeAll(newGameParameters.players.map { it.chosenCiv }) availableCivNames.remove(Constants.barbarians) @@ -132,8 +139,8 @@ object GameStarter { } return availableMilitaryUnits.maxBy { max(it.strength, it.rangedStrength) }!!.name } - - for (civ in gameInfo.civilizations.filter { !it.isBarbarian() }) { + // no starting units for Barbarians and Spectators + for (civ in gameInfo.civilizations.filter { !it.isBarbarian() && !it.isSpectator() }) { val startingLocation = startingLocations[civ]!! for (tile in startingLocation.getTilesInDistance(3)) tile.improvement = null // Remove ancient ruins in immediate vicinity diff --git a/core/src/com/unciv/logic/civilization/CivInfoTransientUpdater.kt b/core/src/com/unciv/logic/civilization/CivInfoTransientUpdater.kt index e7728f3605..63c629516a 100644 --- a/core/src/com/unciv/logic/civilization/CivInfoTransientUpdater.kt +++ b/core/src/com/unciv/logic/civilization/CivInfoTransientUpdater.kt @@ -52,6 +52,14 @@ class CivInfoTransientUpdater(val civInfo: CivilizationInfo) { private fun setNewViewableTiles() { val newViewableTiles = HashSet() + // while spectating all map is visible + if (civInfo.isSpectator()) { + val allTiles = civInfo.gameInfo.tileMap.values.toSet() + civInfo.viewableTiles = allTiles + civInfo.viewableInvisibleUnitsTiles = allTiles + return + } + // There are a LOT of tiles usually. // And making large lists of them just as intermediaries before we shove them into the hashset is very space-inefficient. // And so, sequences to the rescue! diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index 1a4ac2485f..00c6f8091c 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -132,6 +132,7 @@ class CivilizationInfo { gameInfo.gameParameters.oneCityChallenge) fun isCurrentPlayer() = gameInfo.getCurrentPlayerCivilization()==this fun isBarbarian() = nation.isBarbarian() + fun isSpectator() = nation.isSpectator() fun isCityState(): Boolean = nation.isCityState() fun getCityStateType(): CityStateType = nation.cityStateType!! fun isMajorCiv() = nation.isMajorCiv() @@ -269,6 +270,7 @@ class CivilizationInfo { fun isDefeated()= cities.isEmpty() // No cities && exploredTiles.isNotEmpty() // Dirty hack: exploredTiles are empty only before starting units are placed && !isBarbarian() // Barbarians can be never defeated + && !isSpectator() // can't loose in Spectator mode && (citiesCreated > 0 || !getCivUnits().any { it.name == Constants.settler }) fun getEra(): String { diff --git a/core/src/com/unciv/models/ruleset/Nation.kt b/core/src/com/unciv/models/ruleset/Nation.kt index 47c6527563..597f41f44a 100644 --- a/core/src/com/unciv/models/ruleset/Nation.kt +++ b/core/src/com/unciv/models/ruleset/Nation.kt @@ -47,8 +47,9 @@ class Nation : INamed { fun getInnerColor(): Color = innerColorObject fun isCityState()= cityStateType != null - fun isMajorCiv() = !isBarbarian() && !isCityState() + fun isMajorCiv() = !isBarbarian() && !isCityState() &&!isSpectator() fun isBarbarian() = name== Constants.barbarians + fun isSpectator() = name == Constants.spectator // This is its own transient because we'll need to check this for every tile-to-tile movement which is harsh @Transient var forestsAndJunglesAreRoads = false diff --git a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt index 88efaf29d4..48763d6365 100644 --- a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt +++ b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt @@ -5,6 +5,7 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.scenes.scene2d.ui.SelectBox import com.badlogic.gdx.scenes.scene2d.ui.Skin import com.badlogic.gdx.utils.Array +import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.* import com.unciv.logic.civilization.PlayerType @@ -60,6 +61,14 @@ class NewGameScreen(previousScreen:CameraStageBaseScreen, _gameSetupInfo: GameSe return@onClick } + if (gameSetupInfo.gameParameters.players.count { it.chosenCiv == Constants.spectator } > 1) { + val noMoreSpectatorsPopup = Popup(this) + noMoreSpectatorsPopup.addGoodSizedLabel("Sorry! No more than one spectator for the moment".tr()).row() + noMoreSpectatorsPopup.addCloseButton() + noMoreSpectatorsPopup.open() + return@onClick + } + if (gameSetupInfo.gameParameters.isOnlineMultiplayer) { for (player in gameSetupInfo.gameParameters.players.filter { it.playerType == PlayerType.Human }) { try { diff --git a/core/src/com/unciv/ui/newgamescreen/PlayerPickerTable.kt b/core/src/com/unciv/ui/newgamescreen/PlayerPickerTable.kt index 7dbbd5c592..3d4c03c917 100644 --- a/core/src/com/unciv/ui/newgamescreen/PlayerPickerTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/PlayerPickerTable.kt @@ -73,12 +73,19 @@ class PlayerPickerTable(val previousScreen: IPreviousScreen, var gameParameters: playerListTable.add("+".toLabel(Color.BLACK, 30).apply { this.setAlignment(Align.center) } .surroundWithCircle(50f).onClick { var player = Player() - if (noRandom) { player = Player(getAvailablePlayerCivs().first().name) } + // no random mode - add first not spectator civ if still available + if (noRandom) { + val availableCiv = getAvailablePlayerCivs().firstOrNull { !it.isSpectator() } + if (availableCiv != null) player = Player(availableCiv.name) + // Spectators only Humans + else player = Player("Spectator").apply { playerType = PlayerType.Human } + } gameParameters.players.add(player) update() }).pad(10f) } - previousScreen.setRightSideButtonEnabled(gameParameters.players.size > 1) + // can enable start game when more than 1 active player + previousScreen.setRightSideButtonEnabled(gameParameters.players.count{ it.chosenCiv != Constants.spectator } > 1) } /** @@ -122,7 +129,9 @@ class PlayerPickerTable(val previousScreen: IPreviousScreen, var gameParameters: playerTypeTextbutton.onClick { if (player.playerType == PlayerType.AI) player.playerType = PlayerType.Human - else player.playerType = PlayerType.AI + // we cannot change Spectator player to AI type, robots not allowed to spectate :( + else if (player.chosenCiv != Constants.spectator) + player.playerType = PlayerType.AI update() } playerTable.add(playerTypeTextbutton).width(100f).pad(5f).right() @@ -223,8 +232,12 @@ class PlayerPickerTable(val previousScreen: IPreviousScreen, var gameParameters: if (!noRandom) { nationListTable.add(randomPlayerTable).pad(10f).width(nationsPopupWidth).row() } for (nation in getAvailablePlayerCivs()) { + // don't show current player civ if (player.chosenCiv == nation.name) continue + // only humans can spectate, sorry robots + if (player.playerType == PlayerType.AI && nation.isSpectator()) + continue nationListTable.add(NationTable(nation, nationsPopupWidth, previousScreen.ruleset).onClick { if (previousScreen is GameParametersScreen) @@ -261,7 +274,7 @@ class PlayerPickerTable(val previousScreen: IPreviousScreen, var gameParameters: var nations = ArrayList() for (nation in previousScreen.ruleset.nations.values .filter { it.isMajorCiv() }) { - if (gameParameters.players.any { it.chosenCiv == nation.name }) + if (gameParameters.players.any { it.chosenCiv == nation.name && it.chosenCiv != Constants.spectator}) continue nations.add(nation) } diff --git a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt index 2d1c2b1611..0571f9237e 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt @@ -394,7 +394,7 @@ class WorldScreen(val viewingCiv:CivilizationInfo) : CameraStageBaseScreen() { private fun updateDiplomacyButton(civInfo: CivilizationInfo) { diplomacyButtonWrapper.clear() - if(!civInfo.isDefeated() && civInfo.getKnownCivs() + if(!civInfo.isDefeated() && !civInfo.isSpectator() && civInfo.getKnownCivs() .filterNot { it==viewingCiv || it.isBarbarian() } .any()) { displayTutorial(Tutorial.OtherCivEncountered)