Empire Overview Units: Persist scroll, unit select, show due, jump city, fixed header (#6368)

This commit is contained in:
SomeTroglodyte 2022-03-21 20:04:47 +01:00 committed by GitHub
parent 1df49749f2
commit 77839b4b9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 228 additions and 116 deletions

View File

@ -34,8 +34,8 @@ enum class EmpireOverviewCategories(
= TradesOverviewTab(viewingPlayer, overviewScreen), = TradesOverviewTab(viewingPlayer, overviewScreen),
fun (viewingPlayer: CivilizationInfo) = viewingPlayer.diplomacy.values.all { it.trades.isEmpty() }.toState()), fun (viewingPlayer: CivilizationInfo) = viewingPlayer.diplomacy.values.all { it.trades.isEmpty() }.toState()),
Units("OtherIcons/Shield", 'U', Units("OtherIcons/Shield", 'U',
fun (viewingPlayer: CivilizationInfo, overviewScreen: EmpireOverviewScreen, _: EmpireOverviewTabPersistableData?) fun (viewingPlayer: CivilizationInfo, overviewScreen: EmpireOverviewScreen, persistedData: EmpireOverviewTabPersistableData?)
= UnitOverviewTab(viewingPlayer, overviewScreen), = UnitOverviewTab(viewingPlayer, overviewScreen, persistedData),
fun (viewingPlayer: CivilizationInfo) = viewingPlayer.getCivUnits().none().toState()), fun (viewingPlayer: CivilizationInfo) = viewingPlayer.getCivUnits().none().toState()),
Diplomacy("OtherIcons/DiplomacyW", 'D', Diplomacy("OtherIcons/DiplomacyW", 'D',
fun (viewingPlayer: CivilizationInfo, overviewScreen: EmpireOverviewScreen, persistedData: EmpireOverviewTabPersistableData?) fun (viewingPlayer: CivilizationInfo, overviewScreen: EmpireOverviewScreen, persistedData: EmpireOverviewTabPersistableData?)

View File

@ -1,9 +1,13 @@
package com.unciv.ui.overviewscreen package com.unciv.ui.overviewscreen
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup
import com.badlogic.gdx.utils.Align
import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.ui.utils.BaseScreen import com.unciv.ui.utils.BaseScreen
import com.unciv.ui.utils.packIfNeeded
import com.unciv.ui.utils.toLabel
abstract class EmpireOverviewTab ( abstract class EmpireOverviewTab (
val viewingPlayer: CivilizationInfo, val viewingPlayer: CivilizationInfo,
@ -23,8 +27,19 @@ abstract class EmpireOverviewTab (
val gameInfo = viewingPlayer.gameInfo val gameInfo = viewingPlayer.gameInfo
/** Sets first row cell's minWidth to the max of the widths of that column over all given tables
*
* Notes:
* - This aligns columns only if the tables are arranged vertically with equal X coordinates.
* - first table determines columns processed, all others must have at least the same column count.
* - Tables are left as needsLayout==true, so while equal width is ensured, you may have to pack if you want to see the value before this is rendered.
*/
protected fun equalizeColumns(vararg tables: Table) { protected fun equalizeColumns(vararg tables: Table) {
for (table in tables)
table.packIfNeeded()
val columns = tables.first().columns val columns = tables.first().columns
if (tables.any { it.columns < columns })
throw IllegalStateException("EmpireOverviewTab.equalizeColumns needs all tables to have at least the same number of columns as the first one")
val widths = (0 until columns) val widths = (0 until columns)
.mapTo(ArrayList(columns)) { column -> .mapTo(ArrayList(columns)) { column ->
tables.maxOf { it.getColumnWidth(column) } tables.maxOf { it.getColumnWidth(column) }
@ -32,6 +47,14 @@ abstract class EmpireOverviewTab (
for (table in tables) { for (table in tables) {
for (column in 0 until columns) for (column in 0 until columns)
table.cells[column].run { table.cells[column].run {
if (actor == null)
// Empty cells ignore minWidth, so just doing Table.add() for an empty cell in the top row will break this. Fix!
setActor<Label>("".toLabel())
else if (Align.isCenterHorizontal(align)) (actor as? Label)?.run {
// minWidth acts like fillX, so Labels will fill and then left-align by default. Fix!
if (!Align.isCenterHorizontal(labelAlign))
setAlignment(Align.center)
}
minWidth(widths[column] - padLeft - padRight) minWidth(widths[column] - padLeft - padRight)
} }
table.invalidate() table.invalidate()

View File

@ -1,13 +1,18 @@
package com.unciv.ui.overviewscreen package com.unciv.ui.overviewscreen
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.Group
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.WidgetGroup
import com.badlogic.gdx.utils.Align
import com.unciv.Constants import com.unciv.Constants
import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.models.translations.tr import com.unciv.logic.map.MapUnit
import com.unciv.logic.map.TileInfo
import com.unciv.ui.pickerscreens.PromotionPickerScreen import com.unciv.ui.pickerscreens.PromotionPickerScreen
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
import java.text.DecimalFormat import com.unciv.ui.worldscreen.unit.UnitActions
import kotlin.math.abs import kotlin.math.abs
/** /**
@ -15,65 +20,116 @@ import kotlin.math.abs
*/ */
class UnitOverviewTab( class UnitOverviewTab(
viewingPlayer: CivilizationInfo, viewingPlayer: CivilizationInfo,
overviewScreen: EmpireOverviewScreen overviewScreen: EmpireOverviewScreen,
persistedData: EmpireOverviewTabPersistableData? = null
) : EmpireOverviewTab(viewingPlayer, overviewScreen) { ) : EmpireOverviewTab(viewingPlayer, overviewScreen) {
class UnitTabPersistableData(
var scrollY: Float? = null
) : EmpireOverviewTabPersistableData() {
override fun isEmpty() = scrollY == null
}
override val persistableData = (persistedData as? UnitTabPersistableData) ?: UnitTabPersistableData()
init { override fun activated() = persistableData.scrollY
add(getUnitSupplyTable()).top().padRight(25f) override fun deactivated(scrollY: Float) {
add(getUnitListTable()) persistableData.scrollY = scrollY
pack()
} }
private fun getUnitSupplyTable(): Table { private val supplyTableWidth = (overviewScreen.stage.width * 0.25f).coerceAtLeast(240f)
val unitSupplyTable = Table(BaseScreen.skin) private val unitListTable = Table() // could be `this` instead, extra nesting helps readability a little
unitSupplyTable.defaults().pad(5f) private val unitHeaderTable = Table()
unitSupplyTable.apply {
add("Unit Supply".tr()).colspan(2).center().row() override fun getFixedContent(): WidgetGroup {
addSeparator() return Table().apply {
add("Base Supply".tr()).left() add(getUnitSupplyTable()).align(Align.top).padBottom(10f).row()
add(viewingPlayer.stats().getBaseUnitSupply().toLabel()).right().row() add(unitHeaderTable.updateUnitHeaderTable())
add("Cities".tr()).left()
add(viewingPlayer.stats().getUnitSupplyFromCities().toLabel()).right().row() equalizeColumns(unitListTable, unitHeaderTable)
add("Population".tr()).left() }
add(viewingPlayer.stats().getUnitSupplyFromPop().toLabel()).right().row() }
addSeparator()
add("Total Supply".tr()).left() init {
add(viewingPlayer.stats().getUnitSupply().toLabel()).right().row() add(unitListTable.updateUnitListTable())
add("In Use".tr()).left() }
add(viewingPlayer.getCivUnitsSize().toLabel()).right().row()
addSeparator() // Here overloads are simpler than a generic:
val deficit = viewingPlayer.stats().getUnitSupplyDeficit() private fun Table.addLabeledValue (label: String, value: Int) {
add("Supply Deficit".tr()).left() add(label.toLabel()).left()
add(deficit.toLabel()).right().row() add(value.toLabel()).right().row()
add("Production Penalty".tr()).left() }
add((viewingPlayer.stats().getUnitSupplyProductionPenalty()).toInt().toString()+"%").right().row() private fun Table.addLabeledValue (label: String, value: String) {
add(label.toLabel()).left()
add(value.toLabel()).right().row()
}
private fun showWorldScreenAt(position: Vector2, unit: MapUnit?) {
val game = overviewScreen.game
game.setWorldScreen()
game.worldScreen.mapHolder.setCenterPosition(position, forceSelectUnit = unit)
}
private fun showWorldScreenAt(unit: MapUnit) = showWorldScreenAt(unit.currentTile.position, unit)
private fun showWorldScreenAt(tile: TileInfo) = showWorldScreenAt(tile.position, null)
private fun getUnitSupplyTable(): ExpanderTab {
val stats = viewingPlayer.stats()
val deficit = stats.getUnitSupplyDeficit()
val icon = if (deficit <= 0) null else Group().apply {
isTransform = false
setSize(36f, 36f)
val image = ImageGetter.getImage("OtherIcons/ExclamationMark")
image.color = Color.FIREBRICK
image.setSize(36f, 36f)
image.center(this)
image.setOrigin(Align.center)
addActor(image)
}
return ExpanderTab(
title = "Unit Supply",
fontSize = Constants.defaultFontSize,
icon = icon,
startsOutOpened = deficit > 0,
defaultPad = 0f,
expanderWidth = supplyTableWidth
) {
it.defaults().pad(5f).fill(false)
it.background = ImageGetter.getBackground(ImageGetter.getBlue().darken(0.6f))
it.addLabeledValue("Base Supply", stats.getBaseUnitSupply())
it.addLabeledValue("Cities", stats.getUnitSupplyFromCities())
it.addLabeledValue("Population", stats.getUnitSupplyFromPop())
it.addSeparator()
it.addLabeledValue("Total Supply", stats.getUnitSupply())
it.addLabeledValue("In Use", viewingPlayer.getCivUnitsSize())
it.addSeparator()
it.addLabeledValue("Supply Deficit", deficit)
it.addLabeledValue("Production Penalty", "${stats.getUnitSupplyProductionPenalty().toInt()}%")
if (deficit > 0) { if (deficit > 0) {
val penaltyLabel = "Increase your supply or reduce the amount of units to remove the production penalty" val penaltyLabel = "Increase your supply or reduce the amount of units to remove the production penalty"
.toLabel(Color.FIREBRICK) .toLabel(Color.FIREBRICK)
penaltyLabel.wrap = true penaltyLabel.wrap = true
add(penaltyLabel).colspan(2).left() it.add(penaltyLabel).colspan(2).left()
.width(overviewScreen.stage.width * 0.2f).row() .width(supplyTableWidth).row()
} }
pack()
} }
return unitSupplyTable
} }
private fun getUnitListTable(): Table { private fun Table.updateUnitHeaderTable(): Table {
defaults().pad(5f)
add("Name".toLabel())
add("Action".toLabel())
add(Fonts.strength.toString().toLabel())
add(Fonts.rangedStrength.toString().toLabel())
add(Fonts.movement.toString().toLabel())
add("Closest city".toLabel())
add("Promotions".toLabel())
add("Upgrade".toLabel())
add("Health".toLabel())
addSeparator().padBottom(0f)
return this
}
private fun Table.updateUnitListTable(): Table {
val game = overviewScreen.game val game = overviewScreen.game
val unitListTable = Table(BaseScreen.skin) defaults().pad(5f)
unitListTable.defaults().pad(5f)
unitListTable.apply {
add("Name".tr())
add("Action".tr())
add(Fonts.strength.toString())
add(Fonts.rangedStrength.toString())
add(Fonts.movement.toString())
add("Closest city".tr())
add("Promotions".tr())
add("Health".tr())
row()
addSeparator()
for (unit in viewingPlayer.getCivUnits().sortedWith( for (unit in viewingPlayer.getCivUnits().sortedWith(
compareBy({ it.displayName() }, compareBy({ it.displayName() },
@ -83,43 +139,72 @@ class UnitOverviewTab(
)) { )) {
val baseUnit = unit.baseUnit() val baseUnit = unit.baseUnit()
val button = IconTextButton(unit.displayName(), UnitGroup(unit, 20f)) // Unit button column - name, health, fortified, sleeping, embarked are visible here
val button = IconTextButton(
unit.displayName(),
UnitGroup(unit, 20f),
fontColor = if (unit.due && unit.isIdle()) Color.WHITE else Color.LIGHT_GRAY
)
button.onClick { button.onClick {
game.setWorldScreen() showWorldScreenAt(unit)
game.worldScreen.mapHolder.setCenterPosition(unit.currentTile.position)
} }
add(button).left() add(button).fillX()
if (unit.action == null) add()
else add(unit.getActionLabel().tr()) // Columns: action, strength, ranged, moves
if (baseUnit.strength > 0) add(baseUnit.strength.toString()) else add() if (unit.action == null) add() else add(unit.getActionLabel().toLabel())
if (baseUnit.rangedStrength > 0) add(baseUnit.rangedStrength.toString()) else add() if (baseUnit.strength > 0) add(baseUnit.strength.toLabel()) else add()
add(DecimalFormat("0.#").format(unit.currentMovement) + "/" + unit.getMaxMovement()) if (baseUnit.rangedStrength > 0) add(baseUnit.rangedStrength.toLabel()) else add()
add(unit.getMovementString().toLabel())
// Closest city column
val closestCity = val closestCity =
unit.getTile().getTilesInDistance(3).firstOrNull { it.isCityCenter() } unit.getTile().getTilesInDistance(3).firstOrNull { it.isCityCenter() }
if (closestCity != null) add(closestCity.getCity()!!.name.tr()) else add() val cityColor = if (unit.getTile() == closestCity) Color.FOREST.brighten(0.5f) else Color.WHITE
if (closestCity != null)
add(closestCity.getCity()!!.name.toLabel(cityColor).apply {
onClick { showWorldScreenAt(closestCity) }
})
else add()
// Promotions column
val promotionsTable = Table() val promotionsTable = Table()
// getPromotions goes by json order on demand, so this is same sorting as on picker // getPromotions goes by json order on demand, so this is same sorting as on picker
for (promotion in unit.promotions.getPromotions(true)) for (promotion in unit.promotions.getPromotions(true))
promotionsTable.add(ImageGetter.getPromotionIcon(promotion.name)) promotionsTable.add(ImageGetter.getPromotionIcon(promotion.name))
if (unit.promotions.canBePromoted()) promotionsTable.add( if (unit.promotions.canBePromoted())
ImageGetter.getImage("OtherIcons/Star").apply { color = Color.GOLDENROD }) promotionsTable.add(
.size(24f).padLeft(8f) ImageGetter.getImage("OtherIcons/Star").apply {
if (unit.canUpgrade()) promotionsTable.add( color = if (game.worldScreen.canChangeState && unit.currentMovement > 0f && unit.attacksThisTurn == 0)
ImageGetter.getUnitIcon( Color.GOLDENROD
unit.getUnitToUpgradeTo().name, else Color.GOLDENROD.darken(0.25f)
Color.GREEN }
) ).size(24f).padLeft(8f)
).size(28f).padLeft(8f)
promotionsTable.onClick { promotionsTable.onClick {
if (unit.promotions.canBePromoted() || unit.promotions.promotions.isNotEmpty()) { if (unit.promotions.canBePromoted() || unit.promotions.promotions.isNotEmpty()) {
game.setScreen(PromotionPickerScreen(unit)) game.setScreen(PromotionPickerScreen(unit))
overviewScreen.dispose()
} }
} }
add(promotionsTable) add(promotionsTable)
if (unit.health < 100) add(unit.health.toString()) else add()
// Upgrade column
if (unit.canUpgrade()) {
val unitAction = UnitActions.getUpgradeAction(unit)
val enable = unitAction?.action != null
val upgradeIcon = ImageGetter.getUnitIcon(unit.getUnitToUpgradeTo().name,
if (enable) Color.GREEN else Color.GREEN.darken(0.5f))
if (enable) upgradeIcon.onClick {
showWorldScreenAt(unit)
Sounds.play(unitAction!!.uncivSound)
unitAction.action!!()
}
add(upgradeIcon).size(28f)
} else add()
// Numeric health column - there's already a health bar on the button, but...?
if (unit.health < 100) add(unit.health.toLabel()) else add()
row() row()
} }
} return this
return unitListTable
} }
} }

View File

@ -62,7 +62,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
internal fun addTiles() { internal fun addTiles() {
val tileSetStrings = TileSetStrings() val tileSetStrings = TileSetStrings()
val daTileGroups = tileMap.values.map { WorldTileGroup(worldScreen, it, tileSetStrings) } val daTileGroups = tileMap.values.map { WorldTileGroup(worldScreen, it, tileSetStrings) }
val tileGroupMap = TileGroupMap(daTileGroups, worldScreen.stage.width*2, worldScreen.stage.height*2, continuousScrollingX) val tileGroupMap = TileGroupMap(daTileGroups, worldScreen.stage.width, worldScreen.stage.height, continuousScrollingX)
val mirrorTileGroups = tileGroupMap.getMirrorTiles() val mirrorTileGroups = tileGroupMap.getMirrorTiles()
for (tileGroup in daTileGroups) { for (tileGroup in daTileGroups) {
@ -130,7 +130,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
actor = tileGroupMap actor = tileGroupMap
setSize(worldScreen.stage.width * 4, worldScreen.stage.height * 4) setSize(worldScreen.stage.width * 2, worldScreen.stage.height * 2)
setOrigin(width / 2, height / 2) setOrigin(width / 2, height / 2)
center(worldScreen.stage) center(worldScreen.stage)
@ -654,11 +654,11 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
* @param selectUnit Select a unit at the destination * @param selectUnit Select a unit at the destination
* @return `true` if scroll position was changed, `false` otherwise * @return `true` if scroll position was changed, `false` otherwise
*/ */
fun setCenterPosition(vector: Vector2, immediately: Boolean = false, selectUnit: Boolean = true): Boolean { fun setCenterPosition(vector: Vector2, immediately: Boolean = false, selectUnit: Boolean = true, forceSelectUnit: MapUnit? = null): Boolean {
val tileGroup = allWorldTileGroups.firstOrNull { it.tileInfo.position == vector } ?: return false val tileGroup = allWorldTileGroups.firstOrNull { it.tileInfo.position == vector } ?: return false
selectedTile = tileGroup.tileInfo selectedTile = tileGroup.tileInfo
if (selectUnit) if (selectUnit || forceSelectUnit != null)
worldScreen.bottomUnitTable.tileSelected(selectedTile!!) worldScreen.bottomUnitTable.tileSelected(selectedTile!!, forceSelectUnit)
val originalScrollX = scrollX val originalScrollX = scrollX
val originalScrollY = scrollY val originalScrollY = scrollY

View File

@ -243,7 +243,7 @@ class UnitTable(val worldScreen: WorldScreen) : Table(){
return true return true
} }
fun tileSelected(selectedTile: TileInfo) { fun tileSelected(selectedTile: TileInfo, forceSelectUnit: MapUnit? = null) {
val previouslySelectedUnit = selectedUnit val previouslySelectedUnit = selectedUnit
val previousNumberOfSelectedUnits = selectedUnits.size val previousNumberOfSelectedUnits = selectedUnits.size
@ -251,24 +251,28 @@ class UnitTable(val worldScreen: WorldScreen) : Table(){
// Do not select a different unit or city center if we click on it to swap our current unit to it // Do not select a different unit or city center if we click on it to swap our current unit to it
if (selectedUnitIsSwapping && selectedUnit != null && selectedUnit!!.movement.canUnitSwapTo(selectedTile)) return if (selectedUnitIsSwapping && selectedUnit != null && selectedUnit!!.movement.canUnitSwapTo(selectedTile)) return
if (selectedTile.isCityCenter() when {
&& (selectedTile.getOwner() == worldScreen.viewingCiv || worldScreen.viewingCiv.isSpectator())) { forceSelectUnit != null ->
selectUnit(forceSelectUnit)
selectedTile.isCityCenter() &&
(selectedTile.getOwner() == worldScreen.viewingCiv || worldScreen.viewingCiv.isSpectator()) ->
citySelected(selectedTile.getCity()!!) citySelected(selectedTile.getCity()!!)
} else if (selectedTile.militaryUnit != null selectedTile.militaryUnit != null &&
&& (selectedTile.militaryUnit!!.civInfo == worldScreen.viewingCiv || worldScreen.viewingCiv.isSpectator()) (selectedTile.militaryUnit!!.civInfo == worldScreen.viewingCiv || worldScreen.viewingCiv.isSpectator()) &&
&& selectedTile.militaryUnit!! !in selectedUnits selectedTile.militaryUnit!! !in selectedUnits &&
&& (selectedTile.civilianUnit == null || selectedUnit != selectedTile.civilianUnit)) { // Only select the military unit there if we do not currently have the civilian unit selected (selectedTile.civilianUnit == null || selectedUnit != selectedTile.civilianUnit) -> // Only select the military unit there if we do not currently have the civilian unit selected
selectUnit(selectedTile.militaryUnit!!, Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT)) selectUnit(selectedTile.militaryUnit!!, Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT))
} else if (selectedTile.civilianUnit != null selectedTile.civilianUnit != null
&& (selectedTile.civilianUnit!!.civInfo == worldScreen.viewingCiv || worldScreen.viewingCiv.isSpectator()) && (selectedTile.civilianUnit!!.civInfo == worldScreen.viewingCiv || worldScreen.viewingCiv.isSpectator())
&& selectedUnit != selectedTile.civilianUnit) { && selectedUnit != selectedTile.civilianUnit ->
selectUnit(selectedTile.civilianUnit!!, Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT)) selectUnit(selectedTile.civilianUnit!!, Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT))
} else if (selectedTile == previouslySelectedUnit?.currentTile) { selectedTile == previouslySelectedUnit?.currentTile -> {
// tapping the same tile again will deselect a unit. // tapping the same tile again will deselect a unit.
// important for single-tap-move to abort moving easily // important for single-tap-move to abort moving easily
selectUnit() selectUnit()
isVisible = false isVisible = false
} }
}
if (selectedUnit != previouslySelectedUnit || selectedUnits.size != previousNumberOfSelectedUnits) if (selectedUnit != previouslySelectedUnit || selectedUnits.size != previousNumberOfSelectedUnits)
selectedUnitHasChanged = true selectedUnitHasChanged = true