mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-09 07:18:57 +07:00
Next-Turn Progressbar (#9409)
* Next-Turn Progressbar * Next-Turn Progressbar - doc * NextTurnProgress: Rethink max on first turn
This commit is contained in:
@ -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())
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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) }
|
||||
|
@ -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") {}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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()) {
|
||||
|
@ -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 | |
|
||||
|
Reference in New Issue
Block a user