mirror of
https://github.com/yairm210/Unciv.git
synced 2025-08-02 16:19:41 +07:00
Battle table displays bonuses according to tile that unit will attack from, not the current tile it's on
This commit is contained in:
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()) {
|
||||||
|
@ -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
|
||||||

|

|
||||||
- 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
|
||||||

|

|
||||||
|
- 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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user