From 54d872045c7006a764d367d60b9314bfb189d761 Mon Sep 17 00:00:00 2001 From: yairm210 Date: Wed, 26 Jun 2024 00:17:06 +0300 Subject: [PATCH] Better "Withdraws before melee combat" unique --- core/src/com/unciv/logic/battle/Battle.kt | 72 +++++++++---------- .../unciv/models/ruleset/unique/UniqueType.kt | 3 +- docs/Modders/uniques.md | 6 +- 3 files changed, 38 insertions(+), 43 deletions(-) diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index 1d8c0f94ca13e..84c6c1ae14959 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -125,11 +125,21 @@ object Battle { // Withdraw from melee ability if (attacker is MapUnitCombatant && attacker.isMelee() && defender is MapUnitCombatant) { - val withdrawUniques = defender.unit.getMatchingUniques(UniqueType.MayWithdraw) - val combinedProbabilityToStayPut = withdrawUniques.fold(100) { probabilityToStayPut, unique -> probabilityToStayPut * (100-unique.params[0].toInt()) / 100 } - val baseWithdrawChance = 100 - combinedProbabilityToStayPut - // If a mod allows multiple withdraw properties, they stack multiplicatively - if (baseWithdrawChance != 0 && doWithdrawFromMeleeAbility(attacker, defender, baseWithdrawChance)) + val withdrawChance = + if (defender.unit.hasUnique(UniqueType.WithdrawsBeforeMeleeCombat, stateForConditionals = StateForConditionals( + civInfo = defender.getCivInfo(), + ourCombatant = defender, + theirCombatant = attacker, + tile = attackedTile + )) + ) 100 + + else 100 - defender.unit.getMatchingUniques(UniqueType.MayWithdraw) + .fold(100) { probabilityToWithdraw, unique -> + probabilityToWithdraw * (100 - unique.params[0].toInt()) / 100 + } + + if (withdrawChance != 0 && doWithdrawFromMeleeAbility(attacker, defender, withdrawChance)) return DamageDealt.None } @@ -615,44 +625,29 @@ object Battle { } } - private fun doWithdrawFromMeleeAbility(attacker: ICombatant, defender: ICombatant, baseWithdrawChance: Int): Boolean { - if (baseWithdrawChance == 0) return false - // Some notes... - // unit.getUniques() is a union of BaseUnit uniques and Promotion effects. - // according to some strategy guide the Slinger's withdraw ability is inherited on upgrade, - // according to the Ironclad entry of the wiki the Caravel's is lost on upgrade. - // therefore: Implement the flag as unique for the Caravel and Destroyer, as promotion for the Slinger. - if (attacker !is MapUnitCombatant) return false // allow simple access to unit property - if (defender !is MapUnitCombatant) return false + private fun doWithdrawFromMeleeAbility(attacker: MapUnitCombatant, defender: MapUnitCombatant, withdrawChance: Int): Boolean { + if (withdrawChance == 0) return false if (defender.unit.isEmbarked()) return false if (defender.unit.cache.cannotMove) return false + + // This is where the chance comes into play + if (Random( // 'randomness' is consistent for turn and tile, to avoid save-scumming + attacker.getCivInfo().gameInfo.turns * defender.getTile().position.hashCode().toLong() + ).nextInt(100) > withdrawChance) return false + // Promotions have no effect as per what I could find in available documentation - val attackBaseUnit = attacker.unit.baseUnit - val defendBaseUnit = defender.unit.baseUnit val fromTile = defender.getTile() - val attTile = attacker.getTile() + val attackerTile = attacker.getTile() + fun canNotWithdrawTo(tile: Tile): Boolean { // if the tile is what the defender can't withdraw to, this fun will return true return !defender.unit.movement.canMoveTo(tile) - || defendBaseUnit.isLandUnit() && !tile.isLand // forbid retreat from land to sea - embarked already excluded + || defender.isLandUnit() && !tile.isLand // forbid retreat from land to sea - embarked already excluded || tile.isCityCenter() && tile.getOwner() != defender.getCivInfo() // forbid retreat into the city which doesn't belong to the defender } - /* Calculate success chance: Base chance from json, calculation method from https://www.bilibili.com/read/cv2216728 - In general, except attacker's tile, 5 tiles neighbors the defender : - 2 of which are also attacker's neighbors ( we call them 2-Tiles) and the other 3 aren't (we call them 3-Tiles). - Withdraw chance depends on 2 factors : attacker's movement and how many tiles in 3-Tiles the defender can't withdraw to. - If the defender can withdraw, at first we choose a tile as toTile from 3-Tiles the defender can withdraw to. - If 3-Tiles the defender can withdraw to is null, we choose this from 2-Tiles the defender can withdraw to. - If 2-Tiles the defender can withdraw to is also null, we return false. - */ - val percentChance = baseWithdrawChance - max(0, (attackBaseUnit.movement-2)) * 20 - - fromTile.neighbors.filterNot { it == attTile || it in attTile.neighbors }.count { canNotWithdrawTo(it) } * 20 - // Get a random number in [0,100) : if the number <= percentChance, defender will withdraw from melee - if (Random( // 'randomness' is consistent for turn and tile, to avoid save-scumming - (attacker.getCivInfo().gameInfo.turns * defender.getTile().hashCode()).toLong() - ).nextInt(100) > percentChance) return false - val firstCandidateTiles = fromTile.neighbors.filterNot { it == attTile || it in attTile.neighbors } + + val firstCandidateTiles = fromTile.neighbors.filterNot { it == attackerTile || it in attackerTile.neighbors } .filterNot { canNotWithdrawTo(it) } - val secondCandidateTiles = fromTile.neighbors.filter { it in attTile.neighbors } + val secondCandidateTiles = fromTile.neighbors.filter { it in attackerTile.neighbors } .filterNot { canNotWithdrawTo(it) } val toTile: Tile = when { firstCandidateTiles.any() -> firstCandidateTiles.toList().random() @@ -667,11 +662,12 @@ object Battle { // and count 1 attack for attacker but leave it in place reduceAttackerMovementPointsAndAttacks(attacker, defender) - val attackingUnit = attackBaseUnit.name; val defendingUnit = defendBaseUnit.name - val notificationString = "[$defendingUnit] withdrew from a [$attackingUnit]" + val attackerName = attacker.getName() + val defenderName = defender.getName() + val notificationString = "[$defenderName] withdrew from a [$attackerName]" val locations = LocationAction(toTile.position, attacker.getTile().position) - defender.getCivInfo().addNotification(notificationString, locations, NotificationCategory.War, defendingUnit, NotificationIcon.War, attackingUnit) - attacker.getCivInfo().addNotification(notificationString, locations, NotificationCategory.War, defendingUnit, NotificationIcon.War, attackingUnit) + defender.getCivInfo().addNotification(notificationString, locations, NotificationCategory.War, defenderName, NotificationIcon.War, attackerName) + attacker.getCivInfo().addNotification(notificationString, locations, NotificationCategory.War, defenderName, NotificationIcon.War, attackerName) return true } diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index 9dab216dd6fee..9ae79ae63cbd1 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -411,7 +411,8 @@ enum class UniqueType( @Deprecated("As of 4.12.4", ReplaceWith("No damage penalty for wounded units")) NoDamagePenalty("Damage is ignored when determining unit Strength", UniqueTarget.Unit, UniqueTarget.Global), Uncapturable("Uncapturable", UniqueTarget.Unit), - // Replace with "Withdraws before melee combat "? + WithdrawsBeforeMeleeCombat("Withdraws before melee combat", UniqueTarget.Unit), + @Deprecated("As of 4.12.4", ReplaceWith("Withdraws before melee combat ")) MayWithdraw("May withdraw before melee ([amount]%)", UniqueTarget.Unit), CannotCaptureCities("Unable to capture cities", UniqueTarget.Unit), CannotPillage("Unable to pillage tiles", UniqueTarget.Unit), diff --git a/docs/Modders/uniques.md b/docs/Modders/uniques.md index c62f7d980a387..2ed27e84c0e1b 100644 --- a/docs/Modders/uniques.md +++ b/docs/Modders/uniques.md @@ -876,7 +876,7 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl ??? example "No defensive terrain penalty" Applicable to: Global, Unit -??? example "Damage is ignored when determining unit Strength" +??? example "No damage penalty for wounded units" Applicable to: Global, Unit ??? example "No movement cost to pillage" @@ -1343,9 +1343,7 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl ??? example "Uncapturable" Applicable to: Unit -??? example "May withdraw before melee ([amount]%)" - Example: "May withdraw before melee ([3]%)" - +??? example "Withdraws before melee combat" Applicable to: Unit ??? example "Unable to capture cities"