Next-Turn Progressbar (#9409)

* Next-Turn Progressbar

* Next-Turn Progressbar - doc

* NextTurnProgress: Rethink max on first turn
This commit is contained in:
SomeTroglodyte
2023-05-22 16:59:28 +02:00
committed by GitHub
parent 575983578a
commit 5f60c887f7
7 changed files with 167 additions and 23 deletions

View File

@ -34,6 +34,7 @@ import com.unciv.models.ruleset.nation.Difficulty
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.audio.MusicMood
import com.unciv.ui.audio.MusicTrackChooserFlags
import com.unciv.ui.screens.worldscreen.status.NextTurnProgress
import com.unciv.utils.DebugUtils
import com.unciv.utils.debug
import java.util.UUID
@ -277,7 +278,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
fun isSimulation(): Boolean = turns < DebugUtils.SIMULATE_UNTIL_TURN
|| turns < simulateMaxTurns && simulateUntilWin
fun nextTurn() {
fun nextTurn(progressBar: NextTurnProgress? = null) {
var player = currentPlayerCiv
var playerIndex = civilizations.indexOf(player)
@ -299,7 +300,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
// would skip a turn if an AI civ calls nextTurn
// this happens when resigning a multiplayer game)
if (player.isHuman()) {
TurnManager(player).endTurn()
TurnManager(player).endTurn(progressBar)
setNextPlayer()
}
@ -314,7 +315,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
{
// Starting preparations
TurnManager(player).startTurn()
TurnManager(player).startTurn(progressBar)
// Automation done here
TurnManager(player).automateTurn()
@ -326,7 +327,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
}
// Clean up
TurnManager(player).endTurn()
TurnManager(player).endTurn(progressBar)
// To the next player
setNextPlayer()
@ -341,7 +342,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
currentPlayerCiv = getCivilization(currentPlayer)
// Starting his turn
TurnManager(player).startTurn()
TurnManager(player).startTurn(progressBar)
// No popups for spectators
if (currentPlayerCiv.isSpectator())

View File

@ -19,6 +19,7 @@ import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unique.endTurn
import com.unciv.models.stats.Stats
import com.unciv.ui.components.MayaCalendar
import com.unciv.ui.screens.worldscreen.status.NextTurnProgress
import com.unciv.utils.Log
import kotlin.math.max
import kotlin.math.min
@ -27,7 +28,7 @@ import kotlin.random.Random
class TurnManager(val civInfo: Civilization) {
fun startTurn() {
fun startTurn(progressBar: NextTurnProgress? = null) {
if (civInfo.isSpectator()) return
if (civInfo.isMajorCiv() && civInfo.isAlive()) {
@ -66,7 +67,10 @@ class TurnManager(val civInfo: Civilization) {
civInfo.cache.updateCitiesConnectedToCapital()
startTurnFlags()
updateRevolts()
for (city in civInfo.cities) CityTurnManager(city).startTurn() // Most expensive part of startTurn
for (city in civInfo.cities) {
progressBar?.increment()
CityTurnManager(city).startTurn() // Most expensive part of startTurn
}
for (unit in civInfo.units.getCivUnits()) UnitTurnManager(unit).startTurn()
@ -218,7 +222,7 @@ class TurnManager(val civInfo: Civilization) {
((4 + Random.Default.nextInt(3)) * max(civInfo.gameInfo.speed.modifier, 1f)).toInt()
fun endTurn() {
fun endTurn(progressBar: NextTurnProgress? = null) {
val notificationsLog = civInfo.notificationsLog
val notificationsThisTurn = Civilization.NotificationsLog(civInfo.gameInfo.turns)
notificationsThisTurn.notifications.addAll(civInfo.notifications)
@ -278,8 +282,10 @@ class TurnManager(val civInfo: Civilization) {
// To handle tile's owner issue (#8246), we need to run cities being razed first.
// a city can be removed while iterating (if it's being razed) so we need to iterate over a copy - sorting does one
for (city in civInfo.cities.sortedByDescending { it.isBeingRazed })
for (city in civInfo.cities.sortedByDescending { it.isBeingRazed }) {
progressBar?.increment()
CityTurnManager(city).endTurn()
}
civInfo.temporaryUniques.endTurn()

View File

@ -57,6 +57,7 @@ import com.unciv.ui.screens.worldscreen.bottombar.TileInfoTable
import com.unciv.ui.screens.worldscreen.minimap.MinimapHolder
import com.unciv.ui.screens.worldscreen.status.MultiplayerStatusButton
import com.unciv.ui.screens.worldscreen.status.NextTurnButton
import com.unciv.ui.screens.worldscreen.status.NextTurnProgress
import com.unciv.ui.screens.worldscreen.status.StatusButtons
import com.unciv.ui.screens.worldscreen.unit.UnitTable
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsTable
@ -557,6 +558,8 @@ class WorldScreen(
fun nextTurn() {
isPlayersTurn = false
shouldUpdate = true
val progressBar = NextTurnProgress(nextTurnButton)
progressBar.start(this)
// on a separate thread so the user can explore their world while we're passing the turn
nextTurnUpdateJob = Concurrency.runOnNonDaemonThreadPool("NextTurn") {
@ -566,7 +569,9 @@ class WorldScreen(
val gameInfoClone = originalGameInfo.clone()
gameInfoClone.setTransients() // this can get expensive on large games, not the clone itself
gameInfoClone.nextTurn()
progressBar.increment()
gameInfoClone.nextTurn(progressBar)
if (originalGameInfo.gameParameters.isOnlineMultiplayer) {
try {
@ -617,6 +622,8 @@ class WorldScreen(
gameInfoClone.isUpToDate = true
}
progressBar.increment()
startNewScreenJob(gameInfoClone)
}
}
@ -713,7 +720,7 @@ class WorldScreen(
displayTutorial(TutorialTrigger.LuxuryResource) { resources.any { it.resource.resourceType == ResourceType.Luxury } }
displayTutorial(TutorialTrigger.StrategicResource) { resources.any { it.resource.resourceType == ResourceType.Strategic } }
displayTutorial(TutorialTrigger.EnemyCity) {
viewingCiv.getKnownCivs().asSequence().filter { viewingCiv.isAtWarWith(it) }
viewingCiv.getKnownCivs().filter { viewingCiv.isAtWarWith(it) }
.flatMap { it.cities.asSequence() }.any { viewingCiv.hasExplored(it.getCenterTile()) }
}
displayTutorial(TutorialTrigger.ApolloProgram) { viewingCiv.hasUnique(UniqueType.EnablesConstructionOfSpaceshipParts) }

View File

@ -27,7 +27,7 @@ import com.unciv.utils.Concurrency
import com.unciv.utils.launchOnGLThread
class NextTurnButton : IconTextButton("", null, 30) {
private var nextTurnAction = NextTurnAction("", Color.BLACK) {}
private var nextTurnAction = NextTurnAction.Default
init {
// label.setFontSize(30)
@ -41,29 +41,31 @@ class NextTurnButton : IconTextButton("", null, 30) {
fun update(worldScreen: WorldScreen) {
nextTurnAction = getNextTurnAction(worldScreen)
updateButton(nextTurnAction)
isEnabled = !worldScreen.hasOpenPopups() && worldScreen.isPlayersTurn
&& !worldScreen.waitingForAutosave && !worldScreen.isNextTurnUpdateRunning()
}
internal fun updateButton(nextTurnAction: NextTurnAction) {
label.setText(nextTurnAction.text.tr())
label.color = nextTurnAction.color
if (nextTurnAction.icon != null && ImageGetter.imageExists(nextTurnAction.icon!!))
if (nextTurnAction.icon != null && ImageGetter.imageExists(nextTurnAction.icon))
iconCell.setActor(ImageGetter.getImage(nextTurnAction.icon).apply { setSize(30f) })
else
iconCell.clearActor()
pack()
isEnabled = !worldScreen.hasOpenPopups() && worldScreen.isPlayersTurn
&& !worldScreen.waitingForAutosave && !worldScreen.isNextTurnUpdateRunning()
}
private fun getNextTurnAction(worldScreen: WorldScreen): NextTurnAction {
return when {
worldScreen.isNextTurnUpdateRunning() ->
NextTurnAction("Working...", Color.GRAY, "NotificationIcons/Working") {}
NextTurnAction.Working
!worldScreen.isPlayersTurn && worldScreen.gameInfo.gameParameters.isOnlineMultiplayer ->
NextTurnAction("Waiting for [${worldScreen.gameInfo.currentPlayerCiv}]...", Color.GRAY,
"NotificationIcons/Waiting") {}
!worldScreen.isPlayersTurn && !worldScreen.gameInfo.gameParameters.isOnlineMultiplayer ->
NextTurnAction("Waiting for other players...",Color.GRAY,
"NotificationIcons/Waiting") {}
NextTurnAction.Waiting
worldScreen.viewingCiv.cities.any {
!it.isPuppet &&
@ -185,7 +187,12 @@ class NextTurnButton : IconTextButton("", null, 30) {
}
}
}
}
class NextTurnAction(val text: String, val color: Color, val icon: String? = null, val action: () -> Unit)
class NextTurnAction(val text: String, val color: Color, val icon: String? = null, val action: () -> Unit) {
companion object Prefabs {
val Default = NextTurnAction("", Color.BLACK) {}
val Working = NextTurnAction("Working...", Color.GRAY, "NotificationIcons/Working") {}
val Waiting = NextTurnAction("Waiting for other players...",Color.GRAY, "NotificationIcons/Waiting") {}
}
}

View File

@ -0,0 +1,120 @@
package com.unciv.ui.screens.worldscreen.status
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.actions.Actions
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.GUI
import com.unciv.models.metadata.GameParameters
import com.unciv.ui.components.extensions.disable
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.worldscreen.WorldScreen
import com.unciv.utils.Concurrency
import com.unciv.utils.Log
class NextTurnProgress(
// nullable so we can free the reference once the ProgressBar is shown
private var nextTurnButton: NextTurnButton?
) : Table() {
companion object {
/** Background tint will be color of the right (shrinking) part of the bar - UI mods can override this */
private val defaultRightColor = Color(0x600000ff)
/** Minimum Height of the bar if the moddable background has no minHeight */
private const val defaultBarHeight = 4f
/** Distance from bottom of NextTurnButton */
private const val barYPos = 1f
/** Bar width is NextTurnButton.width minus background ninepatch's declared outer widths minus this */
private const val removeHorizontalPad = 25f
/** Speed of fading the bar in when it starts being rendered */
private const val fadeInDuration = 1f
}
private var progress = -1
private var progressMax = 0
private var isDirty = false
private var barWidth = 0f
// Since we do UI update coroutine-decoupled there's a potential race conditon where the worldScreen
// gets replaced, and a pending update comes too late. To prevent re-showing when it's outdated we
// keep a hash in lieu of a weak reference - a normal reference might keep an outdated WorldScreen from
// being garbage collected, and java.lang.ref.WeakReference.refersTo requires a language level 16 opt-in.
private var worldScreenHash = 0
init {
background = BaseScreen.skinStrings.getUiBackground("WorldScreen/NextTurn/ProgressBar", tintColor = defaultRightColor)
val leftColor = BaseScreen.skinStrings.getUIColor("WorldScreen/NextTurn/ProgressColor", Color.FOREST)
add(ImageGetter.getDot(leftColor)) // active bar part
add() // Empty cell for the remainder portion of the bar
}
fun start(worldScreen: WorldScreen) {
progress = 0
val game = worldScreen.gameInfo
worldScreenHash = worldScreen.hashCode()
fun GameParameters.isRandomNumberOfCivs() = randomNumberOfPlayers || randomNumberOfCityStates
fun GameParameters.minNumberOfCivs() =
(if (randomNumberOfPlayers) minNumberOfPlayers else players.size) +
(if (randomNumberOfCityStates) minNumberOfCityStates else numberOfCityStates)
progressMax = 3 + // one extra step after clone and just before new worldscreen, 1 extra so it's never 100%
when {
// Later turns = two steps per city (startTurn and endTurn)
// Note we ignore cities being founded or destroyed - after turn 0 that proportion
// should be small, so the bar may clamp at max for a short while;
// or the new WordScreen starts before it's full. Far simpler code this way.
game.turns > 0 -> game.getCities().count() * 2
// If we shouldn't disclose how many civs there are to Mr. Eagle Eye counting steps:
game.gameParameters.isRandomNumberOfCivs() -> game.gameParameters.minNumberOfCivs()
// One step per expected city to be founded (they get an endTurn, no startTurn)
else -> game.civilizations.count { it.isMajorCiv() && it.isAI() || it.isCityState() }
}
startUpdateProgress()
}
fun increment() {
progress++
startUpdateProgress()
}
private fun startUpdateProgress() {
isDirty = true
Concurrency.runOnGLThread {
updateProgress()
}
}
private fun updateProgress() {
if (!isDirty) return
isDirty = false
val currentWorldScreenHash = GUI.getWorldScreenIfActive()?.hashCode() ?: -1
if (progressMax == 0 || currentWorldScreenHash != worldScreenHash) {
remove()
return
}
// On first update the button text is not yet updated. To stabilize geometry, do it now
if (progress == 0) nextTurnButton?.apply {
disable()
updateButton(NextTurnAction.Working)
barWidth = width - removeHorizontalPad -
(background.leftWidth + background.rightWidth) // "cut off" the rounded parts of the button
this@NextTurnProgress.setPosition((width - barWidth) / 2, barYPos)
}
val cellWidth = barWidth * progress.coerceAtMost(progressMax) / progressMax
val cellHeight = background.minHeight.coerceAtLeast(defaultBarHeight)
cells[0].actor.setSize(cellWidth, cellHeight)
cells[1].width(barWidth - cellWidth) // Necessary - Table has a quirk so a simple fillX() won't shrink
setSize(barWidth, defaultBarHeight)
if (parent == null) {
color.a = 0f
nextTurnButton?.addActor(this)
addAction(Actions.fadeIn(fadeInDuration)) // Also helps hide the jerkiness when many cities are founded on turn 0
nextTurnButton = null // Release reference as early as possible
}
}
}

View File

@ -21,14 +21,15 @@ class UiElementDocsWriter {
val elements = mutableListOf<String>()
val backgroundRegex = Regex("""getUiBackground\((\X*?)"(?<path>.*)"[ ,\n\r]*((BaseScreen\.)?skinStrings\.(?<default>.*)Shape)?\X*?\)""")
@Suppress("RegExpRepeatedSpace") // IDE doesn't know about commented RegExes
val colorRegex = Regex("""
getUIColor\s*\(\s* # function call, whitespace around opening round bracket optional. All \s also allow line breaks!
"(?<path>[^"]*)"\s* # captures "path", anything between double-quotes, not allowing for embedded quotes
(?:,\s* # group for optional default parameter
(?:default\s*=\s*)? # allow for named parameter
(?:Colors\s*\(|colorFromRGB\s*\(|Color\.) # recognize only Color constructor, colorFromRGB helper, or Color.* constants as argument
(?:Color\s*\(|colorFromRGB\s*\(|Color\.) # recognize only Color constructor, colorFromRGB helper, or Color.* constants as argument
(?<default>[^)]*) # capture "default" up until a closing round bracket
)\s*\) # ends default parameter group and checks closing round bracket of the getUIColor call
)?\s*\) # ends default parameter group and checks closing round bracket of the getUIColor call
""", RegexOption.COMMENTS)
for (file in srcFile.walk()) {

View File

@ -107,6 +107,8 @@ These shapes are used all over Unciv and can be replaced to make a lot of UI ele
| WorldScreen/CityButton/ | InfluenceBar | null | |
| WorldScreen/Minimap/ | Background | null | |
| WorldScreen/Minimap/ | Border | null | |
| WorldScreen/NextTurn/ | ProgressBar | null | |
| WorldScreen/NextTurn/ | ProgressColor | FOREST | |
| WorldScreen/TopBar/ | LeftAttachment | roundedEdgeRectangle | |
| WorldScreen/TopBar/ | ResourceTable | null | |
| WorldScreen/TopBar/ | RightAttachment | roundedEdgeRectangle | |