mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-08 14:57:58 +07:00
Connect roads automation (#10631)
* Start on road connect feature. * Rough UI and tile highlighting - Highlight visible tiles for selected unit red -- Maybe change this to all explored tiles - Move action firing inside WorldMapHolder - Set begin and end tiles * Serialize Vector2 instead of Tile * Add road icon * Much better UI handling - Tile highlights go away after choosing a tile - Added restrictions to allowed tile destination choices. - Explored - Land - Passable - Added two-tap button * Refactor part of `onTileClicked` for readability * Band-aid fix null pointer error * Add RoadConnection icon * Tentatively working connect road feature * AStar search implementation * AStar connect road automation * Fix worker getting stuck in city tiles * Heuristic should be between tiles * Add heuristic to road connect, remove maxSize limit * Fix predicates * Cancel automation when worker is force moved off path * Change valid/highlighted tiles to be friendly or neutral * Put log back the way it was * Fix behavior when kicked off path * Worker no longer wastes movement points * Workers will progress multiple tiles at a time towards the next build destination. * Respect civs with certain tiles as roads * Refractor ForceAutomateRoadConnection -> AutomateRoadConnection * Connect road UI button only shows for units with UniqueType.BuildImprovements * Connect road UI button only show when road tech is unlocked * Add wagon sound * Fix destination icon, add KeyboardBinding to 'c' * UI highlight connect road path tiles orange * Downsample wagon.mp3 * Apply migration patch, idiomatic sequence processing * Add notifications on success and failure * Extract movement cost function to be reusable * Refactor road pathfinding into MapPathing.kt * Make pathing calls more general for future extendability * Add UI road connection tile path preview * Keep road path highlighting when routing to a city tile * Adjust road pathing cost function * Path includes pillaged roads * Repair pillaged roads along path * Valid road path tiles now include all passable tiles (open borders)
This commit is contained in:
@ -34,6 +34,9 @@ object CivilianUnitAutomation {
|
||||
if (unit.hasUnique(UniqueType.FoundCity))
|
||||
return SpecificUnitAutomation.automateSettlerActions(unit, tilesWhereWeWillBeCaptured)
|
||||
|
||||
if(unit.isAutomatingRoadConnection())
|
||||
return unit.civ.getWorkerAutomation().automateConnectRoad(unit, tilesWhereWeWillBeCaptured)
|
||||
|
||||
if (unit.cache.hasUniqueToBuildImprovements)
|
||||
return unit.civ.getWorkerAutomation().automateWorkerAction(unit, tilesWhereWeWillBeCaptured)
|
||||
|
||||
|
@ -10,8 +10,10 @@ import com.unciv.logic.automation.unit.UnitAutomation.wander
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.NotificationCategory
|
||||
import com.unciv.logic.civilization.NotificationIcon
|
||||
import com.unciv.logic.map.BFS
|
||||
import com.unciv.logic.map.HexMath
|
||||
import com.unciv.logic.map.MapPathing
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.logic.map.tile.RoadStatus
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
@ -55,6 +57,9 @@ class WorkerAutomation(
|
||||
RoadStatus.None
|
||||
else civInfo.tech.getBestRoadAvailable()
|
||||
|
||||
/** Same as above, but ignores the option */
|
||||
private val actualBestRoadAvailable: RoadStatus = civInfo.tech.getBestRoadAvailable()
|
||||
|
||||
/** Civ-wide list of unconnected Cities, sorted by closest to capital first */
|
||||
private val citiesThatNeedConnecting: List<City> by lazy {
|
||||
val result = civInfo.cities.asSequence()
|
||||
@ -115,6 +120,132 @@ class WorkerAutomation(
|
||||
|
||||
|
||||
///////////////////////////////////////// Methods /////////////////////////////////////////
|
||||
|
||||
|
||||
/**
|
||||
* Automate the process of connecting a road between two points.
|
||||
* Current thoughts:
|
||||
* Will be a special case of MapUnit.automated property
|
||||
* Unit has new attributes startTile endTile
|
||||
* - We will progress towards the end path sequentially, taking absolute least distance w/o regard for movement cost
|
||||
* - Cancel upon risk of capture
|
||||
* - Cancel upon blocked
|
||||
* - End automation upon finish
|
||||
*/
|
||||
// TODO: Caching
|
||||
// TODO: Hide the automate road button if road is not unlocked
|
||||
fun automateConnectRoad(unit: MapUnit, tilesWhereWeWillBeCaptured: Set<Tile>){
|
||||
if (actualBestRoadAvailable == RoadStatus.None) return
|
||||
|
||||
var currentTile = unit.getTile()
|
||||
|
||||
/** Reset side effects from automation, return worker to non-automated state*/
|
||||
fun stopAndCleanAutomation(){
|
||||
unit.automated = false
|
||||
unit.action = null
|
||||
unit.automatedRoadConnectionDestination = null
|
||||
unit.automatedRoadConnectionPath = null
|
||||
currentTile.stopWorkingOnImprovement()
|
||||
}
|
||||
|
||||
/** Conditions for whether it is acceptable to build a road on this tile */
|
||||
fun shouldBuildRoadOnTile(tile: Tile): Boolean {
|
||||
return !tile.isCityCenter() // Can't build road on city tiles
|
||||
// Special case for civs that treat forest/jungles as roads (inside their territory). We shouldn't build if railroads aren't unlocked.
|
||||
&& !(tile.hasConnection(unit.civ) && actualBestRoadAvailable == RoadStatus.Road)
|
||||
// Build (upgrade) if possible
|
||||
&& tile.roadStatus != actualBestRoadAvailable
|
||||
// Build if the road is pillaged
|
||||
|| tile.roadIsPillaged
|
||||
}
|
||||
|
||||
val destinationTile = unit.civ.gameInfo.tileMap[unit.automatedRoadConnectionDestination!!]
|
||||
|
||||
var pathToDest: List<Vector2>? = unit.automatedRoadConnectionPath
|
||||
|
||||
// The path does not exist, create it
|
||||
if (pathToDest == null) {
|
||||
val foundPath: List<Tile>? = MapPathing.getRoadPath(unit, currentTile, destinationTile)
|
||||
if (foundPath == null) {
|
||||
Log.debug("WorkerAutomation: ${unit.label()} -> connect road failed")
|
||||
stopAndCleanAutomation()
|
||||
unit.civ.addNotification("Connect road failed!", currentTile.position, NotificationCategory.Units, NotificationIcon.Construction)
|
||||
return
|
||||
} else {
|
||||
pathToDest = foundPath // Convert to a list of positions for serialization
|
||||
.map { it.position }
|
||||
|
||||
unit.automatedRoadConnectionPath = pathToDest
|
||||
debug("WorkerAutomation: ${unit.label()} -> found connect road path to destination tile: %s, %s", destinationTile, pathToDest)
|
||||
}
|
||||
}
|
||||
|
||||
val currTileIndex = pathToDest.indexOf(currentTile.position)
|
||||
|
||||
// The worker was somehow moved off its path, cancel the action
|
||||
if (currTileIndex == -1) {
|
||||
Log.debug("${unit.label()} -> was moved off its connect road path. Operation cancelled.")
|
||||
stopAndCleanAutomation()
|
||||
unit.civ.addNotification("Connect road cancelled!", currentTile.position, NotificationCategory.Units, unit.name)
|
||||
return
|
||||
}
|
||||
|
||||
if (unit.currentMovement > 0) {
|
||||
/* Can not build a road on this tile, try to move on.
|
||||
* The worker should search for the next furthest tile in the path that:
|
||||
* - It can move to
|
||||
* - Can be improved/upgraded
|
||||
* */
|
||||
if (!shouldBuildRoadOnTile(currentTile)) {
|
||||
when {
|
||||
currTileIndex < pathToDest.size - 1 -> { // Try to move to the next tile in the path
|
||||
val tileMap = unit.civ.gameInfo.tileMap
|
||||
var nextTile: Tile = currentTile
|
||||
|
||||
// Create a new list with tiles where the index is greater than currTileIndex
|
||||
val futureTiles = pathToDest.asSequence()
|
||||
.dropWhile { it != unit.currentTile.position }
|
||||
.drop(1)
|
||||
.map { tileMap[it] }
|
||||
|
||||
for (futureTile in futureTiles){ // Find the furthest tile we can reach in this turn, move to, and does not have a road
|
||||
if (unit.movement.canReachInCurrentTurn(futureTile) && unit.movement.canMoveTo(futureTile)) { // We can at least move to this tile
|
||||
nextTile = futureTile
|
||||
if (shouldBuildRoadOnTile(futureTile)) {
|
||||
break // Stop on this tile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unit.movement.moveToTile(nextTile)
|
||||
currentTile = unit.getTile()
|
||||
}
|
||||
currTileIndex == pathToDest.size - 1 -> { // The last tile in the path is unbuildable or has a road.
|
||||
stopAndCleanAutomation()
|
||||
unit.civ.addNotification("Connect road completed!", currentTile.position, NotificationCategory.Units, unit.name)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to check current movement again after we've (potentially) moved
|
||||
if (unit.currentMovement > 0) {
|
||||
// Repair pillaged roads first
|
||||
if(currentTile.roadStatus != RoadStatus.None && currentTile.roadIsPillaged){
|
||||
currentTile.setRepaired()
|
||||
return
|
||||
}
|
||||
if (shouldBuildRoadOnTile(currentTile) && currentTile.improvementInProgress != actualBestRoadAvailable.name) {
|
||||
val improvement = actualBestRoadAvailable.improvement(ruleSet)!!
|
||||
currentTile.startWorkingOnImprovement(improvement, civInfo, unit)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Automate one Worker - decide what to do and where, move, start or continue work.
|
||||
*/
|
||||
|
185
core/src/com/unciv/logic/map/AStar.kt
Normal file
185
core/src/com/unciv/logic/map/AStar.kt
Normal file
@ -0,0 +1,185 @@
|
||||
package com.unciv.logic.map
|
||||
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import java.util.PriorityQueue
|
||||
|
||||
|
||||
data class TilePriority(val tile: Tile, val priority: Float)
|
||||
|
||||
/**
|
||||
* AStar is an implementation of the A* search algorithm, commonly used for finding the shortest path
|
||||
* in a weighted graph.
|
||||
*
|
||||
* The algorithm maintains a priority queue of paths while exploring the graph, expanding paths in
|
||||
* order of their estimated total cost from the start node to the goal node, factoring in both the
|
||||
* cost so far and an estimated cost (heuristic) to the goal.
|
||||
*
|
||||
* @param startingPoint The initial tile where the search begins.
|
||||
* @param predicate A function that determines if a tile should be considered for further exploration.
|
||||
* For instance, it might return `true` for passable tiles and `false` for obstacles.
|
||||
* @param cost A function that takes two tiles (fromTile, toTile) as input and returns the cost
|
||||
* of moving from 'fromTile' to 'toTile' as a Float. This allows for flexible cost
|
||||
* calculations based on different criteria, such as distance, terrain, or other
|
||||
* custom logic defined by the user.
|
||||
* @param heuristic A function that estimates the cost from a given tile to the goal. For the A*
|
||||
* algorithm to guarantee the shortest path, this heuristic must be admissible,
|
||||
* meaning it should never overestimate the actual cost to reach the goal.
|
||||
* You can set this to `{ tile -> 0 }` for Djikstra's algorithm.
|
||||
*
|
||||
* Usage Example:
|
||||
* ```
|
||||
* val unit: MapUnit = ...
|
||||
* val aStarSearch = AStar(startTile,
|
||||
* { tile -> tile.isPassable },
|
||||
* { from: Tile, to: Tile -> MovementCost.getMovementCostBetweenAdjacentTiles(unit, from, to)},
|
||||
* { tile -> <custom heuristic> })
|
||||
*
|
||||
* val path = aStarSearch.findPath(goalTile)
|
||||
* ```
|
||||
*/
|
||||
class AStar(
|
||||
val startingPoint: Tile,
|
||||
private val predicate : (Tile) -> Boolean,
|
||||
private val cost: (Tile, Tile) -> Float,
|
||||
private val heuristic : (Tile, Tile) -> Float,
|
||||
) {
|
||||
/** Maximum number of tiles to search */
|
||||
var maxSize = Int.MAX_VALUE
|
||||
|
||||
/** Cache for storing the costs */
|
||||
private val costCache = mutableMapOf<Pair<Tile,Tile>, Float>()
|
||||
|
||||
/**
|
||||
* Retrieves the cost of moving to a given tile, utilizing a cache to improve efficiency.
|
||||
* If the cost for a tile is not already cached, it computes the cost using the provided cost function and stores it in the cache.
|
||||
*
|
||||
* @param from The source tile.
|
||||
* @param to The destination tile.
|
||||
* @return The cost of moving between the tiles.
|
||||
*/
|
||||
private fun getCost(from: Tile, to: Tile): Float {
|
||||
return costCache.getOrPut(Pair(from, to)) { cost(from, to) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator for the priority queue used in the A* algorithm.
|
||||
* It compares two `TilePriority` objects based on their priority value,
|
||||
* ensuring that tiles with lower estimated total costs are given precedence in the queue.
|
||||
*/
|
||||
private val tilePriorityComparator = Comparator<TilePriority> { tp1, tp2 ->
|
||||
tp1.priority.compareTo(tp2.priority)
|
||||
}
|
||||
|
||||
/**
|
||||
* Frontier priority queue for managing the tiles to be checked.
|
||||
* Tiles are ordered based on their priority, determined by the cumulative cost so far and the heuristic estimate to the goal.
|
||||
*/
|
||||
private val tilesToCheck = PriorityQueue(27, tilePriorityComparator)
|
||||
|
||||
/**
|
||||
* A map where each tile reached during the search points to its parent tile.
|
||||
* This map is used to reconstruct the path once the destination is reached.
|
||||
*/
|
||||
private val tilesReached = HashMap<Tile, Tile>()
|
||||
|
||||
/**
|
||||
* A map holding the cumulative cost to reach each tile.
|
||||
* This is used to calculate the most efficient path to a tile during the search process.
|
||||
*/
|
||||
private val cumulativeTileCost = HashMap<Tile, Float>()
|
||||
|
||||
init {
|
||||
tilesToCheck.add(TilePriority(startingPoint, 0f))
|
||||
tilesReached[startingPoint] = startingPoint
|
||||
cumulativeTileCost[startingPoint] = 0f
|
||||
}
|
||||
|
||||
/**
|
||||
* Continues the search process until there are no more tiles left to check.
|
||||
*/
|
||||
fun stepToEnd() {
|
||||
while (!hasEnded())
|
||||
nextStep()
|
||||
}
|
||||
|
||||
/**
|
||||
* Continues the search process until either the specified destination is reached or there are no more tiles left to check.
|
||||
*
|
||||
* @param destination The destination tile to reach.
|
||||
* @return This AStar instance, allowing for method chaining.
|
||||
*/
|
||||
fun stepUntilDestination(destination: Tile): AStar {
|
||||
while (!tilesReached.containsKey(destination) && !hasEnded())
|
||||
nextStep()
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes one step in the A* algorithm, expanding the search from the current tile to its neighbors.
|
||||
* It updates the search structures accordingly, considering both the cost so far and the heuristic estimate.
|
||||
*
|
||||
* If the maximum size is reached or no more tiles are available, this method will do nothing.
|
||||
*/
|
||||
fun nextStep() {
|
||||
if (tilesReached.size >= maxSize) { tilesToCheck.clear(); return }
|
||||
val currentTile = tilesToCheck.poll()?.tile ?: return
|
||||
for (neighbor in currentTile.neighbors) {
|
||||
val newCost: Float = cumulativeTileCost[currentTile]!! + getCost(currentTile, neighbor)
|
||||
if (predicate(neighbor) &&
|
||||
(!cumulativeTileCost.containsKey(neighbor)
|
||||
|| newCost < (cumulativeTileCost[neighbor] ?: Float.MAX_VALUE))
|
||||
){
|
||||
cumulativeTileCost[neighbor] = newCost
|
||||
val priority: Float = newCost + heuristic(currentTile, neighbor)
|
||||
tilesToCheck.add(TilePriority(neighbor, priority))
|
||||
tilesReached[neighbor] = currentTile
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a sequence representing the path from the given destination tile back to the starting point.
|
||||
* If the destination has not been reached, the sequence will be empty.
|
||||
*
|
||||
* @param destination The destination tile to trace the path to.
|
||||
* @return A sequence of tiles representing the path from the destination to the starting point.
|
||||
*/
|
||||
fun getPathTo(destination: Tile): Sequence<Tile> = sequence {
|
||||
var currentNode = destination
|
||||
while (true) {
|
||||
val parent = tilesReached[currentNode] ?: break // destination is not in our path
|
||||
yield(currentNode)
|
||||
if (currentNode == startingPoint) break
|
||||
currentNode = parent
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there are no more tiles to be checked in the search.
|
||||
*
|
||||
* @return True if the search has ended, otherwise false.
|
||||
*/
|
||||
fun hasEnded() = tilesToCheck.isEmpty()
|
||||
|
||||
/**
|
||||
* Determines if a specific tile has been reached during the search.
|
||||
*
|
||||
* @param tile The tile to check.
|
||||
* @return True if the tile has been reached, otherwise false.
|
||||
*/
|
||||
fun hasReachedTile(tile: Tile) = tilesReached.containsKey(tile)
|
||||
|
||||
/**
|
||||
* Retrieves all tiles that have been reached so far in the search.
|
||||
*
|
||||
* @return A set of tiles that have been reached.
|
||||
*/
|
||||
fun getReachedTiles(): MutableSet<Tile> = tilesReached.keys
|
||||
|
||||
/**
|
||||
* Provides the number of tiles that have been reached so far in the search.
|
||||
*
|
||||
* @return The count of tiles reached.
|
||||
*/
|
||||
fun size() = tilesReached.size
|
||||
}
|
107
core/src/com/unciv/logic/map/MapPathing.kt
Normal file
107
core/src/com/unciv/logic/map/MapPathing.kt
Normal file
@ -0,0 +1,107 @@
|
||||
package com.unciv.logic.map
|
||||
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.logic.map.tile.RoadStatus
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import com.unciv.utils.Log
|
||||
|
||||
//TODO: Eventually, all path generation in the game should be moved into here.
|
||||
object MapPathing {
|
||||
|
||||
/**
|
||||
* We prefer the worker to prioritize paths connected by existing roads. If a tile has a road, but the civ has the ability
|
||||
* to upgrade it to a railroad, we consider it to be a railroad for pathing since it will be upgraded.
|
||||
* Otherwise, we set every tile to have equal value since building a road on any of them makes the original movement cost irrelevant.
|
||||
*/
|
||||
private fun roadPreferredMovementCost(unit: MapUnit, from: Tile, to: Tile): Float{
|
||||
// hasRoadConnection accounts for civs that treat jungle/forest as roads
|
||||
// Ignore road over river penalties.
|
||||
val areConnectedByRoad = from.hasRoadConnection(unit.civ, mustBeUnpillaged = false) && to.hasRoadConnection(unit.civ, mustBeUnpillaged = false)
|
||||
if (areConnectedByRoad){
|
||||
// If the civ has railroad technology, consider roads as railroads since they will be upgraded
|
||||
if (unit.civ.tech.getBestRoadAvailable() == RoadStatus.Railroad){
|
||||
return RoadStatus.Railroad.movement
|
||||
}else{
|
||||
return unit.civ.tech.movementSpeedOnRoads
|
||||
}
|
||||
}
|
||||
|
||||
val areConnectedByRailroad = from.hasRailroadConnection(mustBeUnpillaged = false) && to.hasRailroadConnection(mustBeUnpillaged = false)
|
||||
if (areConnectedByRailroad)
|
||||
return RoadStatus.Railroad.movement
|
||||
|
||||
return 1f
|
||||
}
|
||||
|
||||
fun isValidRoadPathTile(unit: MapUnit, tile: Tile): Boolean {
|
||||
return tile.isLand
|
||||
&& !tile.isImpassible()
|
||||
&& unit.civ.hasExplored(tile)
|
||||
&& tile.canCivPassThrough(unit.civ)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the path for a road construction between two tiles.
|
||||
*
|
||||
* This function uses the A* search algorithm to find an optimal path for road construction between two specified tiles.
|
||||
*
|
||||
* @param unit The unit that will construct the road.
|
||||
* @param startTile The starting tile of the path.
|
||||
* @param endTile The destination tile of the path.
|
||||
* @return A sequence of tiles representing the path from startTile to endTile, or null if no valid path is found.
|
||||
*/
|
||||
fun getRoadPath(unit: MapUnit, startTile: Tile, endTile: Tile): List<Tile>?{
|
||||
return getPath(unit,
|
||||
startTile,
|
||||
endTile,
|
||||
::isValidRoadPathTile,
|
||||
::roadPreferredMovementCost,
|
||||
{_, _, _ -> 0f}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the path between two tiles.
|
||||
*
|
||||
* This function uses the A* search algorithm to find an optimal path two specified tiles on a game map.
|
||||
*
|
||||
* @param unit The unit for which the path is being calculated.
|
||||
* @param startTile The tile from which the pathfinding begins.
|
||||
* @param endTile The destination tile for the pathfinding.
|
||||
* @param predicate A function that takes a MapUnit and a Tile, returning a Boolean. This function is used to determine whether a tile can be traversed by the unit.
|
||||
* @param cost A function that calculates the cost of moving from one tile to another.
|
||||
* It takes a MapUnit, a 'from' Tile, and a 'to' Tile, returning a Float value representing the cost.
|
||||
* @param heuristic A function that estimates the cost from a given tile to the end tile.
|
||||
* It takes a MapUnit, a 'from' Tile, and a 'to' Tile, returning a Float value representing the heuristic cost estimate.
|
||||
* @return A list of tiles representing the path from the startTile to the endTile. Returns null if no valid path is found.
|
||||
*/
|
||||
private fun getPath(unit: MapUnit,
|
||||
startTile: Tile,
|
||||
endTile: Tile,
|
||||
predicate: (MapUnit, Tile) -> Boolean,
|
||||
cost: (MapUnit, Tile, Tile) -> Float,
|
||||
heuristic: (MapUnit, Tile, Tile) -> Float): List<Tile>? {
|
||||
val astar = AStar(startTile,
|
||||
{ tile -> predicate(unit, tile) },
|
||||
{ from, to -> cost(unit, from, to)},
|
||||
{ from, to -> heuristic(unit, from, to) })
|
||||
while (true) {
|
||||
if (astar.hasEnded()) {
|
||||
// We failed to find a path
|
||||
Log.debug("getRoadPath failed at AStar search size ${astar.size()}")
|
||||
return null
|
||||
}
|
||||
if (!astar.hasReachedTile(endTile)) {
|
||||
astar.nextStep()
|
||||
continue
|
||||
}
|
||||
// Found a path.
|
||||
return astar.getPathTo(endTile)
|
||||
.toList()
|
||||
.reversed()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -103,9 +103,14 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
var currentMovement: Float = 0f
|
||||
var health: Int = 100
|
||||
|
||||
var action: String? = null // work, automation, fortifying, I dunno what.
|
||||
// work, automation, fortifying, ...
|
||||
// Connect roads implies automated is true. It is specified by the action type.
|
||||
var action: String? = null
|
||||
var automated: Boolean = false
|
||||
|
||||
var automatedRoadConnectionDestination: Vector2? = null
|
||||
var automatedRoadConnectionPath: List<Vector2>? = null
|
||||
|
||||
@Transient
|
||||
var showAdditionalActions: Boolean = false
|
||||
|
||||
@ -180,6 +185,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
toReturn.health = health
|
||||
toReturn.action = action
|
||||
toReturn.automated = automated
|
||||
toReturn.automatedRoadConnectionDestination = automatedRoadConnectionDestination
|
||||
toReturn.automatedRoadConnectionPath = automatedRoadConnectionPath
|
||||
toReturn.attacksThisTurn = attacksThisTurn
|
||||
toReturn.turnsFortified = turnsFortified
|
||||
toReturn.promotions = promotions.clone()
|
||||
@ -381,6 +388,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
fun isMoving() = action?.startsWith("moveTo") == true
|
||||
|
||||
fun isAutomated() = automated
|
||||
|
||||
fun isAutomatingRoadConnection() = action == UnitActionType.ConnectRoad.value
|
||||
fun isExploring() = action == UnitActionType.Explore.value
|
||||
fun isPreparingParadrop() = action == UnitActionType.Paradrop.value
|
||||
fun isPreparingAirSweep() = action == UnitActionType.AirSweep.value
|
||||
|
@ -246,7 +246,7 @@ class UnitMovement(val unit: MapUnit) {
|
||||
return getShortestPath(destination).any()
|
||||
}
|
||||
|
||||
private fun canReachInCurrentTurn(destination: Tile): Boolean {
|
||||
fun canReachInCurrentTurn(destination: Tile): Boolean {
|
||||
if (unit.cache.cannotMove) return destination == unit.getTile()
|
||||
if (unit.baseUnit.movesLikeAirUnits())
|
||||
return unit.currentTile.aerialDistanceTo(destination) <= unit.getMaxMovementForAirUnits()
|
||||
|
@ -671,7 +671,19 @@ open class Tile : IsPartOfGameInfoSerialization {
|
||||
}
|
||||
|
||||
fun hasConnection(civInfo: Civilization) =
|
||||
getUnpillagedRoad() != RoadStatus.None || forestOrJungleAreRoads(civInfo)
|
||||
getUnpillagedRoad() != RoadStatus.None || forestOrJungleAreRoads(civInfo)
|
||||
|
||||
fun hasRoadConnection(civInfo: Civilization, mustBeUnpillaged: Boolean) =
|
||||
if (mustBeUnpillaged)
|
||||
(getUnpillagedRoad() == RoadStatus.Road) || forestOrJungleAreRoads(civInfo)
|
||||
else
|
||||
roadStatus == RoadStatus.Road || forestOrJungleAreRoads(civInfo)
|
||||
|
||||
fun hasRailroadConnection(mustBeUnpillaged: Boolean) =
|
||||
if (mustBeUnpillaged)
|
||||
getUnpillagedRoad() == RoadStatus.Railroad
|
||||
else
|
||||
roadStatus == RoadStatus.Railroad
|
||||
|
||||
|
||||
private fun forestOrJungleAreRoads(civInfo: Civilization) =
|
||||
|
@ -99,6 +99,8 @@ enum class UnitActionType(
|
||||
{ ImageGetter.getUnitActionPortrait("Swap") }, false),
|
||||
Automate("Automate",
|
||||
{ ImageGetter.getUnitActionPortrait("Automate") }),
|
||||
ConnectRoad("Connect road",
|
||||
{ ImageGetter.getUnitActionPortrait("RoadConnection") }),
|
||||
StopAutomation("Stop automation",
|
||||
{ ImageGetter.getUnitActionPortrait("Stop") }, false),
|
||||
StopMovement("Stop movement",
|
||||
|
@ -88,6 +88,7 @@ enum class KeyboardBinding(
|
||||
// here as it will not be guaranteed to already be fully initialized.
|
||||
SwapUnits(Category.UnitActions,"Swap units", 'y'),
|
||||
Automate(Category.UnitActions, 'm'),
|
||||
ConnectRoad(Category.UnitActions, "Connect road", 'c'),
|
||||
StopAutomation(Category.UnitActions,"Stop automation", 'm'),
|
||||
StopMovement(Category.UnitActions,"Stop movement", '.'),
|
||||
ShowUnitDestination(Category.UnitActions, "Show unit destination", 'j'),
|
||||
|
@ -15,17 +15,20 @@ import com.badlogic.gdx.utils.Align
|
||||
import com.unciv.Constants
|
||||
import com.unciv.UncivGame
|
||||
import com.unciv.logic.automation.unit.CityLocationTileRanker
|
||||
import com.unciv.logic.automation.unit.UnitAutomation
|
||||
import com.unciv.logic.battle.AttackableTile
|
||||
import com.unciv.logic.battle.Battle
|
||||
import com.unciv.logic.battle.MapUnitCombatant
|
||||
import com.unciv.logic.battle.TargetHelper
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.map.MapPathing
|
||||
import com.unciv.logic.map.TileMap
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.logic.map.mapunit.movement.UnitMovement
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import com.unciv.models.UncivSound
|
||||
import com.unciv.models.UnitActionType
|
||||
import com.unciv.models.ruleset.unique.LocalUniqueCache
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.ui.audio.SoundPlayer
|
||||
@ -39,6 +42,7 @@ import com.unciv.ui.components.extensions.surroundWithCircle
|
||||
import com.unciv.ui.components.extensions.toLabel
|
||||
import com.unciv.ui.components.input.ActivationTypes
|
||||
import com.unciv.ui.components.input.KeyCharAndCode
|
||||
import com.unciv.ui.components.input.KeyboardBinding
|
||||
import com.unciv.ui.components.input.keyShortcuts
|
||||
import com.unciv.ui.components.input.onActivation
|
||||
import com.unciv.ui.components.input.onClick
|
||||
@ -70,6 +74,8 @@ class WorldMapHolder(
|
||||
|
||||
private val unitMovementPaths: HashMap<MapUnit, ArrayList<Tile>> = HashMap()
|
||||
|
||||
private val unitConnectRoadPaths: HashMap<MapUnit, List<Tile>> = HashMap()
|
||||
|
||||
private lateinit var tileGroupMap: TileGroupMap<WorldTileGroup>
|
||||
|
||||
lateinit var currentTileSetStrings: TileSetStrings
|
||||
@ -112,6 +118,10 @@ class WorldMapHolder(
|
||||
// Contains the data required to draw a "swap with" button
|
||||
class SwapWithButtonDto(val unit: MapUnit, val tile: Tile) : ButtonDto
|
||||
|
||||
// Contains the data required to draw a "connect road" button
|
||||
class ConnectRoadButtonDto(val unit: MapUnit, val tile: Tile) : ButtonDto
|
||||
|
||||
|
||||
internal fun addTiles() {
|
||||
val tileSetStrings = TileSetStrings()
|
||||
currentTileSetStrings = tileSetStrings
|
||||
@ -153,38 +163,46 @@ class WorldMapHolder(
|
||||
removeUnitActionOverlay()
|
||||
selectedTile = tile
|
||||
unitMovementPaths.clear()
|
||||
unitConnectRoadPaths.clear()
|
||||
|
||||
val unitTable = worldScreen.bottomUnitTable
|
||||
val previousSelectedUnits = unitTable.selectedUnits.toList() // create copy
|
||||
val previousSelectedCity = unitTable.selectedCity
|
||||
val previousSelectedUnitIsSwapping = unitTable.selectedUnitIsSwapping
|
||||
val previousSelectedUnitIsConnectingRoad = unitTable.selectedUnitIsConnectingRoad
|
||||
unitTable.tileSelected(tile)
|
||||
val newSelectedUnit = unitTable.selectedUnit
|
||||
|
||||
if (previousSelectedCity != null && tile != previousSelectedCity.getCenterTile())
|
||||
tileGroups[previousSelectedCity.getCenterTile()]!!.layerCityButton.moveUp()
|
||||
|
||||
if (previousSelectedUnits.isNotEmpty() && previousSelectedUnits.any { it.getTile() != tile }
|
||||
&& worldScreen.isPlayersTurn
|
||||
&& (
|
||||
if (previousSelectedUnitIsSwapping)
|
||||
previousSelectedUnits.first().movement.canUnitSwapTo(tile)
|
||||
else
|
||||
previousSelectedUnits.any {
|
||||
it.movement.canMoveTo(tile) ||
|
||||
it.movement.isUnknownTileWeShouldAssumeToBePassable(tile) && !it.baseUnit.movesLikeAirUnits()
|
||||
}
|
||||
) && previousSelectedUnits.any { !it.isPreparingAirSweep()}) {
|
||||
if (previousSelectedUnitIsSwapping) {
|
||||
addTileOverlaysWithUnitSwapping(previousSelectedUnits.first(), tile)
|
||||
}
|
||||
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, tile)
|
||||
if (previousSelectedUnits.isNotEmpty()) {
|
||||
val isTileDifferent = previousSelectedUnits.any { it.getTile() != tile }
|
||||
val isPlayerTurn = worldScreen.isPlayersTurn
|
||||
val existsUnitNotPreparingAirSweep = previousSelectedUnits.any { !it.isPreparingAirSweep() }
|
||||
|
||||
// Todo: valid tiles for actions should be handled internally, not here.
|
||||
val canPerformActionsOnTile = if (previousSelectedUnitIsSwapping) {
|
||||
previousSelectedUnits.first().movement.canUnitSwapTo(tile)
|
||||
} else if(previousSelectedUnitIsConnectingRoad) {
|
||||
true
|
||||
} else {
|
||||
previousSelectedUnits.any {
|
||||
it.movement.canMoveTo(tile) ||
|
||||
(it.movement.isUnknownTileWeShouldAssumeToBePassable(tile) && !it.baseUnit.movesLikeAirUnits())
|
||||
}
|
||||
}
|
||||
} else addTileOverlays(tile) // no unit movement but display the units in the tile etc.
|
||||
|
||||
if (isTileDifferent && isPlayerTurn && canPerformActionsOnTile && existsUnitNotPreparingAirSweep) {
|
||||
when {
|
||||
previousSelectedUnitIsSwapping -> addTileOverlaysWithUnitSwapping(previousSelectedUnits.first(), tile)
|
||||
previousSelectedUnitIsConnectingRoad -> addTileOverlaysWithUnitRoadConnecting(previousSelectedUnits.first(), tile)
|
||||
else -> addTileOverlaysWithUnitMovement(previousSelectedUnits, tile) // Long-running task
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addTileOverlays(tile) // no unit movement but display the units in the tile etc.
|
||||
}
|
||||
|
||||
if (newSelectedUnit == null || newSelectedUnit.isCivilian()) {
|
||||
val unitsInTile = selectedTile!!.getUnits()
|
||||
@ -204,6 +222,7 @@ class WorldMapHolder(
|
||||
removeUnitActionOverlay()
|
||||
selectedTile = tile
|
||||
unitMovementPaths.clear()
|
||||
unitConnectRoadPaths.clear()
|
||||
if (!worldScreen.canChangeState) return
|
||||
|
||||
// Concurrency might open up a race condition window - if worldScreen.shouldUpdate is on too
|
||||
@ -327,6 +346,24 @@ class WorldMapHolder(
|
||||
removeUnitActionOverlay()
|
||||
}
|
||||
|
||||
private fun connectRoadToTargetTile(selectedUnit: MapUnit, targetTile: Tile) {
|
||||
selectedUnit.automatedRoadConnectionDestination = targetTile.position
|
||||
selectedUnit.automatedRoadConnectionPath = null
|
||||
selectedUnit.action = UnitActionType.ConnectRoad.value
|
||||
selectedUnit.automated = true
|
||||
UnitAutomation.automateUnitMoves(selectedUnit)
|
||||
|
||||
SoundPlayer.play(UncivSound("wagon"))
|
||||
|
||||
worldScreen.shouldUpdate = true
|
||||
removeUnitActionOverlay()
|
||||
|
||||
// Make highlighting go away
|
||||
worldScreen.bottomUnitTable.selectedUnitIsConnectingRoad = false
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun addTileOverlaysWithUnitMovement(selectedUnits: List<MapUnit>, tile: Tile) {
|
||||
Concurrency.run("TurnsToGetThere") {
|
||||
/** LibGdx sometimes has these weird errors when you try to edit the UI layout from 2 separate threads.
|
||||
@ -397,6 +434,27 @@ class WorldMapHolder(
|
||||
worldScreen.shouldUpdate = true
|
||||
}
|
||||
|
||||
private fun addTileOverlaysWithUnitRoadConnecting(selectedUnit: MapUnit, tile: Tile){
|
||||
Concurrency.run("ConnectRoad") {
|
||||
val validTile = tile.isLand &&
|
||||
!tile.isImpassible() &&
|
||||
selectedUnit.civ.hasExplored(tile)
|
||||
if (validTile) {
|
||||
val roadPath: List<Tile>? = MapPathing.getRoadPath(selectedUnit, selectedUnit.currentTile, tile)
|
||||
launchOnGLThread {
|
||||
if (roadPath == null) { // give the regular tile overlays with no road connection
|
||||
addTileOverlays(tile)
|
||||
worldScreen.shouldUpdate = true
|
||||
return@launchOnGLThread
|
||||
}
|
||||
unitConnectRoadPaths[selectedUnit] = roadPath
|
||||
val connectRoadButtonDto = ConnectRoadButtonDto(selectedUnit, tile)
|
||||
addTileOverlays(tile, connectRoadButtonDto)
|
||||
worldScreen.shouldUpdate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun addTileOverlays(tile: Tile, buttonDto: ButtonDto? = null) {
|
||||
val table = Table().apply { defaults().pad(10f) }
|
||||
if (buttonDto != null && worldScreen.canChangeState)
|
||||
@ -404,6 +462,7 @@ class WorldMapHolder(
|
||||
when (buttonDto) {
|
||||
is MoveHereButtonDto -> getMoveHereButton(buttonDto)
|
||||
is SwapWithButtonDto -> getSwapWithButton(buttonDto)
|
||||
is ConnectRoadButtonDto -> getConnectRoadButton(buttonDto)
|
||||
else -> null
|
||||
}
|
||||
)
|
||||
@ -495,6 +554,26 @@ class WorldMapHolder(
|
||||
return swapWithButton
|
||||
}
|
||||
|
||||
private fun getConnectRoadButton(dto: ConnectRoadButtonDto): Group {
|
||||
val connectRoadButton = Group().apply { width = buttonSize;height = buttonSize; }
|
||||
connectRoadButton.addActor(ImageGetter.getUnitActionPortrait("RoadConnection", buttonSize * 0.8f).apply {
|
||||
center(connectRoadButton)
|
||||
}
|
||||
)
|
||||
|
||||
val unitIcon = UnitGroup(dto.unit, smallerCircleSizes)
|
||||
unitIcon.y = buttonSize - unitIcon.height
|
||||
connectRoadButton.addActor(unitIcon)
|
||||
|
||||
connectRoadButton.onActivation(UncivSound.Silent) {
|
||||
connectRoadToTargetTile(dto.unit, dto.tile)
|
||||
}
|
||||
|
||||
connectRoadButton.keyShortcuts.add(KeyboardBinding.ConnectRoad)
|
||||
|
||||
return connectRoadButton
|
||||
}
|
||||
|
||||
|
||||
fun addOverlayOnTileGroup(group: TileGroup, actor: Actor) {
|
||||
|
||||
@ -575,6 +654,10 @@ class WorldMapHolder(
|
||||
unitTable.selectedCity != null -> {
|
||||
val city = unitTable.selectedCity!!
|
||||
updateBombardableTilesForSelectedCity(city)
|
||||
// We still want to show road paths to the selected city if they are present
|
||||
if (unitTable.selectedUnitIsConnectingRoad){
|
||||
updateTilesForSelectedUnit(unitTable.selectedUnits[0])
|
||||
}
|
||||
}
|
||||
unitTable.selectedUnit != null -> {
|
||||
for (unit in unitTable.selectedUnits) {
|
||||
@ -617,6 +700,7 @@ class WorldMapHolder(
|
||||
}
|
||||
}
|
||||
|
||||
// Z-Layer: 0
|
||||
// Highlight suitable tiles in swapping-mode
|
||||
if (worldScreen.bottomUnitTable.selectedUnitIsSwapping) {
|
||||
val unitSwappableTiles = unit.movement.getUnitSwappableTiles()
|
||||
@ -625,7 +709,29 @@ class WorldMapHolder(
|
||||
tileGroups[tile]!!.layerOverlay.showHighlight(swapUnitsTileOverlayColor,
|
||||
if (UncivGame.Current.settings.singleTapMove) 0.7f else 0.3f)
|
||||
}
|
||||
// In swapping-mode don't want to show other overlays
|
||||
// In swapping-mode we don't want to show other overlays
|
||||
return
|
||||
}
|
||||
|
||||
// Z-Layer: 0
|
||||
// Highlight suitable tiles in road connecting mode
|
||||
if (worldScreen.bottomUnitTable.selectedUnitIsConnectingRoad){
|
||||
val validTiles = unit.civ.gameInfo.tileMap.tileList.filter {
|
||||
MapPathing.isValidRoadPathTile(unit, it)
|
||||
}
|
||||
unit.civ.gameInfo.civilizations
|
||||
val connectRoadTileOverlayColor = Color.RED
|
||||
for (tile in validTiles) {
|
||||
tileGroups[tile]!!.layerOverlay.showHighlight(connectRoadTileOverlayColor, 0.3f)
|
||||
}
|
||||
|
||||
if (unitConnectRoadPaths.containsKey(unit)) {
|
||||
for (tile in unitConnectRoadPaths[unit]!!) {
|
||||
tileGroups[tile]!!.layerOverlay.showHighlight(Color.ORANGE, 0.8f)
|
||||
}
|
||||
}
|
||||
|
||||
// In road connecting mode we don't want to show other overlays
|
||||
return
|
||||
}
|
||||
|
||||
@ -636,6 +742,7 @@ class WorldMapHolder(
|
||||
val nukeBlastRadius = if (unit.baseUnit.isNuclearWeapon() && selectedTile != null && selectedTile != unit.getTile())
|
||||
unit.getNukeBlastRadius() else -1
|
||||
|
||||
// Z-Layer: 1
|
||||
// Highlight tiles within movement range
|
||||
for (tile in tilesInMoveRange) {
|
||||
val group = tileGroups[tile]!!
|
||||
@ -663,6 +770,7 @@ class WorldMapHolder(
|
||||
|
||||
}
|
||||
|
||||
// Z-Layer: 2
|
||||
// Add back in the red markers for Air Unit Attack range since they can't move, but can still attack
|
||||
if (unit.cache.cannotMove && isAirUnit && !unit.isPreparingAirSweep()) {
|
||||
val tilesInAttackRange = unit.getTile().getTilesInDistanceRange(IntRange(1, unit.getRange()))
|
||||
@ -672,6 +780,7 @@ class WorldMapHolder(
|
||||
}
|
||||
}
|
||||
|
||||
// Z-Layer: 3
|
||||
// Movement paths
|
||||
if (unitMovementPaths.containsKey(unit)) {
|
||||
for (tile in unitMovementPaths[unit]!!) {
|
||||
@ -679,11 +788,29 @@ class WorldMapHolder(
|
||||
}
|
||||
}
|
||||
|
||||
// Z-Layer: 4
|
||||
// Highlight road path for workers currently connecting roads
|
||||
if (unit.isAutomatingRoadConnection()) {
|
||||
val currTileIndex = unit.automatedRoadConnectionPath!!.indexOf(unit.currentTile.position)
|
||||
if (currTileIndex != -1) {
|
||||
val futureTiles = unit.automatedRoadConnectionPath!!.filterIndexed { index, _ ->
|
||||
index > currTileIndex
|
||||
}.map{tilePos ->
|
||||
tileMap[tilePos]
|
||||
}
|
||||
for (tile in futureTiles){
|
||||
tileGroups[tile]!!.layerOverlay.showHighlight(Color.ORANGE, if (UncivGame.Current.settings.singleTapMove) 0.7f else 0.3f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Z-Layer: 5
|
||||
// Highlight movement destination tile
|
||||
if (unit.isMoving()) {
|
||||
tileGroups[unit.getMovementDestination()]!!.layerOverlay.showHighlight(Color.WHITE, 0.7f)
|
||||
}
|
||||
|
||||
// Z-Layer: 6
|
||||
// Highlight attackable tiles
|
||||
if (unit.isMilitary()) {
|
||||
|
||||
@ -711,6 +838,7 @@ class WorldMapHolder(
|
||||
}
|
||||
}
|
||||
|
||||
// Z-Layer: 7
|
||||
// Highlight best tiles for city founding
|
||||
if (unit.hasUnique(UniqueType.FoundCity)
|
||||
&& UncivGame.Current.settings.showSettlersSuggestedCityLocations) {
|
||||
|
@ -46,6 +46,9 @@ class UnitTable(val worldScreen: WorldScreen) : Table() {
|
||||
// Whether the (first) selected unit is in unit-swapping mode
|
||||
var selectedUnitIsSwapping = false
|
||||
|
||||
// Whether the (first) selected unit is in road-connecting mode
|
||||
var selectedUnitIsConnectingRoad = false
|
||||
|
||||
/** Sending no unit clears the selected units entirely */
|
||||
fun selectUnit(unit: MapUnit?=null, append:Boolean=false) {
|
||||
if (!append) selectedUnits.clear()
|
||||
@ -55,6 +58,7 @@ class UnitTable(val worldScreen: WorldScreen) : Table() {
|
||||
unit.actionsOnDeselect()
|
||||
}
|
||||
selectedUnitIsSwapping = false
|
||||
selectedUnitIsConnectingRoad = false
|
||||
}
|
||||
|
||||
var selectedCity : City? = null
|
||||
@ -292,7 +296,13 @@ class UnitTable(val worldScreen: WorldScreen) : Table() {
|
||||
}
|
||||
|
||||
fun citySelected(city: City) : Boolean {
|
||||
selectUnit()
|
||||
// If the last selected unit connecting a road, keep it selected. Otherwise, clear.
|
||||
if(selectedUnitIsConnectingRoad){
|
||||
selectUnit(selectedUnits[0])
|
||||
selectedUnitIsConnectingRoad = true // selectUnit resets this
|
||||
}else{
|
||||
selectUnit()
|
||||
}
|
||||
if (city == selectedCity) return false
|
||||
selectedCity = city
|
||||
selectedUnitHasChanged = true
|
||||
|
@ -39,6 +39,7 @@ object UnitActions {
|
||||
UnitActionType.SetUp to UnitActionsFromUniques::getSetupActions,
|
||||
UnitActionType.FoundCity to UnitActionsFromUniques::getFoundCityActions,
|
||||
UnitActionType.ConstructImprovement to UnitActionsFromUniques::getBuildingImprovementsActions,
|
||||
UnitActionType.ConnectRoad to UnitActionsFromUniques::getConnectRoadActions,
|
||||
UnitActionType.Repair to UnitActionsFromUniques::getRepairActions,
|
||||
UnitActionType.HurryResearch to UnitActionsGreatPerson::getHurryResearchActions,
|
||||
UnitActionType.HurryWonder to UnitActionsGreatPerson::getHurryWonderActions,
|
||||
@ -302,7 +303,6 @@ object UnitActions {
|
||||
return
|
||||
|
||||
if (unit.isAutomated()) return
|
||||
|
||||
actionList += UnitAction(UnitActionType.Automate,
|
||||
isCurrentAction = unit.isAutomated(),
|
||||
action = {
|
||||
|
@ -7,6 +7,7 @@ import com.unciv.logic.civilization.Civilization
|
||||
import com.unciv.logic.civilization.PlayerType
|
||||
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.logic.map.tile.RoadStatus
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import com.unciv.models.Counter
|
||||
import com.unciv.models.UncivSound
|
||||
@ -274,6 +275,20 @@ object UnitActionsFromUniques {
|
||||
return finalActions
|
||||
}
|
||||
|
||||
fun getConnectRoadActions(unit: MapUnit, tile: Tile) = sequence {
|
||||
if (!unit.hasUnique(UniqueType.BuildImprovements)) return@sequence
|
||||
if (unit.civ.tech.getBestRoadAvailable() == RoadStatus.None) return@sequence
|
||||
val worldScreen = GUI.getWorldScreen()
|
||||
yield(UnitAction(UnitActionType.ConnectRoad,
|
||||
isCurrentAction = unit.isAutomatingRoadConnection(),
|
||||
action = {
|
||||
worldScreen.bottomUnitTable.selectedUnitIsConnectingRoad =
|
||||
!worldScreen.bottomUnitTable.selectedUnitIsConnectingRoad
|
||||
worldScreen.shouldUpdate = true
|
||||
}
|
||||
)
|
||||
)
|
||||
}.asIterable()
|
||||
|
||||
fun getTransformActions(
|
||||
unit: MapUnit, tile: Tile
|
||||
|
Reference in New Issue
Block a user