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
This commit is contained in:
Yair Morgenstern 2022-01-24 11:04:12 +02:00 committed by GitHub
parent 39ed8bd269
commit a8dbd4784c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 176 additions and 78 deletions

View File

@ -84,10 +84,10 @@ class CityConstructions {
/** /**
* @return [Stats] provided by all built buildings in city plus the bonus from Library * @return [Stats] provided by all built buildings in city plus the bonus from Library
*/ */
fun getStats(): Stats { fun getStats(): StatTreeNode {
val stats = Stats() val stats = StatTreeNode()
for (building in getBuiltBuildings()) for (building in getBuiltBuildings())
stats.add(building.getStats(cityInfo)) stats.addStats(building.getStats(cityInfo), building.name)
return stats return stats
} }

View File

@ -7,9 +7,7 @@ import com.unciv.logic.map.RoadStatus
import com.unciv.models.Counter import com.unciv.models.Counter
import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.ModOptionsConstants 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.Unique
import com.unciv.models.ruleset.unique.UniqueMapTyped
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
@ -19,6 +17,42 @@ import com.unciv.ui.utils.toPercent
import kotlin.math.min import kotlin.math.min
class StatTreeNode {
val children = LinkedHashMap<String, StatTreeNode>()
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. /** Holds and calculates [Stats] for a city.
* *
* No field needs to be saved, all are calculated on the fly, * 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) { class CityStats(val cityInfo: CityInfo) {
//region Fields, Transient //region Fields, Transient
var baseStatTree = StatTreeNode()
var baseStatList = LinkedHashMap<String, Stats>() var baseStatList = LinkedHashMap<String, Stats>()
var statPercentBonusList = LinkedHashMap<String, Stats>() var statPercentBonusList = LinkedHashMap<String, Stats>()
@ -168,10 +204,10 @@ class CityStats(val cityInfo: CityInfo) {
} }
private fun getStatsFromUniquesBySource():StatMap { private fun getStatsFromUniquesBySource(): StatTreeNode {
val sourceToStats = StatMap() val sourceToStats = StatTreeNode()
fun addUniqueStats(unique:Unique) = 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)) for (unique in cityInfo.getMatchingUniques(UniqueType.Stats))
addUniqueStats(unique) addUniqueStats(unique)
@ -184,7 +220,7 @@ class CityStats(val cityInfo: CityInfo) {
for (unique in cityInfo.getMatchingUniques(UniqueType.StatsPerPopulation)) for (unique in cityInfo.getMatchingUniques(UniqueType.StatsPerPopulation))
if (cityInfo.matchesFilter(unique.params[2])) { if (cityInfo.matchesFilter(unique.params[2])) {
val amountOfEffects = (cityInfo.population.population / unique.params[1].toInt()).toFloat() 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)) for (unique in cityInfo.getMatchingUniques(UniqueType.StatsFromXPopulation))
@ -201,7 +237,7 @@ class CityStats(val cityInfo: CityInfo) {
addUniqueStats(unique) addUniqueStats(unique)
// //
renameStatmapKeys(sourceToStats) renameStatmapKeys(sourceToStats.children)
return sourceToStats return sourceToStats
} }
@ -218,6 +254,18 @@ class CityStats(val cityInfo: CityInfo) {
} }
private fun<T> renameStatmapKeys(statMap: LinkedHashMap<String, T>){
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 { private fun getStatPercentBonusesFromGoldenAge(isGoldenAge: Boolean): Stats {
val stats = Stats() val stats = Stats()
if (isGoldenAge) { 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 // 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! // 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 civInfo = cityInfo.civInfo
val newHappinessList = LinkedHashMap<String, Float>() val newHappinessList = LinkedHashMap<String, Float>()
var unhappinessModifier = civInfo.getDifficulty().unhappinessModifier var unhappinessModifier = civInfo.getDifficulty().unhappinessModifier
@ -395,15 +443,15 @@ class CityStats(val cityInfo: CityInfo) {
.toFloat() .toFloat()
if (happinessFromSpecialists > 0) newHappinessList["Specialists"] = happinessFromSpecialists if (happinessFromSpecialists > 0) newHappinessList["Specialists"] = happinessFromSpecialists
newHappinessList["Buildings"] = statsFromBuildings.happiness.toInt().toFloat() newHappinessList["Buildings"] = statsFromBuildings.totalStats.happiness.toInt().toFloat()
newHappinessList["Tile yields"] = statsFromTiles.happiness newHappinessList["Tile yields"] = statsFromTiles.happiness
val happinessBySource = getStatsFromUniquesBySource() val happinessBySource = getStatsFromUniquesBySource()
for ((source, stats) in happinessBySource) for ((source, stats) in happinessBySource.children)
if (stats.happiness != 0f) { if (stats.totalStats.happiness != 0f) {
if (!newHappinessList.containsKey(source)) newHappinessList[source] = 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 // we don't want to modify the existing happiness list because that leads
@ -411,26 +459,28 @@ class CityStats(val cityInfo: CityInfo) {
happinessList = newHappinessList happinessList = newHappinessList
} }
private fun updateBaseStatList(statsFromBuildings: Stats) { private fun updateBaseStatList(statsFromBuildings: StatTreeNode) {
val newBaseStatTree = StatTreeNode()
val newBaseStatList = val newBaseStatList =
StatMap() // we don't edit the existing baseStatList directly, in order to avoid concurrency exceptions 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(), science = cityInfo.population.population.toFloat(),
production = cityInfo.population.getFreePopulation().toFloat() production = cityInfo.population.getFreePopulation().toFloat()
) ), "Population")
newBaseStatList["Tile yields"] = statsFromTiles newBaseStatList["Tile yields"] = statsFromTiles
newBaseStatList["Specialists"] = newBaseStatList["Specialists"] =
getStatsFromSpecialists(cityInfo.population.getNewSpecialists()) getStatsFromSpecialists(cityInfo.population.getNewSpecialists())
newBaseStatList["Trade routes"] = getStatsFromTradeRoute() newBaseStatList["Trade routes"] = getStatsFromTradeRoute()
newBaseStatList["Buildings"] = statsFromBuildings newBaseStatTree.children["Buildings"] = statsFromBuildings
newBaseStatList["City-States"] = getStatsFromCityStates() newBaseStatList["City-States"] = getStatsFromCityStates()
val statMap = getStatsFromUniquesBySource() for ((source, stats) in newBaseStatList)
for ((source, stats) in statMap) newBaseStatTree.addStats(stats, source)
newBaseStatList.add(source, stats)
baseStatList = newBaseStatList newBaseStatTree.add(getStatsFromUniquesBySource())
baseStatTree = newBaseStatTree
} }
@ -490,8 +540,8 @@ class CityStats(val cityInfo: CityInfo) {
private fun updateFinalStatList(currentConstruction: IConstruction, citySpecificUniques: Sequence<Unique>) { private fun updateFinalStatList(currentConstruction: IConstruction, citySpecificUniques: Sequence<Unique>) {
val newFinalStatList = StatMap() // again, we don't edit the existing currentCityStats directly, in order to avoid concurrency exceptions val newFinalStatList = StatMap() // again, we don't edit the existing currentCityStats directly, in order to avoid concurrency exceptions
for (entry in baseStatList) for ((key, value) in baseStatTree.children)
newFinalStatList[entry.key] = entry.value.clone() newFinalStatList[key] = value.totalStats.clone()
val statPercentBonusesSum = Stats() val statPercentBonusesSum = Stats()
for (bonus in statPercentBonusList.values) statPercentBonusesSum.add(bonus) for (bonus in statPercentBonusList.values) statPercentBonusesSum.add(bonus)
@ -499,9 +549,15 @@ class CityStats(val cityInfo: CityInfo) {
for (entry in newFinalStatList.values) for (entry in newFinalStatList.values)
entry.production *= statPercentBonusesSum.production.toPercent() 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()) val statsFromProduction = getStatsFromProduction(newFinalStatList.values.map { it.production }.sum())
baseStatList = LinkedHashMap(baseStatList).apply { put("Construction", statsFromProduction) } // concurrency-safe addition if (!statsFromProduction.isEmpty()) {
newFinalStatList["Construction"] = statsFromProduction baseStatTree = StatTreeNode().apply {
children.putAll(baseStatTree.children)
addStats(statsFromProduction, "Production")
} // concurrency-safe addition
newFinalStatList["Construction"] = statsFromProduction
}
for (entry in newFinalStatList.values) { for (entry in newFinalStatList.values) {
entry.gold *= statPercentBonusesSum.gold.toPercent() entry.gold *= statPercentBonusesSum.gold.toPercent()

View File

@ -167,7 +167,7 @@ class TechManager {
// The Science the Great Scientist generates does not include Science from Policies, Trade routes and City-States. // The Science the Great Scientist generates does not include Science from Policies, Trade routes and City-States.
var allCitiesScience = 0f var allCitiesScience = 0f
civInfo.cities.forEach { it -> 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() val totalBonusPercents = it.cityStats.statPercentBonusList.filter { it.key != "Policies" }.values.map { it.science }.sum()
allCitiesScience += totalBaseScience * totalBonusPercents.toPercent() allCitiesScience += totalBaseScience * totalBonusPercents.toPercent()
} }

View File

@ -1,11 +1,14 @@
package com.unciv.ui.cityscreen package com.unciv.ui.cityscreen
import com.badlogic.gdx.graphics.Color 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.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.city.CityInfo 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.UncivSound
import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.Building
import com.unciv.models.stats.Stat 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<String, Float>, 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() { private fun Table.addStatInfo() {
val cityStats = cityScreen.city.cityStats val cityStats = cityScreen.city.cityStats
for (stat in Stat.values()) { for (stat in Stat.values()) {
val relevantBaseStats = LinkedHashMap<String, Float>() val statValuesTable = Table()
statValuesTable.touchable = Touchable.enabled
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) }
addCategory(stat.name, statValuesTable) addCategory(stat.name, statValuesTable)
statValuesTable.add("Base values".toLabel(fontSize = FONT_SIZE_STAT_INFO_HEADER)).pad(4f).colspan(2).row() updateStatValuesTable(stat, cityStats, statValuesTable)
var sumOfAllBaseValues = 0f }
for (entry in relevantBaseStats) { }
val specificStatValue = entry.value
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<String, Float>()
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 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(entry.key.toLabel())
statValuesTable.add(specificStatValue.toOneDecimalLabel()).row() statValuesTable.add(specificStatValue.toOneDecimalLabel()).row()
} }
statValuesTable.addSeparator() statValuesTable.addSeparator()
statValuesTable.add("Total".toLabel()) statValuesTable.add("Total".toLabel())
statValuesTable.add(sumOfAllBaseValues.toOneDecimalLabel()).row() statValuesTable.add(finalTotal.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.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) { private fun Table.addGreatPersonPointInfo(cityInfo: CityInfo) {