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, "strength": 65,
"cost": 375, "cost": 375,
"requiredTech": "Radar", "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" "attackSound": "shot"
// upgradesTo "XCOM Squad", "No Movement Cost to Pillage" in BNW // upgradesTo "XCOM Squad", "No Movement Cost to Pillage" in BNW
}, },

View File

@ -1039,7 +1039,7 @@
"strength": 65, "strength": 65,
"cost": 375, "cost": 375,
"requiredTech": "Radar", "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" "attackSound": "shot"
// upgradesTo "XCOM Squad", "No Movement Cost to Pillage" in BNW // 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 canEnterCityStates: Boolean = false
var costToDisembark: Float? = null var costToDisembark: Float? = null
var costToEmbark: 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 hasUniqueToBuildImprovements = false // not canBuildImprovements to avoid confusion
var hasUniqueToCreateWaterImprovements = false var hasUniqueToCreateWaterImprovements = false

View File

@ -289,7 +289,7 @@ class UnitMovement(val unit: MapUnit) {
unit.baseUnit.movesLikeAirUnits -> unit.baseUnit.movesLikeAirUnits ->
unit.currentTile.aerialDistanceTo(destination) <= unit.getMaxMovementForAirUnits() unit.currentTile.aerialDistanceTo(destination) <= unit.getMaxMovementForAirUnits()
unit.isPreparingParadrop() -> unit.isPreparingParadrop() ->
unit.currentTile.aerialDistanceTo(destination) <= unit.cache.paradropRange && canParadropOn(destination) canParadropOn(destination, unit.currentTile.aerialDistanceTo(destination))
else -> else ->
specificFunction(destination) // Note: Could pass destination as implicit closure from outer fun to lambda, but explicit is clearer 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.cache.cannotMove -> sequenceOf(unit.getTile())
unit.baseUnit.movesLikeAirUnits -> unit.baseUnit.movesLikeAirUnits ->
unit.getTile().getTilesInDistanceRange(IntRange(1, unit.getMaxMovementForAirUnits())) unit.getTile().getTilesInDistanceRange(IntRange(1, unit.getMaxMovementForAirUnits()))
unit.isPreparingParadrop() -> unit.isPreparingParadrop() -> {
unit.getTile().getTilesInDistance(unit.cache.paradropRange) unit.getTile().getTilesInDistance(unit.cache.paradropDestinationTileFilters.maxOf { it.value } )
.filter { unit.movement.canParadropOn(it) } .filter { unit.movement.canParadropOn(it, it.aerialDistanceTo(unit.getTile())) }
}
includeOtherEscortUnit && unit.isEscorting() -> { includeOtherEscortUnit && unit.isEscorting() -> {
val otherUnitTiles = unit.getOtherEscortUnit()!!.movement.getReachableTilesInCurrentTurn(false).toSet() val otherUnitTiles = unit.getOtherEscortUnit()!!.movement.getReachableTilesInCurrentTurn(false).toSet()
unit.movement.getDistanceToTiles().filter { otherUnitTiles.contains(it.key) }.keys.asSequence() 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? // 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 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 // Based on some testing done in the base game
if (!destination.isLand || destination.isImpassible() || !unit.civ.viewableTiles.contains(destination)) return false if (destination.isImpassible() || !unit.civ.viewableTiles.contains(destination)) return false
return true
// 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), PreventSpreadingReligion("Prevents spreading of religion to the city it is next to", UniqueTarget.Unit),
RemoveOtherReligions("Removes other religions when spreading religion", 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), CanAirsweep("Can perform Air Sweep", UniqueTarget.Unit),
CanSpeedupConstruction("Can speed up construction of a building", 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 UniqueType.MayParadrop // Paradrop - 25% bonus
-> power += power / 4 -> power += power / 4
UniqueType.MayParadropOld // ParadropOld - 25% bonus
-> power += power / 4
UniqueType.MustSetUp // Must set up - 20 % penalty UniqueType.MustSetUp // Must set up - 20 % penalty
-> power -= power / 5 -> power -= power / 5
UniqueType.AdditionalAttacks // Extra attacks - 20% bonus per extra attack 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> { internal fun getParadropActions(unit: MapUnit, tile: Tile): Sequence<UnitAction> {
val paradropUniques = unit.cache.paradropDestinationTileFilters.clear()
unit.getMatchingUniques(UniqueType.MayParadrop)
if (!paradropUniques.any() || unit.isEmbarked()) return emptySequence() // Support the old paradrop unique, going from Friendly Land to any Land tile
unit.cache.paradropRange = paradropUniques.maxOfOrNull { it.params[0] }!!.toInt() 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, return sequenceOf(UnitAction(UnitActionType.Paradrop,
isCurrentAction = unit.isPreparingParadrop(), isCurrentAction = unit.isPreparingParadrop(),
useFrequency = 60f, // While it is important to see, it isn't nessesary used a lot 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 if (unit.isPreparingParadrop()) unit.action = null
else unit.action = UnitActionType.Paradrop.value else unit.action = UnitActionType.Paradrop.value
}.takeIf { }.takeIf {
!unit.hasUnitMovedThisTurn() && !unit.hasUnitMovedThisTurn()
tile.isFriendlyTerritory(unit.civ) &&
!tile.isWater
}) })
) )
} }

View File

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