diff --git a/android/ImagesToPackSeparately/UnitIcons/Paratrooper.png b/android/ImagesToPackSeparately/UnitIcons/Paratrooper.png new file mode 100644 index 0000000000..1a1b78d1a2 Binary files /dev/null and b/android/ImagesToPackSeparately/UnitIcons/Paratrooper.png differ diff --git a/android/assets/UnitIcons.atlas b/android/assets/UnitIcons.atlas index 9136cc5b86..9553d0a871 100644 --- a/android/assets/UnitIcons.atlas +++ b/android/assets/UnitIcons.atlas @@ -487,132 +487,139 @@ Panzer orig: 100, 100 offset: 0, 0 index: -1 -Persian Immortal +Paratrooper rotate: false xy: 1430, 308 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Pikeman +Persian Immortal rotate: false xy: 1532, 410 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Rifleman +Pikeman rotate: false xy: 1328, 105 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Rocket Artillery +Rifleman rotate: false xy: 1430, 206 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Samurai +Rocket Artillery rotate: false xy: 1532, 308 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Scout +Samurai rotate: false xy: 1634, 410 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Settler +Scout rotate: false xy: 1328, 3 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Ship of the Line +Settler rotate: false xy: 1430, 104 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Sipahi +Ship of the Line rotate: false xy: 1430, 2 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Slinger +Sipahi rotate: false xy: 1532, 206 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Spearman +Slinger rotate: false xy: 1634, 308 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Stealth Bomber +Spearman rotate: false xy: 1736, 410 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Submarine +Stealth Bomber rotate: false xy: 1532, 104 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Swordsman +Submarine rotate: false xy: 1532, 2 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Tank +Swordsman rotate: false xy: 1634, 206 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Tercio +Tank rotate: false xy: 1736, 308 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Trebuchet +Tercio rotate: false xy: 1838, 410 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 -Triplane +Trebuchet rotate: false xy: 1634, 104 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 +Triplane + rotate: false + xy: 1634, 2 + size: 100, 100 + orig: 100, 100 + offset: 0, 0 + index: -1 Trireme rotate: false xy: 1736, 205 @@ -622,49 +629,49 @@ Trireme index: -1 Turtle Ship rotate: false - xy: 1634, 2 + xy: 1838, 308 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 War Chariot rotate: false - xy: 1838, 308 + xy: 1940, 410 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 War Elephant rotate: false - xy: 1940, 410 + xy: 1736, 103 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 Warrior rotate: false - xy: 1736, 103 + xy: 1838, 206 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 Work Boats rotate: false - xy: 1838, 206 + xy: 1940, 308 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 Worker rotate: false - xy: 1940, 308 + xy: 1838, 104 size: 100, 100 orig: 100, 100 offset: 0, 0 index: -1 Zero rotate: false - xy: 1838, 104 + xy: 1838, 2 size: 100, 100 orig: 100, 100 offset: 0, 0 diff --git a/android/assets/UnitIcons.png b/android/assets/UnitIcons.png index ba9e72ac87..e0bf572db7 100644 Binary files a/android/assets/UnitIcons.png and b/android/assets/UnitIcons.png differ diff --git a/android/assets/jsons/Civ V - Vanilla/Units.json b/android/assets/jsons/Civ V - Vanilla/Units.json index d781a5f6de..73b2927fd6 100644 --- a/android/assets/jsons/Civ V - Vanilla/Units.json +++ b/android/assets/jsons/Civ V - Vanilla/Units.json @@ -1152,7 +1152,6 @@ "uniques": ["+[20]% Strength in [Foreign Land]"], "attackSound": "shot" }, - /* { "name": "Paratrooper", "unitType": "Melee", @@ -1160,11 +1159,10 @@ "strength": 40, "cost": 375, "requiredTech": "Radar", - "uniques": ["May Paradrop"], + "uniques": ["May Paradrop up to [5] tiles from inside friendly territory"], "attackSound": "shot" // 65 strength in expansions, upgradesTo "XCOM Squad", "No Movement Cost to Pillage" in BNW }, - */ { "name": "Infantry", "unitType": "Melee", diff --git a/android/assets/jsons/translations/Dutch.properties b/android/assets/jsons/translations/Dutch.properties index 04c21ae922..7d97c9d7a4 100644 --- a/android/assets/jsons/translations/Dutch.properties +++ b/android/assets/jsons/translations/Dutch.properties @@ -515,6 +515,7 @@ Sleep = Slaap Sleep until healed = Slaap tot geheeld Moving = Bewegen Set up = Opstellen +Paradrop = Parachutelanding Upgrade to [unitType] ([goldCost] gold) = Waardeer [unitType] op ([goldCost] goud) Found city = Stad stichten Promote = Promoveren @@ -522,7 +523,7 @@ Health = Gezondheid Disband unit = Eenheid opheffen Explore = Ontdekken Stop exploration = Stop ontdekken -Pillage = Plundering +Pillage = Plunderen Do you really want to disband this unit? = Wil je echt deze eenheid opheffen Disband this unit for [goldAmount] gold? = Deze eenheid ontbinden voor [goldAmount] goud? Create [improvement] = Maak [improvement] @@ -919,7 +920,7 @@ Land = Land Wounded = Gewond Marine = Marine Mobile SAM = Mobiele grond-lucht raket -Paratrooper = Paracommando +Paratrooper = Parachutist Helicopter Gunship = Helicopter Wapenschip Atomic Bomb = Atoombom Unbuildable = Onbouwbaar diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 79a02d55fb..c345f78fe5 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -504,6 +504,7 @@ Sleep = Sleep until healed = Moving = Set up = +Paradrop = Upgrade to [unitType] ([goldCost] gold) = Found city = Promote = diff --git a/core/src/com/unciv/Constants.kt b/core/src/com/unciv/Constants.kt index 63e733ffe8..769b3e8e5a 100644 --- a/core/src/com/unciv/Constants.kt +++ b/core/src/com/unciv/Constants.kt @@ -57,6 +57,8 @@ object Constants { const val unitActionSleepUntilHealed = "Sleep until healed" const val unitActionAutomation = "Automate" const val unitActionExplore = "Explore" + const val unitActionParadrop = "Paradrop" + const val futureTech = "Future Tech" const val cancelImprovementOrder = "Cancel improvement order" diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index e0f2e8ca32..a65249dc19 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -71,6 +71,9 @@ class MapUnit { @Transient var cannotEnterOceanTilesUntilAstronomy = false + @Transient + var paradropRange = 0 + lateinit var owner: String /** @@ -547,6 +550,9 @@ class MapUnit { action = null // wake up when healed } + if (action == Constants.unitActionParadrop) + action = null + getCitadelDamage() getTerrainDamage() } diff --git a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt index 7fea549464..7677abc642 100644 --- a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt +++ b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt @@ -2,6 +2,7 @@ package com.unciv.logic.map import com.badlogic.gdx.math.Vector2 import com.unciv.Constants +import com.unciv.logic.HexMath.getDistance import com.unciv.logic.civilization.CivilizationInfo class UnitMovementAlgorithms(val unit:MapUnit) { @@ -169,8 +170,8 @@ class UnitMovementAlgorithms(val unit:MapUnit) { val currentTile = unit.getTile() if (currentTile == finalDestination) return currentTile - // head there directly - if (unit.type.isAirUnit()) return finalDestination + // If we can fly, head there directly + if (unit.type.isAirUnit() || unit.action == Constants.unitActionParadrop) return finalDestination val distanceToTiles = getDistanceToTiles() @@ -209,6 +210,8 @@ class UnitMovementAlgorithms(val unit:MapUnit) { fun canReach(destination: TileInfo): Boolean { if (unit.type.isAirUnit()) return unit.currentTile.aerialDistanceTo(destination) <= unit.getRange()*2 + if (unit.action == Constants.unitActionParadrop) + return getDistance(unit.currentTile.position, destination.position) <= unit.paradropRange && canParadropOn(destination) return getShortestPath(destination).any() } @@ -239,13 +242,27 @@ class UnitMovementAlgorithms(val unit:MapUnit) { fun moveToTile(destination: TileInfo) { if (destination == unit.getTile()) return // already here! - if (unit.type.isAirUnit()) { // they move differently from all other units + if (unit.type.isAirUnit()) { // air units move differently from all other units unit.action = null unit.removeFromTile() unit.isTransported = false // it has left the carrier by own means unit.putInTile(destination) unit.currentMovement = 0f return + } else if (unit.action == Constants.unitActionParadrop) { // paratrooping move differently + unit.action = null + unit.removeFromTile() + unit.putInTile(destination) + unit.currentMovement -= 1f + // Check if unit maintenance changed + // Is also done for other units, but because we skip everything else, we have to manually check it + // The reasong we skip everything, is that otherwise `getPathToTile()` throws an exception + // As we can not reach our destination in a single turn + if (unit.canGarrison() + && (unit.getTile().isCityCenter() || destination.isCityCenter()) + && unit.civInfo.hasUnique("Units in cities cost no Maintenance") + ) unit.civInfo.updateStatsForNextTurn() + return } val distanceToTiles = getDistanceToTiles() @@ -326,7 +343,14 @@ class UnitMovementAlgorithms(val unit:MapUnit) { } return false } - + + // Can a paratrooper land at this tile? + fun canParadropOn(destination: TileInfo): Boolean { + // Can only move to land tiles within range that are visible and not impassible + // Based on some testing done in the base game + if (!destination.isLand || destination.isImpassible() || !unit.civInfo.viewableTiles.contains(destination)) return false + return true + } // This is the most called function in the entire game, // so multiple callees of this function have been optimized, diff --git a/core/src/com/unciv/models/UnitAction.kt b/core/src/com/unciv/models/UnitAction.kt index c4429c5693..b5103576c8 100644 --- a/core/src/com/unciv/models/UnitAction.kt +++ b/core/src/com/unciv/models/UnitAction.kt @@ -10,17 +10,18 @@ data class UnitAction( enum class UnitActionType(val value: String) { Automate("Automate"), - StopMovement("Stop movement"), StopAutomation("Stop automation"), - StopExploration("Stop exploration"), + StopMovement("Stop movement"), Sleep("Sleep"), SleepUntilHealed("Sleep until healed"), Fortify("Fortify"), FortifyUntilHealed("Fortify until healed"), Explore("Explore"), + StopExploration("Stop exploration"), Promote("Promote"), Upgrade("Upgrade"), Pillage("Pillage"), + Paradrop("Paradrop"), SetUp("Set up"), FoundCity("Found city"), ConstructImprovement("Construct improvement"), diff --git a/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt b/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt index d5a9367c9b..f4dc3a05e8 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt @@ -218,7 +218,11 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap val turnsToGetThere = if (unit.type.isAirUnit()) { if (unit.movement.canReach(tileInfo)) 1 else 0 - } else unit.movement.getShortestPath(tileInfo).size // this is what takes the most time, tbh + } else if (unit.action == Constants.unitActionParadrop) { + if (unit.movement.canReach(tileInfo)) 1 + else 0 + } else + unit.movement.getShortestPath(tileInfo).size // this is what takes the most time, tbh unitToTurnsToTile[unit] = turnsToGetThere } @@ -393,11 +397,15 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap } val isAirUnit = unit.type.isAirUnit() - val tilesInMoveRange = - if (isAirUnit) - unit.getTile().getTilesInDistanceRange(IntRange(1, unit.getRange() * 2)) - else - unit.movement.getDistanceToTiles().keys.asSequence() + val moveTileOverlayColor = if (unit.action == Constants.unitActionParadrop) Color.BLUE else Color.WHITE + val tilesInMoveRange = + if (isAirUnit) + unit.getTile().getTilesInDistanceRange(IntRange(1, unit.getRange() * 2)) + else if (unit.action == Constants.unitActionParadrop) + unit.getTile().getTilesInDistance(unit.paradropRange) + .filter { unit.movement.canParadropOn(it) } + else + unit.movement.getDistanceToTiles().keys.asSequence() for (tile in tilesInMoveRange) { for (tileToColor in tileGroups[tile]!!) { @@ -411,7 +419,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap } if (unit.movement.canMoveTo(tile) || unit.movement.isUnknownTileWeShouldAssumeToBePassable(tile) && !unit.type.isAirUnit()) - tileToColor.showCircle(Color.WHITE, + tileToColor.showCircle(moveTileOverlayColor, if (UncivGame.Current.settings.singleTapMove || isAirUnit) 0.7f else 0.3f) } } diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt index 691f27761e..8db5880b23 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt @@ -51,6 +51,7 @@ object UnitActions { addPromoteAction(unit, actionList) addUnitUpgradeAction(unit, actionList) addPillageAction(unit, actionList, worldScreen) + addParadropAction(unit, actionList, worldScreen) addSetupAction(unit, actionList) addFoundCityAction(unit, actionList, tile) addWorkerActions(unit, actionList, tile, worldScreen, unitTable) @@ -152,6 +153,26 @@ object UnitActions { }.takeIf { unit.currentMovement > 0 && !isSetUp }) } + private fun addParadropAction(unit: MapUnit, actionList: ArrayList, worldScreen: WorldScreen) { + val paradropUniques = unit.getMatchingUniques("May Paradrop up to [] tiles from inside friendly territory") + if (!paradropUniques.any() || unit.isEmbarked()) return + unit.paradropRange = paradropUniques.maxOfOrNull { it.params[0] }!!.toInt() + actionList += UnitAction(UnitActionType.Paradrop, + isCurrentAction = unit.action == Constants.unitActionParadrop, + action = { + if (unit.action != Constants.unitActionParadrop) { + unit.action = Constants.unitActionParadrop + } else { + unit.action = null + } + }.takeIf { + unit.currentMovement == unit.getMaxMovement().toFloat() && + unit.currentTile.isFriendlyTerritory(unit.civInfo) && + !unit.isEmbarked() + }) + + } + private fun addPillageAction(unit: MapUnit, actionList: ArrayList, worldScreen: WorldScreen) { val pillageAction = getPillageAction(unit) if (pillageAction == null) return diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActionsTable.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActionsTable.kt index 99599cd13b..ac9b87906e 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActionsTable.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActionsTable.kt @@ -49,12 +49,13 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() { "Start Golden Age" -> return UnitIconAndKey(ImageGetter.getUnitIcon("Great Artist"), 'g') "Hurry Wonder" -> return UnitIconAndKey(ImageGetter.getUnitIcon("Great Engineer"), 'g') "Conduct Trade Mission" -> return UnitIconAndKey(ImageGetter.getUnitIcon("Great Merchant"), 'g') + "Construct road" -> return UnitIconAndKey(ImageGetter.getImprovementIcon("Road"), 'r') + "Paradrop" -> return UnitIconAndKey(ImageGetter.getUnitIcon("Paratrooper"), 'p') "Set up" -> return UnitIconAndKey(ImageGetter.getUnitIcon("Catapult"), 't') - "Disband unit" -> return UnitIconAndKey(ImageGetter.getImage("OtherIcons/DisbandUnit")) "Explore" -> return UnitIconAndKey(ImageGetter.getUnitIcon("Scout"), 'x') "Stop exploration" -> return UnitIconAndKey(ImageGetter.getImage("OtherIcons/Stop"), 'x') "Pillage" -> return UnitIconAndKey(ImageGetter.getImage("OtherIcons/Pillage"), 'p') - "Construct road" -> return UnitIconAndKey(ImageGetter.getImprovementIcon("Road"), 'r') + "Disband unit" -> return UnitIconAndKey(ImageGetter.getImage("OtherIcons/DisbandUnit")) else -> return UnitIconAndKey(ImageGetter.getImage("OtherIcons/Star")) } } diff --git a/docs/Credits.md b/docs/Credits.md index 6e325784bb..7e8c9f5683 100644 --- a/docs/Credits.md +++ b/docs/Credits.md @@ -104,6 +104,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https: * [Nuclear Missile](https://thenounproject.com/marialuisa.iborra/collection/missiles-bombs/?i=1022574) By Lluisa Iborra, ES * Icon for Carrier made by [JackRainy](https://github.com/JackRainy), based on [Aircraft Carrier](https://thenounproject.com/icolabs/collection/flat-icons-transport/?i=2332914) By IcoLabs, BR * [Water Gun](https://thenounproject.com/term/water-gun/2121571) by ProSymbols for Marine +* [Parachute](https://thenounproject.com/term/parachute/2025018) by Nociconist for Paratrooper* ### Great People