mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-14 01:39:40 +07:00
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:
@ -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 */
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user