Battle table displays bonuses according to tile that unit will attack from, not the current tile it's on

This commit is contained in:
Yair Morgenstern
2023-04-08 21:34:55 +03:00
parent ca06d7e54a
commit cb20d91822
4 changed files with 48 additions and 35 deletions

View File

@ -1,5 +1,6 @@
package com.unciv.logic.battle package com.unciv.logic.battle
import com.unciv.logic.map.tile.Tile
import com.unciv.models.Counter import com.unciv.models.Counter
import com.unciv.models.ruleset.GlobalUniques import com.unciv.models.ruleset.GlobalUniques
import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.StateForConditionals
@ -29,7 +30,7 @@ object BattleDamage {
return "$source - $conditionalsText" return "$source - $conditionalsText"
} }
private fun getGeneralModifiers(combatant: ICombatant, enemy: ICombatant, combatAction: CombatAction): Counter<String> { private fun getGeneralModifiers(combatant: ICombatant, enemy: ICombatant, combatAction: CombatAction, tileToAttackFrom:Tile): Counter<String> {
val modifiers = Counter<String>() val modifiers = Counter<String>()
val civInfo = combatant.getCivInfo() val civInfo = combatant.getCivInfo()
@ -58,7 +59,10 @@ object BattleDamage {
} }
//https://www.carlsguides.com/strategy/civilization5/war/combatbonuses.php //https://www.carlsguides.com/strategy/civilization5/war/combatbonuses.php
val adjacentUnits = combatant.getTile().neighbors.flatMap { it.getUnits() } var adjacentUnits = combatant.getTile().neighbors.flatMap { it.getUnits() }
if (enemy.getTile() !in combatant.getTile().neighbors && tileToAttackFrom in combatant.getTile().neighbors
&& enemy is MapUnitCombatant)
adjacentUnits += sequenceOf(enemy.unit)
val strengthMalus = adjacentUnits.filter { it.civ.isAtWarWith(civInfo) } val strengthMalus = adjacentUnits.filter { it.civ.isAtWarWith(civInfo) }
.flatMap { it.getMatchingUniques(UniqueType.StrengthForAdjacentEnemies) } .flatMap { it.getMatchingUniques(UniqueType.StrengthForAdjacentEnemies) }
.filter { combatant.matchesCategory(it.params[1]) && combatant.getTile().matchesFilter(it.params[2]) } .filter { combatant.matchesCategory(it.params[1]) && combatant.getTile().matchesFilter(it.params[2]) }
@ -105,9 +109,9 @@ object BattleDamage {
fun getAttackModifiers( fun getAttackModifiers(
attacker: ICombatant, attacker: ICombatant,
defender: ICombatant defender: ICombatant, tileToAttackFrom: Tile
): Counter<String> { ): Counter<String> {
val modifiers = getGeneralModifiers(attacker, defender, CombatAction.Attack) val modifiers = getGeneralModifiers(attacker, defender, CombatAction.Attack, tileToAttackFrom)
if (attacker is MapUnitCombatant) { if (attacker is MapUnitCombatant) {
if (attacker.unit.isEmbarked() if (attacker.unit.isEmbarked()
@ -139,11 +143,11 @@ object BattleDamage {
modifiers["Flanking"] = modifiers["Flanking"] =
(flankingBonus * numberOfOtherAttackersSurroundingDefender).toInt() (flankingBonus * numberOfOtherAttackersSurroundingDefender).toInt()
} }
if (attacker.getTile().aerialDistanceTo(defender.getTile()) == 1 && if (tileToAttackFrom.aerialDistanceTo(defender.getTile()) == 1 &&
attacker.getTile().isConnectedByRiver(defender.getTile()) && tileToAttackFrom.isConnectedByRiver(defender.getTile()) &&
!attacker.unit.hasUnique(UniqueType.AttackAcrossRiver) !attacker.unit.hasUnique(UniqueType.AttackAcrossRiver)
) { ) {
if (!attacker.getTile() if (!tileToAttackFrom
.hasConnection(attacker.getCivInfo()) // meaning, the tiles are not road-connected for this civ .hasConnection(attacker.getCivInfo()) // meaning, the tiles are not road-connected for this civ
|| !defender.getTile().hasConnection(attacker.getCivInfo()) || !defender.getTile().hasConnection(attacker.getCivInfo())
|| !attacker.getCivInfo().tech.roadsConnectAcrossRivers || !attacker.getCivInfo().tech.roadsConnectAcrossRivers
@ -172,8 +176,8 @@ object BattleDamage {
return modifiers return modifiers
} }
fun getDefenceModifiers(attacker: ICombatant, defender: ICombatant): Counter<String> { fun getDefenceModifiers(attacker: ICombatant, defender: ICombatant, tileToAttackFrom: Tile): Counter<String> {
val modifiers = getGeneralModifiers(defender, attacker, CombatAction.Defend) val modifiers = getGeneralModifiers(defender, attacker, CombatAction.Defend, tileToAttackFrom)
val tile = defender.getTile() val tile = defender.getTile()
if (defender is MapUnitCombatant) { if (defender is MapUnitCombatant) {
@ -222,9 +226,10 @@ object BattleDamage {
*/ */
fun getAttackingStrength( fun getAttackingStrength(
attacker: ICombatant, attacker: ICombatant,
defender: ICombatant defender: ICombatant,
tileToAttackFrom: Tile
): Float { ): Float {
val attackModifier = modifiersToFinalBonus(getAttackModifiers(attacker, defender)) val attackModifier = modifiersToFinalBonus(getAttackModifiers(attacker, defender, tileToAttackFrom))
return max(1f, attacker.getAttackingStrength() * attackModifier) return max(1f, attacker.getAttackingStrength() * attackModifier)
} }
@ -232,34 +237,36 @@ object BattleDamage {
/** /**
* Includes defence modifiers * Includes defence modifiers
*/ */
fun getDefendingStrength(attacker: ICombatant, defender: ICombatant): Float { fun getDefendingStrength(attacker: ICombatant, defender: ICombatant, tileToAttackFrom: Tile): Float {
val defenceModifier = modifiersToFinalBonus(getDefenceModifiers(attacker, defender)) val defenceModifier = modifiersToFinalBonus(getDefenceModifiers(attacker, defender, tileToAttackFrom))
return max(1f, defender.getDefendingStrength(attacker.isRanged()) * defenceModifier) return max(1f, defender.getDefendingStrength(attacker.isRanged()) * defenceModifier)
} }
fun calculateDamageToAttacker( fun calculateDamageToAttacker(
attacker: ICombatant, attacker: ICombatant,
defender: ICombatant, defender: ICombatant,
tileToAttackFrom: Tile = defender.getTile(),
/** Between 0 and 1. */ /** Between 0 and 1. */
randomnessFactor: Float = Random(attacker.getCivInfo().gameInfo.turns * attacker.getTile().position.hashCode().toLong()).nextFloat() randomnessFactor: Float = Random(attacker.getCivInfo().gameInfo.turns * attacker.getTile().position.hashCode().toLong()).nextFloat()
): Int { ): Int {
if (attacker.isRanged() && !attacker.isAirUnit()) return 0 if (attacker.isRanged() && !attacker.isAirUnit()) return 0
if (defender.isCivilian()) return 0 if (defender.isCivilian()) return 0
val ratio = getAttackingStrength(attacker, defender) / getDefendingStrength( val ratio = getAttackingStrength(attacker, defender, tileToAttackFrom) / getDefendingStrength(
attacker, defender) attacker, defender, tileToAttackFrom)
return (damageModifier(ratio, true, randomnessFactor) * getHealthDependantDamageRatio(defender)).roundToInt() return (damageModifier(ratio, true, randomnessFactor) * getHealthDependantDamageRatio(defender)).roundToInt()
} }
fun calculateDamageToDefender( fun calculateDamageToDefender(
attacker: ICombatant, attacker: ICombatant,
defender: ICombatant, defender: ICombatant,
tileToAttackFrom: Tile = defender.getTile(),
/** Between 0 and 1. Defaults to turn and location-based random to avoid save scumming */ /** Between 0 and 1. Defaults to turn and location-based random to avoid save scumming */
randomnessFactor: Float = Random(attacker.getCivInfo().gameInfo.turns * attacker.getTile().position.hashCode().toLong()).nextFloat() randomnessFactor: Float = Random(attacker.getCivInfo().gameInfo.turns * attacker.getTile().position.hashCode().toLong()).nextFloat()
, ,
): Int { ): Int {
if (defender.isCivilian()) return 40 if (defender.isCivilian()) return 40
val ratio = getAttackingStrength(attacker, defender) / getDefendingStrength( val ratio = getAttackingStrength(attacker, defender, tileToAttackFrom) /
attacker, defender) getDefendingStrength(attacker, defender, tileToAttackFrom)
return (damageModifier(ratio, false, randomnessFactor) * getHealthDependantDamageRatio(attacker)).roundToInt() return (damageModifier(ratio, false, randomnessFactor) * getHealthDependantDamageRatio(attacker)).roundToInt()
} }

View File

@ -4,7 +4,6 @@ import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Touchable import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Label import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.UncivGame
import com.unciv.logic.automation.unit.AttackableTile import com.unciv.logic.automation.unit.AttackableTile
import com.unciv.logic.automation.unit.BattleHelper import com.unciv.logic.automation.unit.BattleHelper
import com.unciv.logic.automation.unit.UnitAutomation import com.unciv.logic.automation.unit.UnitAutomation
@ -72,7 +71,14 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
} else { } else {
val defender = tryGetDefender() ?: return hide() val defender = tryGetDefender() ?: return hide()
if (attacker is CityCombatant && defender is CityCombatant) return hide() if (attacker is CityCombatant && defender is CityCombatant) return hide()
simulateBattle(attacker, defender) val tileToAttackFrom = if (attacker is MapUnitCombatant)
BattleHelper.getAttackableEnemies(
attacker.unit,
attacker.unit.movement.getDistanceToTiles()
)
.firstOrNull { it.tileToAttack == defender.getTile() }?.tileToAttackFrom ?: attacker.getTile()
else attacker.getTile()
simulateBattle(attacker, defender, tileToAttackFrom)
} }
isVisible = true isVisible = true
@ -134,7 +140,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
add(modifierLabel).width(quarterScreen - upOrDownLabel.minWidth) add(modifierLabel).width(quarterScreen - upOrDownLabel.minWidth)
} }
private fun simulateBattle(attacker: ICombatant, defender: ICombatant){ private fun simulateBattle(attacker: ICombatant, defender: ICombatant, tileToAttackFrom: Tile){
clear() clear()
val attackerNameWrapper = Table() val attackerNameWrapper = Table()
@ -161,12 +167,12 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
add(defender.getDefendingStrength(attacker.isRanged()).toString() + defenceIcon).row() add(defender.getDefendingStrength(attacker.isRanged()).toString() + defenceIcon).row()
val attackerModifiers = val attackerModifiers =
BattleDamage.getAttackModifiers(attacker, defender).map { BattleDamage.getAttackModifiers(attacker, defender, tileToAttackFrom).map {
getModifierTable(it.key, it.value) getModifierTable(it.key, it.value)
} }
val defenderModifiers = val defenderModifiers =
if (defender is MapUnitCombatant) if (defender is MapUnitCombatant)
BattleDamage.getDefenceModifiers(attacker, defender).map { BattleDamage.getDefenceModifiers(attacker, defender, tileToAttackFrom).map {
getModifierTable(it.key, it.value) getModifierTable(it.key, it.value)
} }
else listOf() else listOf()
@ -179,8 +185,8 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
if (attackerModifiers.any() || defenderModifiers.any()){ if (attackerModifiers.any() || defenderModifiers.any()){
addSeparator() addSeparator()
val attackerStrength = BattleDamage.getAttackingStrength(attacker, defender).roundToInt() val attackerStrength = BattleDamage.getAttackingStrength(attacker, defender, tileToAttackFrom).roundToInt()
val defenderStrength = BattleDamage.getDefendingStrength(attacker, defender).roundToInt() val defenderStrength = BattleDamage.getDefendingStrength(attacker, defender, tileToAttackFrom).roundToInt()
add(attackerStrength.toString() + attackIcon) add(attackerStrength.toString() + attackIcon)
add(defenderStrength.toString() + attackIcon).row() add(defenderStrength.toString() + attackIcon).row()
} }
@ -193,12 +199,12 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
row() row()
} }
val maxDamageToDefender = BattleDamage.calculateDamageToDefender(attacker, defender, 1f) val maxDamageToDefender = BattleDamage.calculateDamageToDefender(attacker, defender, tileToAttackFrom, 1f)
val minDamageToDefender = BattleDamage.calculateDamageToDefender(attacker, defender, 0f) val minDamageToDefender = BattleDamage.calculateDamageToDefender(attacker, defender, tileToAttackFrom, 0f)
var expectedDamageToDefenderForHealthbar = (maxDamageToDefender + minDamageToDefender) / 2 var expectedDamageToDefenderForHealthbar = (maxDamageToDefender + minDamageToDefender) / 2
val maxDamageToAttacker = BattleDamage.calculateDamageToAttacker(attacker, defender, 1f) val maxDamageToAttacker = BattleDamage.calculateDamageToAttacker(attacker, defender, tileToAttackFrom, 1f)
val minDamageToAttacker = BattleDamage.calculateDamageToAttacker(attacker, defender, 0f) val minDamageToAttacker = BattleDamage.calculateDamageToAttacker(attacker, defender, tileToAttackFrom, 0f)
var expectedDamageToAttackerForHealthbar = (maxDamageToAttacker + minDamageToAttacker) / 2 var expectedDamageToAttackerForHealthbar = (maxDamageToAttacker + minDamageToAttacker) / 2
if (expectedDamageToAttackerForHealthbar > attacker.getHealth() && expectedDamageToDefenderForHealthbar > defender.getHealth()) { if (expectedDamageToAttackerForHealthbar > attacker.getHealth() && expectedDamageToDefenderForHealthbar > defender.getHealth()) {

View File

@ -30,12 +30,12 @@ So first things first - the initial "No assumptions" setup to have Unciv run fro
- If you get a `../../docs/uniques.md (No such file or directory)` error that means you forgot to set the working directory! - If you get a `../../docs/uniques.md (No such file or directory)` error that means you forgot to set the working directory!
- Select the Desktop configuration (or however you chose to name it) and click the green arrow button to run! Or you can use the next button -the green critter with six legs and two feelers - to start debugging. - Select the Desktop configuration (or however you chose to name it) and click the green arrow button to run! Or you can use the next button -the green critter with six legs and two feelers - to start debugging.
- A few Android Studio settings that are recommended: - A few Android Studio settings that are recommended:
- Going to Settings > Version Control > Commit and turning off 'Before commit - perform code analysis' - Going to Settings > Version Control > Commit and turning off 'Before commit - perform code analysis'
- Settings > Editor > Code Style > Kotlin > Tabs and Indents > Continuation Indent: 4 - Settings > Editor > Code Style > Kotlin > Tabs and Indents > Continuation Indent: 4
![image](https://user-images.githubusercontent.com/44038014/169315352-9ba0c4cf-307c-44d1-b3bc-2a58752c6854.png) ![image](https://user-images.githubusercontent.com/44038014/169315352-9ba0c4cf-307c-44d1-b3bc-2a58752c6854.png)
- Settings > Editor > General > On Save > Uncheck Remove trailing spaces on: [...] to prevent it from removing necessary trailing whitespace in template.properties for translation files - Settings > Editor > General > On Save > Uncheck Remove trailing spaces on: [...] to prevent it from removing necessary trailing whitespace in template.properties for translation files
![image](https://user-images.githubusercontent.com/44038014/169316243-07e36b8e-4c9e-44c4-941c-47e634c68b4c.png) ![image](https://user-images.githubusercontent.com/44038014/169316243-07e36b8e-4c9e-44c4-941c-47e634c68b4c.png)
- If you download mods, right-click the `android/assets/mods` folder , "Mark directory as" > Excluded, to [disable indexing on mods](https://www.jetbrains.com/help/idea/indexing.html#exclude) for performance
Unciv uses Gradle to specify dependencies and how to run. In the background, the Gradle gnomes will be off fetching the packages (a one-time effort) and, once that's done, will build the project! Unciv uses Gradle to specify dependencies and how to run. In the background, the Gradle gnomes will be off fetching the packages (a one-time effort) and, once that's done, will build the project!
Unciv uses Gradle 7.5 and the Android Gradle Plugin 7.3.1. Can check in File > Project Structure > Project Unciv uses Gradle 7.5 and the Android Gradle Plugin 7.3.1. Can check in File > Project Structure > Project

View File

@ -26,13 +26,13 @@ class TriggeredUniquesTests {
@Test @Test
fun testConditionalTimedUniqueIsTriggerable() { fun testConditionalTimedUniqueIsTriggerable() {
val unique = policy.uniqueObjects.first{ it.type == UniqueType.Strength } val unique = policy.uniqueObjects.first{ it.type == UniqueType.Strength }
Assert.assertTrue("Unique with timed conditional must be triggerable", unique!!.isTriggerable) Assert.assertTrue("Unique with timed conditional must be triggerable", unique.isTriggerable)
} }
@Test @Test
fun testConditionalTimedUniqueStrength() { fun testConditionalTimedUniqueStrength() {
civInfo.policies.adopt(policy, true) civInfo.policies.adopt(policy, true)
val modifiers = BattleDamage.getAttackModifiers(attacker, defender) val modifiers = BattleDamage.getAttackModifiers(attacker, defender, attacker.getTile())
Assert.assertTrue("Timed Strength should work right after triggering", modifiers.sumValues() == 42) Assert.assertTrue("Timed Strength should work right after triggering", modifiers.sumValues() == 42)
} }
@ -43,7 +43,7 @@ class TriggeredUniquesTests {
// and right now that attacker is not in the civ's unit list // and right now that attacker is not in the civ's unit list
civInfo.units.addUnit(attacker.unit, false) civInfo.units.addUnit(attacker.unit, false)
TurnManager(civInfo).endTurn() TurnManager(civInfo).endTurn()
val modifiers = BattleDamage.getAttackModifiers(attacker, defender) val modifiers = BattleDamage.getAttackModifiers(attacker, defender, attacker.getTile())
Assert.assertTrue("Timed Strength should no longer work after endTurn", modifiers.sumValues() == 0) Assert.assertTrue("Timed Strength should no longer work after endTurn", modifiers.sumValues() == 0)
} }
} }