Privateer capture, plunder, and raze Notifications (#4698)

* Privateer capture, plunder, and raze Notifications

* Privateer capture, plunder, and raze Notifications - patch1

* Privateer capture, plunder, and raze Notifications - patch2
This commit is contained in:
SomeTroglodyte 2021-08-02 18:14:31 +02:00 committed by GitHub
parent 7d52cfbcab
commit c9fa68f8ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 95 additions and 74 deletions

View File

@ -15,6 +15,7 @@ import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats import com.unciv.models.stats.Stats
import java.util.* import java.util.*
import kotlin.math.max import kotlin.math.max
import kotlin.math.min
/** /**
* Damage calculations according to civ v wiki and https://steamcommunity.com/sharedfiles/filedetails/?id=170194443 * Damage calculations according to civ v wiki and https://steamcommunity.com/sharedfiles/filedetails/?id=170194443
@ -58,12 +59,20 @@ object Battle {
takeDamage(attacker, defender) takeDamage(attacker, defender)
postBattleNotifications(attacker, defender, attackedTile, attacker.getTile()) // check if unit is captured by the attacker (prize ships unique)
// As ravignir clarified in issue #4374, this only works for aggressor
val captureSuccess = defender is MapUnitCombatant && attacker is MapUnitCombatant
&& defender.isDefeated() && !defender.unit.isCivilian()
&& tryCaptureUnit(attacker, defender)
if (!captureSuccess) // capture creates a new unit, but `defender` still is the original, so this function would still show a kill message
postBattleNotifications(attacker, defender, attackedTile, attacker.getTile())
postBattleNationUniques(defender, attackedTile, attacker) postBattleNationUniques(defender, attackedTile, attacker)
// This needs to come BEFORE the move-to-tile, because if we haven't conquered it we can't move there =) // This needs to come BEFORE the move-to-tile, because if we haven't conquered it we can't move there =)
if (defender.isDefeated() && defender is CityCombatant && attacker is MapUnitCombatant && attacker.isMelee() && !attacker.unit.hasUnique("Unable to capture cities")) if (defender.isDefeated() && defender is CityCombatant && attacker is MapUnitCombatant
&& attacker.isMelee() && !attacker.unit.hasUnique("Unable to capture cities"))
conquerCity(defender.city, attacker) conquerCity(defender.city, attacker)
// Exploring units surviving an attack should "wake up" // Exploring units surviving an attack should "wake up"
@ -72,14 +81,11 @@ object Battle {
// Add culture when defeating a barbarian when Honor policy is adopted, gold from enemy killed when honor is complete // Add culture when defeating a barbarian when Honor policy is adopted, gold from enemy killed when honor is complete
// or any enemy military unit with Sacrificial captives unique (can be either attacker or defender!) // or any enemy military unit with Sacrificial captives unique (can be either attacker or defender!)
// or check if unit is captured by the attacker (prize ships unique)
if (defender.isDefeated() && defender is MapUnitCombatant && !defender.unit.isCivilian()) { if (defender.isDefeated() && defender is MapUnitCombatant && !defender.unit.isCivilian()) {
tryEarnFromKilling(attacker, defender) tryEarnFromKilling(attacker, defender)
tryCaptureUnit(attacker, defender)
tryHealAfterKilling(attacker) tryHealAfterKilling(attacker)
} else if (attacker.isDefeated() && attacker is MapUnitCombatant && !attacker.unit.isCivilian()) { } else if (attacker.isDefeated() && attacker is MapUnitCombatant && !attacker.unit.isCivilian()) {
tryEarnFromKilling(defender, attacker) tryEarnFromKilling(defender, attacker)
tryCaptureUnit(defender, attacker)
tryHealAfterKilling(defender) tryHealAfterKilling(defender)
} }
@ -92,7 +98,8 @@ object Battle {
// we're a melee unit and we destroyed\captured an enemy unit // we're a melee unit and we destroyed\captured an enemy unit
// Should be called after tryCaptureUnit(), as that might spawn a unit on the tile we go to // Should be called after tryCaptureUnit(), as that might spawn a unit on the tile we go to
postBattleMoveToAttackedTile(attacker, defender, attackedTile) if (!captureSuccess)
postBattleMoveToAttackedTile(attacker, defender, attackedTile)
reduceAttackerMovementPointsAndAttacks(attacker, defender) reduceAttackerMovementPointsAndAttacks(attacker, defender)
@ -138,31 +145,36 @@ object Battle {
} }
} }
private fun tryCaptureUnit(attacker: ICombatant, defender: MapUnitCombatant) { private fun tryCaptureUnit(attacker: MapUnitCombatant, defender: MapUnitCombatant): Boolean {
// https://forums.civfanatics.com/threads/prize-ships-for-land-units.650196/ // https://forums.civfanatics.com/threads/prize-ships-for-land-units.650196/
// https://civilization.fandom.com/wiki/Module:Data/Civ5/GK/Defines // https://civilization.fandom.com/wiki/Module:Data/Civ5/GK/Defines
if (!defender.isDefeated()) return
if (attacker !is MapUnitCombatant) return
if (attacker.unit.getMatchingUniques("May capture killed [] units").none { defender.matchesCategory(it.params[0]) }) return
var captureChance = 10 + attacker.getAttackingStrength().toFloat() / defender.getDefendingStrength().toFloat() * 40 if (attacker.unit.getMatchingUniques("May capture killed [] units").none { defender.matchesCategory(it.params[0]) }) return false
if (captureChance > 80) captureChance = 80f
if (100 * Random().nextFloat() > captureChance) return
val newUnit = attacker.getCivInfo().placeUnitNearTile(defender.getTile().position, defender.getName()) val captureChance = min(0.8f, 0.1f + attacker.getAttackingStrength().toFloat() / defender.getDefendingStrength().toFloat() * 0.4f)
if (newUnit == null) return // silently fail if (Random().nextFloat() > captureChance) return false
attacker.getCivInfo().addNotification("Your [${attacker.getName()}] captured an enemy [${defender.getName()}]", newUnit.getTile().position, NotificationIcon.War)
// This is called after takeDamage and so the defeated defender is already destroyed and
// thus removed from the tile - but MapUnit.destroy() will not clear the unit's currentTile.
// Therefore placeUnitNearTile _will_ place the new unit exactly where the defender was
val defenderName = defender.getName()
val newUnit = attacker.getCivInfo().placeUnitNearTile(defender.getTile().position, defenderName)
?: return false // silently fail
attacker.getCivInfo().addNotification(
"Your [${attacker.getName()}] captured an enemy [$defenderName]",
newUnit.getTile().position, attacker.getName(), NotificationIcon.War, defenderName )
newUnit.currentMovement = 0f newUnit.currentMovement = 0f
newUnit.health = 50 newUnit.health = 50
return true
} }
private fun takeDamage(attacker: ICombatant, defender: ICombatant) { private fun takeDamage(attacker: ICombatant, defender: ICombatant) {
var potentialDamageToDefender = BattleDamage.calculateDamageToDefender(attacker, attacker.getTile(), defender) var potentialDamageToDefender = BattleDamage.calculateDamageToDefender(attacker, attacker.getTile(), defender)
var potentialDamageToAttacker = BattleDamage.calculateDamageToAttacker(attacker, attacker.getTile(), defender) var potentialDamageToAttacker = BattleDamage.calculateDamageToAttacker(attacker, attacker.getTile(), defender)
var damageToAttacker = attacker.getHealth() // These variables names don't make any sense as of yet ... val defenderHealthBefore = defender.getHealth()
var damageToDefender = defender.getHealth()
if (defender is MapUnitCombatant && defender.unit.isCivilian() && attacker.isMelee()) { if (defender is MapUnitCombatant && defender.unit.isCivilian() && attacker.isMelee()) {
captureCivilianUnit(attacker, defender) captureCivilianUnit(attacker, defender)
@ -185,38 +197,44 @@ object Battle {
} }
} }
damageToAttacker -= attacker.getHealth() // ... but from here on they are accurate plunderFromDamage(attacker, defender, defenderHealthBefore - defender.getHealth())
damageToDefender -= defender.getHealth()
plunderFromDamage(attacker, defender, damageToDefender)
plunderFromDamage(defender, attacker, damageToAttacker)
} }
private fun plunderFromDamage(plunderingUnit: ICombatant, plunderedUnit: ICombatant, damageDealt: Int) { private object PlunderableStats {
val plunderedGoods = Stats() val stats = setOf (Stat.Gold, Stat.Science, Stat.Culture, Stat.Faith)
}
private fun plunderFromDamage(
plunderingUnit: ICombatant,
plunderedUnit: ICombatant,
damageDealt: Int
) {
// implementation based on the description of the original civilopedia, see issue #4374
if (plunderingUnit !is MapUnitCombatant) return if (plunderingUnit !is MapUnitCombatant) return
val plunderedGoods = Stats()
for (unique in plunderingUnit.unit.getMatchingUniques("Earn []% of the damage done to [] units as []")) { for (unique in plunderingUnit.unit.getMatchingUniques("Earn []% of the damage done to [] units as []")) {
if (plunderedUnit.matchesCategory(unique.params[1])) { if (plunderedUnit.matchesCategory(unique.params[1])) {
val resourcesPlundered = // silently ignore bad mods here - or test in checkModLinks
unique.params[0].toFloat() / 100f * damageDealt val stat = Stat.values().firstOrNull { it.name == unique.params[2] }
plunderedGoods.add(Stat.valueOf(unique.params[2]), resourcesPlundered) ?: continue // stat badly defined in unique
if (stat !in PlunderableStats.stats)
continue // stat known but not valid
val percentage = unique.params[0].toFloatOrNull()
?: continue // percentage parameter invalid
plunderedGoods.add(stat, percentage / 100f * damageDealt)
} }
} }
val plunderableStats = listOf("Gold", "Science", "Culture", "Faith").map { Stat.valueOf(it) } val civ = plunderingUnit.getCivInfo()
for (stat in plunderableStats) { plunderedGoods.toHashMap().filterNot { it.value == 0f }.forEach {
val resourcesPlundered = plunderedGoods.get(stat) val plunderedAmount = it.value.toInt()
if (resourcesPlundered == 0f) continue civ.addStat(it.key, plunderedAmount)
plunderingUnit.getCivInfo().addStat(stat, resourcesPlundered.toInt()) civ.addNotification(
plunderingUnit.getCivInfo() "Your [${plunderingUnit.getName()}] plundered [${plunderedAmount}] [${it.key.name}] from [${plunderedUnit.getName()}]",
.addNotification( plunderedUnit.getTile().position,
"Your [${plunderingUnit.getName()}] plundered [${resourcesPlundered}] [${stat.name}] from [${plunderedUnit.getName()}]", plunderingUnit.getName(), NotificationIcon.War, "StatIcons/${it.key.name}",
plunderedUnit.getTile().position, if (plunderedUnit is CityCombatant) NotificationIcon.City else plunderedUnit.getName()
NotificationIcon.War )
)
} }
} }
@ -226,15 +244,18 @@ object Battle {
attackedTile: TileInfo, attackedTile: TileInfo,
attackerTile: TileInfo? = null attackerTile: TileInfo? = null
) { ) {
if (attacker.getCivInfo() != defender.getCivInfo()) { // If what happened was that a civilian unit was captures, that's dealt with in the captureCivilianUnit function if (attacker.getCivInfo() != defender.getCivInfo()) {
val whatHappenedString = // If what happened was that a civilian unit was captured, that's dealt with in the captureCivilianUnit function
if (attacker !is CityCombatant && attacker.isDefeated()) " was destroyed while attacking" val (whatHappenedIcon, whatHappenedString) = when {
else " has " + ( attacker !is CityCombatant && attacker.isDefeated() ->
if (defender.isDefeated()) NotificationIcon.War to " was destroyed while attacking"
if (defender.getUnitType() == UnitType.City && attacker.isMelee()) !defender.isDefeated() ->
"captured" NotificationIcon.War to " has attacked"
else "destroyed" defender.getUnitType() == UnitType.City && attacker.isMelee() ->
else "attacked") NotificationIcon.War to " has captured"
else ->
NotificationIcon.Death to " has destroyed"
}
val attackerString = val attackerString =
if (attacker.getUnitType() == UnitType.City) "Enemy city [" + attacker.getName() + "]" if (attacker.getUnitType() == UnitType.City) "Enemy city [" + attacker.getName() + "]"
else "An enemy [" + attacker.getName() + "]" else "An enemy [" + attacker.getName() + "]"
@ -244,15 +265,14 @@ object Battle {
else " [" + defender.getName() + "]" else " [" + defender.getName() + "]"
else " our [" + defender.getName() + "]" else " our [" + defender.getName() + "]"
val notificationString = attackerString + whatHappenedString + defenderString val notificationString = attackerString + whatHappenedString + defenderString
val cityIcon = "ImprovementIcons/Citadel" val attackerIcon = if (attacker is CityCombatant) NotificationIcon.City else attacker.getName()
val attackerIcon = if (attacker is CityCombatant) cityIcon else attacker.getName() val defenderIcon = if (defender is CityCombatant) NotificationIcon.City else defender.getName()
val defenderIcon = if (defender is CityCombatant) cityIcon else defender.getName()
val locations = LocationAction ( val locations = LocationAction (
if (attackerTile != null && attackerTile.position != attackedTile.position) if (attackerTile != null && attackerTile.position != attackedTile.position)
listOf(attackedTile.position, attackerTile.position) listOf(attackedTile.position, attackerTile.position)
else listOf(attackedTile.position) else listOf(attackedTile.position)
) )
defender.getCivInfo().addNotification(notificationString, locations, attackerIcon, NotificationIcon.War, defenderIcon) defender.getCivInfo().addNotification(notificationString, locations, attackerIcon, whatHappenedIcon, defenderIcon)
} }
} }
@ -452,6 +472,7 @@ object Battle {
} }
} }
@Suppress("FunctionName") // Yes we want this name to stand out
fun NUKE(attacker: MapUnitCombatant, targetTile: TileInfo) { fun NUKE(attacker: MapUnitCombatant, targetTile: TileInfo) {
val attackingCiv = attacker.getCivInfo() val attackingCiv = attacker.getCivInfo()
fun tryDeclareWar(civSuffered: CivilizationInfo) { fun tryDeclareWar(civSuffered: CivilizationInfo) {
@ -554,7 +575,7 @@ object Battle {
} }
// Damage and/or destroy units on the tile // 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 for (unit in tile.getUnits().toList()) { // toList so if it's destroyed there's no concurrent modification
val defender = MapUnitCombatant(unit) val defender = MapUnitCombatant(unit)
if (defender.unit.isCivilian()) { if (defender.unit.isCivilian()) {
unit.destroy() // destroy the unit unit.destroy() // destroy the unit
@ -603,7 +624,7 @@ object Battle {
city.population.setPopulation(1) // For cities that cannot be destroyed, such as original capitals city.population.setPopulation(1) // For cities that cannot be destroyed, such as original capitals
city.destroyCity() city.destroyCity()
} else { } else {
var populationLoss = city.population.population * (0.6 + Random().nextFloat() * 0.2); var populationLoss = city.population.population * (0.6 + Random().nextFloat() * 0.2)
var populationLossReduced = false var populationLossReduced = false
for (unique in city.civInfo.getMatchingUniques("Population loss from nuclear attacks -[]%")) { for (unique in city.civInfo.getMatchingUniques("Population loss from nuclear attacks -[]%")) {
populationLoss *= 1 - unique.params[0].toFloat() / 100f populationLoss *= 1 - unique.params[0].toFloat() / 100f
@ -621,7 +642,7 @@ object Battle {
} }
// Destroy all hit units // Destroy all hit units
for (defender in tile.getUnits().toList()) { // toList to avoid concurent modification exceptions for (defender in tile.getUnits().toList()) { // toList to avoid concurrent modification exceptions
defender.destroy() defender.destroy()
postBattleNotifications(attacker, MapUnitCombatant(defender), defender.currentTile) postBattleNotifications(attacker, MapUnitCombatant(defender), defender.currentTile)
destroyIfDefeated(defender.civInfo, attacker.getCivInfo()) destroyIfDefeated(defender.civInfo, attacker.getCivInfo())

View File

@ -130,8 +130,9 @@ class CivInfoTransientUpdater(val civInfo: CivilizationInfo) {
if (city !in civInfo.citiesConnectedToCapitalToMediums && city.civInfo == civInfo && city != civInfo.getCapital()) if (city !in civInfo.citiesConnectedToCapitalToMediums && city.civInfo == civInfo && city != civInfo.getCapital())
civInfo.addNotification("[${city.name}] has been connected to your capital!", city.location, NotificationIcon.Gold) civInfo.addNotification("[${city.name}] has been connected to your capital!", city.location, NotificationIcon.Gold)
// This may still contain cities that have just been destroyed by razing - thus the population test
for (city in civInfo.citiesConnectedToCapitalToMediums.keys) for (city in civInfo.citiesConnectedToCapitalToMediums.keys)
if (!citiesReachedToMediums.containsKey(city) && city.civInfo == civInfo) if (!citiesReachedToMediums.containsKey(city) && city.civInfo == civInfo && city.population.population > 0)
civInfo.addNotification("[${city.name}] has been disconnected from your capital!", city.location, NotificationIcon.Gold) civInfo.addNotification("[${city.name}] has been disconnected from your capital!", city.location, NotificationIcon.Gold)
} }

View File

@ -7,15 +7,17 @@ import com.unciv.ui.trade.DiplomacyScreen
import com.unciv.ui.worldscreen.WorldScreen import com.unciv.ui.worldscreen.WorldScreen
object NotificationIcon { object NotificationIcon {
val Culture = "StatIcons/Culture" const val Culture = "StatIcons/Culture"
val Construction = "StatIcons/Production" const val Construction = "StatIcons/Production"
val Growth = "StatIcons/Population" const val Growth = "StatIcons/Population"
val War = "OtherIcons/Pillage" const val War = "OtherIcons/Pillage"
val Trade = "StatIcons/Acquire" const val Trade = "StatIcons/Acquire"
val Science = "StatIcons/Science" const val Science = "StatIcons/Science"
val Gold = "StatIcons/Gold" const val Gold = "StatIcons/Gold"
val Death = "OtherIcons/DisbandUnit" const val Death = "OtherIcons/DisbandUnit"
val Diplomacy = "OtherIcons/Diplomacy" const val Diplomacy = "OtherIcons/Diplomacy"
const val City = "ImprovementIcons/City center"
const val Citadel = "ImprovementIcons/Citadel"
} }
/** /**

View File

@ -961,21 +961,18 @@ class MapUnit {
civInfo.addNotification( civInfo.addNotification(
"An enemy [Citadel] has destroyed our [$name]", "An enemy [Citadel] has destroyed our [$name]",
locations, locations,
name, NotificationIcon.Citadel, NotificationIcon.Death, name
NotificationIcon.Death
) )
citadelTile.getOwner()?.addNotification( citadelTile.getOwner()?.addNotification(
"Your [Citadel] has destroyed an enemy [$name]", "Your [Citadel] has destroyed an enemy [$name]",
locations, locations,
name, NotificationIcon.Citadel, NotificationIcon.Death, name
NotificationIcon.Death
) )
destroy() destroy()
} else civInfo.addNotification( } else civInfo.addNotification(
"An enemy [Citadel] has attacked our [$name]", "An enemy [Citadel] has attacked our [$name]",
locations, locations,
name, NotificationIcon.Citadel, NotificationIcon.War, name
NotificationIcon.War
) )
} }
} }