mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-29 06:51:30 -04:00
chore: Separated Nuke logic into separate object
This commit is contained in:
parent
348910dcf7
commit
cc6ab7f7d5
@ -433,7 +433,7 @@ object NextTurnAutomation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun valueCityStateAlliance(civInfo: Civilization, cityState: Civilization): Int {
|
internal fun valueCityStateAlliance(civInfo: Civilization, cityState: Civilization): Int {
|
||||||
var value = 0
|
var value = 0
|
||||||
|
|
||||||
if (civInfo.wantsToFocusOn(Victory.Focus.Culture) && cityState.cityStateFunctions.canProvideStat(Stat.Culture)) {
|
if (civInfo.wantsToFocusOn(Victory.Focus.Culture) && cityState.cityStateFunctions.canProvideStat(Stat.Culture)) {
|
||||||
@ -1284,31 +1284,6 @@ object NextTurnAutomation {
|
|||||||
diplomacyManager.removeFlag(DiplomacyFlags.SettledCitiesNearUs)
|
diplomacyManager.removeFlag(DiplomacyFlags.SettledCitiesNearUs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Handle decision making after city conquest, namely whether the AI should liberate, puppet,
|
|
||||||
* or raze a city */
|
|
||||||
fun onConquerCity(civInfo: Civilization, city: City) {
|
|
||||||
if (!city.hasDiplomaticMarriage()) {
|
|
||||||
val foundingCiv = civInfo.gameInfo.getCivilization(city.foundingCiv)
|
|
||||||
var valueAlliance = valueCityStateAlliance(civInfo, foundingCiv)
|
|
||||||
if (civInfo.getHappiness() < 0)
|
|
||||||
valueAlliance -= civInfo.getHappiness() // put extra weight on liberating if unhappy
|
|
||||||
if (foundingCiv.isCityState() && city.civ != civInfo && foundingCiv != civInfo
|
|
||||||
&& !civInfo.isAtWarWith(foundingCiv)
|
|
||||||
&& valueAlliance > 0) {
|
|
||||||
city.liberateCity(civInfo)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
city.puppetCity(civInfo)
|
|
||||||
if ((city.population.population < 4 || civInfo.isCityState())
|
|
||||||
&& city.foundingCiv != civInfo.civName && city.canBeDestroyed(justCaptured = true)) {
|
|
||||||
// raze if attacker is a city state
|
|
||||||
if (!civInfo.hasUnique(UniqueType.MayNotAnnexCities)) { city.annexCity() }
|
|
||||||
city.isBeingRazed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMinDistanceBetweenCities(civ1: Civilization, civ2: Civilization): Int {
|
fun getMinDistanceBetweenCities(civ1: Civilization, civ2: Civilization): Int {
|
||||||
return getClosestCities(civ1, civ2)?.aerialDistance ?: Int.MAX_VALUE
|
return getClosestCities(civ1, civ2)?.aerialDistance ?: Int.MAX_VALUE
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
import com.unciv.Constants
|
import com.unciv.Constants
|
||||||
import com.unciv.logic.automation.Automation
|
import com.unciv.logic.automation.Automation
|
||||||
import com.unciv.logic.battle.Battle
|
|
||||||
import com.unciv.logic.battle.GreatGeneralImplementation
|
import com.unciv.logic.battle.GreatGeneralImplementation
|
||||||
import com.unciv.logic.battle.MapUnitCombatant
|
import com.unciv.logic.battle.MapUnitCombatant
|
||||||
|
import com.unciv.logic.battle.Nuke
|
||||||
import com.unciv.logic.battle.TargetHelper
|
import com.unciv.logic.battle.TargetHelper
|
||||||
import com.unciv.logic.city.City
|
import com.unciv.logic.city.City
|
||||||
import com.unciv.logic.civilization.Civilization
|
import com.unciv.logic.civilization.Civilization
|
||||||
@ -496,7 +496,7 @@ object SpecificUnitAutomation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (highestTileNukeValue > 0) {
|
if (highestTileNukeValue > 0) {
|
||||||
Battle.NUKE(MapUnitCombatant(unit), tileToNuke!!)
|
Nuke.NUKE(MapUnitCombatant(unit), tileToNuke!!)
|
||||||
}
|
}
|
||||||
tryRelocateToNearbyAttackableCities(unit)
|
tryRelocateToNearbyAttackableCities(unit)
|
||||||
}
|
}
|
||||||
@ -507,7 +507,7 @@ object SpecificUnitAutomation {
|
|||||||
*/
|
*/
|
||||||
fun getNukeLocationValue(nuke: MapUnit, tile: Tile): Int {
|
fun getNukeLocationValue(nuke: MapUnit, tile: Tile): Int {
|
||||||
val civ = nuke.civ
|
val civ = nuke.civ
|
||||||
if (!Battle.mayUseNuke(MapUnitCombatant(nuke), tile)) return Int.MIN_VALUE
|
if (!Nuke.mayUseNuke(MapUnitCombatant(nuke), tile)) return Int.MIN_VALUE
|
||||||
val blastRadius = nuke.getNukeBlastRadius()
|
val blastRadius = nuke.getNukeBlastRadius()
|
||||||
val tilesInBlastRadius = tile.getTilesInDistance(blastRadius)
|
val tilesInBlastRadius = tile.getTilesInDistance(blastRadius)
|
||||||
val civsInBlastRadius = tilesInBlastRadius.mapNotNull { it.getOwner() } +
|
val civsInBlastRadius = tilesInBlastRadius.mapNotNull { it.getOwner() } +
|
||||||
|
@ -4,11 +4,9 @@ import com.badlogic.gdx.math.Vector2
|
|||||||
import com.unciv.Constants
|
import com.unciv.Constants
|
||||||
import com.unciv.UncivGame
|
import com.unciv.UncivGame
|
||||||
import com.unciv.logic.automation.civilization.NextTurnAutomation
|
import com.unciv.logic.automation.civilization.NextTurnAutomation
|
||||||
import com.unciv.logic.automation.unit.SpecificUnitAutomation
|
|
||||||
import com.unciv.logic.city.City
|
import com.unciv.logic.city.City
|
||||||
import com.unciv.logic.civilization.AlertType
|
import com.unciv.logic.civilization.AlertType
|
||||||
import com.unciv.logic.civilization.Civilization
|
import com.unciv.logic.civilization.Civilization
|
||||||
import com.unciv.logic.civilization.CivilopediaAction
|
|
||||||
import com.unciv.logic.civilization.LocationAction
|
import com.unciv.logic.civilization.LocationAction
|
||||||
import com.unciv.logic.civilization.MapUnitAction
|
import com.unciv.logic.civilization.MapUnitAction
|
||||||
import com.unciv.logic.civilization.NotificationCategory
|
import com.unciv.logic.civilization.NotificationCategory
|
||||||
@ -16,25 +14,19 @@ import com.unciv.logic.civilization.NotificationIcon
|
|||||||
import com.unciv.logic.civilization.PlayerType
|
import com.unciv.logic.civilization.PlayerType
|
||||||
import com.unciv.logic.civilization.PopupAlert
|
import com.unciv.logic.civilization.PopupAlert
|
||||||
import com.unciv.logic.civilization.PromoteUnitAction
|
import com.unciv.logic.civilization.PromoteUnitAction
|
||||||
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
|
|
||||||
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
|
|
||||||
import com.unciv.logic.map.mapunit.MapUnit
|
import com.unciv.logic.map.mapunit.MapUnit
|
||||||
import com.unciv.logic.map.tile.RoadStatus
|
|
||||||
import com.unciv.logic.map.tile.Tile
|
import com.unciv.logic.map.tile.Tile
|
||||||
import com.unciv.models.UnitActionType
|
import com.unciv.models.UnitActionType
|
||||||
import com.unciv.ui.components.UnitMovementMemoryType
|
|
||||||
import com.unciv.models.ruleset.unique.StateForConditionals
|
import com.unciv.models.ruleset.unique.StateForConditionals
|
||||||
import com.unciv.models.ruleset.unique.Unique
|
import com.unciv.models.ruleset.unique.Unique
|
||||||
import com.unciv.models.ruleset.unique.UniqueTriggerActivation
|
import com.unciv.models.ruleset.unique.UniqueTriggerActivation
|
||||||
import com.unciv.models.ruleset.unique.UniqueType
|
import com.unciv.models.ruleset.unique.UniqueType
|
||||||
import com.unciv.models.stats.Stat
|
import com.unciv.models.stats.Stat
|
||||||
import com.unciv.models.stats.Stats
|
import com.unciv.models.stats.Stats
|
||||||
import com.unciv.ui.components.extensions.toPercent
|
import com.unciv.ui.components.UnitMovementMemoryType
|
||||||
import com.unciv.ui.screens.worldscreen.bottombar.BattleTable
|
|
||||||
import com.unciv.utils.debug
|
import com.unciv.utils.debug
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.ulp
|
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,7 +84,7 @@ object Battle {
|
|||||||
*/
|
*/
|
||||||
fun attackOrNuke(attacker: ICombatant, attackableTile: AttackableTile): DamageDealt {
|
fun attackOrNuke(attacker: ICombatant, attackableTile: AttackableTile): DamageDealt {
|
||||||
return if (attacker is MapUnitCombatant && attacker.unit.baseUnit.isNuclearWeapon()) {
|
return if (attacker is MapUnitCombatant && attacker.unit.baseUnit.isNuclearWeapon()) {
|
||||||
NUKE(attacker, attackableTile.tileToAttack)
|
Nuke.NUKE(attacker, attackableTile.tileToAttack)
|
||||||
DamageDealt.None
|
DamageDealt.None
|
||||||
} else {
|
} else {
|
||||||
attack(attacker, getMapCombatantOfTile(attackableTile.tileToAttack)!!)
|
attack(attacker, getMapCombatantOfTile(attackableTile.tileToAttack)!!)
|
||||||
@ -439,7 +431,7 @@ object Battle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun postBattleNotifications(
|
internal fun postBattleNotifications(
|
||||||
attacker: ICombatant,
|
attacker: ICombatant,
|
||||||
defender: ICombatant,
|
defender: ICombatant,
|
||||||
attackedTile: Tile,
|
attackedTile: Tile,
|
||||||
@ -626,9 +618,7 @@ object Battle {
|
|||||||
} else if (attackerCiv.isHuman()) {
|
} else if (attackerCiv.isHuman()) {
|
||||||
// we're not taking our former capital
|
// we're not taking our former capital
|
||||||
attackerCiv.popupAlerts.add(PopupAlert(AlertType.CityConquered, city.id))
|
attackerCiv.popupAlerts.add(PopupAlert(AlertType.CityConquered, city.id))
|
||||||
} else {
|
} else automateCityConquer(attackerCiv, city)
|
||||||
NextTurnAutomation.onConquerCity(attackerCiv, city)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attackerCiv.isCurrentPlayer())
|
if (attackerCiv.isCurrentPlayer())
|
||||||
UncivGame.Current.settings.addCompletedTutorialTask("Conquer a city")
|
UncivGame.Current.settings.addCompletedTutorialTask("Conquer a city")
|
||||||
@ -638,6 +628,31 @@ object Battle {
|
|||||||
UniqueTriggerActivation.triggerCivwideUnique(unique, attackerCiv, city)
|
UniqueTriggerActivation.triggerCivwideUnique(unique, attackerCiv, city)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Handle decision making after city conquest, namely whether the AI should liberate, puppet,
|
||||||
|
* or raze a city */
|
||||||
|
private fun automateCityConquer(civInfo: Civilization, city: City) {
|
||||||
|
if (!city.hasDiplomaticMarriage()) {
|
||||||
|
val foundingCiv = civInfo.gameInfo.getCivilization(city.foundingCiv)
|
||||||
|
var valueAlliance = NextTurnAutomation.valueCityStateAlliance(civInfo, foundingCiv)
|
||||||
|
if (civInfo.getHappiness() < 0)
|
||||||
|
valueAlliance -= civInfo.getHappiness() // put extra weight on liberating if unhappy
|
||||||
|
if (foundingCiv.isCityState() && city.civ != civInfo && foundingCiv != civInfo
|
||||||
|
&& !civInfo.isAtWarWith(foundingCiv)
|
||||||
|
&& valueAlliance > 0) {
|
||||||
|
city.liberateCity(civInfo)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
city.puppetCity(civInfo)
|
||||||
|
if ((city.population.population < 4 || civInfo.isCityState())
|
||||||
|
&& city.foundingCiv != civInfo.civName && city.canBeDestroyed(justCaptured = true)) {
|
||||||
|
// raze if attacker is a city state
|
||||||
|
if (!civInfo.hasUnique(UniqueType.MayNotAnnexCities)) city.annexCity()
|
||||||
|
city.isBeingRazed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getMapCombatantOfTile(tile: Tile): ICombatant? {
|
fun getMapCombatantOfTile(tile: Tile): ICombatant? {
|
||||||
if (tile.isCityCenter()) return CityCombatant(tile.getCity()!!)
|
if (tile.isCityCenter()) return CityCombatant(tile.getCity()!!)
|
||||||
if (tile.militaryUnit != null) return MapUnitCombatant(tile.militaryUnit!!)
|
if (tile.militaryUnit != null) return MapUnitCombatant(tile.militaryUnit!!)
|
||||||
@ -751,245 +766,6 @@ object Battle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether [nuke] is allowed to nuke [targetTile]
|
|
||||||
* - Not if we would need to declare war on someone we can't.
|
|
||||||
* - Disallow nuking the tile the nuke is in, as per Civ5 (but not nuking your own tiles/units otherwise)
|
|
||||||
*
|
|
||||||
* Both [BattleTable.simulateNuke] and [SpecificUnitAutomation.automateNukes] check range, so that check is omitted here.
|
|
||||||
*/
|
|
||||||
fun mayUseNuke(nuke: MapUnitCombatant, targetTile: Tile): Boolean {
|
|
||||||
if (nuke.getTile() == targetTile) return false
|
|
||||||
// Can only nuke visible Tiles
|
|
||||||
if (!targetTile.isVisible(nuke.getCivInfo())) return false
|
|
||||||
|
|
||||||
var canNuke = true
|
|
||||||
val attackerCiv = nuke.getCivInfo()
|
|
||||||
fun checkDefenderCiv(defenderCiv: Civilization?) {
|
|
||||||
if (defenderCiv == null) return
|
|
||||||
// Allow nuking yourself! (Civ5 source: CvUnit::isNukeVictim)
|
|
||||||
if (defenderCiv == attackerCiv || defenderCiv.isDefeated()) return
|
|
||||||
// Gleaned from Civ5 source - this disallows nuking unknown civs even in invisible tiles
|
|
||||||
// https://github.com/Gedemon/Civ5-DLL/blob/master/CvGameCoreDLL_Expansion1/CvUnit.cpp#L5056
|
|
||||||
// https://github.com/Gedemon/Civ5-DLL/blob/master/CvGameCoreDLL_Expansion1/CvTeam.cpp#L986
|
|
||||||
if (attackerCiv.knows(defenderCiv) && attackerCiv.getDiplomacyManager(defenderCiv).canAttack())
|
|
||||||
return
|
|
||||||
canNuke = false
|
|
||||||
}
|
|
||||||
|
|
||||||
val blastRadius = nuke.unit.getNukeBlastRadius()
|
|
||||||
for (tile in targetTile.getTilesInDistance(blastRadius)) {
|
|
||||||
checkDefenderCiv(tile.getOwner())
|
|
||||||
checkDefenderCiv(getMapCombatantOfTile(tile)?.getCivInfo())
|
|
||||||
}
|
|
||||||
return canNuke
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("FunctionName") // Yes we want this name to stand out
|
|
||||||
fun NUKE(attacker: MapUnitCombatant, targetTile: Tile) {
|
|
||||||
val attackingCiv = attacker.getCivInfo()
|
|
||||||
val notifyDeclaredWarCivs = ArrayList<Civilization>()
|
|
||||||
fun tryDeclareWar(civSuffered: Civilization) {
|
|
||||||
if (civSuffered != attackingCiv
|
|
||||||
&& civSuffered.knows(attackingCiv)
|
|
||||||
&& civSuffered.getDiplomacyManager(attackingCiv).diplomaticStatus != DiplomaticStatus.War
|
|
||||||
) {
|
|
||||||
attackingCiv.getDiplomacyManager(civSuffered).declareWar()
|
|
||||||
if (!notifyDeclaredWarCivs.contains(civSuffered)) notifyDeclaredWarCivs.add(civSuffered)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val nukeStrength = attacker.unit.getMatchingUniques(UniqueType.NuclearWeapon)
|
|
||||||
.firstOrNull()?.params?.get(0)?.toInt() ?: return
|
|
||||||
|
|
||||||
val blastRadius = attacker.unit.getMatchingUniques(UniqueType.BlastRadius)
|
|
||||||
.firstOrNull()?.params?.get(0)?.toInt() ?: 2
|
|
||||||
|
|
||||||
// Calculate the tiles that are hit
|
|
||||||
val hitTiles = targetTile.getTilesInDistance(blastRadius)
|
|
||||||
|
|
||||||
val hitCivsTerritory = ArrayList<Civilization>()
|
|
||||||
// Declare war on the owners of all hit tiles
|
|
||||||
for (hitCiv in hitTiles.mapNotNull { it.getOwner() }.distinct()) {
|
|
||||||
hitCivsTerritory.add(hitCiv)
|
|
||||||
tryDeclareWar(hitCiv)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Declare war on all potentially hit units. They'll try to intercept the nuke before it drops
|
|
||||||
for (civWhoseUnitWasAttacked in hitTiles
|
|
||||||
.flatMap { it.getUnits() }
|
|
||||||
.map { it.civ }.distinct()
|
|
||||||
.filter { it != attackingCiv }) {
|
|
||||||
tryDeclareWar(civWhoseUnitWasAttacked)
|
|
||||||
if (attacker.unit.baseUnit.isAirUnit() && !attacker.isDefeated()) {
|
|
||||||
tryInterceptAirAttack(attacker, targetTile, civWhoseUnitWasAttacked, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val nukeNotificationAction = sequenceOf( LocationAction(targetTile.position), CivilopediaAction("Units/" + attacker.getName()))
|
|
||||||
// If the nuke has been intercepted and destroyed then it fails to detonate
|
|
||||||
if (attacker.isDefeated()) {
|
|
||||||
// Notify attacker that they are now at war for the attempt
|
|
||||||
for (defendingCiv in notifyDeclaredWarCivs)
|
|
||||||
attackingCiv.addNotification("After an attempted attack by our [${attacker.getName()}], [${defendingCiv}] has declared war on us!", nukeNotificationAction, NotificationCategory.Diplomacy, defendingCiv.civName, NotificationIcon.War, attacker.getName())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Notify attacker that they are now at war
|
|
||||||
for (defendingCiv in notifyDeclaredWarCivs)
|
|
||||||
attackingCiv.addNotification("After being hit by our [${attacker.getName()}], [${defendingCiv}] has declared war on us!", nukeNotificationAction, NotificationCategory.Diplomacy, defendingCiv.civName, NotificationIcon.War, attacker.getName())
|
|
||||||
|
|
||||||
attacker.unit.attacksSinceTurnStart.add(Vector2(targetTile.position))
|
|
||||||
|
|
||||||
for (tile in hitTiles) {
|
|
||||||
// Handle complicated effects
|
|
||||||
doNukeExplosionForTile(attacker, tile, nukeStrength, targetTile == tile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message all other civs
|
|
||||||
for (otherCiv in attackingCiv.gameInfo.civilizations) {
|
|
||||||
if (!otherCiv.isAlive() || otherCiv == attackingCiv) continue
|
|
||||||
if (hitCivsTerritory.contains(otherCiv))
|
|
||||||
otherCiv.addNotification("A(n) [${attacker.getName()}] from [${attackingCiv.civName}] has exploded in our territory!",
|
|
||||||
nukeNotificationAction, NotificationCategory.War, attackingCiv.civName, NotificationIcon.War, attacker.getName())
|
|
||||||
else if (otherCiv.knows(attackingCiv))
|
|
||||||
otherCiv.addNotification("A(n) [${attacker.getName()}] has been detonated by [${attackingCiv.civName}]!",
|
|
||||||
nukeNotificationAction, NotificationCategory.War, attackingCiv.civName, NotificationIcon.War, attacker.getName())
|
|
||||||
else
|
|
||||||
otherCiv.addNotification("A(n) [${attacker.getName()}] has been detonated by an unkown civilization!",
|
|
||||||
nukeNotificationAction, NotificationCategory.War, NotificationIcon.War, attacker.getName())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instead of postBattleAction() just destroy the unit, all other functions are not relevant
|
|
||||||
if (attacker.unit.hasUnique(UniqueType.SelfDestructs)) attacker.unit.destroy()
|
|
||||||
|
|
||||||
// It's unclear whether using nukes results in a penalty with all civs, or only affected civs.
|
|
||||||
// For now I'll make it give a diplomatic penalty to all known civs, but some testing for this would be appreciated
|
|
||||||
for (civ in attackingCiv.getKnownCivs()) {
|
|
||||||
civ.getDiplomacyManager(attackingCiv).setModifier(DiplomaticModifiers.UsedNuclearWeapons, -50f)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!attacker.isDefeated()) {
|
|
||||||
attacker.unit.attacksThisTurn += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doNukeExplosionForTile(
|
|
||||||
attacker: MapUnitCombatant,
|
|
||||||
tile: Tile,
|
|
||||||
nukeStrength: Int,
|
|
||||||
isGroundZero: Boolean
|
|
||||||
) {
|
|
||||||
// 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.getResourceRequirementsPerTurn().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
|
|
||||||
// - Original Civ5 does *not* reduce damage from missing resource, from source inspection
|
|
||||||
}
|
|
||||||
|
|
||||||
var buildingModifier = 1f // Strange, but in Civ5 a bunker mitigates damage to garrison, even if the city is destroyed by the nuke
|
|
||||||
|
|
||||||
// Damage city and reduce its population
|
|
||||||
val city = tile.getCity()
|
|
||||||
if (city != null && tile.position == city.location) {
|
|
||||||
buildingModifier = city.getAggregateModifier(UniqueType.GarrisonDamageFromNukes)
|
|
||||||
doNukeExplosionDamageToCity(city, nukeStrength, damageModifierFromMissingResource)
|
|
||||||
postBattleNotifications(attacker, CityCombatant(city), city.getCenterTile())
|
|
||||||
destroyIfDefeated(city.civ, 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 damage = (when {
|
|
||||||
isGroundZero || nukeStrength >= 2 -> 100
|
|
||||||
// The following constants are NUKE_UNIT_DAMAGE_BASE / NUKE_UNIT_DAMAGE_RAND_1 / NUKE_UNIT_DAMAGE_RAND_2 in Civ5
|
|
||||||
nukeStrength == 1 -> 30 + Random.Default.nextInt(40) + Random.Default.nextInt(40)
|
|
||||||
// Level 0 does not exist in Civ5 (it treats units same as level 2)
|
|
||||||
else -> 20 + Random.Default.nextInt(30)
|
|
||||||
} * buildingModifier * damageModifierFromMissingResource + 1f.ulp).toInt()
|
|
||||||
val defender = MapUnitCombatant(unit)
|
|
||||||
if (unit.isCivilian()) {
|
|
||||||
if (unit.health - damage <= 40) unit.destroy() // Civ5: NUKE_NON_COMBAT_DEATH_THRESHOLD = 60
|
|
||||||
} else {
|
|
||||||
defender.takeDamage(damage)
|
|
||||||
}
|
|
||||||
postBattleNotifications(attacker, defender, defender.getTile())
|
|
||||||
destroyIfDefeated(defender.getCivInfo(), attacker.getCivInfo())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pillage improvements, pillage roads, add fallout
|
|
||||||
if (tile.isCityCenter()) return // Never touch city centers - if they survived
|
|
||||||
fun applyPillageAndFallout() {
|
|
||||||
if (tile.getUnpillagedImprovement() != null && !tile.getTileImprovement()!!.hasUnique(UniqueType.Irremovable)) {
|
|
||||||
if (tile.getTileImprovement()!!.hasUnique(UniqueType.Unpillagable)) {
|
|
||||||
tile.removeImprovement()
|
|
||||||
} else {
|
|
||||||
tile.setPillaged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (tile.getUnpillagedRoad() != RoadStatus.None)
|
|
||||||
tile.setPillaged()
|
|
||||||
if (tile.isWater || tile.isImpassible() || tile.terrainFeatures.contains("Fallout")) return
|
|
||||||
tile.addTerrainFeature("Fallout")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tile.terrainHasUnique(UniqueType.DestroyableByNukesChance)) {
|
|
||||||
// Note: Safe from concurrent modification exceptions only because removeTerrainFeature
|
|
||||||
// *replaces* terrainFeatureObjects and the loop will continue on the old one
|
|
||||||
for (terrainFeature in tile.terrainFeatureObjects) {
|
|
||||||
for (unique in terrainFeature.getMatchingUniques(UniqueType.DestroyableByNukesChance)) {
|
|
||||||
val chance = unique.params[0].toFloat() / 100f
|
|
||||||
if (!(chance > 0f && isGroundZero) && Random.Default.nextFloat() >= chance) continue
|
|
||||||
tile.removeTerrainFeature(terrainFeature.name)
|
|
||||||
applyPillageAndFallout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (isGroundZero || Random.Default.nextFloat() < 0.5f) { // Civ5: NUKE_FALLOUT_PROB
|
|
||||||
applyPillageAndFallout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return the "protection" modifier from buildings (Bomb Shelter, UniqueType.PopulationLossFromNukes) */
|
|
||||||
private fun doNukeExplosionDamageToCity(targetedCity: City, nukeStrength: Int, damageModifierFromMissingResource: Float) {
|
|
||||||
// Original Capitals must be protected, `canBeDestroyed` is responsible for that check.
|
|
||||||
// The `justCaptured = true` parameter is what allows other Capitals to suffer normally.
|
|
||||||
if ((nukeStrength > 2 || nukeStrength > 1 && targetedCity.population.population < 5)
|
|
||||||
&& targetedCity.canBeDestroyed(true)) {
|
|
||||||
targetedCity.destroyCity()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val cityCombatant = CityCombatant(targetedCity)
|
|
||||||
cityCombatant.takeDamage((cityCombatant.getHealth() * 0.5f * damageModifierFromMissingResource).toInt())
|
|
||||||
|
|
||||||
// Difference to original: Civ5 rounds population loss down twice - before and after bomb shelters
|
|
||||||
val populationLoss = (
|
|
||||||
targetedCity.population.population *
|
|
||||||
targetedCity.getAggregateModifier(UniqueType.PopulationLossFromNukes) *
|
|
||||||
when (nukeStrength) {
|
|
||||||
0 -> 0f
|
|
||||||
1 -> (30 + Random.Default.nextInt(20) + Random.Default.nextInt(20)) / 100f
|
|
||||||
2 -> (60 + Random.Default.nextInt(10) + Random.Default.nextInt(10)) / 100f
|
|
||||||
else -> 1f // hypothetical nukeStrength 3 -> always to 1 pop
|
|
||||||
}
|
|
||||||
).toInt().coerceAtMost(targetedCity.population.population - 1)
|
|
||||||
targetedCity.population.addPopulation(-populationLoss)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun City.getAggregateModifier(uniqueType: UniqueType): Float {
|
|
||||||
var modifier = 1f
|
|
||||||
for (unique in getMatchingUniques(uniqueType)) {
|
|
||||||
if (!matchesFilter(unique.params[1])) continue
|
|
||||||
modifier *= unique.params[0].toPercent()
|
|
||||||
}
|
|
||||||
return modifier
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should draw an Interception if available on the tile from any Civ
|
// Should draw an Interception if available on the tile from any Civ
|
||||||
// Land Units deal 0 damage, and no XP for either party
|
// Land Units deal 0 damage, and no XP for either party
|
||||||
// Air Interceptors do Air Combat as if Melee (mutual damage) but using Ranged Strength. 5XP to both
|
// Air Interceptors do Air Combat as if Melee (mutual damage) but using Ranged Strength. 5XP to both
|
||||||
@ -1103,7 +879,7 @@ object Battle {
|
|||||||
attacker.unit.action = null
|
attacker.unit.action = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryInterceptAirAttack(
|
internal fun tryInterceptAirAttack(
|
||||||
attacker: MapUnitCombatant,
|
attacker: MapUnitCombatant,
|
||||||
attackedTile: Tile,
|
attackedTile: Tile,
|
||||||
interceptingCiv: Civilization,
|
interceptingCiv: Civilization,
|
||||||
|
265
core/src/com/unciv/logic/battle/Nuke.kt
Normal file
265
core/src/com/unciv/logic/battle/Nuke.kt
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
package com.unciv.logic.battle
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Vector2
|
||||||
|
import com.unciv.logic.automation.unit.SpecificUnitAutomation
|
||||||
|
import com.unciv.logic.city.City
|
||||||
|
import com.unciv.logic.civilization.Civilization
|
||||||
|
import com.unciv.logic.civilization.CivilopediaAction
|
||||||
|
import com.unciv.logic.civilization.LocationAction
|
||||||
|
import com.unciv.logic.civilization.NotificationCategory
|
||||||
|
import com.unciv.logic.civilization.NotificationIcon
|
||||||
|
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
|
||||||
|
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
|
||||||
|
import com.unciv.logic.map.tile.RoadStatus
|
||||||
|
import com.unciv.logic.map.tile.Tile
|
||||||
|
import com.unciv.models.ruleset.unique.UniqueType
|
||||||
|
import com.unciv.ui.components.extensions.toPercent
|
||||||
|
import com.unciv.ui.screens.worldscreen.bottombar.BattleTable
|
||||||
|
import kotlin.math.ulp
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
object Nuke {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether [nuke] is allowed to nuke [targetTile]
|
||||||
|
* - Not if we would need to declare war on someone we can't.
|
||||||
|
* - Disallow nuking the tile the nuke is in, as per Civ5 (but not nuking your own tiles/units otherwise)
|
||||||
|
*
|
||||||
|
* Both [BattleTable.simulateNuke] and [SpecificUnitAutomation.automateNukes] check range, so that check is omitted here.
|
||||||
|
*/
|
||||||
|
fun mayUseNuke(nuke: MapUnitCombatant, targetTile: Tile): Boolean {
|
||||||
|
if (nuke.getTile() == targetTile) return false
|
||||||
|
// Can only nuke visible Tiles
|
||||||
|
if (!targetTile.isVisible(nuke.getCivInfo())) return false
|
||||||
|
|
||||||
|
var canNuke = true
|
||||||
|
val attackerCiv = nuke.getCivInfo()
|
||||||
|
fun checkDefenderCiv(defenderCiv: Civilization?) {
|
||||||
|
if (defenderCiv == null) return
|
||||||
|
// Allow nuking yourself! (Civ5 source: CvUnit::isNukeVictim)
|
||||||
|
if (defenderCiv == attackerCiv || defenderCiv.isDefeated()) return
|
||||||
|
// Gleaned from Civ5 source - this disallows nuking unknown civs even in invisible tiles
|
||||||
|
// https://github.com/Gedemon/Civ5-DLL/blob/master/CvGameCoreDLL_Expansion1/CvUnit.cpp#L5056
|
||||||
|
// https://github.com/Gedemon/Civ5-DLL/blob/master/CvGameCoreDLL_Expansion1/CvTeam.cpp#L986
|
||||||
|
if (attackerCiv.knows(defenderCiv) && attackerCiv.getDiplomacyManager(defenderCiv).canAttack())
|
||||||
|
return
|
||||||
|
canNuke = false
|
||||||
|
}
|
||||||
|
|
||||||
|
val blastRadius = nuke.unit.getNukeBlastRadius()
|
||||||
|
for (tile in targetTile.getTilesInDistance(blastRadius)) {
|
||||||
|
checkDefenderCiv(tile.getOwner())
|
||||||
|
checkDefenderCiv(Battle.getMapCombatantOfTile(tile)?.getCivInfo())
|
||||||
|
}
|
||||||
|
return canNuke
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("FunctionName") // Yes we want this name to stand out
|
||||||
|
fun NUKE(attacker: MapUnitCombatant, targetTile: Tile) {
|
||||||
|
val attackingCiv = attacker.getCivInfo()
|
||||||
|
val notifyDeclaredWarCivs = ArrayList<Civilization>()
|
||||||
|
fun tryDeclareWar(civSuffered: Civilization) {
|
||||||
|
if (civSuffered != attackingCiv
|
||||||
|
&& civSuffered.knows(attackingCiv)
|
||||||
|
&& civSuffered.getDiplomacyManager(attackingCiv).diplomaticStatus != DiplomaticStatus.War
|
||||||
|
) {
|
||||||
|
attackingCiv.getDiplomacyManager(civSuffered).declareWar()
|
||||||
|
if (!notifyDeclaredWarCivs.contains(civSuffered)) notifyDeclaredWarCivs.add(civSuffered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val nukeStrength = attacker.unit.getMatchingUniques(UniqueType.NuclearWeapon)
|
||||||
|
.firstOrNull()?.params?.get(0)?.toInt() ?: return
|
||||||
|
|
||||||
|
val blastRadius = attacker.unit.getMatchingUniques(UniqueType.BlastRadius)
|
||||||
|
.firstOrNull()?.params?.get(0)?.toInt() ?: 2
|
||||||
|
|
||||||
|
// Calculate the tiles that are hit
|
||||||
|
val hitTiles = targetTile.getTilesInDistance(blastRadius)
|
||||||
|
|
||||||
|
val hitCivsTerritory = ArrayList<Civilization>()
|
||||||
|
// Declare war on the owners of all hit tiles
|
||||||
|
for (hitCiv in hitTiles.mapNotNull { it.getOwner() }.distinct()) {
|
||||||
|
hitCivsTerritory.add(hitCiv)
|
||||||
|
tryDeclareWar(hitCiv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declare war on all potentially hit units. They'll try to intercept the nuke before it drops
|
||||||
|
for (civWhoseUnitWasAttacked in hitTiles
|
||||||
|
.flatMap { it.getUnits() }
|
||||||
|
.map { it.civ }.distinct()
|
||||||
|
.filter { it != attackingCiv }) {
|
||||||
|
tryDeclareWar(civWhoseUnitWasAttacked)
|
||||||
|
if (attacker.unit.baseUnit.isAirUnit() && !attacker.isDefeated()) {
|
||||||
|
Battle.tryInterceptAirAttack(attacker, targetTile, civWhoseUnitWasAttacked, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val nukeNotificationAction = sequenceOf( LocationAction(targetTile.position), CivilopediaAction("Units/" + attacker.getName()))
|
||||||
|
// If the nuke has been intercepted and destroyed then it fails to detonate
|
||||||
|
if (attacker.isDefeated()) {
|
||||||
|
// Notify attacker that they are now at war for the attempt
|
||||||
|
for (defendingCiv in notifyDeclaredWarCivs)
|
||||||
|
attackingCiv.addNotification("After an attempted attack by our [${attacker.getName()}], [${defendingCiv}] has declared war on us!", nukeNotificationAction, NotificationCategory.Diplomacy, defendingCiv.civName, NotificationIcon.War, attacker.getName())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify attacker that they are now at war
|
||||||
|
for (defendingCiv in notifyDeclaredWarCivs)
|
||||||
|
attackingCiv.addNotification("After being hit by our [${attacker.getName()}], [${defendingCiv}] has declared war on us!", nukeNotificationAction, NotificationCategory.Diplomacy, defendingCiv.civName, NotificationIcon.War, attacker.getName())
|
||||||
|
|
||||||
|
attacker.unit.attacksSinceTurnStart.add(Vector2(targetTile.position))
|
||||||
|
|
||||||
|
for (tile in hitTiles) {
|
||||||
|
// Handle complicated effects
|
||||||
|
doNukeExplosionForTile(attacker, tile, nukeStrength, targetTile == tile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message all other civs
|
||||||
|
for (otherCiv in attackingCiv.gameInfo.civilizations) {
|
||||||
|
if (!otherCiv.isAlive() || otherCiv == attackingCiv) continue
|
||||||
|
if (hitCivsTerritory.contains(otherCiv))
|
||||||
|
otherCiv.addNotification("A(n) [${attacker.getName()}] from [${attackingCiv.civName}] has exploded in our territory!",
|
||||||
|
nukeNotificationAction, NotificationCategory.War, attackingCiv.civName, NotificationIcon.War, attacker.getName())
|
||||||
|
else if (otherCiv.knows(attackingCiv))
|
||||||
|
otherCiv.addNotification("A(n) [${attacker.getName()}] has been detonated by [${attackingCiv.civName}]!",
|
||||||
|
nukeNotificationAction, NotificationCategory.War, attackingCiv.civName, NotificationIcon.War, attacker.getName())
|
||||||
|
else
|
||||||
|
otherCiv.addNotification("A(n) [${attacker.getName()}] has been detonated by an unkown civilization!",
|
||||||
|
nukeNotificationAction, NotificationCategory.War, NotificationIcon.War, attacker.getName())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instead of postBattleAction() just destroy the unit, all other functions are not relevant
|
||||||
|
if (attacker.unit.hasUnique(UniqueType.SelfDestructs)) attacker.unit.destroy()
|
||||||
|
|
||||||
|
// It's unclear whether using nukes results in a penalty with all civs, or only affected civs.
|
||||||
|
// For now I'll make it give a diplomatic penalty to all known civs, but some testing for this would be appreciated
|
||||||
|
for (civ in attackingCiv.getKnownCivs()) {
|
||||||
|
civ.getDiplomacyManager(attackingCiv).setModifier(DiplomaticModifiers.UsedNuclearWeapons, -50f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!attacker.isDefeated()) {
|
||||||
|
attacker.unit.attacksThisTurn += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doNukeExplosionForTile(
|
||||||
|
attacker: MapUnitCombatant,
|
||||||
|
tile: Tile,
|
||||||
|
nukeStrength: Int,
|
||||||
|
isGroundZero: Boolean
|
||||||
|
) {
|
||||||
|
// 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.getResourceRequirementsPerTurn().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
|
||||||
|
// - Original Civ5 does *not* reduce damage from missing resource, from source inspection
|
||||||
|
}
|
||||||
|
|
||||||
|
var buildingModifier = 1f // Strange, but in Civ5 a bunker mitigates damage to garrison, even if the city is destroyed by the nuke
|
||||||
|
|
||||||
|
// Damage city and reduce its population
|
||||||
|
val city = tile.getCity()
|
||||||
|
if (city != null && tile.position == city.location) {
|
||||||
|
buildingModifier = city.getAggregateModifier(UniqueType.GarrisonDamageFromNukes)
|
||||||
|
doNukeExplosionDamageToCity(city, nukeStrength, damageModifierFromMissingResource)
|
||||||
|
Battle.postBattleNotifications(attacker, CityCombatant(city), city.getCenterTile())
|
||||||
|
Battle.destroyIfDefeated(city.civ, 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 damage = (when {
|
||||||
|
isGroundZero || nukeStrength >= 2 -> 100
|
||||||
|
// The following constants are NUKE_UNIT_DAMAGE_BASE / NUKE_UNIT_DAMAGE_RAND_1 / NUKE_UNIT_DAMAGE_RAND_2 in Civ5
|
||||||
|
nukeStrength == 1 -> 30 + Random.Default.nextInt(40) + Random.Default.nextInt(40)
|
||||||
|
// Level 0 does not exist in Civ5 (it treats units same as level 2)
|
||||||
|
else -> 20 + Random.Default.nextInt(30)
|
||||||
|
} * buildingModifier * damageModifierFromMissingResource + 1f.ulp).toInt()
|
||||||
|
val defender = MapUnitCombatant(unit)
|
||||||
|
if (unit.isCivilian()) {
|
||||||
|
if (unit.health - damage <= 40) unit.destroy() // Civ5: NUKE_NON_COMBAT_DEATH_THRESHOLD = 60
|
||||||
|
} else {
|
||||||
|
defender.takeDamage(damage)
|
||||||
|
}
|
||||||
|
Battle.postBattleNotifications(attacker, defender, defender.getTile())
|
||||||
|
Battle.destroyIfDefeated(defender.getCivInfo(), attacker.getCivInfo())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pillage improvements, pillage roads, add fallout
|
||||||
|
if (tile.isCityCenter()) return // Never touch city centers - if they survived
|
||||||
|
fun applyPillageAndFallout() {
|
||||||
|
if (tile.getUnpillagedImprovement() != null && !tile.getTileImprovement()!!.hasUnique(
|
||||||
|
UniqueType.Irremovable)) {
|
||||||
|
if (tile.getTileImprovement()!!.hasUnique(UniqueType.Unpillagable)) {
|
||||||
|
tile.removeImprovement()
|
||||||
|
} else {
|
||||||
|
tile.setPillaged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tile.getUnpillagedRoad() != RoadStatus.None)
|
||||||
|
tile.setPillaged()
|
||||||
|
if (tile.isWater || tile.isImpassible() || tile.terrainFeatures.contains("Fallout")) return
|
||||||
|
tile.addTerrainFeature("Fallout")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tile.terrainHasUnique(UniqueType.DestroyableByNukesChance)) {
|
||||||
|
// Note: Safe from concurrent modification exceptions only because removeTerrainFeature
|
||||||
|
// *replaces* terrainFeatureObjects and the loop will continue on the old one
|
||||||
|
for (terrainFeature in tile.terrainFeatureObjects) {
|
||||||
|
for (unique in terrainFeature.getMatchingUniques(UniqueType.DestroyableByNukesChance)) {
|
||||||
|
val chance = unique.params[0].toFloat() / 100f
|
||||||
|
if (!(chance > 0f && isGroundZero) && Random.Default.nextFloat() >= chance) continue
|
||||||
|
tile.removeTerrainFeature(terrainFeature.name)
|
||||||
|
applyPillageAndFallout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isGroundZero || Random.Default.nextFloat() < 0.5f) { // Civ5: NUKE_FALLOUT_PROB
|
||||||
|
applyPillageAndFallout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return the "protection" modifier from buildings (Bomb Shelter, UniqueType.PopulationLossFromNukes) */
|
||||||
|
private fun doNukeExplosionDamageToCity(targetedCity: City, nukeStrength: Int, damageModifierFromMissingResource: Float) {
|
||||||
|
// Original Capitals must be protected, `canBeDestroyed` is responsible for that check.
|
||||||
|
// The `justCaptured = true` parameter is what allows other Capitals to suffer normally.
|
||||||
|
if ((nukeStrength > 2 || nukeStrength > 1 && targetedCity.population.population < 5)
|
||||||
|
&& targetedCity.canBeDestroyed(true)) {
|
||||||
|
targetedCity.destroyCity()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val cityCombatant = CityCombatant(targetedCity)
|
||||||
|
cityCombatant.takeDamage((cityCombatant.getHealth() * 0.5f * damageModifierFromMissingResource).toInt())
|
||||||
|
|
||||||
|
// Difference to original: Civ5 rounds population loss down twice - before and after bomb shelters
|
||||||
|
val populationLoss = (
|
||||||
|
targetedCity.population.population *
|
||||||
|
targetedCity.getAggregateModifier(UniqueType.PopulationLossFromNukes) *
|
||||||
|
when (nukeStrength) {
|
||||||
|
0 -> 0f
|
||||||
|
1 -> (30 + Random.Default.nextInt(20) + Random.Default.nextInt(20)) / 100f
|
||||||
|
2 -> (60 + Random.Default.nextInt(10) + Random.Default.nextInt(10)) / 100f
|
||||||
|
else -> 1f // hypothetical nukeStrength 3 -> always to 1 pop
|
||||||
|
}
|
||||||
|
).toInt().coerceAtMost(targetedCity.population.population - 1)
|
||||||
|
targetedCity.population.addPopulation(-populationLoss)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private fun City.getAggregateModifier(uniqueType: UniqueType): Float {
|
||||||
|
var modifier = 1f
|
||||||
|
for (unique in getMatchingUniques(uniqueType)) {
|
||||||
|
if (!matchesFilter(unique.params[1])) continue
|
||||||
|
modifier *= unique.params[0].toPercent()
|
||||||
|
}
|
||||||
|
return modifier
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ import com.unciv.logic.battle.BattleDamage
|
|||||||
import com.unciv.logic.battle.CityCombatant
|
import com.unciv.logic.battle.CityCombatant
|
||||||
import com.unciv.logic.battle.ICombatant
|
import com.unciv.logic.battle.ICombatant
|
||||||
import com.unciv.logic.battle.MapUnitCombatant
|
import com.unciv.logic.battle.MapUnitCombatant
|
||||||
|
import com.unciv.logic.battle.Nuke
|
||||||
import com.unciv.logic.battle.TargetHelper
|
import com.unciv.logic.battle.TargetHelper
|
||||||
import com.unciv.logic.map.tile.Tile
|
import com.unciv.logic.map.tile.Tile
|
||||||
import com.unciv.models.UncivSound
|
import com.unciv.models.UncivSound
|
||||||
@ -21,9 +22,9 @@ import com.unciv.ui.components.UnitGroup
|
|||||||
import com.unciv.ui.components.extensions.addBorderAllowOpacity
|
import com.unciv.ui.components.extensions.addBorderAllowOpacity
|
||||||
import com.unciv.ui.components.extensions.addSeparator
|
import com.unciv.ui.components.extensions.addSeparator
|
||||||
import com.unciv.ui.components.extensions.disable
|
import com.unciv.ui.components.extensions.disable
|
||||||
import com.unciv.ui.components.input.onClick
|
|
||||||
import com.unciv.ui.components.extensions.toLabel
|
import com.unciv.ui.components.extensions.toLabel
|
||||||
import com.unciv.ui.components.extensions.toTextButton
|
import com.unciv.ui.components.extensions.toTextButton
|
||||||
|
import com.unciv.ui.components.input.onClick
|
||||||
import com.unciv.ui.images.ImageGetter
|
import com.unciv.ui.images.ImageGetter
|
||||||
import com.unciv.ui.screens.basescreen.BaseScreen
|
import com.unciv.ui.screens.basescreen.BaseScreen
|
||||||
import com.unciv.ui.screens.worldscreen.WorldScreen
|
import com.unciv.ui.screens.worldscreen.WorldScreen
|
||||||
@ -303,7 +304,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
|
|||||||
attackerNameWrapper.add(attackerLabel)
|
attackerNameWrapper.add(attackerLabel)
|
||||||
add(attackerNameWrapper)
|
add(attackerNameWrapper)
|
||||||
|
|
||||||
val canNuke = Battle.mayUseNuke(attacker, targetTile)
|
val canNuke = Nuke.mayUseNuke(attacker, targetTile)
|
||||||
|
|
||||||
val blastRadius = attacker.unit.getNukeBlastRadius()
|
val blastRadius = attacker.unit.getNukeBlastRadius()
|
||||||
|
|
||||||
@ -330,7 +331,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
attackButton.onClick(attacker.getAttackSound()) {
|
attackButton.onClick(attacker.getAttackSound()) {
|
||||||
Battle.NUKE(attacker, targetTile)
|
Nuke.NUKE(attacker, targetTile)
|
||||||
worldScreen.mapHolder.removeUnitActionOverlay() // the overlay was one of attacking
|
worldScreen.mapHolder.removeUnitActionOverlay() // the overlay was one of attacking
|
||||||
worldScreen.shouldUpdate = true
|
worldScreen.shouldUpdate = true
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user