mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-14 17:59:11 +07:00
Overhaul NUKE code to behave closer to original (#9797)
* Overhaul NUKE code to behave closer to original * Separate garrison protection of Bomb Shelter to its own Unique * Reduce code duplication: getNukeBlastRadius * Disallow nuking unknown civs * Don't show Nuke attack table when the Nuke has just been selected * World map display of nuke blast radius and friendly fire
This commit is contained in:
@ -1100,7 +1100,7 @@
|
||||
"cost": 300,
|
||||
"maintenance": 1,
|
||||
"requiredTech": "Telecommunications",
|
||||
"uniques": ["Population loss from nuclear attacks [-75]% [in this city]"]
|
||||
"uniques": ["Population loss from nuclear attacks [-75]% [in this city]","Damage to garrison from nuclear attacks [-75]% [in this city]"]
|
||||
},
|
||||
{
|
||||
"name": "Hubble Space Telescope",
|
||||
|
@ -500,8 +500,7 @@ object SpecificUnitAutomation {
|
||||
&& tile.getOwner()!!.isAtWarWith(unit.civ)
|
||||
&& tile.getCity()!!.health > tile.getCity()!!.getMaxHealth() / 2
|
||||
&& Battle.mayUseNuke(MapUnitCombatant(unit), tile)) {
|
||||
val blastRadius = unit.getMatchingUniques(UniqueType.BlastRadius)
|
||||
.firstOrNull()?.params?.get(0)?.toInt() ?: 2
|
||||
val blastRadius = unit.getNukeBlastRadius()
|
||||
val tilesInBlastRadius = tile.getTilesInDistance(blastRadius)
|
||||
val civsInBlastRadius = tilesInBlastRadius.mapNotNull { it.getOwner() } +
|
||||
tilesInBlastRadius.mapNotNull { it.getFirstUnit()?.civ }
|
||||
|
@ -5,6 +5,7 @@ import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.automation.civilization.NextTurnAutomation
|
||||
import com.unciv.logic.automation.unit.AttackableTile
|
||||
import com.unciv.logic.automation.unit.SpecificUnitAutomation
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.civilization.AlertType
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
@ -27,9 +28,11 @@ import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.models.stats.Stat
|
||||
import com.unciv.models.stats.Stats
|
||||
import com.unciv.ui.components.extensions.toPercent
|
||||
import com.unciv.ui.screens.worldscreen.bottombar.BattleTable
|
||||
import com.unciv.utils.debug
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.ulp
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
@ -742,25 +745,34 @@ 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 {
|
||||
val blastRadius =
|
||||
if (!nuke.hasUnique(UniqueType.BlastRadius)) 2
|
||||
// Don't check conditionals as these are not supported
|
||||
else nuke.unit.getMatchingUniques(UniqueType.BlastRadius).first().params[0].toInt()
|
||||
if (nuke.getTile() == targetTile) return false
|
||||
|
||||
var canNuke = true
|
||||
val attackerCiv = nuke.getCivInfo()
|
||||
for (tile in targetTile.getTilesInDistance(blastRadius)) {
|
||||
val defendingTileCiv = tile.getCity()?.civ
|
||||
if (defendingTileCiv != null && attackerCiv.knows(defendingTileCiv)) {
|
||||
canNuke = canNuke && attackerCiv.getDiplomacyManager(defendingTileCiv).canAttack()
|
||||
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 defender = getMapCombatantOfTile(tile) ?: continue
|
||||
val defendingUnitCiv = defender.getCivInfo()
|
||||
if (attackerCiv.knows(defendingUnitCiv)) {
|
||||
canNuke = canNuke && attackerCiv.getDiplomacyManager(defendingUnitCiv).canAttack()
|
||||
}
|
||||
val blastRadius = nuke.unit.getNukeBlastRadius()
|
||||
for (tile in targetTile.getTilesInDistance(blastRadius)) {
|
||||
checkDefenderCiv(tile.getOwner())
|
||||
checkDefenderCiv(getMapCombatantOfTile(tile)?.getCivInfo())
|
||||
}
|
||||
return canNuke
|
||||
}
|
||||
@ -778,7 +790,7 @@ object Battle {
|
||||
}
|
||||
}
|
||||
|
||||
val strength = attacker.unit.getMatchingUniques(UniqueType.NuclearWeapon)
|
||||
val nukeStrength = attacker.unit.getMatchingUniques(UniqueType.NuclearWeapon)
|
||||
.firstOrNull()?.params?.get(0)?.toInt() ?: return
|
||||
|
||||
val blastRadius = attacker.unit.getMatchingUniques(UniqueType.BlastRadius)
|
||||
@ -794,10 +806,10 @@ object Battle {
|
||||
}
|
||||
|
||||
// Declare war on all potentially hit units. They'll try to intercept the nuke before it drops
|
||||
for(civWhoseUnitWasAttacked in hitTiles
|
||||
for (civWhoseUnitWasAttacked in hitTiles
|
||||
.flatMap { it.getUnits() }
|
||||
.map { it.civ }.distinct()
|
||||
.filter{it != attackingCiv}) {
|
||||
.filter { it != attackingCiv }) {
|
||||
tryDeclareWar(civWhoseUnitWasAttacked)
|
||||
if (attacker.unit.baseUnit.isAirUnit() && !attacker.isDefeated()) {
|
||||
tryInterceptAirAttack(attacker, targetTile, civWhoseUnitWasAttacked, null)
|
||||
@ -807,17 +819,9 @@ object Battle {
|
||||
|
||||
attacker.unit.attacksSinceTurnStart.add(Vector2(targetTile.position))
|
||||
|
||||
// Destroy units on the target tile
|
||||
// Needs the toList() because if we're destroying the units, they're no longer part of the sequence
|
||||
for (defender in targetTile.getUnits().filter { it != attacker.unit }.toList()) {
|
||||
defender.destroy()
|
||||
postBattleNotifications(attacker, MapUnitCombatant(defender), defender.getTile())
|
||||
destroyIfDefeated(defender.civ, attacker.getCivInfo())
|
||||
}
|
||||
|
||||
for (tile in hitTiles) {
|
||||
// Handle complicated effects
|
||||
doNukeExplosionForTile(attacker, tile, strength)
|
||||
doNukeExplosionForTile(attacker, tile, nukeStrength, targetTile == tile)
|
||||
}
|
||||
|
||||
// Instead of postBattleAction() just destroy the unit, all other functions are not relevant
|
||||
@ -834,7 +838,12 @@ object Battle {
|
||||
}
|
||||
}
|
||||
|
||||
private fun doNukeExplosionForTile(attacker: MapUnitCombatant, tile: Tile, nukeStrength: Int) {
|
||||
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
|
||||
@ -845,11 +854,15 @@ object Battle {
|
||||
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())
|
||||
@ -857,19 +870,26 @@ object Battle {
|
||||
|
||||
// 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 (defender.unit.isCivilian() || nukeStrength >= 2) {
|
||||
unit.destroy()
|
||||
} else if (nukeStrength == 1) {
|
||||
defender.takeDamage(((40 + Random.Default.nextInt(60)) * damageModifierFromMissingResource).toInt())
|
||||
} else if (nukeStrength == 0) {
|
||||
defender.takeDamage(((20 + Random.Default.nextInt(30)) * damageModifierFromMissingResource).toInt())
|
||||
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.changeImprovement(null)
|
||||
@ -879,43 +899,60 @@ object Battle {
|
||||
}
|
||||
if (tile.getUnpillagedRoad() != RoadStatus.None)
|
||||
tile.setPillaged()
|
||||
if (tile.isLand && !tile.isImpassible() && !tile.isCityCenter()) {
|
||||
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)) {
|
||||
if (Random.Default.nextFloat() >= unique.params[0].toFloat() / 100f) continue
|
||||
val chance = unique.params[0].toFloat() / 100f
|
||||
if (!(chance > 0f && isGroundZero) && Random.Default.nextFloat() >= chance) continue
|
||||
tile.removeTerrainFeature(terrainFeature.name)
|
||||
if (!tile.terrainFeatures.contains("Fallout"))
|
||||
tile.addTerrainFeature("Fallout")
|
||||
applyPillageAndFallout()
|
||||
}
|
||||
}
|
||||
} else if (Random.Default.nextFloat() < 0.5f && !tile.terrainFeatures.contains("Fallout")) {
|
||||
tile.addTerrainFeature("Fallout")
|
||||
}
|
||||
} 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) {
|
||||
if (nukeStrength > 1 && targetedCity.population.population < 5 && targetedCity.canBeDestroyed(true)) {
|
||||
// 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())
|
||||
|
||||
var populationLoss = targetedCity.population.population *
|
||||
// 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(40)) / 100f
|
||||
2 -> (60 + Random.Default.nextInt(20)) / 100f
|
||||
else -> 1f
|
||||
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
|
||||
}
|
||||
for (unique in targetedCity.getMatchingUniques(UniqueType.PopulationLossFromNukes)) {
|
||||
if (!targetedCity.matchesFilter(unique.params[1])) continue
|
||||
populationLoss *= unique.params[0].toPercent()
|
||||
).toInt().coerceAtMost(targetedCity.population.population - 1)
|
||||
targetedCity.population.addPopulation(-populationLoss)
|
||||
}
|
||||
targetedCity.population.addPopulation(-populationLoss.toInt())
|
||||
if (targetedCity.population.population < 1) targetedCity.population.setPopulation(1)
|
||||
|
||||
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
|
||||
|
@ -789,6 +789,10 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
return true
|
||||
}
|
||||
|
||||
/** Gets a Nuke's blast radius from the BlastRadius unique, defaulting to 2. No check whether the unit actually is a Nuke. */
|
||||
fun getNukeBlastRadius() = getMatchingUniques(UniqueType.BlastRadius)
|
||||
// Don't check conditionals as these are not supported
|
||||
.firstOrNull()?.params?.get(0)?.toInt() ?: 2
|
||||
|
||||
private fun isAlly(otherCiv: Civilization): Boolean {
|
||||
return otherCiv == civ
|
||||
|
@ -377,10 +377,7 @@ open class Tile : IsPartOfGameInfoSerialization {
|
||||
|
||||
fun getBaseTerrain(): Terrain = baseTerrainObject
|
||||
|
||||
fun getOwner(): Civilization? {
|
||||
val containingCity = getCity() ?: return null
|
||||
return containingCity.civ
|
||||
}
|
||||
fun getOwner(): Civilization? = getCity()?.civ
|
||||
|
||||
fun getRoadOwner(): Civilization? {
|
||||
return if (roadOwner != "")
|
||||
|
@ -241,6 +241,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
|
||||
GoldenAgeLength("[relativeAmount]% Golden Age length", UniqueTarget.Global),
|
||||
|
||||
PopulationLossFromNukes("Population loss from nuclear attacks [relativeAmount]% [cityFilter]", UniqueTarget.Global),
|
||||
GarrisonDamageFromNukes("Damage to garrison from nuclear attacks [relativeAmount]% [cityFilter]", UniqueTarget.Global),
|
||||
|
||||
SpawnRebels("Rebel units may spawn", UniqueTarget.Global),
|
||||
|
||||
|
@ -325,12 +325,20 @@ class TileLayerMisc(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, si
|
||||
determineVisibility()
|
||||
}
|
||||
|
||||
/** Activates a colored semitransparent overlay. [color] is cloned, brightened by 0.3f and an alpha of 0.4f applied. */
|
||||
fun overlayTerrain(color: Color) {
|
||||
terrainOverlay.color = color.cpy().lerp(Color.WHITE, 0.3f).apply { a = 0.4f }
|
||||
terrainOverlay.isVisible = true
|
||||
determineVisibility()
|
||||
}
|
||||
|
||||
/** Activates a colored semitransparent overlay. [color] is cloned and [alpha] applied. No brightening unlike the overload without explicit alpha! */
|
||||
fun overlayTerrain(color: Color, alpha: Float) {
|
||||
terrainOverlay.color = color.cpy().apply { a = alpha }
|
||||
terrainOverlay.isVisible = true
|
||||
determineVisibility()
|
||||
}
|
||||
|
||||
fun hideTerrainOverlay(){
|
||||
terrainOverlay.isVisible = false
|
||||
determineVisibility()
|
||||
|
@ -637,6 +637,9 @@ class WorldMapHolder(
|
||||
val isAirUnit = unit.baseUnit.movesLikeAirUnits()
|
||||
val moveTileOverlayColor = if (unit.isPreparingParadrop()) Color.BLUE else Color.WHITE
|
||||
val tilesInMoveRange = unit.movement.getReachableTilesInCurrentTurn()
|
||||
// Prepare special Nuke blast radius display
|
||||
val nukeBlastRadius = if (unit.baseUnit.isNuclearWeapon() && selectedTile != null && selectedTile != unit.getTile())
|
||||
unit.getNukeBlastRadius() else -1
|
||||
|
||||
// Highlight tiles within movement range
|
||||
for (tile in tilesInMoveRange) {
|
||||
@ -644,7 +647,10 @@ class WorldMapHolder(
|
||||
|
||||
// Air-units have additional highlights
|
||||
if (isAirUnit && !unit.isPreparingAirSweep()) {
|
||||
if (tile.aerialDistanceTo(unit.getTile()) <= unit.getRange()) {
|
||||
if (nukeBlastRadius >= 0 && tile.aerialDistanceTo(selectedTile!!) <= nukeBlastRadius) {
|
||||
// The tile is within the nuke blast radius
|
||||
group.layerMisc.overlayTerrain(Color.FIREBRICK, 0.6f)
|
||||
} else if (tile.aerialDistanceTo(unit.getTile()) <= unit.getRange()) {
|
||||
// The tile is within attack range
|
||||
group.layerMisc.overlayTerrain(Color.RED)
|
||||
} else if (tile.isExplored(worldScreen.viewingCiv) && tile.aerialDistanceTo(unit.getTile()) <= unit.getRange()*2) {
|
||||
@ -687,7 +693,12 @@ class WorldMapHolder(
|
||||
if (unit.isMilitary()) {
|
||||
|
||||
val attackableTiles: List<AttackableTile> =
|
||||
BattleHelper.getAttackableEnemies(unit, unit.movement.getDistanceToTiles())
|
||||
if (nukeBlastRadius >= 0)
|
||||
selectedTile!!.getTilesInDistance(nukeBlastRadius)
|
||||
.filter { it.getFirstUnit() != null }
|
||||
.map { AttackableTile(unit.getTile(), it, 1f, null) }
|
||||
.toList()
|
||||
else BattleHelper.getAttackableEnemies(unit, unit.movement.getDistanceToTiles())
|
||||
.filter { it.tileToAttack.isVisible(unit.civ) }
|
||||
.distinctBy { it.tileToAttack }
|
||||
|
||||
|
@ -63,6 +63,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
|
||||
if (attacker is MapUnitCombatant && attacker.unit.baseUnit.isNuclearWeapon()) {
|
||||
val selectedTile = worldScreen.mapHolder.selectedTile
|
||||
?: return hide() // no selected tile
|
||||
if (selectedTile == attacker.getTile()) return hide() // mayUseNuke would test this again, but not actually seeing the nuke-yourself table just by selecting the nuke is nicer
|
||||
simulateNuke(attacker, selectedTile)
|
||||
} else if (attacker is MapUnitCombatant && attacker.unit.isPreparingAirSweep()) {
|
||||
val selectedTile = worldScreen.mapHolder.selectedTile
|
||||
@ -305,9 +306,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
|
||||
|
||||
val canNuke = Battle.mayUseNuke(attacker, targetTile)
|
||||
|
||||
val blastRadius =
|
||||
if (!attacker.unit.hasUnique(UniqueType.BlastRadius)) 2
|
||||
else attacker.unit.getMatchingUniques(UniqueType.BlastRadius).first().params[0].toInt()
|
||||
val blastRadius = attacker.unit.getNukeBlastRadius()
|
||||
|
||||
val defenderNameWrapper = Table()
|
||||
for (tile in targetTile.getTilesInDistance(blastRadius)) {
|
||||
|
@ -738,6 +738,11 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
|
||||
|
||||
Applicable to: Global
|
||||
|
||||
??? example "Damage to garrison from nuclear attacks [relativeAmount]% [cityFilter]"
|
||||
Example: "Damage to garrison from nuclear attacks [+20]% [in all cities]"
|
||||
|
||||
Applicable to: Global
|
||||
|
||||
??? example "Rebel units may spawn"
|
||||
Applicable to: Global
|
||||
|
||||
|
Reference in New Issue
Block a user