From a8dbd4784cc6fc55a4108921e4fa804b60ef847e Mon Sep 17 00:00:00 2001 From: Yair Morgenstern Date: Mon, 24 Jan 2022 11:04:12 +0200 Subject: [PATCH] Converted stat list to stat tree (#6022) * Converted stat list to stat tree - current changes do not affect UI at all, since we're still going by the shallow mapping that existed beforehand * Display details of both buildings and uniques * Unique stats now add correctly to building base stats, good thing we have tests :) * Stat details are now click-to-expand, and calculate correctly :) * Added small +/- button to show it's expandable --- .../com/unciv/logic/city/CityConstructions.kt | 6 +- core/src/com/unciv/logic/city/CityStats.kt | 104 ++++++++++--- .../unciv/logic/civilization/TechManager.kt | 2 +- .../com/unciv/ui/cityscreen/CityInfoTable.kt | 142 ++++++++++++------ 4 files changed, 176 insertions(+), 78 deletions(-) diff --git a/core/src/com/unciv/logic/city/CityConstructions.kt b/core/src/com/unciv/logic/city/CityConstructions.kt index 1bbab6c4aa..49a9890213 100644 --- a/core/src/com/unciv/logic/city/CityConstructions.kt +++ b/core/src/com/unciv/logic/city/CityConstructions.kt @@ -84,10 +84,10 @@ class CityConstructions { /** * @return [Stats] provided by all built buildings in city plus the bonus from Library */ - fun getStats(): Stats { - val stats = Stats() + fun getStats(): StatTreeNode { + val stats = StatTreeNode() for (building in getBuiltBuildings()) - stats.add(building.getStats(cityInfo)) + stats.addStats(building.getStats(cityInfo), building.name) return stats } diff --git a/core/src/com/unciv/logic/city/CityStats.kt b/core/src/com/unciv/logic/city/CityStats.kt index cc049aa506..113bdcab4e 100644 --- a/core/src/com/unciv/logic/city/CityStats.kt +++ b/core/src/com/unciv/logic/city/CityStats.kt @@ -7,9 +7,7 @@ import com.unciv.logic.map.RoadStatus import com.unciv.models.Counter import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.ModOptionsConstants -import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.Unique -import com.unciv.models.ruleset.unique.UniqueMapTyped import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.stats.Stat @@ -19,6 +17,42 @@ import com.unciv.ui.utils.toPercent import kotlin.math.min +class StatTreeNode { + val children = LinkedHashMap() + private var innerStats: Stats? = null + + private fun addInnerStats(stats: Stats) { + if (innerStats == null) innerStats = stats + else innerStats!!.add(stats) // What happens if we add 2 stats to the same leaf? + } + + fun addStats(newStats: Stats, vararg hierarchyList: String) { + if (hierarchyList.isEmpty()) { + addInnerStats(newStats) + return + } + val childName = hierarchyList.first() + if (!children.containsKey(childName)) + children[childName] = StatTreeNode() + children[childName]!!.addStats(newStats, *hierarchyList.drop(1).toTypedArray()) + } + + fun add(otherTree: StatTreeNode) { + if (otherTree.innerStats != null) addInnerStats(otherTree.innerStats!!) + for ((key, value) in otherTree.children) { + if (!children.containsKey(key)) children[key] = value + else children[key]!!.add(value) + } + } + + val totalStats: Stats by lazy { + val toReturn = Stats() + if (innerStats != null) toReturn.add(innerStats!!) + for (child in children.values) toReturn.add(child.totalStats) + toReturn + } +} + /** Holds and calculates [Stats] for a city. * * No field needs to be saved, all are calculated on the fly, @@ -27,6 +61,8 @@ import kotlin.math.min class CityStats(val cityInfo: CityInfo) { //region Fields, Transient + var baseStatTree = StatTreeNode() + var baseStatList = LinkedHashMap() var statPercentBonusList = LinkedHashMap() @@ -168,10 +204,10 @@ class CityStats(val cityInfo: CityInfo) { } - private fun getStatsFromUniquesBySource():StatMap { - val sourceToStats = StatMap() + private fun getStatsFromUniquesBySource(): StatTreeNode { + val sourceToStats = StatTreeNode() fun addUniqueStats(unique:Unique) = - sourceToStats.add(unique.sourceObjectType?.name ?: "", unique.stats) + sourceToStats.addStats(unique.stats, unique.sourceObjectType?.name ?: "", unique.sourceObjectName ?: "") for (unique in cityInfo.getMatchingUniques(UniqueType.Stats)) addUniqueStats(unique) @@ -184,7 +220,7 @@ class CityStats(val cityInfo: CityInfo) { for (unique in cityInfo.getMatchingUniques(UniqueType.StatsPerPopulation)) if (cityInfo.matchesFilter(unique.params[2])) { val amountOfEffects = (cityInfo.population.population / unique.params[1].toInt()).toFloat() - sourceToStats.add(unique.sourceObjectType?.name ?: "", unique.stats.times(amountOfEffects)) + sourceToStats.addStats(unique.stats.times(amountOfEffects), unique.sourceObjectType?.name ?: "", unique.sourceObjectName ?: "") } for (unique in cityInfo.getMatchingUniques(UniqueType.StatsFromXPopulation)) @@ -201,7 +237,7 @@ class CityStats(val cityInfo: CityInfo) { addUniqueStats(unique) // - renameStatmapKeys(sourceToStats) + renameStatmapKeys(sourceToStats.children) return sourceToStats } @@ -218,6 +254,18 @@ class CityStats(val cityInfo: CityInfo) { } + private fun renameStatmapKeys(statMap: LinkedHashMap){ + fun rename(source: String, displayedSource: String) { + if (!statMap.containsKey(source)) return + statMap.put(displayedSource, statMap[source]!!) + statMap.remove(source) + } + rename("Wonder", "Wonders") + rename("Building", "Buildings") + rename("Policy", "Policies") + } + + private fun getStatPercentBonusesFromGoldenAge(isGoldenAge: Boolean): Stats { val stats = Stats() if (isGoldenAge) { @@ -348,7 +396,7 @@ class CityStats(val cityInfo: CityInfo) { // needs to be a separate function because we need to know the global happiness state // in order to determine how much food is produced in a city! - fun updateCityHappiness(statsFromBuildings: Stats) { + fun updateCityHappiness(statsFromBuildings: StatTreeNode) { val civInfo = cityInfo.civInfo val newHappinessList = LinkedHashMap() var unhappinessModifier = civInfo.getDifficulty().unhappinessModifier @@ -395,15 +443,15 @@ class CityStats(val cityInfo: CityInfo) { .toFloat() if (happinessFromSpecialists > 0) newHappinessList["Specialists"] = happinessFromSpecialists - newHappinessList["Buildings"] = statsFromBuildings.happiness.toInt().toFloat() + newHappinessList["Buildings"] = statsFromBuildings.totalStats.happiness.toInt().toFloat() newHappinessList["Tile yields"] = statsFromTiles.happiness val happinessBySource = getStatsFromUniquesBySource() - for ((source, stats) in happinessBySource) - if (stats.happiness != 0f) { + for ((source, stats) in happinessBySource.children) + if (stats.totalStats.happiness != 0f) { if (!newHappinessList.containsKey(source)) newHappinessList[source] = 0f - newHappinessList[source] = newHappinessList[source]!! + stats.happiness + newHappinessList[source] = newHappinessList[source]!! + stats.totalStats.happiness } // we don't want to modify the existing happiness list because that leads @@ -411,26 +459,28 @@ class CityStats(val cityInfo: CityInfo) { happinessList = newHappinessList } - private fun updateBaseStatList(statsFromBuildings: Stats) { + private fun updateBaseStatList(statsFromBuildings: StatTreeNode) { + val newBaseStatTree = StatTreeNode() + val newBaseStatList = StatMap() // we don't edit the existing baseStatList directly, in order to avoid concurrency exceptions - newBaseStatList["Population"] = Stats( + newBaseStatTree.addStats(Stats( science = cityInfo.population.population.toFloat(), production = cityInfo.population.getFreePopulation().toFloat() - ) + ), "Population") newBaseStatList["Tile yields"] = statsFromTiles newBaseStatList["Specialists"] = getStatsFromSpecialists(cityInfo.population.getNewSpecialists()) newBaseStatList["Trade routes"] = getStatsFromTradeRoute() - newBaseStatList["Buildings"] = statsFromBuildings + newBaseStatTree.children["Buildings"] = statsFromBuildings newBaseStatList["City-States"] = getStatsFromCityStates() - val statMap = getStatsFromUniquesBySource() - for ((source, stats) in statMap) - newBaseStatList.add(source, stats) + for ((source, stats) in newBaseStatList) + newBaseStatTree.addStats(stats, source) - baseStatList = newBaseStatList + newBaseStatTree.add(getStatsFromUniquesBySource()) + baseStatTree = newBaseStatTree } @@ -490,8 +540,8 @@ class CityStats(val cityInfo: CityInfo) { private fun updateFinalStatList(currentConstruction: IConstruction, citySpecificUniques: Sequence) { val newFinalStatList = StatMap() // again, we don't edit the existing currentCityStats directly, in order to avoid concurrency exceptions - for (entry in baseStatList) - newFinalStatList[entry.key] = entry.value.clone() + for ((key, value) in baseStatTree.children) + newFinalStatList[key] = value.totalStats.clone() val statPercentBonusesSum = Stats() for (bonus in statPercentBonusList.values) statPercentBonusesSum.add(bonus) @@ -499,9 +549,15 @@ class CityStats(val cityInfo: CityInfo) { for (entry in newFinalStatList.values) entry.production *= statPercentBonusesSum.production.toPercent() + // We only add the 'extra stats from production' AFTER we calculate the production INCLUDING BONUSES val statsFromProduction = getStatsFromProduction(newFinalStatList.values.map { it.production }.sum()) - baseStatList = LinkedHashMap(baseStatList).apply { put("Construction", statsFromProduction) } // concurrency-safe addition - newFinalStatList["Construction"] = statsFromProduction + if (!statsFromProduction.isEmpty()) { + baseStatTree = StatTreeNode().apply { + children.putAll(baseStatTree.children) + addStats(statsFromProduction, "Production") + } // concurrency-safe addition + newFinalStatList["Construction"] = statsFromProduction + } for (entry in newFinalStatList.values) { entry.gold *= statPercentBonusesSum.gold.toPercent() diff --git a/core/src/com/unciv/logic/civilization/TechManager.kt b/core/src/com/unciv/logic/civilization/TechManager.kt index afcb427a9a..631d96defc 100644 --- a/core/src/com/unciv/logic/civilization/TechManager.kt +++ b/core/src/com/unciv/logic/civilization/TechManager.kt @@ -167,7 +167,7 @@ class TechManager { // The Science the Great Scientist generates does not include Science from Policies, Trade routes and City-States. var allCitiesScience = 0f civInfo.cities.forEach { it -> - val totalBaseScience = it.cityStats.baseStatList.values.map { it.science }.sum() + val totalBaseScience = it.cityStats.baseStatTree.totalStats.science val totalBonusPercents = it.cityStats.statPercentBonusList.filter { it.key != "Policies" }.values.map { it.science }.sum() allCitiesScience += totalBaseScience * totalBonusPercents.toPercent() } diff --git a/core/src/com/unciv/ui/cityscreen/CityInfoTable.kt b/core/src/com/unciv/ui/cityscreen/CityInfoTable.kt index 6c7e7e773f..779ed3e4fc 100644 --- a/core/src/com/unciv/ui/cityscreen/CityInfoTable.kt +++ b/core/src/com/unciv/ui/cityscreen/CityInfoTable.kt @@ -1,11 +1,14 @@ package com.unciv.ui.cityscreen import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.scenes.scene2d.Touchable import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.utils.Align import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.city.CityInfo +import com.unciv.logic.city.CityStats +import com.unciv.logic.city.StatTreeNode import com.unciv.models.UncivSound import com.unciv.models.ruleset.Building import com.unciv.models.stats.Stat @@ -143,69 +146,108 @@ class CityInfoTable(private val cityScreen: CityScreen) : Table(BaseScreen.skin) } } + private fun addStatsToHashmap(statTreeNode: StatTreeNode, hashMap: HashMap, stat:Stat, + showDetails:Boolean, indentation:Int=0) { + for ((name, child) in statTreeNode.children) { + hashMap["- ".repeat(indentation) + name] = child.totalStats[stat] + if (showDetails) addStatsToHashmap(child, hashMap, stat, showDetails, indentation + 1) + } + } + private fun Table.addStatInfo() { val cityStats = cityScreen.city.cityStats - for (stat in Stat.values()) { - val relevantBaseStats = LinkedHashMap() - - if (stat != Stat.Happiness) - for ((key, value) in cityStats.baseStatList) - relevantBaseStats[key] = value[stat] - else relevantBaseStats.putAll(cityStats.happinessList) - for (key in relevantBaseStats.keys.toList()) - if (relevantBaseStats[key] == 0f) relevantBaseStats.remove(key) - - if (relevantBaseStats.isEmpty()) continue - - val statValuesTable = Table().apply { defaults().pad(2f) } + val statValuesTable = Table() + statValuesTable.touchable = Touchable.enabled addCategory(stat.name, statValuesTable) - statValuesTable.add("Base values".toLabel(fontSize = FONT_SIZE_STAT_INFO_HEADER)).pad(4f).colspan(2).row() - var sumOfAllBaseValues = 0f - for (entry in relevantBaseStats) { - val specificStatValue = entry.value + updateStatValuesTable(stat, cityStats, statValuesTable) + } + } + + private fun updateStatValuesTable( + stat: Stat, + cityStats: CityStats, + statValuesTable: Table, + showDetails:Boolean = false + ) { + statValuesTable.clear() + statValuesTable.defaults().pad(2f) + statValuesTable.onClick { + updateStatValuesTable( + stat, + cityStats, + statValuesTable, + !showDetails + ) + } + + val relevantBaseStats = LinkedHashMap() + + if (stat != Stat.Happiness) + addStatsToHashmap(cityStats.baseStatTree, relevantBaseStats, stat, showDetails) + else relevantBaseStats.putAll(cityStats.happinessList) + for (key in relevantBaseStats.keys.toList()) + if (relevantBaseStats[key] == 0f) relevantBaseStats.remove(key) + + if (relevantBaseStats.isEmpty()) return + + statValuesTable.add("Base values".toLabel(fontSize = FONT_SIZE_STAT_INFO_HEADER)).pad(4f) + .colspan(2).row() + var sumOfAllBaseValues = 0f + for (entry in relevantBaseStats) { + val specificStatValue = entry.value + if (!entry.key.startsWith('-')) sumOfAllBaseValues += specificStatValue + statValuesTable.add(entry.key.toLabel()).left() + statValuesTable.add(specificStatValue.toOneDecimalLabel()).row() + } + statValuesTable.addSeparator() + statValuesTable.add("Total".toLabel()) + statValuesTable.add(sumOfAllBaseValues.toOneDecimalLabel()).row() + + val relevantBonuses = cityStats.statPercentBonusList.filter { it.value[stat] != 0f } + if (relevantBonuses.isNotEmpty()) { + statValuesTable.add("Bonuses".toLabel(fontSize = FONT_SIZE_STAT_INFO_HEADER)).colspan(2) + .padTop(20f).row() + var sumOfBonuses = 0f + for (entry in relevantBonuses) { + val specificStatValue = entry.value[stat] + sumOfBonuses += specificStatValue + statValuesTable.add(entry.key.toLabel()) + statValuesTable.add(specificStatValue.toPercentLabel()).row() // negative bonus + } + statValuesTable.addSeparator() + statValuesTable.add("Total".toLabel()) + statValuesTable.add(sumOfBonuses.toPercentLabel()).row() // negative bonus + } + + if (stat != Stat.Happiness) { + statValuesTable.add("Final".toLabel(fontSize = FONT_SIZE_STAT_INFO_HEADER)).colspan(2) + .padTop(20f).row() + var finalTotal = 0f + for (entry in cityStats.finalStatList) { + val specificStatValue = entry.value[stat] + finalTotal += specificStatValue + if (specificStatValue == 0f) continue statValuesTable.add(entry.key.toLabel()) statValuesTable.add(specificStatValue.toOneDecimalLabel()).row() } statValuesTable.addSeparator() statValuesTable.add("Total".toLabel()) - statValuesTable.add(sumOfAllBaseValues.toOneDecimalLabel()).row() - - val relevantBonuses = cityStats.statPercentBonusList.filter { it.value[stat] != 0f } - if (relevantBonuses.isNotEmpty()) { - statValuesTable.add("Bonuses".toLabel(fontSize = FONT_SIZE_STAT_INFO_HEADER)).colspan(2).padTop(20f).row() - var sumOfBonuses = 0f - for (entry in relevantBonuses) { - val specificStatValue = entry.value[stat] - sumOfBonuses += specificStatValue - statValuesTable.add(entry.key.toLabel()) - statValuesTable.add(specificStatValue.toPercentLabel()).row() // negative bonus - } - statValuesTable.addSeparator() - statValuesTable.add("Total".toLabel()) - statValuesTable.add(sumOfBonuses.toPercentLabel()).row() // negative bonus - } - - if (stat != Stat.Happiness) { - statValuesTable.add("Final".toLabel(fontSize = FONT_SIZE_STAT_INFO_HEADER)).colspan(2).padTop(20f).row() - var finalTotal = 0f - for (entry in cityStats.finalStatList) { - val specificStatValue = entry.value[stat] - finalTotal += specificStatValue - if (specificStatValue == 0f) continue - statValuesTable.add(entry.key.toLabel()) - statValuesTable.add(specificStatValue.toOneDecimalLabel()).row() - } - statValuesTable.addSeparator() - statValuesTable.add("Total".toLabel()) - statValuesTable.add(finalTotal.toOneDecimalLabel()).row() - } - - statValuesTable.padBottom(4f) + statValuesTable.add(finalTotal.toOneDecimalLabel()).row() } + + statValuesTable.pack() + val toggleButtonChar = if (showDetails) "-" else "+" + val toggleButton = toggleButtonChar.toLabel().apply { setAlignment(Align.center) } + .surroundWithCircle(25f, color = ImageGetter.getBlue()) + .surroundWithCircle(27f, false) + statValuesTable.addActor(toggleButton) + toggleButton.setPosition(0f, statValuesTable.height, Align.topLeft) + + statValuesTable.padBottom(4f) } private fun Table.addGreatPersonPointInfo(cityInfo: CityInfo) {