mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-13 17:28:57 +07:00
Show arrows on map for unit actions. (#5824)
* Show arrows on map for unit actions. * Don't wrap arrows unless world wrap is actually enabled. * Fix transported air units always being treated like teleports. * Simple reviews. * Separate movement arrow visibility checks from WorldScreen. Co-authored-by: Yair Morgenstern <yairm210@hotmail.com>
This commit is contained in:
@ -539,6 +539,7 @@ Show pixel units =
|
||||
Show pixel improvements =
|
||||
Enable nuclear weapons =
|
||||
Show tile yields =
|
||||
Show unit movement arrows =
|
||||
Continuous rendering =
|
||||
When disabled, saves battery life but certain animations will be suspended =
|
||||
Order trade offers by amount =
|
||||
|
@ -36,6 +36,20 @@ object HexMath {
|
||||
return vector.x - vector.y
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a latitude and longitude back into a hex coordinate.
|
||||
* Inverse function of [getLatitude] and [getLongitude].
|
||||
*
|
||||
* @param latitude As from [getLatitude].
|
||||
* @param longitude As from [getLongitude].
|
||||
* @return Hex coordinate. May need to be passed through [roundHexCoords] for further use.
|
||||
* */
|
||||
fun hexFromLatLong(latitude: Float, longitude: Float): Vector2 {
|
||||
val y = (latitude - longitude) / 2f
|
||||
val x = longitude + y
|
||||
return Vector2(x, y)
|
||||
}
|
||||
|
||||
/** returns a vector containing width and height a rectangular map should have to have
|
||||
* approximately the same number of tiles as an hexagonal map given a height/width ratio */
|
||||
fun getEquivalentRectangularSize(size: Int, ratio: Float = 0.65f): Vector2 {
|
||||
@ -73,6 +87,23 @@ object HexMath {
|
||||
// For example, to get to the cell above me, I'll use a (1,1) vector.
|
||||
// To get to the cell below the cell to my bottom-right, I'll use a (-1,-2) vector.
|
||||
|
||||
/**
|
||||
* @param unwrapHexCoord Hex coordinate to unwrap.
|
||||
* @param staticHexCoord Reference hex coordinate.
|
||||
* @param longitudinalRadius Maximum longitudinal absolute value of world tiles, such as from [TileMap.maxLongitude]. The total width is assumed one less than twice this.
|
||||
*
|
||||
* @return The closest hex coordinate to [staticHexCoord] that is equivalent to [unwrapHexCoord]. THIS MAY NOT BE A VALID TILE COORDINATE. It may also require rounding for further use.
|
||||
*
|
||||
* @see [com.unciv.logic.map.TileMap.getUnWrappedPosition]
|
||||
*/
|
||||
fun getUnwrappedNearestTo(unwrapHexCoord: Vector2, staticHexCoord: Vector2, longitudinalRadius: Number): Vector2 {
|
||||
val referenceLong = getLongitude(staticHexCoord)
|
||||
val toWrapLat = getLatitude(unwrapHexCoord) // Working in Cartesian space is easier.
|
||||
val toWrapLong = getLongitude(unwrapHexCoord)
|
||||
val longRadius = longitudinalRadius.toFloat()
|
||||
return hexFromLatLong(toWrapLat, (toWrapLong - referenceLong + longRadius).mod(longRadius * 2f) - longRadius + referenceLong)
|
||||
}
|
||||
|
||||
fun hex2WorldCoords(hexCoord: Vector2): Vector2 {
|
||||
// Distance between cells = 2* normal of triangle = 2* (sqrt(3)/2) = sqrt(3)
|
||||
val xVector = getVectorByClockHour(10).scl(sqrt(3.0).toFloat())
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.unciv.logic.battle
|
||||
|
||||
import com.badlogic.gdx.math.Vector2
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.city.CityInfo
|
||||
@ -10,6 +11,8 @@ import com.unciv.logic.map.RoadStatus
|
||||
import com.unciv.logic.map.TileInfo
|
||||
import com.unciv.models.AttackableTile
|
||||
import com.unciv.models.UnitActionType
|
||||
import com.unciv.models.helpers.UnitMovementMemoryType
|
||||
|
||||
import com.unciv.models.ruleset.unique.StateForConditionals
|
||||
import com.unciv.models.ruleset.unique.Unique
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
@ -57,6 +60,9 @@ object Battle {
|
||||
defender.getCivInfo().civName + " " + defender.getName())
|
||||
}
|
||||
val attackedTile = defender.getTile()
|
||||
if (attacker is MapUnitCombatant) {
|
||||
attacker.unit.attacksSinceTurnStart.add(Vector2(attackedTile.position))
|
||||
}
|
||||
|
||||
if (attacker is MapUnitCombatant && attacker.unit.baseUnit.isAirUnit()) {
|
||||
tryInterceptAirAttack(attacker, attackedTile, defender.getCivInfo())
|
||||
@ -369,6 +375,7 @@ object Battle {
|
||||
// are exempt from zone of control, since units that cannot move after attacking already
|
||||
// lose all remaining movement points anyway.
|
||||
attacker.unit.movement.moveToTile(attackedTile, considerZoneOfControl = false)
|
||||
attacker.unit.mostRecentMoveType = UnitMovementMemoryType.UnitAttacked
|
||||
}
|
||||
}
|
||||
|
||||
@ -899,6 +906,7 @@ object Battle {
|
||||
// NOT defender.unit.movement.moveToTile(toTile) - we want a free teleport
|
||||
defender.unit.removeFromTile()
|
||||
defender.unit.putInTile(toTile)
|
||||
defender.unit.mostRecentMoveType = UnitMovementMemoryType.UnitWithdrew
|
||||
// and count 1 attack for attacker but leave it in place
|
||||
reduceAttackerMovementPointsAndAttacks(attacker, defender)
|
||||
|
||||
|
@ -10,6 +10,7 @@ import com.unciv.logic.city.RejectionReason
|
||||
import com.unciv.logic.civilization.CivilizationInfo
|
||||
import com.unciv.logic.civilization.LocationAction
|
||||
import com.unciv.logic.civilization.NotificationIcon
|
||||
import com.unciv.models.helpers.UnitMovementMemoryType
|
||||
import com.unciv.models.UnitActionType
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.ruleset.tile.TerrainType
|
||||
@ -24,6 +25,7 @@ import com.unciv.ui.utils.toPercent
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.math.pow
|
||||
|
||||
|
||||
/**
|
||||
* The immutable properties and mutable game state of an individual unit present on the map
|
||||
*/
|
||||
@ -149,7 +151,7 @@ class MapUnit {
|
||||
|
||||
fun shortDisplayName(): String {
|
||||
return if (instanceName != null) "[$instanceName]"
|
||||
else "[$name]"
|
||||
else "[$name]"
|
||||
}
|
||||
|
||||
var currentMovement: Float = 0f
|
||||
@ -170,6 +172,47 @@ class MapUnit {
|
||||
var religion: String? = null
|
||||
var religiousStrengthLost = 0
|
||||
|
||||
/**
|
||||
* Container class to represent a single instant in a [MapUnit]'s recent movement history.
|
||||
*
|
||||
* @property position Position on the map at this instant.
|
||||
* @property type Category of the last change in position that brought the unit to this position.
|
||||
* */
|
||||
class UnitMovementMemory() {
|
||||
constructor(position: Vector2, type: UnitMovementMemoryType): this() {
|
||||
this.position = position
|
||||
this.type = type
|
||||
}
|
||||
lateinit var position: Vector2
|
||||
lateinit var type: UnitMovementMemoryType
|
||||
fun clone() = UnitMovementMemory(Vector2(position), type)
|
||||
override fun toString() = "${this::class.simpleName}($position, $type)"
|
||||
}
|
||||
|
||||
/** Deep clone an ArrayList of [UnitMovementMemory]s. */
|
||||
private fun ArrayList<UnitMovementMemory>.copy() = ArrayList(this.map { it.clone() })
|
||||
|
||||
/** FIFO list of this unit's past positions. Should never exceed two items in length. New item added once at end of turn and once at start, to allow rare between-turn movements like melee withdrawal to be distinguished. Used in movement arrow overlay. */
|
||||
var movementMemories = ArrayList<UnitMovementMemory>()
|
||||
|
||||
/** Add the current position and the most recent movement type to [movementMemories]. Called once at end and once at start of turn, and at unit creation. */
|
||||
fun addMovementMemory() {
|
||||
movementMemories.add(UnitMovementMemory(Vector2(getTile().position), mostRecentMoveType))
|
||||
while (movementMemories.size > 2) { // O(n) but n == 2.
|
||||
// Keep at most one arrow segment— A lot of the time even that won't be rendered because the two positions will be the same.
|
||||
// When in the unit's turn— I.E. For a player unit— The last two entries will be from .endTurn() followed by from .startTurn(), so the segment from .movementMemories will have zero length. Instead, what gets seen will be the segment from the end of .movementMemories to the unit's current position.
|
||||
// When not in the unit's turn— I.E. For a foreign unit— The segment from the end of .movementMemories to the unit's current position will have zero length, while the last two entries here will be from .startTurn() followed by .endTurn(), so the segment here will be what gets shown.
|
||||
// The exception is when a unit changes position when not in its turn, such as by melee withdrawal or foreign territory expulsion. Then the segment here and the segment from the end of here to the current position can both be shown.
|
||||
movementMemories.removeFirst()
|
||||
}
|
||||
}
|
||||
|
||||
/** The most recent type of position change this unit has experienced. Used in movement arrow overlay.*/
|
||||
var mostRecentMoveType = UnitMovementMemoryType.UnitMoved
|
||||
|
||||
/** Array list of all the tiles that this unit has attacked since the start of its most recent turn. Used in movement arrow overlay. */
|
||||
var attacksSinceTurnStart = ArrayList<Vector2>()
|
||||
|
||||
//region pure functions
|
||||
fun clone(): MapUnit {
|
||||
val toReturn = MapUnit()
|
||||
@ -189,6 +232,9 @@ class MapUnit {
|
||||
toReturn.maxAbilityUses.putAll(maxAbilityUses)
|
||||
toReturn.religion = religion
|
||||
toReturn.religiousStrengthLost = religiousStrengthLost
|
||||
toReturn.movementMemories = movementMemories.copy()
|
||||
toReturn.mostRecentMoveType = mostRecentMoveType
|
||||
toReturn.attacksSinceTurnStart = ArrayList(attacksSinceTurnStart.map { Vector2(it) })
|
||||
return toReturn
|
||||
}
|
||||
|
||||
@ -740,6 +786,8 @@ class MapUnit {
|
||||
|
||||
doCitadelDamage()
|
||||
doTerrainDamage()
|
||||
|
||||
addMovementMemory()
|
||||
}
|
||||
|
||||
fun startTurn() {
|
||||
@ -773,6 +821,9 @@ class MapUnit {
|
||||
val tileOwner = getTile().getOwner()
|
||||
if (tileOwner != null && !canEnterForeignTerrain && !civInfo.canPassThroughTiles(tileOwner) && !tileOwner.isCityState()) // if an enemy city expanded onto this tile while I was in it
|
||||
movement.teleportToClosestMoveableTile()
|
||||
|
||||
addMovementMemory()
|
||||
attacksSinceTurnStart.clear()
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
|
27
core/src/com/unciv/logic/map/MapVisualization.kt
Normal file
27
core/src/com/unciv/logic/map/MapVisualization.kt
Normal file
@ -0,0 +1,27 @@
|
||||
package com.unciv.logic.map
|
||||
|
||||
import com.badlogic.gdx.math.Vector2
|
||||
import com.unciv.logic.GameInfo
|
||||
import com.unciv.logic.civilization.CivilizationInfo
|
||||
|
||||
/** Helper class for making decisions about more abstract information that may be displayed on the world map (or fair to use in AI), but which does not have any direct influence on save state, rules, or behaviour. */
|
||||
class MapVisualization(val gameInfo: GameInfo, val viewingCiv: CivilizationInfo) {
|
||||
|
||||
/** @return Whether a unit's past movements should be visible to the player. */
|
||||
fun isUnitPastVisible(unit: MapUnit): Boolean {
|
||||
if (unit.civInfo == viewingCiv)
|
||||
return true
|
||||
val checkPositions = sequenceOf(unit.movementMemories.asSequence().map { it.position }, sequenceOf(unit.getTile().position)).flatten()
|
||||
return checkPositions.all { gameInfo.tileMap[it] in viewingCiv.viewableTiles }
|
||||
&& (!unit.isInvisible(viewingCiv) || unit.getTile() in viewingCiv.viewableInvisibleUnitsTiles)
|
||||
// Past should always be visible for own units. Past should be visible for foreign units if the unit is visible and both its current tile and previous tiles are visible.
|
||||
}
|
||||
|
||||
/** @return Whether a unit's planned movements should be visible to the player. */
|
||||
fun isUnitFutureVisible(unit: MapUnit) = (viewingCiv.isSpectator() || unit.civInfo == viewingCiv)
|
||||
// Plans should be visible always for own units and never for foreign units.
|
||||
|
||||
/** @return Whether an attack by a unit to a target should be visible to the player. */
|
||||
fun isAttackVisible(attacker: MapUnit, target: Vector2) = (attacker.civInfo == viewingCiv || attacker.getTile() in viewingCiv.viewableTiles || gameInfo.tileMap[target] in viewingCiv.viewableTiles)
|
||||
// Attacks by the player civ should always be visible, and attacks by foreign civs should be visible if either the tile they targeted or the attacker's tile are visible. E.G. Civ V shows bombers coming out of the Fog of War.
|
||||
}
|
@ -496,6 +496,7 @@ class TileMap {
|
||||
// only once we know the unit can be placed do we add it to the civ's unit list
|
||||
unit.putInTile(unitToPlaceTile)
|
||||
unit.currentMovement = unit.getMaxMovement().toFloat()
|
||||
unit.addMovementMemory()
|
||||
|
||||
// Only once we add the unit to the civ we can activate addPromotion, because it will try to update civ viewable tiles
|
||||
for (promotion in unit.baseUnit.promotions)
|
||||
|
@ -4,6 +4,7 @@ import com.badlogic.gdx.math.Vector2
|
||||
import com.unciv.Constants
|
||||
import com.unciv.logic.HexMath.getDistance
|
||||
import com.unciv.logic.civilization.CivilizationInfo
|
||||
import com.unciv.models.helpers.UnitMovementMemoryType
|
||||
|
||||
class UnitMovementAlgorithms(val unit:MapUnit) {
|
||||
|
||||
@ -392,6 +393,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
|
||||
// Cancel sleep or fortification if forcibly displaced - for now, leave movement / auto / explore orders
|
||||
if (unit.isSleeping() || unit.isFortified())
|
||||
unit.action = null
|
||||
unit.mostRecentMoveType = UnitMovementMemoryType.UnitTeleported
|
||||
}
|
||||
// it's possible that there is no close tile, and all the guy's cities are full.
|
||||
// Nothing we can do.
|
||||
@ -401,17 +403,20 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
|
||||
fun moveToTile(destination: TileInfo, considerZoneOfControl: Boolean = true) {
|
||||
if (destination == unit.getTile()) return // already here!
|
||||
|
||||
|
||||
if (unit.baseUnit.movesLikeAirUnits()) { // air units move differently from all other units
|
||||
unit.action = null
|
||||
unit.removeFromTile()
|
||||
unit.isTransported = false // it has left the carrier by own means
|
||||
unit.putInTile(destination)
|
||||
unit.currentMovement = 0f
|
||||
unit.mostRecentMoveType = UnitMovementMemoryType.UnitTeleported
|
||||
return
|
||||
} else if (unit.isPreparingParadrop()) { // paradropping units move differently
|
||||
unit.action = null
|
||||
unit.removeFromTile()
|
||||
unit.putInTile(destination)
|
||||
unit.mostRecentMoveType = UnitMovementMemoryType.UnitTeleported
|
||||
unit.useMovementPoints(1f)
|
||||
unit.attacksThisTurn += 1
|
||||
// Check if unit maintenance changed
|
||||
@ -430,6 +435,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
|
||||
val movableTiles = pathToDestination.takeWhile { canPassThrough(it) }
|
||||
val lastReachableTile = movableTiles.lastOrNull { canMoveTo(it) }
|
||||
?: return // no tiles can pass though/can move to
|
||||
unit.mostRecentMoveType = UnitMovementMemoryType.UnitMoved
|
||||
val pathToLastReachableTile = distanceToTiles.getPathToTile(lastReachableTile)
|
||||
|
||||
if (unit.isFortified() || unit.isSetUpForSiege() || unit.isSleeping())
|
||||
@ -492,6 +498,7 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
|
||||
payload.removeFromTile()
|
||||
payload.putInTile(lastReachableTile)
|
||||
payload.isTransported = true // restore the flag to not leave the payload in the cit
|
||||
payload.mostRecentMoveType = UnitMovementMemoryType.UnitMoved
|
||||
}
|
||||
|
||||
// Unit maintenance changed
|
||||
@ -524,6 +531,8 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
|
||||
otherUnit.putInTile(theirOldPosition)
|
||||
otherUnit.movement.moveToTile(ourOldPosition)
|
||||
unit.putInTile(theirOldPosition)
|
||||
otherUnit.mostRecentMoveType = UnitMovementMemoryType.UnitMoved
|
||||
unit.mostRecentMoveType = UnitMovementMemoryType.UnitMoved
|
||||
}
|
||||
|
||||
/**
|
||||
|
25
core/src/com/unciv/models/helpers/MapArrowType.kt
Normal file
25
core/src/com/unciv/models/helpers/MapArrowType.kt
Normal file
@ -0,0 +1,25 @@
|
||||
package com.unciv.models.helpers
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
|
||||
/** Base interface for classes the instances of which signify a distinctive type of look and feel with which to draw arrows on the map. */
|
||||
interface MapArrowType
|
||||
|
||||
/** Enum constants describing how/why a unit changed position. Each is also associated with an arrow type to draw on the map overlay. */
|
||||
enum class UnitMovementMemoryType: MapArrowType {
|
||||
UnitMoved,
|
||||
UnitAttacked, // For when attacked, killed, and moved into tile.
|
||||
UnitWithdrew, // Caravel, destroyer, etc.
|
||||
UnitTeleported, // Paradrop, open borders end, air rebase, etc.
|
||||
}
|
||||
|
||||
/** Enum constants describing assorted commonly used arrow types. */
|
||||
enum class MiscArrowTypes: MapArrowType {
|
||||
UnitMoving,
|
||||
UnitHasAttacked, // For attacks that didn't result in moving into the target tile. E.G. Ranged, air strike, melee but the target survived, melee but not allowed in target terrain.
|
||||
}
|
||||
|
||||
/** Class for arrow types signifying that a generic arrow style should be used and tinted.
|
||||
* @property color The colour that the arrow should be tinted. */
|
||||
data class TintedMapArrow(val color: Color): MapArrowType
|
||||
// Not currently used in core code, but allows one-off colour-coded arrows to be drawn without having to add a whole new texture and enum constant. Could be useful for debug— Visualize what your AI is doing, or which tiles are affecting resource placement or whatever. Also thinking of mod scripting.
|
@ -14,6 +14,8 @@ class GameSettings {
|
||||
var showWorkedTiles: Boolean = false
|
||||
var showResourcesAndImprovements: Boolean = true
|
||||
var showTileYields: Boolean = false
|
||||
var showUnitMovements: Boolean = false
|
||||
|
||||
var checkForDueUnits: Boolean = true
|
||||
var singleTapMove: Boolean = false
|
||||
var language: String = "English"
|
||||
|
@ -150,4 +150,4 @@ class TileGroupMap<T: TileGroup>(
|
||||
// For debugging purposes
|
||||
override fun draw(batch: Batch?, parentAlpha: Float) { super.draw(batch, parentAlpha) }
|
||||
override fun act(delta: Float) { super.act(delta) }
|
||||
}
|
||||
}
|
||||
|
@ -2,22 +2,25 @@ package com.unciv.ui.tilegroups
|
||||
|
||||
import com.badlogic.gdx.graphics.Color
|
||||
import com.badlogic.gdx.graphics.g2d.Batch
|
||||
import com.badlogic.gdx.math.Vector2
|
||||
import com.badlogic.gdx.scenes.scene2d.Actor
|
||||
import com.badlogic.gdx.scenes.scene2d.Group
|
||||
import com.badlogic.gdx.scenes.scene2d.Touchable
|
||||
import com.badlogic.gdx.scenes.scene2d.ui.Image
|
||||
import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.HexMath
|
||||
import com.unciv.logic.civilization.CivilizationInfo
|
||||
import com.unciv.logic.map.RoadStatus
|
||||
import com.unciv.logic.map.TileInfo
|
||||
import com.unciv.models.*
|
||||
import com.unciv.models.helpers.MapArrowType
|
||||
import com.unciv.models.helpers.MiscArrowTypes
|
||||
import com.unciv.models.helpers.TintedMapArrow
|
||||
import com.unciv.models.helpers.UnitMovementMemoryType
|
||||
import com.unciv.ui.cityscreen.YieldGroup
|
||||
import com.unciv.ui.utils.ImageGetter
|
||||
import com.unciv.ui.utils.center
|
||||
import com.unciv.ui.utils.centerX
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.atan
|
||||
import kotlin.math.atan2
|
||||
import com.unciv.ui.utils.*
|
||||
import kotlin.math.*
|
||||
import kotlin.random.Random
|
||||
|
||||
/** A lot of the render time was spent on snapshot arrays of the TileGroupMap's groups, in the act() function.
|
||||
@ -31,7 +34,7 @@ open class ActionlessGroup(val checkHit:Boolean=false):Group() {
|
||||
}
|
||||
}
|
||||
|
||||
open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings, private val groupSize: Float = 54f) : ActionlessGroup(true) {
|
||||
open class TileGroup(var tileInfo: TileInfo, val tileSetStrings:TileSetStrings, private val groupSize: Float = 54f) : ActionlessGroup(true) {
|
||||
/*
|
||||
Layers:
|
||||
Base image (+ overlay)
|
||||
@ -93,7 +96,9 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
|
||||
)
|
||||
|
||||
private val roadImages = HashMap<TileInfo, RoadImage>()
|
||||
private val borderSegments = HashMap<TileInfo, BorderSegment>() // map of neighboring tile to border segments
|
||||
/** map of neighboring tile to border segments */
|
||||
private val borderSegments = HashMap<TileInfo, BorderSegment>()
|
||||
private val arrows = HashMap<TileInfo, ArrayList<Actor>>()
|
||||
|
||||
@Suppress("LeakingThis") // we trust TileGroupIcons not to use our `this` in its constructor except storing it for later
|
||||
val icons = TileGroupIcons(this)
|
||||
@ -117,6 +122,30 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
|
||||
private val crosshairImage = ImageGetter.getImage("OtherIcons/Crosshair") // for when a unit is targeted
|
||||
private val fogImage = ImageGetter.getImage(tileSetStrings.crosshatchHexagon)
|
||||
|
||||
/**
|
||||
* Class for representing an arrow to add to the map at this tile.
|
||||
*
|
||||
* @property targetTile The tile that arrow should stretch to.
|
||||
* @property arrowType Style of the arrow.
|
||||
* @property tileSetStrings Helper for getting the paths of images in the current tileset.
|
||||
* */
|
||||
private class MapArrow(val targetTile: TileInfo, val arrowType: MapArrowType, val tileSetStrings: TileSetStrings) {
|
||||
/** @return An Image from a named arrow texture. */
|
||||
private fun getArrow(imageName: String): Image {
|
||||
val imagePath = tileSetStrings.getString(tileSetStrings.tileSetLocation, "Arrows/", imageName)
|
||||
return ImageGetter.getImage(imagePath)
|
||||
}
|
||||
/** @return An actor for the arrow, based on the type of the arrow. */
|
||||
fun getImage(): Image = when (arrowType) {
|
||||
is UnitMovementMemoryType -> getArrow(arrowType.name)
|
||||
is MiscArrowTypes -> getArrow(arrowType.name)
|
||||
is TintedMapArrow -> getArrow("Generic").apply { color = arrowType.color }
|
||||
else -> getArrow("Generic")
|
||||
}
|
||||
}
|
||||
|
||||
/** Array list of all arrows to draw from this tile on the next update. */
|
||||
private val arrowsToDraw = ArrayList<MapArrow>()
|
||||
|
||||
var showEntireMap = UncivGame.Current.viewEntireMapForDebug
|
||||
var forMapEditorIcon = false
|
||||
@ -332,6 +361,7 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
|
||||
|
||||
updateRoadImages()
|
||||
updateBorderImages()
|
||||
updateArrows()
|
||||
|
||||
crosshairImage.isVisible = false
|
||||
fogImage.isVisible = !(tileIsViewable || showEntireMap)
|
||||
@ -514,6 +544,48 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
|
||||
}
|
||||
}
|
||||
|
||||
/** Create and setup Actors for all arrows to be drawn from this tile. */
|
||||
private fun updateArrows() {
|
||||
for (actorList in arrows.values) {
|
||||
for (actor in actorList) {
|
||||
actor.remove()
|
||||
}
|
||||
}
|
||||
arrows.clear()
|
||||
|
||||
val tileScale = 50f * 0.8f // See notes in updateRoadImages.
|
||||
|
||||
for (arrowToAdd in arrowsToDraw) {
|
||||
val targetTile = arrowToAdd.targetTile
|
||||
var targetCoord = Vector2(targetTile.position)
|
||||
if (tileInfo.tileMap.mapParameters.worldWrap)
|
||||
targetCoord = HexMath.getUnwrappedNearestTo(targetCoord, tileInfo.position, tileInfo.tileMap.maxLongitude)
|
||||
val targetRelative = HexMath.hex2WorldCoords(targetCoord)
|
||||
.sub(HexMath.hex2WorldCoords(tileInfo.position))
|
||||
|
||||
val targetDistance = sqrt(targetRelative.x.pow(2) + targetRelative.y.pow(2))
|
||||
val targetAngle = atan2(targetRelative.y, targetRelative.x)
|
||||
|
||||
if (targetTile !in arrows) {
|
||||
arrows[targetTile] = ArrayList()
|
||||
}
|
||||
|
||||
val arrowImage = arrowToAdd.getImage()
|
||||
arrowImage.moveBy(25f, -5f) // Move to tile center— Y is +25f too, but subtract half the image height. Based on updateRoadImages.
|
||||
|
||||
arrowImage.setSize(tileScale * targetDistance, 60f)
|
||||
arrowImage.setOrigin(0f, 30f)
|
||||
|
||||
arrowImage.rotation = targetAngle / Math.PI.toFloat() * 180
|
||||
|
||||
arrows[targetTile]!!.add(arrowImage)
|
||||
miscLayerGroup.addActor(arrowImage)
|
||||
// FIXME: Culled when too large and panned away.
|
||||
// https://libgdx.badlogicgames.com/ci/nightlies/docs/api/com/badlogic/gdx/scenes/scene2d/utils/Cullable.html
|
||||
// .getCullingArea returns null for both miscLayerGroup and worldMapHolder. Don't know where it's happening. Somewhat rare, and fixing it may have a hefty performance cost.
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRoadImages() {
|
||||
if (forMapEditorIcon) return
|
||||
for (neighbor in tileInfo.neighbors) {
|
||||
@ -690,6 +762,31 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an arrow to be drawn from this tile.
|
||||
* Similar to [showCircle].
|
||||
*
|
||||
* Zero-length arrows are ignored.
|
||||
*
|
||||
* @param targetTile The tile the arrow should stretch to.
|
||||
* @param type Style of the arrow.
|
||||
* */
|
||||
fun addArrow(targetTile: TileInfo, type: MapArrowType) {
|
||||
if (targetTile.position != tileInfo.position) {
|
||||
arrowsToDraw.add(
|
||||
MapArrow(targetTile, type, tileSetStrings)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all arrows to be drawn from this tile.
|
||||
* Similar to [hideCircle].
|
||||
*/
|
||||
fun resetArrows() {
|
||||
arrowsToDraw.clear()
|
||||
}
|
||||
|
||||
fun showCircle(color: Color, alpha: Float = 0.3f) {
|
||||
circleImage.isVisible = true
|
||||
circleImage.color = color.cpy().apply { a = alpha }
|
||||
|
@ -188,7 +188,7 @@ class MinimapHolder(val mapHolder: WorldMapHolder): Table() {
|
||||
|
||||
}
|
||||
}
|
||||
// So, the "Food" and "Population" stat icons have green as part of their image, but the "Cattle" icon needs a ackground colour, which is… An interesting mixture/reuse of texture data and render-time processing.
|
||||
// So, the "Food" and "Population" stat icons have green as part of their image, but the "Cattle" icon needs a background colour, which is… An interesting mixture/reuse of texture data and render-time processing.
|
||||
innerActor.surroundWithCircle(40f).apply {
|
||||
circle.color = Color.BLACK
|
||||
}
|
||||
@ -211,6 +211,13 @@ class MinimapHolder(val mapHolder: WorldMapHolder): Table() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Button, next to the minimap, to toggle the unit movement map overlay. */
|
||||
val movementsImageButton = MapOverlayToggleButton(
|
||||
ImageGetter.getImage("StatIcons/Movement").apply { setColor(0f, 0f, 0f, 1f) },
|
||||
getter = { UncivGame.Current.settings.showUnitMovements },
|
||||
setter = { UncivGame.Current.settings.showUnitMovements = it },
|
||||
backgroundColor = Color.GREEN
|
||||
)
|
||||
/** Button, next to the minimap, to toggle the tile yield map overlay. */
|
||||
val yieldImageButton = MapOverlayToggleButton(
|
||||
ImageGetter.getImage("StatIcons/Food"),
|
||||
@ -266,6 +273,7 @@ class MinimapHolder(val mapHolder: WorldMapHolder): Table() {
|
||||
private fun getToggleIcons(): Table {
|
||||
val toggleIconTable = Table()
|
||||
|
||||
toggleIconTable.add(movementsImageButton.actor).row()
|
||||
toggleIconTable.add(yieldImageButton.actor).row()
|
||||
toggleIconTable.add(populationImageButton.actor).row()
|
||||
toggleIconTable.add(resourceImageButton.actor).row()
|
||||
@ -278,6 +286,7 @@ class MinimapHolder(val mapHolder: WorldMapHolder): Table() {
|
||||
isVisible = UncivGame.Current.settings.showMinimap
|
||||
if (isVisible) {
|
||||
minimap.update(civInfo)
|
||||
movementsImageButton.update()
|
||||
yieldImageButton.update()
|
||||
populationImageButton.update()
|
||||
resourceImageButton.update()
|
||||
|
@ -21,15 +21,16 @@ import com.unciv.logic.battle.Battle
|
||||
import com.unciv.logic.battle.MapUnitCombatant
|
||||
import com.unciv.logic.city.CityInfo
|
||||
import com.unciv.logic.civilization.CivilizationInfo
|
||||
import com.unciv.logic.civilization.PlayerType
|
||||
import com.unciv.logic.map.*
|
||||
import com.unciv.models.AttackableTile
|
||||
import com.unciv.models.UncivSound
|
||||
import com.unciv.models.*
|
||||
import com.unciv.models.helpers.MapArrowType
|
||||
import com.unciv.models.helpers.MiscArrowTypes
|
||||
import com.unciv.ui.map.TileGroupMap
|
||||
import com.unciv.ui.tilegroups.TileGroup
|
||||
import com.unciv.ui.tilegroups.TileSetStrings
|
||||
import com.unciv.ui.tilegroups.WorldTileGroup
|
||||
import com.unciv.ui.utils.*
|
||||
//import com.unciv.ui.worldscreen.unit.UnitMovementsOverlayGroup
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
|
||||
@ -432,6 +433,58 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
|
||||
&& viewingCiv.isCurrentPlayer()
|
||||
&& viewingCiv.isDefeated()
|
||||
|
||||
/** Clear all arrows to be drawn on the next update. */
|
||||
fun resetArrows() {
|
||||
for (tile in tileGroups.values) {
|
||||
for (group in tile) {
|
||||
group.resetArrows()
|
||||
}
|
||||
} // Inefficient?
|
||||
}
|
||||
|
||||
/** Add an arrow to draw on the next update. */
|
||||
fun addArrow(fromTile: TileInfo, toTile: TileInfo, arrowType: MapArrowType) {
|
||||
val tile = tileGroups[fromTile]
|
||||
if (tile != null) for (group in tile) {
|
||||
group.addArrow(toTile, arrowType)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add arrows to show all past and planned movements and attacks, if the options setting to do so is enabled.
|
||||
*
|
||||
* @param pastVisibleUnits Sequence of [MapUnit]s for which the last turn's movement history can be displayed.
|
||||
* @param targetVisibleUnits Sequence of [MapUnit]s for which the active movement target can be displayed.
|
||||
* @param visibleAttacks Sequence of pairs of MapUnits to the target coordinates of attacks that they have done and can be displayed.
|
||||
* */
|
||||
internal fun updateMovementOverlay(pastVisibleUnits: Sequence<MapUnit>, targetVisibleUnits: Sequence<MapUnit>, visibleAttacks: Sequence<Pair<MapUnit, Vector2>>) {
|
||||
if (!UncivGame.Current.settings.showUnitMovements) {
|
||||
return
|
||||
}
|
||||
for (unit in pastVisibleUnits) {
|
||||
if (unit.movementMemories.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
val stepIter = unit.movementMemories.iterator()
|
||||
var previous = stepIter.next()
|
||||
while (stepIter.hasNext()) {
|
||||
val next = stepIter.next()
|
||||
addArrow(tileMap[previous.position], tileMap[next.position], next.type)
|
||||
previous = next
|
||||
}
|
||||
addArrow(tileMap[previous.position], unit.getTile(), unit.mostRecentMoveType)
|
||||
}
|
||||
for (unit in targetVisibleUnits) {
|
||||
if (!unit.isMoving())
|
||||
continue
|
||||
val toTile = unit.getMovementDestination()
|
||||
addArrow(unit.getTile(), toTile, MiscArrowTypes.UnitMoving)
|
||||
}
|
||||
for ((from, to) in visibleAttacks) {
|
||||
addArrow(tileMap[from.getTile().position], tileMap[to], MiscArrowTypes.UnitHasAttacked)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updateTiles(viewingCiv: CivilizationInfo) {
|
||||
|
||||
if (isMapRevealEnabled(viewingCiv)) {
|
||||
|
@ -19,6 +19,8 @@ import com.unciv.logic.GameSaver
|
||||
import com.unciv.logic.civilization.CivilizationInfo
|
||||
import com.unciv.logic.civilization.ReligionState
|
||||
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
|
||||
import com.unciv.logic.map.MapUnit
|
||||
import com.unciv.logic.map.MapVisualization
|
||||
import com.unciv.logic.trade.TradeEvaluation
|
||||
import com.unciv.models.Tutorial
|
||||
import com.unciv.models.UncivSound
|
||||
@ -63,6 +65,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
||||
val canChangeState
|
||||
get() = isPlayersTurn && !viewingCiv.isSpectator()
|
||||
private var waitingForAutosave = false
|
||||
val mapVisualization = MapVisualization(gameInfo, viewingCiv)
|
||||
|
||||
val mapHolder = WorldMapHolder(this, gameInfo.tileMap)
|
||||
private val minimapWrapper = MinimapHolder(mapHolder)
|
||||
@ -400,6 +403,15 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
|
||||
unitActionsTable.update(bottomUnitTable.selectedUnit)
|
||||
unitActionsTable.y = bottomUnitTable.height
|
||||
|
||||
mapHolder.resetArrows()
|
||||
val allUnits = gameInfo.civilizations.asSequence().flatMap { it.getCivUnits() }
|
||||
val allAttacks = allUnits.map { unit -> unit.attacksSinceTurnStart.asSequence().map { attacked -> unit to attacked } }.flatten()
|
||||
mapHolder.updateMovementOverlay(
|
||||
allUnits.filter(mapVisualization::isUnitPastVisible),
|
||||
allUnits.filter(mapVisualization::isUnitFutureVisible),
|
||||
allAttacks.filter { (attacker, target) -> mapVisualization.isAttackVisible(attacker, target) }
|
||||
)
|
||||
|
||||
// if we use the clone, then when we update viewable tiles
|
||||
// it doesn't update the explored tiles of the civ... need to think about that harder
|
||||
// it causes a bug when we move a unit to an unexplored tile (for instance a cavalry unit which can move far)
|
||||
|
@ -157,9 +157,10 @@ class OptionsPopup(val previousScreen: BaseScreen) : Popup(previousScreen) {
|
||||
pad(10f)
|
||||
defaults().pad(2.5f)
|
||||
|
||||
addYesNoRow("Show unit movement arrows", settings.showUnitMovements, true) { settings.showUnitMovements = it }
|
||||
addYesNoRow("Show tile yields", settings.showTileYields, true) { settings.showTileYields = it } // JN
|
||||
addYesNoRow("Show worked tiles", settings.showWorkedTiles, true) { settings.showWorkedTiles = it }
|
||||
addYesNoRow("Show resources and improvements", settings.showResourcesAndImprovements, true) { settings.showResourcesAndImprovements = it }
|
||||
addYesNoRow("Show tile yields", settings.showTileYields, true) { settings.showTileYields = it } // JN
|
||||
addYesNoRow("Show tutorials", settings.showTutorials, true) { settings.showTutorials = it }
|
||||
addMinimapSizeSlider()
|
||||
|
||||
|
Reference in New Issue
Block a user