chore(purity): MapUnit

This commit is contained in:
yairm210 2025-07-23 23:39:27 +03:00
parent 5fe0517c81
commit be6216d2fb
6 changed files with 72 additions and 36 deletions

View File

@ -56,6 +56,7 @@ allprojects {
"com.badlogic.gdx.math.Vector2.len", "com.badlogic.gdx.math.Vector2.len",
"com.badlogic.gdx.math.Vector2.cpy", "com.badlogic.gdx.math.Vector2.cpy",
"kotlin.collections.Collection.contains", "kotlin.collections.Collection.contains",
"kotlin.collections.dropLastWhile",
) )
wellKnownPureClasses = setOf( wellKnownPureClasses = setOf(
"java.text.NumberFormat" "java.text.NumberFormat"

View File

@ -26,6 +26,7 @@ import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.ruleset.unit.UnitType import com.unciv.models.ruleset.unit.UnitType
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.components.UnitMovementMemoryType import com.unciv.ui.components.UnitMovementMemoryType
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly import yairm210.purity.annotations.Readonly
import java.text.DecimalFormat import java.text.DecimalFormat
import kotlin.math.pow import kotlin.math.pow
@ -252,28 +253,30 @@ class MapUnit : IsPartOfGameInfoSerialization {
return turnsFortified return turnsFortified
} }
fun isSleeping() = action?.startsWith(UnitActionType.Sleep.value) == true @Readonly fun isSleeping() = action?.startsWith(UnitActionType.Sleep.value) == true
fun isSleepingUntilHealed() = isSleeping() && isActionUntilHealed() @Readonly fun isSleepingUntilHealed() = isSleeping() && isActionUntilHealed()
fun isMoving() = action?.startsWith("moveTo") == true @Readonly fun isMoving() = action?.startsWith("moveTo") == true
@Readonly
fun getMovementDestination(): Tile { fun getMovementDestination(): Tile {
val destination = action!!.replace("moveTo ", "").split(",").dropLastWhile { it.isEmpty() } val destination = action!!.replace("moveTo ", "").split(",").dropLastWhile { it.isEmpty() }
val destinationVector = Vector2(destination[0].toFloat(), destination[1].toFloat()) val destinationVector = Vector2(destination[0].toFloat(), destination[1].toFloat())
return currentTile.tileMap[destinationVector] return currentTile.tileMap[destinationVector]
} }
fun isAutomated() = automated @Readonly fun isAutomated() = automated
fun isAutomatingRoadConnection() = action == UnitActionType.ConnectRoad.value @Readonly fun isAutomatingRoadConnection() = action == UnitActionType.ConnectRoad.value
fun isExploring() = action == UnitActionType.Explore.value @Readonly fun isExploring() = action == UnitActionType.Explore.value
fun isPreparingParadrop() = action == UnitActionType.Paradrop.value @Readonly fun isPreparingParadrop() = action == UnitActionType.Paradrop.value
fun isPreparingAirSweep() = action == UnitActionType.AirSweep.value @Readonly fun isPreparingAirSweep() = action == UnitActionType.AirSweep.value
fun isSetUpForSiege() = action == UnitActionType.SetUp.value @Readonly fun isSetUpForSiege() = action == UnitActionType.SetUp.value
/** /**
* @param includeOtherEscortUnit determines whether this method will also check if it's other escort unit is idle if it has one * @param includeOtherEscortUnit determines whether this method will also check if it's other escort unit is idle if it has one
* Leave it as default unless you know what [isIdle] does. * Leave it as default unless you know what [isIdle] does.
*/ */
@Readonly
fun isIdle(includeOtherEscortUnit: Boolean = true): Boolean { fun isIdle(includeOtherEscortUnit: Boolean = true): Boolean {
if (!hasMovement()) return false if (!hasMovement()) return false
val tile = getTile() val tile = getTile()
@ -285,7 +288,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
return !(isFortified() || isExploring() || isSleeping() || isAutomated() || isMoving() || isGuarding()) return !(isFortified() || isExploring() || isSleeping() || isAutomated() || isMoving() || isGuarding())
} }
fun getUniques(): Sequence<Unique> = tempUniquesMap.getAllUniques() @Readonly fun getUniques(): Sequence<Unique> = tempUniquesMap.getAllUniques()
@Readonly @Readonly
fun getMatchingUniques( fun getMatchingUniques(
@ -309,6 +312,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
return getMatchingUniques(uniqueType, gameContext, checkCivInfoUniques).any() return getMatchingUniques(uniqueType, gameContext, checkCivInfoUniques).any()
} }
@Readonly
fun getTriggeredUniques( fun getTriggeredUniques(
trigger: UniqueType, trigger: UniqueType,
gameContext: GameContext = cache.state, gameContext: GameContext = cache.state,
@ -320,14 +324,16 @@ class MapUnit : IsPartOfGameInfoSerialization {
/** Gets *per turn* resource requirements - does not include immediate costs for stockpiled resources. /** Gets *per turn* resource requirements - does not include immediate costs for stockpiled resources.
* StateForConditionals is assumed to regarding this mapUnit*/ * StateForConditionals is assumed to regarding this mapUnit*/
@Readonly
fun getResourceRequirementsPerTurn(): Counter<String> { fun getResourceRequirementsPerTurn(): Counter<String> {
val resourceRequirements = Counter<String>() @LocalState val resourceRequirements = Counter<String>()
if (baseUnit.requiredResource != null) resourceRequirements[baseUnit.requiredResource!!] = 1 if (baseUnit.requiredResource != null) resourceRequirements[baseUnit.requiredResource!!] = 1
for (unique in getMatchingUniques(UniqueType.ConsumesResources, cache.state)) for (unique in getMatchingUniques(UniqueType.ConsumesResources, cache.state))
resourceRequirements[unique.params[1]] += unique.params[0].toInt() resourceRequirements.add(unique.params[1], unique.params[0].toInt())
return resourceRequirements return resourceRequirements
} }
@Readonly
fun requiresResource(resource: String): Boolean { fun requiresResource(resource: String): Boolean {
if (getResourceRequirementsPerTurn().contains(resource)) return true if (getResourceRequirementsPerTurn().contains(resource)) return true
for (unique in getMatchingUniques(UniqueType.CostsResources, cache.state)) { for (unique in getMatchingUniques(UniqueType.CostsResources, cache.state)) {
@ -336,8 +342,9 @@ class MapUnit : IsPartOfGameInfoSerialization {
return false return false
} }
fun hasMovement() = currentMovement > 0 @Readonly fun hasMovement() = currentMovement > 0
@Readonly
fun getMaxMovement(ignoreOtherUnit: Boolean = false): Int { fun getMaxMovement(ignoreOtherUnit: Boolean = false): Int {
var movement = var movement =
if (isEmbarked()) 2 if (isEmbarked()) 2
@ -364,6 +371,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
return movement return movement
} }
@Readonly
fun hasUnitMovedThisTurn(): Boolean { fun hasUnitMovedThisTurn(): Boolean {
val max = getMaxMovement().toFloat() val max = getMaxMovement().toFloat()
return currentMovement < max - max.ulp return currentMovement < max - max.ulp
@ -373,6 +381,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
* Determines this (land or sea) unit's current maximum vision range from unit properties, civ uniques and terrain. * Determines this (land or sea) unit's current maximum vision range from unit properties, civ uniques and terrain.
* @return Maximum distance of tiles this unit may possibly see * @return Maximum distance of tiles this unit may possibly see
*/ */
@Readonly
private fun getVisibilityRange(): Int { private fun getVisibilityRange(): Int {
var visibilityRange = 2 var visibilityRange = 2
@ -387,17 +396,20 @@ class MapUnit : IsPartOfGameInfoSerialization {
return visibilityRange return visibilityRange
} }
@Readonly
fun maxAttacksPerTurn(): Int { fun maxAttacksPerTurn(): Int {
return 1 + getMatchingUniques(UniqueType.AdditionalAttacks, checkCivInfoUniques = true) return 1 + getMatchingUniques(UniqueType.AdditionalAttacks, checkCivInfoUniques = true)
.sumOf { it.params[0].toInt() } .sumOf { it.params[0].toInt() }
} }
@Readonly
fun canAttack(): Boolean { fun canAttack(): Boolean {
if (!hasMovement()) return false if (!hasMovement()) return false
if (isCivilian()) return false if (isCivilian()) return false
return attacksThisTurn < maxAttacksPerTurn() return attacksThisTurn < maxAttacksPerTurn()
} }
@Readonly
fun getRange(): Int { fun getRange(): Int {
if (baseUnit.isMelee()) return 1 if (baseUnit.isMelee()) return 1
var range = baseUnit.range var range = baseUnit.range
@ -406,9 +418,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
return range return range
} }
fun getMaxMovementForAirUnits(): Int { @Readonly fun getMaxMovementForAirUnits(): Int = getRange() * 2
return getRange() * 2
}
@Readonly @Readonly
fun isEmbarked(): Boolean { fun isEmbarked(): Boolean {
@ -428,7 +438,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
return false return false
} }
@Readonly
fun canFortify(ignoreAlreadyFortified: Boolean = false) = when { fun canFortify(ignoreAlreadyFortified: Boolean = false) = when {
baseUnit.isWaterUnit -> false baseUnit.isWaterUnit -> false
isCivilian() -> false isCivilian() -> false
@ -440,10 +450,12 @@ class MapUnit : IsPartOfGameInfoSerialization {
else -> true else -> true
} }
@Readonly
private fun adjacentHealingBonus(): Int { private fun adjacentHealingBonus(): Int {
return getMatchingUniques(UniqueType.HealAdjacentUnits).sumOf { it.params[0].toInt() } return getMatchingUniques(UniqueType.HealAdjacentUnits).sumOf { it.params[0].toInt() }
} }
@Readonly
fun getHealAmountForCurrentTile() = when { fun getHealAmountForCurrentTile() = when {
isEmbarked() -> 0 // embarked units can't heal isEmbarked() -> 0 // embarked units can't heal
health >= 100 -> 0 // No need to heal if at max health health >= 100 -> 0 // No need to heal if at max health
@ -451,9 +463,10 @@ class MapUnit : IsPartOfGameInfoSerialization {
else -> rankTileForHealing(getTile()) else -> rankTileForHealing(getTile())
} }
fun canHealInCurrentTile() = getHealAmountForCurrentTile() > 0 @Readonly fun canHealInCurrentTile() = getHealAmountForCurrentTile() > 0
/** Returns the health points [MapUnit] will receive if healing on [tile] */ /** Returns the health points [MapUnit] will receive if healing on [tile] */
@Readonly
fun rankTileForHealing(tile: Tile): Int { fun rankTileForHealing(tile: Tile): Int {
val isFriendlyTerritory = tile.isFriendlyTerritory(civ) val isFriendlyTerritory = tile.isFriendlyTerritory(civ)
@ -499,20 +512,23 @@ class MapUnit : IsPartOfGameInfoSerialization {
@Readonly fun canGarrison() = isMilitary() && baseUnit.isLandUnit @Readonly fun canGarrison() = isMilitary() && baseUnit.isLandUnit
@Readonly fun isGreatPerson() = baseUnit.isGreatPerson @Readonly fun isGreatPerson() = baseUnit.isGreatPerson
fun isGreatPersonOfType(type: String) = baseUnit.isGreatPersonOfType(type) @Readonly fun isGreatPersonOfType(type: String) = baseUnit.isGreatPersonOfType(type)
@Readonly
fun canIntercept(attackedTile: Tile): Boolean { fun canIntercept(attackedTile: Tile): Boolean {
if (!canIntercept()) return false if (!canIntercept()) return false
if (currentTile.aerialDistanceTo(attackedTile) > getInterceptionRange()) return false if (currentTile.aerialDistanceTo(attackedTile) > getInterceptionRange()) return false
return true return true
} }
@Readonly
fun getInterceptionRange(): Int { fun getInterceptionRange(): Int {
val rangeFromUniques = getMatchingUniques(UniqueType.AirInterceptionRange, checkCivInfoUniques = true) val rangeFromUniques = getMatchingUniques(UniqueType.AirInterceptionRange, checkCivInfoUniques = true)
.sumOf { it.params[0].toInt() } .sumOf { it.params[0].toInt() }
return baseUnit.interceptRange + rangeFromUniques return baseUnit.interceptRange + rangeFromUniques
} }
@Readonly
fun canIntercept(): Boolean { fun canIntercept(): Boolean {
if (interceptChance() == 0) return false if (interceptChance() == 0) return false
// Air Units can only Intercept if they didn't move this turn // Air Units can only Intercept if they didn't move this turn
@ -524,15 +540,18 @@ class MapUnit : IsPartOfGameInfoSerialization {
return true return true
} }
@Readonly
fun interceptChance(): Int { fun interceptChance(): Int {
return getMatchingUniques(UniqueType.ChanceInterceptAirAttacks).sumOf { it.params[0].toInt() } return getMatchingUniques(UniqueType.ChanceInterceptAirAttacks).sumOf { it.params[0].toInt() }
} }
@Readonly
fun interceptDamagePercentBonus(): Int { fun interceptDamagePercentBonus(): Int {
return getMatchingUniques(UniqueType.DamageWhenIntercepting) return getMatchingUniques(UniqueType.DamageWhenIntercepting)
.sumOf { it.params[0].toInt() } .sumOf { it.params[0].toInt() }
} }
@Readonly
fun receivedInterceptDamageFactor(): Float { fun receivedInterceptDamageFactor(): Float {
var damageFactor = 1f var damageFactor = 1f
for (unique in getMatchingUniques(UniqueType.DamageFromInterceptionReduced)) for (unique in getMatchingUniques(UniqueType.DamageFromInterceptionReduced))
@ -540,16 +559,19 @@ class MapUnit : IsPartOfGameInfoSerialization {
return damageFactor return damageFactor
} }
@Readonly
fun getDamageFromTerrain(tile: Tile = currentTile): Int { fun getDamageFromTerrain(tile: Tile = currentTile): Int {
return tile.allTerrains.sumOf { it.damagePerTurn } return tile.allTerrains.sumOf { it.damagePerTurn }
} }
@Readonly
fun isTransportTypeOf(mapUnit: MapUnit): Boolean { fun isTransportTypeOf(mapUnit: MapUnit): Boolean {
// Currently, only missiles and airplanes can be carried // Currently, only missiles and airplanes can be carried
if (!mapUnit.baseUnit.movesLikeAirUnits) return false if (!mapUnit.baseUnit.movesLikeAirUnits) return false
return getMatchingUniques(UniqueType.CarryAirUnits).any { mapUnit.matchesFilter(it.params[1]) } return getMatchingUniques(UniqueType.CarryAirUnits).any { mapUnit.matchesFilter(it.params[1]) }
} }
@Readonly
private fun carryCapacity(unit: MapUnit): Int { private fun carryCapacity(unit: MapUnit): Int {
return (getMatchingUniques(UniqueType.CarryAirUnits) return (getMatchingUniques(UniqueType.CarryAirUnits)
+ getMatchingUniques(UniqueType.CarryExtraAirUnits)) + getMatchingUniques(UniqueType.CarryExtraAirUnits))
@ -557,6 +579,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
.sumOf { it.params[0].toInt() } .sumOf { it.params[0].toInt() }
} }
@Readonly
fun canTransport(unit: MapUnit): Boolean { fun canTransport(unit: MapUnit): Boolean {
if (owner != unit.owner) return false if (owner != unit.owner) return false
if (!isTransportTypeOf(unit)) return false if (!isTransportTypeOf(unit)) return false
@ -566,10 +589,12 @@ class MapUnit : IsPartOfGameInfoSerialization {
} }
/** Gets a Nuke's blast radius from the BlastRadius unique, defaulting to 2. No check whether the unit actually is a Nuke. */ /** Gets a Nuke's blast radius from the BlastRadius unique, defaulting to 2. No check whether the unit actually is a Nuke. */
@Readonly
fun getNukeBlastRadius() = getMatchingUniques(UniqueType.BlastRadius) fun getNukeBlastRadius() = getMatchingUniques(UniqueType.BlastRadius)
// Don't check conditionals as these are not supported // Don't check conditionals as these are not supported
.firstOrNull()?.params?.get(0)?.toInt() ?: 2 .firstOrNull()?.params?.get(0)?.toInt() ?: 2
@Readonly
private fun isAlly(otherCiv: Civilization): Boolean { private fun isAlly(otherCiv: Civilization): Boolean {
return otherCiv == civ return otherCiv == civ
|| (otherCiv.isCityState && otherCiv.getAllyCivName() == civ.civName) || (otherCiv.isCityState && otherCiv.getAllyCivName() == civ.civName)
@ -577,10 +602,12 @@ class MapUnit : IsPartOfGameInfoSerialization {
} }
/** Implements [UniqueParameterType.MapUnitFilter][com.unciv.models.ruleset.unique.UniqueParameterType.MapUnitFilter] */ /** Implements [UniqueParameterType.MapUnitFilter][com.unciv.models.ruleset.unique.UniqueParameterType.MapUnitFilter] */
@Readonly
fun matchesFilter(filter: String, multiFilter: Boolean = true): Boolean { fun matchesFilter(filter: String, multiFilter: Boolean = true): Boolean {
return if (multiFilter) MultiFilter.multiFilter(filter, ::matchesSingleFilter) else matchesSingleFilter(filter) return if (multiFilter) MultiFilter.multiFilter(filter, ::matchesSingleFilter) else matchesSingleFilter(filter)
} }
@Readonly
private fun matchesSingleFilter(filter: String): Boolean { private fun matchesSingleFilter(filter: String): Boolean {
return when (filter) { return when (filter) {
Constants.wounded, "wounded units" -> health < 100 Constants.wounded, "wounded units" -> health < 100
@ -599,6 +626,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
} }
} }
@Readonly
fun canBuildImprovement(improvement: TileImprovement, tile: Tile = currentTile): Boolean { fun canBuildImprovement(improvement: TileImprovement, tile: Tile = currentTile): Boolean {
if (civ.isBarbarian) return false if (civ.isBarbarian) return false
@ -617,6 +645,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
.any { improvement.matchesFilter(it.params[0], cache.state) || tile.matchesTerrainFilter(it.params[0], civ) } .any { improvement.matchesFilter(it.params[0], cache.state) || tile.matchesTerrainFilter(it.params[0], civ) }
} }
@Readonly
fun getReligionDisplayName(): String? { fun getReligionDisplayName(): String? {
if (religion == null) return null if (religion == null) return null
return civ.gameInfo.religions[religion]!!.getReligionDisplayName() return civ.gameInfo.religions[religion]!!.getReligionDisplayName()
@ -631,6 +660,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
return power return power
} }
@Readonly
fun getOtherEscortUnit(): MapUnit? { fun getOtherEscortUnit(): MapUnit? {
if (!::currentTile.isInitialized) return null // In some cases we might not have the unit placed on the map yet if (!::currentTile.isInitialized) return null // In some cases we might not have the unit placed on the map yet
if (isCivilian()) return getTile().militaryUnit if (isCivilian()) return getTile().militaryUnit
@ -638,14 +668,16 @@ class MapUnit : IsPartOfGameInfoSerialization {
return null return null
} }
@Readonly @Suppress("purity") // Updates escorting state
fun isEscorting(): Boolean { fun isEscorting(): Boolean {
if (escorting) { if (escorting) {
if (getOtherEscortUnit() != null) return true if (getOtherEscortUnit() != null) return true
escorting = false escorting = false
} }
return false return false
} }
@Readonly
fun threatensCiv(civInfo: Civilization): Boolean { fun threatensCiv(civInfo: Civilization): Boolean {
if (getTile().getOwner() == civInfo) if (getTile().getOwner() == civInfo)
return true return true
@ -1050,9 +1082,9 @@ class MapUnit : IsPartOfGameInfoSerialization {
movementMemories.removeAt(0) movementMemories.removeAt(0)
} }
} }
fun getStatus(name:String): UnitStatus? = statusMap[name] @Readonly fun getStatus(name:String): UnitStatus? = statusMap[name]
fun hasStatus(name:String): Boolean = getStatus(name) != null @Readonly fun hasStatus(name:String): Boolean = getStatus(name) != null
fun setStatus(name:String, turns:Int){ fun setStatus(name:String, turns:Int){
val existingStatus = getStatus(name) val existingStatus = getStatus(name)

View File

@ -261,7 +261,7 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
return null return null
} }
fun getCity(): City? = owningCity @Readonly fun getCity(): City? = owningCity
@Readonly internal fun getNaturalWonder(): Terrain = @Readonly internal fun getNaturalWonder(): Terrain =
if (naturalWonder == null) throw Exception("No natural wonder exists for this tile!") if (naturalWonder == null) throw Exception("No natural wonder exists for this tile!")
@ -281,7 +281,7 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
return exploredBy.contains(player.civName) return exploredBy.contains(player.civName)
} }
fun isCityCenter(): Boolean = isCityCenterInternal @Readonly fun isCityCenter(): Boolean = isCityCenterInternal
@Readonly fun isNaturalWonder(): Boolean = naturalWonder != null @Readonly fun isNaturalWonder(): Boolean = naturalWonder != null
@Readonly fun isImpassible() = lastTerrain.impassable @Readonly fun isImpassible() = lastTerrain.impassable

View File

@ -70,10 +70,12 @@ class Religion() : INamed, IsPartOfGameInfoSerialization {
updateUniqueMaps() updateUniqueMaps()
} }
@Readonly
fun getIconName() = fun getIconName() =
if (isPantheon()) "Pantheon" if (isPantheon()) "Pantheon"
else name else name
@Readonly
fun getReligionDisplayName() = fun getReligionDisplayName() =
if (displayName != null) displayName!! if (displayName != null) displayName!!
else name else name

View File

@ -22,11 +22,9 @@ class Technology: RulesetObject() {
var row: Int = 0 var row: Int = 0
var quote = "" var quote = ""
@Readonly @Readonly fun era(): String = column!!.era
fun era(): String = column!!.era
@Readonly @Readonly fun isContinuallyResearchable() = hasUnique(UniqueType.ResearchableMultipleTimes)
fun isContinuallyResearchable() = hasUnique(UniqueType.ResearchableMultipleTimes)
/** Get Civilization-specific description for TechPicker or AlertType.TechResearched */ /** Get Civilization-specific description for TechPicker or AlertType.TechResearched */
@ -40,6 +38,7 @@ class Technology: RulesetObject() {
override fun era(ruleset: Ruleset) = ruleset.eras[era()] override fun era(ruleset: Ruleset) = ruleset.eras[era()]
@Readonly
fun matchesFilter(filter: String, state: GameContext? = null, multiFilter: Boolean = true): Boolean { fun matchesFilter(filter: String, state: GameContext? = null, multiFilter: Boolean = true): Boolean {
return if (multiFilter) MultiFilter.multiFilter(filter, { return if (multiFilter) MultiFilter.multiFilter(filter, {
matchesSingleFilter(filter) || matchesSingleFilter(filter) ||
@ -50,7 +49,8 @@ class Technology: RulesetObject() {
state != null && hasUnique(filter, state) || state != null && hasUnique(filter, state) ||
state == null && hasTagUnique(filter) state == null && hasTagUnique(filter)
} }
@Readonly
fun matchesSingleFilter(filter: String): Boolean { fun matchesSingleFilter(filter: String): Boolean {
return when (filter) { return when (filter) {
in Constants.all -> true in Constants.all -> true

View File

@ -411,9 +411,10 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
} }
private val cachedMatchesFilterResult = HashMap<String, Boolean>() @LocalState private val cachedMatchesFilterResult = HashMap<String, Boolean>()
/** Implements [UniqueParameterType.BaseUnitFilter][com.unciv.models.ruleset.unique.UniqueParameterType.BaseUnitFilter] */ /** Implements [UniqueParameterType.BaseUnitFilter][com.unciv.models.ruleset.unique.UniqueParameterType.BaseUnitFilter] */
@Readonly
fun matchesFilter(filter: String, state: GameContext? = null, multiFilter: Boolean = true): Boolean { fun matchesFilter(filter: String, state: GameContext? = null, multiFilter: Boolean = true): Boolean {
return if (multiFilter) MultiFilter.multiFilter(filter, { return if (multiFilter) MultiFilter.multiFilter(filter, {
cachedMatchesFilterResult.getOrPut(it) { matchesSingleFilter(it) } || cachedMatchesFilterResult.getOrPut(it) { matchesSingleFilter(it) } ||
@ -424,8 +425,8 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
state != null && hasUnique(filter, state) || state != null && hasUnique(filter, state) ||
state == null && hasTagUnique(filter) state == null && hasTagUnique(filter)
} }
@Readonly
fun matchesSingleFilter(filter: String): Boolean { fun matchesSingleFilter(filter: String): Boolean {
// all cases are constants for performance // all cases are constants for performance
return when (filter) { return when (filter) {
@ -464,10 +465,10 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
/** Determine whether this is a City-founding unit - abstract, **without any game context**. /** Determine whether this is a City-founding unit - abstract, **without any game context**.
* Use other methods for MapUnits or when there is a better StateForConditionals available. */ * Use other methods for MapUnits or when there is a better StateForConditionals available. */
fun isCityFounder() = hasUnique(UniqueType.FoundCity, GameContext.IgnoreConditionals) @Readonly fun isCityFounder() = hasUnique(UniqueType.FoundCity, GameContext.IgnoreConditionals)
val isGreatPerson by lazy { getMatchingUniques(UniqueType.GreatPerson).any() } val isGreatPerson by lazy { getMatchingUniques(UniqueType.GreatPerson).any() }
fun isGreatPersonOfType(type: String) = getMatchingUniques(UniqueType.GreatPerson).any { it.params[0] == type } @Readonly fun isGreatPersonOfType(type: String) = getMatchingUniques(UniqueType.GreatPerson).any { it.params[0] == type }
/** Has a MapUnit implementation that does not ignore conditionals, which should be usually used */ /** Has a MapUnit implementation that does not ignore conditionals, which should be usually used */
@Readonly private fun isNuclearWeapon() = hasUnique(UniqueType.NuclearWeapon, GameContext.IgnoreConditionals) @Readonly private fun isNuclearWeapon() = hasUnique(UniqueType.NuclearWeapon, GameContext.IgnoreConditionals)