Stat bonus drilldown (#6053)

* Step 1 - converted stat bonus list to tree.
No visual difference yet, since the stat bonus list is still generated in the same way.

* Step 2 - updateStatPercentBonusList converted to tree form

* Step 3 - buildings converted to tree form - now user visible!

* Step 4 - Bonuses from uniques are now drilldownable

* Removed unneeded todo

* Welp, turns out I forgot to apply conditionals
This commit is contained in:
Yair Morgenstern 2022-01-26 22:42:05 +02:00 committed by GitHub
parent 824efcb1a9
commit 84ef8944d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 49 additions and 58 deletions

View File

@ -253,9 +253,7 @@ object SpecificUnitAutomation {
val relatedStat = improvement.maxByOrNull { it.value }?.key ?: Stat.Culture val relatedStat = improvement.maxByOrNull { it.value }?.key ?: Stat.Culture
val citiesByStatBoost = unit.civInfo.cities.sortedByDescending { val citiesByStatBoost = unit.civInfo.cities.sortedByDescending {
val stats = Stats() it.cityStats.statPercentBonusTree.totalStats[relatedStat]
for (bonus in it.cityStats.statPercentBonusList.values) stats.add(bonus)
stats[relatedStat]
} }

View File

@ -107,16 +107,6 @@ class CityConstructions {
return maintenanceCost return maintenanceCost
} }
/**
* @return Bonus (%) [Stats] provided by all built buildings in city
*/
fun getStatPercentBonuses(): Stats {
val stats = Stats()
for (building in getBuiltBuildings())
stats.add(building.getStatPercentageBonuses(cityInfo))
return stats
}
fun getCityProductionTextForCityButton(): String { fun getCityProductionTextForCityButton(): String {
val currentConstructionSnapshot = currentConstructionFromQueue // See below val currentConstructionSnapshot = currentConstructionFromQueue // See below
var result = currentConstructionSnapshot.tr() var result = currentConstructionSnapshot.tr()

View File

@ -45,12 +45,13 @@ class StatTreeNode {
} }
} }
val totalStats: Stats by lazy { val totalStats: Stats
val toReturn = Stats() get() {
if (innerStats != null) toReturn.add(innerStats!!) val toReturn = Stats()
for (child in children.values) toReturn.add(child.totalStats) if (innerStats != null) toReturn.add(innerStats!!)
toReturn for (child in children.values) toReturn.add(child.totalStats)
} return toReturn
}
} }
/** Holds and calculates [Stats] for a city. /** Holds and calculates [Stats] for a city.
@ -63,10 +64,10 @@ class CityStats(val cityInfo: CityInfo) {
var baseStatTree = StatTreeNode() var baseStatTree = StatTreeNode()
var baseStatList = LinkedHashMap<String, Stats>()
var statPercentBonusList = LinkedHashMap<String, Stats>() var statPercentBonusList = LinkedHashMap<String, Stats>()
var statPercentBonusTree = StatTreeNode()
// Computed from baseStatList and statPercentBonusList - this is so the players can see a breakdown // Computed from baseStatList and statPercentBonusList - this is so the players can see a breakdown
var finalStatList = LinkedHashMap<String, Stats>() var finalStatList = LinkedHashMap<String, Stats>()
@ -124,6 +125,12 @@ class CityStats(val cityInfo: CityInfo) {
return stats return stats
} }
private fun addStatPercentBonusesFromBuildings(statPercentBonusTree: StatTreeNode) {
for (building in cityInfo.cityConstructions.getBuiltBuildings())
statPercentBonusTree.addStats(building.getStatPercentageBonuses(cityInfo), "Buildings", building.name)
}
private fun getStatsFromCityStates(): Stats { private fun getStatsFromCityStates(): Stats {
val stats = Stats() val stats = Stats()
@ -265,10 +272,11 @@ class CityStats(val cityInfo: CityInfo) {
return stats return stats
} }
private fun getStatsPercentBonusesFromUniquesBySource(currentConstruction: IConstruction):StatMap { private fun getStatsPercentBonusesFromUniquesBySource(currentConstruction: IConstruction): StatTreeNode {
val sourceToStats = StatMap() val sourceToStats = StatTreeNode()
fun addUniqueStats(unique: Unique, stat:Stat, amount:Float) {
sourceToStats.add(getSourceNameForUnique(unique), Stats().add(stat, amount)) fun addUniqueStats(unique:Unique, stat:Stat, amount:Float) {
sourceToStats.addStats(Stats().add(stat, amount), getSourceNameForUnique(unique), unique.sourceObjectName ?: "")
} }
for (unique in cityInfo.getMatchingUniques(UniqueType.StatPercentBonus)) { for (unique in cityInfo.getMatchingUniques(UniqueType.StatPercentBonus)) {
@ -472,25 +480,24 @@ class CityStats(val cityInfo: CityInfo) {
} }
private fun updateStatPercentBonusList(currentConstruction: IConstruction, localBuildingUniques: Sequence<Unique>) { private fun updateStatPercentBonusList(currentConstruction: IConstruction) {
val newStatPercentBonusList = StatMap() val newStatsBonusTree = StatTreeNode()
newStatPercentBonusList["Golden Age"] = getStatPercentBonusesFromGoldenAge(cityInfo.civInfo.goldenAges.isGoldenAge()) newStatsBonusTree.addStats(getStatPercentBonusesFromGoldenAge(cityInfo.civInfo.goldenAges.isGoldenAge()),"Golden Age")
.plus(cityInfo.cityConstructions.getStatPercentBonuses()) // This function is to be deprecated but it'll take a while. addStatPercentBonusesFromBuildings(newStatsBonusTree)
newStatPercentBonusList["Railroads"] = getStatPercentBonusesFromRailroad() // Name chosen same as tech, for translation, but theoretically independent newStatsBonusTree.addStats(getStatPercentBonusesFromRailroad(), "Railroad")
newStatPercentBonusList["Puppet City"] = getStatPercentBonusesFromPuppetCity() newStatsBonusTree.addStats(getStatPercentBonusesFromPuppetCity(), "Puppet City")
newStatPercentBonusList["Unit Supply"] = getStatPercentBonusesFromUnitSupply() newStatsBonusTree.addStats(getStatPercentBonusesFromUnitSupply(), "Unit Supply")
for ((source, stats) in getStatsPercentBonusesFromUniquesBySource(currentConstruction)) newStatsBonusTree.add(getStatsPercentBonusesFromUniquesBySource(currentConstruction))
newStatPercentBonusList.add(source, stats)
if (UncivGame.Current.superchargedForDebug) { if (UncivGame.Current.superchargedForDebug) {
val stats = Stats() val stats = Stats()
for (stat in Stat.values()) stats[stat] = 10000f for (stat in Stat.values()) stats[stat] = 10000f
newStatPercentBonusList["Supercharged"] = stats newStatsBonusTree.addStats(stats, "Supercharged")
} }
statPercentBonusList = newStatPercentBonusList statPercentBonusTree = newStatsBonusTree
} }
/** Does not update tile stats - instead, updating tile stats updates this */ /** Does not update tile stats - instead, updating tile stats updates this */
@ -498,17 +505,12 @@ class CityStats(val cityInfo: CityInfo) {
updateTileStats:Boolean = true) { updateTileStats:Boolean = true) {
if (updateTileStats) updateTileStats() if (updateTileStats) updateTileStats()
// We calculate this here for concurrency reasons
// If something needs this, we pass this through as a parameter
val localBuildingUniques = cityInfo.cityConstructions.builtBuildingUniqueMap.getAllUniques()
// We need to compute Tile yields before happiness // We need to compute Tile yields before happiness
val statsFromBuildings = cityInfo.cityConstructions.getStats() // this is performance heavy, so calculate once val statsFromBuildings = cityInfo.cityConstructions.getStats() // this is performance heavy, so calculate once
updateBaseStatList(statsFromBuildings) updateBaseStatList(statsFromBuildings)
updateCityHappiness(statsFromBuildings) updateCityHappiness(statsFromBuildings)
updateStatPercentBonusList(currentConstruction, localBuildingUniques) updateStatPercentBonusList(currentConstruction)
updateFinalStatList(currentConstruction) // again, we don't edit the existing currentCityStats directly, in order to avoid concurrency exceptions updateFinalStatList(currentConstruction) // again, we don't edit the existing currentCityStats directly, in order to avoid concurrency exceptions
@ -525,8 +527,7 @@ class CityStats(val cityInfo: CityInfo) {
for ((key, value) in baseStatTree.children) for ((key, value) in baseStatTree.children)
newFinalStatList[key] = value.totalStats.clone() newFinalStatList[key] = value.totalStats.clone()
val statPercentBonusesSum = Stats() val statPercentBonusesSum = statPercentBonusTree.totalStats
for (bonus in statPercentBonusList.values) statPercentBonusesSum.add(bonus)
for (entry in newFinalStatList.values) for (entry in newFinalStatList.values)
entry.production *= statPercentBonusesSum.production.toPercent() entry.production *= statPercentBonusesSum.production.toPercent()

View File

@ -168,7 +168,8 @@ class TechManager {
var allCitiesScience = 0f var allCitiesScience = 0f
civInfo.cities.forEach { it -> civInfo.cities.forEach { it ->
val totalBaseScience = it.cityStats.baseStatTree.totalStats.science 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.statPercentBonusTree.children.asSequence()
.filter { it.key != "Policies" }.map { it.value.totalStats.science }.sum()
allCitiesScience += totalBaseScience * totalBonusPercents.toPercent() allCitiesScience += totalBaseScience * totalBonusPercents.toPercent()
} }
scienceOfLast8Turns[civInfo.gameInfo.turns % 8] = allCitiesScience.toInt() scienceOfLast8Turns[civInfo.gameInfo.turns % 8] = allCitiesScience.toInt()

View File

@ -19,7 +19,7 @@ interface IHasUniques {
* But making this a function is relevant for future "unify Unciv object" plans ;) * But making this a function is relevant for future "unify Unciv object" plans ;)
* */ * */
fun getUniqueTarget(): UniqueTarget fun getUniqueTarget(): UniqueTarget
fun getMatchingUniques(uniqueTemplate: String, stateForConditionals: StateForConditionals? = null): Sequence<Unique> { fun getMatchingUniques(uniqueTemplate: String, stateForConditionals: StateForConditionals? = null): Sequence<Unique> {
val matchingUniques = uniqueMap[uniqueTemplate] ?: return sequenceOf() val matchingUniques = uniqueMap[uniqueTemplate] ?: return sequenceOf()
return matchingUniques.asSequence().filter { it.conditionalsApply(stateForConditionals) } return matchingUniques.asSequence().filter { it.conditionalsApply(stateForConditionals) }

View File

@ -149,7 +149,9 @@ class CityInfoTable(private val cityScreen: CityScreen) : Table(BaseScreen.skin)
private fun addStatsToHashmap(statTreeNode: StatTreeNode, hashMap: HashMap<String, Float>, stat:Stat, private fun addStatsToHashmap(statTreeNode: StatTreeNode, hashMap: HashMap<String, Float>, stat:Stat,
showDetails:Boolean, indentation:Int=0) { showDetails:Boolean, indentation:Int=0) {
for ((name, child) in statTreeNode.children) { for ((name, child) in statTreeNode.children) {
hashMap["- ".repeat(indentation) + name] = child.totalStats[stat] val statAmount = child.totalStats[stat]
if (statAmount == 0f) continue
hashMap["- ".repeat(indentation) + name] = statAmount
if (showDetails) addStatsToHashmap(child, hashMap, stat, showDetails, indentation + 1) if (showDetails) addStatsToHashmap(child, hashMap, stat, showDetails, indentation + 1)
} }
} }
@ -207,23 +209,22 @@ class CityInfoTable(private val cityScreen: CityScreen) : Table(BaseScreen.skin)
statValuesTable.add("Total".toLabel()) statValuesTable.add("Total".toLabel())
statValuesTable.add(sumOfAllBaseValues.toOneDecimalLabel()).row() statValuesTable.add(sumOfAllBaseValues.toOneDecimalLabel()).row()
val relevantBonuses = cityStats.statPercentBonusList.filter { it.value[stat] != 0f } val relevantBonuses = LinkedHashMap<String, Float>()
if (relevantBonuses.isNotEmpty()) { addStatsToHashmap(cityStats.statPercentBonusTree, relevantBonuses, stat, showDetails)
val totalBonusStats = cityStats.statPercentBonusTree.totalStats
if (totalBonusStats[stat] != 0f) {
statValuesTable.add("Bonuses".toLabel(fontSize = FONT_SIZE_STAT_INFO_HEADER)).colspan(2) statValuesTable.add("Bonuses".toLabel(fontSize = FONT_SIZE_STAT_INFO_HEADER)).colspan(2)
.padTop(20f).row() .padTop(20f).row()
var sumOfBonuses = 0f for ((source, bonusAmount) in relevantBonuses) {
for (entry in relevantBonuses) { statValuesTable.add(source.toLabel()).left()
val specificStatValue = entry.value[stat] statValuesTable.add(bonusAmount.toPercentLabel()).row() // negative bonus
sumOfBonuses += specificStatValue
statValuesTable.add(entry.key.toLabel())
statValuesTable.add(specificStatValue.toPercentLabel()).row() // negative bonus
} }
statValuesTable.addSeparator() statValuesTable.addSeparator()
statValuesTable.add("Total".toLabel()) statValuesTable.add("Total".toLabel())
statValuesTable.add(sumOfBonuses.toPercentLabel()).row() // negative bonus statValuesTable.add(totalBonusStats[stat].toPercentLabel()).row() // negative bonus
}
if (stat != Stat.Happiness) {
statValuesTable.add("Final".toLabel(fontSize = FONT_SIZE_STAT_INFO_HEADER)).colspan(2) statValuesTable.add("Final".toLabel(fontSize = FONT_SIZE_STAT_INFO_HEADER)).colspan(2)
.padTop(20f).row() .padTop(20f).row()
var finalTotal = 0f var finalTotal = 0f