Add Air Sweep (#7484)

* Add Air Sweep Unique
Enable Dogfighting Promotion
Add Air Sweep mode button and crosshair overlay

* Adding Air Sweep Battle Table

* Add airSweep to Battle
Remove double XP
While in AirSweep can't select other units on tile

* initial airsweep code

* Implement airSweep

* BattleTable indicates tile you're AirSweeping

* some notifications

* Clean up notifications.
Add icons

* Revert game.atlas and game.png

* Fix selection properly

* Update Vanilla UnitPromotions.json

* Better handling of movement use
comment cleanup

* missing credit

* Proper code so that Seas units also deal no damage
Adding Tutorials!

* Remove Intercept Bonus Damage/Protection

* Remove chance of Interceptor missing

* Battle Table a bit more consistent

* Defender also gets Air Sweep Modifiers

* Defender doesn't get bonus

* Remove unused getInterceptBonus
Combine logic

* Show damage in notifications for Air Sweep

* Randomize intercepting Civ and prioritize Air Units

* Remove debug code

* Updated atlas

* Clean up Uniques

* Object-ify DamageDealt for ease of reference

* code clean up

Co-authored-by: itanasi <spellman23@gmail.com>
This commit is contained in:
itanasi 2022-07-27 12:16:53 -07:00 committed by GitHub
parent 026ba380cc
commit af6ab8e4e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1327 additions and 1025 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -130,83 +130,90 @@ UnitPromotionIcons/Cover
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Drill
UnitPromotionIcons/Dogfighting
rotate: false
xy: 924, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Evasion
UnitPromotionIcons/Drill
rotate: false
xy: 982, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Evasion
rotate: false
xy: 1040, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Extended Range
rotate: false
xy: 1040, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Operational Range
rotate: false
xy: 1040, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Flight Deck
rotate: false
xy: 1098, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Formation
UnitPromotionIcons/Operational Range
rotate: false
xy: 1098, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Flight Deck
rotate: false
xy: 1156, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Formation
rotate: false
xy: 1214, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Great Generals
rotate: false
xy: 1214, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Quick Study
rotate: false
xy: 1214, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Haka War Dance
rotate: false
xy: 1272, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Heal Instantly
UnitPromotionIcons/Quick Study
rotate: false
xy: 1272, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Haka War Dance
rotate: false
xy: 1330, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Home Sweet Home
UnitPromotionIcons/Heal Instantly
rotate: false
xy: 1388, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Home Sweet Home
rotate: false
xy: 1446, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Ignore terrain cost
rotate: false
xy: 4, 12
@ -216,133 +223,133 @@ UnitPromotionIcons/Ignore terrain cost
index: -1
UnitPromotionIcons/Indirect Fire
rotate: false
xy: 1446, 62
xy: 1504, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Interception
rotate: false
xy: 1504, 62
xy: 1562, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Logistics
rotate: false
xy: 1562, 62
xy: 1620, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/March
rotate: false
xy: 1620, 62
xy: 1678, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Medic
rotate: false
xy: 1678, 62
xy: 1736, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Mobility
rotate: false
xy: 1736, 62
xy: 1794, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Morale
rotate: false
xy: 1794, 62
xy: 1852, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Pictish Courage
rotate: false
xy: 1852, 62
xy: 1910, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Rejuvenation
rotate: false
xy: 1910, 62
xy: 1968, 62
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Scouting
rotate: false
xy: 1968, 62
xy: 112, 4
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Sentry
rotate: false
xy: 1968, 62
xy: 112, 4
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Shock
rotate: false
xy: 112, 4
xy: 170, 4
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Slinger Withdraw
rotate: false
xy: 170, 4
xy: 228, 4
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Sortie
rotate: false
xy: 228, 4
xy: 286, 4
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Supply
rotate: false
xy: 286, 4
xy: 344, 4
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Survivalism
rotate: false
xy: 344, 4
xy: 402, 4
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Volley
rotate: false
xy: 402, 4
xy: 460, 4
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Wolfpack
rotate: false
xy: 460, 4
xy: 518, 4
size: 50, 50
orig: 50, 50
offset: 0, 0
index: -1
UnitPromotionIcons/Woodsman
rotate: false
xy: 518, 4
xy: 576, 4
size: 50, 50
orig: 50, 50
offset: 0, 0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -358,29 +358,26 @@
"uniques": ["[+34]% Damage when intercepting"],
"unitTypes": ["Fighter"]
},
/*
{
"name": "Dogfighting I",
"uniques": ["Bonus when performing air sweep [33]%"], // todo
"uniques": ["[+33]% Strength when performing Air Sweep"],
"unitTypes": ["Fighter"]
},
{
"name": "Dogfighting II",
"prerequisites": ["Dogfighting I"],
"uniques": ["Bonus when performing air sweep [33]%"],
"uniques": ["[+33]% Strength when performing Air Sweep"],
"unitTypes": ["Fighter"]
},
{
"name": "Dogfighting III",
"prerequisites": ["Dogfighting II"],
"uniques": ["Bonus when performing air sweep [34]%"],
"uniques": ["[+34]% Strength when performing Air Sweep"],
"unitTypes": ["Fighter"]
}
*/
},
{
"name": "Air Targeting I",
"prerequisites": ["Interception I","Siege I","Bombardment I"], // "Dogfighting I"
"prerequisites": ["Interception I","Siege I","Bombardment I","Dogfighting I"],
"uniques": ["[+33]% Strength <vs [Water] units>"],
"unitTypes": ["Fighter","Bomber"]
},
@ -393,20 +390,20 @@
{
"name": "Sortie",
"prerequisites": ["Interception II"], // "Dogfighting II"
"prerequisites": ["Interception II", "Dogfighting II"],
"uniques": ["[1] extra interceptions may be made per turn"],
"unitTypes": ["Fighter"]
},
{
"name": "Operational Range",
"prerequisites": ["Interception I", /*"Dogfighting I",*/ "Siege I", "Bombardment I"],
"prerequisites": ["Interception I", "Dogfighting I", "Siege I", "Bombardment I"],
"uniques": ["[+2] Range"],
"unitTypes": ["Fighter","Bomber"]
},
{
"name": "Air Repair",
"prerequisites": ["Interception II", /*"Dogfighting II",*/ "Siege II", "Bombardment II", "Mobility II", "Anti-Armor II"],
"prerequisites": ["Interception II", "Dogfighting II", "Siege II", "Bombardment II", "Mobility II", "Anti-Armor II"],
"uniques": ["Unit will heal every turn, even if it performs an action"],
"unitTypes": ["Fighter", "Bomber", "Helicopter"]
},

View File

@ -59,7 +59,7 @@
{
"name": "Fighter",
"movementType": "Air",
"uniques": ["Aircraft", "[+4] Sight", "Can see over obstacles"]
"uniques": ["Aircraft", "[+4] Sight", "Can see over obstacles", "Can perform Air Sweep"]
},
{
"name": "Bomber",
@ -81,9 +81,9 @@
"movementType": "Land",
"uniques": ["Can pass through impassable tiles"]
},
// Deprecated unit types required for mods without a UnitTypes.json file to work
{
"name": "Melee",
"movementType": "Land"

View File

@ -358,29 +358,26 @@
"uniques": ["[+34]% Damage when intercepting"],
"unitTypes": ["Fighter"]
},
/*
{
"name": "Dogfighting I",
"uniques": ["Bonus when performing air sweep [33]%"], // todo
"uniques": ["[+33]% Strength when performing Air Sweep"],
"unitTypes": ["Fighter"]
},
{
"name": "Dogfighting II",
"prerequisites": ["Dogfighting I"],
"uniques": ["Bonus when performing air sweep [33]%"],
"uniques": ["[+33]% Strength when performing Air Sweep"],
"unitTypes": ["Fighter"]
},
{
"name": "Dogfighting III",
"prerequisites": ["Dogfighting II"],
"uniques": ["Bonus when performing air sweep [34]%"],
"uniques": ["[+34]% Strength when performing Air Sweep"],
"unitTypes": ["Fighter"]
}
*/
},
{
"name": "Air Targeting I",
"prerequisites": ["Interception I","Siege I","Bombardment I"], // "Dogfighting I"
"prerequisites": ["Interception I","Siege I","Bombardment I", "Dogfighting I"],
"uniques": ["[+33]% Strength <vs [Water] units>"],
"unitTypes": ["Fighter","Bomber"]
},
@ -393,20 +390,20 @@
{
"name": "Sortie",
"prerequisites": ["Interception II"], // "Dogfighting II"
"prerequisites": ["Interception II", "Dogfighting II"],
"uniques": ["[1] extra interceptions may be made per turn"],
"unitTypes": ["Fighter"]
},
{
"name": "Operational Range",
"prerequisites": ["Interception I", /*"Dogfighting I",*/ "Siege I", "Bombardment I"],
"prerequisites": ["Interception I", "Dogfighting I", "Siege I", "Bombardment I"],
"uniques": ["[+2] Range"],
"unitTypes": ["Fighter","Bomber"]
},
{
"name": "Air Repair",
"prerequisites": ["Interception II", /*"Dogfighting II",*/ "Siege II", "Bombardment II", "Mobility II", "Anti-Armor II"],
"prerequisites": ["Interception II", "Dogfighting II", "Siege II", "Bombardment II", "Mobility II", "Anti-Armor II"],
"uniques": ["Unit will heal every turn, even if it performs an action"],
"unitTypes": ["Fighter", "Bomber", "Helicopter"]
},

View File

@ -59,7 +59,7 @@
{
"name": "Fighter",
"movementType": "Air",
"uniques": ["Aircraft", "[+4] Sight", "Can see over obstacles"]
"uniques": ["Aircraft", "[+4] Sight", "Can see over obstacles", "Can perform Air Sweep"]
},
{
"name": "Bomber",
@ -81,9 +81,9 @@
"movementType": "Land",
"uniques": ["Can pass through impassable tiles"]
},
// Deprecated unit types required for mods without a UnitTypes.json file to work
{
"name": "Melee",
"movementType": "Land"

View File

@ -365,5 +365,15 @@
"During the We Love The King Day, the city will grow 25% faster.",
"This means exploration and trade is important to grow your cities!"
]
},
{
"name": "Air Sweeps",
"steps": [
"Certain Units are able to perform Air Sweeps over a tile helping clear out potential enemy Interceptors.",
"While this Action will take an Attack, the benefit is drawing out Interceptions to help protect your other Air Units. Especially your Bombers.",
"Your unit will always draw an Interception, if one can reach the target tile, even if the Intercepting unit has a chance to miss.",
"In addition, if the Interceptor is not an Air Unit (eg Land or Sea), the Air Sweeping unit takes no damage!",
"Intercepting Air Units will damage each other in a straight fight with no Interception bonuses. And only the Attacking Unit gets any Air Sweep bonuses."
]
}
]

View File

@ -773,10 +773,16 @@ An enemy [RangedUnit] has destroyed the defence of [cityName] =
Enemy city [cityName] has destroyed our [ourUnit] =
An enemy [unit] was destroyed while attacking [cityName] =
An enemy [unit] was destroyed while attacking our [ourUnit] =
Our [attackerName] was destroyed by an intercepting [interceptorName] =
Our [interceptorName] intercepted and destroyed an enemy [attackerName] =
Our [attackerName] was attacked by an intercepting [interceptorName] =
Our [interceptorName] intercepted and attacked an enemy [attackerName] =
Our [attackerName] ([amount]) was destroyed by an intercepting [interceptorName] ([amount]) =
Our [attackerName] ([amount]) was destroyed by an unknown interceptor =
Our [interceptorName] ([amount]) intercepted and destroyed an enemy [attackerName] ([amount]) =
Our [attackerName] ([amount]) destroyed an intercepting [interceptorName] ([amount]) =
Our [interceptorName] ([amount]) intercepted and was destroyed by an enemy [attackerName] ([amount]) =
Our [interceptorName] ([amount]) intercepted and was destroyed by an unknown enemy =
Our [attackerName] ([amount]) was attacked by an intercepting [interceptorName] ([amount]) =
Our [attackerName] ([amount]) was attacked by an unknown interceptor =
Our [interceptorName] ([amount]) intercepted and attacked an enemy [attackerName] ([amount]) =
Nothing tried to intercept out [attackerName] =
An enemy [unit] was spotted near our territory =
An enemy [unit] was spotted in our territory =
Your city [cityName] can bombard the enemy! =

View File

@ -72,11 +72,18 @@ object BattleHelper {
.asSequence()
for (tile in tilesInAttackRange) {
if (tile in tilesWithEnemies) attackableTiles += AttackableTile(reachableTile, tile, movementLeft)
if (tile in tilesWithEnemies) attackableTiles += AttackableTile(
reachableTile,
tile,
movementLeft
)
else if (tile in tilesWithoutEnemies) continue // avoid checking the same empty tile multiple times
else if (checkTile(unit, tile, tilesToCheck)) {
tilesWithEnemies += tile
attackableTiles += AttackableTile(reachableTile, tile, movementLeft)
} else if (unit.isPreparingAirSweep()){
tilesWithEnemies += tile
attackableTiles += AttackableTile(reachableTile, tile, movementLeft)
} else {
tilesWithoutEnemies += tile
}

View File

@ -13,6 +13,7 @@ import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.civilization.PopupAlert
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.logic.map.MapUnit
import com.unciv.logic.map.RoadStatus
import com.unciv.logic.map.TileInfo
import com.unciv.models.AttackableTile
@ -305,10 +306,13 @@ object Battle {
return true
}
private fun takeDamage(attacker: ICombatant, defender: ICombatant) {
private data class DamageDealt(val attackerDealt: Int, val defenderDealt: Int) {}
private fun takeDamage(attacker: ICombatant, defender: ICombatant): DamageDealt {
var potentialDamageToDefender = BattleDamage.calculateDamageToDefender(attacker, defender)
var potentialDamageToAttacker = BattleDamage.calculateDamageToAttacker(attacker, defender)
val attackerHealthBefore = attacker.getHealth()
val defenderHealthBefore = defender.getHealth()
if (defender is MapUnitCombatant && defender.unit.isCivilian() && attacker.isMelee()) {
@ -332,7 +336,11 @@ object Battle {
}
}
plunderFromDamage(attacker, defender, defenderHealthBefore - defender.getHealth())
val defenderDamageDealt = attackerHealthBefore - attacker.getHealth()
val attackerDamageDealt = defenderHealthBefore - defender.getHealth()
plunderFromDamage(attacker, defender, attackerDamageDealt)
return DamageDealt(attackerDamageDealt, defenderDamageDealt)
}
private fun plunderFromDamage(
@ -810,6 +818,153 @@ object Battle {
if (targetedCity.population.population < 1) targetedCity.population.setPopulation(1)
}
// Should draw an Interception if available on the tile from any Civ
// Land Units deal 0 damage, and no XP for either party
// Air Interceptors do Air Combat as if Melee (mutual damage) but using Ranged Strength. 5XP to both
// But does not use the Interception mechanic bonuses/promotions
// Counts as an Attack for both units
// Will always draw out an Interceptor's attack (they cannot miss)
// This means the combat against Air Units will execute and always deal damage
// Random Civ at War will Intercept, prioritizing Air Units,
// sorted by highest Intercept chance (same as regular Intercept)
fun airSweep(attacker: MapUnitCombatant, attackedTile: TileInfo) {
// Air Sweep counts as an attack, even if nothing else happens
attacker.unit.attacksThisTurn++
// copied and modified from reduceAttackerMovementPointsAndAttacks()
// use up movement
if (attacker.unit.hasUnique(UniqueType.CanMoveAfterAttacking) || attacker.unit.maxAttacksPerTurn() > attacker.unit.attacksThisTurn) {
// if it was a melee attack and we won, then the unit ALREADY got movement points deducted,
// for the movement to the enemy's tile!
// and if it's an air unit, it only has 1 movement anyway, so...
if (!attacker.unit.baseUnit.movesLikeAirUnits())
attacker.unit.useMovementPoints(1f)
} else attacker.unit.currentMovement = 0f
val attackerName = attacker.getName()
// Make giant sequence of all potential Interceptors from all Civs isAtWarWith()
var potentialInterceptors = sequence<MapUnit> { }
for (interceptingCiv in UncivGame.Current.gameInfo!!.civilizations
.filter {attacker.getCivInfo().isAtWarWith(it)}) {
potentialInterceptors += interceptingCiv.getCivUnits()
.filter { it.canIntercept(attackedTile) }
}
// first priority, only Air Units
if (potentialInterceptors.any { it.baseUnit.isAirUnit() })
potentialInterceptors = potentialInterceptors.filter { it.baseUnit.isAirUnit() }
// Pick highest chance interceptor
for (interceptor in potentialInterceptors
.shuffled() // randomize Civ
.sortedByDescending { it.interceptChance() }) {
// No chance of Interceptor to miss (unlike regular Interception). Always want to deal damage
val interceptingCiv = interceptor.civInfo
val interceptorName = interceptor.name
// pairs of LocationAction for Notification
val locations = LocationAction(
interceptor.currentTile.position,
attacker.unit.currentTile.position
)
val locationsAttackerUnknown =
LocationAction(interceptor.currentTile.position, attackedTile.position)
val locationsInterceptorUnknown =
LocationAction(attackedTile.position, attacker.unit.currentTile.position)
interceptor.attacksThisTurn++ // even if you miss, you took the shot
val damageDealt: DamageDealt
if (!interceptor.baseUnit.isAirUnit()) {
// Deal no damage (moddable in future?) and no XP
val attackerText =
"Our [$attackerName] (0) was attacked by an intercepting [$interceptorName] (0)"
val interceptorText =
"Our [$interceptorName] (0) intercepted and attacked an enemy [$attackerName] (0)"
attacker.getCivInfo().addNotification(
attackerText, locations,
attackerName, NotificationIcon.War, interceptorName
)
interceptingCiv.addNotification(
interceptorText, locations,
interceptorName, NotificationIcon.War, attackerName
)
attacker.unit.action = null
return
} else {
// Damage if Air v Air should work similar to Melee
damageDealt = takeDamage(attacker, MapUnitCombatant(interceptor))
// 5 XP to both
addXp(MapUnitCombatant(interceptor), 5, attacker)
addXp(attacker, 5, MapUnitCombatant(interceptor))
}
if (attacker.isDefeated()) {
if (interceptor.getTile() in attacker.getCivInfo().viewableTiles) {
val attackerText =
"Our [$attackerName] (${damageDealt.attackerDealt}) was destroyed by an intercepting [$interceptorName] (${damageDealt.defenderDealt})"
attacker.getCivInfo().addNotification(
attackerText, locations,
attackerName, NotificationIcon.War, interceptorName
)
} else {
val attackerText =
"Our [$attackerName] (${damageDealt.attackerDealt}) was destroyed by an unknown interceptor"
attacker.getCivInfo().addNotification(
attackerText, locationsInterceptorUnknown,
attackerName, NotificationIcon.War, NotificationIcon.Question
)
}
val interceptorText =
"Our [$interceptorName] (${damageDealt.defenderDealt}) intercepted and destroyed an enemy [$attackerName] (${damageDealt.attackerDealt})"
interceptingCiv.addNotification(
interceptorText, locations,
interceptorName, NotificationIcon.War, attackerName
)
} else if (MapUnitCombatant(interceptor).isDefeated()) {
val attackerText =
"Our [$attackerName] (${damageDealt.attackerDealt}) destroyed an intercepting [$interceptorName] (${damageDealt.defenderDealt})"
attacker.getCivInfo().addNotification(
attackerText, locations,
attackerName, NotificationIcon.War, interceptorName
)
if (attacker.getTile() in interceptingCiv.viewableTiles) {
val interceptorText =
"Our [$interceptorName] (${damageDealt.defenderDealt}) intercepted and was destroyed by an enemy [$attackerName](${damageDealt.attackerDealt}) "
interceptingCiv.addNotification(
interceptorText, locations,
interceptorName, NotificationIcon.War, attackerName
)
} else {
val interceptorText =
"Our [$interceptorName] (${damageDealt.defenderDealt}) intercepted and was destroyed by an unknown enemy"
interceptingCiv.addNotification(
interceptorText, locationsAttackerUnknown,
interceptorName, NotificationIcon.War, NotificationIcon.Question
)
}
} else {
val attackerText =
"Our [$attackerName] (${damageDealt.attackerDealt}) was attacked by an intercepting [$interceptorName] (${damageDealt.defenderDealt})"
val interceptorText =
"Our [$interceptorName] (${damageDealt.defenderDealt}) intercepted and attacked an enemy [$attackerName] (${damageDealt.attackerDealt})"
attacker.getCivInfo().addNotification(
attackerText, locations,
attackerName, NotificationIcon.War, interceptorName
)
interceptingCiv.addNotification(
interceptorText, locations,
interceptorName, NotificationIcon.War, attackerName
)
}
attacker.unit.action = null
return
}
// No Interceptions available
val attackerText = "Nothing tried to intercept our [$attackerName]"
attacker.getCivInfo().addNotification(attackerText, attackerName)
attacker.unit.action = null
}
private fun tryInterceptAirAttack(attacker: MapUnitCombatant, attackedTile: TileInfo, interceptingCiv: CivilizationInfo, defender: ICombatant?) {
if (attacker.unit.hasUnique(UniqueType.CannotBeIntercepted, StateForConditionals(attacker.getCivInfo(), ourCombatant = attacker, theirCombatant = defender, attackedTile = attackedTile)))
return

View File

@ -122,6 +122,9 @@ object BattleDamage {
if (attacker.unit.type.isWaterUnit() && attacker.isMelee() && !defender.getTile().isWater
&& !attacker.unit.hasUnique(UniqueType.AttackAcrossCoast) && !defender.isCity())
modifiers["Landing"] = -50
// Air unit attacking with Air Sweep
if (attacker.unit.isPreparingAirSweep())
modifiers.add(getAirSweepAttackModifiers(attacker))
if (attacker.isMelee()) {
val numberOfAttackersSurroundingDefender = defender.getTile().neighbors.count {
@ -155,6 +158,20 @@ object BattleDamage {
return modifiers
}
fun getAirSweepAttackModifiers(
attacker: ICombatant
): Counter<String> {
val modifiers = Counter<String>()
if (attacker is MapUnitCombatant) {
for (unique in attacker.unit.getUniques().filter{it.isOfType(UniqueType.StrengthWhenAirsweep)}) {
modifiers.add(getModifierStringFromUnique(unique), unique.params[0].toInt())
}
}
return modifiers
}
fun getDefenceModifiers(attacker: ICombatant, defender: ICombatant): Counter<String> {
val modifiers = getGeneralModifiers(defender, attacker, CombatAction.Defend)
val tile = defender.getTile()

View File

@ -56,4 +56,5 @@ class MapUnitCombatant(val unit: MapUnit) : ICombatant {
fun hasUnique(uniqueType: UniqueType, conditionalState: StateForConditionals? = null): Boolean =
if (conditionalState == null) unit.hasUnique(uniqueType)
else unit.hasUnique(uniqueType, conditionalState)
}

View File

@ -35,6 +35,7 @@ object NotificationIcon {
const val Scout = "UnitIcons/Scout"
const val Ruins = "ImprovementIcons/Ancient ruins"
const val Barbarians = "ImprovementIcons/Barbarian encampment"
const val Question = "OtherIcons/Question"
}
/**

View File

@ -440,6 +440,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
fun isAutomated() = action == UnitActionType.Automate.value
fun isExploring() = action == UnitActionType.Explore.value
fun isPreparingParadrop() = action == UnitActionType.Paradrop.value
fun isPreparingAirSweep() = action == UnitActionType.AirSweep.value
fun isSetUpForSiege() = action == UnitActionType.SetUp.value
/** For display in Unit Overview */
@ -858,7 +859,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
action = null // wake up when healed
}
if (isPreparingParadrop())
if (isPreparingParadrop() || isPreparingAirSweep())
action = null
if (hasUnique(UniqueType.ReligiousUnit)
@ -1302,7 +1303,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
fun actionsOnDeselect() {
showAdditionalActions = false
if (isPreparingParadrop()) action = null
if (isPreparingParadrop() || isPreparingAirSweep()) action = null
}
fun getForceEvaluation(): Int {

View File

@ -101,6 +101,8 @@ enum class UnitActionType(
{ ImageGetter.getImage("OtherIcons/Pillage") }, 'p'),
Paradrop("Paradrop",
{ ImageGetter.getUnitIcon("Paratrooper") }, 'p'),
AirSweep("Air Sweep",
{ ImageGetter.getImage("OtherIcons/AirSweep") }, 'a'),
SetUp("Set up",
{ ImageGetter.getUnitIcon("Catapult") }, 't', UncivSound.Setup),
FoundCity("Found city",

View File

@ -430,6 +430,8 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
ExtraInterceptionsPerTurn("[amount] extra interceptions may be made per turn", UniqueTarget.Unit),
CannotBeIntercepted("Cannot be intercepted", UniqueTarget.Unit),
CannotInterceptUnits("Cannot intercept [mapUnitFilter] units", UniqueTarget.Unit),
CanAirsweep("Can perform Air Sweep", UniqueTarget.Unit),
StrengthWhenAirsweep("[relativeAmount]% Strength when performing Air Sweep", UniqueTarget.Unit),
UnitMaintenanceDiscount("[relativeAmount]% maintenance costs", UniqueTarget.Unit, UniqueTarget.Global),
UnitUpgradeCost("[relativeAmount]% Gold cost of upgrading", UniqueTarget.Unit, UniqueTarget.Global),

View File

@ -190,7 +190,7 @@ class WorldMapHolder(
it.movement.canMoveTo(tileInfo) ||
it.movement.isUnknownTileWeShouldAssumeToBePassable(tileInfo) && !it.baseUnit.movesLikeAirUnits()
}
)) {
) && previousSelectedUnits.any { !it.isPreparingAirSweep()}) {
if (previousSelectedUnitIsSwapping) {
addTileOverlaysWithUnitSwapping(previousSelectedUnits.first(), tileInfo)
}
@ -647,7 +647,7 @@ class WorldMapHolder(
for (tile in tilesInMoveRange) {
for (tileToColor in tileGroups[tile]!!) {
if (isAirUnit)
if (isAirUnit && !unit.isPreparingAirSweep()) {
if (tile.aerialDistanceTo(unit.getTile()) <= unit.getRange()) {
// The tile is within attack range
tileToColor.showHighlight(Color.RED, 0.3f)
@ -655,6 +655,7 @@ class WorldMapHolder(
// The tile is within move range
tileToColor.showHighlight(Color.BLUE, 0.3f)
}
}
if (unit.movement.canMoveTo(tile) ||
unit.movement.isUnknownTileWeShouldAssumeToBePassable(tile) && !unit.baseUnit.movesLikeAirUnits())
tileToColor.showHighlight(moveTileOverlayColor,

View File

@ -60,6 +60,10 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
val selectedTile = worldScreen.mapHolder.selectedTile
?: return hide() // no selected tile
simulateNuke(attacker, selectedTile)
} else if (attacker is MapUnitCombatant && attacker.unit.isPreparingAirSweep()) {
val selectedTile = worldScreen.mapHolder.selectedTile
?: return hide() // no selected tile
simulateAirsweep(attacker, selectedTile)
} else {
val defender = tryGetDefender() ?: return hide()
if (attacker is CityCombatant && defender is CityCombatant) return hide()
@ -113,6 +117,21 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
if (combatant is MapUnitCombatant) UnitGroup(combatant.unit,25f)
else ImageGetter.getNationIndicator(combatant.getCivInfo().nation, 25f)
private val quarterScreen = worldScreen.stage.width / 4
private fun getModifierTable(key: String, value: Int) = Table().apply {
val description = if (key.startsWith("vs "))
("vs [" + key.drop(3) + "]").tr()
else key.tr()
val percentage = (if (value > 0) "+" else "") + value + "%"
val upOrDownLabel = if (value > 0f) "".toLabel(Color.GREEN)
else "".toLabel(Color.RED)
add(upOrDownLabel)
val modifierLabel = "$percentage $description".toLabel(fontSize = 14).apply { wrap = true }
add(modifierLabel).width(quarterScreen - upOrDownLabel.minWidth)
}
private fun simulateBattle(attacker: ICombatant, defender: ICombatant){
clear()
@ -139,21 +158,6 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
add(attacker.getAttackingStrength().toString() + attackIcon)
add(defender.getDefendingStrength(attacker.isRanged()).toString() + defenceIcon).row()
val quarterScreen = worldScreen.stage.width / 4
fun getModifierTable(key: String, value: Int) = Table().apply {
val description = if (key.startsWith("vs "))
("vs [" + key.drop(3) + "]").tr()
else key.tr()
val percentage = (if (value > 0) "+" else "") + value + "%"
val upOrDownLabel = if (value > 0f) "".toLabel(Color.GREEN)
else "".toLabel(Color.RED)
add(upOrDownLabel)
val modifierLabel = "$percentage $description".toLabel(fontSize = 14).apply { wrap = true }
add(modifierLabel).width(quarterScreen - upOrDownLabel.minWidth)
}
val attackerModifiers =
BattleDamage.getAttackModifiers(attacker, defender).map {
getModifierTable(it.key, it.value)
@ -323,4 +327,63 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
setPosition(worldScreen.stage.width / 2 - width / 2, 5f)
}
private fun simulateAirsweep(attacker: MapUnitCombatant, targetTile: TileInfo)
{
clear()
val attackerNameWrapper = Table()
val attackerLabel = attacker.getName().toLabel()
attackerNameWrapper.add(getIcon(attacker)).padRight(5f)
attackerNameWrapper.add(attackerLabel)
add(attackerNameWrapper)
val canAttack = attacker.canAttack()
val defenderLabel = Label("???", skin)
add(defenderLabel).row()
addSeparator().pad(0f)
val attackIcon = Fonts.rangedStrength
add(attacker.getAttackingStrength().toString() + attackIcon)
add("???$attackIcon").row()
val attackerModifiers =
BattleDamage.getAirSweepAttackModifiers(attacker).map {
getModifierTable(it.key, it.value)
}
for (modifier in attackerModifiers) {
add(modifier)
add()
row().pad(2f)
}
add(getHealthBar(attacker.getHealth(), attacker.getMaxHealth(), 0))
add(getHealthBar(attacker.getMaxHealth(), attacker.getMaxHealth(), 0))
row().pad(5f)
val attackButton = "Air Sweep".toTextButton().apply { color = Color.RED }
val canReach = attacker.unit.currentTile.getTilesInDistance(attacker.unit.getRange()).contains(targetTile)
if (!worldScreen.isPlayersTurn || !attacker.canAttack() || !canReach || !canAttack) {
attackButton.disable()
attackButton.label.color = Color.GRAY
}
else {
attackButton.onClick(attacker.getAttackSound()) {
Battle.airSweep(attacker, targetTile)
worldScreen.mapHolder.removeUnitActionOverlay() // the overlay was one of attacking
worldScreen.shouldUpdate = true
}
}
add(attackButton).colspan(2)
pack()
setPosition(worldScreen.stage.width / 2 - width / 2, 5f)
}
}

View File

@ -57,6 +57,7 @@ object UnitActions {
addUnitUpgradeAction(unit, actionList)
addPillageAction(unit, actionList, worldScreen)
addParadropAction(unit, actionList)
addAirSweepAction(unit, actionList)
addSetupAction(unit, actionList)
addFoundCityAction(unit, actionList, tile)
addBuildingImprovementsAction(unit, actionList, tile, worldScreen, unitTable)
@ -267,6 +268,21 @@ object UnitActions {
})
}
private fun addAirSweepAction(unit: MapUnit, actionList: ArrayList<UnitAction>) {
val airsweepUniques =
unit.getMatchingUniques(UniqueType.CanAirsweep)
if (!airsweepUniques.any()) return
actionList += UnitAction(UnitActionType.AirSweep,
isCurrentAction = unit.isPreparingAirSweep(),
action = {
if (unit.isPreparingAirSweep()) unit.action = null
else unit.action = UnitActionType.AirSweep.value
}.takeIf {
unit.canAttack()
}
)
}
private fun addPillageAction(unit: MapUnit, actionList: ArrayList<UnitAction>, worldScreen: WorldScreen) {
val pillageAction = getPillageAction(unit)
?: return

View File

@ -255,6 +255,8 @@ class UnitTable(val worldScreen: WorldScreen) : Table(){
// Do not select a different unit or city center if we click on it to swap our current unit to it
if (selectedUnitIsSwapping && selectedUnit != null && selectedUnit!!.movement.canUnitSwapTo(selectedTile)) return
// Do no select a different unit while in Air Sweep mode
if (selectedUnit != null && selectedUnit!!.isPreparingAirSweep()) return
when {
forceSelectUnit != null ->

View File

@ -621,6 +621,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https:
- [survival knife](https://thenounproject.com/search/?q=survival&i=2663392) by b faris for Survivalism
- [Shamrock](https://thenounproject.com/term/shamrock/358507/) By P Thanga Vignesh for Pictish Courage
- [home sweet home](https://thenounproject.com/term/home-sweet-home/3817166/) By Silviu Ojog for Home Sweet Home
- [Star](https://thenounproject.com/icon/star-35340/) by Trent Kuhn for Dogfighting
### Religions
@ -717,6 +718,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https:
- [Aircraft](https://thenounproject.com/search/?q=aircraft&i=1629000) By Tom Fricker for aircraft icon in city button
- [radar scan](https://thenounproject.com/search/?q=range&i=1500234) By icon 54 for Range
- [short range radar](https://thenounproject.com/search/?q=air%20range&i=2612731) by Vectors Point for Intercept range
- [AirSweep](https://thenounproject.com/icon/jet-134340/) by Creative Stall for Air Sweep icon
- [Puppet](https://thenounproject.com/search/?q=puppet&i=285735) By Ben Davis for puppeted cities
- [City](https://thenounproject.com/search/?q=city&i=1765370) By Muhajir ila Robbi in the Icon center
- [Lock](https://thenounproject.com/search/?q=lock&i=3217613) by Vadim Solomakhin for locked tiles
@ -742,6 +744,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https:
- [turn right](https://thenounproject.com/icon/turn-right-1920867/) by Alice Design for Resource Overview
- [Tyrannosaurus Rex](https://thenounproject.com/icon/tyrannosaurus-rex-4130976/) by Amethyst Studio for Civilopedia Eras header
- [Timer](https://www.flaticon.com/free-icons/timer) created by Gregor Cresnar Premium - Flaticon
- [Question](https://thenounproject.com/icon/question-1157126/) created by Aneeque Ahmed for Question Icon
### Main menu