From cdbdda366be78543dfd9c23c92a3fee5cb4477a5 Mon Sep 17 00:00:00 2001 From: Xander Lenstra <71121390+xlenstra@users.noreply.github.com> Date: Sun, 2 Jan 2022 13:04:43 +0100 Subject: [PATCH] Revamped nukes again to closer match the original (#5892) --- core/src/com/unciv/logic/battle/Battle.kt | 137 +++++++--------------- docs/uniques.md | 72 ++++++------ 2 files changed, 74 insertions(+), 135 deletions(-) diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index c50b437064..08d131f51f 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -652,11 +652,7 @@ object Battle { for (tile in hitTiles) { // Handle complicated effects - when (strength) { - 1 -> nukeStrength1Effect(attacker, tile) - 2 -> nukeStrength2Effect(attacker, tile) - else -> nukeStrength1Effect(attacker, tile) - } + doNukeExplosion(attacker, tile, strength) } // Instead of postBattleAction() just destroy the unit, all other functions are not relevant @@ -672,66 +668,48 @@ object Battle { attacker.unit.attacksThisTurn += 1 } } - - // todo: reduce extreme code duplication, parameterize probabilities where an unique already used - private fun nukeStrength1Effect(attacker: MapUnitCombatant, tile: TileInfo) { + + private fun doNukeExplosion(attacker: MapUnitCombatant, tile: TileInfo, nukeStrength: Int) { // https://forums.civfanatics.com/resources/unit-guide-modern-future-units-g-k.25628/ // https://www.carlsguides.com/strategy/civilization5/units/aircraft-nukes.ph // Testing done by Ravignir + // original source code: GenerateNuclearExplosionDamage(), ApplyNuclearExplosionDamage() + var damageModifierFromMissingResource = 1f val civResources = attacker.getCivInfo().getCivResourcesByName() for (resource in attacker.unit.baseUnit.getResourceRequirements().keys) { if (civResources[resource]!! < 0 && !attacker.getCivInfo().isBarbarian()) damageModifierFromMissingResource *= 0.5f // I could not find a source for this number, but this felt about right } - - // Decrease health & population of a hit city + + // Damage city and reduce its population val city = tile.getCity() if (city != null && tile.position == city.location) { - var populationLoss = city.population.population * (0.3 + Random().nextFloat() * 0.4) - var populationLossReduced = false - // Deprecated since 3.16.11 - for (unique in city.getLocalMatchingUniques("Population loss from nuclear attacks -[]%")) { - populationLoss *= 1 - unique.params[0].toFloat() / 100f - populationLossReduced = true - } - // - for (unique in city.getMatchingUniques("Population loss from nuclear attacks []% []")) { - if (!city.matchesFilter(unique.params[1])) continue - populationLoss *= unique.params[0].toPercent() - populationLossReduced = true - } - if (city.population.population < 5 && !populationLossReduced) { - city.population.setPopulation(1) // For cities that cannot be destroyed, such as original capitals - city.destroyCity() - } else { - city.population.addPopulation(-populationLoss.toInt()) - if (city.population.population < 1) city.population.setPopulation(1) - city.population.unassignExtraPopulation() - city.health -= ((0.5 + 0.25 * Random().nextFloat()) * city.health * damageModifierFromMissingResource).toInt() - if (city.health < 1) city.health = 1 - } + doNukeExplosionDamageToCity(city, nukeStrength, damageModifierFromMissingResource) postBattleNotifications(attacker, CityCombatant(city), city.getCenterTile()) + destroyIfDefeated(city.civInfo, attacker.getCivInfo()) } - + // Damage and/or destroy units on the tile for (unit in tile.getUnits().toList()) { // toList so if it's destroyed there's no concurrent modification val defender = MapUnitCombatant(unit) - if (defender.unit.isCivilian()) { - unit.destroy() // destroy the unit - } else { + if (defender.unit.isCivilian() || nukeStrength >= 2) { + unit.destroy() + } else if (nukeStrength == 1) { defender.takeDamage(((40 + Random().nextInt(60)) * damageModifierFromMissingResource).toInt()) + } else if (nukeStrength == 0) { + defender.takeDamage(((20 + Random().nextInt(30)) * damageModifierFromMissingResource).toInt()) } postBattleNotifications(attacker, defender, defender.getTile()) destroyIfDefeated(defender.getCivInfo(), attacker.getCivInfo()) } - // Remove improvements, add fallout + // Pillage improvements, remove roads, add fallout if (tile.improvement != null && !tile.getTileImprovement()!!.hasUnique(UniqueType.Indestructible)) { + tile.turnsToImprovement = 2 + tile.improvementInProgress = tile.improvement tile.improvement = null } - tile.improvementInProgress = null - tile.turnsToImprovement = 0 tile.roadStatus = RoadStatus.None if (tile.isLand && !tile.isImpassible() && !tile.terrainFeatures.contains("Fallout")) { if (tile.terrainFeatures.any { attacker.getCivInfo().gameInfo.ruleSet.terrains[it]!!.hasUnique(UniqueType.ResistsNukes) }) { @@ -745,68 +723,33 @@ object Battle { } } } - - private fun nukeStrength2Effect(attacker: MapUnitCombatant, tile: TileInfo) { - // https://forums.civfanatics.com/threads/unit-guide-modern-future-units-g-k.429987/#2 - // https://www.carlsguides.com/strategy/civilization5/units/aircraft-nukes.php - // Testing done by Ravignir - var damageModifierFromMissingResource = 1f - val civResources = attacker.getCivInfo().getCivResourcesByName() - for (resource in attacker.unit.baseUnit.getResourceRequirements().keys) { - if (civResources[resource]!! < 0 && !attacker.getCivInfo().isBarbarian()) - damageModifierFromMissingResource *= 0.5f // I could not find a source for this number, but this felt about right + + private fun doNukeExplosionDamageToCity(targetedCity: CityInfo, nukeStrength: Int, damageModifierFromMissingResource: Float) { + if (nukeStrength > 1 && targetedCity.population.population < 5 && targetedCity.canBeDestroyed(true)) { + targetedCity.destroyCity() + return } + val cityCombatant = CityCombatant(targetedCity) + cityCombatant.takeDamage((cityCombatant.getHealth() * 0.5f * damageModifierFromMissingResource).toInt()) - // Damage and/or destroy cities - val city = tile.getCity() - if (city != null && city.location == tile.position) { - if (city.population.population < 5) { - city.population.setPopulation(1) // For cities that cannot be destroyed, such as original capitals - city.destroyCity() - } else { - var populationLoss = city.population.population * (0.6 + Random().nextFloat() * 0.2) - var populationLossReduced = false - for (unique in city.getMatchingUniques("Population loss from nuclear attacks []% []")) { - if (!city.matchesFilter(unique.params[1])) - populationLoss *= unique.params[0].toPercent() - populationLossReduced = true - } - city.population.addPopulation(-populationLoss.toInt()) - if (city.population.population < 5 && populationLossReduced) city.population.setPopulation(5) - if (city.population.population < 1) city.population.setPopulation(1) - city.population.unassignExtraPopulation() - city.health -= (0.5 * city.getMaxHealth() * damageModifierFromMissingResource).toInt() - if (city.health < 1) city.health = 1 + var populationLoss = targetedCity.population.population * + when (nukeStrength) { + 0 -> 0f + 1 -> (30 + Random().nextInt(40)) / 100f + 2 -> (60 + Random().nextInt(20)) / 100f + else -> 1f } - postBattleNotifications(attacker, CityCombatant(city), city.getCenterTile()) - destroyIfDefeated(city.civInfo, attacker.getCivInfo()) - } - - // Destroy all hit units - for (defender in tile.getUnits().toList()) { // toList to avoid concurrent modification exceptions - defender.destroy() - postBattleNotifications(attacker, MapUnitCombatant(defender), defender.currentTile) - destroyIfDefeated(defender.civInfo, attacker.getCivInfo()) - } - - // Remove improvements - if (tile.improvement != null && !tile.getTileImprovement()!!.hasUnique(UniqueType.Indestructible)) { - tile.improvement = null - } - tile.improvementInProgress = null - tile.turnsToImprovement = 0 - tile.roadStatus = RoadStatus.None - if (tile.isLand && !tile.isImpassible() && !tile.terrainFeatures.contains("Fallout")) { - if (tile.terrainFeatures.any { attacker.getCivInfo().gameInfo.ruleSet.terrains[it]!!.hasUnique(UniqueType.ResistsNukes) }) { - if (Random().nextFloat() < 0.25f) { - tile.terrainFeatures.removeAll { attacker.getCivInfo().gameInfo.ruleSet.terrains[it]!!.hasUnique(UniqueType.DestroyableByNukes) } - tile.terrainFeatures.add("Fallout") - } - } else if (Random().nextFloat() < 0.5f) { - tile.terrainFeatures.removeAll { attacker.getCivInfo().gameInfo.ruleSet.terrains[it]!!.hasUnique(UniqueType.DestroyableByNukes) } - tile.terrainFeatures.add("Fallout") + // Deprecated since 3.16.11 + for (unique in targetedCity.getLocalMatchingUniques("Population loss from nuclear attacks -[]%")) { + populationLoss *= 1 - unique.params[0].toFloat() / 100f } + // + for (unique in targetedCity.getMatchingUniques("Population loss from nuclear attacks []% []")) { + if (!targetedCity.matchesFilter(unique.params[1])) continue + populationLoss *= unique.params[0].toPercent() } + targetedCity.population.addPopulation(-populationLoss.toInt()) + if (targetedCity.population.population < 1) targetedCity.population.setPopulation(1) } private fun tryInterceptAirAttack(attacker: MapUnitCombatant, attackedTile:TileInfo, interceptingCiv:CivilizationInfo) { diff --git a/docs/uniques.md b/docs/uniques.md index d66f837765..138bac6223 100644 --- a/docs/uniques.md +++ b/docs/uniques.md @@ -23,17 +23,17 @@ Applicable to: Global, FollowerBelief, Improvement #### [stats] [cityFilter] Example: "[+1 Gold, +2 Production] [in all cities]" -Applicable to: Global +Applicable to: Global, FollowerBelief #### [stats] from every specialist [cityFilter] Example: "[+1 Gold, +2 Production] from every specialist [in all cities]" -Applicable to: Global +Applicable to: Global, FollowerBelief #### [stats] per [amount] population [cityFilter] Example: "[+1 Gold, +2 Production] per [20] population [in all cities]" -Applicable to: Global +Applicable to: Global, FollowerBelief #### [stats] in cities with [amount] or more population Example: "[+1 Gold, +2 Production] in cities with [20] or more population" @@ -45,11 +45,6 @@ Example: "[+1 Gold, +2 Production] in cities on [Grassland] tiles" Applicable to: Global, FollowerBelief -#### [stats] per turn from cities before [tech/policy] -Example: "[+1 Gold, +2 Production] per turn from cities before [tech/policy]" - -Applicable to: Global - #### [stats] whenever a Great Person is expended Example: "[+1 Gold, +2 Production] whenever a Great Person is expended" @@ -58,7 +53,7 @@ Applicable to: Global #### [stats] from [tileFilter] tiles [cityFilter] Example: "[+1 Gold, +2 Production] from [Farm] tiles [in all cities]" -Applicable to: Global +Applicable to: Global, FollowerBelief #### [stats] from [tileFilter] tiles without [tileFilter] [cityFilter] Example: "[+1 Gold, +2 Production] from [Farm] tiles without [Farm] [in all cities]" @@ -73,7 +68,7 @@ Applicable to: Global, FollowerBelief #### [amount]% [stat] Example: "[20]% [Culture]" -Applicable to: Global +Applicable to: Global, FollowerBelief #### [amount]% [stat] from City-States Example: "[20]% [Culture] from City-States" @@ -83,7 +78,7 @@ Applicable to: Global #### [amount]% [stat] [cityFilter] Example: "[20]% [Culture] [in all cities]" -Applicable to: Global +Applicable to: Global, FollowerBelief #### [amount]% Production when constructing [buildingFilter] wonders [cityFilter] Example: "[20]% Production when constructing [buildingFilter] wonders [in all cities]" @@ -93,17 +88,17 @@ Applicable to: Global, FollowerBelief, Resource #### [amount]% Production when constructing [buildingFilter] buildings [cityFilter] Example: "[20]% Production when constructing [buildingFilter] buildings [in all cities]" -Applicable to: Global +Applicable to: Global, FollowerBelief #### [amount]% Production when constructing [baseUnitFilter] units [cityFilter] Example: "[20]% Production when constructing [Melee] units [in all cities]" -Applicable to: Global +Applicable to: Global, FollowerBelief #### [amount]% unhappiness from population [cityFilter] Example: "[20]% unhappiness from population [in all cities]" -Applicable to: Global +Applicable to: Global, FollowerBelief #### Military Units gifted from City-States start with [amount] XP Example: "Military Units gifted from City-States start with [20] XP" @@ -154,7 +149,7 @@ Applicable to: Global #### [amount]% food consumption by specialists [cityFilter] Example: "[20]% food consumption by specialists [in all cities]" -Applicable to: Global +Applicable to: Global, FollowerBelief #### [amount]% of excess happiness converted to [stat] Example: "[20]% of excess happiness converted to [Culture]" @@ -164,12 +159,12 @@ Applicable to: Global #### May buy [baseUnitFilter] units for [amount] [stat] [cityFilter] at an increasing price ([amount]) Example: "May buy [Melee] units for [20] [Culture] [in all cities] at an increasing price ([20])" -Applicable to: Global +Applicable to: Global, FollowerBelief #### May buy [buildingFilter] buildings for [amount] [stat] [cityFilter] at an increasing price ([amount]) Example: "May buy [buildingFilter] buildings for [20] [Culture] [in all cities] at an increasing price ([20])" -Applicable to: Global +Applicable to: Global, FollowerBelief #### May buy [baseUnitFilter] units for [amount] [stat] [cityFilter] Example: "May buy [Melee] units for [20] [Culture] [in all cities]" @@ -201,6 +196,19 @@ Example: "May buy [buildingFilter] buildings with [Culture] for [20] times their Applicable to: Global, FollowerBelief +#### Receive a free Great Person at the end of every [comment] (every 394 years), after researching [tech]. Each bonus person can only be chosen once. +Example: "Receive a free Great Person at the end of every [comment] (every 394 years), after researching [tech]. Each bonus person can only be chosen once." + +Applicable to: Global + +#### Once The Long Count activates, the year on the world screen displays as the traditional Mayan Long Count. +Applicable to: Global + +#### Retain [amount]% of the happiness from a luxury after the last copy has been traded away +Example: "Retain [20]% of the happiness from a luxury after the last copy has been traded away" + +Applicable to: Global + #### Enables Research agreements Applicable to: Global @@ -210,6 +218,11 @@ Applicable to: Global #### Triggers a Cultural Victory upon completion Applicable to: Global +#### Cannot build [baseUnitFilter] units +Example: "Cannot build [Melee] units" + +Applicable to: Global + #### [amount]% Strength Example: "[20]% Strength" @@ -348,24 +361,6 @@ Applicable to: Global #### Will not be chosen for new games Applicable to: Nation -#### Receive a free Great Person at the end of every [comment] (every 394 years), after researching [tech]. Each bonus person can only be chosen once. -Example: "Receive a free Great Person at the end of every [comment] (every 394 years), after researching [tech]. Each bonus person can only be chosen once." - -Applicable to: Nation - -#### Once The Long Count activates, the year on the world screen displays as the traditional Mayan Long Count. -Applicable to: Nation - -#### Retain [amount]% of the happiness from a luxury after the last copy has been traded away -Example: "Retain [20]% of the happiness from a luxury after the last copy has been traded away" - -Applicable to: Nation - -#### Cannot build [baseUnitFilter] units -Example: "Cannot build [Melee] units" - -Applicable to: Nation - ## FollowerBelief uniques #### [amount]% [stat] from every follower, up to [amount]% Example: "[20]% [Culture] from every follower, up to [20]%" @@ -378,9 +373,6 @@ Example: "Earn [20]% of [Wounded] unit's [costOrStrength] as [Culture] when kill Applicable to: FollowerBelief ## Building uniques -#### Remove extra unhappiness from annexed cities -Applicable to: Building - #### Consumes [amount] [resource] Example: "Consumes [20] [Iron]" @@ -465,6 +457,9 @@ Applicable to: Building #### Unsellable Applicable to: Building +#### Remove extra unhappiness from annexed cities +Applicable to: Building + #### Spaceship part Applicable to: Building, Unit @@ -1044,6 +1039,7 @@ Example: "" Applicable to: Conditional ## Deprecated uniques + - "[stats] per turn from cities before [tech/policy]" - Deprecated As of 3.18.14, replace with "[stats] [in all cities] OR [stats] [in all cities] " - "+[amount]% [stat] [cityFilter]" - Deprecated As of 3.17.10, replace with "[+amount]% [stat] [cityFilter]" - "+[amount]% [stat] in all cities" - Deprecated As of 3.17.10, replace with "[+amount]% [stat] [in all cities]" - "[amount]% [stat] while the empire is happy" - Deprecated As of 3.17.1, replace with "[amount]% [stat] [in all cities] "