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>
After Width: | Height: | Size: 9.2 KiB |
BIN
android/Images/OtherIcons/AirSweep.png
Normal file
After Width: | Height: | Size: 6.1 KiB |
BIN
android/Images/OtherIcons/Question.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
@ -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
|
||||
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
@ -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"]
|
||||
},
|
||||
|
@ -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"
|
||||
|
@ -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"]
|
||||
},
|
||||
|
@ -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"
|
||||
|
@ -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."
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -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! =
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 ->
|
||||
|
@ -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
|
||||
|
||||
|