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
5 changed files with 228 additions and 116 deletions

View File

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

View File

@ -1,9 +1,13 @@
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.WidgetGroup
import com.badlogic.gdx.utils.Align
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.ui.utils.BaseScreen
import com.unciv.ui.utils.packIfNeeded
import com.unciv.ui.utils.toLabel
abstract class EmpireOverviewTab (
val viewingPlayer: CivilizationInfo,
@ -23,8 +27,19 @@ abstract class EmpireOverviewTab (
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) {
for (table in tables)
table.packIfNeeded()
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)
.mapTo(ArrayList(columns)) { column ->
tables.maxOf { it.getColumnWidth(column) }
@ -32,6 +47,14 @@ abstract class EmpireOverviewTab (
for (table in tables) {
for (column in 0 until columns)
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)
}
table.invalidate()

View File

@ -1,13 +1,18 @@
package com.unciv.ui.overviewscreen
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.WidgetGroup
import com.badlogic.gdx.utils.Align
import com.unciv.Constants
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.utils.*
import java.text.DecimalFormat
import com.unciv.ui.worldscreen.unit.UnitActions
import kotlin.math.abs
/**
@ -15,111 +20,191 @@ import kotlin.math.abs
*/
class UnitOverviewTab(
viewingPlayer: CivilizationInfo,
overviewScreen: EmpireOverviewScreen
overviewScreen: EmpireOverviewScreen,
persistedData: EmpireOverviewTabPersistableData? = null
) : EmpireOverviewTab(viewingPlayer, overviewScreen) {
class UnitTabPersistableData(
var scrollY: Float? = null
) : EmpireOverviewTabPersistableData() {
override fun isEmpty() = scrollY == null
}
override val persistableData = (persistedData as? UnitTabPersistableData) ?: UnitTabPersistableData()
init {
add(getUnitSupplyTable()).top().padRight(25f)
add(getUnitListTable())
pack()
override fun activated() = persistableData.scrollY
override fun deactivated(scrollY: Float) {
persistableData.scrollY = scrollY
}
private fun getUnitSupplyTable(): Table {
val unitSupplyTable = Table(BaseScreen.skin)
unitSupplyTable.defaults().pad(5f)
unitSupplyTable.apply {
add("Unit Supply".tr()).colspan(2).center().row()
addSeparator()
add("Base Supply".tr()).left()
add(viewingPlayer.stats().getBaseUnitSupply().toLabel()).right().row()
add("Cities".tr()).left()
add(viewingPlayer.stats().getUnitSupplyFromCities().toLabel()).right().row()
add("Population".tr()).left()
add(viewingPlayer.stats().getUnitSupplyFromPop().toLabel()).right().row()
addSeparator()
add("Total Supply".tr()).left()
add(viewingPlayer.stats().getUnitSupply().toLabel()).right().row()
add("In Use".tr()).left()
add(viewingPlayer.getCivUnitsSize().toLabel()).right().row()
addSeparator()
val deficit = viewingPlayer.stats().getUnitSupplyDeficit()
add("Supply Deficit".tr()).left()
add(deficit.toLabel()).right().row()
add("Production Penalty".tr()).left()
add((viewingPlayer.stats().getUnitSupplyProductionPenalty()).toInt().toString()+"%").right().row()
private val supplyTableWidth = (overviewScreen.stage.width * 0.25f).coerceAtLeast(240f)
private val unitListTable = Table() // could be `this` instead, extra nesting helps readability a little
private val unitHeaderTable = Table()
override fun getFixedContent(): WidgetGroup {
return Table().apply {
add(getUnitSupplyTable()).align(Align.top).padBottom(10f).row()
add(unitHeaderTable.updateUnitHeaderTable())
equalizeColumns(unitListTable, unitHeaderTable)
}
}
init {
add(unitListTable.updateUnitListTable())
}
// Here overloads are simpler than a generic:
private fun Table.addLabeledValue (label: String, value: Int) {
add(label.toLabel()).left()
add(value.toLabel()).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) {
val penaltyLabel = "Increase your supply or reduce the amount of units to remove the production penalty"
.toLabel(Color.FIREBRICK)
penaltyLabel.wrap = true
add(penaltyLabel).colspan(2).left()
.width(overviewScreen.stage.width * 0.2f).row()
it.add(penaltyLabel).colspan(2).left()
.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 unitListTable = Table(BaseScreen.skin)
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()
defaults().pad(5f)
for (unit in viewingPlayer.getCivUnits().sortedWith(
compareBy({ it.displayName() },
{ !it.due },
{ it.currentMovement <= Constants.minimumMovementEpsilon },
{ abs(it.currentTile.position.x) + abs(it.currentTile.position.y) })
)) {
val baseUnit = unit.baseUnit()
for (unit in viewingPlayer.getCivUnits().sortedWith(
compareBy({ it.displayName() },
{ !it.due },
{ it.currentMovement <= Constants.minimumMovementEpsilon },
{ abs(it.currentTile.position.x) + abs(it.currentTile.position.y) })
)) {
val baseUnit = unit.baseUnit()
val button = IconTextButton(unit.displayName(), UnitGroup(unit, 20f))
button.onClick {
game.setWorldScreen()
game.worldScreen.mapHolder.setCenterPosition(unit.currentTile.position)
}
add(button).left()
if (unit.action == null) add()
else add(unit.getActionLabel().tr())
if (baseUnit.strength > 0) add(baseUnit.strength.toString()) else add()
if (baseUnit.rangedStrength > 0) add(baseUnit.rangedStrength.toString()) else add()
add(DecimalFormat("0.#").format(unit.currentMovement) + "/" + unit.getMaxMovement())
val closestCity =
unit.getTile().getTilesInDistance(3).firstOrNull { it.isCityCenter() }
if (closestCity != null) add(closestCity.getCity()!!.name.tr()) else add()
val promotionsTable = Table()
// getPromotions goes by json order on demand, so this is same sorting as on picker
for (promotion in unit.promotions.getPromotions(true))
promotionsTable.add(ImageGetter.getPromotionIcon(promotion.name))
if (unit.promotions.canBePromoted()) promotionsTable.add(
ImageGetter.getImage("OtherIcons/Star").apply { color = Color.GOLDENROD })
.size(24f).padLeft(8f)
if (unit.canUpgrade()) promotionsTable.add(
ImageGetter.getUnitIcon(
unit.getUnitToUpgradeTo().name,
Color.GREEN
)
).size(28f).padLeft(8f)
promotionsTable.onClick {
if (unit.promotions.canBePromoted() || unit.promotions.promotions.isNotEmpty()) {
game.setScreen(PromotionPickerScreen(unit))
}
}
add(promotionsTable)
if (unit.health < 100) add(unit.health.toString()) else add()
row()
// 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 {
showWorldScreenAt(unit)
}
add(button).fillX()
// Columns: action, strength, ranged, moves
if (unit.action == null) add() else add(unit.getActionLabel().toLabel())
if (baseUnit.strength > 0) add(baseUnit.strength.toLabel()) else add()
if (baseUnit.rangedStrength > 0) add(baseUnit.rangedStrength.toLabel()) else add()
add(unit.getMovementString().toLabel())
// Closest city column
val closestCity =
unit.getTile().getTilesInDistance(3).firstOrNull { it.isCityCenter() }
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()
// getPromotions goes by json order on demand, so this is same sorting as on picker
for (promotion in unit.promotions.getPromotions(true))
promotionsTable.add(ImageGetter.getPromotionIcon(promotion.name))
if (unit.promotions.canBePromoted())
promotionsTable.add(
ImageGetter.getImage("OtherIcons/Star").apply {
color = if (game.worldScreen.canChangeState && unit.currentMovement > 0f && unit.attacksThisTurn == 0)
Color.GOLDENROD
else Color.GOLDENROD.darken(0.25f)
}
).size(24f).padLeft(8f)
promotionsTable.onClick {
if (unit.promotions.canBePromoted() || unit.promotions.promotions.isNotEmpty()) {
game.setScreen(PromotionPickerScreen(unit))
overviewScreen.dispose()
}
}
add(promotionsTable)
// 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()
}
return unitListTable
return this
}
}

View File

@ -62,7 +62,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
internal fun addTiles() {
val tileSetStrings = 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()
for (tileGroup in daTileGroups) {
@ -130,7 +130,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
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)
center(worldScreen.stage)
@ -654,11 +654,11 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
* @param selectUnit Select a unit at the destination
* @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
selectedTile = tileGroup.tileInfo
if (selectUnit)
worldScreen.bottomUnitTable.tileSelected(selectedTile!!)
if (selectUnit || forceSelectUnit != null)
worldScreen.bottomUnitTable.tileSelected(selectedTile!!, forceSelectUnit)
val originalScrollX = scrollX
val originalScrollY = scrollY

View File

@ -243,7 +243,7 @@ class UnitTable(val worldScreen: WorldScreen) : Table(){
return true
}
fun tileSelected(selectedTile: TileInfo) {
fun tileSelected(selectedTile: TileInfo, forceSelectUnit: MapUnit? = null) {
val previouslySelectedUnit = selectedUnit
val previousNumberOfSelectedUnits = selectedUnits.size
@ -251,23 +251,27 @@ 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
if (selectedUnitIsSwapping && selectedUnit != null && selectedUnit!!.movement.canUnitSwapTo(selectedTile)) return
if (selectedTile.isCityCenter()
&& (selectedTile.getOwner() == worldScreen.viewingCiv || worldScreen.viewingCiv.isSpectator())) {
citySelected(selectedTile.getCity()!!)
} else if (selectedTile.militaryUnit != null
&& (selectedTile.militaryUnit!!.civInfo == worldScreen.viewingCiv || worldScreen.viewingCiv.isSpectator())
&& 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
selectUnit(selectedTile.militaryUnit!!, Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT))
} else if (selectedTile.civilianUnit != null
&& (selectedTile.civilianUnit!!.civInfo == worldScreen.viewingCiv || worldScreen.viewingCiv.isSpectator())
&& selectedUnit != selectedTile.civilianUnit) {
selectUnit(selectedTile.civilianUnit!!, Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT))
} else if (selectedTile == previouslySelectedUnit?.currentTile) {
// tapping the same tile again will deselect a unit.
// important for single-tap-move to abort moving easily
selectUnit()
isVisible = false
when {
forceSelectUnit != null ->
selectUnit(forceSelectUnit)
selectedTile.isCityCenter() &&
(selectedTile.getOwner() == worldScreen.viewingCiv || worldScreen.viewingCiv.isSpectator()) ->
citySelected(selectedTile.getCity()!!)
selectedTile.militaryUnit != null &&
(selectedTile.militaryUnit!!.civInfo == worldScreen.viewingCiv || worldScreen.viewingCiv.isSpectator()) &&
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
selectUnit(selectedTile.militaryUnit!!, Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT))
selectedTile.civilianUnit != null
&& (selectedTile.civilianUnit!!.civInfo == worldScreen.viewingCiv || worldScreen.viewingCiv.isSpectator())
&& selectedUnit != selectedTile.civilianUnit ->
selectUnit(selectedTile.civilianUnit!!, Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT))
selectedTile == previouslySelectedUnit?.currentTile -> {
// tapping the same tile again will deselect a unit.
// important for single-tap-move to abort moving easily
selectUnit()
isVisible = false
}
}
if (selectedUnit != previouslySelectedUnit || selectedUnits.size != previousNumberOfSelectedUnits)