chore(purity): Many autodetected functions and friends 2

This commit is contained in:
yairm210 2025-08-05 00:09:36 +03:00
parent c8893723bf
commit 152acba973
13 changed files with 66 additions and 38 deletions

View File

@ -66,6 +66,9 @@ allprojects {
"kotlin.collections.mutableSetOf",
"kotlin.collections.withIndex", // applicable to sequence as well
"kotlin.collections.intersect",
"kotlin.collections.maxOfOrNull",
"kotlin.collections.minOfOrNull",
"kotlin.reflect.KMutableProperty0.get", // also 1 and 2
)
wellKnownPureClasses = setOf(
)

View File

@ -1,6 +1,7 @@
package com.unciv.logic
import com.unciv.models.translations.tr
import yairm210.purity.annotations.Readonly
/**
* An [Exception] wrapper marking an Exception as suitable to be shown to the user.
@ -27,6 +28,6 @@ class MissingModsException(
val missingMods: Iterable<String>
) : UncivShowableException("Missing mods: [${shorten(missingMods)}]") {
companion object {
private fun shorten(missingMods: Iterable<String>) = missingMods.joinToString(limit = 5) { it }
@Readonly private fun shorten(missingMods: Iterable<String>) = missingMods.joinToString(limit = 5) { it }
}
}

View File

@ -11,7 +11,6 @@ import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.INonPerpetualConstruction
import com.unciv.models.ruleset.PerpetualConstruction
import com.unciv.models.ruleset.nation.PersonalityValue
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.TileImprovement
import com.unciv.models.ruleset.unique.LocalUniqueCache
@ -21,6 +20,7 @@ import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats
import com.unciv.ui.screens.victoryscreen.RankingType
import yairm210.purity.annotations.Readonly
import kotlin.math.min
@ -406,6 +406,7 @@ object Automation {
return true
}
@Readonly
fun threatAssessment(assessor: Civilization, assessed: Civilization): ThreatLevel {
val powerLevelComparison =
assessed.getStatForRanking(RankingType.Force) / assessor.getStatForRanking(RankingType.Force).toFloat()

View File

@ -19,6 +19,7 @@ import com.unciv.logic.trade.TradeOfferType
import com.unciv.models.ruleset.nation.PersonalityValue
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.screens.victoryscreen.RankingType
import yairm210.purity.annotations.Readonly
import kotlin.math.abs
import kotlin.random.Random
@ -40,6 +41,7 @@ object DiplomacyAutomation {
}
}
@Readonly
internal fun wantsToSignDeclarationOfFrienship(civInfo: Civilization, otherCiv: Civilization): Boolean {
val diploManager = civInfo.getDiplomacyManager(otherCiv)!!
if (diploManager.hasFlag(DiplomacyFlags.DeclinedDeclarationOfFriendship)) return false
@ -187,6 +189,7 @@ object DiplomacyAutomation {
/**
* Test if [otherCiv] wants to accept our embassy in their capital
*/
@Readonly
fun wantsToAcceptEmbassy(civInfo: Civilization, otherCiv: Civilization): Boolean {
val theirDiploManager = otherCiv.getDiplomacyManager(civInfo)!!
if (civInfo.getDiplomacyManager(otherCiv)!!.hasFlag(DiplomacyFlags.DeclinedEmbassy)) return false
@ -211,6 +214,7 @@ object DiplomacyAutomation {
return true // Relationship is Afraid or greater
}
@Readonly
fun wantsToOpenBorders(civInfo: Civilization, otherCiv: Civilization): Boolean {
val ourDiploManager = civInfo.getDiplomacyManager(otherCiv)!!
if (ourDiploManager.hasFlag(DiplomacyFlags.DeclinedOpenBorders)) return false
@ -278,6 +282,7 @@ object DiplomacyAutomation {
}
}
@Readonly
fun wantsToSignDefensivePact(civInfo: Civilization, otherCiv: Civilization): Boolean {
val ourDiploManager = civInfo.getDiplomacyManager(otherCiv)!!
if (ourDiploManager.hasFlag(DiplomacyFlags.DeclinedDefensivePact)) return false
@ -450,6 +455,7 @@ object DiplomacyAutomation {
}
}
@Readonly
private fun areWeOfferingTrade(civInfo: Civilization, otherCiv: Civilization, offerName: String): Boolean {
return otherCiv.tradeRequests.filter { request -> request.requestingCiv == civInfo.civName }
.any { trade -> trade.trade.ourOffers.any { offer -> offer.name == offerName }

View File

@ -16,11 +16,13 @@ import com.unciv.models.ruleset.nation.PersonalityValue
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.ui.screens.victoryscreen.RankingType
import yairm210.purity.annotations.Readonly
object MotivationToAttackAutomation {
/** Will return the motivation to attack, but might short circuit if the value is guaranteed to
* be lower than `atLeast`. So any values below `atLeast` should not be used for comparison. */
@Readonly @Suppress("purity")
fun hasAtLeastMotivationToAttack(civInfo: Civilization, targetCiv: Civilization, atLeast: Float): Float {
val diplomacyManager = civInfo.getDiplomacyManager(targetCiv)!!
val personality = civInfo.getPersonality()

View File

@ -868,6 +868,7 @@ class Civilization : IsPartOfGameInfoSerialization {
}
}
@Readonly
fun hasStatToBuy(stat: Stat, price: Int): Boolean {
return when {
gameInfo.gameParameters.godMode -> true
@ -930,22 +931,6 @@ class Civilization : IsPartOfGameInfoSerialization {
}
}
fun getReserve(stat: GameResource): Int {
if (stat is TileResource && !stat.isCityWide && stat.isStockpiled)
return resourceStockpiles[stat.name]
return when (stat) {
Stat.Culture -> policies.storedCulture
Stat.Science -> {
if (tech.currentTechnology() == null) 0
else tech.researchOfTech(tech.currentTechnology()!!.name)
}
Stat.Gold -> gold
Stat.Faith -> religionManager.storedFaith
SubStat.GoldenAgePoints -> goldenAges.storedHappiness
else -> 0
}
}
// region addNotification
fun addNotification(text: String, category: NotificationCategory, vararg notificationIcons: String) =
addNotification(text, null, category, *notificationIcons)

View File

@ -255,16 +255,16 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
}
}
/** @see compareRelationshipLevel */
fun isRelationshipLevelEQ(level: RelationshipLevel) =
@Readonly fun isRelationshipLevelEQ(level: RelationshipLevel) =
compareRelationshipLevel(level, 0)
/** @see compareRelationshipLevel */
fun isRelationshipLevelLT(level: RelationshipLevel) =
@Readonly fun isRelationshipLevelLT(level: RelationshipLevel) =
compareRelationshipLevel(level, -1)
/** @see compareRelationshipLevel */
fun isRelationshipLevelGT(level: RelationshipLevel) =
@Readonly fun isRelationshipLevelGT(level: RelationshipLevel) =
compareRelationshipLevel(level, 1)
/** @see compareRelationshipLevel */
fun isRelationshipLevelLE(level: RelationshipLevel) =
@Readonly fun isRelationshipLevelLE(level: RelationshipLevel) =
if (level == RelationshipLevel.Ally) true
else compareRelationshipLevel(level + 1, -1)
/** @see compareRelationshipLevel */
@ -468,9 +468,9 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
}
/** Returns the [civilizations][Civilization] that know about both sides ([civInfo] and [otherCiv]) */
fun getCommonKnownCivs(): Set<Civilization> = civInfo.getKnownCivs().asIterable().intersect(otherCiv().getKnownCivs().toSet())
@Readonly fun getCommonKnownCivs(): Set<Civilization> = civInfo.getKnownCivs().asIterable().intersect(otherCiv().getKnownCivs().toSet())
fun getCommonKnownCivsWithSpectators(): Set<Civilization> = civInfo.getKnownCivsWithSpectators().asIterable().intersect(otherCiv().getKnownCivsWithSpectators().toSet())
@Readonly fun getCommonKnownCivsWithSpectators(): Set<Civilization> = civInfo.getKnownCivsWithSpectators().asIterable().intersect(otherCiv().getKnownCivsWithSpectators().toSet())
/** Returns true when the [civInfo]'s territory is considered allied for [otherCiv].
* This includes friendly and allied city-states and the open border treaties.
*/

View File

@ -5,6 +5,7 @@ import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
import com.unciv.ui.screens.victoryscreen.RankingType
import yairm210.purity.annotations.Readonly
/**
* Handles optimised operations related to finding threats or allies in an area.
@ -83,6 +84,7 @@ class ThreatManager(val civInfo: Civilization) {
* May be quicker than a manual search because of caching.
* Also ends up calculating and caching [getDistanceToClosestEnemyUnit].
*/
@Readonly @Suppress("purity")
fun getTilesWithEnemyUnitsInDistance(tile: Tile, maxDist: Int): MutableList<Tile> {
val tileData = distanceToClosestEnemyTiles[tile]
@ -129,19 +131,15 @@ class ThreatManager(val civInfo: Civilization) {
return tilesWithEnemies
}
/**
* Returns all enemy military units within maxDistance of the tile.
*/
fun getEnemyMilitaryUnitsInDistance(tile: Tile, maxDist: Int): List<MapUnit> =
getEnemyUnitsOnTiles(getTilesWithEnemyUnitsInDistance(tile, maxDist))
/**
* Returns all enemy military units on tiles
*/
@Readonly
fun getEnemyUnitsOnTiles(tilesWithEnemyUnitsInDistance:List<Tile>): List<MapUnit> =
tilesWithEnemyUnitsInDistance.flatMap { enemyTile -> enemyTile.getUnits()
.filter { it.isMilitary() && civInfo.isAtWarWith(it.civ) } }
@Readonly
fun getDangerousTiles(unit: MapUnit, distance: Int = 3): HashSet<Tile> {
val tilesWithEnemyUnits = getTilesWithEnemyUnitsInDistance(unit.getTile(), distance)
val nearbyRangedEnemyUnits = getEnemyUnitsOnTiles(tilesWithEnemyUnits)
@ -162,6 +160,7 @@ class ThreatManager(val civInfo: Civilization) {
/**
* Returns true if the tile has a visible enemy, otherwise returns false.
*/
@Readonly
fun doesTileHaveMilitaryEnemy(tile: Tile): Boolean {
if (!tile.isExplored(civInfo)) return false
if (tile.isCityCenter() && tile.getCity()!!.civ.isAtWarWith(civInfo)) return true
@ -174,13 +173,17 @@ class ThreatManager(val civInfo: Civilization) {
}
/** @return a sequence of pairs of cities, the first city is our city and the second city is a nearby city that is not from our civ. */
@Readonly
fun getNeighboringCitiesOfOtherCivs(): Sequence<Pair<City,City>> = civInfo.cities.flatMap {
ourCity -> ourCity.neighboringCities.filter { it.civ != civInfo }.map { Pair(ourCity, it) }
}.asSequence()
fun getNeighboringCivilizations(): Set<Civilization> = civInfo.cities.flatMap { it.neighboringCities }.filter { it.civ != civInfo && civInfo.knows(it.civ) }.map { it.civ }.toSet()
@Readonly fun getNeighboringCivilizations(): Set<Civilization> = civInfo.cities
.flatMap { it.neighboringCities }
.filter { it.civ != civInfo && civInfo.knows(it.civ) }
.map { it.civ }.toSet()
fun getCombinedForceOfWarringCivs(): Int = civInfo.getCivsAtWarWith().sumOf { it.getStatForRanking(RankingType.Force) }
@Readonly fun getCombinedForceOfWarringCivs(): Int = civInfo.getCivsAtWarWith().sumOf { it.getStatForRanking(RankingType.Force) }
fun clear() {
distanceToClosestEnemyTiles.clear()

View File

@ -15,6 +15,7 @@ import com.unciv.models.stats.Stat
import com.unciv.models.stats.StatMap
import com.unciv.models.stats.Stats
import com.unciv.ui.components.extensions.toPercent
import yairm210.purity.annotations.Readonly
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
@ -125,6 +126,7 @@ class CivInfoStatsForNextTurn(val civInfo: Civilization) {
return transportationUpkeep
}
@Readonly
fun getUnitSupply(): Int {
/* TotalSupply = BaseSupply + NumCities*modifier + Population*modifier
* In civ5, it seems population modifier is always 0.5, so i hardcoded it down below */
@ -135,15 +137,18 @@ class CivInfoStatsForNextTurn(val civInfo: Civilization) {
return supply
}
@Readonly
fun getBaseUnitSupply(): Int {
return civInfo.getDifficulty().unitSupplyBase +
civInfo.getMatchingUniques(UniqueType.BaseUnitSupply).sumOf { it.params[0].toInt() }
}
@Readonly
fun getUnitSupplyFromCities(): Int {
return civInfo.cities.size *
(civInfo.getDifficulty().unitSupplyPerCity
+ civInfo.getMatchingUniques(UniqueType.UnitSupplyPerCity).sumOf { it.params[0].toInt() })
}
@Readonly
fun getUnitSupplyFromPop(): Int {
var totalSupply = civInfo.cities.sumOf { it.population.population } * civInfo.gameInfo.ruleset.modOptions.constants.unitSupplyPerPopulation
@ -155,10 +160,10 @@ class CivInfoStatsForNextTurn(val civInfo: Civilization) {
}
return totalSupply.toInt()
}
fun getUnitSupplyDeficit(): Int = max(0,civInfo.units.getCivUnitsSize() - getUnitSupply())
@Readonly fun getUnitSupplyDeficit(): Int = max(0,civInfo.units.getCivUnitsSize() - getUnitSupply())
/** Per each supply missing, a player gets -10% production. Capped at -70%. */
fun getUnitSupplyProductionPenalty(): Float = -min(getUnitSupplyDeficit() * 10f, 70f)
@Readonly fun getUnitSupplyProductionPenalty(): Float = -min(getUnitSupplyDeficit() * 10f, 70f)
fun getStatMapForNextTurn(): StatMap {
val statMap = StatMap()

View File

@ -1,6 +1,7 @@
package com.unciv.logic.map
import com.unciv.logic.map.tile.Tile
import yairm210.purity.annotations.Readonly
import kotlin.collections.ArrayDeque
/**
@ -67,6 +68,7 @@ class BFS(
/**
* @return a Sequence from the [destination] back to the [startingPoint], including both, or empty if [destination] has not been reached
*/
@Readonly
fun getPathTo(destination: Tile): Sequence<Tile> = sequence {
var currentNode = destination
while (true) {

View File

@ -237,6 +237,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
@Readonly fun getTile(): Tile = currentTile
@Readonly
fun getClosestCity(): City? = civ.cities.minByOrNull {
it.getCenterTile().aerialDistanceTo(currentTile)
}

View File

@ -1,11 +1,12 @@
package com.unciv.models.ruleset.nation
import com.unciv.Constants
import com.unciv.logic.civilization.Civilization
import com.unciv.models.ruleset.RulesetObject
import com.unciv.models.ruleset.unique.UniqueTarget
import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats
import yairm210.purity.annotations.Pure
import yairm210.purity.annotations.Readonly
import kotlin.reflect.KMutableProperty0
/**
@ -65,6 +66,7 @@ class Personality: RulesetObject() {
var preferredVictoryType: String = Constants.neutralVictoryType
var isNeutralPersonality: Boolean = false
@Pure
private fun nameToVariable(value: PersonalityValue): KMutableProperty0<Float> {
return when(value) {
PersonalityValue.Production -> ::production
@ -95,6 +97,7 @@ class Personality: RulesetObject() {
/**
* Scales the value to a more meaningful range, where 10 is 2, and 5 is 1, and 0 is 0
*/
@Readonly
fun scaledFocus(value: PersonalityValue): Float {
return nameToVariable(value).get() / 5
}
@ -102,6 +105,7 @@ class Personality: RulesetObject() {
/**
* Inverse scales the value to a more meaningful range, where 0 is 2, and 5 is 1 and 10 is 0
*/
@Readonly
fun inverseScaledFocus(value: PersonalityValue): Float {
return (10 - nameToVariable(value).get()) / 5
}
@ -110,6 +114,7 @@ class Personality: RulesetObject() {
* @param weight a value between 0 and 1 that determines how much the modifier deviates from 1
* @return a modifier between 0 and 2 centered around 1 based off of the personality value and the weight given
*/
@Readonly
fun modifierFocus(value: PersonalityValue, weight: Float): Float {
return 1f + (scaledFocus(value) - 1) * weight
}
@ -119,6 +124,7 @@ class Personality: RulesetObject() {
* @param weight a value between 0 and 1 that determines how much the modifier deviates from 1
* @return a modifier between 0 and 2 centered around 1 based off of the personality value and the weight given
*/
@Readonly
fun inverseModifierFocus(value: PersonalityValue, weight: Float): Float {
return 1f - (inverseScaledFocus(value) - 2) * weight
}
@ -128,7 +134,7 @@ class Personality: RulesetObject() {
* @param weight a positive value that determines how much the personality should impact the stats given
*/
fun scaleStats(stats: Stats, weight: Float): Stats {
Stat.values().forEach { stats[it] *= modifierFocus(PersonalityValue[it], weight) }
Stat.entries.forEach { stats[it] *= modifierFocus(PersonalityValue[it], weight) }
return stats
}

View File

@ -9,19 +9,24 @@ import com.unciv.models.translations.removeConditionals
import com.unciv.models.translations.tr
import com.unciv.ui.components.fonts.FontRulesetIcons
import com.unciv.ui.components.fonts.Fonts
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly
import kotlin.math.ceil
object UnitActionModifiers {
@Readonly
fun canUse(unit: MapUnit, actionUnique: Unique): Boolean {
val usagesLeft = usagesLeft(unit, actionUnique)
return usagesLeft == null || usagesLeft > 0
}
@Readonly
fun getUsableUnitActionUniques(unit: MapUnit, actionUniqueType: UniqueType) =
unit.getMatchingUniques(actionUniqueType)
.filter { unique -> !unique.hasModifier(UniqueType.UnitActionExtraLimitedTimes) }
.filter { canUse(unit, it) }
@Readonly
private fun getMovementPointsToUse(unit: MapUnit, actionUnique: Unique, defaultAllMovement: Boolean = false): Int {
if (actionUnique.hasModifier(UniqueType.UnitActionMovementCostAll))
return unit.getMaxMovement()
@ -34,6 +39,7 @@ object UnitActionModifiers {
return if (defaultAllMovement) unit.getMaxMovement() else 1
}
@Readonly
private fun getMovementPointsRequired(actionUnique: Unique): Int {
if (actionUnique.hasModifier(UniqueType.UnitActionMovementCostAll))
return 1
@ -46,6 +52,7 @@ object UnitActionModifiers {
* going into the negatives
* @return Boolean
*/
@Readonly
private fun canSpendStatsCost(unit: MapUnit, actionUnique: Unique): Boolean {
for (conditional in actionUnique.getModifiers(UniqueType.UnitActionStatsCost)) {
for ((stat, value) in conditional.stats) {
@ -63,6 +70,7 @@ object UnitActionModifiers {
return true
}
@Readonly
private fun canSpendStockpileCost(unit: MapUnit, actionUnique: Unique): Boolean {
for (conditional in actionUnique.getModifiers(UniqueType.UnitActionStockpileCost)) {
val amount = conditional.params[0].toInt()
@ -79,6 +87,7 @@ object UnitActionModifiers {
* @param actionUnique: Unique that defines the Action
* @return Boolean
*/
@Readonly
fun canActivateSideEffects(unit: MapUnit, actionUnique: Unique): Boolean {
if (!canUse(unit, actionUnique)) return false
if (getMovementPointsRequired(actionUnique) > ceil(unit.currentMovement).toInt()) return false
@ -134,12 +143,14 @@ object UnitActionModifiers {
}
/** Returns 'null' if usages are not limited */
@Readonly
private fun usagesLeft(unit: MapUnit, actionUnique: Unique): Int?{
val usagesTotal = getMaxUsages(unit, actionUnique) ?: return null
val usagesSoFar = unit.abilityToTimesUsed[actionUnique.text.removeConditionals()] ?: 0
return usagesTotal - usagesSoFar
}
@Readonly
private fun getMaxUsages(unit: MapUnit, actionUnique: Unique): Int? {
val extraTimes = unit.getMatchingUniques(actionUnique.type!!)
.filter { it.text.removeConditionals() == actionUnique.text.removeConditionals() }
@ -154,12 +165,14 @@ object UnitActionModifiers {
return null
}
@Readonly
fun actionTextWithSideEffects(originalText: String, actionUnique: Unique, unit: MapUnit): String {
val sideEffectString = getSideEffectString(unit, actionUnique)
if (sideEffectString == "") return originalText
else return "{$originalText} $sideEffectString"
}
@Readonly
fun getSideEffectString(unit: MapUnit, actionUnique: Unique, defaultAllMovement: Boolean = false): String {
val effects = ArrayList<String>()
@ -167,7 +180,7 @@ object UnitActionModifiers {
if (maxUsages!=null) effects += "${usagesLeft(unit, actionUnique)}/$maxUsages"
if (actionUnique.hasModifier(UniqueType.UnitActionStatsCost)) {
val statCost = Stats()
@LocalState val statCost = Stats()
for (conditional in actionUnique.getModifiers(UniqueType.UnitActionStatsCost))
statCost.add(conditional.stats)
effects += statCost.toStringOnlyIcons(false)