Skip to content

Commit

Permalink
Better "Withdraws before melee combat" unique
Browse files Browse the repository at this point in the history
  • Loading branch information
yairm210 committed Jun 25, 2024
1 parent d075ad0 commit 54d8720
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 43 deletions.
72 changes: 34 additions & 38 deletions core/src/com/unciv/logic/battle/Battle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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()
Expand All @@ -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
}

Expand Down
3 changes: 2 additions & 1 deletion core/src/com/unciv/models/ruleset/unique/UniqueType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 <with [amount]% chance>"?
WithdrawsBeforeMeleeCombat("Withdraws before melee combat", UniqueTarget.Unit),
@Deprecated("As of 4.12.4", ReplaceWith("Withdraws before melee combat <with [amount]% chance>"))
MayWithdraw("May withdraw before melee ([amount]%)", UniqueTarget.Unit),
CannotCaptureCities("Unable to capture cities", UniqueTarget.Unit),
CannotPillage("Unable to pillage tiles", UniqueTarget.Unit),
Expand Down
6 changes: 2 additions & 4 deletions docs/Modders/uniques.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 54d8720

Please sign in to comment.