mirror of
https://github.com/yairm210/Unciv.git
synced 2025-02-11 11:28:03 +07:00
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:
parent
39ed8bd269
commit
a8dbd4784c
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user