Reorganize and fix WorldScreenTopBar (#10154)

* Reorganize WorldScreenTopBar and change its update to rebuild the cells instead of tweaking layout

* Fix layout error: filler backgrounds looking too small
This commit is contained in:
SomeTroglodyte
2023-09-21 14:00:13 +02:00
committed by GitHub
parent b6db8df484
commit 4fcbd48662
6 changed files with 466 additions and 389 deletions

View File

@ -28,6 +28,7 @@ fun String.getConsumesAmountString(amount: Int, isStockpiled:Boolean): String {
/** Convert a [resource name][this] into "Need [amount] more $resource" string (untranslated) */
fun String.getNeedMoreAmountString(amount: Int) = "Need [$amount] more [$this]"
// todo: There's a few other `if (>0) "+" else ""` around, and a DecimalFormat solution in DetailedStatsPopup: unify
fun Int.toStringSigned() = if (this > 0) "+$this" else this.toString()
/** Formats the [Duration] into a translated string */

View File

@ -61,6 +61,7 @@ import com.unciv.ui.screens.worldscreen.status.MultiplayerStatusButton
import com.unciv.ui.screens.worldscreen.status.NextTurnButton
import com.unciv.ui.screens.worldscreen.status.NextTurnProgress
import com.unciv.ui.screens.worldscreen.status.StatusButtons
import com.unciv.ui.screens.worldscreen.topbar.WorldScreenTopBar
import com.unciv.ui.screens.worldscreen.unit.UnitTable
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsTable
import com.unciv.utils.Concurrency

View File

@ -1,389 +0,0 @@
package com.unciv.ui.screens.worldscreen
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Group
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Cell
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.logic.civilization.Civilization
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stats
import com.unciv.models.translations.tr
import com.unciv.ui.components.Fonts
import com.unciv.ui.components.MayaCalendar
import com.unciv.ui.components.YearTextUtil
import com.unciv.ui.components.extensions.colorFromRGB
import com.unciv.ui.components.extensions.darken
import com.unciv.ui.components.extensions.setFontColor
import com.unciv.ui.components.extensions.setFontSize
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toStringSigned
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.components.input.KeyboardBinding
import com.unciv.ui.components.input.onActivation
import com.unciv.ui.components.input.onClick
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.civilopediascreen.CivilopediaCategories
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
import com.unciv.ui.screens.overviewscreen.EmpireOverviewCategories
import com.unciv.ui.screens.overviewscreen.EmpireOverviewScreen
import com.unciv.ui.screens.pickerscreens.PolicyPickerScreen
import com.unciv.ui.screens.pickerscreens.TechPickerScreen
import com.unciv.ui.screens.victoryscreen.VictoryScreen
import com.unciv.ui.screens.worldscreen.mainmenu.WorldScreenMenuPopup
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.roundToInt
/**
* Table consisting of the menu button, current civ, some stats and the overview button for the top of [WorldScreen]
*/
//region Fields
class WorldScreenTopBar(val worldScreen: WorldScreen) : Table() {
//TODO shouldn't most onClick be addActivationAction instead?
private val turnsLabel = "Turns: 0/400".toLabel()
private val goldLabel = "0".toLabel(colorFromRGB(225, 217, 71))
private val scienceLabel = "0".toLabel(colorFromRGB(78, 140, 151))
private val happinessLabel = "0".toLabel()
private val cultureLabel = "0".toLabel(colorFromRGB(210, 94, 210))
private val faithLabel = "0".toLabel(colorFromRGB(168, 196, 241))
private data class ResourceActors(val resource: TileResource, val label: Label, val icon: Group)
private val resourceActors = ArrayList<ResourceActors>(12)
private val happinessImage = Group()
// These are all to improve performance IE reduce update time (was 150 ms on my phone, which is a lot!)
private val malcontentColor = colorFromRGB(239,83,80) // Color.valueOf("ef5350")
private val happinessColor = colorFromRGB(92, 194, 77) // Color.valueOf("8cc24d")
private val malcontentGroup = ImageGetter.getStatIcon("Malcontent")
private val happinessGroup = ImageGetter.getStatIcon("Happiness")
private val statsTable = getStatsTable()
private val resourcesWrapper = Table()
private val resourceTable = getResourceTable()
private val selectedCivTable = SelectedCivilizationTable(worldScreen)
private val overviewButton = OverviewAndSupplyTable(worldScreen)
private val leftFillerCell: Cell<BackgroundActor>
private val rightFillerCell: Cell<BackgroundActor>
//endregion
init {
// Not the Table, the Cells (all except one) have the background. To avoid gaps, _no_
// padding except inside the cell actors, and all actors need to _fill_ their cell.
val backColor = BaseScreen.skinStrings.skinConfig.baseColor.darken(0.5f)
statsTable.background = BaseScreen.skinStrings.getUiBackground("WorldScreen/TopBar/StatsTable", tintColor = backColor)
resourceTable.background = BaseScreen.skinStrings.getUiBackground("WorldScreen/TopBar/ResourceTable", tintColor = backColor)
add(statsTable).colspan(3).growX().row()
add(resourceTable).colspan(3).growX().row()
val leftFillerBG = BaseScreen.skinStrings.getUiBackground("WorldScreen/TopBar/LeftAttachment", BaseScreen.skinStrings.roundedEdgeRectangleShape, backColor)
leftFillerCell = add(BackgroundActor(leftFillerBG, Align.topLeft))
add().growX()
val rightFillerBG = BaseScreen.skinStrings.getUiBackground("WorldScreen/TopBar/RightAttachment", BaseScreen.skinStrings.roundedEdgeRectangleShape, backColor)
rightFillerCell = add(BackgroundActor(rightFillerBG, Align.topRight))
pack()
}
private fun getStatsTable(): Table {
val statsTable = Table()
statsTable.defaults().pad(8f, 3f, 3f, 3f)
fun addStat(label: Label, icon: String, isLast: Boolean = false, screenFactory: ()-> BaseScreen) {
val image = ImageGetter.getStatIcon(icon)
val action = {
worldScreen.game.pushScreen(screenFactory())
}
label.onClick(action)
image.onClick(action)
statsTable.add(label)
statsTable.add(image).padBottom(6f).size(20f).apply {
if (!isLast) padRight(20f)
}
}
fun addStat(label: Label, icon: String, overviewPage: EmpireOverviewCategories, isLast: Boolean = false) =
addStat(label, icon, isLast) { EmpireOverviewScreen(worldScreen.selectedCiv, overviewPage) }
addStat(goldLabel, "Gold", EmpireOverviewCategories.Stats)
addStat(scienceLabel, "Science") { TechPickerScreen(worldScreen.selectedCiv) }
statsTable.add(happinessImage).padBottom(6f).size(20f)
statsTable.add(happinessLabel).padRight(20f)
val invokeResourcesPage = {
worldScreen.openEmpireOverview(EmpireOverviewCategories.Resources)
}
happinessImage.onClick(invokeResourcesPage)
happinessLabel.onClick(invokeResourcesPage)
addStat(cultureLabel, "Culture") { PolicyPickerScreen(worldScreen.selectedCiv, worldScreen.canChangeState) }
if (worldScreen.gameInfo.isReligionEnabled()) {
addStat(faithLabel, "Faith", EmpireOverviewCategories.Religion, isLast = true)
} else {
statsTable.add("Religion: Off".toLabel())
}
statsTable.pack()
return statsTable
}
private fun getResourceTable(): Table {
// Since cells with invisible actors still occupy the full actor dimensions, we only prepare
// the future contents for resourcesWrapper here, they're added to the Table in updateResourcesTable
val resourceTable = Table()
resourcesWrapper.defaults().pad(5f, 5f, 10f, 5f)
resourcesWrapper.touchable = Touchable.enabled
turnsLabel.onClick {
if (worldScreen.selectedCiv.isLongCountDisplay()) {
val gameInfo = worldScreen.selectedCiv.gameInfo
MayaCalendar.openPopup(worldScreen, worldScreen.selectedCiv, gameInfo.getYear())
} else {
worldScreen.game.pushScreen(VictoryScreen(worldScreen))
}
}
resourcesWrapper.onClick {
worldScreen.openEmpireOverview(EmpireOverviewCategories.Resources)
}
val strategicResources = worldScreen.gameInfo.ruleset.tileResources.values
.filter { it.resourceType == ResourceType.Strategic && !it.hasUnique(UniqueType.CityResource) }
for (resource in strategicResources) {
val resourceImage = ImageGetter.getResourcePortrait(resource.name, 20f)
val resourceLabel = "0".toLabel()
resourceActors += ResourceActors(resource, resourceLabel, resourceImage)
}
// in case the icons are configured higher than a label, we add a dummy - height will be measured once before it's updated
if (resourceActors.isNotEmpty()) {
resourcesWrapper.add(resourceActors[0].icon)
resourceTable.add(resourcesWrapper)
}
resourceTable.add(turnsLabel).pad(5f, 5f, 10f, 5f)
return resourceTable
}
private class OverviewAndSupplyTable(worldScreen: WorldScreen) : Table(BaseScreen.skin) {
val unitSupplyImage = ImageGetter.getImage("OtherIcons/ExclamationMark")
.apply { color = Color.FIREBRICK }
val unitSupplyCell: Cell<Actor?>
init {
unitSupplyImage.onClick {
worldScreen.openEmpireOverview(EmpireOverviewCategories.Units)
}
val overviewButton = "Overview".toTextButton()
overviewButton.onActivation(binding = KeyboardBinding.EmpireOverview) {
worldScreen.openEmpireOverview()
}
unitSupplyCell = add()
add(overviewButton).pad(10f)
pack()
}
fun update(worldScreen: WorldScreen) {
val newVisible = worldScreen.selectedCiv.stats.getUnitSupplyDeficit() > 0
if (newVisible == unitSupplyCell.hasActor()) return
if (newVisible) unitSupplyCell.setActor(unitSupplyImage)
.size(50f).padLeft(10f)
else unitSupplyCell.setActor(null).size(0f).pad(0f)
invalidate()
pack()
}
}
private class SelectedCivilizationTable(worldScreen: WorldScreen) : Table(BaseScreen.skin) {
private var selectedCiv = ""
private val selectedCivLabel = "".toLabel()
private val menuButton = ImageGetter.getImage("OtherIcons/MenuIcon")
init {
left()
defaults().pad(10f)
menuButton.color = Color.WHITE
menuButton.onActivation(binding = KeyboardBinding.Menu) {
WorldScreenMenuPopup(worldScreen).open(force = true)
}
selectedCivLabel.setFontSize(25)
selectedCivLabel.onClick {
val civilopediaScreen = CivilopediaScreen(
worldScreen.selectedCiv.gameInfo.ruleset,
CivilopediaCategories.Nation,
worldScreen.selectedCiv.civName
)
worldScreen.game.pushScreen(civilopediaScreen)
}
add(menuButton).size(50f).padRight(0f)
add(selectedCivLabel).padRight(10f)
pack()
}
fun update(worldScreen: WorldScreen) {
val newCiv = worldScreen.selectedCiv.civName
if (this.selectedCiv == newCiv) return
this.selectedCiv = newCiv
selectedCivLabel.setText(newCiv.tr())
invalidate()
pack()
}
}
private fun layoutButtons() {
removeActor(selectedCivTable)
removeActor(overviewButton)
validate()
val statsWidth = statsTable.minWidth
val resourceWidth = resourceTable.minWidth
val overviewWidth = overviewButton.minWidth
val selectedCivWidth = selectedCivTable.minWidth
val leftRightNeeded = max(selectedCivWidth, overviewWidth)
val statsRowHeight = getRowHeight(0)
val baseHeight = statsRowHeight + getRowHeight(1)
// Check whether it gets cramped on narrow aspect ratios
val fillerHeight: Float // Height of the background filler cells
val buttonY: Float // Vertical center of Civ+Overview buttons relative to this.y
when {
leftRightNeeded * 2f > stage.width - resourceWidth -> {
// Need to shift buttons down to below both stats and resources
fillerHeight = baseHeight +1
buttonY = overviewButton.minHeight / 2f
}
leftRightNeeded * 2f > stage.width - statsWidth -> {
// Shifting buttons down to below stats row is enough
fillerHeight = statsRowHeight +1
buttonY = overviewButton.minHeight / 2f
}
else -> {
// Enough space to keep buttons to the left and right of stats and resources
fillerHeight = 0f
buttonY = baseHeight / 2f
}
}
val leftFillerWidth = if (fillerHeight > 0f) selectedCivWidth else 0f
val rightFillerWidth = if (fillerHeight > 0f) overviewWidth else 0f
if (leftFillerCell.minHeight != fillerHeight
|| leftFillerCell.minWidth != leftFillerWidth
|| rightFillerCell.minWidth != rightFillerWidth) {
// Gdx fail: containing Table isn't invalidated when setting Cell size
leftFillerCell.width(leftFillerWidth).height(fillerHeight)
rightFillerCell.width(rightFillerWidth).height(fillerHeight)
invalidate() // Without this all attempts to get a recalculated height are doomed
pack() // neither validate nor layout will include the new row height in height
}
width = stage.width
setPosition(0f, stage.height, Align.topLeft)
selectedCivTable.setPosition(1f, buttonY, Align.left)
overviewButton.setPosition(stage.width, buttonY, Align.right)
addActor(selectedCivTable) // needs to be after pack
addActor(overviewButton)
}
internal fun update(civInfo: Civilization) {
updateStatsTable(civInfo)
updateResourcesTable(civInfo)
selectedCivTable.update(worldScreen)
overviewButton.update(worldScreen)
layoutButtons()
}
private fun rateLabel(value: Float): String {
return (if (value > 0) "+" else "") + value.roundToInt()
}
private fun updateStatsTable(civInfo: Civilization) {
val nextTurnStats = civInfo.stats.statsForNextTurn
val goldPerTurn = " (" + rateLabel(nextTurnStats.gold) + ")"
goldLabel.setText(civInfo.gold.toString() + goldPerTurn)
scienceLabel.setText(rateLabel(nextTurnStats.science))
happinessLabel.setText(getHappinessText(civInfo))
if (civInfo.getHappiness() < 0) {
happinessLabel.setFontColor(malcontentColor)
happinessImage.clearChildren()
happinessImage.addActor(malcontentGroup)
} else {
happinessLabel.setFontColor(happinessColor)
happinessImage.clearChildren()
happinessImage.addActor(happinessGroup)
}
cultureLabel.setText(getCultureText(civInfo, nextTurnStats))
faithLabel.setText(civInfo.religionManager.storedFaith.toString() +
" (" + rateLabel(nextTurnStats.faith) + ")")
}
private fun updateResourcesTable(civInfo: Civilization) {
val yearText = YearTextUtil.toYearText(
civInfo.gameInfo.getYear(), civInfo.isLongCountDisplay()
)
turnsLabel.setText(Fonts.turn + "" + civInfo.gameInfo.turns + " | " + yearText)
resourcesWrapper.clearChildren()
var firstPadLeft = 20f // We want a distance from the turns entry to the first resource, but only if any resource is displayed
val civResources = civInfo.getCivResourcesByName()
val civResourceSupply = civInfo.getCivResourceSupply()
for ((resource, label, icon) in resourceActors) {
if (resource.hasUnique(UniqueType.NotShownOnWorldScreen)) continue
val amount = civResources[resource.name] ?: 0
if (resource.revealedBy != null && !civInfo.tech.isResearched(resource.revealedBy!!)
&& amount == 0) // You can trade for resources you cannot process yourself yet
continue
resourcesWrapper.add(icon).padLeft(firstPadLeft).padRight(0f)
firstPadLeft = 5f
if (!resource.isStockpiled())
label.setText(amount)
else {
val perTurn = civResourceSupply.firstOrNull { it.resource == resource }?.amount ?: 0
if (perTurn == 0) label.setText(amount)
else label.setText("$amount (${perTurn.toStringSigned()})")
}
resourcesWrapper.add(label).padTop(8f) // digits don't have descenders, so push them down a little
}
resourceTable.pack()
}
private fun getCultureText(civInfo: Civilization, nextTurnStats: Stats): String {
var cultureString = rateLabel(nextTurnStats.culture)
//if (nextTurnStats.culture == 0f) return cultureString // when you start the game, you're not producing any culture
val turnsToNextPolicy = (civInfo.policies.getCultureNeededForNextPolicy() - civInfo.policies.storedCulture) / nextTurnStats.culture
cultureString += if (turnsToNextPolicy <= 0f) " (!)"
else if (nextTurnStats.culture <= 0) " (∞)"
else " (" + ceil(turnsToNextPolicy).toInt() + ")"
return cultureString
}
private fun getHappinessText(civInfo: Civilization): String {
var happinessText = civInfo.getHappiness().toString()
val goldenAges = civInfo.goldenAges
happinessText +=
if (goldenAges.isGoldenAge())
" {GOLDEN AGE}(${goldenAges.turnsLeftForCurrentGoldenAge})".tr()
else
" (${goldenAges.storedHappiness}/${goldenAges.happinessRequiredForNextGoldenAge()})"
return happinessText
}
}

View File

@ -0,0 +1,230 @@
package com.unciv.ui.screens.worldscreen.topbar
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.ui.Cell
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.logic.civilization.Civilization
import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.darken
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.KeyboardBinding
import com.unciv.ui.components.input.onActivation
import com.unciv.ui.components.input.onClick
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.civilopediascreen.CivilopediaCategories
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
import com.unciv.ui.screens.overviewscreen.EmpireOverviewCategories
import com.unciv.ui.screens.worldscreen.BackgroundActor
import com.unciv.ui.screens.worldscreen.WorldScreen
import com.unciv.ui.screens.worldscreen.mainmenu.WorldScreenMenuPopup
import kotlin.math.max
/**
* Table consisting of the menu button, current civ, some stats and the overview button for the top of [WorldScreen].
*
* Calling [update] will refresh content and layout, and place the Table on the top edge of the stage, filling its width.
*
* [update] will also attempt geometry optimization:
* * When there's enough room, the top bar has the stats row ([WorldScreenTopBarStats]) and the resources
* row ([WorldScreenTopBarResources]), and the selected-civ ([SelectedCivilizationTable]) and overview
* ([OverviewAndSupplyTable]) button elements are overlaid (floating, not in a Cell) to the left and right.
* * When screen space gets cramped (low resolution or portrait mode) and one of the overlaid elements would
* cover parts of the stats and/or resources lines, we move them down accordingly - below the stats line
* if the resources still have enough room, below both otherwise.
* * But the elements should have a background - this is done with "filler cells". This Table is now 3x3,
* with the stats line as colspan(3) in the top row, resources also colspan(3) in the second row,
* and the third row is filler - empty - filler. These fillers do a background with just one rounded
* corner - bottom and to the screen center. The middle cell of that row has no actor and expands,
* and since the entire Table is Touchable.childrenOnly, completely transparent to the map below.
*
* Table layout in the "cramped" case:
* ```
* +----------------------------------------+
* | WorldScreenTopBarStats colspan(3) |
* +----------------------------------------+
* | WorldScreenTopBarResources colspan(3) |
* +----------------------------------------+
* | Filler | transparent!!! | Filler |
* +--------╝ ╚--------+
* ```
* Reminder: Not the `Table`, the `Cell` actors (all except the transparent one) have the background.
* To avoid gaps, _no_ padding except inside the cell actors, and all actors need to _fill_ their cell.
*/
//region Fields
class WorldScreenTopBar(internal val worldScreen: WorldScreen) : Table() {
private val statsTable = WorldScreenTopBarStats(this)
private val resourceTable = WorldScreenTopBarResources(this)
private val selectedCivTable = SelectedCivilizationTable(worldScreen)
private val overviewButton = OverviewAndSupplyTable(worldScreen)
private val leftFiller: BackgroundActor
private val rightFiller: BackgroundActor
companion object {
/** When the "fillers" are used, this is added to the required height, alleviating the "gap" problem a little. */
const val gapFillingExtraHeight = 1f
}
//endregion
init {
// init only prepares, the cells are created by update()
defaults().center()
setRound(false) // Prevent Table from doing internal rounding which would provoke gaps
val backColor = BaseScreen.skinStrings.skinConfig.baseColor.darken(0.5f)
statsTable.background = BaseScreen.skinStrings.getUiBackground("WorldScreen/TopBar/StatsTable", tintColor = backColor)
resourceTable.background = BaseScreen.skinStrings.getUiBackground("WorldScreen/TopBar/ResourceTable", tintColor = backColor)
val leftFillerBG = BaseScreen.skinStrings.getUiBackground("WorldScreen/TopBar/LeftAttachment", BaseScreen.skinStrings.roundedEdgeRectangleShape, backColor)
leftFiller = BackgroundActor(leftFillerBG, Align.topLeft)
val rightFillerBG = BaseScreen.skinStrings.getUiBackground("WorldScreen/TopBar/RightAttachment", BaseScreen.skinStrings.roundedEdgeRectangleShape, backColor)
rightFiller = BackgroundActor(rightFillerBG, Align.topRight)
}
internal fun update(civInfo: Civilization) {
setLayoutEnabled(false)
statsTable.update(civInfo)
resourceTable.update(civInfo)
selectedCivTable.update(worldScreen)
overviewButton.update(worldScreen)
updateLayout()
setLayoutEnabled(true)
}
/** Performs the layout tricks mentioned in the class Kdoc */
private fun updateLayout() {
val targetWidth = stage.width
val resourceWidth = resourceTable.minWidth
val overviewWidth = overviewButton.minWidth
val overviewHeight = overviewButton.minHeight
val selectedCivWidth = selectedCivTable.minWidth
val selectedCivHeight = selectedCivTable.minHeight
// Since stats/resource lines are centered, the max decides when to snap the overlaid elements down
val leftRightNeeded = max(selectedCivWidth, overviewWidth)
// Height of the two "overlay" elements should be equal, but just in case:
val overlayHeight = max(overviewHeight, selectedCivHeight)
clear()
// Without the explicit cell width, a 'stats' line wider than the stage can force the Table to
// misbehave and place the filler actors out of bounds, even if Table.width is correct.
add(statsTable).colspan(3).growX().width(targetWidth).row()
// Probability of a too-wide resources line is low in Vanilla, but mods may have lots more...
add(resourceTable).colspan(3).growX().width(targetWidth).row()
layout() // force rowHeight calculation - validate is not enough - Table quirks
val statsRowHeight = getRowHeight(0)
val baseHeight = statsRowHeight + getRowHeight(1)
val statsWidth = statsTable.width
fun addFillers(fillerHeight: Float) {
add(leftFiller).size(selectedCivWidth, fillerHeight + gapFillingExtraHeight)
add().growX()
add(rightFiller).size(overviewWidth, fillerHeight + gapFillingExtraHeight)
}
// Check whether it gets cramped on narrow aspect ratios
val centerButtonsToHeight = when {
leftRightNeeded * 2f > targetWidth - resourceWidth -> {
// Need to shift buttons down to below both stats and resources
addFillers(overlayHeight)
overlayHeight
}
leftRightNeeded * 2f > targetWidth - statsWidth -> {
// Shifting buttons down to below stats row is enough
addFillers(statsRowHeight)
overlayHeight
}
else -> {
// Enough space to keep buttons to the left and right of stats and resources - no fillers
baseHeight
}
}
// Don't use align with setPosition as we haven't pack()ed and element dimensions might not be final
setSize(targetWidth, prefHeight) // sizing to prefHeight is half a pack()
setPosition(0f, stage.height - prefHeight)
selectedCivTable.setPosition(0f, (centerButtonsToHeight - selectedCivHeight) / 2f)
overviewButton.setPosition(targetWidth - overviewWidth, (centerButtonsToHeight - overviewHeight) / 2f)
addActor(selectedCivTable) // needs to be after size
addActor(overviewButton)
}
private class OverviewAndSupplyTable(worldScreen: WorldScreen) : Table(BaseScreen.skin) {
val unitSupplyImage = ImageGetter.getImage("OtherIcons/ExclamationMark")
.apply { color = Color.FIREBRICK }
val unitSupplyCell: Cell<Actor?>
init {
unitSupplyImage.onClick {
worldScreen.openEmpireOverview(EmpireOverviewCategories.Units)
}
val overviewButton = "Overview".toTextButton()
overviewButton.onActivation(binding = KeyboardBinding.EmpireOverview) {
worldScreen.openEmpireOverview()
}
unitSupplyCell = add()
add(overviewButton).pad(10f)
pack()
}
fun update(worldScreen: WorldScreen) {
val newVisible = worldScreen.selectedCiv.stats.getUnitSupplyDeficit() > 0
if (newVisible == unitSupplyCell.hasActor()) return
if (newVisible) unitSupplyCell.setActor(unitSupplyImage)
.size(50f).padLeft(10f)
else unitSupplyCell.setActor(null).size(0f).pad(0f)
invalidate()
pack()
}
}
private class SelectedCivilizationTable(worldScreen: WorldScreen) : Table(BaseScreen.skin) {
private var selectedCiv = ""
private val selectedCivLabel = "".toLabel()
private val menuButton = ImageGetter.getImage("OtherIcons/MenuIcon")
init {
left()
defaults().pad(10f)
menuButton.color = Color.WHITE
menuButton.onActivation(binding = KeyboardBinding.Menu) {
WorldScreenMenuPopup(worldScreen).open(force = true)
}
selectedCivLabel.setFontSize(25)
selectedCivLabel.onClick {
val civilopediaScreen = CivilopediaScreen(
worldScreen.selectedCiv.gameInfo.ruleset,
CivilopediaCategories.Nation,
worldScreen.selectedCiv.civName
)
worldScreen.game.pushScreen(civilopediaScreen)
}
add(menuButton).size(50f).padRight(0f)
add(selectedCivLabel)
pack()
}
fun update(worldScreen: WorldScreen) {
val newCiv = worldScreen.selectedCiv.civName
if (this.selectedCiv == newCiv) return
this.selectedCiv = newCiv
selectedCivLabel.setText(newCiv.tr()) // Will include nation icon
invalidate()
pack()
}
}
}

View File

@ -0,0 +1,93 @@
package com.unciv.ui.screens.worldscreen.topbar
import com.badlogic.gdx.scenes.scene2d.Group
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.logic.civilization.Civilization
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.components.Fonts
import com.unciv.ui.components.MayaCalendar
import com.unciv.ui.components.YearTextUtil
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toStringSigned
import com.unciv.ui.components.input.onClick
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.overviewscreen.EmpireOverviewCategories
import com.unciv.ui.screens.victoryscreen.VictoryScreen
internal class WorldScreenTopBarResources(topbar: WorldScreenTopBar) : Table() {
private val turnsLabel = "Turns: 0/400".toLabel()
private data class ResourceActors(val resource: TileResource, val label: Label, val icon: Group)
private val resourceActors = ArrayList<ResourceActors>(12)
private val resourcesWrapper = Table()
init {
resourcesWrapper.defaults().pad(5f, 5f, 10f, 5f)
resourcesWrapper.touchable = Touchable.enabled
val worldScreen = topbar.worldScreen
turnsLabel.onClick {
if (worldScreen.selectedCiv.isLongCountDisplay()) {
val gameInfo = worldScreen.selectedCiv.gameInfo
MayaCalendar.openPopup(worldScreen, worldScreen.selectedCiv, gameInfo.getYear())
} else {
worldScreen.game.pushScreen(VictoryScreen(worldScreen))
}
}
resourcesWrapper.onClick {
worldScreen.openEmpireOverview(EmpireOverviewCategories.Resources)
}
val strategicResources = worldScreen.gameInfo.ruleset.tileResources.values
.filter { it.resourceType == ResourceType.Strategic && !it.hasUnique(UniqueType.CityResource) }
for (resource in strategicResources) {
val resourceImage = ImageGetter.getResourcePortrait(resource.name, 20f)
val resourceLabel = "0".toLabel()
resourceActors += ResourceActors(resource, resourceLabel, resourceImage)
}
// in case the icons are configured higher than a label, we add a dummy - height will be measured once before it's updated
if (resourceActors.isNotEmpty()) {
resourcesWrapper.add(resourceActors[0].icon)
add(resourcesWrapper)
}
add(turnsLabel).pad(5f, 5f, 10f, 5f)
}
fun update(civInfo: Civilization) {
val yearText = YearTextUtil.toYearText(
civInfo.gameInfo.getYear(), civInfo.isLongCountDisplay()
)
turnsLabel.setText(Fonts.turn + "" + civInfo.gameInfo.turns + " | " + yearText)
resourcesWrapper.clearChildren()
var firstPadLeft = 20f // We want a distance from the turns entry to the first resource, but only if any resource is displayed
val civResources = civInfo.getCivResourcesByName()
val civResourceSupply = civInfo.getCivResourceSupply()
for ((resource, label, icon) in resourceActors) {
if (resource.hasUnique(UniqueType.NotShownOnWorldScreen)) continue
val amount = civResources[resource.name] ?: 0
if (resource.revealedBy != null && !civInfo.tech.isResearched(resource.revealedBy!!)
&& amount == 0) // You can trade for resources you cannot process yourself yet
continue
resourcesWrapper.add(icon).padLeft(firstPadLeft).padRight(0f)
firstPadLeft = 5f
if (!resource.isStockpiled())
label.setText(amount)
else {
val perTurn = civResourceSupply.firstOrNull { it.resource == resource }?.amount ?: 0
if (perTurn == 0) label.setText(amount)
else label.setText("$amount (${perTurn.toStringSigned()})")
}
resourcesWrapper.add(label).padTop(8f) // digits don't have descenders, so push them down a little
}
pack()
}
}

View File

@ -0,0 +1,141 @@
package com.unciv.ui.screens.worldscreen.topbar
import com.badlogic.gdx.scenes.scene2d.Group
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.logic.civilization.Civilization
import com.unciv.models.stats.Stats
import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.colorFromRGB
import com.unciv.ui.components.extensions.setFontColor
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toStringSigned
import com.unciv.ui.components.input.onClick
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.overviewscreen.EmpireOverviewCategories
import com.unciv.ui.screens.overviewscreen.EmpireOverviewScreen
import com.unciv.ui.screens.pickerscreens.PolicyPickerScreen
import com.unciv.ui.screens.pickerscreens.TechPickerScreen
import kotlin.math.ceil
import kotlin.math.roundToInt
internal class WorldScreenTopBarStats(topbar: WorldScreenTopBar) : Table() {
private val goldLabel = "0".toLabel(colorFromRGB(225, 217, 71))
private val scienceLabel = "0".toLabel(colorFromRGB(78, 140, 151))
private val happinessLabel = "0".toLabel()
private val cultureLabel = "0".toLabel(colorFromRGB(210, 94, 210))
private val faithLabel = "0".toLabel(colorFromRGB(168, 196, 241))
private val happinessContainer = Group()
// These are all to improve performance IE reduce update time (was 150 ms on my phone, which is a lot!)
private val malcontentColor = colorFromRGB(239,83,80) // Color.valueOf("ef5350")
private val happinessColor = colorFromRGB(92, 194, 77) // Color.valueOf("8cc24d")
private val malcontentImage = ImageGetter.getStatIcon("Malcontent")
private val happinessImage = ImageGetter.getStatIcon("Happiness")
private val worldScreen = topbar.worldScreen
companion object {
const val defaultImageSize = 20f
const val defaultHorizontalPad = 3f
const val defaultTopPad = 8f
const val defaultBottomPad = 3f
const val defaultImageBottomPad = 6f
const val padRightBetweenStats = 20f
}
init {
fun addStat(label: Label, icon: String, isLast: Boolean = false, screenFactory: ()-> BaseScreen) {
val image = ImageGetter.getStatIcon(icon)
val action = {
worldScreen.game.pushScreen(screenFactory())
}
label.onClick(action)
image.onClick(action)
add(label)
add(image).padBottom(defaultImageBottomPad).size(defaultImageSize).apply {
if (!isLast) padRight(padRightBetweenStats)
}
}
fun addStat(label: Label, icon: String, overviewPage: EmpireOverviewCategories, isLast: Boolean = false) =
addStat(label, icon, isLast) { EmpireOverviewScreen(worldScreen.selectedCiv, overviewPage) }
defaults().pad(defaultTopPad, defaultHorizontalPad, defaultBottomPad, defaultHorizontalPad)
addStat(goldLabel, "Gold", EmpireOverviewCategories.Stats)
addStat(scienceLabel, "Science") { TechPickerScreen(worldScreen.selectedCiv) }
add(happinessContainer).padBottom(defaultImageBottomPad).size(defaultImageSize)
add(happinessLabel).padRight(padRightBetweenStats)
val invokeResourcesPage = {
worldScreen.openEmpireOverview(EmpireOverviewCategories.Resources)
}
happinessContainer.onClick(invokeResourcesPage)
happinessLabel.onClick(invokeResourcesPage)
addStat(cultureLabel, "Culture") { PolicyPickerScreen(worldScreen.selectedCiv, worldScreen.canChangeState) }
if (worldScreen.gameInfo.isReligionEnabled()) {
addStat(faithLabel, "Faith", EmpireOverviewCategories.Religion, isLast = true)
} else {
add("Religion: Off".toLabel())
}
//saveDimensions()
}
private fun rateLabel(value: Float) = value.roundToInt().toStringSigned()
fun update(civInfo: Civilization) {
//resetChildrenSizes()
val nextTurnStats = civInfo.stats.statsForNextTurn
val goldPerTurn = " (" + rateLabel(nextTurnStats.gold) + ")"
goldLabel.setText(civInfo.gold.toString() + goldPerTurn)
scienceLabel.setText(rateLabel(nextTurnStats.science))
happinessLabel.setText(getHappinessText(civInfo))
if (civInfo.getHappiness() < 0) {
happinessLabel.setFontColor(malcontentColor)
happinessContainer.clearChildren()
happinessContainer.addActor(malcontentImage)
} else {
happinessLabel.setFontColor(happinessColor)
happinessContainer.clearChildren()
happinessContainer.addActor(happinessImage)
}
cultureLabel.setText(getCultureText(civInfo, nextTurnStats))
faithLabel.setText(civInfo.religionManager.storedFaith.toString() +
" (" + rateLabel(nextTurnStats.faith) + ")")
//scaleToMaxWidth(worldScreen.stage.width)
pack()
}
private fun getCultureText(civInfo: Civilization, nextTurnStats: Stats): String {
var cultureString = rateLabel(nextTurnStats.culture)
//if (nextTurnStats.culture == 0f) return cultureString // when you start the game, you're not producing any culture
val turnsToNextPolicy = (civInfo.policies.getCultureNeededForNextPolicy() - civInfo.policies.storedCulture) / nextTurnStats.culture
cultureString += if (turnsToNextPolicy <= 0f) " (!)"
else if (nextTurnStats.culture <= 0) " (∞)"
else " (" + ceil(turnsToNextPolicy).toInt() + ")"
return cultureString
}
private fun getHappinessText(civInfo: Civilization): String {
var happinessText = civInfo.getHappiness().toString()
val goldenAges = civInfo.goldenAges
happinessText +=
if (goldenAges.isGoldenAge())
" {GOLDEN AGE}(${goldenAges.turnsLeftForCurrentGoldenAge})".tr()
else
" (${goldenAges.storedHappiness}/${goldenAges.happinessRequiredForNextGoldenAge()})"
return happinessText
}
}