Unique: May Paradrop to [tileFilter] tiles up to [positiveAmount] tiles away (#13586)

* Paradrop: Allow mods to enable paradropping from outside friendly territory

* Fix Paratrooper range in Vanilla

* Keep support for the deprecated unique

* Update docs

* Allow setting a destination tileFilter for Paradrops

* Apply suggestion from @RobLoach

* Apply suggestion from @RobLoach

* Update language for paradrop
This commit is contained in:
Rob Loach 2025-07-11 08:35:46 -04:00 committed by GitHub
parent 4819cf1053
commit 9b4ed0d4ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 52 additions and 21 deletions

View File

@ -1362,7 +1362,7 @@
"strength": 65,
"cost": 375,
"requiredTech": "Radar",
"uniques": ["May Paradrop up to [5] tiles from inside friendly territory", "Never appears as a Barbarian unit"],
"uniques": ["May Paradrop to [Land] tiles up to [5] tiles away <in [{Friendly} {Land}] tiles>", "Never appears as a Barbarian unit"],
"attackSound": "shot"
// upgradesTo "XCOM Squad", "No Movement Cost to Pillage" in BNW
},

View File

@ -1039,7 +1039,7 @@
"strength": 65,
"cost": 375,
"requiredTech": "Radar",
"uniques": ["May Paradrop up to [5] tiles from inside friendly territory", "Never appears as a Barbarian unit"],
"uniques": ["May Paradrop to [Land] tiles up to [5] tiles away <in [{Friendly} {Land}] tiles>", "Never appears as a Barbarian unit"],
"attackSound": "shot"
// upgradesTo "XCOM Squad", "No Movement Cost to Pillage" in BNW
},

View File

@ -59,7 +59,9 @@ class MapUnitCache(private val mapUnit: MapUnit) {
var canEnterCityStates: Boolean = false
var costToDisembark: Float? = null
var costToEmbark: Float? = null
var paradropRange = 0
/** A hashmap where the key represents the tileFilter, and the value is how far away the tile could be */
val paradropDestinationTileFilters = mutableMapOf<String, Int>()
var hasUniqueToBuildImprovements = false // not canBuildImprovements to avoid confusion
var hasUniqueToCreateWaterImprovements = false

View File

@ -289,7 +289,7 @@ class UnitMovement(val unit: MapUnit) {
unit.baseUnit.movesLikeAirUnits ->
unit.currentTile.aerialDistanceTo(destination) <= unit.getMaxMovementForAirUnits()
unit.isPreparingParadrop() ->
unit.currentTile.aerialDistanceTo(destination) <= unit.cache.paradropRange && canParadropOn(destination)
canParadropOn(destination, unit.currentTile.aerialDistanceTo(destination))
else ->
specificFunction(destination) // Note: Could pass destination as implicit closure from outer fun to lambda, but explicit is clearer
}
@ -303,9 +303,10 @@ class UnitMovement(val unit: MapUnit) {
unit.cache.cannotMove -> sequenceOf(unit.getTile())
unit.baseUnit.movesLikeAirUnits ->
unit.getTile().getTilesInDistanceRange(IntRange(1, unit.getMaxMovementForAirUnits()))
unit.isPreparingParadrop() ->
unit.getTile().getTilesInDistance(unit.cache.paradropRange)
.filter { unit.movement.canParadropOn(it) }
unit.isPreparingParadrop() -> {
unit.getTile().getTilesInDistance(unit.cache.paradropDestinationTileFilters.maxOf { it.value } )
.filter { unit.movement.canParadropOn(it, it.aerialDistanceTo(unit.getTile())) }
}
includeOtherEscortUnit && unit.isEscorting() -> {
val otherUnitTiles = unit.getOtherEscortUnit()!!.movement.getReachableTilesInCurrentTurn(false).toSet()
unit.movement.getDistanceToTiles().filter { otherUnitTiles.contains(it.key) }.keys.asSequence()
@ -636,12 +637,19 @@ class UnitMovement(val unit: MapUnit) {
}
// Can a paratrooper land at this tile?
private fun canParadropOn(destination: Tile): Boolean {
private fun canParadropOn(destination: Tile, distance: Int): Boolean {
if (unit.cache.cannotMove) return false
// Can only move to land tiles within range that are visible and not impassible
// Can only move to tiles within range that are visible and not impassible
// Based on some testing done in the base game
if (!destination.isLand || destination.isImpassible() || !unit.civ.viewableTiles.contains(destination)) return false
return true
if (destination.isImpassible() || !unit.civ.viewableTiles.contains(destination)) return false
// The destination is valid if any of the `tileFilters` match, and is within range
for ((tileFilter, distanceAllowed) in unit.cache.paradropDestinationTileFilters) {
if (distance <= distanceAllowed && destination.matchesFilter(tileFilter, unit.civ)) return true
}
return false
}
/**

View File

@ -377,7 +377,9 @@ enum class UniqueType(
PreventSpreadingReligion("Prevents spreading of religion to the city it is next to", UniqueTarget.Unit),
RemoveOtherReligions("Removes other religions when spreading religion", UniqueTarget.Unit),
MayParadrop("May Paradrop up to [amount] tiles from inside friendly territory", UniqueTarget.Unit),
@Deprecated("As of 4.17.4", ReplaceWith("May Paradrop to [Land] tiles up to [positiveAmount] tiles away <in [{Friendly} {Land}] tiles>"), DeprecationLevel.WARNING)
MayParadropOld("May Paradrop up to [positiveAmount] tiles from inside friendly territory", UniqueTarget.Unit),
MayParadrop("May Paradrop to [tileFilter] tiles up to [positiveAmount] tiles away", UniqueTarget.Unit),
CanAirsweep("Can perform Air Sweep", UniqueTarget.Unit),
CanSpeedupConstruction("Can speed up construction of a building", UniqueTarget.Unit),

View File

@ -545,6 +545,8 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
UniqueType.MayParadrop // Paradrop - 25% bonus
-> power += power / 4
UniqueType.MayParadropOld // ParadropOld - 25% bonus
-> power += power / 4
UniqueType.MustSetUp // Must set up - 20 % penalty
-> power -= power / 5
UniqueType.AdditionalAttacks // Extra attacks - 20% bonus per extra attack

View File

@ -142,10 +142,29 @@ object UnitActionsFromUniques {
}
internal fun getParadropActions(unit: MapUnit, tile: Tile): Sequence<UnitAction> {
val paradropUniques =
unit.getMatchingUniques(UniqueType.MayParadrop)
if (!paradropUniques.any() || unit.isEmbarked()) return emptySequence()
unit.cache.paradropRange = paradropUniques.maxOfOrNull { it.params[0] }!!.toInt()
unit.cache.paradropDestinationTileFilters.clear()
// Support the old paradrop unique, going from Friendly Land to any Land tile
val paradropOldUniques = unit.getMatchingUniques(UniqueType.MayParadropOld)
if (paradropOldUniques.any() && !unit.isEmbarked() && !unit.getTile().isWater && unit.getTile().isFriendlyTerritory(unit.civ)) {
unit.cache.paradropDestinationTileFilters["Land"] = paradropOldUniques.maxOf { it.params[0] }.toInt()
}
// Retrieve all parardrop uniques, considering the state of the unit
val paradropUniques = unit.getMatchingUniques(UniqueType.MayParadrop, unit.cache.state)
// Construct the list of possible destination tile filters, keeping the largest distance
for (unique in paradropUniques) {
val tileFilter = unique.params[0]
val distance = unique.params[1].toInt()
val existingDistance = unit.cache.paradropDestinationTileFilters[tileFilter]
if (existingDistance == null || distance > existingDistance) {
unit.cache.paradropDestinationTileFilters[tileFilter] = distance
}
}
if (unit.cache.paradropDestinationTileFilters.isEmpty()) return emptySequence()
return sequenceOf(UnitAction(UnitActionType.Paradrop,
isCurrentAction = unit.isPreparingParadrop(),
useFrequency = 60f, // While it is important to see, it isn't nessesary used a lot
@ -153,9 +172,7 @@ object UnitActionsFromUniques {
if (unit.isPreparingParadrop()) unit.action = null
else unit.action = UnitActionType.Paradrop.value
}.takeIf {
!unit.hasUnitMovedThisTurn() &&
tile.isFriendlyTerritory(unit.civ) &&
!tile.isWater
!unit.hasUnitMovedThisTurn()
})
)
}

View File

@ -1861,8 +1861,8 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
??? example "Removes other religions when spreading religion"
Applicable to: Unit
??? example "May Paradrop up to [amount] tiles from inside friendly territory"
Example: "May Paradrop up to [3] tiles from inside friendly territory"
??? example "May Paradrop to [tileFilter] tiles up to [positiveAmount] tiles away"
Example: "May Paradrop to [Farm] tiles up to [3] tiles away"
Applicable to: Unit