mirror of
https://github.com/yairm210/Unciv.git
synced 2025-01-07 14:02:48 +07:00
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:
parent
851ab2e7b8
commit
beced92a69
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user