mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-26 13:27:22 -04:00
Don't prematurely round Great person point boni and GPP breakdown UI (#10924)
* GreatPersonPointsBreakdown class and logic use * Minor comment * Fix untrue wiki claim that civilopediaText color accepts names * GreatPersonPointsBreakdown UI available by clicking in the CityStatsTable GPP section * Two separate assembly lines for two different products of the factory * One more code-deduplication * Fix rounding - rounds *up* on x.5 * Clearer display code (but output unchanged) * Minor comment clarification
This commit is contained in:
parent
7dc398184f
commit
5b698102ef
@ -12,12 +12,10 @@ import com.unciv.logic.city.managers.CityPopulationManager
|
||||
import com.unciv.logic.city.managers.CityReligionManager
|
||||
import com.unciv.logic.city.managers.SpyFleeReason
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
|
||||
import com.unciv.logic.map.TileMap
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.logic.map.tile.RoadStatus
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import com.unciv.models.Counter
|
||||
import com.unciv.models.ruleset.Building
|
||||
import com.unciv.models.ruleset.unique.StateForConditionals
|
||||
import com.unciv.models.ruleset.unique.Unique
|
||||
@ -216,69 +214,8 @@ class City : IsPartOfGameInfoSerialization {
|
||||
fun containsBuildingUnique(uniqueType: UniqueType) =
|
||||
cityConstructions.builtBuildingUniqueMap.getUniques(uniqueType).any()
|
||||
|
||||
fun getGreatPersonPercentageBonus(): Int{
|
||||
var allGppPercentageBonus = 0
|
||||
for (unique in getMatchingUniques(UniqueType.GreatPersonPointPercentage)) {
|
||||
if (!matchesFilter(unique.params[1])) continue
|
||||
allGppPercentageBonus += unique.params[0].toInt()
|
||||
}
|
||||
|
||||
// Sweden UP
|
||||
for (otherCiv in civ.getKnownCivs()) {
|
||||
if (!civ.getDiplomacyManager(otherCiv).hasFlag(DiplomacyFlags.DeclarationOfFriendship))
|
||||
continue
|
||||
|
||||
for (ourUnique in civ.getMatchingUniques(UniqueType.GreatPersonBoostWithFriendship))
|
||||
allGppPercentageBonus += ourUnique.params[0].toInt()
|
||||
for (theirUnique in otherCiv.getMatchingUniques(UniqueType.GreatPersonBoostWithFriendship))
|
||||
allGppPercentageBonus += theirUnique.params[0].toInt()
|
||||
}
|
||||
return allGppPercentageBonus
|
||||
}
|
||||
|
||||
fun getGreatPersonPointsForNextTurn(): HashMap<String, Counter<String>> {
|
||||
val sourceToGPP = HashMap<String, Counter<String>>()
|
||||
|
||||
val specialistsCounter = Counter<String>()
|
||||
for ((specialistName, amount) in population.getNewSpecialists())
|
||||
if (getRuleset().specialists.containsKey(specialistName)) { // To solve problems in total remake mods
|
||||
val specialist = getRuleset().specialists[specialistName]!!
|
||||
specialistsCounter.add(specialist.greatPersonPoints.times(amount))
|
||||
}
|
||||
sourceToGPP["Specialists"] = specialistsCounter
|
||||
|
||||
val buildingsCounter = Counter<String>()
|
||||
for (building in cityConstructions.getBuiltBuildings())
|
||||
buildingsCounter.add(building.greatPersonPoints)
|
||||
sourceToGPP["Buildings"] = buildingsCounter
|
||||
|
||||
val stateForConditionals = StateForConditionals(civInfo = civ, city = this)
|
||||
for ((_, gppCounter) in sourceToGPP) {
|
||||
for (unique in civ.getMatchingUniques(UniqueType.GreatPersonEarnedFaster, stateForConditionals)) {
|
||||
val unitName = unique.params[0]
|
||||
if (!gppCounter.containsKey(unitName)) continue
|
||||
gppCounter.add(unitName, gppCounter[unitName] * unique.params[1].toInt() / 100)
|
||||
}
|
||||
|
||||
val allGppPercentageBonus = getGreatPersonPercentageBonus()
|
||||
|
||||
for (unitName in gppCounter.keys)
|
||||
gppCounter.add(unitName, gppCounter[unitName] * allGppPercentageBonus / 100)
|
||||
}
|
||||
|
||||
return sourceToGPP
|
||||
}
|
||||
|
||||
fun getGreatPersonPoints(): Counter<String> {
|
||||
val gppCounter = Counter<String>()
|
||||
for (entry in getGreatPersonPointsForNextTurn().values)
|
||||
gppCounter.add(entry)
|
||||
// Remove all "gpp" values that are not valid units
|
||||
for (key in gppCounter.keys.toSet())
|
||||
if (key !in getRuleset().units)
|
||||
gppCounter.remove(key)
|
||||
return gppCounter
|
||||
}
|
||||
fun getGreatPersonPercentageBonus() = GreatPersonPointsBreakdown.getGreatPersonPercentageBonus(this)
|
||||
fun getGreatPersonPoints() = GreatPersonPointsBreakdown(this).sum()
|
||||
|
||||
fun addStat(stat: Stat, amount: Int) {
|
||||
when (stat) {
|
||||
|
158
core/src/com/unciv/logic/city/GreatPersonPointsBreakdown.kt
Normal file
158
core/src/com/unciv/logic/city/GreatPersonPointsBreakdown.kt
Normal file
@ -0,0 +1,158 @@
|
||||
package com.unciv.logic.city
|
||||
|
||||
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
|
||||
import com.unciv.models.Counter
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.ruleset.unique.StateForConditionals
|
||||
import com.unciv.models.ruleset.unique.Unique
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
|
||||
/** Manages calculating Great Person Points per City for nextTurn. See public constructor(city) below for details. */
|
||||
class GreatPersonPointsBreakdown private constructor(private val ruleset: Ruleset) {
|
||||
// ruleset kept as class field for reuse in sum(), for the "Remove all gpp values that are not valid units" step.
|
||||
// I am unsure why that existed before this class was written, what the UX for invalid mods should be.
|
||||
// Refactoring through allNames so that ruleset is only needed in init should be easy if all UI should behave as if any invalid definition did not exist at all.
|
||||
// As is, the "lost" points can still display.
|
||||
|
||||
/** Return type component of the [Companion.getPercentagesApplyingToAllGP] helper */
|
||||
private class AllGPPercentageEntry(
|
||||
val source: String,
|
||||
val pediaLink: String?,
|
||||
val bonus: Int
|
||||
)
|
||||
|
||||
/** Represents any source of Great Person Points or GPP percentage bonuses */
|
||||
class Entry (
|
||||
/** Simple label for the source of these points */
|
||||
val source: String,
|
||||
/** In case we want to show the breakdown with decorations and/or Civilopedia linking */
|
||||
val pediaLink: String? = null,
|
||||
/** For display only - this entry affects all Great Persons and can be displayed as simple percentage without listing all GP keys */
|
||||
val isAllGP: Boolean = false,
|
||||
/** Reference to the points, **do not mutate** */
|
||||
// To lift the mutability restriction, clone building.greatPersonPoints below - all others are already owned here
|
||||
val counter: Counter<String> = Counter()
|
||||
)
|
||||
|
||||
companion object {
|
||||
// Using fixed-point(n.3) math in sum() to avoid surprises by rounding while still leveraging the Counter class
|
||||
const val fixedPointFactor = 1000
|
||||
|
||||
|
||||
private fun getUniqueSourceName(unique: Unique) = unique.sourceObjectName ?: "Bonus"
|
||||
|
||||
private fun guessPediaLink(unique: Unique): String? {
|
||||
if (unique.sourceObjectName == null) return null
|
||||
return unique.sourceObjectType!!.name + "/" + unique.sourceObjectName
|
||||
}
|
||||
|
||||
/** List all percentage bonuses that apply to all GPP
|
||||
*
|
||||
* This is used internally from the public constructor to include them in the brakdown,
|
||||
* and exposed to autoAssignPopulation via [getGreatPersonPercentageBonus]
|
||||
*/
|
||||
private fun getPercentagesApplyingToAllGP(city: City) = sequence {
|
||||
// Now add boni for GreatPersonPointPercentage
|
||||
for (unique in city.getMatchingUniques(UniqueType.GreatPersonPointPercentage)) {
|
||||
if (!city.matchesFilter(unique.params[1])) continue
|
||||
yield(AllGPPercentageEntry(getUniqueSourceName(unique), guessPediaLink(unique), unique.params[0].toInt()))
|
||||
}
|
||||
|
||||
// Now add boni for GreatPersonBoostWithFriendship (Sweden UP)
|
||||
val civ = city.civ
|
||||
for (otherCiv in civ.getKnownCivs()) {
|
||||
if (!civ.getDiplomacyManager(otherCiv).hasFlag(DiplomacyFlags.DeclarationOfFriendship))
|
||||
continue
|
||||
val boostUniques = civ.getMatchingUniques(UniqueType.GreatPersonBoostWithFriendship) +
|
||||
otherCiv.getMatchingUniques(UniqueType.GreatPersonBoostWithFriendship)
|
||||
for (unique in boostUniques)
|
||||
yield(AllGPPercentageEntry("Declaration of Friendship", null, unique.params[0].toInt()))
|
||||
}
|
||||
}
|
||||
|
||||
/** Aggregate all percentage bonuses that apply to all GPP
|
||||
*
|
||||
* For use by [City.getGreatPersonPercentageBonus] (which in turn is only used by autoAssignPopulation)
|
||||
*/
|
||||
fun getGreatPersonPercentageBonus(city: City) = getPercentagesApplyingToAllGP(city).sumOf { it.bonus }
|
||||
}
|
||||
|
||||
/** Collects all GPP names that have base points */
|
||||
val allNames = mutableSetOf<String>()
|
||||
|
||||
val basePoints = ArrayList<Entry>()
|
||||
val percentBonuses = ArrayList<Entry>()
|
||||
|
||||
/** Manages calculating Great Person Points per City for nextTurn.
|
||||
*
|
||||
* Keeps flat points and percentage boni as separate items.
|
||||
*
|
||||
* See [sum] to calculate the aggregate, use [basePoints] and [percentBonuses] to list as breakdown
|
||||
*/
|
||||
constructor(city: City) : this(city.getRuleset()) {
|
||||
// Collect points from Specialists
|
||||
val specialists = Entry("Specialists") // "Tutorial/Great People" as link doesn't quite fit
|
||||
for ((specialistName, amount) in city.population.getNewSpecialists())
|
||||
if (ruleset.specialists.containsKey(specialistName)) { // To solve problems in total remake mods
|
||||
val specialist = ruleset.specialists[specialistName]!!
|
||||
specialists.counter.add(specialist.greatPersonPoints.times(amount))
|
||||
}
|
||||
basePoints.add(specialists)
|
||||
allNames += specialists.counter.keys
|
||||
|
||||
// Collect points from buildings - duplicates listed individually (should not happen in vanilla Unciv)
|
||||
for (building in city.cityConstructions.getBuiltBuildings()) {
|
||||
if (building.greatPersonPoints.isEmpty()) continue
|
||||
basePoints.add(Entry(building.name, building.makeLink(), counter = building.greatPersonPoints))
|
||||
allNames += building.greatPersonPoints.keys
|
||||
}
|
||||
|
||||
// Translate bonuses applying to all GP equally
|
||||
for (item in getPercentagesApplyingToAllGP(city)) {
|
||||
val bonusEntry = Entry(item.source, item.pediaLink, isAllGP = true)
|
||||
for (name in allNames)
|
||||
bonusEntry.counter.add(name, item.bonus)
|
||||
percentBonuses.add(bonusEntry)
|
||||
}
|
||||
|
||||
// And last, the GPP-type-specific GreatPersonEarnedFaster Unique
|
||||
val stateForConditionals = StateForConditionals(city)
|
||||
for (unique in city.civ.getMatchingUniques(UniqueType.GreatPersonEarnedFaster, stateForConditionals)) {
|
||||
val gppName = unique.params[0]
|
||||
if (gppName !in allNames) continue // No sense applying a percentage without base points
|
||||
val bonusEntry = Entry(getUniqueSourceName(unique), guessPediaLink(unique))
|
||||
bonusEntry.counter.add(gppName, unique.params[1].toInt())
|
||||
percentBonuses.add(bonusEntry)
|
||||
}
|
||||
}
|
||||
|
||||
/** Aggregate over sources, applying percentage boni using fixed-point math to avoid rounding surprises */
|
||||
fun sum(): Counter<String> {
|
||||
// Accumulate base points as fake "fixed-point"
|
||||
val result = Counter<String>()
|
||||
for (entry in basePoints)
|
||||
result.add(entry.counter * fixedPointFactor)
|
||||
|
||||
// Accumulate percentage bonuses additively not multiplicatively
|
||||
val bonuses = Counter<String>()
|
||||
for (entry in percentBonuses) {
|
||||
bonuses.add(entry.counter)
|
||||
}
|
||||
|
||||
// Apply percent bonuses
|
||||
for (key in result.keys.filter { it in bonuses }) {
|
||||
result.add(key, result[key] * bonuses[key] / 100)
|
||||
}
|
||||
|
||||
// Round fixed-point to integers, toSet() because a result of 0 will remove the entry (-99% bonus in a certain Mod)
|
||||
for (key in result.keys.toSet())
|
||||
result[key] = (result[key] + fixedPointFactor / 2) / fixedPointFactor
|
||||
|
||||
// Remove all "gpp" values that are not valid units
|
||||
for (key in result.keys.toSet())
|
||||
if (key !in ruleset.units)
|
||||
result.remove(key)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
@ -146,7 +146,7 @@ class CityPopulationManager : IsPartOfGameInfoSerialization {
|
||||
internal fun autoAssignPopulation() {
|
||||
city.cityStats.update() // calculate current stats with current assignments
|
||||
val cityStats = city.cityStats.currentCityStats
|
||||
city.currentGPPBonus = city.getGreatPersonPercentageBonus() // pre-calculate
|
||||
city.currentGPPBonus = city.getGreatPersonPercentageBonus() // pre-calculate for use in Automation.rankSpecialist
|
||||
var specialistFoodBonus = 2f // See CityStats.calcFoodEaten()
|
||||
for (unique in city.getMatchingUniques(UniqueType.FoodConsumptionBySpecialists))
|
||||
if (city.matchesFilter(unique.params[1]))
|
||||
|
@ -11,6 +11,7 @@ import com.unciv.UncivGame
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.city.CityFlags
|
||||
import com.unciv.logic.city.CityFocus
|
||||
import com.unciv.logic.city.GreatPersonPointsBreakdown
|
||||
import com.unciv.models.Counter
|
||||
import com.unciv.models.ruleset.Building
|
||||
import com.unciv.models.ruleset.tile.TileResource
|
||||
@ -350,21 +351,14 @@ class CityStatsTable(private val cityScreen: CityScreen) : Table() {
|
||||
|
||||
val greatPeopleTable = Table()
|
||||
|
||||
val greatPersonPoints = city.getGreatPersonPointsForNextTurn()
|
||||
val allGreatPersonNames = greatPersonPoints.asSequence().flatMap { it.value.keys }.distinct()
|
||||
|
||||
if (allGreatPersonNames.none())
|
||||
val gppBreakdown = GreatPersonPointsBreakdown(city)
|
||||
if (gppBreakdown.allNames.isEmpty())
|
||||
return
|
||||
val greatPersonPoints = gppBreakdown.sum()
|
||||
|
||||
for (greatPersonName in allGreatPersonNames) {
|
||||
|
||||
var gppPerTurn = 0
|
||||
|
||||
for ((_, gppCounter) in greatPersonPoints) {
|
||||
val gppPointsFromSource = gppCounter[greatPersonName]
|
||||
if (gppPointsFromSource == 0) continue
|
||||
gppPerTurn += gppPointsFromSource
|
||||
}
|
||||
// Iterating over allNames instead of greatPersonPoints will include those where the aggregation had points but ended up zero
|
||||
for (greatPersonName in gppBreakdown.allNames) {
|
||||
val gppPerTurn = greatPersonPoints[greatPersonName]
|
||||
|
||||
val info = Table()
|
||||
|
||||
@ -391,9 +385,15 @@ class CityStatsTable(private val cityScreen: CityScreen) : Table() {
|
||||
progressBar.setLabel(Color.WHITE, "$gppCurrent/$gppNeeded", fontSize = 14)
|
||||
|
||||
info.add(progressBar).colspan(2).left().expandX().row()
|
||||
|
||||
info.onClick {
|
||||
GreatPersonPointsBreakdownPopup(cityScreen, gppBreakdown, greatPersonName)
|
||||
}
|
||||
greatPeopleTable.add(info).growX().top().padBottom(10f)
|
||||
greatPeopleTable.add(ImageGetter.getConstructionPortrait(greatPersonName, 50f)).row()
|
||||
val icon = ImageGetter.getConstructionPortrait(greatPersonName, 50f)
|
||||
icon.onClick {
|
||||
GreatPersonPointsBreakdownPopup(cityScreen, gppBreakdown, null)
|
||||
}
|
||||
greatPeopleTable.add(icon).row()
|
||||
}
|
||||
|
||||
lowerTable.addCategory("Great People", greatPeopleTable, KeyboardBinding.GreatPeopleDetail)
|
||||
|
@ -0,0 +1,59 @@
|
||||
package com.unciv.ui.screens.cityscreen
|
||||
|
||||
import com.unciv.logic.city.GreatPersonPointsBreakdown
|
||||
import com.unciv.ui.components.extensions.toStringSigned
|
||||
import com.unciv.ui.popups.Popup
|
||||
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
|
||||
import com.unciv.ui.screens.civilopediascreen.FormattedLine
|
||||
import com.unciv.ui.screens.civilopediascreen.MarkupRenderer
|
||||
|
||||
class GreatPersonPointsBreakdownPopup(cityScreen: CityScreen, gppBreakdown: GreatPersonPointsBreakdown, greatPerson: String?) : Popup(cityScreen) {
|
||||
init {
|
||||
val lines = ArrayList<FormattedLine>()
|
||||
val headerText = "«GOLD»{${greatPerson ?: "Great person points"}}«» ({${cityScreen.city.name}})"
|
||||
lines += FormattedLine(headerText, header = 2, centered = true)
|
||||
lines += FormattedLine(separator = true)
|
||||
|
||||
fun addFormattedEntry(entry: GreatPersonPointsBreakdown.Entry, isPercentage: Boolean) {
|
||||
val text = if (greatPerson == null) {
|
||||
// Popup shows all GP for a city - this will resolve the counters if necessary and dhow GP names from the keys
|
||||
entry.toString(isPercentage)
|
||||
} else {
|
||||
// Popup shows only a specific GP - check counters directly
|
||||
val amount = entry.counter[greatPerson]
|
||||
if (amount == 0) return
|
||||
// Formatter does not need the GP name as in all cases the one in the header is clear enough
|
||||
entry.toString(isPercentage, amount)
|
||||
}
|
||||
lines += FormattedLine(text, entry.pediaLink ?: "")
|
||||
}
|
||||
|
||||
for (entry in gppBreakdown.basePoints)
|
||||
addFormattedEntry(entry, false)
|
||||
|
||||
for (entry in gppBreakdown.percentBonuses)
|
||||
addFormattedEntry(entry, true)
|
||||
|
||||
val game = cityScreen.game
|
||||
val ruleset = game.gameInfo!!.ruleset
|
||||
add(MarkupRenderer.render(lines) {
|
||||
game.pushScreen(CivilopediaScreen(ruleset, link = it))
|
||||
})
|
||||
|
||||
addCloseButton()
|
||||
open(true)
|
||||
}
|
||||
|
||||
private fun GreatPersonPointsBreakdown.Entry.toString(isPercentage: Boolean) =
|
||||
"{$source}: " +
|
||||
when {
|
||||
isAllGP -> (counter.values.firstOrNull() ?: 0).toStringSigned() + (if (isPercentage) "%" else "")
|
||||
isPercentage -> counter.entries.joinToString { it.value.toStringSigned() + "% {${it.key}}" }
|
||||
else -> counter.entries.joinToString { it.value.toStringSigned() + " {${it.key}}" }
|
||||
}
|
||||
|
||||
private fun GreatPersonPointsBreakdown.Entry.toString(isPercentage: Boolean, amount: Int) =
|
||||
"{$source}: " +
|
||||
amount.toStringSigned() +
|
||||
(if (isPercentage) "%" else "")
|
||||
}
|
@ -2,6 +2,7 @@ package com.unciv.ui.screens.civilopediascreen
|
||||
|
||||
import com.badlogic.gdx.Gdx
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.graphics.Colors
|
||||
import com.badlogic.gdx.graphics.Pixmap
|
||||
import com.badlogic.gdx.graphics.g2d.TextureRegion
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
@ -16,8 +17,8 @@ import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.ruleset.RulesetCache
|
||||
import com.unciv.models.ruleset.unique.Unique
|
||||
import com.unciv.ui.components.extensions.getReadonlyPixmap
|
||||
import com.unciv.ui.components.widgets.ColorMarkupLabel
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.components.widgets.ColorMarkupLabel
|
||||
import com.unciv.ui.images.ImageGetter
|
||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||
import com.unciv.utils.Log
|
||||
@ -65,7 +66,7 @@ class FormattedLine (
|
||||
val indent: Int = 0,
|
||||
/** Defines vertical padding between rows, defaults to 5f. */
|
||||
val padding: Float = Float.NaN,
|
||||
/** Sets text color, accepts 6/3-digit web colors (e.g. #FFA040). */
|
||||
/** Sets text color, accepts 6/3-digit web colors (e.g. #FFA040) or names as defined by Gdx [Colors]. */
|
||||
val color: String = "",
|
||||
/** Renders a separator line instead of text. Can be combined only with [color] and [size] (line width, default 2) */
|
||||
val separator: Boolean = false,
|
||||
@ -226,7 +227,7 @@ class FormattedLine (
|
||||
val hex6 = String(charArrayOf(color[1], color[1], color[2], color[2], color[3], color[3]))
|
||||
return Color.valueOf(hex6)
|
||||
}
|
||||
return defaultColor
|
||||
return Colors.get(color.uppercase()) ?: defaultColor
|
||||
}
|
||||
|
||||
/** Used only as parameter to [FormattedLine.render] and [MarkupRenderer.render] */
|
||||
|
Loading…
x
Reference in New Issue
Block a user