Unit upgrade menu can scroll (#11218)

* ScrollableAnimatedMenuPopup widget and base UnitUpgradeMenu off it

* More lack of resources info in UnitUpgradeMenu

* Make UnitUpgradeMenu available even if you can't afford any upgrade
This commit is contained in:
SomeTroglodyte 2024-02-28 22:53:04 +01:00 committed by GitHub
parent 8946ee8bfa
commit 83fb2d048c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 128 additions and 30 deletions

View File

@ -1,6 +1,5 @@
package com.unciv.ui.objectdescriptions package com.unciv.ui.objectdescriptions
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.GUI import com.unciv.GUI
import com.unciv.logic.city.City import com.unciv.logic.city.City
@ -18,7 +17,6 @@ import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.getConsumesAmountString import com.unciv.ui.components.extensions.getConsumesAmountString
import com.unciv.ui.components.fonts.Fonts import com.unciv.ui.components.fonts.Fonts
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.civilopediascreen.FormattedLine import com.unciv.ui.screens.civilopediascreen.FormattedLine
import com.unciv.ui.screens.civilopediascreen.MarkupRenderer import com.unciv.ui.screens.civilopediascreen.MarkupRenderer
@ -311,8 +309,6 @@ object BaseUnitDescriptions {
val info = sequenceOf(FormattedLine(title, color = "#FDA", icon = unitToUpgradeTo.makeLink(), header = 5)) + val info = sequenceOf(FormattedLine(title, color = "#FDA", icon = unitToUpgradeTo.makeLink(), header = 5)) +
getDifferences(ruleset, unitUpgrading, unitToUpgradeTo) getDifferences(ruleset, unitUpgrading, unitToUpgradeTo)
.map { FormattedLine(it.first, icon = it.second ?: "") } .map { FormattedLine(it.first, icon = it.second ?: "") }
val infoTable = MarkupRenderer.render(info.asIterable(), 400f) return MarkupRenderer.render(info.asIterable(), 400f)
infoTable.background = BaseScreen.skinStrings.getUiBackground("General/Tooltip", BaseScreen.skinStrings.roundedEdgeRectangleShape, Color.DARK_GRAY)
return infoTable
} }
} }

View File

@ -26,7 +26,8 @@ import com.unciv.utils.Concurrency
* *
* You must provide content by overriding [createContentTable] - see its doc. * You must provide content by overriding [createContentTable] - see its doc.
* *
* The Popup opens automatically once created. Meant to be used for small menus. * The Popup opens automatically once created.
* **Meant to be used for small menus.** - otherwise use [ScrollableAnimatedMenuPopup].
* No default close button - recommended to simply use "click-behind". * No default close button - recommended to simply use "click-behind".
* *
* The "click-behind" semi-transparent covering of the rest of the stage is much darker than a normal * The "click-behind" semi-transparent covering of the rest of the stage is much darker than a normal

View File

@ -0,0 +1,77 @@
package com.unciv.ui.popups
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.Stage
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.ui.components.widgets.AutoScrollPane
/**
* Adds (partial) scrollability to [AnimatedMenuPopup]. See its doc for details.
*
* Provide content by implementing [createScrollableContent] and [createFixedContent].
* If you need to modify outer wrapper styling, override [createWrapperTable].
*/
abstract class ScrollableAnimatedMenuPopup(
stage: Stage,
position: Vector2
) : AnimatedMenuPopup(stage, position) {
/** The API of this Widget is moved to [createScrollableContent], [createFixedContent], [createWrapperTable]. */
final override fun createContentTable(): Table? {
val top = createScrollableContent()
?: return null
// Build content by wrapping scrollable and fixed parts
val table = createWrapperTable()
val scroll = AutoScrollPane(top)
val scrollCell = table.add(scroll).growX()
table.row()
val bottom = createFixedContent()
if (bottom != null) table.add(bottom)
// ScrollBars need to be told their size
val paddedMaxHeight = maxPopupHeight()
val desiredTotalHeight = table.prefHeight
val desiredScrollHeight = table.getRowPrefHeight(0)
val scrollHeight = if (desiredTotalHeight <= paddedMaxHeight) desiredScrollHeight
else paddedMaxHeight - (desiredTotalHeight - desiredScrollHeight)
val paddedMaxWidth = maxPopupWidth()
val desiredTotalWidth = table.prefWidth
val desiredContentWidth = table.getColumnPrefWidth(0)
val scrollWidth = if (desiredTotalWidth <= paddedMaxHeight) desiredContentWidth
else paddedMaxWidth - (desiredTotalWidth - desiredContentWidth)
scrollCell.size(scrollWidth, scrollHeight)
return table
}
/** Provides an empty wrapper Table.
*
* Override only to change styling.
* By default, a rounded edge dark gray background and 5f vertical / 15f horizontal padding for the two halves is used. */
open fun createWrapperTable(): Table = super.createContentTable()!!
/** Provides the scrollable top part
* @return `null` to abort opening the entire Popup */
abstract fun createScrollableContent(): Table?
/** Provides the fixed bottom part
* @return `null` to make the entire Popup scrollable (so the fixed part takes no vertical space, not even the default padding) */
abstract fun createFixedContent(): Table?
/** Determines maximum usable width
*
* Use [stageToShowOn] to measure the Stage (from the underlying [Popup]).
* Do not use [Actor.stage][stage], it is uninitialized at this point.
*/
open fun maxPopupWidth() = 0.95f * stageToShowOn.width - 5f
/** Determines maximum usable height
*
* Use [stageToShowOn] to measure the Stage (from the underlying [Popup]).
* Do not use [Actor.stage][stage], it is uninitialized at this point.
*/
open fun maxPopupHeight() = 0.95f * stageToShowOn.height - 5f
}

View File

@ -1,13 +1,17 @@
package com.unciv.ui.popups package com.unciv.ui.popups
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Stage import com.badlogic.gdx.scenes.scene2d.Stage
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.models.Counter
import com.unciv.models.UpgradeUnitAction import com.unciv.models.UpgradeUnitAction
import com.unciv.models.translations.tr
import com.unciv.ui.audio.SoundPlayer import com.unciv.ui.audio.SoundPlayer
import com.unciv.ui.components.extensions.pad
import com.unciv.ui.components.input.KeyboardBinding import com.unciv.ui.components.input.KeyboardBinding
import com.unciv.ui.components.widgets.ColorMarkupLabel
import com.unciv.ui.objectdescriptions.BaseUnitDescriptions import com.unciv.ui.objectdescriptions.BaseUnitDescriptions
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade
@ -16,9 +20,10 @@ import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade
* similar units. * similar units.
* *
* @param stage The stage this will be shown on, passed to Popup and used for clamping **`position`** * @param stage The stage this will be shown on, passed to Popup and used for clamping **`position`**
* @param position stage coordinates to show this centered over - clamped so that nothing is clipped outside the [stage] * @param positionNextTo stage coordinates to show this centered over - clamped so that nothing is clipped outside the [stage]
* @param unit Who is ready to upgrade? * @param unit Who is ready to upgrade?
* @param unitAction Holds pre-calculated info like unitToUpgradeTo, cost or resource requirements. Its action is mapped to the Upgrade button. * @param unitAction Holds pre-calculated info like unitToUpgradeTo, cost or resource requirements. Its action is mapped to the Upgrade button.
* @param enable Whether the buttons should be enabled - allows use to display benefits when you can't actually afford them.
* @param callbackAfterAnimation If true the following will be delayed until the Popup is actually closed (Stage.hasOpenPopups returns false). * @param callbackAfterAnimation If true the following will be delayed until the Popup is actually closed (Stage.hasOpenPopups returns false).
* @param onButtonClicked A callback after one or several upgrades have been performed (and the menu is about to close) * @param onButtonClicked A callback after one or several upgrades have been performed (and the menu is about to close)
*/ */
@ -33,9 +38,10 @@ class UnitUpgradeMenu(
positionNextTo: Actor, positionNextTo: Actor,
private val unit: MapUnit, private val unit: MapUnit,
private val unitAction: UpgradeUnitAction, private val unitAction: UpgradeUnitAction,
private val enable: Boolean,
private val callbackAfterAnimation: Boolean = false, private val callbackAfterAnimation: Boolean = false,
private val onButtonClicked: () -> Unit private val onButtonClicked: () -> Unit
) : AnimatedMenuPopup(stage, getActorTopRight(positionNextTo)) { ) : ScrollableAnimatedMenuPopup(stage, getActorTopRight(positionNextTo)) {
private val unitToUpgradeTo by lazy { unitAction.unitToUpgradeTo } private val unitToUpgradeTo by lazy { unitAction.unitToUpgradeTo }
@ -58,16 +64,17 @@ class UnitUpgradeMenu(
else closeListeners.add(action) else closeListeners.add(action)
} }
override fun createContentTable(): Table { override fun createScrollableContent() =
val newInnerTable = BaseUnitDescriptions.getUpgradeInfoTable( BaseUnitDescriptions.getUpgradeInfoTable(unitAction.title, unit.baseUnit, unitToUpgradeTo)
unitAction.title, unit.baseUnit, unitToUpgradeTo
) override fun createFixedContent() = Table().apply {
newInnerTable.row() val singleButton = getButton("Upgrade", KeyboardBinding.Upgrade, ::doUpgrade)
newInnerTable.add(getButton("Upgrade", KeyboardBinding.Upgrade, ::doUpgrade)) // Using Gdx standard here, not our extension `isEnabled`: These have full styling
.pad(15f, 15f, 5f, 15f).growX().row() singleButton.isDisabled = !enable
add(singleButton).growX().row()
val allCount = allUpgradableUnits.count() val allCount = allUpgradableUnits.count()
if (allCount <= 1) return newInnerTable if (allCount <= 1) return@apply
// Note - all same-baseunit units cost the same to upgrade? What if a mod says e.g. 50% discount on Oasis? // Note - all same-baseunit units cost the same to upgrade? What if a mod says e.g. 50% discount on Oasis?
// - As far as I can see the rest of the upgrading code doesn't support such conditions at the moment. // - As far as I can see the rest of the upgrading code doesn't support such conditions at the moment.
@ -75,15 +82,26 @@ class UnitUpgradeMenu(
val allResources = unitAction.newResourceRequirements * allCount val allResources = unitAction.newResourceRequirements * allCount
val upgradeAllText = "Upgrade all [$allCount] [${unit.name}] ([$allCost] gold)" val upgradeAllText = "Upgrade all [$allCount] [${unit.name}] ([$allCost] gold)"
val upgradeAllButton = getButton(upgradeAllText, KeyboardBinding.UpgradeAll, ::doAllUpgrade) val upgradeAllButton = getButton(upgradeAllText, KeyboardBinding.UpgradeAll, ::doAllUpgrade)
upgradeAllButton.isDisabled = unit.civ.gold < allCost || val insufficientGold = unit.civ.gold < allCost
allResources.isNotEmpty() && val insufficientResources = getInsufficientResourcesMessage(allResources, unit.civ)
unit.civ.getCivResourcesByName().run { upgradeAllButton.isDisabled = insufficientGold || insufficientResources.isNotEmpty()
allResources.any { add(upgradeAllButton).padTop(7f).growX().row()
it.value > (this[it.key] ?: 0) if (insufficientResources.isEmpty()) return@apply
val label = ColorMarkupLabel(insufficientResources, Color.SCARLET)
add(label).center()
} }
private fun getInsufficientResourcesMessage(requiredResources: Counter<String>, civ: Civilization): String {
if (requiredResources.isEmpty()) return ""
val available = civ.getCivResourcesByName()
val sb = StringBuilder()
for ((name, amount) in requiredResources) {
val difference = amount - (available[name] ?: 0)
if (difference <= 0) continue
if (sb.isEmpty()) sb.append('\n')
sb.append("Need [$difference] more [$name]".tr())
} }
newInnerTable.add(upgradeAllButton).pad(2f, 15f).growX().row() return sb.toString()
return newInnerTable
} }
private fun doUpgrade() { private fun doUpgrade() {

View File

@ -281,8 +281,8 @@ class UnitOverviewTab(
val selectKey = getUnitIdentifier(unit, unitToUpgradeTo) val selectKey = getUnitIdentifier(unit, unitToUpgradeTo)
val upgradeIcon = ImageGetter.getUnitIcon(unitToUpgradeTo.name, val upgradeIcon = ImageGetter.getUnitIcon(unitToUpgradeTo.name,
if (enable) Color.GREEN else Color.GREEN.darken(0.5f)) if (enable) Color.GREEN else Color.GREEN.darken(0.5f))
if (enable) upgradeIcon.onClick { upgradeIcon.onClick {
UnitUpgradeMenu(overviewScreen.stage, upgradeIcon, unit, unitAction) { UnitUpgradeMenu(overviewScreen.stage, upgradeIcon, unit, unitAction, enable) {
unitListTable.updateUnitListTable() unitListTable.updateUnitListTable()
select(selectKey) select(selectKey)
} }

View File

@ -1,6 +1,7 @@
package com.unciv.ui.screens.worldscreen.unit.actions package com.unciv.ui.screens.worldscreen.unit.actions
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Button import com.badlogic.gdx.scenes.scene2d.ui.Button
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.UncivGame import com.unciv.UncivGame
@ -102,8 +103,13 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
for (unitAction in pageActionBuckets[currentPage]) { for (unitAction in pageActionBuckets[currentPage]) {
val button = getUnitActionButton(unit, unitAction) val button = getUnitActionButton(unit, unitAction)
if (unitAction is UpgradeUnitAction) { if (unitAction is UpgradeUnitAction) {
// This is bound even when the button is disabled, but Actor.activate in ActivationExtensions will block any activation for disabled actors...
// But the menu is built to be useful even when you can't upgrade - so **hack** it to get the handler through.
// Works because our disable() extension also changes style, and because the normal click is ignored due to unitAction.action being null.
button.isDisabled = false
button.touchable = Touchable.enabled
button.onRightClick { button.onRightClick {
UnitUpgradeMenu(worldScreen.stage, button, unit, unitAction, callbackAfterAnimation = true) { UnitUpgradeMenu(worldScreen.stage, button, unit, unitAction, enable = unitAction.action != null, callbackAfterAnimation = true) {
worldScreen.shouldUpdate = true worldScreen.shouldUpdate = true
} }
} }