Double movement unique parameterized (#5319)

* Double movement unique parameterized

* Double movement unique - all filters
This commit is contained in:
SomeTroglodyte 2021-09-27 11:35:38 +02:00 committed by GitHub
parent 96511e16ef
commit 2e72fd52c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 209 additions and 93 deletions

View File

@ -132,9 +132,7 @@
{ {
"name": "Woodsman", "name": "Woodsman",
"prerequisites": ["Shock III","Drill III"], "prerequisites": ["Shock III","Drill III"],
"uniques": ["Double movement rate through Forest and Jungle"], "uniques": ["Double movement in [Forest]","Double movement in [Jungle]"],
// This could be generalized: ["-[50]% movement costs through [Forest] tiles", "-[50]% movement costs through [Jungle] tiles"],
// but with how getMovementCostBetweenAdjacentTiles() is optimized, that's difficult to implement.
"unitTypes": ["Sword","Gunpowder"] "unitTypes": ["Sword","Gunpowder"]
}, },
{ {

View File

@ -1011,7 +1011,12 @@
"requiredTech": "Rifling", "requiredTech": "Rifling",
"obsoleteTech": "Replaceable Parts", "obsoleteTech": "Replaceable Parts",
"upgradesTo": "Great War Infantry", "upgradesTo": "Great War Infantry",
"uniques": ["+[25]% Strength in [Snow]", "+[25]% Strength in [Tundra]", "+[25]% Strength in [Hill]", "Double movement in Snow, Tundra and Hills"], "uniques": ["+[25]% Strength in [Snow]",
"+[25]% Strength in [Tundra]",
"+[25]% Strength in [Hill]",
"Double movement in [Snow]",
"Double movement in [Tundra]",
"Double movement in [Hill]"],
"attackSound": "shot" "attackSound": "shot"
}, },
{ {
@ -1083,7 +1088,7 @@
"requiredResource": "Coal", "requiredResource": "Coal",
"upgradesTo": "Destroyer", "upgradesTo": "Destroyer",
"obsoleteTech": "Combustion", "obsoleteTech": "Combustion",
"uniques": ["+[33]% Strength vs [City]","Double movement in coast"], "uniques": ["+[33]% Strength vs [City]","Double movement in [Coast]"],
"attackSound": "shipguns" "attackSound": "shipguns"
}, },
{ {

View File

@ -335,6 +335,7 @@ class CivilizationInfo {
else city.getAllUniquesWithNonLocalEffects() else city.getAllUniquesWithNonLocalEffects()
} }
fun hasUnique(uniqueType: UniqueType) = getMatchingUniques(uniqueType).any()
fun hasUnique(unique: String) = getMatchingUniques(unique).any() fun hasUnique(unique: String) = getMatchingUniques(unique).any()
/** Destined to replace getMatchingUniques, gradually, as we fill the enum */ /** Destined to replace getMatchingUniques, gradually, as we fill the enum */

View File

@ -11,6 +11,7 @@ import com.unciv.logic.civilization.LocationAction
import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.civilization.NotificationIcon
import com.unciv.models.UnitActionType import com.unciv.models.UnitActionType
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.tile.TerrainType
import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.tile.TileImprovement import com.unciv.models.ruleset.tile.TileImprovement
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
@ -36,7 +37,7 @@ class MapUnit {
@Transient @Transient
val movement = UnitMovementAlgorithms(this) val movement = UnitMovementAlgorithms(this)
@Transient @Transient
var isDestroyed = false var isDestroyed = false
@ -51,27 +52,45 @@ class MapUnit {
// which in turn is a component of getShortestPath and canReach // which in turn is a component of getShortestPath and canReach
@Transient @Transient
var ignoresTerrainCost = false var ignoresTerrainCost = false
private set
@Transient @Transient
var ignoresZoneOfControl = false var ignoresZoneOfControl = false
private set
@Transient @Transient
var allTilesCosts1 = false var allTilesCosts1 = false
private set
@Transient @Transient
var canPassThroughImpassableTiles = false var canPassThroughImpassableTiles = false
private set
@Transient @Transient
var roughTerrainPenalty = false var roughTerrainPenalty = false
private set
/** If set causes an early exit in getMovementCostBetweenAdjacentTiles
* - means no double movement uniques, roughTerrainPenalty or ignoreHillMovementCost */
@Transient @Transient
var doubleMovementInCoast = false var noTerrainMovementUniques = false
private set
/** If set causes a second early exit in getMovementCostBetweenAdjacentTiles */
@Transient @Transient
var doubleMovementInForestAndJungle = false var noBaseTerrainOrHillDoubleMovementUniques = false
private set
/** If set skips tile.matchesFilter tests for double movement in getMovementCostBetweenAdjacentTiles */
@Transient @Transient
var doubleMovementInSnowTundraAndHills = false var noFilteredDoubleMovementUniques = false
private set
/** Used for getMovementCostBetweenAdjacentTiles only, based on order of testing */
enum class DoubleMovementTerrainTarget { Feature, Base, Hill, Filter }
/** Mod-friendly cache of double-movement terrains */
@Transient
val doubleMovementInTerrain = HashMap<String, DoubleMovementTerrainTarget>()
@Transient @Transient
var canEnterIceTiles = false var canEnterIceTiles = false
@ -91,6 +110,7 @@ class MapUnit {
@Transient @Transient
var hasUniqueToBuildImprovements = false // not canBuildImprovements to avoid confusion var hasUniqueToBuildImprovements = false // not canBuildImprovements to avoid confusion
/** civName owning the unit */
lateinit var owner: String lateinit var owner: String
/** /**
@ -208,35 +228,75 @@ class MapUnit {
tempUniques.asSequence().filter { it.placeholderText == placeholderText } + tempUniques.asSequence().filter { it.placeholderText == placeholderText } +
civInfo.getMatchingUniques(placeholderText) civInfo.getMatchingUniques(placeholderText)
fun getMatchingUniques(uniqueType: UniqueType): Sequence<Unique> =
tempUniques.asSequence().filter { it.type == uniqueType } +
civInfo.getMatchingUniques(uniqueType)
fun hasUnique(unique: String): Boolean { fun hasUnique(unique: String): Boolean {
return getUniques().any { it.placeholderText == unique } || civInfo.hasUnique(unique) return tempUniques.any { it.placeholderText == unique } || civInfo.hasUnique(unique)
} }
fun updateUniques() { fun hasUnique(uniqueType: UniqueType): Boolean {
return tempUniques.any { it.type == uniqueType } || civInfo.hasUnique(uniqueType)
}
fun updateUniques(ruleset: Ruleset) {
val uniques = ArrayList<Unique>() val uniques = ArrayList<Unique>()
val baseUnit = baseUnit() val baseUnit = baseUnit()
uniques.addAll(baseUnit.uniqueObjects) uniques.addAll(baseUnit.uniqueObjects)
uniques.addAll(type.uniqueObjects) uniques.addAll(type.uniqueObjects)
for (promotion in promotions.promotions) { for (promotion in promotions.getPromotions()) {
uniques.addAll(currentTile.tileMap.gameInfo.ruleSet.unitPromotions[promotion]!!.uniqueObjects) uniques.addAll(promotion.uniqueObjects)
} }
tempUniques = uniques tempUniques = uniques
//todo: parameterize [terrainFilter] in 5 to 7 of the following: allTilesCosts1 = hasUnique(UniqueType.AllTilesCost1Move)
canPassThroughImpassableTiles = hasUnique(UniqueType.CanPassImpassable)
ignoresTerrainCost = hasUnique(UniqueType.IgnoresTerrainCost)
ignoresZoneOfControl = hasUnique(UniqueType.IgnoresZOC)
roughTerrainPenalty = hasUnique(UniqueType.RoughTerrainPenalty)
doubleMovementInTerrain.clear()
// Cache the deprecated uniques
if (hasUnique(UniqueType.DoubleMovementCoast)) {
doubleMovementInTerrain[Constants.coast] = DoubleMovementTerrainTarget.Base
}
if (hasUnique(UniqueType.DoubleMovementForestJungle)) {
doubleMovementInTerrain[Constants.forest] = DoubleMovementTerrainTarget.Feature
doubleMovementInTerrain[Constants.jungle] = DoubleMovementTerrainTarget.Feature
}
if (hasUnique(UniqueType.DoubleMovementSnowTundraHill)) {
doubleMovementInTerrain[Constants.snow] = DoubleMovementTerrainTarget.Base
doubleMovementInTerrain[Constants.tundra] = DoubleMovementTerrainTarget.Base
doubleMovementInTerrain[Constants.hill] = DoubleMovementTerrainTarget.Feature
}
// Now the current unique
for (unique in getMatchingUniques(UniqueType.DoubleMovementOnTerrain)) {
val param = unique.params[0]
val terrain = ruleset.terrains[param]
doubleMovementInTerrain[param] = when {
terrain == null -> DoubleMovementTerrainTarget.Filter
terrain.name == Constants.hill -> DoubleMovementTerrainTarget.Hill
terrain.type == TerrainType.TerrainFeature -> DoubleMovementTerrainTarget.Feature
terrain.type.isBaseTerrain -> DoubleMovementTerrainTarget.Base
else -> DoubleMovementTerrainTarget.Filter
}
}
// Init shortcut flags
noTerrainMovementUniques = doubleMovementInTerrain.isEmpty() &&
!roughTerrainPenalty && !civInfo.nation.ignoreHillMovementCost
noBaseTerrainOrHillDoubleMovementUniques = doubleMovementInTerrain
.none { it.value != DoubleMovementTerrainTarget.Feature }
noFilteredDoubleMovementUniques = doubleMovementInTerrain
.none { it.value == DoubleMovementTerrainTarget.Filter }
//todo: consider parameterizing [terrainFilter] in some of the following:
canEnterIceTiles = hasUnique(UniqueType.CanEnterIceTiles)
cannotEnterOceanTiles = hasUnique(UniqueType.CannotEnterOcean)
cannotEnterOceanTilesUntilAstronomy = hasUnique(UniqueType.CannotEnterOceanUntilAstronomy)
allTilesCosts1 = hasUnique("All tiles cost 1 movement")
canPassThroughImpassableTiles = hasUnique("Can pass through impassable tiles")
ignoresTerrainCost = hasUnique("Ignores terrain cost")
ignoresZoneOfControl = hasUnique("Ignores Zone of Control")
roughTerrainPenalty = hasUnique("Rough terrain penalty")
doubleMovementInCoast = hasUnique("Double movement in coast")
doubleMovementInForestAndJungle = hasUnique("Double movement rate through Forest and Jungle")
doubleMovementInSnowTundraAndHills = hasUnique("Double movement in Snow, Tundra and Hills")
canEnterIceTiles = hasUnique("Can enter ice tiles")
cannotEnterOceanTiles = hasUnique("Cannot enter ocean tiles")
cannotEnterOceanTilesUntilAstronomy = hasUnique("Cannot enter ocean tiles until Astronomy")
hasUniqueToBuildImprovements = hasUnique(Constants.canBuildImprovements) hasUniqueToBuildImprovements = hasUnique(Constants.canBuildImprovements)
canEnterForeignTerrain = canEnterForeignTerrain =
hasUnique("May enter foreign tiles without open borders, but loses [] religious strength each turn it ends there") hasUnique("May enter foreign tiles without open borders, but loses [] religious strength each turn it ends there")
@ -255,7 +315,7 @@ class MapUnit {
newUnit.promotions = promotions.clone() newUnit.promotions = promotions.clone()
newUnit.updateUniques() newUnit.updateUniques(civInfo.gameInfo.ruleSet)
newUnit.updateVisibleTiles() newUnit.updateVisibleTiles()
} }
@ -266,9 +326,9 @@ class MapUnit {
private fun getVisibilityRange(): Int { private fun getVisibilityRange(): Int {
if (isEmbarked() && !hasUnique("Normal vision when embarked")) if (isEmbarked() && !hasUnique("Normal vision when embarked"))
return 1 return 1
var visibilityRange = 2 var visibilityRange = 2
for (unique in getMatchingUniques("[] Sight for all [] units")) for (unique in getMatchingUniques("[] Sight for all [] units"))
if (matchesFilter(unique.params[1])) if (matchesFilter(unique.params[1]))
visibilityRange += unique.params[0].toInt() visibilityRange += unique.params[0].toInt()
@ -454,7 +514,7 @@ class MapUnit {
baseUnit = ruleset.units[name] baseUnit = ruleset.units[name]
?: throw java.lang.Exception("Unit $name is not found!") ?: throw java.lang.Exception("Unit $name is not found!")
updateUniques() updateUniques(ruleset)
} }
fun useMovementPoints(amount: Float) { fun useMovementPoints(amount: Float) {
@ -988,7 +1048,7 @@ class MapUnit {
return getMatchingUniques("Can [] [] times").any { it.params[0] == action } return getMatchingUniques("Can [] [] times").any { it.params[0] == action }
} }
/** For the actual value, check the member variable `maxAbilityUses` /** For the actual value, check the member variable [maxAbilityUses]
*/ */
fun getBaseMaxActionUses(action: String): Int { fun getBaseMaxActionUses(action: String): Int {
return getMatchingUniques("Can [] [] times") return getMatchingUniques("Can [] [] times")

View File

@ -8,11 +8,16 @@ import com.unciv.logic.civilization.CivilizationInfo
class UnitMovementAlgorithms(val unit:MapUnit) { class UnitMovementAlgorithms(val unit:MapUnit) {
// This function is called ALL THE TIME and should be as time-optimal as possible! // This function is called ALL THE TIME and should be as time-optimal as possible!
fun getMovementCostBetweenAdjacentTiles(from: TileInfo, to: TileInfo, civInfo: CivilizationInfo, considerZoneOfControl: Boolean = true): Float { private fun getMovementCostBetweenAdjacentTiles(
from: TileInfo,
to: TileInfo,
civInfo: CivilizationInfo,
considerZoneOfControl: Boolean = true
): Float {
if (from.isLand != to.isLand && unit.baseUnit.isLandUnit()) if (from.isLand != to.isLand && unit.baseUnit.isLandUnit())
if (unit.civInfo.nation.disembarkCosts1 && from.isWater && to.isLand) return 1f return if (unit.civInfo.nation.disembarkCosts1 && from.isWater && to.isLand) 1f
else return 100f // this is embarkment or disembarkment, and will take the entire turn else 100f // this is embarkment or disembarkment, and will take the entire turn
// If the movement is affected by a Zone of Control, all movement points are expended // If the movement is affected by a Zone of Control, all movement points are expended
if (considerZoneOfControl && isMovementAffectedByZoneOfControl(from, to, civInfo)) if (considerZoneOfControl && isMovementAffectedByZoneOfControl(from, to, civInfo))
@ -22,11 +27,13 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
if (unit.allTilesCosts1) if (unit.allTilesCosts1)
return 1f return 1f
var extraCost = 0f
val toOwner = to.getOwner() val toOwner = to.getOwner()
if (toOwner != null && to.isLand && toOwner.hasActiveGreatWall && civInfo.isAtWarWith(toOwner)) val extraCost = if (
extraCost += 1 toOwner != null &&
to.isLand &&
toOwner.hasActiveGreatWall &&
civInfo.isAtWarWith(toOwner)
) 1f else 0f
if (from.roadStatus == RoadStatus.Railroad && to.roadStatus == RoadStatus.Railroad) if (from.roadStatus == RoadStatus.Railroad && to.roadStatus == RoadStatus.Railroad)
return RoadStatus.Railroad.movement + extraCost return RoadStatus.Railroad.movement + extraCost
@ -40,26 +47,38 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
if (unit.ignoresTerrainCost) return 1f + extraCost if (unit.ignoresTerrainCost) return 1f + extraCost
if (areConnectedByRiver) return 100f // Rivers take the entire turn to cross if (areConnectedByRiver) return 100f // Rivers take the entire turn to cross
if (unit.doubleMovementInForestAndJungle && val terrainCost = to.getLastTerrain().movementCost.toFloat()
(to.terrainFeatures.contains(Constants.forest) || to.terrainFeatures.contains(Constants.jungle)))
return 1f + extraCost // usually forest and jungle take 2 movements, so here it is 1 if (unit.noTerrainMovementUniques)
return terrainCost + extraCost
if (to.terrainFeatures.any { unit.doubleMovementInTerrain[it] == MapUnit.DoubleMovementTerrainTarget.Feature })
return terrainCost * 0.5f + extraCost
if (unit.roughTerrainPenalty && to.isRoughTerrain()) if (unit.roughTerrainPenalty && to.isRoughTerrain())
return 100f // units that have to sped all movement in rough terrain, have to spend all movement in rough terrain return 100f // units that have to spend all movement in rough terrain, have to spend all movement in rough terrain
// Placement of this 'if' based on testing, see #4232 // Placement of this 'if' based on testing, see #4232
if (civInfo.nation.ignoreHillMovementCost && to.isHill()) if (civInfo.nation.ignoreHillMovementCost && to.isHill())
return 1f + extraCost // usually hills take 2 movements, so here it is 1 return 1f + extraCost // usually hills take 2 movements, so here it is 1
if (unit.doubleMovementInCoast && to.baseTerrain == Constants.coast) if (unit.noBaseTerrainOrHillDoubleMovementUniques)
return 1 / 2f + extraCost return terrainCost + extraCost
if (unit.doubleMovementInSnowTundraAndHills && to.isHill()) if (unit.doubleMovementInTerrain[to.baseTerrain] == MapUnit.DoubleMovementTerrainTarget.Base)
return 1f + extraCost // usually hills take 2 return terrainCost * 0.5f + extraCost
if (unit.doubleMovementInSnowTundraAndHills && (to.baseTerrain == Constants.snow || to.baseTerrain == Constants.tundra)) if (unit.doubleMovementInTerrain[Constants.hill] == MapUnit.DoubleMovementTerrainTarget.Hill && to.isHill())
return 1 / 2f + extraCost return terrainCost * 0.5f + extraCost
return to.getLastTerrain().movementCost.toFloat() + extraCost // no road if (unit.noFilteredDoubleMovementUniques)
return terrainCost + extraCost
if (unit.doubleMovementInTerrain.any {
it.value == MapUnit.DoubleMovementTerrainTarget.Filter &&
to.matchesFilter(it.key)
})
return terrainCost * 0.5f + extraCost
return terrainCost + extraCost // no road or other movement cost reduction
} }
/** Returns whether the movement between the adjacent tiles [from] and [to] is affected by Zone of Control */ /** Returns whether the movement between the adjacent tiles [from] and [to] is affected by Zone of Control */
@ -130,20 +149,18 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
val updatedTiles = ArrayList<TileInfo>() val updatedTiles = ArrayList<TileInfo>()
for (tileToCheck in tilesToCheck) for (tileToCheck in tilesToCheck)
for (neighbor in tileToCheck.neighbors) { for (neighbor in tileToCheck.neighbors) {
var totalDistanceToTile: Float var totalDistanceToTile: Float = if (unit.civInfo.exploredTiles.contains(neighbor.position)) {
if (unit.civInfo.exploredTiles.contains(neighbor.position)) {
if (!canPassThrough(neighbor)) if (!canPassThrough(neighbor))
totalDistanceToTile = unitMovement // Can't go here. unitMovement // Can't go here.
// The reason that we don't just "return" is so that when calculating how to reach an enemy, // The reason that we don't just "return" is so that when calculating how to reach an enemy,
// You need to assume his tile is reachable, otherwise all movement algorithms on reaching enemy // You need to assume his tile is reachable, otherwise all movement algorithms on reaching enemy
// cities and units goes kaput. // cities and units goes kaput.
else { else {
val distanceBetweenTiles = getMovementCostBetweenAdjacentTiles(tileToCheck, neighbor, unit.civInfo, considerZoneOfControl) val distanceBetweenTiles = getMovementCostBetweenAdjacentTiles(tileToCheck, neighbor, unit.civInfo, considerZoneOfControl)
totalDistanceToTile = distanceToTiles[tileToCheck]!!.totalDistance + distanceBetweenTiles distanceToTiles[tileToCheck]!!.totalDistance + distanceBetweenTiles
} }
} else totalDistanceToTile = distanceToTiles[tileToCheck]!!.totalDistance + 1f // If we don't know then we just guess it to be 1. } else distanceToTiles[tileToCheck]!!.totalDistance + 1f // If we don't know then we just guess it to be 1.
if (!distanceToTiles.containsKey(neighbor) || distanceToTiles[neighbor]!!.totalDistance > totalDistanceToTile) { // this is the new best path if (!distanceToTiles.containsKey(neighbor) || distanceToTiles[neighbor]!!.totalDistance > totalDistanceToTile) { // this is the new best path
if (totalDistanceToTile < unitMovement) // We can still keep moving from here! if (totalDistanceToTile < unitMovement) // We can still keep moving from here!
@ -281,7 +298,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
return getShortestPath(destination).any() return getShortestPath(destination).any()
} }
fun canReachInCurrentTurn(destination: TileInfo): Boolean { private fun canReachInCurrentTurn(destination: TileInfo): Boolean {
if (unit.baseUnit.movesLikeAirUnits()) if (unit.baseUnit.movesLikeAirUnits())
return unit.currentTile.aerialDistanceTo(destination) <= unit.getMaxMovementForAirUnits() return unit.currentTile.aerialDistanceTo(destination) <= unit.getMaxMovementForAirUnits()
if (unit.isPreparingParadrop()) if (unit.isPreparingParadrop())
@ -412,8 +429,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
val pathToDestination = distanceToTiles.getPathToTile(destination) val pathToDestination = distanceToTiles.getPathToTile(destination)
val movableTiles = pathToDestination.takeWhile { canPassThrough(it) } val movableTiles = pathToDestination.takeWhile { canPassThrough(it) }
val lastReachableTile = movableTiles.lastOrNull { canMoveTo(it) } val lastReachableTile = movableTiles.lastOrNull { canMoveTo(it) }
if (lastReachableTile == null) // no tiles can pass though/can move to ?: return // no tiles can pass though/can move to
return
val pathToLastReachableTile = distanceToTiles.getPathToTile(lastReachableTile) val pathToLastReachableTile = distanceToTiles.getPathToTile(lastReachableTile)
if (unit.isFortified() || unit.isSetUpForSiege() || unit.isSleeping()) if (unit.isFortified() || unit.isSetUpForSiege() || unit.isSleeping())
@ -423,11 +439,11 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
val origin = unit.getTile() val origin = unit.getTile()
var needToFindNewRoute = false var needToFindNewRoute = false
// Cache this in case something goes wrong // Cache this in case something goes wrong
var lastReachedEnterableTile = unit.getTile() var lastReachedEnterableTile = unit.getTile()
unit.removeFromTile() unit.removeFromTile()
for (tile in pathToLastReachableTile) { for (tile in pathToLastReachableTile) {
if (!unit.movement.canPassThrough(tile)) { if (!unit.movement.canPassThrough(tile)) {
// AAAH something happened making our previous path invalid // AAAH something happened making our previous path invalid
@ -439,16 +455,16 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
break // If you ever remove this break, remove the `assumeCanPassThrough` param below break // If you ever remove this break, remove the `assumeCanPassThrough` param below
} }
unit.moveThroughTile(tile) unit.moveThroughTile(tile)
// In case something goes wrong, cache the last tile we were able to end on // In case something goes wrong, cache the last tile we were able to end on
// We can assume we can pass through this tile, as we would have broken earlier // We can assume we can pass through this tile, as we would have broken earlier
if (unit.movement.canMoveTo(tile, assumeCanPassThrough = true)) { if (unit.movement.canMoveTo(tile, assumeCanPassThrough = true)) {
lastReachedEnterableTile = tile lastReachedEnterableTile = tile
} }
if (unit.isDestroyed) break if (unit.isDestroyed) break
} }
if (!unit.isDestroyed) if (!unit.isDestroyed)
unit.putInTile(lastReachedEnterableTile) unit.putInTile(lastReachedEnterableTile)
@ -532,7 +548,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
} }
return false return false
} }
// Can a paratrooper land at this tile? // Can a paratrooper land at this tile?
fun canParadropOn(destination: TileInfo): Boolean { fun canParadropOn(destination: TileInfo): Boolean {
// Can only move to land tiles within range that are visible and not impassible // Can only move to land tiles within range that are visible and not impassible
@ -578,7 +594,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
return false return false
} }
if (tile.naturalWonder != null) return false if (tile.naturalWonder != null) return false
if (!unit.canEnterForeignTerrain && !tile.canCivPassThrough(unit.civInfo)) return false if (!unit.canEnterForeignTerrain && !tile.canCivPassThrough(unit.civInfo)) return false
val firstUnit = tile.getFirstUnit() val firstUnit = tile.getFirstUnit()

View File

@ -11,20 +11,45 @@ class UnitPromotions {
@Transient @Transient
private lateinit var unit: MapUnit private lateinit var unit: MapUnit
/** Experience this unit has accumulated on top of the last promotion */
@Suppress("PropertyName") @Suppress("PropertyName")
var XP = 0 var XP = 0
/** The _names_ of the promotions this unit has acquired - see [getPromotions] for object access */
var promotions = HashSet<String>() var promotions = HashSet<String>()
// The number of times this unit has been promoted private set
// some promotions don't come from being promoted but from other things, // some promotions don't come from being promoted but from other things,
// like from being constructed in a specific city etc. // like from being constructed in a specific city etc.
/** The number of times this unit has been promoted using experience, not counting free promotions */
var numberOfPromotions = 0 var numberOfPromotions = 0
/** Gets this unit's promotions as objects.
* @param sorted if `true` return the promotions in json order (`false` gives hashset order) for display.
* @return a Sequence of this unit's promotions
*/
fun getPromotions(sorted: Boolean = false): Sequence<Promotion> = sequence {
if (promotions.isEmpty()) return@sequence
val unitPromotions = unit.civInfo.gameInfo.ruleSet.unitPromotions
if (sorted && promotions.size > 1) {
for (promotion in unitPromotions.values)
if (promotion.name in promotions) yield(promotion)
} else {
for (name in promotions)
yield(unitPromotions[name] ?: continue)
}
}
fun setTransients(unit: MapUnit) { fun setTransients(unit: MapUnit) {
this.unit = unit this.unit = unit
} }
fun xpForNextPromotion() = (numberOfPromotions+1)*10 /** @return the XP points needed to "buy" the next promotion. 10, 30, 60, 100, 150,... */
fun xpForNextPromotion() = (numberOfPromotions + 1) * 10
/** @return Total XP including that already "spent" on promotions */
fun totalXpProduced() = XP + (numberOfPromotions * (numberOfPromotions + 1)) * 5
fun canBePromoted(): Boolean { fun canBePromoted(): Boolean {
if (XP < xpForNextPromotion()) return false if (XP < xpForNextPromotion()) return false
if (getAvailablePromotions().none()) return false if (getAvailablePromotions().none()) return false
@ -37,26 +62,30 @@ class UnitPromotions {
numberOfPromotions++ numberOfPromotions++
} }
val promotion = unit.civInfo.gameInfo.ruleSet.unitPromotions[promotionName]!! val ruleset = unit.civInfo.gameInfo.ruleSet
val promotion = ruleset.unitPromotions[promotionName]!!
doDirectPromotionEffects(promotion) doDirectPromotionEffects(promotion)
if (promotion.uniqueObjects.none { it.placeholderText == "Doing so will consume this opportunity to choose a Promotion" }) if (promotion.uniqueObjects.none { it.placeholderText == "Doing so will consume this opportunity to choose a Promotion" })
promotions.add(promotionName) promotions.add(promotionName)
unit.updateUniques() unit.updateUniques(ruleset)
// Since some units get promotions upon construction, they will get the addPromotion from the unit.postBuildEvent // Since some units get promotions upon construction, they will get the addPromotion from the unit.postBuildEvent
// upon creation, BEFORE they are assigned to a tile, so the updateVisibleTiles() would crash. // upon creation, BEFORE they are assigned to a tile, so the updateVisibleTiles() would crash.
// So, if the addPromotion was triggered from there, simply don't update // So, if the addPromotion was triggered from there, simply don't update
unit.updateVisibleTiles() // some promotions/uniques give the unit bonus sight unit.updateVisibleTiles() // some promotions/uniques give the unit bonus sight
} }
fun doDirectPromotionEffects(promotion: Promotion) { private fun doDirectPromotionEffects(promotion: Promotion) {
for (unique in promotion.uniqueObjects) { for (unique in promotion.uniqueObjects) {
UniqueTriggerActivation.triggerUnitwideUnique(unique, unit) UniqueTriggerActivation.triggerUnitwideUnique(unique, unit)
} }
} }
/** Gets all promotions this unit could currently "buy" with enough [XP]
* Checks unit type, already acquired promotions, prerequisites and incompatibility uniques.
*/
fun getAvailablePromotions(): Sequence<Promotion> { fun getAvailablePromotions(): Sequence<Promotion> {
return unit.civInfo.gameInfo.ruleSet.unitPromotions.values return unit.civInfo.gameInfo.ruleSet.unitPromotions.values
.asSequence() .asSequence()
@ -78,10 +107,4 @@ class UnitPromotions {
toReturn.unit = unit toReturn.unit = unit
return toReturn return toReturn
} }
fun totalXpProduced(): Int {
var sum = XP
for(i in 1..numberOfPromotions) sum += 10*i
return sum
}
} }

View File

@ -15,12 +15,10 @@ interface IHasUniques {
* But making this a function is relevant for future "unify Unciv object" plans ;) * But making this a function is relevant for future "unify Unciv object" plans ;)
* */ * */
fun getUniqueTarget(): UniqueTarget fun getUniqueTarget(): UniqueTarget
fun getMatchingUniques(uniqueTemplate: String) = uniqueObjects.asSequence().filter { it.placeholderText == uniqueTemplate } fun getMatchingUniques(uniqueTemplate: String) = uniqueObjects.asSequence().filter { it.placeholderText == uniqueTemplate }
fun getMatchingUniques(uniqueType: UniqueType) = uniqueObjects.asSequence().filter { it.isOfType(uniqueType) } fun getMatchingUniques(uniqueType: UniqueType) = uniqueObjects.asSequence().filter { it.isOfType(uniqueType) }
fun hasUnique(uniqueTemplate: String) = uniqueObjects.any { it.placeholderText == uniqueTemplate } fun hasUnique(uniqueTemplate: String) = uniqueObjects.any { it.placeholderText == uniqueTemplate }
fun hasUnique(uniqueType: UniqueType) = uniqueObjects.any { it.isOfType(uniqueType) } fun hasUnique(uniqueType: UniqueType) = uniqueObjects.any { it.isOfType(uniqueType) }
} }

View File

@ -113,6 +113,23 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget) {
TerrainGrantsPromotion("Grants [promotion] ([comment]) to adjacent [mapUnitFilter] units for the rest of the game", UniqueTarget.Terrain), TerrainGrantsPromotion("Grants [promotion] ([comment]) to adjacent [mapUnitFilter] units for the rest of the game", UniqueTarget.Terrain),
// The following block gets cached in MapUnit for faster getMovementCostBetweenAdjacentTiles
DoubleMovementOnTerrain("Double movement in [terrainFilter]", UniqueTarget.Unit),
@Deprecated("As of 3.17.1", ReplaceWith("Double movement in [terrainFilter]"), DeprecationLevel.WARNING)
DoubleMovementCoast("Double movement in coast", UniqueTarget.Unit),
@Deprecated("As of 3.17.1", ReplaceWith("Double movement in [terrainFilter]"), DeprecationLevel.WARNING)
DoubleMovementForestJungle("Double movement rate through Forest and Jungle", UniqueTarget.Unit),
@Deprecated("As of 3.17.1", ReplaceWith("Double movement in [terrainFilter]"), DeprecationLevel.WARNING)
DoubleMovementSnowTundraHill("Double movement in Snow, Tundra and Hills", UniqueTarget.Unit),
AllTilesCost1Move("All tiles cost 1 movement", UniqueTarget.Unit),
CanPassImpassable("Can pass through impassable tiles", UniqueTarget.Unit),
IgnoresTerrainCost("Ignores terrain cost", UniqueTarget.Unit),
IgnoresZOC("Ignores Zone of Control", UniqueTarget.Unit),
RoughTerrainPenalty("Rough terrain penalty", UniqueTarget.Unit),
CanEnterIceTiles("Can enter ice tiles", UniqueTarget.Unit),
CannotEnterOcean("Cannot enter ocean tiles", UniqueTarget.Unit),
CannotEnterOceanUntilAstronomy("Cannot enter ocean tiles until Astronomy", UniqueTarget.Unit),
///// CONDITIONALS ///// CONDITIONALS

View File

@ -201,7 +201,7 @@ class MapEditorOptionsTable(val mapEditorScreen: MapEditorScreen): Table(CameraS
unit.name = currentUnit.name unit.name = currentUnit.name
unit.owner = currentNation.name unit.owner = currentNation.name
unit.civInfo = CivilizationInfo(currentNation.name).apply { nation = currentNation } // needed for the unit icon to render correctly unit.civInfo = CivilizationInfo(currentNation.name).apply { nation = currentNation } // needed for the unit icon to render correctly
unit.updateUniques() unit.updateUniques(ruleset)
if (unit.movement.canMoveTo(it)) { if (unit.movement.canMoveTo(it)) {
when { when {
unit.baseUnit.movesLikeAirUnits() -> { unit.baseUnit.movesLikeAirUnits() -> {

View File

@ -101,10 +101,8 @@ class UnitOverviewTable(
unit.getTile().getTilesInDistance(3).firstOrNull { it.isCityCenter() } unit.getTile().getTilesInDistance(3).firstOrNull { it.isCityCenter() }
if (closestCity != null) add(closestCity.getCity()!!.name.tr()) else add() if (closestCity != null) add(closestCity.getCity()!!.name.tr()) else add()
val promotionsTable = Table() val promotionsTable = Table()
val promotionsForUnit = unit.civInfo.gameInfo.ruleSet.unitPromotions.values.filter { // getPromotions goes by json order on demand, so this is same sorting as on picker
unit.promotions.promotions.contains(it.name) for (promotion in unit.promotions.getPromotions(true))
} // force same sorting as on picker (.sorted() would be simpler code, but...)
for (promotion in promotionsForUnit)
promotionsTable.add(ImageGetter.getPromotionIcon(promotion.name)) promotionsTable.add(ImageGetter.getPromotionIcon(promotion.name))
if (unit.promotions.canBePromoted()) promotionsTable.add( if (unit.promotions.canBePromoted()) promotionsTable.add(
ImageGetter.getImage("OtherIcons/Star").apply { color = Color.GOLDENROD }) ImageGetter.getImage("OtherIcons/Star").apply { color = Color.GOLDENROD })

View File

@ -216,8 +216,8 @@ class UnitTable(val worldScreen: WorldScreen) : Table(){
if (selectedUnits.size == 1) { // single selected unit if (selectedUnits.size == 1) { // single selected unit
unitIconHolder.add(UnitGroup(selectedUnit!!, 30f)).pad(5f) unitIconHolder.add(UnitGroup(selectedUnit!!, 30f)).pad(5f)
for (promotion in selectedUnit!!.promotions.promotions.sorted()) for (promotion in selectedUnit!!.promotions.getPromotions(true))
promotionsTable.add(ImageGetter.getPromotionIcon(promotion)) promotionsTable.add(ImageGetter.getPromotionIcon(promotion.name))
// Since Clear also clears the listeners, we need to re-add it every time // Since Clear also clears the listeners, we need to re-add it every time
promotionsTable.onClick { promotionsTable.onClick {

View File

@ -118,7 +118,7 @@ class UnitMovementAlgorithmsTests {
for (type in ruleSet.unitTypes) { for (type in ruleSet.unitTypes) {
unit.baseUnit = BaseUnit().apply { unitType = type.key; ruleset = ruleSet } unit.baseUnit = BaseUnit().apply { unitType = type.key; ruleset = ruleSet }
unit.updateUniques() unit.updateUniques(ruleSet)
Assert.assertTrue( Assert.assertTrue(
"$type cannot be in Ice", "$type cannot be in Ice",
@ -190,7 +190,7 @@ class UnitMovementAlgorithmsTests {
if (this.isRanged()) if (this.isRanged())
uniques.add("Cannot enter ocean tiles until Astronomy") uniques.add("Cannot enter ocean tiles until Astronomy")
} }
unit.updateUniques() unit.updateUniques(ruleSet)
Assert.assertTrue("$type cannot be in Ocean", Assert.assertTrue("$type cannot be in Ocean",
(unit.baseUnit.isMelee()) != unit.movement.canPassThrough(tile)) (unit.baseUnit.isMelee()) != unit.movement.canPassThrough(tile))