From a0cf30831c4817a8f46ede7556f7fa8b948e0eb6 Mon Sep 17 00:00:00 2001 From: Xander Lenstra <71121390+xlenstra@users.noreply.github.com> Date: Wed, 30 Jun 2021 16:09:02 +0200 Subject: [PATCH] Updated promotions - make more generalizable, update to G&K (#4292) * Generalized the "Heal Instantly" promotion * Extended "Indirect Fire" to WaterRanged units, conform the main game * Generalized Extend Range, Operational Range * Generalized "logistics" * Typo * Generalized the healing from "Medic" * Implemented requested changes * Generalized "[amount] Movement"; "[amount] Visibility Range" * Added survavalism promotions * Updated Boarding Party strength bonus values to G&K * Implemented requested changes --- .../jsons/Civ V - Vanilla/UnitPromotions.json | 60 ++++++++++------ .../assets/jsons/Civ V - Vanilla/Units.json | 12 ++-- core/src/com/unciv/logic/battle/Battle.kt | 5 +- core/src/com/unciv/logic/map/MapUnit.kt | 71 ++++++++++++++----- core/src/com/unciv/logic/map/TileInfo.kt | 4 +- .../src/com/unciv/logic/map/UnitPromotions.kt | 13 +++- 6 files changed, 112 insertions(+), 53 deletions(-) diff --git a/android/assets/jsons/Civ V - Vanilla/UnitPromotions.json b/android/assets/jsons/Civ V - Vanilla/UnitPromotions.json index 00b9e4114f..8f4cafe140 100644 --- a/android/assets/jsons/Civ V - Vanilla/UnitPromotions.json +++ b/android/assets/jsons/Civ V - Vanilla/UnitPromotions.json @@ -2,7 +2,7 @@ { "name": "Heal Instantly", - "effect": "Heal this Unit by 50 HP; Doing so will consume this opportunity to choose a Promotion", + "uniques": ["Heal this Unit by [50] HP", "Doing so will consume this opportunity to choose a Promotion"], "unitTypes": ["Melee","Mounted","Scout","Siege","Ranged","Armor","WaterMelee","WaterRanged","WaterSubmarine"] }, @@ -52,14 +52,14 @@ { "name": "Extended Range", "prerequisites": ["Accuracy III","Barrage III","Targeting II","Bombardment II", "Wolfpack II"], - "effect": "+1 Range", + "effect": "[+1] Range", "unitTypes": ["Ranged","Siege","WaterRanged","WaterSubmarine"] }, { "name": "Indirect Fire", - "prerequisites": ["Accuracy III","Barrage III"], + "prerequisites": ["Accuracy III", "Barrage III", "Bombardment II", "Targeting II"], "effect": "Ranged attacks may be performed over obstacles", - "unitTypes": ["Ranged","Siege"] + "unitTypes": ["Ranged","Siege","WaterRanged"] }, // Melee, Mounted+Armor @@ -112,7 +112,7 @@ }, { "name": "Formation I", - "prerequisites": ["Shock II","Drill II"], + "prerequisites": ["Shock II","Drill II"], // G&K also has Accuracy II & Barrage II as possible prerequisites for this, but I couldn't find a source for the unittypes "effect": "+[33]% Strength vs [Mounted]", "unitTypes": ["Melee","Mounted"] }, @@ -126,13 +126,15 @@ { "name": "Blitz", "prerequisites": ["Shock III","Drill III"], - "effect": "1 additional attack per turn", + "effect": "[1] additional attacks per turn", "unitTypes": ["Melee","Mounted","Armor"] }, { "name": "Woodsman", "prerequisites": ["Shock III","Drill III"], - "effect": "Double movement rate through Forest and Jungle", + "effect": "Double movement rate through Forest and 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": ["Melee"] }, { @@ -143,33 +145,49 @@ }, { "name": "Medic", - "prerequisites": ["Shock I", "Drill I", "Scouting II"], - "effect": "This unit and all others in adjacent tiles heal 5 additional HP per turn", + "prerequisites": ["Shock I", "Drill I", "Scouting II", "Survivalism II"], + "uniques": ["[+5] HP when healing", "All adjacent units heal [5] HP when healing"], "unitTypes": ["Melee","Mounted","Scout"] }, { "name": "Medic II", "prerequisites": ["Medic"], - "effect": "This unit and all others in adjacent tiles heal 5 additional HP. This unit heals 5 additional HP outside of friendly territory.", + "uniques": ["[+5] HP when healing", "All adjacent units heal [5] HP when healing", + "[+5] HP when healing in [Foreign Land] tiles"], "unitTypes": ["Melee","Mounted","Scout"] }, // Scout { "name": "Scouting I", - "effect": "+1 Visibility Range", + "effect": "[+1] Visibility Range", "unitTypes": ["Scout"] }, { "name": "Scouting II", - "prerequisites": ["Scouting I"], - "effect": "+1 Movement", + "prerequisites": ["Scouting II"], + "effect": "[+1] Visibility Range", "unitTypes": ["Scout"] }, { "name": "Scouting III", - "prerequisites": ["Scouting II"], - "effect": "+1 Visibility Range", + "prerequisites": ["Scouting I"], + "effect": "[+1] Movement", + "unitTypes": ["Scout"] + }, + { + "name": "Survivalism I", + "uniques": ["[+5] HP when healing in [Foreign Land] tiles", "+[25]% Strength when defending"], + "unitTypes": ["Scout"] + }, + { + "name": "Survivalism II", + "uniques": ["[+5] HP when healing in [Foreign Land] tiles", "+[25]% Strength when defending"], + "unitTypes": ["Scout"] + }, + { + "name": "Survivalism III", + "uniques": ["Unit will heal every turn, even if it performs an action", "May withdraw before melee ([75]%)"], // This number is not based on any source "unitTypes": ["Scout"] }, @@ -177,19 +195,19 @@ // Water melee { "name": "Boarding Party I", - "effect": "+[33]% Strength vs [water units]", + "effect": "+[15]% Strength vs [water units]", "unitTypes": ["WaterMelee"] }, { "name": "Boarding Party II", "prerequisites": ["Boarding Party I"], - "effect": "+[33]% Strength vs [water units]", + "effect": "+[15]% Strength vs [water units]", "unitTypes": ["WaterMelee"] }, { "name": "Boarding Party III", "prerequisites": ["Boarding Party II"], - "effect": "+[33]% Strength vs [water units]", + "effect": "+[15]% Strength vs [water units]", "unitTypes": ["WaterMelee"] }, @@ -339,7 +357,7 @@ { "name": "Operational Range", "prerequisites": ["Interception I", /*"Dogfighting I",*/ "Siege I", "Bombardment I"], - "effect": "+2 Range", + "effect": "[+2] Range", "unitTypes": ["Fighter","Bomber"] }, { @@ -372,13 +390,13 @@ "name": "Mobility", "prerequisites": ["Shock II","Drill II","Targeting I", "Bombardment I","Boarding Party I", "Coastal Raider I", "Wolfpack I"], - "effect": "+1 Movement", + "effect": "+[1] Movement", "unitTypes": ["Mounted","WaterMelee","WaterRanged","Armor","WaterSubmarine"] }, { "name": "Sentry", "prerequisites": ["Accuracy I","Barrage I","Shock II","Drill II","Bombardment I","Targeting I","Boarding Party I","Coastal Raider I"], - "effect": "+1 Visibility Range", + "effect": "[+1] Visibility Range", "unitTypes": ["Melee","Mounted","WaterRanged","Armor","WaterMelee"] }, { diff --git a/android/assets/jsons/Civ V - Vanilla/Units.json b/android/assets/jsons/Civ V - Vanilla/Units.json index b63ad55557..7b9d731567 100644 --- a/android/assets/jsons/Civ V - Vanilla/Units.json +++ b/android/assets/jsons/Civ V - Vanilla/Units.json @@ -251,7 +251,7 @@ "requiredTech": "Bronze Working", "obsoleteTech": "Civil Service", "upgradesTo": "Pikeman", - "uniques": ["+[50]% Strength vs [Mounted]","+10 HP when healing"], + "uniques": ["+[50]% Strength vs [Mounted]","+[10] HP when healing"], "attackSound": "metalhit" }, /* @@ -493,7 +493,7 @@ "requiredResource": "Horses", "upgradesTo": "Cavalry", "obsoleteTech": "Military Science", - "uniques": ["Can move after attacking","No defensive terrain bonus", "Founds a new city", "+2 Visibility Range", "Defense bonus when embarked"], + "uniques": ["Can move after attacking","No defensive terrain bonus", "Founds a new city", "[+2] Visibility Range", "Defense bonus when embarked"], "attackSound": "horse" //Conquistador should have no penalty attacking cities //Ability to found new cities can only be used on a foreign continent that does not contain the Spanish capital. @@ -674,7 +674,7 @@ "requiredTech": "Astronomy", "upgradesTo": "Ironclad", "obsoleteTech": "Combustion", - "uniques": ["+1 Visibility Range","May withdraw before melee ([80]%)"], + "uniques": ["[+1] Visibility Range", "May withdraw before melee ([80]%)"], "hurryCostModifier": 30 }, { @@ -781,7 +781,7 @@ "rangedStrength": 35, "cost": 185, "requiredResource": "Iron", - "uniques": ["+1 Visibility Range"], + "uniques": ["[+1] Visibility Range"], "requiredTech": "Navigation", "obsoleteTech": "Electronics", "upgradesTo": "Battleship", @@ -812,7 +812,7 @@ "requiredTech": "Metallurgy", "requiredResource": "Horses", "uniques": ["Can move after attacking","No defensive terrain bonus","Penalty vs City 33%", - "+1 Visibility Range", "No movement cost to pillage"], + "[+1] Visibility Range", "No movement cost to pillage"], "promotions": ["Formation I"], "upgradesTo": "Anti-Tank Gun", "obsoleteTech": "Combined Arms", @@ -1289,7 +1289,7 @@ "cost": 425, "requiredTech": "Telecommunications", "uniques": ["+[75]% Strength when attacking", "Invisible to others", "Can only attack water", - "Can attack submarines", "Can enter ice tiles", "+1 Visibility Range", "Can carry [2] [Missile] units"] + "Can attack submarines", "Can enter ice tiles", "[+1] Visibility Range", "Can carry [2] [Missile] units"] }, { "name": "Mechanized Infantry", diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index 373c7dc68c..67984cf126 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -6,7 +6,6 @@ import com.unciv.logic.city.CityInfo import com.unciv.logic.civilization.* import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.logic.civilization.diplomacy.DiplomaticStatus -import com.unciv.logic.map.MapUnit import com.unciv.logic.map.RoadStatus import com.unciv.logic.map.TileInfo import com.unciv.models.AttackableTile @@ -15,7 +14,6 @@ import com.unciv.models.ruleset.unit.UnitType import com.unciv.models.stats.Stat import com.unciv.models.translations.tr import java.util.* -import kotlin.math.min import kotlin.math.max /** @@ -250,8 +248,7 @@ object Battle { private fun reduceAttackerMovementPointsAndAttacks(attacker: ICombatant, defender: ICombatant) { if (attacker is MapUnitCombatant) { val unit = attacker.unit - if (unit.hasUnique("Can move after attacking") - || (unit.hasUnique("1 additional attack per turn") && unit.attacksThisTurn == 0)) { + if (unit.hasUnique("Can move after attacking") || unit.maxAttacksPerTurn() > unit.attacksThisTurn) { // if it was a melee attack and we won, then the unit ALREADY got movement points deducted, // for the movement to the enemy's tile! // and if it's an air unit, it only has 1 movement anyway, so... diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index 54023e4ef9..b8c7706c48 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -141,8 +141,11 @@ class MapUnit { if (isEmbarked()) return getEmbarkedMovement() var movement = baseUnit.movement - movement += getUniques().count { it.text == "+1 Movement" } + movement += getMatchingUniques("[] Movement").sumBy { it.params[0].toInt() } + // Deprecated since 3.15.6 + movement += getUniques().count { it.text == "+1 Movement" } + // // Deprecated since 3.14.17 if (type.isMilitary() && type.isWaterUnit() && civInfo.hasUnique("All military naval units receive +1 movement and +1 sight")) { movement += 1 @@ -206,15 +209,21 @@ class MapUnit { */ private fun getVisibilityRange(): Int { var visibilityRange = 2 - visibilityRange += getUniques().count { it.text == "+1 Visibility Range" } for (unique in civInfo.getMatchingUniques("+[] Sight for all [] units")) if (matchesFilter(unique.params[1])) visibilityRange += unique.params[0].toInt() - if (hasUnique("+2 Visibility Range")) visibilityRange += 2 // This shouldn't be stackable + visibilityRange += getMatchingUniques("[] Visibility Range").sumBy { it.params[0].toInt() } + if (hasUnique("Limited Visibility")) visibilityRange -= 1 + + // Deprecated since 3.15.6 + visibilityRange += getUniques().count { it.text == "+1 Visibility Range" } + if (hasUnique("+2 Visibility Range")) visibilityRange += 2 // This shouldn't be stackable + // // Deprecated since 3.15.1 - if (civInfo.hasUnique("+1 Sight for all land military units") && type.isMilitary() && type.isLandUnit()) - visibilityRange += 1 + if (civInfo.hasUnique("+1 Sight for all land military units") && type.isMilitary() && type.isLandUnit()) + visibilityRange += 1 + // // Deprecated since 3.14.17 if (type.isMilitary() && type.isWaterUnit() && civInfo.hasUnique("All military naval units receive +1 movement and +1 sight")) @@ -273,18 +282,28 @@ class MapUnit { return true } + fun maxAttacksPerTurn(): Int { + var maxAttacksPerTurn = 1 + getMatchingUniques("[] additional attacks per turn").sumBy { it.params[0].toInt() } + // Deprecated since 3.15.6 + if (hasUnique("+1 additional attack per turn")) + maxAttacksPerTurn++ + // + return maxAttacksPerTurn + } + fun canAttack(): Boolean { if (currentMovement == 0f) return false - if (attacksThisTurn > 0 && !hasUnique("1 additional attack per turn")) return false - if (attacksThisTurn > 1) return false - return true + return attacksThisTurn < maxAttacksPerTurn() } fun getRange(): Int { if (type.isMelee()) return 1 var range = baseUnit().range - if (hasUnique("+1 Range")) range++ - if (hasUnique("+2 Range")) range += 2 + // Deprecated since 3.15.6 + if (hasUnique("+1 Range")) range++ + if (hasUnique("+2 Range")) range += 2 + // + range += getMatchingUniques("[] Range").sumBy { it.params[0].toInt() } return range } @@ -375,8 +394,11 @@ class MapUnit { private fun adjacentHealingBonus(): Int { var healingBonus = 0 - if (hasUnique("This unit and all others in adjacent tiles heal 5 additional HP per turn")) healingBonus += 5 - if (hasUnique("This unit and all others in adjacent tiles heal 5 additional HP. This unit heals 5 additional HP outside of friendly territory.")) healingBonus += 5 + healingBonus += getMatchingUniques("All adjacent units heal [] HP when healing").sumBy { it.params[0].toInt() } + // Deprecated since 3.15.6 + if (hasUnique("This unit and all others in adjacent tiles heal 5 additional HP per turn")) healingBonus += 5 + if (hasUnique("This unit and all others in adjacent tiles heal 5 additional HP. This unit heals 5 additional HP outside of friendly territory.")) healingBonus += 5 + // return healingBonus } @@ -507,7 +529,11 @@ class MapUnit { var amountToHealBy = rankTileForHealing(getTile()) if (amountToHealBy == 0) return - if (hasUnique("+10 HP when healing")) amountToHealBy += 10 + // Deprecated since 3.15.6 + if (hasUnique("+10 HP when healing")) amountToHealBy += 10 + // + amountToHealBy += getMatchingUniques("[] HP when healing").sumBy { it.params[0].toInt() } + val maxAdjacentHealingBonus = currentTile.getTilesInDistance(1) .flatMap { it.getUnits().asSequence() }.map { it.adjacentHealingBonus() }.maxOrNull() if (maxAdjacentHealingBonus != null) @@ -535,11 +561,20 @@ class MapUnit { else -> 5 // Enemy territory } - if (hasUnique("This unit and all others in adjacent tiles heal 5 additional HP. This unit heals 5 additional HP outside of friendly territory.") - && !isFriendlyTerritory - && healing > 0 - )// Additional healing from medic is only applied when the unit is able to heal - healing += 5 + // Deprecated since 3.15.6 + if (hasUnique("This unit and all others in adjacent tiles heal 5 additional HP. This unit heals 5 additional HP outside of friendly territory.") + && !isFriendlyTerritory + && healing > 0 + )// Additional healing from medic is only applied when the unit is able to heal + healing += 5 + // + if (healing > 0) { + for (unique in getMatchingUniques("[] HP when healing in [] tiles")) { + if (tileInfo.matchesFilter(unique.params[1])) { + healing += unique.params[0].toInt() + } + } + } return healing } diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index 1243595d2b..b6965cc273 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -415,8 +415,8 @@ open class TileInfo { naturalWonder -> true "Open terrain" -> !isRoughTerrain() "Rough terrain" -> isRoughTerrain() - "Foreign Land" -> observingCiv != null && !isFriendlyTerritory(observingCiv) - "Friendly Land" -> observingCiv != null && isFriendlyTerritory(observingCiv) + "Foreign Land", "Foreign" -> observingCiv != null && !isFriendlyTerritory(observingCiv) + "Friendly Land", "Friendly" -> observingCiv != null && isFriendlyTerritory(observingCiv) resource -> observingCiv != null && hasViewableResource(observingCiv) else -> { if (terrainFeatures.contains(filter)) return true diff --git a/core/src/com/unciv/logic/map/UnitPromotions.kt b/core/src/com/unciv/logic/map/UnitPromotions.kt index da1775c9a1..6a615f6439 100644 --- a/core/src/com/unciv/logic/map/UnitPromotions.kt +++ b/core/src/com/unciv/logic/map/UnitPromotions.kt @@ -26,8 +26,12 @@ class UnitPromotions{ numberOfPromotions++ } - if(promotionName=="Heal Instantly") unit.healBy(50) - else promotions.add(promotionName) + val promotion = unit.civInfo.gameInfo.ruleSet.unitPromotions[promotionName]!! + doDirectPromotionEffects(promotion) + + // This usage of a promotion name as its identifier is deprecated since 3.15.6 + if (promotionName != "Heal Instantly" && promotion.uniqueObjects.none { it.placeholderText == "Doing so will consume this opportunity to choose a Promotion" }) + promotions.add(promotionName) unit.updateUniques() @@ -36,6 +40,11 @@ class UnitPromotions{ // So, if the addPromotion was triggered from there, simply don't update unit.updateVisibleTiles() // some promotions/uniques give the unit bonus sight } + + fun doDirectPromotionEffects(promotion: Promotion) { + for (unique in promotion.uniqueObjects.filter { it.placeholderText == "Heal this unit by [] HP"}) + unit.healBy(unique.params[0].toInt()) + } fun getAvailablePromotions(): List { return unit.civInfo.gameInfo.ruleSet.unitPromotions.values