diff --git a/build.gradle.kts b/build.gradle.kts index ccb0fc9889..a16ef43e85 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -56,6 +56,7 @@ allprojects { "com.badlogic.gdx.math.Vector2.len", "com.badlogic.gdx.math.Vector2.cpy", "kotlin.collections.Collection.contains", + "kotlin.collections.dropLastWhile", ) wellKnownPureClasses = setOf( "java.text.NumberFormat" diff --git a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt index aeae10638d..3b6b0d79d5 100644 --- a/core/src/com/unciv/logic/map/mapunit/MapUnit.kt +++ b/core/src/com/unciv/logic/map/mapunit/MapUnit.kt @@ -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.LocalState import yairm210.purity.annotations.Readonly import java.text.DecimalFormat import kotlin.math.pow @@ -252,28 +253,30 @@ class MapUnit : IsPartOfGameInfoSerialization { return turnsFortified } - fun isSleeping() = action?.startsWith(UnitActionType.Sleep.value) == true - fun isSleepingUntilHealed() = isSleeping() && isActionUntilHealed() + @Readonly fun isSleeping() = action?.startsWith(UnitActionType.Sleep.value) == true + @Readonly fun isSleepingUntilHealed() = isSleeping() && isActionUntilHealed() - fun isMoving() = action?.startsWith("moveTo") == true + @Readonly fun isMoving() = action?.startsWith("moveTo") == true + @Readonly fun getMovementDestination(): Tile { val destination = action!!.replace("moveTo ", "").split(",").dropLastWhile { it.isEmpty() } val destinationVector = Vector2(destination[0].toFloat(), destination[1].toFloat()) return currentTile.tileMap[destinationVector] } - fun isAutomated() = automated + @Readonly fun isAutomated() = automated - fun isAutomatingRoadConnection() = action == UnitActionType.ConnectRoad.value - fun isExploring() = action == UnitActionType.Explore.value - fun isPreparingParadrop() = action == UnitActionType.Paradrop.value - fun isPreparingAirSweep() = action == UnitActionType.AirSweep.value - fun isSetUpForSiege() = action == UnitActionType.SetUp.value + @Readonly fun isAutomatingRoadConnection() = action == UnitActionType.ConnectRoad.value + @Readonly fun isExploring() = action == UnitActionType.Explore.value + @Readonly fun isPreparingParadrop() = action == UnitActionType.Paradrop.value + @Readonly fun isPreparingAirSweep() = action == UnitActionType.AirSweep.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 * Leave it as default unless you know what [isIdle] does. */ + @Readonly fun isIdle(includeOtherEscortUnit: Boolean = true): Boolean { if (!hasMovement()) return false val tile = getTile() @@ -285,7 +288,7 @@ class MapUnit : IsPartOfGameInfoSerialization { return !(isFortified() || isExploring() || isSleeping() || isAutomated() || isMoving() || isGuarding()) } - fun getUniques(): Sequence = tempUniquesMap.getAllUniques() + @Readonly fun getUniques(): Sequence = tempUniquesMap.getAllUniques() @Readonly fun getMatchingUniques( @@ -309,6 +312,7 @@ class MapUnit : IsPartOfGameInfoSerialization { return getMatchingUniques(uniqueType, gameContext, checkCivInfoUniques).any() } + @Readonly fun getTriggeredUniques( trigger: UniqueType, gameContext: GameContext = cache.state, @@ -320,14 +324,16 @@ class MapUnit : IsPartOfGameInfoSerialization { /** Gets *per turn* resource requirements - does not include immediate costs for stockpiled resources. * StateForConditionals is assumed to regarding this mapUnit*/ + @Readonly fun getResourceRequirementsPerTurn(): Counter { - val resourceRequirements = Counter() + @LocalState val resourceRequirements = Counter() if (baseUnit.requiredResource != null) resourceRequirements[baseUnit.requiredResource!!] = 1 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 } + @Readonly fun requiresResource(resource: String): Boolean { if (getResourceRequirementsPerTurn().contains(resource)) return true for (unique in getMatchingUniques(UniqueType.CostsResources, cache.state)) { @@ -336,8 +342,9 @@ class MapUnit : IsPartOfGameInfoSerialization { return false } - fun hasMovement() = currentMovement > 0 + @Readonly fun hasMovement() = currentMovement > 0 + @Readonly fun getMaxMovement(ignoreOtherUnit: Boolean = false): Int { var movement = if (isEmbarked()) 2 @@ -364,6 +371,7 @@ class MapUnit : IsPartOfGameInfoSerialization { return movement } + @Readonly fun hasUnitMovedThisTurn(): Boolean { val max = getMaxMovement().toFloat() 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. * @return Maximum distance of tiles this unit may possibly see */ + @Readonly private fun getVisibilityRange(): Int { var visibilityRange = 2 @@ -387,17 +396,20 @@ class MapUnit : IsPartOfGameInfoSerialization { return visibilityRange } + @Readonly fun maxAttacksPerTurn(): Int { return 1 + getMatchingUniques(UniqueType.AdditionalAttacks, checkCivInfoUniques = true) .sumOf { it.params[0].toInt() } } + @Readonly fun canAttack(): Boolean { if (!hasMovement()) return false if (isCivilian()) return false return attacksThisTurn < maxAttacksPerTurn() } + @Readonly fun getRange(): Int { if (baseUnit.isMelee()) return 1 var range = baseUnit.range @@ -406,9 +418,7 @@ class MapUnit : IsPartOfGameInfoSerialization { return range } - fun getMaxMovementForAirUnits(): Int { - return getRange() * 2 - } + @Readonly fun getMaxMovementForAirUnits(): Int = getRange() * 2 @Readonly fun isEmbarked(): Boolean { @@ -428,7 +438,7 @@ class MapUnit : IsPartOfGameInfoSerialization { return false } - + @Readonly fun canFortify(ignoreAlreadyFortified: Boolean = false) = when { baseUnit.isWaterUnit -> false isCivilian() -> false @@ -440,10 +450,12 @@ class MapUnit : IsPartOfGameInfoSerialization { else -> true } + @Readonly private fun adjacentHealingBonus(): Int { return getMatchingUniques(UniqueType.HealAdjacentUnits).sumOf { it.params[0].toInt() } } - + + @Readonly fun getHealAmountForCurrentTile() = when { isEmbarked() -> 0 // embarked units can't heal health >= 100 -> 0 // No need to heal if at max health @@ -451,9 +463,10 @@ class MapUnit : IsPartOfGameInfoSerialization { else -> rankTileForHealing(getTile()) } - fun canHealInCurrentTile() = getHealAmountForCurrentTile() > 0 + @Readonly fun canHealInCurrentTile() = getHealAmountForCurrentTile() > 0 /** Returns the health points [MapUnit] will receive if healing on [tile] */ + @Readonly fun rankTileForHealing(tile: Tile): Int { val isFriendlyTerritory = tile.isFriendlyTerritory(civ) @@ -499,20 +512,23 @@ class MapUnit : IsPartOfGameInfoSerialization { @Readonly fun canGarrison() = isMilitary() && baseUnit.isLandUnit @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 { if (!canIntercept()) return false if (currentTile.aerialDistanceTo(attackedTile) > getInterceptionRange()) return false return true } + @Readonly fun getInterceptionRange(): Int { val rangeFromUniques = getMatchingUniques(UniqueType.AirInterceptionRange, checkCivInfoUniques = true) .sumOf { it.params[0].toInt() } return baseUnit.interceptRange + rangeFromUniques } + @Readonly fun canIntercept(): Boolean { if (interceptChance() == 0) return false // Air Units can only Intercept if they didn't move this turn @@ -524,15 +540,18 @@ class MapUnit : IsPartOfGameInfoSerialization { return true } + @Readonly fun interceptChance(): Int { return getMatchingUniques(UniqueType.ChanceInterceptAirAttacks).sumOf { it.params[0].toInt() } } + @Readonly fun interceptDamagePercentBonus(): Int { return getMatchingUniques(UniqueType.DamageWhenIntercepting) .sumOf { it.params[0].toInt() } } + @Readonly fun receivedInterceptDamageFactor(): Float { var damageFactor = 1f for (unique in getMatchingUniques(UniqueType.DamageFromInterceptionReduced)) @@ -540,16 +559,19 @@ class MapUnit : IsPartOfGameInfoSerialization { return damageFactor } + @Readonly fun getDamageFromTerrain(tile: Tile = currentTile): Int { return tile.allTerrains.sumOf { it.damagePerTurn } } + @Readonly fun isTransportTypeOf(mapUnit: MapUnit): Boolean { // Currently, only missiles and airplanes can be carried if (!mapUnit.baseUnit.movesLikeAirUnits) return false return getMatchingUniques(UniqueType.CarryAirUnits).any { mapUnit.matchesFilter(it.params[1]) } } + @Readonly private fun carryCapacity(unit: MapUnit): Int { return (getMatchingUniques(UniqueType.CarryAirUnits) + getMatchingUniques(UniqueType.CarryExtraAirUnits)) @@ -557,6 +579,7 @@ class MapUnit : IsPartOfGameInfoSerialization { .sumOf { it.params[0].toInt() } } + @Readonly fun canTransport(unit: MapUnit): Boolean { if (owner != unit.owner) 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. */ + @Readonly fun getNukeBlastRadius() = getMatchingUniques(UniqueType.BlastRadius) // Don't check conditionals as these are not supported .firstOrNull()?.params?.get(0)?.toInt() ?: 2 + @Readonly private fun isAlly(otherCiv: Civilization): Boolean { return otherCiv == civ || (otherCiv.isCityState && otherCiv.getAllyCivName() == civ.civName) @@ -577,10 +602,12 @@ class MapUnit : IsPartOfGameInfoSerialization { } /** Implements [UniqueParameterType.MapUnitFilter][com.unciv.models.ruleset.unique.UniqueParameterType.MapUnitFilter] */ + @Readonly fun matchesFilter(filter: String, multiFilter: Boolean = true): Boolean { return if (multiFilter) MultiFilter.multiFilter(filter, ::matchesSingleFilter) else matchesSingleFilter(filter) } + @Readonly private fun matchesSingleFilter(filter: String): Boolean { return when (filter) { Constants.wounded, "wounded units" -> health < 100 @@ -599,6 +626,7 @@ class MapUnit : IsPartOfGameInfoSerialization { } } + @Readonly fun canBuildImprovement(improvement: TileImprovement, tile: Tile = currentTile): Boolean { 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) } } + @Readonly fun getReligionDisplayName(): String? { if (religion == null) return null return civ.gameInfo.religions[religion]!!.getReligionDisplayName() @@ -631,6 +660,7 @@ class MapUnit : IsPartOfGameInfoSerialization { return power } + @Readonly fun getOtherEscortUnit(): MapUnit? { if (!::currentTile.isInitialized) return null // In some cases we might not have the unit placed on the map yet if (isCivilian()) return getTile().militaryUnit @@ -638,14 +668,16 @@ class MapUnit : IsPartOfGameInfoSerialization { return null } + @Readonly @Suppress("purity") // Updates escorting state fun isEscorting(): Boolean { - if (escorting) { + if (escorting) { if (getOtherEscortUnit() != null) return true escorting = false } return false } + @Readonly fun threatensCiv(civInfo: Civilization): Boolean { if (getTile().getOwner() == civInfo) return true @@ -1050,9 +1082,9 @@ class MapUnit : IsPartOfGameInfoSerialization { movementMemories.removeAt(0) } } - - fun getStatus(name:String): UnitStatus? = statusMap[name] - fun hasStatus(name:String): Boolean = getStatus(name) != null + + @Readonly fun getStatus(name:String): UnitStatus? = statusMap[name] + @Readonly fun hasStatus(name:String): Boolean = getStatus(name) != null fun setStatus(name:String, turns:Int){ val existingStatus = getStatus(name) diff --git a/core/src/com/unciv/logic/map/tile/Tile.kt b/core/src/com/unciv/logic/map/tile/Tile.kt index b79aa6aab9..8053344ffb 100644 --- a/core/src/com/unciv/logic/map/tile/Tile.kt +++ b/core/src/com/unciv/logic/map/tile/Tile.kt @@ -261,7 +261,7 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable { return null } - fun getCity(): City? = owningCity + @Readonly fun getCity(): City? = owningCity @Readonly internal fun getNaturalWonder(): Terrain = 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) } - fun isCityCenter(): Boolean = isCityCenterInternal + @Readonly fun isCityCenter(): Boolean = isCityCenterInternal @Readonly fun isNaturalWonder(): Boolean = naturalWonder != null @Readonly fun isImpassible() = lastTerrain.impassable diff --git a/core/src/com/unciv/models/Religion.kt b/core/src/com/unciv/models/Religion.kt index 68a7fa4c0b..65888b4f20 100644 --- a/core/src/com/unciv/models/Religion.kt +++ b/core/src/com/unciv/models/Religion.kt @@ -70,10 +70,12 @@ class Religion() : INamed, IsPartOfGameInfoSerialization { updateUniqueMaps() } + @Readonly fun getIconName() = if (isPantheon()) "Pantheon" else name + @Readonly fun getReligionDisplayName() = if (displayName != null) displayName!! else name diff --git a/core/src/com/unciv/models/ruleset/tech/Technology.kt b/core/src/com/unciv/models/ruleset/tech/Technology.kt index 4a28e4ec2c..6c66a82e95 100644 --- a/core/src/com/unciv/models/ruleset/tech/Technology.kt +++ b/core/src/com/unciv/models/ruleset/tech/Technology.kt @@ -22,11 +22,9 @@ class Technology: RulesetObject() { var row: Int = 0 var quote = "" - @Readonly - fun era(): String = column!!.era + @Readonly fun era(): String = column!!.era - @Readonly - fun isContinuallyResearchable() = hasUnique(UniqueType.ResearchableMultipleTimes) + @Readonly fun isContinuallyResearchable() = hasUnique(UniqueType.ResearchableMultipleTimes) /** Get Civilization-specific description for TechPicker or AlertType.TechResearched */ @@ -40,6 +38,7 @@ class Technology: RulesetObject() { override fun era(ruleset: Ruleset) = ruleset.eras[era()] + @Readonly fun matchesFilter(filter: String, state: GameContext? = null, multiFilter: Boolean = true): Boolean { return if (multiFilter) MultiFilter.multiFilter(filter, { matchesSingleFilter(filter) || @@ -50,7 +49,8 @@ class Technology: RulesetObject() { state != null && hasUnique(filter, state) || state == null && hasTagUnique(filter) } - + + @Readonly fun matchesSingleFilter(filter: String): Boolean { return when (filter) { in Constants.all -> true diff --git a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt index db0a5ab2f8..d486bd241c 100644 --- a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt +++ b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt @@ -411,9 +411,10 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { } - private val cachedMatchesFilterResult = HashMap() + @LocalState private val cachedMatchesFilterResult = HashMap() /** Implements [UniqueParameterType.BaseUnitFilter][com.unciv.models.ruleset.unique.UniqueParameterType.BaseUnitFilter] */ + @Readonly fun matchesFilter(filter: String, state: GameContext? = null, multiFilter: Boolean = true): Boolean { return if (multiFilter) MultiFilter.multiFilter(filter, { cachedMatchesFilterResult.getOrPut(it) { matchesSingleFilter(it) } || @@ -424,8 +425,8 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { state != null && hasUnique(filter, state) || state == null && hasTagUnique(filter) } - - + + @Readonly fun matchesSingleFilter(filter: String): Boolean { // all cases are constants for performance return when (filter) { @@ -464,10 +465,10 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { /** 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. */ - fun isCityFounder() = hasUnique(UniqueType.FoundCity, GameContext.IgnoreConditionals) + @Readonly fun isCityFounder() = hasUnique(UniqueType.FoundCity, GameContext.IgnoreConditionals) 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 */ @Readonly private fun isNuclearWeapon() = hasUnique(UniqueType.NuclearWeapon, GameContext.IgnoreConditionals)