diff --git a/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt b/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt index 1f78936197..08d8a07b8a 100644 --- a/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt +++ b/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt @@ -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) } } diff --git a/core/src/com/unciv/ui/popups/AnimatedMenuPopup.kt b/core/src/com/unciv/ui/popups/AnimatedMenuPopup.kt index a2e7ac4aca..cf55662469 100644 --- a/core/src/com/unciv/ui/popups/AnimatedMenuPopup.kt +++ b/core/src/com/unciv/ui/popups/AnimatedMenuPopup.kt @@ -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 diff --git a/core/src/com/unciv/ui/popups/ScrollableAnimatedMenuPopup.kt b/core/src/com/unciv/ui/popups/ScrollableAnimatedMenuPopup.kt new file mode 100644 index 0000000000..a1486b8d26 --- /dev/null +++ b/core/src/com/unciv/ui/popups/ScrollableAnimatedMenuPopup.kt @@ -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 +} diff --git a/core/src/com/unciv/ui/popups/UnitUpgradeMenu.kt b/core/src/com/unciv/ui/popups/UnitUpgradeMenu.kt index 1fddbd6ec8..d903841974 100644 --- a/core/src/com/unciv/ui/popups/UnitUpgradeMenu.kt +++ b/core/src/com/unciv/ui/popups/UnitUpgradeMenu.kt @@ -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, 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() { @@ -95,7 +113,7 @@ class UnitUpgradeMenu( SoundPlayer.playRepeated(unitAction.uncivSound) for (unit in allUpgradableUnits) { val otherAction = UnitActionsUpgrade.getUpgradeActions(unit) - .firstOrNull{ (it as UpgradeUnitAction).unitToUpgradeTo == unitToUpgradeTo && + .firstOrNull{ (it as UpgradeUnitAction).unitToUpgradeTo == unitToUpgradeTo && it.action != null } otherAction?.action?.invoke() } diff --git a/core/src/com/unciv/ui/screens/overviewscreen/UnitOverviewTab.kt b/core/src/com/unciv/ui/screens/overviewscreen/UnitOverviewTab.kt index 2817ff0b47..4d29a8f1e0 100644 --- a/core/src/com/unciv/ui/screens/overviewscreen/UnitOverviewTab.kt +++ b/core/src/com/unciv/ui/screens/overviewscreen/UnitOverviewTab.kt @@ -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) } diff --git a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsTable.kt b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsTable.kt index ddf1dff77f..aa4b03e5cb 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsTable.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsTable.kt @@ -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 } }