Unit swapping (#4049)

* Added an icon for unit swapping

* Implemented unit swapping

In the original Civ V, unit swapping is a supported mechanic.
If you try to move a unit to a tile with another of your units, and both
units have enough movement points left to reach the other's tile, they
will swap places. They will consume only the movement points needed to
reach the other's tile in this way.

This change implements unit swapping for Unciv.

To prevent all kinds of problems from arising with automatic unit
movement, unit swapping can only be done explicitly. This also means
that it can only be done if the unit-swap movement is possible in a
single turn. It is however not limited to adjacent units.

Because Unciv supports mobile devices, there is in general no separation
between a unit-selection click and a movement click. Clicking on another
unit while a unit is selected simply selects that other unit. Because we
do not want to make it more difficult to select other units in this way,
unit swapping is implemented as a separate "movement mode": to toggle
this mode on or off, the new unit action "Swap units" must be used.
Newly selected units still always start in the normal movement mode.

In the unit-swap movement mode, the possible swap tiles are highlighted
instead of the possible movement and attack tiles. Clicking on a
highlighted tile will display a swap button, similar to the movement
button, or instantly perform the swap if single-click-movement is
enabled. This new behavior overrides the selection of the unit on the
target tile: if the user wants to select the unit instead, they have to
exit the unit-swapping mode first.

The swapping code is robust, it can even handle swaps that involve a
paradrop!

An option to always swap-move when an eligible tile is clicked instead
of requiring the unit-swapping mode, similar to the existing
single-click-movement option, could perhaps be added in later.

* Added some comments to existing movement functions

* Fixed a silly mistake

Fixed a silly mistake which caused the unit-swapping eligibility
detection to sometimes remove units from the world.

* Removed some unneeded code

* Fixed movement buttons not showing with world wrap

Fixed a bug where the "move here" and "swap with" buttons would only
show on the leftmost copy of the world when world wrap was enabled.

* Made the swap action only display if usable

Made the unit swapping button only display if there is at least one
possible swap movement.
This commit is contained in:
Arthur van der Staaij 2021-06-06 21:56:25 +02:00 committed by GitHub
parent cc3eeb3c7c
commit a7afc0718c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 599 additions and 385 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 956 KiB

After

Width:  |  Height:  |  Size: 958 KiB

View File

@ -508,6 +508,7 @@ Bombard strength = Bombard sterkte
Range = Bereik
Move unit = Eenheid verplaatsen
Stop movement = Verplaatsen stoppen
Swap units = Verwissel eenheden
Construct improvement = Verbetering bouwen
Automate = Automatiseren
Stop automation = Stop automatiseren

View File

@ -493,6 +493,7 @@ Bombard strength =
Range =
Move unit =
Stop movement =
Swap units =
Construct improvement =
Automate =
Stop automation =

View File

@ -61,6 +61,10 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
fun isUnknownTileWeShouldAssumeToBePassable(tileInfo: TileInfo) = !unit.civInfo.exploredTiles.contains(tileInfo.position)
/**
* Does not consider if tiles can actually be entered, use canMoveTo for that.
* If a tile can be reached within the turn, but it cannot be passed through, the total distance to it is set to unitMovement
*/
fun getDistanceToTilesWithinTurn(origin: Vector2, unitMovement: Float): PathsToTilesWithinTurn {
val distanceToTiles = PathsToTilesWithinTurn()
if (unitMovement == 0f) return distanceToTiles
@ -96,7 +100,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
else
totalDistanceToTile = unitMovement
// In Civ V, you can always travel between adjacent tiles, even if you don't technically
// have enough movement points - it simple depletes what you have
// have enough movement points - it simply depletes what you have
distanceToTiles[neighbor] = ParentTileAndTotalDistance(tileToCheck, totalDistanceToTile)
}
@ -108,7 +112,10 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
return distanceToTiles
}
/** Returns an empty list if there's no way to get there */
/**
* Does not consider if the destination tile can actually be entered, use canMoveTo for that.
* Returns an empty list if there's no way to get to the destination.
*/
fun getShortestPath(destination: TileInfo): List<TileInfo> {
val currentTile = unit.getTile()
if (currentTile.position == destination) return listOf(currentTile) // edge case that's needed, so that workers will know that they can reach their own tile. *sigh*
@ -208,11 +215,73 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
/** This is performance-heavy - use as last resort, only after checking everything else! */
fun canReach(destination: TileInfo): Boolean {
if (unit.type.isAirUnit() || unit.action == Constants.unitActionParadrop)
return canReachInCurrentTurn(destination)
return getShortestPath(destination).any()
}
fun canReachInCurrentTurn(destination: TileInfo): Boolean {
if (unit.type.isAirUnit())
return unit.currentTile.aerialDistanceTo(destination) <= unit.getRange()*2
if (unit.action == Constants.unitActionParadrop)
return getDistance(unit.currentTile.position, destination.position) <= unit.paradropRange && canParadropOn(destination)
return getShortestPath(destination).any()
return getDistance(unit.currentTile.position, destination.position) <= unit.paradropRange && canParadropOn(destination)
return getDistanceToTiles().containsKey(destination)
}
fun getReachableTilesInCurrentTurn(): Sequence<TileInfo> {
return when {
unit.type.isAirUnit() ->
unit.getTile().getTilesInDistanceRange(IntRange(1, unit.getRange() * 2))
unit.action == Constants.unitActionParadrop ->
unit.getTile().getTilesInDistance(unit.paradropRange)
.filter { unit.movement.canParadropOn(it) }
else ->
unit.movement.getDistanceToTiles().keys.asSequence()
}
}
/** Returns whether we can perform a swap move to the specified tile */
fun canUnitSwapTo(destination: TileInfo): Boolean {
return canReachInCurrentTurn(destination) && canUnitSwapToReachableTile(destination)
}
/** Returns the tiles to which we can perform a swap move */
fun getUnitSwappableTiles(): Sequence<TileInfo> {
return getReachableTilesInCurrentTurn().filter { canUnitSwapToReachableTile(it) }
}
/**
* Returns whether we can perform a unit swap move to the specified tile, given that it is
* reachable in the current turn
*/
private fun canUnitSwapToReachableTile(reachableTile: TileInfo): Boolean {
// Air units cannot swap
if (unit.type.isAirUnit()) return false
// We can't swap with ourself
if (reachableTile == unit.getTile()) return false
// Check whether the tile contains a unit of the same type as us that we own and that can
// also reach our tile in its current turn.
val otherUnit = (
if (unit.type.isCivilian())
reachableTile.civilianUnit
else
reachableTile.militaryUnit
) ?: return false
val ourPosition = unit.getTile()
if (otherUnit.owner != unit.owner || !otherUnit.movement.canReachInCurrentTurn(ourPosition)) return false
// Check if we could enter their tile if they wouldn't be there
otherUnit.removeFromTile()
val weCanEnterTheirTile = canMoveTo(reachableTile)
otherUnit.putInTile(reachableTile)
if (!weCanEnterTheirTile) return false
// Check if they could enter our tile if we wouldn't be here
unit.removeFromTile()
val theyCanEnterOurTile = otherUnit.movement.canMoveTo(ourPosition)
unit.putInTile(ourPosition)
if (!theyCanEnterOurTile) return false
// All clear!
return true
}
@ -249,7 +318,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
unit.putInTile(destination)
unit.currentMovement = 0f
return
} else if (unit.action == Constants.unitActionParadrop) { // paratroopers move differently
} else if (unit.action == Constants.unitActionParadrop) { // paradropping units move differently
unit.action = null
unit.removeFromTile()
unit.putInTile(destination)
@ -312,6 +381,29 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
}
/**
* Swaps this unit with the unit on the given tile
* Precondition: this unit can swap-move to the given tile, as determined by canUnitSwapTo
*/
fun swapMoveToTile(destination: TileInfo) {
val otherUnit = (
if (unit.type.isCivilian())
destination.civilianUnit
else
destination.militaryUnit
)!! // The precondition guarantees that there is an eligible same-type unit at the destination
val ourOldPosition = unit.getTile()
val theirOldPosition = otherUnit.getTile()
// Swap the units
otherUnit.removeFromTile()
unit.movement.moveToTile(destination)
unit.removeFromTile()
otherUnit.putInTile(theirOldPosition)
otherUnit.movement.moveToTile(ourOldPosition)
unit.putInTile(theirOldPosition)
}
/**
* Designates whether we can enter the tile - without attacking

View File

@ -9,6 +9,7 @@ data class UnitAction(
)
enum class UnitActionType(val value: String) {
SwapUnits("Swap units"),
Automate("Automate"),
StopAutomation("Stop automation"),
StopMovement("Stop movement"),

View File

@ -49,8 +49,12 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
continuousScrollingX = tileMap.mapParameters.worldWrap
}
// Used to transfer data on the "move here" button that should be created, from the side thread to the main thread
class MoveHereButtonDto(val unitToTurnsToDestination: HashMap<MapUnit, Int>, val tileInfo: TileInfo)
// Interface for classes that contain the data required to draw a button
interface ButtonDto
// Contains the data required to draw a "move here" button
class MoveHereButtonDto(val unitToTurnsToDestination: HashMap<MapUnit, Int>, val tileInfo: TileInfo) : ButtonDto
// Contains the data required to draw a "swap with" button
class SwapWithButtonDto(val unit: MapUnit, val tileInfo: TileInfo) : ButtonDto
internal fun addTiles() {
val tileSetStrings = TileSetStrings()
@ -92,6 +96,14 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
thread {
val tile = tileGroup.tileInfo
if (worldScreen.bottomUnitTable.selectedUnitIsSwapping) {
if (unit.movement.canUnitSwapTo(tile)) {
swapMoveUnitToTargetTile(unit, tile)
}
// If we are in unit-swapping mode, we don't want to move or attack
return@thread
}
val attackableTile = BattleHelper.getAttackableEnemies(unit, unit.movement.getDistanceToTiles())
.firstOrNull { it.tileToAttack == tileGroup.tileInfo }
if (unit.canAttack() && attackableTile != null) {
@ -105,7 +117,6 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
moveUnitToTargetTile(listOf(unit), tile)
return@thread
}
}
}
})
@ -128,17 +139,28 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
val unitTable = worldScreen.bottomUnitTable
val previousSelectedUnits = unitTable.selectedUnits.toList() // create copy
val previousSelectedCity = unitTable.selectedCity
val previousSelectedUnitIsSwapping = unitTable.selectedUnitIsSwapping
unitTable.tileSelected(tileInfo)
val newSelectedUnit = unitTable.selectedUnit
if (previousSelectedUnits.isNotEmpty() && previousSelectedUnits.any { it.getTile() != tileInfo }
&& worldScreen.isPlayersTurn
&& previousSelectedUnits.any {
it.movement.canMoveTo(tileInfo) ||
it.movement.isUnknownTileWeShouldAssumeToBePassable(tileInfo) && !it.type.isAirUnit()
}) {
// this can take a long time, because of the unit-to-tile calculation needed, so we put it in a different thread
addTileOverlaysWithUnitMovement(previousSelectedUnits, tileInfo)
&& (
if (previousSelectedUnitIsSwapping)
previousSelectedUnits.first().movement.canUnitSwapTo(tileInfo)
else
previousSelectedUnits.any {
it.movement.canMoveTo(tileInfo) ||
it.movement.isUnknownTileWeShouldAssumeToBePassable(tileInfo) && !it.type.isAirUnit()
}
)) {
if (previousSelectedUnitIsSwapping) {
addTileOverlaysWithUnitSwapping(previousSelectedUnits.first(), tileInfo)
}
else {
// this can take a long time, because of the unit-to-tile calculation needed, so we put it in a different thread
addTileOverlaysWithUnitMovement(previousSelectedUnits, tileInfo)
}
} else addTileOverlays(tileInfo) // no unit movement but display the units in the tile etc.
@ -157,7 +179,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
}
fun moveUnitToTargetTile(selectedUnits: List<MapUnit>, targetTile: TileInfo) {
private fun moveUnitToTargetTile(selectedUnits: List<MapUnit>, targetTile: TileInfo) {
// this can take a long time, because of the unit-to-tile calculation needed, so we put it in a different thread
// THIS PART IS REALLY ANNOYING
// So lets say you have 2 units you want to move in the same direction, right
@ -204,6 +226,20 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
}
}
private fun swapMoveUnitToTargetTile(selectedUnit: MapUnit, targetTile: TileInfo) {
selectedUnit.movement.swapMoveToTile(targetTile)
if (selectedUnit.action == Constants.unitActionExplore || selectedUnit.isMoving())
selectedUnit.action = null // remove explore on manual swap-move
// Perhaps something like a swish-swoosh would be clearer
Sounds.play(UncivSound.Whoosh)
if (selectedUnit.currentMovement > 0) worldScreen.bottomUnitTable.selectUnit(selectedUnit)
worldScreen.shouldUpdate = true
removeUnitActionOverlay()
}
private fun addTileOverlaysWithUnitMovement(selectedUnits: List<MapUnit>, tileInfo: TileInfo) {
thread(name = "TurnsToGetThere") {
@ -250,15 +286,37 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
}
worldScreen.shouldUpdate = true
}
}
}
private fun addTileOverlays(tileInfo: TileInfo, moveHereDto: MoveHereButtonDto? = null) {
private fun addTileOverlaysWithUnitSwapping(selectedUnit: MapUnit, tileInfo: TileInfo) {
if (!selectedUnit.movement.canUnitSwapTo(tileInfo)) { // give the regular tile overlays with no unit swapping
addTileOverlays(tileInfo)
worldScreen.shouldUpdate = true
return
}
if (UncivGame.Current.settings.singleTapMove) {
swapMoveUnitToTargetTile(selectedUnit, tileInfo)
}
else {
// Add "swap with" button
val swapWithButtonDto = SwapWithButtonDto(selectedUnit, tileInfo)
addTileOverlays(tileInfo, swapWithButtonDto)
}
worldScreen.shouldUpdate = true
}
private fun addTileOverlays(tileInfo: TileInfo, buttonDto: ButtonDto? = null) {
for (group in tileGroups[tileInfo]!!) {
val table = Table().apply { defaults().pad(10f) }
if (moveHereDto != null && worldScreen.canChangeState)
table.add(getMoveHereButton(moveHereDto))
if (buttonDto != null && worldScreen.canChangeState)
table.add(
when (buttonDto) {
is MoveHereButtonDto -> getMoveHereButton(buttonDto)
is SwapWithButtonDto -> getSwapWithButton(buttonDto)
else -> null
}
)
val unitList = ArrayList<MapUnit>()
if (tileInfo.isCityCenter()
@ -297,7 +355,6 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
val numberCircle = ImageGetter.getCircle().apply { width = size / 2; height = size / 2;color = Color.BLUE }
moveHereButton.addActor(numberCircle)
moveHereButton.addActor(dto.unitToTurnsToDestination.values.maxOrNull()!!.toLabel().apply { center(numberCircle) })
val firstUnit = dto.unitToTurnsToDestination.keys.first()
val unitIcon = if (dto.unitToTurnsToDestination.size == 1) UnitGroup(firstUnit, size / 2)
@ -319,6 +376,27 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
return moveHereButton
}
private fun getSwapWithButton(dto: SwapWithButtonDto): Group {
val size = 60f
val swapWithButton = Group().apply { width = size;height = size; }
swapWithButton.addActor(ImageGetter.getCircle().apply { width = size; height = size })
swapWithButton.addActor(ImageGetter.getImage("OtherIcons/Swap")
.apply { color = Color.BLACK; width = size / 2; height = size / 2; center(swapWithButton) })
val unitIcon = UnitGroup(dto.unit, size / 2)
unitIcon.y = size - unitIcon.height
swapWithButton.addActor(unitIcon)
swapWithButton.onClick(UncivSound.Silent) {
UncivGame.Current.settings.addCompletedTutorialTask("Move unit")
if (dto.unit.type.isAirUnit())
UncivGame.Current.settings.addCompletedTutorialTask("Move an air unit")
swapMoveUnitToTargetTile(dto.unit, dto.tileInfo)
}
return swapWithButton
}
private fun addOverlayOnTileGroup(group: TileGroup, actor: Actor) {
@ -396,16 +474,32 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
group.selectUnit(unit)
}
// Fade out less relevant images if a military unit is selected
val fadeout = if (unit.type.isCivilian()) 1f
else 0.5f
for (tile in allWorldTileGroups) {
if (tile.icons.populationIcon != null) tile.icons.populationIcon!!.color.a = fadeout
if (tile.icons.improvementIcon != null && tile.tileInfo.improvement != Constants.barbarianEncampment
&& tile.tileInfo.improvement != Constants.ancientRuins)
tile.icons.improvementIcon!!.color.a = fadeout
if (tile.resourceImage != null) tile.resourceImage!!.color.a = fadeout
}
if (worldScreen.bottomUnitTable.selectedUnitIsSwapping) {
val unitSwappableTiles = unit.movement.getUnitSwappableTiles()
val swapUnitsTileOverlayColor = Color.PURPLE
for (tile in unitSwappableTiles) {
for (tileToColor in tileGroups[tile]!!) {
tileToColor.showCircle(swapUnitsTileOverlayColor,
if (UncivGame.Current.settings.singleTapMove) 0.7f else 0.3f)
}
}
return // We don't want to show normal movement or attack overlays in unit-swapping mode
}
val isAirUnit = unit.type.isAirUnit()
val moveTileOverlayColor = if (unit.action == Constants.unitActionParadrop) Color.BLUE else Color.WHITE
val tilesInMoveRange =
if (isAirUnit)
unit.getTile().getTilesInDistanceRange(IntRange(1, unit.getRange() * 2))
else if (unit.action == Constants.unitActionParadrop)
unit.getTile().getTilesInDistance(unit.paradropRange)
.filter { unit.movement.canParadropOn(it) }
else
unit.movement.getDistanceToTiles().keys.asSequence()
val tilesInMoveRange = unit.movement.getReachableTilesInCurrentTurn()
for (tile in tilesInMoveRange) {
for (tileToColor in tileGroups[tile]!!) {
@ -450,18 +544,6 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
else Color.RED
)
}
}
// Fade out less relevant images if a military unit is selected
val fadeout = if (unit.type.isCivilian()) 1f
else 0.5f
for (tile in allWorldTileGroups) {
if (tile.icons.populationIcon != null) tile.icons.populationIcon!!.color.a = fadeout
if (tile.icons.improvementIcon != null && tile.tileInfo.improvement != Constants.barbarianEncampment
&& tile.tileInfo.improvement != Constants.ancientRuins)
tile.icons.improvementIcon!!.color.a = fadeout
if (tile.resourceImage != null) tile.resourceImage!!.color.a = fadeout
}
}

View File

@ -47,6 +47,7 @@ object UnitActions {
)
}
addSwapAction(unit, actionList, worldScreen)
addExplorationActions(unit, actionList)
addPromoteAction(unit, actionList)
addUnitUpgradeAction(unit, actionList)
@ -64,6 +65,27 @@ object UnitActions {
return actionList
}
private fun addSwapAction(unit: MapUnit, actionList: ArrayList<UnitAction>, worldScreen: WorldScreen) {
// Air units cannot swap
if (unit.type.isAirUnit()) return
// Disable unit swapping if multiple units are selected. It would make little sense.
// In principle, the unit swapping mode /will/ function with multiselect: it will simply
// only consider the first selected unit, and ignore the other selections. However, it does
// have the visual bug that the tile overlays for the eligible swap locations are drawn for
// /all/ selected units instead of only the first one. This could be fixed, but again,
// swapping makes little sense for multiselect anyway.
if (worldScreen.bottomUnitTable.selectedUnits.count() > 1) return
// Only show the swap action if there is at least one possible swap movement
if (unit.movement.getUnitSwappableTiles().none()) return
actionList += UnitAction(
type = UnitActionType.SwapUnits,
isCurrentAction = worldScreen.bottomUnitTable.selectedUnitIsSwapping,
action = {
worldScreen.bottomUnitTable.selectedUnitIsSwapping = !worldScreen.bottomUnitTable.selectedUnitIsSwapping
worldScreen.shouldUpdate = true
}
)
}
private fun addDisbandAction(actionList: ArrayList<UnitAction>, unit: MapUnit, worldScreen: WorldScreen) {
actionList += UnitAction(type = UnitActionType.DisbandUnit, action = {

View File

@ -40,6 +40,7 @@ class UnitActionsTable(val worldScreen: WorldScreen) : Table() {
else -> when (unitAction) {
"Move unit" -> return UnitIconAndKey(ImageGetter.getStatIcon("Movement"))
"Stop movement" -> return UnitIconAndKey(ImageGetter.getStatIcon("Movement").apply { color = Color.RED }, '.')
"Swap units" -> return UnitIconAndKey(ImageGetter.getImage("OtherIcons/Swap"), 'y')
"Promote" -> return UnitIconAndKey(ImageGetter.getImage("OtherIcons/Star").apply { color = Color.GOLD }, 'o')
"Construct improvement" -> return UnitIconAndKey(ImageGetter.getUnitIcon(Constants.worker), 'i')
"Automate" -> return UnitIconAndKey(ImageGetter.getUnitIcon("Great Engineer"), 'm')

View File

@ -32,12 +32,15 @@ class UnitTable(val worldScreen: WorldScreen) : Table(){
/** This is in preparation for multi-select and multi-move */
val selectedUnits = ArrayList<MapUnit>()
// Whether the (first) selected unit is in unit-swapping mode
var selectedUnitIsSwapping = false
/** Sending no unit clears the selected units entirely */
fun selectUnit(unit:MapUnit?=null, append:Boolean=false) {
if (!append) selectedUnits.clear()
selectedCity = null
if (unit != null) selectedUnits.add(unit)
selectedUnitIsSwapping = false
}
var selectedCity : CityInfo? = null
@ -223,13 +226,16 @@ class UnitTable(val worldScreen: WorldScreen) : Table(){
val previouslySelectedUnit = selectedUnit
val previousNumberOfSelectedUnits = selectedUnits.size
// 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
if (selectedTile.isCityCenter()
&& (selectedTile.getOwner() == worldScreen.viewingCiv || worldScreen.viewingCiv.isSpectator())) {
citySelected(selectedTile.getCity()!!)
} else if (selectedTile.militaryUnit != null
&& (selectedTile.militaryUnit!!.civInfo == worldScreen.viewingCiv || worldScreen.viewingCiv.isSpectator())
&& selectedTile.militaryUnit!! !in selectedUnits
&& (selectedTile.civilianUnit == null || selectedUnit != selectedTile.civilianUnit)) {
&& (selectedTile.civilianUnit == null || selectedUnit != selectedTile.civilianUnit)) { // Only select the military unit there if we do not currently have the civilian unit selected
selectUnit(selectedTile.militaryUnit!!, Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT))
} else if (selectedTile.civilianUnit != null
&& (selectedTile.civilianUnit!!.civInfo == worldScreen.viewingCiv || worldScreen.viewingCiv.isSpectator())

View File

@ -509,6 +509,7 @@ Unless otherwise specified, all the following are from [the Noun Project](https:
* [Circle](https://thenounproject.com/term/circle/1841891/) By Aybige
* [Arrow](https://thenounproject.com/term/arrow/18123/) By Joe Mortell for movement
* [Swap](https://thenounproject.com/search/?q=swap&i=1259600) By iconomania for swapping units
* [Connection](https://thenounproject.com/search/?q=connection&i=1521886) By Travis Avery
* [Skull](https://thenounproject.com/search/?q=Skull&i=1030702) By Vladimir Belochkin for disbanding units
* [Crosshair](https://thenounproject.com/search/?q=crosshairs&i=916030) By Bakunetsu Kaito for selecting enemies to attack