mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-22 19:08:48 -04:00
chore(purity): MapUnit
This commit is contained in:
parent
5fe0517c81
commit
be6216d2fb
@ -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"
|
||||
|
@ -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<Unique> = tempUniquesMap.getAllUniques()
|
||||
@Readonly fun getUniques(): Sequence<Unique> = 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<String> {
|
||||
val resourceRequirements = Counter<String>()
|
||||
@LocalState val resourceRequirements = Counter<String>()
|
||||
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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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] */
|
||||
@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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user