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.union",
"kotlin.collections.intersect",
"kotlin.collections.List.indexOf",
)
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.ui.screens.worldscreen.unit.actions.UnitActions
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsFromUniques
import yairm210.purity.annotations.Readonly
import kotlin.math.roundToInt
object SpecificUnitAutomation {
@ -347,6 +348,7 @@ object SpecificUnitAutomation {
return tileBeforeMoving != unit.currentTile
}
@Readonly
private fun getWonderThatWouldBenefitFromBeingSpedUp(city: City): Building? {
return city.cityConstructions.getBuildableBuildings().filter { building ->
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.utils.withItem
import com.unciv.utils.withoutItem
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly
import kotlin.math.ceil
import kotlin.math.min
@ -101,17 +102,20 @@ class CityConstructions : IsPartOfGameInfoSerialization {
}
// Why is one of these called 'buildable' and the other 'constructable'?
@Readonly
internal fun getBuildableBuildings(): Sequence<Building> = city.getRuleset().buildings.values
.asSequence().filter { it.isBuildable(this) }
@Readonly
fun getConstructableUnits() = city.getRuleset().units.values
.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 {
val stats = StatTreeNode()
@LocalState val stats = StatTreeNode()
for (building in getBuiltBuildings())
stats.addStats(building.getStats(city, localUniqueCache), building.name)
return stats
@ -120,6 +124,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
/**
* @return Maintenance cost of all built buildings
*/
@Readonly
fun getMaintenanceCosts(): Float {
var maintenanceCost = 0f
val freeBuildings = city.civ.civConstructions.getFreeBuildingNames(city)
@ -144,6 +149,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
return maintenanceCost
}
@Readonly
fun getCityProductionTextForCityButton(): String {
val currentConstructionSnapshot = currentConstructionFromQueue // See below
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 */
@Readonly
internal fun getTurnsToConstructionString(constructionName: String, useStoredProduction: Boolean = true) =
getTurnsToConstructionString(getConstruction(constructionName), useStoredProduction)
/** @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 {
if (construction !is INonPerpetualConstruction) return "" // shouldn't happen
val cost = construction.getProductionCost(city.civ, city)
@ -179,6 +187,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
return lines.joinToString("\n", "\n")
}
@Readonly
fun getProductionMarkup(ruleset: Ruleset): FormattedLine {
val currentConstructionSnapshot = currentConstructionFromQueue
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] */
@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 {
val currentConstruction = getCurrentConstruction()
return currentConstruction is Building && currentConstruction.isWonder
}
@Readonly
fun canBeHurried(): Boolean {
val currentConstruction = getCurrentConstruction()
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 */
@Readonly
fun isFirstConstructionOfItsKind(constructionQueueIndex: Int, name: String): Boolean {
// Simply compare index of first found [name] with given index
return constructionQueueIndex == constructionQueue.indexOf(name)
@ -276,6 +288,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
}
}
@Readonly
fun turnsToConstruction(constructionName: String, useStoredProduction: Boolean = true): Int {
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
@ -287,6 +300,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
return ceil((workLeft-productionOverflow) / productionForConstruction(constructionName).toDouble()).toInt()
}
@Readonly
fun productionForConstruction(constructionName: String): Int {
val cityStatsForConstruction: Stats
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.
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
val construction = city.cityConstructions.getConstruction(constructionName)
cityStats.update(construction, false, false)
@ -312,6 +326,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
return cityStatsForConstruction.production.roundToInt()
}
@Readonly
fun cheapestStatBuilding(stat: Stat): Building? {
return city.getRuleset().buildings.values.asSequence()
.filter { !it.isAnyWonder() && it.isStatRelated(stat, city) &&

View File

@ -120,6 +120,7 @@ class CityStats(val city: City) {
return stats
}
@Readonly
fun getStatConversionRate(stat: Stat): Float {
var conversionRate = 1 / 4f
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 {
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
companion object {
@ -325,7 +325,7 @@ open class PerpetualStatConversion(val stat: Stat) :
override fun getProductionTooltip(city: City, withIcon: Boolean) : String
= "\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 {
val city = cityConstructions.city