From 4fcbd48662de898f0031c2ffd67b98b477beb6dd Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Thu, 21 Sep 2023 14:00:13 +0200 Subject: [PATCH] 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 --- .../extensions/FormattingExtensions.kt | 1 + .../ui/screens/worldscreen/WorldScreen.kt | 1 + .../screens/worldscreen/WorldScreenTopBar.kt | 389 ------------------ .../worldscreen/topbar/WorldScreenTopBar.kt | 230 +++++++++++ .../topbar/WorldScreenTopBarResources.kt | 93 +++++ .../topbar/WorldScreenTopBarStats.kt | 141 +++++++ 6 files changed, 466 insertions(+), 389 deletions(-) delete mode 100644 core/src/com/unciv/ui/screens/worldscreen/WorldScreenTopBar.kt create mode 100644 core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBar.kt create mode 100644 core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBarResources.kt create mode 100644 core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBarStats.kt diff --git a/core/src/com/unciv/ui/components/extensions/FormattingExtensions.kt b/core/src/com/unciv/ui/components/extensions/FormattingExtensions.kt index c9988bb31a..9649545532 100644 --- a/core/src/com/unciv/ui/components/extensions/FormattingExtensions.kt +++ b/core/src/com/unciv/ui/components/extensions/FormattingExtensions.kt @@ -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 */ diff --git a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt index dd76f0a6e2..3fed17ebd6 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/WorldScreen.kt @@ -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 diff --git a/core/src/com/unciv/ui/screens/worldscreen/WorldScreenTopBar.kt b/core/src/com/unciv/ui/screens/worldscreen/WorldScreenTopBar.kt deleted file mode 100644 index 19f41ca514..0000000000 --- a/core/src/com/unciv/ui/screens/worldscreen/WorldScreenTopBar.kt +++ /dev/null @@ -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(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 - private val rightFillerCell: Cell - - //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 - - 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 - } - -} diff --git a/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBar.kt b/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBar.kt new file mode 100644 index 0000000000..2c243cc6b0 --- /dev/null +++ b/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBar.kt @@ -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 + + 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() + } + } +} diff --git a/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBarResources.kt b/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBarResources.kt new file mode 100644 index 0000000000..479efc1448 --- /dev/null +++ b/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBarResources.kt @@ -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(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() + } +} diff --git a/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBarStats.kt b/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBarStats.kt new file mode 100644 index 0000000000..88d5985c0c --- /dev/null +++ b/core/src/com/unciv/ui/screens/worldscreen/topbar/WorldScreenTopBarStats.kt @@ -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 + } +}