chore(purity): CityConstructions

This commit is contained in:
yairm210 2025-08-07 08:09:17 +03:00
parent 99fa2cd964
commit 5e0d23fb1c
5 changed files with 25 additions and 6 deletions

View File

@ -66,6 +66,7 @@ allprojects {
"kotlin.collections.subtract", "kotlin.collections.subtract",
"kotlin.collections.union", "kotlin.collections.union",
"kotlin.collections.intersect", "kotlin.collections.intersect",
"kotlin.collections.List.indexOf",
) )
wellKnownPureClasses = setOf<String>( wellKnownPureClasses = setOf<String>(

View File

@ -15,6 +15,7 @@ import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsFromUniques import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsFromUniques
import yairm210.purity.annotations.Readonly
import kotlin.math.roundToInt import kotlin.math.roundToInt
object SpecificUnitAutomation { object SpecificUnitAutomation {
@ -347,6 +348,7 @@ object SpecificUnitAutomation {
return tileBeforeMoving != unit.currentTile return tileBeforeMoving != unit.currentTile
} }
@Readonly
private fun getWonderThatWouldBenefitFromBeingSpedUp(city: City): Building? { private fun getWonderThatWouldBenefitFromBeingSpedUp(city: City): Building? {
return city.cityConstructions.getBuildableBuildings().filter { building -> return city.cityConstructions.getBuildableBuildings().filter { building ->
building.isWonder && !building.hasUnique(UniqueType.CannotBeHurried) building.isWonder && !building.hasUnique(UniqueType.CannotBeHurried)

View File

@ -37,6 +37,7 @@ import com.unciv.ui.screens.civilopediascreen.FormattedLine
import com.unciv.ui.screens.pickerscreens.PromotionTree import com.unciv.ui.screens.pickerscreens.PromotionTree
import com.unciv.utils.withItem import com.unciv.utils.withItem
import com.unciv.utils.withoutItem import com.unciv.utils.withoutItem
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly import yairm210.purity.annotations.Readonly
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.min import kotlin.math.min
@ -101,17 +102,20 @@ class CityConstructions : IsPartOfGameInfoSerialization {
} }
// Why is one of these called 'buildable' and the other 'constructable'? // Why is one of these called 'buildable' and the other 'constructable'?
@Readonly
internal fun getBuildableBuildings(): Sequence<Building> = city.getRuleset().buildings.values internal fun getBuildableBuildings(): Sequence<Building> = city.getRuleset().buildings.values
.asSequence().filter { it.isBuildable(this) } .asSequence().filter { it.isBuildable(this) }
@Readonly
fun getConstructableUnits() = city.getRuleset().units.values fun getConstructableUnits() = city.getRuleset().units.values
.asSequence().filter { it.isBuildable(this) } .asSequence().filter { it.isBuildable(this) }
/** /**
* @return [Stats] provided by all built buildings in city plus the bonus from Library * @return [Stats] provided by all built buildings in city
*/ */
@Readonly
fun getStats(localUniqueCache: LocalUniqueCache): StatTreeNode { fun getStats(localUniqueCache: LocalUniqueCache): StatTreeNode {
val stats = StatTreeNode() @LocalState val stats = StatTreeNode()
for (building in getBuiltBuildings()) for (building in getBuiltBuildings())
stats.addStats(building.getStats(city, localUniqueCache), building.name) stats.addStats(building.getStats(city, localUniqueCache), building.name)
return stats return stats
@ -120,6 +124,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
/** /**
* @return Maintenance cost of all built buildings * @return Maintenance cost of all built buildings
*/ */
@Readonly
fun getMaintenanceCosts(): Float { fun getMaintenanceCosts(): Float {
var maintenanceCost = 0f var maintenanceCost = 0f
val freeBuildings = city.civ.civConstructions.getFreeBuildingNames(city) val freeBuildings = city.civ.civConstructions.getFreeBuildingNames(city)
@ -144,6 +149,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
return maintenanceCost return maintenanceCost
} }
@Readonly
fun getCityProductionTextForCityButton(): String { fun getCityProductionTextForCityButton(): String {
val currentConstructionSnapshot = currentConstructionFromQueue // See below val currentConstructionSnapshot = currentConstructionFromQueue // See below
var result = currentConstructionSnapshot.tr(true) var result = currentConstructionSnapshot.tr(true)
@ -156,10 +162,12 @@ class CityConstructions : IsPartOfGameInfoSerialization {
} }
/** @param constructionName needs to be a non-perpetual construction, else an empty string is returned */ /** @param constructionName needs to be a non-perpetual construction, else an empty string is returned */
@Readonly
internal fun getTurnsToConstructionString(constructionName: String, useStoredProduction: Boolean = true) = internal fun getTurnsToConstructionString(constructionName: String, useStoredProduction: Boolean = true) =
getTurnsToConstructionString(getConstruction(constructionName), useStoredProduction) getTurnsToConstructionString(getConstruction(constructionName), useStoredProduction)
/** @param construction needs to be a non-perpetual construction, else an empty string is returned */ /** @param construction needs to be a non-perpetual construction, else an empty string is returned */
@Readonly
internal fun getTurnsToConstructionString(construction: IConstruction, useStoredProduction: Boolean = true): String { internal fun getTurnsToConstructionString(construction: IConstruction, useStoredProduction: Boolean = true): String {
if (construction !is INonPerpetualConstruction) return "" // shouldn't happen if (construction !is INonPerpetualConstruction) return "" // shouldn't happen
val cost = construction.getProductionCost(city.civ, city) val cost = construction.getProductionCost(city.civ, city)
@ -179,6 +187,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
return lines.joinToString("\n", "\n") return lines.joinToString("\n", "\n")
} }
@Readonly
fun getProductionMarkup(ruleset: Ruleset): FormattedLine { fun getProductionMarkup(ruleset: Ruleset): FormattedLine {
val currentConstructionSnapshot = currentConstructionFromQueue val currentConstructionSnapshot = currentConstructionFromQueue
if (currentConstructionSnapshot.isEmpty()) return FormattedLine() if (currentConstructionSnapshot.isEmpty()) return FormattedLine()
@ -219,19 +228,22 @@ class CityConstructions : IsPartOfGameInfoSerialization {
/** @return `true` if [constructionName] is anywhere in the construction queue - [isBeingConstructed] **or** [isEnqueuedForLater] */ /** @return `true` if [constructionName] is anywhere in the construction queue - [isBeingConstructed] **or** [isEnqueuedForLater] */
@Readonly fun isBeingConstructedOrEnqueued(constructionName: String) = constructionQueue.contains(constructionName) @Readonly fun isBeingConstructedOrEnqueued(constructionName: String) = constructionQueue.contains(constructionName)
fun isQueueFull(): Boolean = constructionQueue.size >= queueMaxSize @Readonly fun isQueueFull(): Boolean = constructionQueue.size >= queueMaxSize
@Readonly
fun isBuildingWonder(): Boolean { fun isBuildingWonder(): Boolean {
val currentConstruction = getCurrentConstruction() val currentConstruction = getCurrentConstruction()
return currentConstruction is Building && currentConstruction.isWonder return currentConstruction is Building && currentConstruction.isWonder
} }
@Readonly
fun canBeHurried(): Boolean { fun canBeHurried(): Boolean {
val currentConstruction = getCurrentConstruction() val currentConstruction = getCurrentConstruction()
return currentConstruction is INonPerpetualConstruction && !currentConstruction.hasUnique(UniqueType.CannotBeHurried) return currentConstruction is INonPerpetualConstruction && !currentConstruction.hasUnique(UniqueType.CannotBeHurried)
} }
/** If the city is constructing multiple units of the same type, subsequent units will require the full cost */ /** If the city is constructing multiple units of the same type, subsequent units will require the full cost */
@Readonly
fun isFirstConstructionOfItsKind(constructionQueueIndex: Int, name: String): Boolean { fun isFirstConstructionOfItsKind(constructionQueueIndex: Int, name: String): Boolean {
// Simply compare index of first found [name] with given index // Simply compare index of first found [name] with given index
return constructionQueueIndex == constructionQueue.indexOf(name) return constructionQueueIndex == constructionQueue.indexOf(name)
@ -276,6 +288,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
} }
} }
@Readonly
fun turnsToConstruction(constructionName: String, useStoredProduction: Boolean = true): Int { fun turnsToConstruction(constructionName: String, useStoredProduction: Boolean = true): Int {
val workLeft = getRemainingWork(constructionName, useStoredProduction) val workLeft = getRemainingWork(constructionName, useStoredProduction)
if (workLeft <= 0) // This most often happens when a production is more than finished in a multiplayer game while its not your turn if (workLeft <= 0) // This most often happens when a production is more than finished in a multiplayer game while its not your turn
@ -287,6 +300,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
return ceil((workLeft-productionOverflow) / productionForConstruction(constructionName).toDouble()).toInt() return ceil((workLeft-productionOverflow) / productionForConstruction(constructionName).toDouble()).toInt()
} }
@Readonly
fun productionForConstruction(constructionName: String): Int { fun productionForConstruction(constructionName: String): Int {
val cityStatsForConstruction: Stats val cityStatsForConstruction: Stats
if (currentConstructionFromQueue == constructionName) cityStatsForConstruction = city.cityStats.currentCityStats if (currentConstructionFromQueue == constructionName) cityStatsForConstruction = city.cityStats.currentCityStats
@ -302,7 +316,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
we get all sorts of fun concurrency problems when accessing various parts of the cityStats. we get all sorts of fun concurrency problems when accessing various parts of the cityStats.
SO, we create an entirely new CityStats and iterate there - problem solve! SO, we create an entirely new CityStats and iterate there - problem solve!
*/ */
val cityStats = CityStats(city) @LocalState val cityStats = CityStats(city)
cityStats.statsFromTiles = city.cityStats.statsFromTiles // take as-is cityStats.statsFromTiles = city.cityStats.statsFromTiles // take as-is
val construction = city.cityConstructions.getConstruction(constructionName) val construction = city.cityConstructions.getConstruction(constructionName)
cityStats.update(construction, false, false) cityStats.update(construction, false, false)
@ -312,6 +326,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
return cityStatsForConstruction.production.roundToInt() return cityStatsForConstruction.production.roundToInt()
} }
@Readonly
fun cheapestStatBuilding(stat: Stat): Building? { fun cheapestStatBuilding(stat: Stat): Building? {
return city.getRuleset().buildings.values.asSequence() return city.getRuleset().buildings.values.asSequence()
.filter { !it.isAnyWonder() && it.isStatRelated(stat, city) && .filter { !it.isAnyWonder() && it.isStatRelated(stat, city) &&

View File

@ -120,6 +120,7 @@ class CityStats(val city: City) {
return stats return stats
} }
@Readonly
fun getStatConversionRate(stat: Stat): Float { fun getStatConversionRate(stat: Stat): Float {
var conversionRate = 1 / 4f var conversionRate = 1 / 4f
val conversionUnique = city.civ.getMatchingUniques(UniqueType.ProductionToCivWideStatConversionBonus).firstOrNull { it.params[0] == stat.name } val conversionUnique = city.civ.getMatchingUniques(UniqueType.ProductionToCivWideStatConversionBonus).firstOrNull { it.params[0] == stat.name }

View File

@ -293,7 +293,7 @@ open class PerpetualConstruction(override var name: String, val description: Str
IConstruction { IConstruction {
override fun shouldBeDisplayed(cityConstructions: CityConstructions) = isBuildable(cityConstructions) override fun shouldBeDisplayed(cityConstructions: CityConstructions) = isBuildable(cityConstructions)
open fun getProductionTooltip(city: City, withIcon: Boolean = false) : String = "" @Readonly open fun getProductionTooltip(city: City, withIcon: Boolean = false) : String = ""
override fun getStockpiledResourceRequirements(state: GameContext) = Counter.ZERO override fun getStockpiledResourceRequirements(state: GameContext) = Counter.ZERO
companion object { companion object {
@ -325,7 +325,7 @@ open class PerpetualStatConversion(val stat: Stat) :
override fun getProductionTooltip(city: City, withIcon: Boolean) : String override fun getProductionTooltip(city: City, withIcon: Boolean) : String
= "\r\n${(city.cityStats.currentCityStats.production / getConversionRate(city)).roundToInt()}${if (withIcon) stat.character else ""}/${Fonts.turn}" = "\r\n${(city.cityStats.currentCityStats.production / getConversionRate(city)).roundToInt()}${if (withIcon) stat.character else ""}/${Fonts.turn}"
fun getConversionRate(city: City) : Int = (1/city.cityStats.getStatConversionRate(stat)).roundToInt() @Readonly fun getConversionRate(city: City) : Int = (1/city.cityStats.getStatConversionRate(stat)).roundToInt()
override fun isBuildable(cityConstructions: CityConstructions): Boolean { override fun isBuildable(cityConstructions: CityConstructions): Boolean {
val city = cityConstructions.city val city = cityConstructions.city