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
*/
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
}

View File

@ -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<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.
*
* 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<String, Stats>()
var statPercentBonusList = LinkedHashMap<String, Stats>()
@ -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<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 {
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<String, Float>()
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<Unique>) {
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()

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.
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()
}

View File

@ -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<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() {
val cityStats = cityScreen.city.cityStats
for (stat in Stat.values()) {
val relevantBaseStats = LinkedHashMap<String, Float>()
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<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
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) {