chore(purity): BattleDamage

This commit is contained in:
yairm210 2025-08-01 15:52:45 +03:00
parent d26a38a77d
commit d074612c0d
6 changed files with 58 additions and 26 deletions

View File

@ -55,12 +55,13 @@ allprojects {
"kotlin.error", "kotlin.error",
) )
wellKnownReadonlyFunctions = setOf( wellKnownReadonlyFunctions = setOf(
// Looks like the Collection.contains is not considered overridden :thunk:
"com.badlogic.gdx.math.Vector2.len", "com.badlogic.gdx.math.Vector2.len",
"com.badlogic.gdx.math.Vector2.cpy", "com.badlogic.gdx.math.Vector2.cpy",
"com.badlogic.gdx.math.Vector2.hashCode",
"java.lang.reflect.Field.getAnnotation", // not sure if generic enough to be useful globally "java.lang.reflect.Field.getAnnotation", // not sure if generic enough to be useful globally
"java.lang.Class.getField", "java.lang.Class.getField",
// Looks like the Collection.contains is not considered overridden :thunk:
"kotlin.collections.Iterable.iterator", // moved "kotlin.collections.Iterable.iterator", // moved
"kotlin.collections.Collection.containsAll", // moved "kotlin.collections.Collection.containsAll", // moved
"kotlin.collections.filterKeys", // moved "kotlin.collections.filterKeys", // moved

View File

@ -9,6 +9,8 @@ import com.unciv.models.ruleset.unique.UniqueTarget
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.components.extensions.toPercent
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly
import kotlin.collections.set import kotlin.collections.set
import kotlin.math.max import kotlin.math.max
import kotlin.math.pow import kotlin.math.pow
@ -17,6 +19,7 @@ import kotlin.random.Random
object BattleDamage { object BattleDamage {
@Readonly
private fun getModifierStringFromUnique(unique: Unique): String { private fun getModifierStringFromUnique(unique: Unique): String {
val source = when (unique.sourceObjectType) { val source = when (unique.sourceObjectType) {
UniqueTarget.Unit -> "Unit ability" UniqueTarget.Unit -> "Unit ability"
@ -30,17 +33,22 @@ object BattleDamage {
return "$source - $conditionalsText" return "$source - $conditionalsText"
} }
@Readonly
private fun getGeneralModifiers(combatant: ICombatant, enemy: ICombatant, combatAction: CombatAction, tileToAttackFrom: Tile): Counter<String> { private fun getGeneralModifiers(combatant: ICombatant, enemy: ICombatant, combatAction: CombatAction, tileToAttackFrom: Tile): Counter<String> {
val modifiers = Counter<String>() @LocalState val modifiers = Counter<String>()
val conditionalState = getStateForConditionals(combatAction, combatant, enemy) val conditionalState = getStateForConditionals(combatAction, combatant, enemy)
val civInfo = combatant.getCivInfo() val civInfo = combatant.getCivInfo()
if (combatant is MapUnitCombatant) { if (combatant is MapUnitCombatant) {
addUnitUniqueModifiers(combatant, enemy, conditionalState, tileToAttackFrom, modifiers) val unitUniqueModifiers = getUnitUniqueModifiers(combatant, enemy, conditionalState, tileToAttackFrom)
modifiers.add(unitUniqueModifiers)
addResourceLackingMalus(combatant, modifiers) val civResources = civInfo.getCivResourcesByName()
for (resource in combatant.unit.getResourceRequirementsPerTurn().keys)
if (civResources[resource]!! < 0 && !civInfo.isBarbarian)
modifiers["Missing resource"] = BattleConstants.MISSING_RESOURCES_MALUS
val (greatGeneralName, greatGeneralBonus) = GreatGeneralImplementation.getGreatGeneralBonus(combatant, enemy, combatAction) val (greatGeneralName, greatGeneralBonus) = GreatGeneralImplementation.getGreatGeneralBonus(combatant, enemy, combatAction)
if (greatGeneralBonus != 0) if (greatGeneralBonus != 0)
@ -68,6 +76,7 @@ object BattleDamage {
return modifiers return modifiers
} }
@Readonly
private fun getStateForConditionals( private fun getStateForConditionals(
combatAction: CombatAction, combatAction: CombatAction,
combatant: ICombatant, combatant: ICombatant,
@ -88,9 +97,11 @@ object BattleDamage {
return conditionalState return conditionalState
} }
private fun addUnitUniqueModifiers(combatant: MapUnitCombatant, enemy: ICombatant, conditionalState: GameContext, @Readonly
tileToAttackFrom: Tile, modifiers: Counter<String>) { private fun getUnitUniqueModifiers(combatant: MapUnitCombatant, enemy: ICombatant, conditionalState: GameContext,
tileToAttackFrom: Tile): Counter<String> {
val civInfo = combatant.getCivInfo() val civInfo = combatant.getCivInfo()
@LocalState val modifiers = Counter<String>()
for (unique in combatant.getMatchingUniques(UniqueType.Strength, conditionalState, true)) { for (unique in combatant.getMatchingUniques(UniqueType.Strength, conditionalState, true)) {
modifiers.add(getModifierStringFromUnique(unique), unique.params[0].toInt()) modifiers.add(getModifierStringFromUnique(unique), unique.params[0].toInt())
@ -124,25 +135,20 @@ object BattleDamage {
if (strengthMalus != null) { if (strengthMalus != null) {
modifiers.add("Adjacent enemy units", strengthMalus.params[0].toInt()) modifiers.add("Adjacent enemy units", strengthMalus.params[0].toInt())
} }
return modifiers
} }
private fun addResourceLackingMalus(combatant: MapUnitCombatant, modifiers: Counter<String>) { @Readonly
val civInfo = combatant.getCivInfo()
val civResources = civInfo.getCivResourcesByName()
for (resource in combatant.unit.getResourceRequirementsPerTurn().keys)
if (civResources[resource]!! < 0 && !civInfo.isBarbarian)
modifiers["Missing resource"] = BattleConstants.MISSING_RESOURCES_MALUS
}
fun getAttackModifiers( fun getAttackModifiers(
attacker: ICombatant, attacker: ICombatant,
defender: ICombatant, tileToAttackFrom: Tile defender: ICombatant, tileToAttackFrom: Tile
): Counter<String> { ): Counter<String> {
val modifiers = getGeneralModifiers(attacker, defender, CombatAction.Attack, tileToAttackFrom) @LocalState val modifiers = getGeneralModifiers(attacker, defender, CombatAction.Attack, tileToAttackFrom)
if (attacker is MapUnitCombatant) { if (attacker is MapUnitCombatant) {
addTerrainAttackModifiers(attacker, defender, tileToAttackFrom, modifiers) val terrainAttackModifiers = getTerrainAttackModifiers(attacker, defender, tileToAttackFrom)
modifiers.add(terrainAttackModifiers)
// Air unit attacking with Air Sweep // Air unit attacking with Air Sweep
if (attacker.unit.isPreparingAirSweep()) if (attacker.unit.isPreparingAirSweep())
@ -171,8 +177,9 @@ object BattleDamage {
return modifiers return modifiers
} }
private fun addTerrainAttackModifiers(attacker: MapUnitCombatant, defender: ICombatant, @Readonly
tileToAttackFrom: Tile, modifiers: Counter<String>) { private fun getTerrainAttackModifiers(attacker: MapUnitCombatant, defender: ICombatant, tileToAttackFrom: Tile): Counter<String> {
@LocalState val modifiers = Counter<String>()
if (attacker.unit.isEmbarked() && defender.getTile().isLand if (attacker.unit.isEmbarked() && defender.getTile().isLand
&& !attacker.unit.hasUnique(UniqueType.AttackAcrossCoast) && !attacker.unit.hasUnique(UniqueType.AttackAcrossCoast)
) )
@ -192,8 +199,10 @@ object BattleDamage {
if (isMeleeAttackingAcrossRiverWithNoBridge(attacker, tileToAttackFrom, defender)) if (isMeleeAttackingAcrossRiverWithNoBridge(attacker, tileToAttackFrom, defender))
modifiers["Across river"] = BattleConstants.ATTACKING_ACROSS_RIVER_MALUS modifiers["Across river"] = BattleConstants.ATTACKING_ACROSS_RIVER_MALUS
return modifiers
} }
@Readonly
private fun isMeleeAttackingAcrossRiverWithNoBridge(attacker: MapUnitCombatant, tileToAttackFrom: Tile, defender: ICombatant) = ( private fun isMeleeAttackingAcrossRiverWithNoBridge(attacker: MapUnitCombatant, tileToAttackFrom: Tile, defender: ICombatant) = (
attacker.isMelee() attacker.isMelee()
&& &&
@ -206,10 +215,11 @@ object BattleDamage {
|| !attacker.getCivInfo().tech.roadsConnectAcrossRivers) || !attacker.getCivInfo().tech.roadsConnectAcrossRivers)
) )
@Readonly
fun getAirSweepAttackModifiers( fun getAirSweepAttackModifiers(
attacker: ICombatant attacker: ICombatant
): Counter<String> { ): Counter<String> {
val modifiers = Counter<String>() @LocalState val modifiers = Counter<String>()
if (attacker is MapUnitCombatant) { if (attacker is MapUnitCombatant) {
for (unique in attacker.unit.getMatchingUniques(UniqueType.StrengthWhenAirsweep)) { for (unique in attacker.unit.getMatchingUniques(UniqueType.StrengthWhenAirsweep)) {
@ -220,8 +230,9 @@ object BattleDamage {
return modifiers return modifiers
} }
@Readonly
fun getDefenceModifiers(attacker: ICombatant, defender: ICombatant, tileToAttackFrom: Tile): Counter<String> { fun getDefenceModifiers(attacker: ICombatant, defender: ICombatant, tileToAttackFrom: Tile): Counter<String> {
val modifiers = getGeneralModifiers(defender, attacker, CombatAction.Defend, tileToAttackFrom) @LocalState val modifiers = getGeneralModifiers(defender, attacker, CombatAction.Defend, tileToAttackFrom)
val tile = defender.getTile() val tile = defender.getTile()
if (defender is MapUnitCombatant && !defender.unit.isEmbarked()) { // Embarked units get no terrain defensive bonuses if (defender is MapUnitCombatant && !defender.unit.isEmbarked()) { // Embarked units get no terrain defensive bonuses
@ -240,13 +251,14 @@ object BattleDamage {
return modifiers return modifiers
} }
@Readonly
private fun modifiersToFinalBonus(modifiers: Counter<String>): Float { private fun modifiersToFinalBonus(modifiers: Counter<String>): Float {
var finalModifier = 1f var finalModifier = 1f
for (modifierValue in modifiers.values) finalModifier += modifierValue / 100f for (modifierValue in modifiers.values) finalModifier += modifierValue / 100f
return finalModifier return finalModifier
} }
@Readonly
private fun getHealthDependantDamageRatio(combatant: ICombatant): Float { private fun getHealthDependantDamageRatio(combatant: ICombatant): Float {
return if (combatant !is MapUnitCombatant return if (combatant !is MapUnitCombatant
|| combatant.unit.hasUnique(UniqueType.NoDamagePenaltyWoundedUnits, checkCivInfoUniques = true) || combatant.unit.hasUnique(UniqueType.NoDamagePenaltyWoundedUnits, checkCivInfoUniques = true)
@ -259,6 +271,7 @@ object BattleDamage {
/** /**
* Includes attack modifiers * Includes attack modifiers
*/ */
@Readonly
fun getAttackingStrength( fun getAttackingStrength(
attacker: ICombatant, attacker: ICombatant,
defender: ICombatant, defender: ICombatant,
@ -272,11 +285,13 @@ object BattleDamage {
/** /**
* Includes defence modifiers * Includes defence modifiers
*/ */
@Readonly
fun getDefendingStrength(attacker: ICombatant, defender: ICombatant, tileToAttackFrom: Tile): Float { fun getDefendingStrength(attacker: ICombatant, defender: ICombatant, tileToAttackFrom: Tile): Float {
val defenceModifier = modifiersToFinalBonus(getDefenceModifiers(attacker, defender, tileToAttackFrom)) val defenceModifier = modifiersToFinalBonus(getDefenceModifiers(attacker, defender, tileToAttackFrom))
return max(1f, defender.getDefendingStrength(attacker.isRanged()) * defenceModifier) return max(1f, defender.getDefendingStrength(attacker.isRanged()) * defenceModifier)
} }
@Readonly
fun calculateDamageToAttacker( fun calculateDamageToAttacker(
attacker: ICombatant, attacker: ICombatant,
defender: ICombatant, defender: ICombatant,
@ -291,6 +306,7 @@ object BattleDamage {
return (damageModifier(ratio, true, randomnessFactor) * getHealthDependantDamageRatio(defender)).roundToInt() return (damageModifier(ratio, true, randomnessFactor) * getHealthDependantDamageRatio(defender)).roundToInt()
} }
@Readonly
fun calculateDamageToDefender( fun calculateDamageToDefender(
attacker: ICombatant, attacker: ICombatant,
defender: ICombatant, defender: ICombatant,
@ -305,6 +321,7 @@ object BattleDamage {
return (damageModifier(ratio, false, randomnessFactor) * getHealthDependantDamageRatio(attacker)).roundToInt() return (damageModifier(ratio, false, randomnessFactor) * getHealthDependantDamageRatio(attacker)).roundToInt()
} }
@Readonly
private fun damageModifier( private fun damageModifier(
attackerToDefenderRatio: Float, attackerToDefenderRatio: Float,
damageToAttacker: Boolean, damageToAttacker: Boolean,

View File

@ -6,6 +6,7 @@ import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.unique.GameContext import com.unciv.models.ruleset.unique.GameContext
import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import yairm210.purity.annotations.Readonly
object GreatGeneralImplementation { object GreatGeneralImplementation {
@ -26,6 +27,7 @@ object GreatGeneralImplementation {
* *
* @return A pair of unit's name and bonus (percentage) as Int (typically 15), or 0 if no applicable Great General equivalents found * @return A pair of unit's name and bonus (percentage) as Int (typically 15), or 0 if no applicable Great General equivalents found
*/ */
@Readonly
fun getGreatGeneralBonus( fun getGreatGeneralBonus(
ourUnitCombatant: MapUnitCombatant, ourUnitCombatant: MapUnitCombatant,
enemy: ICombatant, enemy: ICombatant,

View File

@ -8,6 +8,7 @@ import com.unciv.models.ruleset.unique.GameContext
import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.UnitType import com.unciv.models.ruleset.unit.UnitType
import yairm210.purity.annotations.Readonly
class MapUnitCombatant(val unit: MapUnit) : ICombatant { class MapUnitCombatant(val unit: MapUnit) : ICombatant {
override fun getHealth(): Int = unit.health override fun getHealth(): Int = unit.health
@ -46,9 +47,11 @@ class MapUnitCombatant(val unit: MapUnit) : ICombatant {
return unit.name+" of "+unit.civ.civName return unit.name+" of "+unit.civ.civName
} }
@Readonly
fun getMatchingUniques(uniqueType: UniqueType, gameContext: GameContext, checkCivUniques: Boolean): Sequence<Unique> = fun getMatchingUniques(uniqueType: UniqueType, gameContext: GameContext, checkCivUniques: Boolean): Sequence<Unique> =
unit.getMatchingUniques(uniqueType, gameContext, checkCivUniques) unit.getMatchingUniques(uniqueType, gameContext, checkCivUniques)
@Readonly
fun hasUnique(uniqueType: UniqueType, conditionalState: GameContext? = null): Boolean = fun hasUnique(uniqueType: UniqueType, conditionalState: GameContext? = null): Boolean =
if (conditionalState == null) unit.hasUnique(uniqueType) if (conditionalState == null) unit.hasUnique(uniqueType)
else unit.hasUnique(uniqueType, conditionalState) else unit.hasUnique(uniqueType, conditionalState)

View File

@ -10,6 +10,7 @@ import com.unciv.models.Religion
import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.components.extensions.toPercent
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly import yairm210.purity.annotations.Readonly
class CityReligionManager : IsPartOfGameInfoSerialization { class CityReligionManager : IsPartOfGameInfoSerialization {
@ -65,7 +66,7 @@ class CityReligionManager : IsPartOfGameInfoSerialization {
} }
fun getPressures(): Counter<String> = pressures.clone() @Readonly fun getPressures(): Counter<String> = pressures.clone()
private fun clearAllPressures() { private fun clearAllPressures() {
pressures.clear() pressures.clear()
@ -261,6 +262,7 @@ class CityReligionManager : IsPartOfGameInfoSerialization {
updateNumberOfFollowers() updateNumberOfFollowers()
} }
@Readonly
private fun getSpreadRange(): Int { private fun getSpreadRange(): Int {
var spreadRange = 10 var spreadRange = 10
@ -278,8 +280,9 @@ class CityReligionManager : IsPartOfGameInfoSerialization {
} }
/** Doesn't update the pressures, only returns what they are if the update were to happen right now */ /** Doesn't update the pressures, only returns what they are if the update were to happen right now */
@Readonly
fun getPressuresFromSurroundingCities(): Counter<String> { fun getPressuresFromSurroundingCities(): Counter<String> {
val addedPressure = Counter<String>() @LocalState val addedPressure = Counter<String>()
if (city.isHolyCity()) { if (city.isHolyCity()) {
addedPressure[religionThisIsTheHolyCityOf!!] = 5 * pressureFromAdjacentCities addedPressure[religionThisIsTheHolyCityOf!!] = 5 * pressureFromAdjacentCities
} }
@ -299,6 +302,7 @@ class CityReligionManager : IsPartOfGameInfoSerialization {
return addedPressure return addedPressure
} }
@Readonly
fun isProtectedByInquisitor(fromReligion: String? = null): Boolean { fun isProtectedByInquisitor(fromReligion: String? = null): Boolean {
for (tile in city.getCenterTile().getTilesInDistance(1)) { for (tile in city.getCenterTile().getTilesInDistance(1)) {
for (unit in listOf(tile.civilianUnit, tile.militaryUnit)) { for (unit in listOf(tile.civilianUnit, tile.militaryUnit)) {
@ -311,6 +315,7 @@ class CityReligionManager : IsPartOfGameInfoSerialization {
return false return false
} }
@Readonly
private fun pressureAmountToAdjacentCities(pressuredCity: City): Int { private fun pressureAmountToAdjacentCities(pressuredCity: City): Int {
var pressure = pressureFromAdjacentCities.toFloat() var pressure = pressureFromAdjacentCities.toFloat()
@ -331,7 +336,10 @@ class CityReligionManager : IsPartOfGameInfoSerialization {
return pressure.toInt() return pressure.toInt()
} }
/** Calculates how much pressure this religion is lacking compared to the majority religion */
@Readonly
fun getPressureDeficit(otherReligion: String?): Int { fun getPressureDeficit(otherReligion: String?): Int {
return (getPressures()[getMajorityReligionName()] ?: 0) - (getPressures()[otherReligion] ?: 0) val pressures = getPressures()
return (pressures[getMajorityReligionName()] ?: 0) - (pressures[otherReligion] ?: 0)
} }
} }

View File

@ -318,6 +318,7 @@ object TranslationActiveModsCache {
* defaults to the input string if no translation is available, * defaults to the input string if no translation is available,
* but with placeholder or sentence brackets removed. * but with placeholder or sentence brackets removed.
*/ */
@Readonly @Suppress("purity")
fun String.tr(hideIcons: Boolean = false, hideStats: Boolean = false): String { fun String.tr(hideIcons: Boolean = false, hideStats: Boolean = false): String {
val language: String = UncivGame.Current.settings.language val language: String = UncivGame.Current.settings.language