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
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.GUI
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.fonts.Fonts
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.MarkupRenderer
@ -311,8 +309,6 @@ object BaseUnitDescriptions {
val info = sequenceOf(FormattedLine(title, color = "#FDA", icon = unitToUpgradeTo.makeLink(), header = 5)) +
getDifferences(ruleset, unitUpgrading, unitToUpgradeTo)
.map { FormattedLine(it.first, icon = it.second ?: "") }
val infoTable = MarkupRenderer.render(info.asIterable(), 400f)
infoTable.background = BaseScreen.skinStrings.getUiBackground("General/Tooltip", BaseScreen.skinStrings.roundedEdgeRectangleShape, Color.DARK_GRAY)
return infoTable
return MarkupRenderer.render(info.asIterable(), 400f)
}
}

View File

@ -26,7 +26,8 @@ import com.unciv.utils.Concurrency
*
* 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".
*
* 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
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Stage
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.models.Counter
import com.unciv.models.UpgradeUnitAction
import com.unciv.models.translations.tr
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.widgets.ColorMarkupLabel
import com.unciv.ui.objectdescriptions.BaseUnitDescriptions
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade
@ -16,9 +20,10 @@ import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade
* similar units.
*
* @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 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 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,
private val unit: MapUnit,
private val unitAction: UpgradeUnitAction,
private val enable: Boolean,
private val callbackAfterAnimation: Boolean = false,
private val onButtonClicked: () -> Unit
) : AnimatedMenuPopup(stage, getActorTopRight(positionNextTo)) {
) : ScrollableAnimatedMenuPopup(stage, getActorTopRight(positionNextTo)) {
private val unitToUpgradeTo by lazy { unitAction.unitToUpgradeTo }
@ -58,16 +64,17 @@ class UnitUpgradeMenu(
else closeListeners.add(action)
}
override fun createContentTable(): Table {
val newInnerTable = BaseUnitDescriptions.getUpgradeInfoTable(
unitAction.title, unit.baseUnit, unitToUpgradeTo
)
newInnerTable.row()
newInnerTable.add(getButton("Upgrade", KeyboardBinding.Upgrade, ::doUpgrade))
.pad(15f, 15f, 5f, 15f).growX().row()
override fun createScrollableContent() =
BaseUnitDescriptions.getUpgradeInfoTable(unitAction.title, unit.baseUnit, unitToUpgradeTo)
override fun createFixedContent() = Table().apply {
val singleButton = getButton("Upgrade", KeyboardBinding.Upgrade, ::doUpgrade)
// Using Gdx standard here, not our extension `isEnabled`: These have full styling
singleButton.isDisabled = !enable
add(singleButton).growX().row()
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?
// - 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 upgradeAllText = "Upgrade all [$allCount] [${unit.name}] ([$allCost] gold)"
val upgradeAllButton = getButton(upgradeAllText, KeyboardBinding.UpgradeAll, ::doAllUpgrade)
upgradeAllButton.isDisabled = unit.civ.gold < allCost ||
allResources.isNotEmpty() &&
unit.civ.getCivResourcesByName().run {
allResources.any {
it.value > (this[it.key] ?: 0)
}
}
newInnerTable.add(upgradeAllButton).pad(2f, 15f).growX().row()
return newInnerTable
val insufficientGold = unit.civ.gold < allCost
val insufficientResources = getInsufficientResourcesMessage(allResources, unit.civ)
upgradeAllButton.isDisabled = insufficientGold || insufficientResources.isNotEmpty()
add(upgradeAllButton).padTop(7f).growX().row()
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())
}
return sb.toString()
}
private fun doUpgrade() {

View File

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

View File

@ -1,6 +1,7 @@
package com.unciv.ui.screens.worldscreen.unit.actions
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.Table
import com.unciv.UncivGame
@ -102,8 +103,13 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
for (unitAction in pageActionBuckets[currentPage]) {
val button = getUnitActionButton(unit, unitAction)
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 {
UnitUpgradeMenu(worldScreen.stage, button, unit, unitAction, callbackAfterAnimation = true) {
UnitUpgradeMenu(worldScreen.stage, button, unit, unitAction, enable = unitAction.action != null, callbackAfterAnimation = true) {
worldScreen.shouldUpdate = true
}
}