Unit actions "paging" new architecture (#10988)

* Linting (and prioritize addGiftAction efficiency over the get used only by unit test)

* Move "paging" out of MapUnit, drop Normal/Additional getter split, "page" preference via UnitActionType

* Optimize UnitActions API a little - fallback does not enumerate actionTypeToFunctions again

* Cleaner access to GiftUnit action from unit test

* Allow freely switching from Sleep to Sleep-until-healed and back
This commit is contained in:
SomeTroglodyte 2024-01-24 22:43:31 +01:00 committed by GitHub
parent dabd105cf8
commit e576b03fc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 202 additions and 141 deletions

View File

@ -313,7 +313,6 @@ object SpecificUnitAutomation {
0,
wonderToHurry.name
)
unit.showAdditionalActions = false // make sure getUnitActions doesn't skip the important ones
return UnitActions.invokeUnitAction(unit, UnitActionType.HurryBuilding)
|| UnitActions.invokeUnitAction(unit, UnitActionType.HurryWonder)
}

View File

@ -46,6 +46,8 @@ class BFS(
* and to the yet-to-be-processed set.
*
* Will do nothing when [hasEnded] returns `true`
*
* @return The Tile that was checked, or `null` if there was nothing to do
*/
fun nextStep(): Tile? {
if (tilesReached.size >= maxSize) { tilesToCheck.clear(); return null }

View File

@ -122,9 +122,6 @@ class MapUnit : IsPartOfGameInfoSerialization {
@Transient
var viewableTiles = HashSet<Tile>()
@Transient
var showAdditionalActions: Boolean = false
//endregion
/**
@ -931,7 +928,6 @@ class MapUnit : IsPartOfGameInfoSerialization {
}
fun actionsOnDeselect() {
showAdditionalActions = false
if (isPreparingParadrop() || isPreparingAirSweep()) action = null
}

View File

@ -93,10 +93,12 @@ enum class UnitActionType(
val imageGetter: (()-> Actor)?,
binding: KeyboardBinding? = null,
val isSkippingToNextUnit: Boolean = true,
val uncivSound: UncivSound = UncivSound.Click
val uncivSound: UncivSound = UncivSound.Click,
/** UI "page" preference, 0-based - Dynamic overrides to this are in `UnitActions.actionTypeToPageGetter` */
val defaultPage: Int
) {
SwapUnits("Swap units",
{ ImageGetter.getUnitActionPortrait("Swap") }, false),
{ ImageGetter.getUnitActionPortrait("Swap") }, false, defaultPage = 1),
Automate("Automate",
{ ImageGetter.getUnitActionPortrait("Automate") }),
ConnectRoad("Connect road",
@ -106,7 +108,7 @@ enum class UnitActionType(
StopMovement("Stop movement",
{ ImageGetter.getUnitActionPortrait("StopMove") }, false),
ShowUnitDestination("Show unit destination",
{ ImageGetter.getUnitActionPortrait("ShowUnitDestination")}, false),
{ ImageGetter.getUnitActionPortrait("ShowUnitDestination")}, false, defaultPage = 1),
Sleep("Sleep",
{ ImageGetter.getUnitActionPortrait("Sleep") }),
SleepUntilHealed("Sleep until healed",
@ -164,24 +166,24 @@ enum class UnitActionType(
EnhanceReligion("Enhance a Religion",
{ ImageGetter.getUnitActionPortrait("EnhanceReligion") }, UncivSound.Choir),
DisbandUnit("Disband unit",
{ ImageGetter.getUnitActionPortrait("DisbandUnit") }, false),
{ ImageGetter.getUnitActionPortrait("DisbandUnit") }, false, defaultPage = 1),
GiftUnit("Gift unit",
{ ImageGetter.getUnitActionPortrait("Present") }, UncivSound.Silent),
{ ImageGetter.getUnitActionPortrait("Present") }, UncivSound.Silent, defaultPage = 1),
Wait("Wait",
{ ImageGetter.getUnitActionPortrait("Wait") }, UncivSound.Silent),
ShowAdditionalActions("Show more",
{ ImageGetter.getUnitActionPortrait("ShowMore") }, false),
HideAdditionalActions("Back",
{ ImageGetter.getUnitActionPortrait("HideMore") }, false),
{ ImageGetter.getUnitActionPortrait("HideMore") }, false, defaultPage = 1),
AddInCapital( "Add in capital",
{ ImageGetter.getUnitActionPortrait("AddInCapital")}, UncivSound.Chimes),
;
// Allow shorter initializations
constructor(value: String, imageGetter: (() -> Actor)?, uncivSound: UncivSound = UncivSound.Click)
: this(value, imageGetter, null, true, uncivSound)
constructor(value: String, imageGetter: (() -> Actor)?, isSkippingToNextUnit: Boolean = true, uncivSound: UncivSound = UncivSound.Click)
: this(value, imageGetter, null, isSkippingToNextUnit, uncivSound)
constructor(value: String, imageGetter: (() -> Actor)?, uncivSound: UncivSound = UncivSound.Click, defaultPage: Int = 0)
: this(value, imageGetter, null, true, uncivSound, defaultPage)
constructor(value: String, imageGetter: (() -> Actor)?, isSkippingToNextUnit: Boolean = true, uncivSound: UncivSound = UncivSound.Click, defaultPage: Int = 0)
: this(value, imageGetter, null, isSkippingToNextUnit, uncivSound, defaultPage)
val binding: KeyboardBinding =
binding ?:

View File

@ -66,7 +66,7 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s
fun conditionalsApply(state: StateForConditionals = StateForConditionals()): Boolean {
if (state.ignoreConditionals) return true
// Always allow Timed conditional uniques. They are managed elsewhere
if (conditionals.any{ it.isOfType(UniqueType.ConditionalTimedUnique) }) return true
if (conditionals.any { it.isOfType(UniqueType.ConditionalTimedUnique) }) return true
for (condition in conditionals) {
if (!conditionalApplies(condition, state)) return false
}

View File

@ -5,7 +5,6 @@ import com.badlogic.gdx.Gdx
import com.badlogic.gdx.Input
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.utils.Align
import com.unciv.Constants
import com.unciv.UncivGame
@ -26,10 +25,7 @@ import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.components.extensions.centerX
import com.unciv.ui.components.extensions.darken
import com.unciv.ui.components.extensions.isEnabled
import com.unciv.ui.components.extensions.setFontSize
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.components.input.KeyCharAndCode
import com.unciv.ui.components.input.KeyShortcutDispatcherVeto
import com.unciv.ui.components.input.KeyboardBinding

View File

@ -10,49 +10,69 @@ import com.unciv.models.UnitAction
import com.unciv.models.UnitActionType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.yieldIfNotNull
import com.unciv.ui.popups.ConfirmPopup
import com.unciv.ui.popups.hasOpenPopups
import com.unciv.ui.screens.pickerscreens.PromotionPickerScreen
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions.getGiftAction
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions.getActionDefaultPage
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions.getPagingActions
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions.getUnitActions
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions.invokeUnitAction
/**
* Manages creation of [UnitAction] instances.
*
* API for UI: [getUnitActions]
* API for Automation: [invokeUnitAction]
* API for unit tests: [getGiftAction]
* API used by UI: [getUnitActions] without `unitActionType` parameter, [getActionDefaultPage], [getPagingActions]
* API used by Automation: [invokeUnitAction]
* API used by unit tests: [getUnitActions] with `unitActionType` parameter
* Note on unit test use: Some UnitAction factories access GUI helpers that crash from a unit test.
* Avoid testing actions that need WorldScreen context, and migrate any un-mapped ones you need to `actionTypeToFunctions`.
*/
object UnitActions {
fun getUnitActions(unit: MapUnit): Sequence<UnitAction> {
return if (unit.showAdditionalActions) getAdditionalActions(unit)
else getNormalActions(unit)
}
/**
* Get an instance of [UnitAction] of the [unitActionType] type for [unit] and execute its [action][UnitAction.action], if enabled.
*
* Includes optimization for direct creation of the needed instance type, falls back to enumerating [getUnitActions] to look for the given type.
*
* @return whether the action was invoked */
* @return whether the action was invoked
*/
fun invokeUnitAction(unit: MapUnit, unitActionType: UnitActionType): Boolean {
val unitAction =
if (unitActionType in actionTypeToFunctions)
actionTypeToFunctions[unitActionType]!! // we have a mapped getter...
.invoke(unit, unit.getTile()) // ...call it to get a collection...
.firstOrNull { it.action != null } // ...then take the first enabled one.
else
(getNormalActions(unit) + getAdditionalActions(unit)) // No mapped getter: Enumerate all...
.firstOrNull { it.type == unitActionType && it.action != null } // ...and take first enabled one to match the type.
val internalAction = unitAction?.action ?: return false
val internalAction =
getUnitActions(unit, unitActionType)
.firstOrNull { it.action != null } // If there's more than one, take the first enabled one.
?.action ?: return false
internalAction.invoke()
return true
}
private val actionTypeToFunctions = linkedMapOf<UnitActionType, (unit:MapUnit, tile: Tile) -> Sequence<UnitAction>>(
/**
* Get all currently possible instances of [UnitAction] for [unit].
*/
fun getUnitActions(unit: MapUnit) = sequence {
val tile = unit.getTile()
// Actions standardized with a directly callable invokeUnitAction
for (getActionsFunction in actionTypeToFunctions.values)
yieldAll(getActionsFunction(unit, tile))
// Actions not migrated to actionTypeToFunctions
addUnmappedUnitActions(unit)
}
/**
* Get all instances of [UnitAction] of the [unitActionType] type for [unit].
*
* Includes optimization for direct creation of the needed instance type, falls back to enumerating [getUnitActions] to look for the given type.
*/
fun getUnitActions(unit: MapUnit, unitActionType: UnitActionType) =
if (unitActionType in actionTypeToFunctions)
actionTypeToFunctions[unitActionType]!! // we have a mapped getter...
.invoke(unit, unit.getTile()) // ...call it to get a collection...
else sequence {
addUnmappedUnitActions(unit) // No mapped getter: Enumerate all...
}.filter { it.type == unitActionType } // ...and take ones matching the type.
private val actionTypeToFunctions = linkedMapOf<UnitActionType, (unit: MapUnit, tile: Tile) -> Sequence<UnitAction>>(
// Determined by unit uniques
UnitActionType.Transform to UnitActionsFromUniques::getTransformActions,
UnitActionType.Paradrop to UnitActionsFromUniques::getParadropActions,
@ -73,21 +93,46 @@ object UnitActions {
UnitActionType.SpreadReligion to UnitActionsReligion::getSpreadReligionActions,
UnitActionType.RemoveHeresy to UnitActionsReligion::getRemoveHeresyActions,
UnitActionType.TriggerUnique to UnitActionsFromUniques::getTriggerUniqueActions,
UnitActionType.AddInCapital to UnitActionsFromUniques::getAddInCapitalActions
UnitActionType.AddInCapital to UnitActionsFromUniques::getAddInCapitalActions,
UnitActionType.GiftUnit to UnitActions::getGiftActions
)
private fun shouldAutomationBePrimaryAction(unit:MapUnit) = unit.cache.hasUniqueToBuildImprovements || unit.hasUnique(UniqueType.AutomationPrimaryAction)
/** Gets the preferred "page" to display a [UnitAction] of type [unitActionType] on, possibly dynamic depending on the state or situation [unit] is in. */
fun getActionDefaultPage(unit: MapUnit, unitActionType: UnitActionType) =
actionTypeToPageGetter[unitActionType]?.invoke(unit) ?: unitActionType.defaultPage
private fun getNormalActions(unit: MapUnit) = sequence {
/** Only for action types that wish to change their "More/Back" page position depending on context.
* All others get a defaultPage statically from [UnitActionType].
*/
private val actionTypeToPageGetter = linkedMapOf<UnitActionType, (unit: MapUnit) -> Int>(
UnitActionType.Automate to { unit ->
if (unit.cache.hasUniqueToBuildImprovements || unit.hasUnique(UniqueType.AutomationPrimaryAction)) 0 else 1
},
UnitActionType.Fortify to { unit ->
// Fortify moves to second page if current action is FortifyUntilHealed or if unit is wounded and it's not already the current action
if (unit.isFortifyingUntilHealed() || unit.health < 100 && !(unit.isFortified() && !unit.isActionUntilHealed())) 1 else 0
},
UnitActionType.FortifyUntilHealed to { unit ->
// FortifyUntilHealed only moves to the second page if Fortify is the current action
if (unit.isFortified() && !unit.isActionUntilHealed()) 1 else 0
},
UnitActionType.Sleep to { unit ->
// Sleep moves to second page if current action is SleepUntilHealed or if unit is wounded and it's not already the current action
if (unit.isSleepingUntilHealed() || unit.health < 100 && !(unit.isSleeping() && !unit.isActionUntilHealed())) 1 else 0
},
UnitActionType.SleepUntilHealed to { unit ->
// SleepUntilHealed only moves to the second page if Sleep is the current action
if (unit.isSleeping() && !unit.isActionUntilHealed()) 1 else 0
},
UnitActionType.Explore to { unit ->
if (unit.isCivilian()) 1 else 0
},
)
private suspend fun SequenceScope<UnitAction>.addUnmappedUnitActions(unit: MapUnit) {
val tile = unit.getTile()
// Actions standardized with a directly callable invokeUnitAction
for (getActionsFunction in actionTypeToFunctions.values)
yieldAll(getActionsFunction(unit, tile))
// General actions
if (shouldAutomationBePrimaryAction(unit))
addAutomateActions(unit)
if (unit.isMoving())
yield(UnitAction(UnitActionType.StopMovement) { unit.action = null })
@ -104,39 +149,24 @@ object UnitActions {
yieldAll(UnitActionsPillage.getPillageActions(unit, tile))
addSleepActions(unit, tile)
addSleepUntilHealedActions(unit, tile)
addFortifyActions(unit)
addFortifyActions(unit, false)
if (unit.isMilitary())
addExplorationActions(unit)
addWaitAction(unit)
addToggleActionsAction(unit)
}
private fun getAdditionalActions(unit: MapUnit) = sequence {
// From here we have actions defaulting to the second page
if (unit.isMoving()) {
yield(UnitAction(UnitActionType.ShowUnitDestination) {
GUI.getMap().setCenterPosition(unit.getMovementDestination().position, true)
})
}
addFortifyActions(unit, true)
if (!shouldAutomationBePrimaryAction(unit))
addAutomateActions(unit)
addSwapAction(unit)
addDisbandAction(unit)
addGiftAction(unit, unit.getTile())
if (unit.isCivilian())
addExplorationActions(unit)
addToggleActionsAction(unit)
}
private suspend fun SequenceScope<UnitAction>.addSwapAction(unit: MapUnit) {
val worldScreen = GUI.getWorldScreen()
// Air units cannot swap
if (unit.baseUnit.movesLikeAirUnits()) return
// Disable unit swapping if multiple units are selected. It would make little sense.
@ -145,6 +175,7 @@ object UnitActions {
// have the visual bug that the tile overlays for the eligible swap locations are drawn for
// /all/ selected units instead of only the first one. This could be fixed, but again,
// swapping makes little sense for multiselect anyway.
val worldScreen = GUI.getWorldScreen()
if (worldScreen.bottomUnitTable.selectedUnits.size > 1) return
// Only show the swap action if there is at least one possible swap movement
if (unit.movement.getUnitSwappableTiles().none()) return
@ -160,8 +191,9 @@ object UnitActions {
}
private suspend fun SequenceScope<UnitAction>.addDisbandAction(unit: MapUnit) {
yield(UnitAction(type = UnitActionType.DisbandUnit,
action = {
val worldScreen = GUI.getWorldScreen()
yield(UnitAction(type = UnitActionType.DisbandUnit, action = {
if (!worldScreen.hasOpenPopups()) {
val disbandText = if (unit.currentTile.getOwner() == unit.civ)
"Disband this unit for [${unit.baseUnit.getDisbandGold(unit.civ)}] gold?".tr()
@ -174,10 +206,10 @@ object UnitActions {
worldScreen.switchToNextUnit()
}.open()
}
}.takeIf { unit.currentMovement > 0 }))
}.takeIf { unit.currentMovement > 0 }
))
}
private suspend fun SequenceScope<UnitAction>.addPromoteActions(unit: MapUnit) {
if (unit.isCivilian() || !unit.promotions.canBePromoted()) return
// promotion does not consume movement points, but is not allowed if a unit has exhausted its movement or has attacked
@ -197,9 +229,8 @@ object UnitActions {
})
}
private suspend fun SequenceScope<UnitAction>.addFortifyActions(unit: MapUnit, showingAdditionalActions: Boolean) {
if (unit.isFortified() && !showingAdditionalActions) {
private suspend fun SequenceScope<UnitAction>.addFortifyActions(unit: MapUnit) {
if (unit.isFortified()) {
yield(UnitAction(
type = if (unit.isActionUntilHealed())
UnitActionType.FortifyUntilHealed else
@ -210,37 +241,27 @@ object UnitActions {
return
}
if (!unit.canFortify()) return
if (unit.currentMovement == 0f) return
if (!unit.canFortify() || unit.currentMovement == 0f) return
val isFortified = unit.isFortified()
val isDamaged = unit.health < 100
if (isDamaged && !showingAdditionalActions && unit.rankTileForHealing(unit.currentTile) != 0)
yield(UnitAction(UnitActionType.FortifyUntilHealed,
action = { unit.fortifyUntilHealed() }.takeIf { !unit.isFortifyingUntilHealed() }
))
else if (isDamaged || !showingAdditionalActions)
yield(UnitAction(UnitActionType.Fortify,
action = { unit.fortify() }.takeIf { !isFortified }
action = { unit.fortify() }.takeIf { !unit.isFortified() || unit.isFortifyingUntilHealed() }
))
if (unit.health == 100) return
yield(UnitAction(UnitActionType.FortifyUntilHealed,
action = { unit.fortifyUntilHealed() }
.takeIf { !unit.isFortifyingUntilHealed() && unit.canHealInCurrentTile() }
))
}
private fun shouldHaveSleepAction(unit: MapUnit, tile: Tile): Boolean {
if (unit.isFortified() || unit.canFortify() || unit.currentMovement == 0f) return false
return !(tile.hasImprovementInProgress()
&& unit.canBuildImprovement(tile.getTileImprovementInProgress()!!))
}
private suspend fun SequenceScope<UnitAction>.addSleepActions(unit: MapUnit, tile: Tile) {
if (!shouldHaveSleepAction(unit, tile)) return
if (unit.health < 100) return
yield(UnitAction(UnitActionType.Sleep,
action = { unit.action = UnitActionType.Sleep.value }.takeIf { !unit.isSleeping() }
))
}
if (unit.isFortified() || unit.canFortify() || unit.currentMovement == 0f) return
if (tile.hasImprovementInProgress() && unit.canBuildImprovement(tile.getTileImprovementInProgress()!!)) return
yield(UnitAction(UnitActionType.Sleep,
action = { unit.action = UnitActionType.Sleep.value }.takeIf { !unit.isSleeping() || unit.isSleepingUntilHealed() }
))
private suspend fun SequenceScope<UnitAction>.addSleepUntilHealedActions(unit: MapUnit, tile: Tile) {
if (!shouldHaveSleepAction(unit, tile)) return
if (unit.health == 100) return
yield(UnitAction(UnitActionType.SleepUntilHealed,
action = { unit.action = UnitActionType.SleepUntilHealed.value }
@ -248,17 +269,13 @@ object UnitActions {
))
}
private suspend fun SequenceScope<UnitAction>.addGiftAction(unit: MapUnit, tile: Tile) {
yieldIfNotNull(getGiftAction(unit, tile))
}
fun getGiftAction(unit: MapUnit, tile: Tile): UnitAction? {
private fun getGiftActions(unit: MapUnit, tile: Tile) = sequence {
val recipient = tile.getOwner()
// We need to be in another civs territory.
if (recipient == null || recipient.isCurrentPlayer()) return null
if (recipient == null || recipient.isCurrentPlayer()) return@sequence
if (recipient.isCityState()) {
if (recipient.isAtWarWith(unit.civ)) return null // No gifts to enemy CS
if (recipient.isAtWarWith(unit.civ)) return@sequence // No gifts to enemy CS
// City States only take military units (and units specifically allowed by uniques)
if (!unit.isMilitary()
&& unit.getMatchingUniques(
@ -266,16 +283,18 @@ object UnitActions {
checkCivInfoUniques = true
)
.none { unit.matchesFilter(it.params[1]) }
) return null
) return@sequence
}
// If gifting to major civ they need to be friendly
else if (!tile.isFriendlyTerritory(unit.civ)) return null
else if (!tile.isFriendlyTerritory(unit.civ)) return@sequence
// Transported units can't be gifted
if (unit.isTransported) return null
if (unit.isTransported) return@sequence
if (unit.currentMovement <= 0)
return UnitAction(UnitActionType.GiftUnit, action = null)
if (unit.currentMovement <= 0) {
yield(UnitAction(UnitActionType.GiftUnit, action = null))
return@sequence
}
val giftAction = {
if (recipient.isCityState()) {
@ -300,8 +319,7 @@ object UnitActions {
unit.gift(recipient)
GUI.setUpdateWorldOnNextRender()
}
return UnitAction(UnitActionType.GiftUnit, action = giftAction)
yield(UnitAction(UnitActionType.GiftUnit, action = giftAction))
}
private suspend fun SequenceScope<UnitAction>.addAutomateActions(unit: MapUnit) {
@ -327,15 +345,18 @@ object UnitActions {
))
}
private suspend fun SequenceScope<UnitAction>.addToggleActionsAction(unit: MapUnit) {
yield(UnitAction(
type = if (unit.showAdditionalActions) UnitActionType.HideAdditionalActions
else UnitActionType.ShowAdditionalActions,
action = {
unit.showAdditionalActions = !unit.showAdditionalActions
GUI.getWorldScreen().bottomUnitTable.update()
}
))
/**
* Creates the "paging" [UnitAction]s for:
* - [first][Pair.first] - [UnitActionType.ShowAdditionalActions] (page forward)
* - [second][Pair.second] - [UnitActionType.HideAdditionalActions] (page back)
*
* These are not returned as part of [getUnitActions]!
*/
// This function is here for historic reasons, and to keep UnitActionType implementations closer together.
// The code might move to UnitActionsTable altogether, no big difference.
internal fun getPagingActions(unit: MapUnit, actionsTable: UnitActionsTable): Pair<UnitAction, UnitAction> {
return UnitAction(UnitActionType.ShowAdditionalActions) { actionsTable.changePage(1, unit) } to
UnitAction(UnitActionType.HideAdditionalActions) { actionsTable.changePage(-1, unit) }
}
}

View File

@ -17,19 +17,57 @@ import com.unciv.ui.popups.UnitUpgradeMenu
import com.unciv.ui.screens.worldscreen.WorldScreen
class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
/** Distribute UnitActions on "pages" */
// todo since this runs surprisingly often - some caching? Does it even need to do anything if unit and currentPage are the same?
private var currentPage = 0
private val numPages = 2 //todo static for now
private var shownForUnitHash = 0
companion object {
/** Maximum for how many pages there can be. */
private const val maxAllowedPages = 10
/** Padding between and to the left of the Buttons */
private const val padBetweenButtons = 2f
}
init {
defaults().left().padLeft(padBetweenButtons).padBottom(padBetweenButtons)
}
fun changePage(delta: Int, unit: MapUnit) {
if (delta == 0) return
currentPage = (currentPage + delta) % numPages
update(unit)
}
fun update(unit: MapUnit?) {
val newUnitHash = unit?.hashCode() ?: 0
if (shownForUnitHash != newUnitHash) {
currentPage = 0
shownForUnitHash = newUnitHash
}
clear()
if (unit == null) return
if (!worldScreen.canChangeState) return // No actions when it's not your turn or spectator!
val pageActionBuckets = Array<ArrayDeque<UnitAction>>(maxAllowedPages) { ArrayDeque() }
val (nextPageAction, previousPageAction) = UnitActions.getPagingActions(unit, this)
val nextPageButton = getUnitActionButton(unit, nextPageAction)
val previousPageButton = getUnitActionButton(unit, previousPageAction)
// Distribute sequentially into the buckets
for (unitAction in UnitActions.getUnitActions(unit)) {
val actionPage = UnitActions.getActionDefaultPage(unit, unitAction.type)
if (actionPage >= maxAllowedPages) break
pageActionBuckets[actionPage].addLast(unitAction)
}
// clamp currentPage
if (currentPage !in 0 until numPages) currentPage = 0
// actually show the buttons of the currentPage
for (unitAction in pageActionBuckets[currentPage]) {
val button = getUnitActionButton(unit, unitAction)
if (unitAction is UpgradeUnitAction) {
button.onRightClick {
@ -38,11 +76,16 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
}
}
}
add(button).row()
}
pack()
add(button).colspan(2).row()
}
// show page navigation
if (currentPage > 0)
add(previousPageButton)
if (currentPage < numPages - 1)
add(nextPageButton)
pack()
}
private fun getUnitActionButton(unit: MapUnit, unitAction: UnitAction): Button {
val icon = unitAction.getIcon()

View File

@ -2,6 +2,7 @@ package com.unciv.uniques
import com.badlogic.gdx.math.Vector2
import com.unciv.logic.map.mapunit.UnitTurnManager
import com.unciv.models.UnitActionType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.fillPlaceholders
import com.unciv.testing.GdxTestRunner
@ -42,7 +43,8 @@ class UnitUniquesTests {
val greatPerson = game.addUnit("Great Scientist", mainCiv, unitTile)
// then
val giftAction = UnitActions.getGiftAction(greatPerson, unitTile)
val giftAction = UnitActions.getUnitActions(greatPerson, UnitActionType.GiftUnit)
.firstOrNull { it.action != null } // This tests that the action should be enabled, too
Assert.assertNotNull("Great Person should have a gift action", giftAction)
}