Linting of MapUnit class (#10785)

* Reorganize - restore meaning to existing regions, sort fields into regions, treat warnings

* Reorganize - double-check pure/side-effect regions and move inappropriate placements
This commit is contained in:
SomeTroglodyte 2023-12-21 16:19:02 +01:00 committed by GitHub
parent 851ab2e7b8
commit beced92a69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -35,6 +35,62 @@ import kotlin.math.ulp
*/
class MapUnit : IsPartOfGameInfoSerialization {
//region Persisted fields
/** civName owning the unit */
lateinit var owner: String
/** civName of original owner - relevant for returning captured workers from barbarians */
var originalOwner: String? = null
/**
* Name key of the unit, used for serialization
*/
lateinit var name: String
/**
* Name of this individual unit, usually resulting from promotion
*/
var instanceName: String? = null
var currentMovement: Float = 0f
var health: Int = 100
// work, automation, fortifying, ...
// Connect roads implies automated is true. It is specified by the action type.
var action: String? = null
var automated: Boolean = false
var automatedRoadConnectionDestination: Vector2? = null
var automatedRoadConnectionPath: List<Vector2>? = null
var attacksThisTurn = 0
var promotions = UnitPromotions()
/** Indicates if unit should be located with 'next unit' action */
var due: Boolean = true
var isTransported: Boolean = false
var turnsFortified = 0
// New - track only *how many have been used*, derive max from uniques, left = max - used
var abilityToTimesUsed: HashMap<String, Int> = hashMapOf()
var religion: String? = null
var religiousStrengthLost = 0
/** FIFO list of this unit's past positions. Should never exceed two items in length. New item added once at end of turn and once at start, to allow rare between-turn movements like melee withdrawal to be distinguished. Used in movement arrow overlay. */
var movementMemories = ArrayList<UnitMovementMemory>()
/** The most recent type of position change this unit has experienced. Used in movement arrow overlay.*/
var mostRecentMoveType = UnitMovementMemoryType.UnitMoved
/** Array list of all the tiles that this unit has attacked since the start of its most recent turn. Used in movement arrow overlay. */
var attacksSinceTurnStart = ArrayList<Vector2>()
//endregion
//region Transient fields
@Transient
lateinit var civ: Civilization
@ -45,6 +101,9 @@ class MapUnit : IsPartOfGameInfoSerialization {
lateinit var currentTile: Tile
fun hasTile() = ::currentTile.isInitialized
@Transient
private var tempUniquesMap = UniqueMap()
@Transient
val movement = UnitMovement(this)
@ -63,21 +122,31 @@ class MapUnit : IsPartOfGameInfoSerialization {
@Transient
var viewableTiles = HashSet<Tile>()
/** civName owning the unit */
lateinit var owner: String
@Transient
var showAdditionalActions: Boolean = false
/** civName of original owner - relevant for returning captured workers from barbarians */
var originalOwner: String? = null
//endregion
/**
* Name key of the unit, used for serialization
*/
lateinit var name: String
* Container class to represent a single instant in a [MapUnit]'s recent movement history.
*
* @property position Position on the map at this instant, cloned on instantiation.
* @property type Category of the last change in position that brought the unit to this position.
* @see [movementMemories]
* */
class UnitMovementMemory(position: Vector2, val type: UnitMovementMemoryType) : IsPartOfGameInfoSerialization {
@Suppress("unused") // needed because this is part of a save and gets deserialized
constructor(): this(Vector2.Zero, UnitMovementMemoryType.UnitMoved)
val position = Vector2(position)
/**
* Name of this individual unit, usually resulting from promotion
*/
var instanceName: String? = null
fun clone() = UnitMovementMemory(position, type)
override fun toString() = "${this::class.simpleName}($position, $type)"
}
//region pure functions
// debug helper (please update comment if you see some "$unit" using this - MapUnit.label in WorkerAutomation is console log only as well)
override fun toString() = "$name - $owner"
/**
* Name which should be displayed in UI
@ -96,78 +165,9 @@ class MapUnit : IsPartOfGameInfoSerialization {
fun shortDisplayName(): String {
return if (instanceName != null) "[$instanceName]"
else "[$name]"
else "[$name]"
}
var currentMovement: Float = 0f
var health: Int = 100
// work, automation, fortifying, ...
// Connect roads implies automated is true. It is specified by the action type.
var action: String? = null
var automated: Boolean = false
var automatedRoadConnectionDestination: Vector2? = null
var automatedRoadConnectionPath: List<Vector2>? = null
@Transient
var showAdditionalActions: Boolean = false
var attacksThisTurn = 0
var promotions = UnitPromotions()
/** Indicates if unit should be located with 'next unit' action */
var due: Boolean = true
var isTransported: Boolean = false
var turnsFortified = 0
// New - track only *how many have been used*, derive max from uniques, left = max - used
var abilityToTimesUsed: HashMap<String, Int> = hashMapOf()
var religion: String? = null
var religiousStrengthLost = 0
/**
* Container class to represent a single instant in a [MapUnit]'s recent movement history.
*
* @property position Position on the map at this instant, cloned on instantiation.
* @property type Category of the last change in position that brought the unit to this position.
* @see [movementMemories]
* */
class UnitMovementMemory(position: Vector2, val type: UnitMovementMemoryType) : IsPartOfGameInfoSerialization {
@Suppress("unused") // needed because this is part of a save and gets deserialized
constructor(): this(Vector2.Zero, UnitMovementMemoryType.UnitMoved)
val position = Vector2(position)
fun clone() = UnitMovementMemory(position, type)
override fun toString() = "${this::class.simpleName}($position, $type)"
}
/** Deep clone an ArrayList of [UnitMovementMemory]s. */
private fun ArrayList<UnitMovementMemory>.copy() = ArrayList(this.map { it.clone() })
/** FIFO list of this unit's past positions. Should never exceed two items in length. New item added once at end of turn and once at start, to allow rare between-turn movements like melee withdrawal to be distinguished. Used in movement arrow overlay. */
var movementMemories = ArrayList<UnitMovementMemory>()
/** Add the current position and the most recent movement type to [movementMemories]. Called once at end and once at start of turn, and at unit creation. */
fun addMovementMemory() {
movementMemories.add(UnitMovementMemory(getTile().position, mostRecentMoveType))
while (movementMemories.size > 2) { // O(n) but n == 2.
// Keep at most one arrow segment— A lot of the time even that won't be rendered because the two positions will be the same.
// When in the unit's turn— I.E. For a player unit— The last two entries will be from .endTurn() followed by from .startTurn(), so the segment from .movementMemories will have zero length. Instead, what gets seen will be the segment from the end of .movementMemories to the unit's current position.
// When not in the unit's turn— I.E. For a foreign unit— The segment from the end of .movementMemories to the unit's current position will have zero length, while the last two entries here will be from .startTurn() followed by .endTurn(), so the segment here will be what gets shown.
// The exception is when a unit changes position when not in its turn, such as by melee withdrawal or foreign territory expulsion. Then the segment here and the segment from the end of here to the current position can both be shown.
movementMemories.removeFirst()
}
}
/** The most recent type of position change this unit has experienced. Used in movement arrow overlay.*/
var mostRecentMoveType = UnitMovementMemoryType.UnitMoved
/** Array list of all the tiles that this unit has attacked since the start of its most recent turn. Used in movement arrow overlay. */
var attacksSinceTurnStart = ArrayList<Vector2>()
//region pure functions
fun clone(): MapUnit {
val toReturn = MapUnit()
toReturn.baseUnit = baseUnit
@ -204,8 +204,45 @@ class MapUnit : IsPartOfGameInfoSerialization {
fun getTile(): Tile = currentTile
@Transient
private var tempUniquesMap = UniqueMap()
fun isMilitary() = baseUnit.isMilitary()
fun isCivilian() = baseUnit.isCivilian()
fun isActionUntilHealed() = action?.endsWith("until healed") == true
fun isFortified() = action?.startsWith(UnitActionType.Fortify.value) == true
fun isFortifyingUntilHealed() = isFortified() && isActionUntilHealed()
fun getFortificationTurns(): Int {
if (!isFortified()) return 0
return turnsFortified
}
fun isSleeping() = action?.startsWith(UnitActionType.Sleep.value) == true
fun isSleepingUntilHealed() = isSleeping() && isActionUntilHealed()
fun isMoving() = action?.startsWith("moveTo") == true
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
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
fun isIdle(): Boolean {
if (currentMovement == 0f) return false
val tile = getTile()
if (tile.improvementInProgress != null &&
canBuildImprovement(tile.getTileImprovementInProgress()!!) &&
!tile.isMarkedForCreatesOneImprovement()
) return false
return !(isFortified() || isExploring() || isSleeping() || isAutomated() || isMoving())
}
fun getUniques(): Sequence<Unique> = tempUniquesMap.values.asSequence().flatten()
@ -229,39 +266,14 @@ class MapUnit : IsPartOfGameInfoSerialization {
return getMatchingUniques(uniqueType, stateForConditionals, checkCivInfoUniques).any()
}
fun updateUniques() {
val uniques = ArrayList<Unique>()
val baseUnit = baseUnit()
uniques.addAll(baseUnit.uniqueObjects)
uniques.addAll(type.uniqueObjects)
for (promotion in promotions.getPromotions()) {
uniques.addAll(promotion.uniqueObjects)
fun getTriggeredUniques(
trigger: UniqueType,
stateForConditionals: StateForConditionals = StateForConditionals(civInfo = civ, unit = this)
): Sequence<Unique> {
return getUniques().filter { unique ->
unique.conditionals.any { it.type == trigger }
&& unique.conditionalsApply(stateForConditionals)
}
tempUniquesMap = UniqueMap().apply {
addUniques(uniques)
}
cache.updateUniques()
}
fun copyStatisticsTo(newUnit: MapUnit) {
newUnit.health = health
newUnit.instanceName = instanceName
newUnit.currentMovement = currentMovement
newUnit.attacksThisTurn = attacksThisTurn
newUnit.isTransported = isTransported
for (promotion in newUnit.promotions.promotions)
if (promotion !in promotions.promotions)
promotions.addPromotion(promotion, isFree = true)
newUnit.promotions = promotions.clone()
newUnit.automated = automated
newUnit.action = action // Needed too for Unit Overview action column
newUnit.updateUniques()
newUnit.updateVisibleTiles()
}
/** Gets *per turn* resource requirements - does not include immediate costs for stockpiled resources.
@ -328,88 +340,6 @@ class MapUnit : IsPartOfGameInfoSerialization {
return visibilityRange
}
/**
* Update this unit's cache of viewable tiles and its civ's as well.
*/
fun updateVisibleTiles(updateCivViewableTiles:Boolean = true, explorerPosition: Vector2? = null) {
val oldViewableTiles = viewableTiles
viewableTiles = when {
hasUnique(UniqueType.NoSight) -> hashSetOf()
hasUnique(UniqueType.CanSeeOverObstacles) ->
getTile().getTilesInDistance(getVisibilityRange()).toHashSet() // it's that simple
else -> getTile().getViewableTilesList(getVisibilityRange()).toHashSet()
}
// Set equality automatically determines if anything changed - https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-abstract-set/equals.html
if (updateCivViewableTiles && oldViewableTiles != viewableTiles
// Don't bother updating if all previous and current viewable tiles are within our borders
&& (oldViewableTiles.any { it !in civ.cache.ourTilesAndNeighboringTiles }
|| viewableTiles.any { it !in civ.cache.ourTilesAndNeighboringTiles })) {
val unfilteredTriggeredUniques = getTriggeredUniques(UniqueType.TriggerUponDiscoveringTile, StateForConditionals.IgnoreConditionals).toList()
if (unfilteredTriggeredUniques.isNotEmpty()) {
val newlyExploredTiles = viewableTiles.filter {
!it.isExplored(civ)
}
for (tile in newlyExploredTiles) {
// Include tile in the state for correct RNG seeding
val state = StateForConditionals(civInfo=civ, unit=this, tile=tile);
for (unique in unfilteredTriggeredUniques) {
if (unique.conditionals.any {
it.type == UniqueType.TriggerUponDiscoveringTile
&& tile.matchesFilter(it.params[0], civ)
} && unique.conditionalsApply(state)
)
UniqueTriggerActivation.triggerUnitwideUnique(unique, this)
}
}
}
civ.cache.updateViewableTiles(explorerPosition)
}
}
fun isActionUntilHealed() = action?.endsWith("until healed") == true
fun isFortified() = action?.startsWith(UnitActionType.Fortify.value) == true
fun isFortifyingUntilHealed() = isFortified() && isActionUntilHealed()
fun isSleeping() = action?.startsWith(UnitActionType.Sleep.value) == true
fun isSleepingUntilHealed() = isSleeping() && isActionUntilHealed()
fun isMoving() = action?.startsWith("moveTo") == true
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
fun isMilitary() = baseUnit.isMilitary()
fun isCivilian() = baseUnit.isCivilian()
fun getFortificationTurns(): Int {
if (!isFortified()) return 0
return turnsFortified
}
// debug helper (please update comment if you see some "$unit" using this)
override fun toString() = "$name - $owner"
fun isIdle(): Boolean {
if (currentMovement == 0f) return false
val tile = getTile()
if (tile.improvementInProgress != null &&
canBuildImprovement(tile.getTileImprovementInProgress()!!) &&
!tile.isMarkedForCreatesOneImprovement()
) return false
return !(isFortified() || isExploring() || isSleeping() || isAutomated() || isMoving())
}
fun maxAttacksPerTurn(): Int {
return 1 + getMatchingUniques(UniqueType.AdditionalAttacks, checkCivInfoUniques = true)
.sumOf { it.params[0].toInt() }
@ -460,18 +390,6 @@ class MapUnit : IsPartOfGameInfoSerialization {
return true
}
fun fortify() {
action = "Fortify"
}
fun fortifyUntilHealed() {
action = "Fortify until healed"
}
fun fortifyIfCan() {
if (canFortify()) fortify()
}
private fun adjacentHealingBonus(): Int {
return getMatchingUniques(UniqueType.HealAdjacentUnits).sumOf { it.params[0].toInt() }
}
@ -484,14 +402,194 @@ class MapUnit : IsPartOfGameInfoSerialization {
}
fun canHealInCurrentTile() = getHealAmountForCurrentTile() > 0
/** Returns the health points [MapUnit] will receive if healing on [tile] */
fun rankTileForHealing(tile: Tile): Int {
val isFriendlyTerritory = tile.isFriendlyTerritory(civ)
var healing = when {
tile.isCityCenter() -> 25
tile.isWater && isFriendlyTerritory && (baseUnit.isWaterUnit() || isTransported) -> 20 // Water unit on friendly water
tile.isWater && isFriendlyTerritory && cache.canMoveOnWater -> 20 // Treated as a water unit on friendly water
tile.isWater -> 0 // All other water cases
isFriendlyTerritory -> 20 // Allied territory
tile.getOwner() == null -> 10 // Neutral territory
else -> 10 // Enemy territory
}
@Suppress("KotlinConstantConditions") // Wrong warning. isWater **is** dynamic, set in Tile.setTerrainTransients
val mayHeal = healing > 0 || (tile.isWater && hasUnique(UniqueType.HealsOutsideFriendlyTerritory, checkCivInfoUniques = true))
@Suppress("KotlinConstantConditions") // Warning is right, but `return healing` reads nicer than `return 0`
if (!mayHeal) return healing
healing += getMatchingUniques(UniqueType.Heal, checkCivInfoUniques = true).sumOf { it.params[0].toInt() }
val healingCity = tile.getTilesInDistance(1).firstOrNull {
it.isCityCenter() && it.getCity()!!.getMatchingUniques(UniqueType.CityHealingUnits).any()
}?.getCity()
if (healingCity != null) {
for (unique in healingCity.getMatchingUniques(UniqueType.CityHealingUnits)) {
if (!matchesFilter(unique.params[0]) || !isAlly(healingCity.civ)) continue // only heal our units or allied units
healing += unique.params[1].toInt()
}
}
val maxAdjacentHealingBonus = currentTile.neighbors
.flatMap { it.getUnits() }.filter { it.civ == civ }
.map { it.adjacentHealingBonus() }.maxOrNull()
if (maxAdjacentHealingBonus != null)
healing += maxAdjacentHealingBonus
return healing
}
// Only military land units can truly "garrison"
fun canGarrison() = isMilitary() && baseUnit.isLandUnit()
fun isGreatPerson() = baseUnit.isGreatPerson()
fun isGreatPersonOfType(type: String) = baseUnit.isGreatPersonOfType(type)
//endregion
fun canIntercept(attackedTile: Tile): Boolean {
if (!canIntercept()) return false
if (currentTile.aerialDistanceTo(attackedTile) > getInterceptionRange()) return false
return true
}
fun getInterceptionRange():Int {
val rangeFromUniques = getMatchingUniques(UniqueType.AirInterceptionRange, checkCivInfoUniques = true)
.sumOf { it.params[0].toInt() }
return baseUnit.interceptRange + rangeFromUniques
}
fun canIntercept(): Boolean {
if (interceptChance() == 0) return false
// Air Units can only Intercept if they didn't move this turn
if (baseUnit.isAirUnit() && currentMovement == 0f) return false
val maxAttacksPerTurn = 1 +
getMatchingUniques(UniqueType.ExtraInterceptionsPerTurn)
.sumOf { it.params[0].toInt() }
if (attacksThisTurn >= maxAttacksPerTurn) return false
return true
}
fun interceptChance(): Int {
return getMatchingUniques(UniqueType.ChanceInterceptAirAttacks).sumOf { it.params[0].toInt() }
}
fun interceptDamagePercentBonus(): Int {
return getMatchingUniques(UniqueType.DamageWhenIntercepting)
.sumOf { it.params[0].toInt() }
}
fun receivedInterceptDamageFactor(): Float {
var damageFactor = 1f
for (unique in getMatchingUniques(UniqueType.DamageFromInterceptionReduced))
damageFactor *= 1f - unique.params[0].toFloat() / 100f
return damageFactor
}
fun getDamageFromTerrain(tile: Tile = currentTile): Int {
if (civ.nonStandardTerrainDamage) {
for (unique in getMatchingUniques(UniqueType.DamagesContainingUnits)) {
if (unique.params[0] in tile.allTerrains.map { it.name }) {
return unique.params[1].toInt() // Use the damage from the unique
}
}
}
// Otherwise fall back to the defined standard damage
return tile.allTerrains.sumOf { it.damagePerTurn }
}
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]) }
}
private fun carryCapacity(unit: MapUnit): Int {
return (getMatchingUniques(UniqueType.CarryAirUnits)
+ getMatchingUniques(UniqueType.CarryExtraAirUnits))
.filter { unit.matchesFilter(it.params[1]) }
.sumOf { it.params[0].toInt() }
}
fun canTransport(unit: MapUnit): Boolean {
if (owner != unit.owner) return false
if (!isTransportTypeOf(unit)) return false
if (unit.getMatchingUniques(UniqueType.CannotBeCarriedBy).any { matchesFilter(it.params[0]) }) return false
if (currentTile.airUnits.count { it.isTransported } >= carryCapacity(unit)) return false
return true
}
/** Gets a Nuke's blast radius from the BlastRadius unique, defaulting to 2. No check whether the unit actually is a Nuke. */
fun getNukeBlastRadius() = getMatchingUniques(UniqueType.BlastRadius)
// Don't check conditionals as these are not supported
.firstOrNull()?.params?.get(0)?.toInt() ?: 2
private fun isAlly(otherCiv: Civilization): Boolean {
return otherCiv == civ
|| (otherCiv.isCityState() && otherCiv.getAllyCiv() == civ.civName)
|| (civ.isCityState() && civ.getAllyCiv() == otherCiv.civName)
}
/** Implements [UniqueParameterType.MapUnitFilter][com.unciv.models.ruleset.unique.UniqueParameterType.MapUnitFilter] */
fun matchesFilter(filter: String): Boolean {
return MultiFilter.multiFilter(filter, ::matchesSingleFilter)
}
private fun matchesSingleFilter(filter:String): Boolean {
return when (filter) {
Constants.wounded, "wounded units" -> health < 100
Constants.barbarians, "Barbarian" -> civ.isBarbarian()
"City-State" -> civ.isCityState()
Constants.embarked -> isEmbarked()
"Non-City" -> true
else -> {
if (baseUnit.matchesFilter(filter)) return true
if (civ.nation.matchesFilter(filter)) return true
if (tempUniquesMap.containsKey(filter)) return true
return false
}
}
}
fun canBuildImprovement(improvement: TileImprovement, tile: Tile = currentTile): Boolean {
// Workers (and similar) should never be able to (instantly) construct things, only build them
// HOWEVER, they should be able to repair such things if they are pillaged
if (improvement.turnsToBuild == -1
&& improvement.name != Constants.cancelImprovementOrder
&& tile.improvementInProgress != improvement.name
) return false
val buildImprovementUniques = getMatchingUniques(UniqueType.BuildImprovements)
if (tile.improvementInProgress == Constants.repair && buildImprovementUniques.any()) return true
return buildImprovementUniques
.any { improvement.matchesFilter(it.params[0]) || tile.matchesTerrainFilter(it.params[0]) }
}
fun getReligionDisplayName(): String? {
if (religion == null) return null
return civ.gameInfo.religions[religion]!!.getReligionDisplayName()
}
fun getForceEvaluation(): Int {
val promotionBonus = (promotions.numberOfPromotions + 1).toFloat().pow(0.3f)
var power = (baseUnit.getForceEvaluation() * promotionBonus).toInt()
power *= health
power /= 100
return power
}
fun threatensCiv(civInfo: Civilization): Boolean {
if (getTile().getOwner() == civInfo)
return true
return getTile().neighbors.any { it.getOwner() == civInfo }
}
/** Deep clone an ArrayList of [UnitMovementMemory]s. */
private fun ArrayList<UnitMovementMemory>.copy() = ArrayList(this.map { it.clone() })
//endregion
//region state-changing functions
fun setTransients(ruleset: Ruleset) {
promotions.setTransients(this)
baseUnit = ruleset.units[name]
@ -501,9 +599,81 @@ class MapUnit : IsPartOfGameInfoSerialization {
if (action == UnitActionType.Automate.value) automated = true
}
fun getTriggeredUniques(trigger: UniqueType,
stateForConditionals: StateForConditionals = StateForConditionals(civInfo = civ, unit = this)): Sequence<Unique> {
return getUniques().filter { it.conditionals.any { it.type == trigger } && it.conditionalsApply(stateForConditionals) }
fun updateUniques() {
val uniques = ArrayList<Unique>()
val baseUnit = baseUnit()
uniques.addAll(baseUnit.uniqueObjects)
uniques.addAll(type.uniqueObjects)
for (promotion in promotions.getPromotions()) {
uniques.addAll(promotion.uniqueObjects)
}
tempUniquesMap = UniqueMap().apply {
addUniques(uniques)
}
cache.updateUniques()
}
fun copyStatisticsTo(newUnit: MapUnit) {
newUnit.health = health
newUnit.instanceName = instanceName
newUnit.currentMovement = currentMovement
newUnit.attacksThisTurn = attacksThisTurn
newUnit.isTransported = isTransported
for (promotion in newUnit.promotions.promotions)
if (promotion !in promotions.promotions)
promotions.addPromotion(promotion, isFree = true)
newUnit.promotions = promotions.clone()
newUnit.automated = automated
newUnit.action = action // Needed too for Unit Overview action column
newUnit.updateUniques()
newUnit.updateVisibleTiles()
}
/**
* Update this unit's cache of viewable tiles and its civ's as well.
*/
fun updateVisibleTiles(updateCivViewableTiles:Boolean = true, explorerPosition: Vector2? = null) {
val oldViewableTiles = viewableTiles
viewableTiles = when {
hasUnique(UniqueType.NoSight) -> hashSetOf()
hasUnique(UniqueType.CanSeeOverObstacles) ->
getTile().getTilesInDistance(getVisibilityRange()).toHashSet() // it's that simple
else -> getTile().getViewableTilesList(getVisibilityRange()).toHashSet()
}
// Set equality automatically determines if anything changed - https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-abstract-set/equals.html
if (updateCivViewableTiles && oldViewableTiles != viewableTiles
// Don't bother updating if all previous and current viewable tiles are within our borders
&& (oldViewableTiles.any { it !in civ.cache.ourTilesAndNeighboringTiles }
|| viewableTiles.any { it !in civ.cache.ourTilesAndNeighboringTiles })) {
val unfilteredTriggeredUniques = getTriggeredUniques(UniqueType.TriggerUponDiscoveringTile, StateForConditionals.IgnoreConditionals).toList()
if (unfilteredTriggeredUniques.isNotEmpty()) {
val newlyExploredTiles = viewableTiles.filter {
!it.isExplored(civ)
}
for (tile in newlyExploredTiles) {
// Include tile in the state for correct RNG seeding
val state = StateForConditionals(civInfo=civ, unit=this, tile=tile)
for (unique in unfilteredTriggeredUniques) {
if (unique.conditionals.any {
it.type == UniqueType.TriggerUponDiscoveringTile
&& tile.matchesFilter(it.params[0], civ)
} && unique.conditionalsApply(state)
)
UniqueTriggerActivation.triggerUnitwideUnique(unique, this)
}
}
}
civ.cache.updateViewableTiles(explorerPosition)
}
}
fun useMovementPoints(amount: Float) {
@ -512,10 +682,16 @@ class MapUnit : IsPartOfGameInfoSerialization {
if (currentMovement < 0) currentMovement = 0f
}
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 fortify() {
action = "Fortify"
}
fun fortifyUntilHealed() {
action = "Fortify until healed"
}
fun fortifyIfCan() {
if (canFortify()) fortify()
}
fun doAction() {
@ -563,44 +739,6 @@ class MapUnit : IsPartOfGameInfoSerialization {
if (health > 100) health = 100
}
/** Returns the health points [MapUnit] will receive if healing on [tile] */
fun rankTileForHealing(tile: Tile): Int {
val isFriendlyTerritory = tile.isFriendlyTerritory(civ)
var healing = when {
tile.isCityCenter() -> 25
tile.isWater && isFriendlyTerritory && (baseUnit.isWaterUnit() || isTransported) -> 20 // Water unit on friendly water
tile.isWater && isFriendlyTerritory && cache.canMoveOnWater -> 20 // Treated as a water unit on friendly water
tile.isWater -> 0 // All other water cases
isFriendlyTerritory -> 20 // Allied territory
tile.getOwner() == null -> 10 // Neutral territory
else -> 10 // Enemy territory
}
val mayHeal = healing > 0 || (tile.isWater && hasUnique(UniqueType.HealsOutsideFriendlyTerritory, checkCivInfoUniques = true))
if (!mayHeal) return healing
healing += getMatchingUniques(UniqueType.Heal, checkCivInfoUniques = true).sumOf { it.params[0].toInt() }
val healingCity = tile.getTilesInDistance(1).firstOrNull {
it.isCityCenter() && it.getCity()!!.getMatchingUniques(UniqueType.CityHealingUnits).any()
}?.getCity()
if (healingCity != null) {
for (unique in healingCity.getMatchingUniques(UniqueType.CityHealingUnits)) {
if (!matchesFilter(unique.params[0]) || !isAlly(healingCity.civ)) continue // only heal our units or allied units
healing += unique.params[1].toInt()
}
}
val maxAdjacentHealingBonus = currentTile.neighbors
.flatMap { it.getUnits() }.filter { it.civ == civ }
.map { it.adjacentHealingBonus() }.maxOrNull()
if (maxAdjacentHealingBonus != null)
healing += maxAdjacentHealingBonus
return healing
}
fun destroy(destroyTransportedUnit: Boolean = true) {
val currentPosition = Vector2(getTile().position)
civ.attacksSinceTurnStart.addAll(attacksSinceTurnStart.asSequence().map { Civilization.HistoricalAttackMemory(this.name, currentPosition, it) })
@ -662,6 +800,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
/** Return null if military on tile, or no civilian */
// Could be local to moveThroughTile, therefore left in the state-changing region
private fun Tile.getUnguardedCivilian(attacker: MapUnit): MapUnit? {
return when {
militaryUnit != null && militaryUnit != attacker -> null
@ -791,148 +930,21 @@ class MapUnit : IsPartOfGameInfoSerialization {
}
}
fun canIntercept(attackedTile: Tile): Boolean {
if (!canIntercept()) return false
if (currentTile.aerialDistanceTo(attackedTile) > getInterceptionRange()) return false
return true
}
fun getInterceptionRange():Int {
val rangeFromUniques = getMatchingUniques(UniqueType.AirInterceptionRange, checkCivInfoUniques = true)
.sumOf { it.params[0].toInt() }
return baseUnit.interceptRange + rangeFromUniques
}
fun canIntercept(): Boolean {
if (interceptChance() == 0) return false
// Air Units can only Intercept if they didn't move this turn
if (baseUnit.isAirUnit() && currentMovement == 0f) return false
val maxAttacksPerTurn = 1 +
getMatchingUniques(UniqueType.ExtraInterceptionsPerTurn)
.sumOf { it.params[0].toInt() }
if (attacksThisTurn >= maxAttacksPerTurn) return false
return true
}
fun interceptChance(): Int {
return getMatchingUniques(UniqueType.ChanceInterceptAirAttacks).sumOf { it.params[0].toInt() }
}
fun interceptDamagePercentBonus(): Int {
return getMatchingUniques(UniqueType.DamageWhenIntercepting)
.sumOf { it.params[0].toInt() }
}
fun receivedInterceptDamageFactor(): Float {
var damageFactor = 1f
for (unique in getMatchingUniques(UniqueType.DamageFromInterceptionReduced))
damageFactor *= 1f - unique.params[0].toFloat() / 100f
return damageFactor
}
fun getDamageFromTerrain(tile: Tile = currentTile): Int {
if (civ.nonStandardTerrainDamage) {
for (unique in getMatchingUniques(UniqueType.DamagesContainingUnits)) {
if (unique.params[0] in tile.allTerrains.map { it.name }) {
return unique.params[1].toInt() // Use the damage from the unique
}
}
}
// Otherwise fall back to the defined standard damage
return tile.allTerrains.sumOf { it.damagePerTurn }
}
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]) }
}
private fun carryCapacity(unit: MapUnit): Int {
return (getMatchingUniques(UniqueType.CarryAirUnits)
+ getMatchingUniques(UniqueType.CarryExtraAirUnits))
.filter { unit.matchesFilter(it.params[1]) }
.sumOf { it.params[0].toInt() }
}
fun canTransport(unit: MapUnit): Boolean {
if (owner != unit.owner) return false
if (!isTransportTypeOf(unit)) return false
if (unit.getMatchingUniques(UniqueType.CannotBeCarriedBy).any { matchesFilter(it.params[0]) }) return false
if (currentTile.airUnits.count { it.isTransported } >= carryCapacity(unit)) return false
return true
}
/** Gets a Nuke's blast radius from the BlastRadius unique, defaulting to 2. No check whether the unit actually is a Nuke. */
fun getNukeBlastRadius() = getMatchingUniques(UniqueType.BlastRadius)
// Don't check conditionals as these are not supported
.firstOrNull()?.params?.get(0)?.toInt() ?: 2
private fun isAlly(otherCiv: Civilization): Boolean {
return otherCiv == civ
|| (otherCiv.isCityState() && otherCiv.getAllyCiv() == civ.civName)
|| (civ.isCityState() && civ.getAllyCiv() == otherCiv.civName)
}
/** Implements [UniqueParameterType.MapUnitFilter][com.unciv.models.ruleset.unique.UniqueParameterType.MapUnitFilter] */
fun matchesFilter(filter: String): Boolean {
return MultiFilter.multiFilter(filter, ::matchesSingleFilter)
}
private fun matchesSingleFilter(filter:String): Boolean {
return when (filter) {
Constants.wounded, "wounded units" -> health < 100
Constants.barbarians, "Barbarian" -> civ.isBarbarian()
"City-State" -> civ.isCityState()
Constants.embarked -> isEmbarked()
"Non-City" -> true
else -> {
if (baseUnit.matchesFilter(filter)) return true
if (civ.nation.matchesFilter(filter)) return true
if (tempUniquesMap.containsKey(filter)) return true
return false
}
}
}
fun canBuildImprovement(improvement: TileImprovement, tile: Tile = currentTile): Boolean {
// Workers (and similar) should never be able to (instantly) construct things, only build them
// HOWEVER, they should be able to repair such things if they are pillaged
if (improvement.turnsToBuild == -1
&& improvement.name != Constants.cancelImprovementOrder
&& tile.improvementInProgress != improvement.name
) return false
val buildImprovementUniques = getMatchingUniques(UniqueType.BuildImprovements)
if (tile.improvementInProgress == Constants.repair && buildImprovementUniques.any()) return true
return buildImprovementUniques
.any { improvement.matchesFilter(it.params[0]) || tile.matchesTerrainFilter(it.params[0]) }
}
fun getReligionDisplayName(): String? {
if (religion == null) return null
return civ.gameInfo.religions[religion]!!.getReligionDisplayName()
}
fun actionsOnDeselect() {
showAdditionalActions = false
if (isPreparingParadrop() || isPreparingAirSweep()) action = null
}
fun getForceEvaluation(): Int {
val promotionBonus = (promotions.numberOfPromotions + 1).toFloat().pow(0.3f)
var power = (baseUnit.getForceEvaluation() * promotionBonus).toInt()
power *= health
power /= 100
return power
}
fun threatensCiv(civInfo: Civilization): Boolean {
if (getTile().getOwner() == civInfo)
return true
return getTile().neighbors.any { it.getOwner() == civInfo }
/** Add the current position and the most recent movement type to [movementMemories]. Called once at end and once at start of turn, and at unit creation. */
fun addMovementMemory() {
movementMemories.add(UnitMovementMemory(getTile().position, mostRecentMoveType))
while (movementMemories.size > 2) { // O(n) but n == 2.
// Keep at most one arrow segment— A lot of the time even that won't be rendered because the two positions will be the same.
// When in the unit's turn— I.E. For a player unit— The last two entries will be from .endTurn() followed by from .startTurn(), so the segment from .movementMemories will have zero length. Instead, what gets seen will be the segment from the end of .movementMemories to the unit's current position.
// When not in the unit's turn— I.E. For a foreign unit— The segment from the end of .movementMemories to the unit's current position will have zero length, while the last two entries here will be from .startTurn() followed by .endTurn(), so the segment here will be what gets shown.
// The exception is when a unit changes position when not in its turn, such as by melee withdrawal or foreign territory expulsion. Then the segment here and the segment from the end of here to the current position can both be shown.
movementMemories.removeFirst()
}
}
//endregion