chore: Tile purity

This commit is contained in:
yairm210 2025-07-18 11:15:02 +03:00
parent 673c33bb01
commit c1f0b97e2d
7 changed files with 89 additions and 71 deletions

View File

@ -240,6 +240,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
/** Get barbarian civ
* @throws NoSuchElementException in no-barbarians games! */
fun getBarbarianCivilization() = getCivilization(Constants.barbarians)
@Readonly @Suppress("purity") // This should be autorecognized!!
fun getDifficulty() = difficultyObject
/** Access a cached `GlobalUniques` that combines the [ruleset]'s [globalUniques][Ruleset.globalUniques]
* with the Uniques of the chosen [speed] and [difficulty][getDifficulty] */

View File

@ -324,10 +324,8 @@ class Civilization : IsPartOfGameInfoSerialization {
if (!knows(civInfo)) diplomacyFunctions.makeCivilizationsMeet(civInfo)
return getDiplomacyManager(civInfo.civName)!!
}
@Readonly
fun getDiplomacyManager(civInfo: Civilization): DiplomacyManager? = getDiplomacyManager(civInfo.civName)
@Readonly
fun getDiplomacyManager(civName: String): DiplomacyManager? = diplomacy[civName]
@Readonly fun getDiplomacyManager(civInfo: Civilization): DiplomacyManager? = getDiplomacyManager(civInfo.civName)
@Readonly fun getDiplomacyManager(civName: String): DiplomacyManager? = diplomacy[civName]
fun getProximity(civInfo: Civilization) = getProximity(civInfo.civName)
@Suppress("MemberVisibilityCanBePrivate") // same visibility for overloads
@ -347,17 +345,15 @@ class Civilization : IsPartOfGameInfoSerialization {
fun getKnownCivsWithSpectators() = diplomacy.values.asSequence().map { it.otherCiv() }
.filter { !it.isDefeated() }
@Readonly
fun knows(otherCivName: String) = diplomacy.containsKey(otherCivName)
@Readonly
fun knows(otherCiv: Civilization) = knows(otherCiv.civName)
@Readonly fun knows(otherCivName: String) = diplomacy.containsKey(otherCivName)
@Readonly fun knows(otherCiv: Civilization) = knows(otherCiv.civName)
@Readonly
fun getCapital(firstCityIfNoCapital: Boolean = true) = cities.firstOrNull { it.isCapital() } ?:
if (firstCityIfNoCapital) cities.firstOrNull() else null
@Readonly
fun isHuman() = playerType == PlayerType.Human
@Readonly
fun isAI() = playerType == PlayerType.AI
@Readonly fun isHuman() = playerType == PlayerType.Human
@Readonly fun isAI() = playerType == PlayerType.AI
@Readonly
fun isAIOrAutoPlaying(): Boolean {
if (playerType == PlayerType.AI) return true
@ -365,14 +361,11 @@ class Civilization : IsPartOfGameInfoSerialization {
val worldScreen = UncivGame.Current.worldScreen ?: return false
return worldScreen.viewingCiv == this && worldScreen.autoPlay.isAutoPlaying()
}
@Readonly
fun isOneCityChallenger() = playerType == PlayerType.Human && gameInfo.gameParameters.oneCityChallenge
@Readonly
fun isCurrentPlayer() = gameInfo.currentPlayerCiv == this
@Readonly
fun isMajorCiv() = nation.isMajorCiv
@Readonly
fun isMinorCiv() = nation.isCityState || nation.isBarbarian
@Readonly fun isOneCityChallenger() = playerType == PlayerType.Human && gameInfo.gameParameters.oneCityChallenge
@Readonly fun isCurrentPlayer() = gameInfo.currentPlayerCiv == this
@Readonly fun isMajorCiv() = nation.isMajorCiv
@Readonly fun isMinorCiv() = nation.isCityState || nation.isBarbarian
@delegate:Transient
val isCityState by lazy { nation.isCityState }
@ -380,10 +373,9 @@ class Civilization : IsPartOfGameInfoSerialization {
@delegate:Transient
val isBarbarian by lazy { nation.isBarbarian }
@Readonly
fun isSpectator() = nation.isSpectator
@Readonly
fun isAlive(): Boolean = !isDefeated()
@Readonly fun isSpectator() = nation.isSpectator
@Readonly fun isAlive(): Boolean = !isDefeated()
@delegate:Transient
val cityStateType: CityStateType by lazy { gameInfo.ruleset.cityStateTypes[nation.cityStateType!!]!! }
@ -407,13 +399,6 @@ class Civilization : IsPartOfGameInfoSerialization {
}
@Suppress("MemberVisibilityCanBePrivate")
fun getPreferredVictoryTypeObjects(): List<Victory> {
val preferredVictoryTypes = getPreferredVictoryTypes()
return if (preferredVictoryTypes.contains(Constants.neutralVictoryType)) emptyList()
else preferredVictoryTypes.map { gameInfo.ruleset.victories[it]!! }
}
@Readonly
fun getPersonality(): Personality {
return if (isAIOrAutoPlaying()) gameInfo.ruleset.personalities[nation.personality] ?: Personality.neutralPersonality
@ -1081,6 +1066,7 @@ class Civilization : IsPartOfGameInfoSerialization {
fun asPreview() = CivilizationInfoPreview(this)
@Readonly
fun getLastSeenImprovement(position: Vector2): String? {
if (isAI() || isSpectator()) return null
return lastSeenImprovement[position]

View File

@ -147,6 +147,7 @@ class DiplomacyFunctions(val civInfo: Civilization) {
* Use [UnitMovement.canPassThrough] to check whether a specific unit can pass through
* a specific tile.
*/
@Readonly
fun canPassThroughTiles(otherCiv: Civilization): Boolean {
if (otherCiv == civInfo) return true
if (otherCiv.isBarbarian) return true

View File

@ -18,10 +18,11 @@ import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.utils.addToMapOfSets
import com.unciv.utils.contains
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly
import java.lang.Integer.max
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.abs
import kotlin.math.max
/** An Unciv map with all properties as produced by the [map editor][com.unciv.ui.screens.mapeditorscreen.MapEditorScreen]
* or [MapGenerator][com.unciv.logic.map.mapgenerator.MapGenerator]; or as part of a running [game][GameInfo].
@ -400,8 +401,10 @@ class TileMap(initialCapacity: Int = 10) : IsPartOfGameInfoSerialization {
data class ViewableTile(val tile: Tile, val maxHeightSeenToTile: Int, val isVisible: Boolean, val isAttackable: Boolean)
/** @return List of tiles visible from location [position] for a unit with sight range [sightDistance] */
@Readonly
fun getViewableTiles(position: Vector2, sightDistance: Int, forAttack: Boolean = false): List<Tile> {
val aUnitHeight = get(position).unitHeight
@LocalState
val viewableTiles = mutableListOf(ViewableTile(
get(position),
aUnitHeight,
@ -412,6 +415,7 @@ class TileMap(initialCapacity: Int = 10) : IsPartOfGameInfoSerialization {
for (i in 1..sightDistance+1) { // in each layer,
// This is so we don't use tiles in the same distance to "see over",
// that is to say, the "viewableTiles.contains(it) check will return false for neighbors from the same distance
@LocalState
val tilesToAddInDistanceI = ArrayList<ViewableTile>()
for (cTile in getTilesAtDistance(position, i)) { // for each tile in that layer,

View File

@ -26,6 +26,7 @@ import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.ruleset.unit.UnitType
import com.unciv.models.translations.tr
import com.unciv.ui.components.UnitMovementMemoryType
import yairm210.purity.annotations.Readonly
import java.text.DecimalFormat
import kotlin.math.pow
import kotlin.math.ulp
@ -230,6 +231,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
fun getMovementString(): String =
(DecimalFormat("0.#").format(currentMovement.toDouble()) + "/" + getMaxMovement()).tr()
@Readonly @Suppress("purity") // should be autorecognized
fun getTile(): Tile = currentTile
fun getClosestCity(): City? = civ.cities.minByOrNull {
@ -284,6 +286,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
fun getUniques(): Sequence<Unique> = tempUniquesMap.getAllUniques()
@Readonly
fun getMatchingUniques(
uniqueType: UniqueType,
gameContext: GameContext = cache.state,
@ -296,6 +299,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
yieldAll(civ.getMatchingUniques(uniqueType, gameContext))
}
@Readonly
fun hasUnique(
uniqueType: UniqueType,
gameContext: GameContext = cache.state,
@ -405,12 +409,14 @@ class MapUnit : IsPartOfGameInfoSerialization {
return getRange() * 2
}
@Readonly
fun isEmbarked(): Boolean {
if (!baseUnit.isLandUnit) return false
if (cache.canMoveOnWater) return false
return currentTile.isWater
}
@Readonly
fun isInvisible(to: Civilization): Boolean {
if (hasUnique(UniqueType.Invisible) && !to.isSpectator())
return true

View File

@ -34,6 +34,7 @@ import com.unciv.utils.DebugUtils
import com.unciv.utils.Log
import com.unciv.utils.withItem
import com.unciv.utils.withoutItem
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly
import kotlin.collections.ArrayList
import kotlin.collections.HashSet
@ -241,9 +242,10 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
//region pure functions
fun isHill() = baseTerrain == Constants.hill || terrainFeatures.contains(Constants.hill)
@Readonly fun isHill() = baseTerrain == Constants.hill || terrainFeatures.contains(Constants.hill)
/** Returns military, civilian and air units in tile */
@Readonly
fun getUnits() = sequence {
if (militaryUnit != null) yield(militaryUnit!!)
if (civilianUnit != null) yield(civilianUnit!!)
@ -251,6 +253,7 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
}
/** This is for performance reasons of canPassThrough() - faster than getUnits().firstOrNull() */
@Readonly
fun getFirstUnit(): MapUnit? {
if (militaryUnit != null) return militaryUnit!!
if (civilianUnit != null) return civilianUnit!!
@ -258,41 +261,39 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
return null
}
@Readonly
@Readonly @Suppress("purity") // should be autorecognized as readonly
fun getCity(): City? = owningCity
internal fun getNaturalWonder(): Terrain =
@Readonly internal fun getNaturalWonder(): Terrain =
if (naturalWonder == null) throw Exception("No natural wonder exists for this tile!")
else ruleset.terrains[naturalWonder!!]!!
@Readonly
fun isVisible(player: Civilization): Boolean {
if (DebugUtils.VISIBLE_MAP)
return true
return player.viewableTiles.contains(this)
}
@Readonly
fun isExplored(player: Civilization): Boolean {
if (DebugUtils.VISIBLE_MAP || player.civName == Constants.spectator)
return true
return exploredBy.contains(player.civName)
}
@Readonly @Suppress("purity") // should be autorecognized as readonly
fun isCityCenter(): Boolean = isCityCenterInternal
fun isNaturalWonder(): Boolean = naturalWonder != null
fun isImpassible() = lastTerrain.impassable
@Readonly fun isNaturalWonder(): Boolean = naturalWonder != null
@Readonly fun isImpassible() = lastTerrain.impassable
fun hasImprovementInProgress() = improvementQueue.isNotEmpty()
@Readonly
fun getTileImprovement(): TileImprovement? = if (improvement == null) null else ruleset.tileImprovements[improvement!!]
@Readonly
fun isPillaged(): Boolean = improvementIsPillaged || roadIsPillaged
@Readonly
fun getUnpillagedTileImprovement(): TileImprovement? = if (getUnpillagedImprovement() == null) null else ruleset.tileImprovements[improvement!!]
@Readonly
fun getTileImprovementInProgress(): TileImprovement? = improvementQueue.firstOrNull()?.let { ruleset.tileImprovements[it.improvement] }
@Readonly
fun containsGreatImprovement() = getTileImprovement()?.isGreatImprovement() == true
@Readonly fun hasImprovementInProgress() = improvementQueue.isNotEmpty()
@Readonly fun getTileImprovement(): TileImprovement? = if (improvement == null) null else ruleset.tileImprovements[improvement!!]
@Readonly fun isPillaged(): Boolean = improvementIsPillaged || roadIsPillaged
@Readonly fun getUnpillagedTileImprovement(): TileImprovement? = if (getUnpillagedImprovement() == null) null else ruleset.tileImprovements[improvement!!]
@Readonly fun getTileImprovementInProgress(): TileImprovement? = improvementQueue.firstOrNull()?.let { ruleset.tileImprovements[it.improvement] }
@Readonly fun containsGreatImprovement() = getTileImprovement()?.isGreatImprovement() == true
@Readonly
fun getImprovementToPillage(): TileImprovement? {
@ -319,10 +320,7 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
return ruleset.tileImprovements[roadStatus.name]!!
return null
}
@Readonly
fun canPillageTile(): Boolean {
return canPillageTileImprovement() || canPillageRoad()
}
@Readonly fun canPillageTile(): Boolean = canPillageTileImprovement() || canPillageRoad()
@Readonly
fun canPillageTileImprovement(): Boolean {
return improvement != null && !improvementIsPillaged
@ -335,12 +333,10 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
&& !ruleset.tileImprovements[roadStatus.name]!!.hasUnique(UniqueType.Unpillagable)
&& !ruleset.tileImprovements[roadStatus.name]!!.hasUnique(UniqueType.Irremovable)
}
@Readonly
fun getUnpillagedImprovement(): String? = if (improvementIsPillaged) null else improvement
@Readonly fun getUnpillagedImprovement(): String? = if (improvementIsPillaged) null else improvement
/** @return [RoadStatus] on this [Tile], pillaged road counts as [RoadStatus.None] */
@Readonly
fun getUnpillagedRoad(): RoadStatus = if (roadIsPillaged) RoadStatus.None else roadStatus
@Readonly fun getUnpillagedRoad(): RoadStatus = if (roadIsPillaged) RoadStatus.None else roadStatus
@Readonly
fun getUnpillagedRoadImprovement(): TileImprovement? {
@ -354,12 +350,13 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
* @param viewingCiv `null` means civ-agnostic and thus always showing the actual improvement
* @return The improvement name, or `null` if no improvement should be shown
*/
@Readonly
fun getShownImprovement(viewingCiv: Civilization?): String? =
if (viewingCiv == null || viewingCiv.playerType == PlayerType.AI || viewingCiv.isSpectator()) improvement
else viewingCiv.getLastSeenImprovement(position)
/** Returns true if this tile has fallout or an equivalent terrain feature */
fun hasFalloutEquivalent(): Boolean = terrainFeatures.any { ruleset.terrains[it]!!.hasUnique(UniqueType.NullifyYields)}
@Readonly fun hasFalloutEquivalent(): Boolean = terrainFeatures.any { ruleset.terrains[it]!!.hasUnique(UniqueType.NullifyYields)}
fun getRow() = HexMath.getRow(position)
@ -367,9 +364,9 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
fun getBaseTerrain(): Terrain = baseTerrainObject
@Readonly
fun getOwner(): Civilization? = getCity()?.civ
@Readonly fun getOwner(): Civilization? = getCity()?.civ
@Readonly
fun getRoadOwner(): Civilization? {
return if (roadOwner != "")
tileMap.gameInfo.getCivilization(roadOwner)
@ -394,19 +391,22 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
return civInfo.isAtWarWith(tileOwner)
}
fun isRoughTerrain() = allTerrains.any { it.isRough() }
@Readonly fun isRoughTerrain() = allTerrains.any { it.isRough() }
@Transient
internal var stateThisTile: GameContext = GameContext.EmptyState
/** Checks whether any of the TERRAINS of this tile has a certain unique */
@Readonly
fun terrainHasUnique(uniqueType: UniqueType, state: GameContext = stateThisTile) =
cachedTerrainData.uniques.hasMatchingUnique(uniqueType, state)
/** Get all uniques of this type that any TERRAIN on this tile has */
@Readonly
fun getTerrainMatchingUniques(uniqueType: UniqueType, gameContext: GameContext = stateThisTile ): Sequence<Unique> {
return cachedTerrainData.uniques.getMatchingUniques(uniqueType, gameContext)
}
/** Get all uniques of this type that any part of this tile has: terrains, improvement, resource */
@Readonly
fun getMatchingUniques(uniqueType: UniqueType, gameContext: GameContext = stateThisTile): Sequence<Unique> {
var uniques = getTerrainMatchingUniques(uniqueType, gameContext)
if (getUnpillagedImprovement() != null) {
@ -427,6 +427,7 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
return civInfo.cities.firstOrNull { it != owningCity && it.isWorked(this) }
}
@Readonly
fun isBlockaded(): Boolean {
val owner = getOwner() ?: return false
val unit = militaryUnit
@ -463,11 +464,13 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
|| terrainHasUnique(UniqueType.TileProvidesYieldWithoutPopulation)
}
@Readonly
fun isLocked(): Boolean {
val workingCity = getWorkingCity()
return workingCity != null && workingCity.lockedTiles.contains(position)
}
@Readonly
fun providesResources(civInfo: Civilization): Boolean {
if (!hasViewableResource(civInfo)) return false
if (isCityCenter()) {
@ -520,6 +523,7 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
}
/** Implements [UniqueParameterType.TerrainFilter][com.unciv.models.ruleset.unique.UniqueParameterType.TerrainFilter] */
@Readonly
fun matchesTerrainFilter(filter: String, observingCiv: Civilization?, multiFilter: Boolean = true): Boolean {
return if (multiFilter) MultiFilter.multiFilter(filter, { matchesSingleTerrainFilter(it, observingCiv) })
else matchesSingleTerrainFilter(filter, observingCiv)
@ -586,11 +590,13 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
fun hasViewableResource(civInfo: Civilization): Boolean =
resource != null && civInfo.tech.isRevealed(tileResource)
fun getViewableTilesList(distance: Int): List<Tile> = tileMap.getViewableTiles(position, distance)
fun getTilesInDistance(distance: Int): Sequence<Tile> = tileMap.getTilesInDistance(position, distance)
fun getTilesInDistanceRange(range: IntRange): Sequence<Tile> = tileMap.getTilesInDistanceRange(position, range)
fun getTilesAtDistance(distance: Int): Sequence<Tile> = tileMap.getTilesAtDistance(position, distance)
@Readonly fun getViewableTilesList(distance: Int): List<Tile> = tileMap.getViewableTiles(position, distance)
@Readonly fun getTilesInDistance(distance: Int): Sequence<Tile> = tileMap.getTilesInDistance(position, distance)
@Readonly fun getTilesInDistanceRange(range: IntRange): Sequence<Tile> = tileMap.getTilesInDistanceRange(position, range)
@Readonly fun getTilesAtDistance(distance: Int): Sequence<Tile> = tileMap.getTilesAtDistance(position, distance)
@Readonly
fun getDefensiveBonus(includeImprovementBonus: Boolean = true, unit: MapUnit? = null): Float {
var bonus = baseTerrainObject.defenceBonus
if (terrainFeatureObjects.isNotEmpty()) {
@ -612,6 +618,7 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
* @param otherTile Destination tile
* @return Shortest distance from this [Tile] to [otherTile] in count of tiles including impassable tiles but not including origin tile
*/
@Readonly
fun aerialDistanceTo(otherTile: Tile): Int {
val xDelta = position.x - otherTile.position.x
val yDelta = position.y - otherTile.position.y
@ -628,6 +635,7 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
return min(distance, wrappedDistance).toInt()
}
@Readonly
fun canBeSettled(): Boolean {
val modConstants = tileMap.gameInfo.ruleset.modOptions.constants
return when {
@ -641,6 +649,7 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
}
/** The two tiles have a river between them */
@Readonly
fun isConnectedByRiver(otherTile: Tile): Boolean {
if (otherTile == this) throw Exception("Should not be called to compare to self!")
@ -672,6 +681,7 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
* @returns whether units of [civInfo] can pass through this tile, considering only civ-wide filters.
* Use [UnitMovement.canPassThrough] to check whether a specific unit can pass through a tile.
*/
@Readonly
fun canCivPassThrough(civInfo: Civilization): Boolean {
val tileOwner = getOwner()
// comparing the CivInfo objects is cheaper than comparing strings
@ -680,7 +690,8 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
&& !getCity()!!.hasJustBeenConquered) return false
return civInfo.diplomacyFunctions.canPassThroughTiles(tileOwner)
}
@Readonly
fun hasEnemyInvisibleUnit(viewingCiv: Civilization): Boolean {
val unitsInTile = getUnits()
return when {
@ -691,28 +702,33 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
}
}
@Readonly
fun hasConnection(civInfo: Civilization) =
getUnpillagedRoad() != RoadStatus.None || forestOrJungleAreRoads(civInfo)
@Readonly
fun hasRoadConnection(civInfo: Civilization, mustBeUnpillaged: Boolean) =
if (mustBeUnpillaged)
(getUnpillagedRoad() == RoadStatus.Road) || forestOrJungleAreRoads(civInfo)
else
roadStatus == RoadStatus.Road || forestOrJungleAreRoads(civInfo)
@Readonly
fun hasRailroadConnection(mustBeUnpillaged: Boolean) =
if (mustBeUnpillaged)
getUnpillagedRoad() == RoadStatus.Railroad
else
roadStatus == RoadStatus.Railroad
@Readonly
private fun forestOrJungleAreRoads(civInfo: Civilization) =
civInfo.nation.forestsAndJunglesAreRoads
&& (terrainFeatures.contains(Constants.jungle) || terrainFeatures.contains(Constants.forest))
&& isFriendlyTerritory(civInfo)
@Readonly
fun getRulesetIncompatibility(ruleset: Ruleset): HashSet<String> {
@LocalState
val out = HashSet<String>()
if (!ruleset.terrains.containsKey(baseTerrain))
out.add("Base terrain [$baseTerrain] does not exist in ruleset!")
@ -727,15 +743,17 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
return out
}
@Readonly @Suppress("purity") // should be auto-recognized as readonly
fun getContinent() = continent
/** Checks if this tile is marked as target tile for a building with a [UniqueType.CreatesOneImprovement] unique */
fun isMarkedForCreatesOneImprovement() =
@Readonly fun isMarkedForCreatesOneImprovement() =
turnsToImprovement < 0 && improvementInProgress != null
/** Checks if this tile is marked as target tile for a building with a [UniqueType.CreatesOneImprovement] unique creating a specific [improvement] */
fun isMarkedForCreatesOneImprovement(improvement: String) =
@Readonly fun isMarkedForCreatesOneImprovement(improvement: String) =
turnsToImprovement < 0 && improvementInProgress == improvement
@Readonly
private fun approximateMajorDepositDistribution(): Double {
// We can't replicate the MapRegions resource distributor, so let's try to get
// a close probability of major deposits per tile

View File

@ -13,6 +13,7 @@ import com.unciv.models.stats.GameResource
import com.unciv.models.stats.Stats
import com.unciv.ui.objectdescriptions.uniquesToCivilopediaTextLines
import com.unciv.ui.screens.civilopediascreen.FormattedLine
import yairm210.purity.annotations.Readonly
class TileResource : RulesetStatsObject(), GameResource {
@ -52,6 +53,7 @@ class TileResource : RulesetStatsObject(), GameResource {
* @see improvedBy
* @see UniqueType.ImprovesResources
*/
@Readonly @Suppress("purity") // requires some plumbing
fun getImprovements(): Set<String> {
if (improvementsInitialized) return allImprovements
val ruleset = this.ruleset