mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-26 21:35:14 -04:00
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:
parent
7d52cfbcab
commit
c9fa68f8ea
@ -15,6 +15,7 @@ import com.unciv.models.stats.Stat
|
||||
import com.unciv.models.stats.Stats
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Damage calculations according to civ v wiki and https://steamcommunity.com/sharedfiles/filedetails/?id=170194443
|
||||
@ -58,12 +59,20 @@ object Battle {
|
||||
|
||||
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)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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
|
||||
// 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()) {
|
||||
tryEarnFromKilling(attacker, defender)
|
||||
tryCaptureUnit(attacker, defender)
|
||||
tryHealAfterKilling(attacker)
|
||||
} else if (attacker.isDefeated() && attacker is MapUnitCombatant && !attacker.unit.isCivilian()) {
|
||||
tryEarnFromKilling(defender, attacker)
|
||||
tryCaptureUnit(defender, attacker)
|
||||
tryHealAfterKilling(defender)
|
||||
}
|
||||
|
||||
@ -92,7 +98,8 @@ object Battle {
|
||||
|
||||
// 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
|
||||
postBattleMoveToAttackedTile(attacker, defender, attackedTile)
|
||||
if (!captureSuccess)
|
||||
postBattleMoveToAttackedTile(attacker, defender, attackedTile)
|
||||
|
||||
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://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 (captureChance > 80) captureChance = 80f
|
||||
if (100 * Random().nextFloat() > captureChance) return
|
||||
if (attacker.unit.getMatchingUniques("May capture killed [] units").none { defender.matchesCategory(it.params[0]) }) return false
|
||||
|
||||
val newUnit = attacker.getCivInfo().placeUnitNearTile(defender.getTile().position, defender.getName())
|
||||
if (newUnit == null) return // silently fail
|
||||
attacker.getCivInfo().addNotification("Your [${attacker.getName()}] captured an enemy [${defender.getName()}]", newUnit.getTile().position, NotificationIcon.War)
|
||||
val captureChance = min(0.8f, 0.1f + attacker.getAttackingStrength().toFloat() / defender.getDefendingStrength().toFloat() * 0.4f)
|
||||
if (Random().nextFloat() > captureChance) return false
|
||||
|
||||
// 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.health = 50
|
||||
return true
|
||||
}
|
||||
|
||||
private fun takeDamage(attacker: ICombatant, defender: ICombatant) {
|
||||
var potentialDamageToDefender = BattleDamage.calculateDamageToDefender(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 ...
|
||||
var damageToDefender = defender.getHealth()
|
||||
val defenderHealthBefore = defender.getHealth()
|
||||
|
||||
if (defender is MapUnitCombatant && defender.unit.isCivilian() && attacker.isMelee()) {
|
||||
captureCivilianUnit(attacker, defender)
|
||||
@ -185,38 +197,44 @@ object Battle {
|
||||
}
|
||||
}
|
||||
|
||||
damageToAttacker -= attacker.getHealth() // ... but from here on they are accurate
|
||||
damageToDefender -= defender.getHealth()
|
||||
|
||||
plunderFromDamage(attacker, defender, damageToDefender)
|
||||
plunderFromDamage(defender, attacker, damageToAttacker)
|
||||
plunderFromDamage(attacker, defender, defenderHealthBefore - defender.getHealth())
|
||||
}
|
||||
|
||||
private fun plunderFromDamage(plunderingUnit: ICombatant, plunderedUnit: ICombatant, damageDealt: Int) {
|
||||
val plunderedGoods = Stats()
|
||||
private object PlunderableStats {
|
||||
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
|
||||
|
||||
val plunderedGoods = Stats()
|
||||
|
||||
for (unique in plunderingUnit.unit.getMatchingUniques("Earn []% of the damage done to [] units as []")) {
|
||||
if (plunderedUnit.matchesCategory(unique.params[1])) {
|
||||
val resourcesPlundered =
|
||||
unique.params[0].toFloat() / 100f * damageDealt
|
||||
plunderedGoods.add(Stat.valueOf(unique.params[2]), resourcesPlundered)
|
||||
// silently ignore bad mods here - or test in checkModLinks
|
||||
val stat = Stat.values().firstOrNull { it.name == unique.params[2] }
|
||||
?: 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) }
|
||||
for (stat in plunderableStats) {
|
||||
val resourcesPlundered = plunderedGoods.get(stat)
|
||||
if (resourcesPlundered == 0f) continue
|
||||
plunderingUnit.getCivInfo().addStat(stat, resourcesPlundered.toInt())
|
||||
plunderingUnit.getCivInfo()
|
||||
.addNotification(
|
||||
"Your [${plunderingUnit.getName()}] plundered [${resourcesPlundered}] [${stat.name}] from [${plunderedUnit.getName()}]",
|
||||
plunderedUnit.getTile().position,
|
||||
NotificationIcon.War
|
||||
)
|
||||
|
||||
|
||||
val civ = plunderingUnit.getCivInfo()
|
||||
plunderedGoods.toHashMap().filterNot { it.value == 0f }.forEach {
|
||||
val plunderedAmount = it.value.toInt()
|
||||
civ.addStat(it.key, plunderedAmount)
|
||||
civ.addNotification(
|
||||
"Your [${plunderingUnit.getName()}] plundered [${plunderedAmount}] [${it.key.name}] from [${plunderedUnit.getName()}]",
|
||||
plunderedUnit.getTile().position,
|
||||
plunderingUnit.getName(), NotificationIcon.War, "StatIcons/${it.key.name}",
|
||||
if (plunderedUnit is CityCombatant) NotificationIcon.City else plunderedUnit.getName()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,15 +244,18 @@ object Battle {
|
||||
attackedTile: TileInfo,
|
||||
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
|
||||
val whatHappenedString =
|
||||
if (attacker !is CityCombatant && attacker.isDefeated()) " was destroyed while attacking"
|
||||
else " has " + (
|
||||
if (defender.isDefeated())
|
||||
if (defender.getUnitType() == UnitType.City && attacker.isMelee())
|
||||
"captured"
|
||||
else "destroyed"
|
||||
else "attacked")
|
||||
if (attacker.getCivInfo() != defender.getCivInfo()) {
|
||||
// If what happened was that a civilian unit was captured, that's dealt with in the captureCivilianUnit function
|
||||
val (whatHappenedIcon, whatHappenedString) = when {
|
||||
attacker !is CityCombatant && attacker.isDefeated() ->
|
||||
NotificationIcon.War to " was destroyed while attacking"
|
||||
!defender.isDefeated() ->
|
||||
NotificationIcon.War to " has attacked"
|
||||
defender.getUnitType() == UnitType.City && attacker.isMelee() ->
|
||||
NotificationIcon.War to " has captured"
|
||||
else ->
|
||||
NotificationIcon.Death to " has destroyed"
|
||||
}
|
||||
val attackerString =
|
||||
if (attacker.getUnitType() == UnitType.City) "Enemy city [" + attacker.getName() + "]"
|
||||
else "An enemy [" + attacker.getName() + "]"
|
||||
@ -244,15 +265,14 @@ object Battle {
|
||||
else " [" + defender.getName() + "]"
|
||||
else " our [" + defender.getName() + "]"
|
||||
val notificationString = attackerString + whatHappenedString + defenderString
|
||||
val cityIcon = "ImprovementIcons/Citadel"
|
||||
val attackerIcon = if (attacker is CityCombatant) cityIcon else attacker.getName()
|
||||
val defenderIcon = if (defender is CityCombatant) cityIcon else defender.getName()
|
||||
val attackerIcon = if (attacker is CityCombatant) NotificationIcon.City else attacker.getName()
|
||||
val defenderIcon = if (defender is CityCombatant) NotificationIcon.City else defender.getName()
|
||||
val locations = LocationAction (
|
||||
if (attackerTile != null && attackerTile.position != attackedTile.position)
|
||||
listOf(attackedTile.position, attackerTile.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) {
|
||||
val attackingCiv = attacker.getCivInfo()
|
||||
fun tryDeclareWar(civSuffered: CivilizationInfo) {
|
||||
@ -554,7 +575,7 @@ 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
|
||||
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
|
||||
@ -603,7 +624,7 @@ object Battle {
|
||||
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 populationLoss = city.population.population * (0.6 + Random().nextFloat() * 0.2)
|
||||
var populationLossReduced = false
|
||||
for (unique in city.civInfo.getMatchingUniques("Population loss from nuclear attacks -[]%")) {
|
||||
populationLoss *= 1 - unique.params[0].toFloat() / 100f
|
||||
@ -621,7 +642,7 @@ object Battle {
|
||||
}
|
||||
|
||||
// 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()
|
||||
postBattleNotifications(attacker, MapUnitCombatant(defender), defender.currentTile)
|
||||
destroyIfDefeated(defender.civInfo, attacker.getCivInfo())
|
||||
|
@ -130,8 +130,9 @@ class CivInfoTransientUpdater(val civInfo: CivilizationInfo) {
|
||||
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)
|
||||
|
||||
// This may still contain cities that have just been destroyed by razing - thus the population test
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -7,15 +7,17 @@ import com.unciv.ui.trade.DiplomacyScreen
|
||||
import com.unciv.ui.worldscreen.WorldScreen
|
||||
|
||||
object NotificationIcon {
|
||||
val Culture = "StatIcons/Culture"
|
||||
val Construction = "StatIcons/Production"
|
||||
val Growth = "StatIcons/Population"
|
||||
val War = "OtherIcons/Pillage"
|
||||
val Trade = "StatIcons/Acquire"
|
||||
val Science = "StatIcons/Science"
|
||||
val Gold = "StatIcons/Gold"
|
||||
val Death = "OtherIcons/DisbandUnit"
|
||||
val Diplomacy = "OtherIcons/Diplomacy"
|
||||
const val Culture = "StatIcons/Culture"
|
||||
const val Construction = "StatIcons/Production"
|
||||
const val Growth = "StatIcons/Population"
|
||||
const val War = "OtherIcons/Pillage"
|
||||
const val Trade = "StatIcons/Acquire"
|
||||
const val Science = "StatIcons/Science"
|
||||
const val Gold = "StatIcons/Gold"
|
||||
const val Death = "OtherIcons/DisbandUnit"
|
||||
const val Diplomacy = "OtherIcons/Diplomacy"
|
||||
const val City = "ImprovementIcons/City center"
|
||||
const val Citadel = "ImprovementIcons/Citadel"
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -961,21 +961,18 @@ class MapUnit {
|
||||
civInfo.addNotification(
|
||||
"An enemy [Citadel] has destroyed our [$name]",
|
||||
locations,
|
||||
name,
|
||||
NotificationIcon.Death
|
||||
NotificationIcon.Citadel, NotificationIcon.Death, name
|
||||
)
|
||||
citadelTile.getOwner()?.addNotification(
|
||||
"Your [Citadel] has destroyed an enemy [$name]",
|
||||
locations,
|
||||
name,
|
||||
NotificationIcon.Death
|
||||
NotificationIcon.Citadel, NotificationIcon.Death, name
|
||||
)
|
||||
destroy()
|
||||
} else civInfo.addNotification(
|
||||
"An enemy [Citadel] has attacked our [$name]",
|
||||
locations,
|
||||
name,
|
||||
NotificationIcon.War
|
||||
NotificationIcon.Citadel, NotificationIcon.War, name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user