Victory screen replay gets a Slider (#9116)

* VictoryScreen tweaks for narrow portrait, UncivSlider update

* VictoryScreen Replay Slider
This commit is contained in:
SomeTroglodyte
2023-04-04 21:06:22 +02:00
committed by GitHub
parent ad080b4dc6
commit 910778418a
8 changed files with 119 additions and 33 deletions

View File

@ -541,6 +541,7 @@ open class TabbedPager(
* @param insertBefore -1 to add at the end, or index of existing page to insert this before it. * @param insertBefore -1 to add at the end, or index of existing page to insert this before it.
* @param secret Marks page as 'secret'. A password is asked once per [TabbedPager] and if it does not match the has passed in the constructor the page and all subsequent secret pages are dropped. * @param secret Marks page as 'secret'. A password is asked once per [TabbedPager] and if it does not match the has passed in the constructor the page and all subsequent secret pages are dropped.
* @param disabled Initial disabled state. Disabled pages cannot be selected even with [selectPage], their button is dimmed. * @param disabled Initial disabled state. Disabled pages cannot be selected even with [selectPage], their button is dimmed.
* @param scrollAlign Used only once on first page activation - sets the content ScrollPane's scrollX/scrollY so your content (which must have valid width/height at the time) aligns as specified to the pager's content area.
* @param shortcutKey Optional keyboard key to associate. * @param shortcutKey Optional keyboard key to associate.
* @param syncScroll If on, the ScrollPanes for [content] and [fixed content][IPageExtensions.getFixedContent] will synchronize horizontally. * @param syncScroll If on, the ScrollPanes for [content] and [fixed content][IPageExtensions.getFixedContent] will synchronize horizontally.
* @return The new page's index or -1 if it could not be immediately added (secret). * @return The new page's index or -1 if it could not be immediately added (secret).

View File

@ -32,7 +32,7 @@ import kotlin.math.sign
/** /**
* Modified Gdx [Slider] * Modified Gdx [Slider]
* *
* Has +/- buttons at the end for easier single steps * Optionally has +/- buttons at the end for easier single steps
* Shows a timed tip with the actual value every time it changes * Shows a timed tip with the actual value every time it changes
* Disables listeners of any ScrollPanes this is nested in while dragging * Disables listeners of any ScrollPanes this is nested in while dragging
* *
@ -43,8 +43,12 @@ import kotlin.math.sign
* @param max Initializes [Slider.max] * @param max Initializes [Slider.max]
* @param step Initializes [Slider.stepSize] * @param step Initializes [Slider.stepSize]
* @param vertical Initializes [Slider.vertical] * @param vertical Initializes [Slider.vertical]
* @param plusMinus Enable +/- buttons * @param plusMinus Enable +/- buttons - note they will also snap to [setSnapToValues].
* @param onChange Optional lambda gets called with the current value on change * @param initial Initializes [value]
* @param sound Plays _only_ on user dragging (+/- always play the normal Click sound). Consider using [UncivSound.Silent] for sliders with many steps.
* @param tipType None disables the tooltip, Auto animates it on change, Permanent leaves it on screen after initial fade-in.
* @param getTipText Formats a value for the tooltip. Default formats as numeric, precision depends on [stepSize]. You can also use [UncivSlider::formatPercent][formatPercent].
* @param onChange Optional lambda gets called with the current value on a user change (not when setting value programmatically).
*/ */
class UncivSlider ( class UncivSlider (
min: Float, min: Float,
@ -54,10 +58,12 @@ class UncivSlider (
plusMinus: Boolean = true, plusMinus: Boolean = true,
initial: Float, initial: Float,
sound: UncivSound = UncivSound.Slider, sound: UncivSound = UncivSound.Slider,
private var permanentTip: Boolean = true, private val tipType: TipType = TipType.Permanent,
private val getTipText: ((Float) -> String)? = null, private val getTipText: ((Float) -> String)? = null,
onChange: ((Float) -> Unit)? = null private val onChange: ((Float) -> Unit)? = null
): Table(BaseScreen.skin) { ): Table(BaseScreen.skin) {
enum class TipType { None, Auto, Permanent }
companion object { companion object {
/** Can be passed directly to the [getTipText] constructor parameter */ /** Can be passed directly to the [getTipText] constructor parameter */
fun formatPercent(value: Float): String { fun formatPercent(value: Float): String {
@ -88,6 +94,7 @@ class UncivSlider (
private var snapThreshold: Float = 0f private var snapThreshold: Float = 0f
// Compatibility with default Slider // Compatibility with default Slider
@Suppress("unused") // Part of the Slider API
val minValue: Float val minValue: Float
get() = slider.minValue get() = slider.minValue
@Suppress("unused") // Part of the Slider API @Suppress("unused") // Part of the Slider API
@ -96,7 +103,9 @@ class UncivSlider (
var value: Float var value: Float
get() = slider.value get() = slider.value
set(newValue) { set(newValue) {
blockListener = true
slider.value = newValue slider.value = newValue
blockListener = false
valueChanged() valueChanged()
} }
var stepSize: Float var stepSize: Float
@ -116,6 +125,7 @@ class UncivSlider (
slider.isDisabled = value slider.isDisabled = value
setPlusMinusEnabled() setPlusMinusEnabled()
} }
@Suppress("unused") // Part of the Slider API
/** Sets the range of this slider. The slider's current value is clamped to the range. */ /** Sets the range of this slider. The slider's current value is clamped to the range. */
fun setRange(min: Float, max: Float) { fun setRange(min: Float, max: Float) {
slider.setRange(min, max) slider.setRange(min, max)
@ -133,16 +143,13 @@ class UncivSlider (
// Detect changes in isDragging // Detect changes in isDragging
private var hasFocus = false private var hasFocus = false
// Help value set not to trigger change listener events
private var blockListener = false
init { init {
tipLabel.setOrigin(Align.center) tipLabel.setOrigin(Align.center)
tipContainer.touchable = Touchable.disabled tipContainer.touchable = Touchable.disabled
/** Prevents hiding the value tooltip over the slider knob */
if(permanentTip)
tipHideTask.cancel()
stepChanged() // Initialize tip formatting stepChanged() // Initialize tip formatting
if (plusMinus) { if (plusMinus) {
@ -158,7 +165,7 @@ class UncivSlider (
if (vertical) row() if (vertical) row()
} else minusButton = null } else minusButton = null
add(slider).pad(padding).fill() add(slider).pad(padding).fillY().growX()
if (plusMinus) { if (plusMinus) {
if (vertical) row() if (vertical) row()
@ -179,6 +186,7 @@ class UncivSlider (
// Add the listener late so the setting of the initial value is silent // Add the listener late so the setting of the initial value is silent
slider.addListener(object : ChangeListener() { slider.addListener(object : ChangeListener() {
override fun changed(event: ChangeEvent?, actor: Actor?) { override fun changed(event: ChangeEvent?, actor: Actor?) {
if (blockListener) return
if (slider.isDragging != hasFocus) { if (slider.isDragging != hasFocus) {
hasFocus = slider.isDragging hasFocus = slider.isDragging
if (hasFocus) if (hasFocus)
@ -203,6 +211,7 @@ class UncivSlider (
Gdx.input.isKeyPressed(Input.Keys.SHIFT_RIGHT) Gdx.input.isKeyPressed(Input.Keys.SHIFT_RIGHT)
) { ) {
value += delta value += delta
onChange?.invoke(value)
return return
} }
var bestDiff = -1f var bestDiff = -1f
@ -219,19 +228,28 @@ class UncivSlider (
bestIndex += delta.sign.toInt() bestIndex += delta.sign.toInt()
if (bestIndex !in snapToValues!!.indices) return if (bestIndex !in snapToValues!!.indices) return
value = snapToValues!![bestIndex] value = snapToValues!![bestIndex]
onChange?.invoke(value)
} }
// Visual feedback // Visual feedback
private fun valueChanged() { private fun valueChanged() {
if (getTipText == null) when {
tipLabel.setText(tipFormat.format(slider.value)) tipType == TipType.None -> Unit
else getTipText == null ->
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION") // warning wrong, without !! won't compile tipLabel.setText(tipFormat.format(slider.value))
tipLabel.setText(getTipText!!(slider.value)) else ->
if (!tipHideTask.isScheduled) showTip() @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") // warning wrong, without !! won't compile
tipHideTask.cancel() tipLabel.setText(getTipText!!(slider.value))
if (!permanentTip) }
Timer.schedule(tipHideTask, hideDelay) when(tipType) {
TipType.None -> Unit
TipType.Auto -> {
if (!tipHideTask.isScheduled) showTip()
tipHideTask.cancel()
Timer.schedule(tipHideTask, hideDelay)
}
TipType.Permanent -> showTip()
}
setPlusMinusEnabled() setPlusMinusEnabled()
} }
@ -251,7 +269,7 @@ class UncivSlider (
stepSize > 0.0099f -> "%.2f" stepSize > 0.0099f -> "%.2f"
else -> "%.3f" else -> "%.3f"
} }
if (getTipText == null) if (tipType != TipType.None && getTipText == null)
tipLabel.setText(tipFormat.format(slider.value)) tipLabel.setText(tipFormat.format(slider.value))
} }

View File

@ -90,7 +90,11 @@ class MapEditorEditTab(
defaults().pad(10f).left() defaults().pad(10f).left()
add(brushLabel) add(brushLabel)
brushCell = add().padLeft(0f) brushCell = add().padLeft(0f)
brushSlider = UncivSlider(1f,6f,1f, initial = 1f, getTipText = { getBrushTip(it).tr() }, permanentTip = false) { brushSlider = UncivSlider(1f,6f,1f,
initial = 1f,
getTipText = { getBrushTip(it).tr() },
tipType = UncivSlider.TipType.Auto
) {
brushSize = if (it > 5f) -1 else it.toInt() brushSize = if (it > 5f) -1 else it.toInt()
brushLabel.setText("Brush ([${getBrushTip(it, true)}]):".tr()) brushLabel.setText("Brush ([${getBrushTip(it, true)}]):".tr())
} }

View File

@ -25,7 +25,6 @@ import com.unciv.ui.screens.pickerscreens.PickerScreen
import com.unciv.ui.screens.worldscreen.WorldScreen import com.unciv.ui.screens.worldscreen.WorldScreen
//TODO someoneHasWon should look at gameInfo.victoryData //TODO someoneHasWon should look at gameInfo.victoryData
//TODO replay slider
class VictoryScreen( class VictoryScreen(
private val worldScreen: WorldScreen, private val worldScreen: WorldScreen,
@ -44,8 +43,6 @@ class VictoryScreen(
val key: Char, val key: Char,
val iconName: String = "", val iconName: String = "",
val caption: String? = null, val caption: String? = null,
val align: Int = Align.topLeft,
val syncScroll: Boolean = true,
val allowAsSecret: Boolean = false val allowAsSecret: Boolean = false
) { ) {
OurStatus('O', "StatIcons/Specialist", caption = "Our status") { OurStatus('O', "StatIcons/Specialist", caption = "Our status") {
@ -63,7 +60,7 @@ class VictoryScreen(
override fun getContent(worldScreen: WorldScreen) = VictoryScreenCivRankings(worldScreen) override fun getContent(worldScreen: WorldScreen) = VictoryScreenCivRankings(worldScreen)
override fun isHidden(playerCiv: Civilization) = UncivGame.Current.settings.useDemographics override fun isHidden(playerCiv: Civilization) = UncivGame.Current.settings.useDemographics
}, },
Replay('P', "OtherIcons/Load", align = Align.top, syncScroll = false, allowAsSecret = true) { Replay('P', "OtherIcons/Load", allowAsSecret = true) {
override fun getContent(worldScreen: WorldScreen) = VictoryScreenReplay(worldScreen) override fun getContent(worldScreen: WorldScreen) = VictoryScreenReplay(worldScreen)
override fun isHidden(playerCiv: Civilization) = override fun isHidden(playerCiv: Civilization) =
!playerCiv.isSpectator() && playerCiv.gameInfo.victoryData == null && playerCiv.isAlive() !playerCiv.isSpectator() && playerCiv.gameInfo.victoryData == null && playerCiv.isAlive()
@ -75,7 +72,7 @@ class VictoryScreen(
init { init {
//**************** Set up the tabs **************** //**************** Set up the tabs ****************
splitPane.setFirstWidget(tabs) splitPane.setFirstWidget(tabs)
val iconSize = Constants.defaultFontSize.toFloat() val iconSize = Constants.headingFontSize.toFloat()
for (tab in VictoryTabs.values()) { for (tab in VictoryTabs.values()) {
val tabHidden = tab.isHidden(playerCiv) val tabHidden = tab.isHidden(playerCiv)
@ -86,7 +83,7 @@ class VictoryScreen(
tab.caption ?: tab.name, tab.caption ?: tab.name,
tab.getContent(worldScreen), tab.getContent(worldScreen),
icon, iconSize, icon, iconSize,
scrollAlign = tab.align, syncScroll = tab.syncScroll, scrollAlign = Align.topLeft,
shortcutKey = KeyCharAndCode(tab.key), shortcutKey = KeyCharAndCode(tab.key),
secret = tabHidden && tab.allowAsSecret secret = tabHidden && tab.allowAsSecret
) )

View File

@ -2,6 +2,7 @@ package com.unciv.ui.screens.victoryscreen
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.Constants import com.unciv.Constants
import com.unciv.ui.components.TabbedPager import com.unciv.ui.components.TabbedPager
import com.unciv.ui.components.extensions.addSeparator import com.unciv.ui.components.extensions.addSeparator
@ -15,6 +16,8 @@ class VictoryScreenCivRankings(
private val header = Table() private val header = Table()
init { init {
align(Align.topLeft)
header.align(Align.topLeft)
defaults().pad(10f) defaults().pad(10f)
val majorCivs = worldScreen.gameInfo.civilizations.filter { it.isMajorCiv() } val majorCivs = worldScreen.gameInfo.civilizations.filter { it.isMajorCiv() }

View File

@ -2,6 +2,7 @@ package com.unciv.ui.screens.victoryscreen
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.models.ruleset.Victory import com.unciv.models.ruleset.Victory
import com.unciv.ui.components.TabbedPager import com.unciv.ui.components.TabbedPager
@ -16,6 +17,8 @@ class VictoryScreenGlobalVictory(
private val header = Table() private val header = Table()
init { init {
align(Align.top)
val gameInfo = worldScreen.gameInfo val gameInfo = worldScreen.gameInfo
val majorCivs = gameInfo.civilizations.asSequence().filter { it.isMajorCiv() } val majorCivs = gameInfo.civilizations.asSequence().filter { it.isMajorCiv() }
val victoriesToShow = gameInfo.getEnabledVictories() val victoriesToShow = gameInfo.getEnabledVictories()

View File

@ -2,6 +2,7 @@ package com.unciv.ui.screens.victoryscreen
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.models.ruleset.Victory import com.unciv.models.ruleset.Victory
import com.unciv.ui.components.TabbedPager import com.unciv.ui.components.TabbedPager
@ -16,6 +17,8 @@ class VictoryScreenOurVictory(
private val header = Table() private val header = Table()
init { init {
align(Align.top)
val gameInfo = worldScreen.gameInfo val gameInfo = worldScreen.gameInfo
val victoriesToShow = gameInfo.getEnabledVictories() val victoriesToShow = gameInfo.getEnabledVictories()

View File

@ -1,10 +1,17 @@
package com.unciv.ui.screens.victoryscreen package com.unciv.ui.screens.victoryscreen
import com.badlogic.gdx.scenes.scene2d.ui.Container
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.badlogic.gdx.utils.Timer import com.badlogic.gdx.utils.Timer
import com.unciv.models.UncivSound
import com.unciv.ui.components.TabbedPager import com.unciv.ui.components.TabbedPager
import com.unciv.ui.components.UncivSlider
import com.unciv.ui.components.YearTextUtil import com.unciv.ui.components.YearTextUtil
import com.unciv.ui.components.extensions.onClick
import com.unciv.ui.components.extensions.setSize
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.worldscreen.WorldScreen import com.unciv.ui.screens.worldscreen.WorldScreen
@ -13,20 +20,62 @@ class VictoryScreenReplay(
) : Table(BaseScreen.skin), TabbedPager.IPageExtensions { ) : Table(BaseScreen.skin), TabbedPager.IPageExtensions {
private val gameInfo = worldScreen.gameInfo private val gameInfo = worldScreen.gameInfo
private val finalTurn = gameInfo.turns
private var replayTimer : Timer.Task? = null private var replayTimer : Timer.Task? = null
private val yearLabel = "".toLabel()
private val replayMap = ReplayMap(gameInfo.tileMap) private val replayMap = ReplayMap(gameInfo.tileMap)
private val header = Table() private val header = Table()
private val yearLabel = "".toLabel()
private val slider: UncivSlider
private val playImage = ImageGetter.getImage("OtherIcons/ForwardArrow")
private val pauseImage = ImageGetter.getImage("OtherIcons/Pause")
private val playPauseButton = Container(pauseImage)
init { init {
header.add(yearLabel).pad(10f) // yearLabel should be OK with 80f, Label("4000 BC").prefWidth is nearly 78f.
// The 190f is twice (that plus space=15f). The minimum 120f should never happen, just in case.
val firstTurn = gameInfo.historyStartTurn
val maxSliderPercent = if (worldScreen.isPortrait()) 0.75f else 0.5f
val sliderWidth = ((finalTurn - firstTurn) * 15f + 60f)
.coerceAtMost(worldScreen.stage.width * maxSliderPercent)
.coerceAtMost(worldScreen.stage.width - 190f)
.coerceAtLeast(120f)
slider = UncivSlider(
firstTurn.toFloat(), finalTurn.toFloat(), 1f,
initial = firstTurn.toFloat(),
sound = UncivSound.Silent,
tipType = UncivSlider.TipType.None,
onChange = this::sliderChanged
)
playImage.setSize(24f)
pauseImage.setSize(24f)
playPauseButton.apply {
// I decided against `align(Align.left)`: since the button is so much smaller than the year label, perfect symmetry is impossible
setSize(26f, 26f)
onClick(::togglePause)
}
yearLabel.setAlignment(Align.right)
yearLabel.onClick {
updateReplayTable(gameInfo.historyStartTurn)
restartTimer()
}
header.defaults().space(15f).fillX().padTop(15f)
header.add(yearLabel).minWidth(80f).right()
header.add(slider).width(sliderWidth)
header.add(playPauseButton).minWidth(80f)
add(replayMap).pad(10f) add(replayMap).pad(10f)
} }
private fun togglePause() {
if (replayTimer == null) restartTimer() else resetTimer()
}
private fun restartTimer() { private fun restartTimer() {
replayTimer?.cancel() replayTimer?.cancel()
val firstTurn = gameInfo.historyStartTurn val firstTurn = slider.value.toInt()
val finalTurn = gameInfo.turns
replayTimer = Timer.schedule( replayTimer = Timer.schedule(
object : Timer.Task() { object : Timer.Task() {
private var nextTurn = firstTurn private var nextTurn = firstTurn
@ -39,22 +88,30 @@ class VictoryScreenReplay(
// End at the last turn. // End at the last turn.
finalTurn - firstTurn finalTurn - firstTurn
) )
playPauseButton.actor = pauseImage
} }
private fun resetTimer() { private fun resetTimer() {
replayTimer?.cancel() replayTimer?.cancel()
replayTimer = null replayTimer = null
playPauseButton.actor = playImage
}
private fun sliderChanged(value: Float) {
resetTimer()
updateReplayTable(value.toInt())
} }
private fun updateReplayTable(turn: Int) { private fun updateReplayTable(turn: Int) {
val finalTurn = gameInfo.turns
val year = gameInfo.getYear(turn - finalTurn) val year = gameInfo.getYear(turn - finalTurn)
yearLabel.setText( yearLabel.setText(
YearTextUtil.toYearText( YearTextUtil.toYearText(
year, gameInfo.currentPlayerCiv.isLongCountDisplay() year, gameInfo.currentPlayerCiv.isLongCountDisplay()
) )
) )
slider.value = turn.toFloat()
replayMap.update(turn) replayMap.update(turn)
if (turn == finalTurn) resetTimer()
} }
override fun activated(index: Int, caption: String, pager: TabbedPager) { override fun activated(index: Int, caption: String, pager: TabbedPager) {