diff --git a/core/src/com/unciv/ui/audio/SoundPlayer.kt b/core/src/com/unciv/ui/audio/SoundPlayer.kt
index 56f413150c..43ea323ff3 100644
--- a/core/src/com/unciv/ui/audio/SoundPlayer.kt
+++ b/core/src/com/unciv/ui/audio/SoundPlayer.kt
@@ -160,9 +160,12 @@ object SoundPlayer {
* and lastly Unciv's own assets/sounds. Will fail silently if the sound file cannot be found.
*
* This will wait for the Stream to become ready (Android issue) if necessary, and do so on a
- * separate thread. No new thread is created if the sound can be played immediately.
+ * separate thread. **No new thread is created** if the sound can be played immediately.
+ *
+ * That also means that it's the caller's responsibility to ensure calling this only on the GL thread.
*
* @param sound The sound to play
+ * @see playRepeated
*/
fun play(sound: UncivSound) {
val volume = UncivGame.Current.settings.soundEffectsVolume
@@ -177,4 +180,20 @@ object SoundPlayer {
}
}
}
+
+ /** Play a sound repeatedly - e.g. to express that an action was applied multiple times or to multiple targets.
+ *
+ * Runs the actual sound player decoupled on the GL thread unlike [SoundPlayer.play], which leaves that responsibility to the caller.
+ */
+ fun playRepeated(sound: UncivSound, count: Int = 2, delay: Long = 200) {
+ Concurrency.runOnGLThread {
+ SoundPlayer.play(sound)
+ if (count > 1) Concurrency.run {
+ repeat(count - 1) {
+ delay(delay)
+ Concurrency.runOnGLThread { SoundPlayer.play(sound) }
+ }
+ }
+ }
+ }
}
diff --git a/core/src/com/unciv/ui/components/input/KeyboardBinding.kt b/core/src/com/unciv/ui/components/input/KeyboardBinding.kt
index 10491f20e4..220812d5b0 100644
--- a/core/src/com/unciv/ui/components/input/KeyboardBinding.kt
+++ b/core/src/com/unciv/ui/components/input/KeyboardBinding.kt
@@ -116,6 +116,7 @@ enum class KeyboardBinding(
// Popups
Confirm(Category.Popups, "Confirm Dialog", 'y'),
Cancel(Category.Popups, "Cancel Dialog", 'n'),
+ UpgradeAll(Category.Popups, KeyCharAndCode.ctrl('a')),
;
//endregion
diff --git a/core/src/com/unciv/ui/popups/AnimatedMenuPopup.kt b/core/src/com/unciv/ui/popups/AnimatedMenuPopup.kt
new file mode 100644
index 0000000000..3799706139
--- /dev/null
+++ b/core/src/com/unciv/ui/popups/AnimatedMenuPopup.kt
@@ -0,0 +1,182 @@
+package com.unciv.ui.popups
+
+import com.badlogic.gdx.graphics.Color
+import com.badlogic.gdx.graphics.g2d.NinePatch
+import com.badlogic.gdx.math.Interpolation
+import com.badlogic.gdx.math.Vector2
+import com.badlogic.gdx.scenes.scene2d.Stage
+import com.badlogic.gdx.scenes.scene2d.Touchable
+import com.badlogic.gdx.scenes.scene2d.actions.Actions
+import com.badlogic.gdx.scenes.scene2d.ui.Container
+import com.badlogic.gdx.scenes.scene2d.ui.Table
+import com.badlogic.gdx.scenes.scene2d.ui.TextButton
+import com.badlogic.gdx.scenes.scene2d.utils.NinePatchDrawable
+import com.unciv.ui.components.extensions.toTextButton
+import com.unciv.ui.components.input.KeyCharAndCode
+import com.unciv.ui.components.input.KeyboardBinding
+import com.unciv.ui.components.input.keyShortcuts
+import com.unciv.ui.components.input.onActivation
+import com.unciv.ui.images.ImageGetter
+import com.unciv.ui.screens.basescreen.BaseScreen
+import com.unciv.utils.Concurrency
+
+/**
+ * A popup menu that animates on open/close, centered on a given Position (unlike other [Popup]s which are always stage-centered).
+ *
+ * You must provide content by overriding [createContentTable] - see its doc.
+ *
+ * The Popup opens automatically once created. Meant to be used for small menus.
+ * 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
+ * Popup (give the impression to take away illumination and spotlight the menu) and fades in together
+ * with the AnimatedMenuPopup itself. Closing the menu in any of the four ways will fade out everything
+ * inverting the fade-and-scale-in. Callbacks registered with [Popup.closeListeners] will run before the animation starts.
+ * Use [afterCloseCallback] instead if you need a notification after the animation finishes and the Popup is cleaned up.
+ *
+ * @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]
+ */
+open class AnimatedMenuPopup(
+ stage: Stage,
+ position: Vector2
+) : Popup(stage, Scrollability.None) {
+ private val container: Container
= Container()
+ private val animationDuration = 0.33f
+ private val backgroundColor = (background as NinePatchDrawable).patch.color
+ private val smallButtonStyle by lazy { SmallButtonStyle() }
+
+ /** Will be notified after this Popup is closed, the animation finished, and cleanup is done (removed from stage). */
+ var afterCloseCallback: (() -> Unit)? = null
+
+ /** Allows differentiating the close reason in [afterCloseCallback] or [closeListeners]
+ * When still `false` in a callback, then ESC/BACK or the click-behind listener closed this. */
+ var anyButtonWasClicked = false
+ private set
+
+ /**
+ * Provides the Popup content.
+ *
+ * Call super to fetch an empty default with prepared padding and background.
+ * You can use [getButton], which produces TextButtons slightly smaller than Unciv's default ones.
+ * The content adding functions offered by [Popup] or [Table] won't work.
+ * The content needs to be complete when the method finishes, it will be `pack()`ed and measured immediately.
+ */
+ open fun createContentTable() = Table().apply {
+ defaults().pad(5f, 15f, 5f, 15f).growX()
+ background = BaseScreen.skinStrings.getUiBackground("General/AnimatedMenu", BaseScreen.skinStrings.roundedEdgeRectangleShape, Color.DARK_GRAY)
+ }
+
+ init {
+ clickBehindToClose = true
+ keyShortcuts.add(KeyCharAndCode.BACK) { close() }
+ innerTable.remove()
+
+ // Decouple the content creation from object initialization so it can access its own fields
+ // (initialization order super->sub - see LeakingThis)
+ Concurrency.runOnGLThread { createAndShow(position) }
+ }
+
+ private fun createAndShow(position: Vector2) {
+ val newInnerTable = createContentTable()
+ newInnerTable.pack()
+ container.actor = newInnerTable
+ container.touchable = Touchable.childrenOnly
+ container.isTransform = true
+ container.setScale(0.05f)
+ container.color.a = 0f
+
+ open(true) // this only does the screen-covering "click-behind" portion
+
+ container.setPosition(
+ position.x.coerceAtMost(stage.width - newInnerTable.width / 2),
+ position.y.coerceAtLeast(newInnerTable.height / 2)
+ )
+ super.addActor(container)
+
+ // This "zoomfades" the container "in"
+ container.addAction(
+ Actions.parallel(
+ Actions.scaleTo(1f, 1f, animationDuration, Interpolation.fade),
+ Actions.fadeIn(animationDuration, Interpolation.fade)
+ ))
+
+ // This gradually darkens the "outside" at the same time
+ backgroundColor.set(0)
+ super.addAction(Actions.alpha(0.35f, animationDuration, Interpolation.fade).apply {
+ color = backgroundColor
+ })
+ }
+
+ override fun close() {
+ val toNotify = closeListeners.toList()
+ closeListeners.clear()
+ for (listener in toNotify) listener()
+
+ addAction(Actions.alpha(0f, animationDuration, Interpolation.fade).apply {
+ color = backgroundColor
+ })
+ container.addAction(
+ Actions.sequence(
+ Actions.parallel(
+ Actions.scaleTo(0.05f, 0.05f, animationDuration, Interpolation.fade),
+ Actions.fadeOut(animationDuration, Interpolation.fade)
+ ),
+ Actions.run {
+ container.remove()
+ super.close()
+ afterCloseCallback?.invoke()
+ }
+ )
+ )
+ }
+
+ /**
+ * Creates a button - for use in [AnimatedMenuPopup]'s `contentBuilder` parameter.
+ *
+ * On activation it will set [anyButtonWasClicked], call [action], then close the Popup.
+ */
+ fun getButton(text: String, binding: KeyboardBinding, action: () -> Unit) =
+ text.toTextButton(smallButtonStyle).apply {
+ onActivation(binding = binding) {
+ anyButtonWasClicked = true
+ action()
+ close()
+ }
+ }
+
+ class SmallButtonStyle : TextButton.TextButtonStyle(BaseScreen.skin[TextButton.TextButtonStyle::class.java]) {
+ /** Modify NinePatch geometry so the roundedEdgeRectangleMidShape button is 38f high instead of 48f,
+ * Otherwise this excercise would be futile - normal roundedEdgeRectangleShape based buttons are 50f high.
+ */
+ private fun NinePatchDrawable.reduce(): NinePatchDrawable {
+ val patch = NinePatch(this.patch)
+ patch.padTop = 10f
+ patch.padBottom = 10f
+ patch.topHeight = 10f
+ patch.bottomHeight = 10f
+ return NinePatchDrawable(this).also { it.patch = patch }
+ }
+
+ init {
+ val upColor = BaseScreen.skin.getColor("color")
+ val downColor = BaseScreen.skin.getColor("pressed")
+ val overColor = BaseScreen.skin.getColor("highlight")
+ val disabledColor = BaseScreen.skin.getColor("disabled")
+ // UiElementDocsWriter inspects source, which is why this isn't prettified better
+ val shape = BaseScreen.run {
+ // Let's use _one_ skinnable background lookup but with different tints
+ val skinned = skinStrings.getUiBackground("AnimatedMenu/Button", skinStrings.roundedEdgeRectangleMidShape)
+ // Reduce height only if not skinned
+ val default = ImageGetter.getNinePatch(skinStrings.roundedEdgeRectangleMidShape)
+ if (skinned === default) default.reduce() else skinned
+ }
+ // Now get the tinted variants
+ up = shape.tint(upColor)
+ down = shape.tint(downColor)
+ over = shape.tint(overColor)
+ disabled = shape.tint(disabledColor)
+ disabledFontColor = Color.GRAY
+ }
+ }
+}
diff --git a/core/src/com/unciv/ui/popups/UnitUpgradeMenu.kt b/core/src/com/unciv/ui/popups/UnitUpgradeMenu.kt
new file mode 100644
index 0000000000..5bd9302fe5
--- /dev/null
+++ b/core/src/com/unciv/ui/popups/UnitUpgradeMenu.kt
@@ -0,0 +1,99 @@
+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.logic.map.mapunit.MapUnit
+import com.unciv.models.UpgradeUnitAction
+import com.unciv.ui.audio.SoundPlayer
+import com.unciv.ui.components.extensions.pad
+import com.unciv.ui.components.input.KeyboardBinding
+import com.unciv.ui.objectdescriptions.BaseUnitDescriptions
+import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade
+
+/**
+ * A popup menu showing info about an Unit upgrade, with buttons to upgrade "this" unit or _all_
+ * 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 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 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)
+ */
+/*
+ Note - callbackAfterAnimation has marginal value: When this is called from UnitOverview, where the
+ callback updates the upgrade symbol column, that can happen before/while the animation plays.
+ Called from the WorldScreen, to set shouldUpdate, that **needs** to fire late, or else the update is wasted.
+ Therefore, simplifying to always use afterCloseCallback would only be visible to the quick keen eye.
+ */
+class UnitUpgradeMenu(
+ stage: Stage,
+ position: Vector2,
+ private val unit: MapUnit,
+ private val unitAction: UpgradeUnitAction,
+ private val callbackAfterAnimation: Boolean = false,
+ private val onButtonClicked: () -> Unit
+) : AnimatedMenuPopup(stage, position) {
+
+ private val allUpgradableUnits: Sequence by lazy {
+ unit.civ.units.getCivUnits()
+ .filter {
+ it.baseUnit.name == unit.baseUnit.name
+ && it.currentMovement > 0f
+ && it.currentTile.getOwner() == unit.civ
+ && !it.isEmbarked()
+ && it.upgrade.canUpgrade(unitAction.unitToUpgradeTo, ignoreResources = true)
+ }
+ }
+
+ init {
+ val action = {
+ if (anyButtonWasClicked) onButtonClicked()
+ }
+ if (callbackAfterAnimation) afterCloseCallback = action
+ else closeListeners.add(action)
+ }
+
+ override fun createContentTable(): Table {
+ val newInnerTable = BaseUnitDescriptions.getUpgradeInfoTable(
+ unitAction.title, unit.baseUnit, unitAction.unitToUpgradeTo
+ )
+ newInnerTable.row()
+ newInnerTable.add(getButton("Upgrade", KeyboardBinding.Upgrade, ::doUpgrade))
+ .pad(15f, 15f, 5f, 15f).growX().row()
+
+ val allCount = allUpgradableUnits.count()
+ if (allCount <= 1) return newInnerTable
+
+ // 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.
+ val allCost = unitAction.goldCostOfUpgrade * allCount
+ 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
+ }
+
+ private fun doUpgrade() {
+ SoundPlayer.play(unitAction.uncivSound)
+ unitAction.action!!()
+ }
+
+ private fun doAllUpgrade() {
+ SoundPlayer.playRepeated(unitAction.uncivSound)
+ for (unit in allUpgradableUnits) {
+ val otherAction = UnitActionsUpgrade.getUpgradeAction(unit)
+ 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 fc4e907c2e..6079547778 100644
--- a/core/src/com/unciv/ui/screens/overviewscreen/UnitOverviewTab.kt
+++ b/core/src/com/unciv/ui/screens/overviewscreen/UnitOverviewTab.kt
@@ -32,6 +32,7 @@ import com.unciv.ui.components.extensions.toPrettyString
import com.unciv.ui.components.input.onClick
import com.unciv.ui.images.IconTextButton
import com.unciv.ui.images.ImageGetter
+import com.unciv.ui.popups.UnitUpgradeMenu
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.pickerscreens.PromotionPickerScreen
import com.unciv.ui.screens.pickerscreens.UnitRenamePopup
diff --git a/core/src/com/unciv/ui/screens/overviewscreen/UnitUpgradeMenu.kt b/core/src/com/unciv/ui/screens/overviewscreen/UnitUpgradeMenu.kt
deleted file mode 100644
index a5a815e23a..0000000000
--- a/core/src/com/unciv/ui/screens/overviewscreen/UnitUpgradeMenu.kt
+++ /dev/null
@@ -1,217 +0,0 @@
-package com.unciv.ui.screens.overviewscreen
-
-import com.badlogic.gdx.graphics.Color
-import com.badlogic.gdx.graphics.g2d.NinePatch
-import com.badlogic.gdx.math.Interpolation
-import com.badlogic.gdx.math.Vector2
-import com.badlogic.gdx.scenes.scene2d.Stage
-import com.badlogic.gdx.scenes.scene2d.Touchable
-import com.badlogic.gdx.scenes.scene2d.actions.Actions
-import com.badlogic.gdx.scenes.scene2d.ui.Container
-import com.badlogic.gdx.scenes.scene2d.ui.Table
-import com.badlogic.gdx.scenes.scene2d.ui.TextButton
-import com.badlogic.gdx.scenes.scene2d.utils.NinePatchDrawable
-import com.unciv.logic.map.mapunit.MapUnit
-import com.unciv.models.UpgradeUnitAction
-import com.unciv.ui.audio.SoundPlayer
-import com.unciv.ui.components.input.KeyCharAndCode
-import com.unciv.ui.components.input.KeyboardBinding
-import com.unciv.ui.components.input.keyShortcuts
-import com.unciv.ui.components.input.onActivation
-import com.unciv.ui.components.extensions.pad
-import com.unciv.ui.components.extensions.toTextButton
-import com.unciv.ui.images.ImageGetter
-import com.unciv.ui.objectdescriptions.BaseUnitDescriptions
-import com.unciv.ui.popups.Popup
-import com.unciv.ui.screens.basescreen.BaseScreen
-import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade
-
-//TODO When this gets reused. e.g. from UnitActionsTable, move to another package.
-
-/**
- * A popup menu showing info about an Unit upgrade, with buttons to upgrade "this" unit or _all_
- * similar units.
- *
- * Meant to animate "in" at a given position - unlike other [Popup]s which are always stage-centered.
- * No close button - use "click-behind".
- * The "click-behind" semi-transparent covering of the rest of the stage is much darker than a normal
- * Popup (give the impression to take away illumination and spotlight the menu) and fades in together
- * with the UnitUpgradeMenu itself. Closing the menu in any of the four ways will fade out everything
- * inverting the fade-and-scale-in.
- *
- * @param stage The stage this will be shown on, passed to Popup and used for clamping **`position`**
- * @param position stage coortinates 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 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)
- */
-class UnitUpgradeMenu(
- stage: Stage,
- position: Vector2,
- private val unit: MapUnit,
- private val unitAction: UpgradeUnitAction,
- private val callbackAfterAnimation: Boolean = false,
- private val onButtonClicked: () -> Unit
-) : Popup(stage, Scrollability.None) {
- private val container: Container
- private val allUpgradableUnits: Sequence
- private val animationDuration = 0.33f
- private val backgroundColor = (background as NinePatchDrawable).patch.color
- private var afterCloseCallback: (() -> Unit)? = null
-
- init {
- innerTable.remove()
-
- // Note: getUpgradeInfoTable skins this as General/Tooltip, roundedEdgeRectangle, DARK_GRAY
- // TODO - own skinnable path, possibly when tooltip use of getUpgradeInfoTable gets replaced
- val newInnerTable = BaseUnitDescriptions.getUpgradeInfoTable(
- unitAction.title, unit.baseUnit, unitAction.unitToUpgradeTo
- )
-
- newInnerTable.row()
- val smallButtonStyle = SmallButtonStyle()
- val upgradeButton = "Upgrade".toTextButton(smallButtonStyle)
- upgradeButton.onActivation(::doUpgrade)
- upgradeButton.keyShortcuts.add(KeyboardBinding.Confirm)
- newInnerTable.add(upgradeButton).pad(15f, 15f, 5f, 15f).growX().row()
-
- allUpgradableUnits = unit.civ.units.getCivUnits()
- .filter {
- it.baseUnit.name == unit.baseUnit.name
- && it.currentMovement > 0f
- && it.currentTile.getOwner() == unit.civ
- && !it.isEmbarked()
- && it.upgrade.canUpgrade(unitAction.unitToUpgradeTo, ignoreResources = true)
- }
- newInnerTable.tryAddUpgradeAllUnitsButton(smallButtonStyle)
-
- clickBehindToClose = true
- keyShortcuts.add(KeyCharAndCode.BACK) { close() }
-
- newInnerTable.pack()
- container = Container(newInnerTable)
- container.touchable = Touchable.childrenOnly
- container.isTransform = true
- container.setScale(0.05f)
- container.color.a = 0f
-
- open(true) // this only does the screen-covering "click-behind" portion
-
- container.setPosition(
- position.x.coerceAtMost(stage.width - newInnerTable.width / 2),
- position.y.coerceAtLeast(newInnerTable.height / 2)
- )
- addActor(container)
-
- container.addAction(
- Actions.parallel(
- Actions.scaleTo(1f, 1f, animationDuration, Interpolation.fade),
- Actions.fadeIn(animationDuration, Interpolation.fade)
- ))
-
- backgroundColor.set(0)
- addAction(Actions.alpha(0.35f, animationDuration, Interpolation.fade).apply {
- color = backgroundColor
- })
- }
-
- private fun Table.tryAddUpgradeAllUnitsButton(buttonStyle: TextButton.TextButtonStyle) {
- val allCount = allUpgradableUnits.count()
- if (allCount <= 1) return
- // 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.
- val allCost = unitAction.goldCostOfUpgrade * allCount
- val allResources = unitAction.newResourceRequirements * allCount
- val upgradeAllButton = "Upgrade all [$allCount] [${unit.name}] ([$allCost] gold)"
- .toTextButton(buttonStyle)
- upgradeAllButton.isDisabled = unit.civ.gold < allCost ||
- allResources.isNotEmpty() &&
- unit.civ.getCivResourcesByName().run {
- allResources.any {
- it.value > (this[it.key] ?: 0)
- }
- }
- upgradeAllButton.onActivation(::doAllUpgrade)
- add(upgradeAllButton).pad(2f, 15f).growX().row()
- }
-
- private fun doUpgrade() {
- SoundPlayer.play(unitAction.uncivSound)
- unitAction.action!!()
- launchCallbackAndClose()
- }
-
- private fun doAllUpgrade() {
- stage.addAction(
- Actions.sequence(
- Actions.run { SoundPlayer.play(unitAction.uncivSound) },
- Actions.delay(0.2f),
- Actions.run { SoundPlayer.play(unitAction.uncivSound) }
- ))
- for (unit in allUpgradableUnits) {
- val otherAction = UnitActionsUpgrade.getUpgradeAction(unit)
- otherAction?.action?.invoke()
- }
- launchCallbackAndClose()
- }
-
- private fun launchCallbackAndClose() {
- if (callbackAfterAnimation) afterCloseCallback = onButtonClicked
- else onButtonClicked()
- close()
- }
-
- override fun close() {
- addAction(Actions.alpha(0f, animationDuration, Interpolation.fade).apply {
- color = backgroundColor
- })
- container.addAction(
- Actions.sequence(
- Actions.parallel(
- Actions.scaleTo(0.05f, 0.05f, animationDuration, Interpolation.fade),
- Actions.fadeOut(animationDuration, Interpolation.fade)
- ),
- Actions.run {
- container.remove()
- super.close()
- afterCloseCallback?.invoke()
- }
- ))
- }
-
- class SmallButtonStyle : TextButton.TextButtonStyle(BaseScreen.skin[TextButton.TextButtonStyle::class.java]) {
- /** Modify NinePatch geometry so the roundedEdgeRectangleMidShape button is 38f high instead of 48f,
- * Otherwise this excercise would be futile - normal roundedEdgeRectangleShape based buttons are 50f high.
- */
- private fun NinePatchDrawable.reduce(): NinePatchDrawable {
- val patch = NinePatch(this.patch)
- patch.padTop = 10f
- patch.padBottom = 10f
- patch.topHeight = 10f
- patch.bottomHeight = 10f
- return NinePatchDrawable(this).also { it.patch = patch }
- }
-
- init {
- val upColor = BaseScreen.skin.getColor("color")
- val downColor = BaseScreen.skin.getColor("pressed")
- val overColor = BaseScreen.skin.getColor("highlight")
- val disabledColor = BaseScreen.skin.getColor("disabled")
- // UiElementDocsWriter inspects source, which is why this isn't prettified better
- val shape = BaseScreen.run {
- // Let's use _one_ skinnable background lookup but with different tints
- val skinned = skinStrings.getUiBackground("UnitUpgradeMenu/Button", skinStrings.roundedEdgeRectangleMidShape)
- // Reduce height only if not skinned
- val default = ImageGetter.getNinePatch(skinStrings.roundedEdgeRectangleMidShape)
- if (skinned === default) default.reduce() else skinned
- }
- // Now get the tinted variants
- up = shape.tint(upColor)
- down = shape.tint(downColor)
- over = shape.tint(overColor)
- disabled = shape.tint(disabledColor)
- disabledFontColor = Color.GRAY
- }
- }
-}
diff --git a/core/src/com/unciv/ui/screens/pickerscreens/PromotionPickerScreen.kt b/core/src/com/unciv/ui/screens/pickerscreens/PromotionPickerScreen.kt
index ba068eb0f2..2c773df040 100644
--- a/core/src/com/unciv/ui/screens/pickerscreens/PromotionPickerScreen.kt
+++ b/core/src/com/unciv/ui/screens/pickerscreens/PromotionPickerScreen.kt
@@ -21,8 +21,6 @@ import com.unciv.ui.components.input.onDoubleClick
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.basescreen.RecreateOnResize
-import com.unciv.utils.Concurrency
-import kotlinx.coroutines.delay
import kotlin.math.abs
class PromotionPickerScreen(
@@ -95,19 +93,8 @@ class PromotionPickerScreen(
// if user managed to click disabled button, still do nothing
if (button == null || !button.isPickable) return
- // Can't use stage.addAction as the screen is going to die immediately
val path = tree.getPathTo(button.node.promotion)
- if (path.size == 1) {
- Concurrency.runOnGLThread { SoundPlayer.play(UncivSound.Promote) }
- } else {
- Concurrency.runOnGLThread {
- SoundPlayer.play(UncivSound.Promote)
- Concurrency.run {
- delay(200)
- Concurrency.runOnGLThread { SoundPlayer.play(UncivSound.Promote) }
- }
- }
- }
+ SoundPlayer.playRepeated(UncivSound.Promote, path.size.coerceAtMost(2))
for (promotion in path)
unit.promotions.addPromotion(promotion.name)
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 2147bda19e..eb1caa757c 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
@@ -16,7 +16,7 @@ import com.unciv.ui.components.input.keyShortcuts
import com.unciv.ui.components.input.onActivation
import com.unciv.ui.components.input.onRightClick
import com.unciv.ui.images.IconTextButton
-import com.unciv.ui.screens.overviewscreen.UnitUpgradeMenu
+import com.unciv.ui.popups.UnitUpgradeMenu
import com.unciv.ui.screens.worldscreen.WorldScreen
class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
diff --git a/docs/Modders/Creating-a-UI-skin.md b/docs/Modders/Creating-a-UI-skin.md
index 751c425008..314ebd8050 100644
--- a/docs/Modders/Creating-a-UI-skin.md
+++ b/docs/Modders/Creating-a-UI-skin.md
@@ -34,6 +34,7 @@ These shapes are used all over Unciv and can be replaced to make a lot of UI ele
| Directory | Name | Default shape | Image |
|---|:---:|:---:|---|
+| AnimatedMenu/ | Button | roundedEdgeRectangleMid | |
| CityScreen/ | CityPickerTable | roundedEdgeRectangle | |
| CityScreen/CitizenManagementTable/ | AvoidCell | null | |
| CityScreen/CitizenManagementTable/ | FocusCell | null | |
@@ -52,6 +53,7 @@ These shapes are used all over Unciv and can be replaced to make a lot of UI ele
| CityScreen/ConstructionInfoTable/ | Background | null | |
| CityScreen/ConstructionInfoTable/ | SelectedConstructionTable | null | |
| CivilopediaScreen/ | EntryButton | null | |
+| General/ | AnimatedMenu | roundedEdgeRectangle | |
| General/ | Border | null | |
| General/ | ExpanderTab | null | |
| General/ | HealthBar | null | |
@@ -103,7 +105,6 @@ These shapes are used all over Unciv and can be replaced to make a lot of UI ele
| TechPickerScreen/ | ResearchedFutureTechColor | 127, 50, 0 | |
| TechPickerScreen/ | ResearchedTechColor | 255, 215, 0 | |
| TechPickerScreen/ | TechButtonIconsOutline | roundedEdgeRectangleSmall | |
-| UnitUpgradeMenu/ | Button | roundedEdgeRectangleMid | |
| VictoryScreen/ | CivGroup | roundedEdgeRectangle | |
| WorldScreen/ | AirUnitTable | null | |
| WorldScreen/ | BattleTable | null | |