AI worker build roads improvement (#11615)

* Reworked how city connection is calculated

* Fixed neighboringCities being saved

* Improved the A* road finding to work!

* Fixed railroad upgrading incorrectly

* Workers will now try to swap with lazy units blocking them

* Improved support for military worker units

* Worker checks if they can swap with the unit before trying to swap
This commit is contained in:
Oskar Niesen 2024-05-21 11:52:31 -05:00 committed by GitHub
parent ca1a2816c8
commit 783c0aa7c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 257 additions and 170 deletions

View File

@ -6,11 +6,14 @@ import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.BFS import com.unciv.logic.map.BFS
import com.unciv.logic.map.HexMath import com.unciv.logic.map.HexMath
import com.unciv.logic.map.MapPathing
import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.RoadStatus import com.unciv.logic.map.tile.RoadStatus
import com.unciv.logic.map.tile.Tile import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.utils.Log import com.unciv.utils.Log
import com.unciv.utils.debug import com.unciv.utils.debug
import kotlin.math.max
private object WorkerAutomationConst { private object WorkerAutomationConst {
/** BFS max size is determined by the aerial distance of two cities to connect, padded with this */ /** BFS max size is determined by the aerial distance of two cities to connect, padded with this */
@ -57,21 +60,122 @@ class RoadBetweenCitiesAutomation(val civInfo: Civilization, cachedForTurn: Int,
result result
} }
/** Cache of roads to connect cities each turn */ /** Cache of roads to connect cities each turn. Call [getRoadsToBuildFromCity] instead of using this */
internal val roadsToConnectCitiesCache: HashMap<City, List<Tile>> = HashMap() private val roadsToBuildByCitiesCache: HashMap<City, List<RoadPlan>> = HashMap()
/** Hashmap of all cached tiles in each list in [roadsToConnectCitiesCache] */ /** Hashmap of all cached tiles in each list in [roadsToBuildByCitiesCache] */
internal val tilesOfRoadsToConnectCities: HashMap<Tile, City> = HashMap() internal val tilesOfRoadsMap: HashMap<Tile, RoadPlan> = HashMap()
inner class RoadPlan(val tiles: List<Tile>, val priority: Float, val fromCity: City, val toCity: City) {
val numberOfRoadsToBuild: Int by lazy { tiles.count { it.getUnpillagedRoad() != bestRoadAvailable } }
}
/** /**
* Uses a cache to find and return the connection to make that is associated with a city. * Tries to return a list of road plans to connect this city to the surrounding cities.
* May not work if the unit that originally created this cache is different from the next. * If there are no surrounding cities to connect to and this city is still unconnected to the capital it will try and build a special road to the capital.
* (Due to the difference in [UnitMovement.canPassThrough()]) *
* @return every road that we want to try and connect assosiated with this city.
*/ */
private fun getRoadConnectionBetweenCities(unit: MapUnit, city: City): List<Tile> { fun getRoadsToBuildFromCity(city: City): List<RoadPlan> {
if (city in roadsToConnectCitiesCache) return roadsToConnectCitiesCache[city]!! if (roadsToBuildByCitiesCache.containsKey(city)) {
return roadsToBuildByCitiesCache[city]!!
}
// TODO: some better worker representative needs to be used here
val workerUnit = civInfo.gameInfo.ruleset.units.map { it.value }.firstOrNull { it.hasUnique(UniqueType.BuildImprovements) }?.getMapUnit(civInfo) ?: return listOf()
val roadToCapitalStatus = city.cityStats.getRoadTypeOfConnectionToCapital()
fun rankRoadCapitalPriority(roadStatus: RoadStatus): Float {
return when(roadStatus) {
RoadStatus.None -> if (bestRoadAvailable != RoadStatus.None) 2f else 0f
RoadStatus.Road -> if (bestRoadAvailable != RoadStatus.Road) 1f else 0f
else -> 0f
}
}
val basePriority = rankRoadCapitalPriority(roadToCapitalStatus)
val roadsToBuild: MutableList<RoadPlan> = mutableListOf()
for (closeCity in city.neighboringCities.filter { it.civ == civInfo }) {
// Try to find if the other city has planned to build a road to this city
if (roadsToBuildByCitiesCache.containsKey(closeCity)) {
// There should only ever be one or zero possible connections from their city to this city
val roadToBuild = roadsToBuildByCitiesCache[closeCity]!!.firstOrNull { it.fromCity == city || it.toCity == city}
if (roadToBuild != null) {
roadsToBuild.add(roadToBuild)
}
// We already did the hard work, there can't be any other possible roads to this city
continue
}
// Try to build a plan for the road to the city
val roadPath = if (civInfo.cities.indexOf(city) < civInfo.cities.indexOf(closeCity)) MapPathing.getRoadPath(workerUnit, city.getCenterTile(), closeCity.getCenterTile()) ?: continue
else MapPathing.getRoadPath(workerUnit, closeCity.getCenterTile(), city.getCenterTile()) ?: continue
val worstRoadStatus = getWorstRoadTypeInPath(roadPath)
if (worstRoadStatus == bestRoadAvailable) continue
// Make sure that we are taking in to account the other cities needs
var roadPriority = max(basePriority, rankRoadCapitalPriority(closeCity.cityStats.getRoadTypeOfConnectionToCapital()))
if (worstRoadStatus == RoadStatus.None) {
roadPriority += 2
} else if (worstRoadStatus == RoadStatus.Road && bestRoadAvailable == RoadStatus.Railroad) {
roadPriority += 1
}
if (closeCity.cityStats.getRoadTypeOfConnectionToCapital() > roadToCapitalStatus)
roadPriority += 1
val newRoadPlan = RoadPlan(roadPath, roadPriority + (city.population.population + closeCity.population.population) / 4f, city, closeCity)
roadsToBuild.add(newRoadPlan)
for (tile in newRoadPlan.tiles) {
if (tile !in tilesOfRoadsMap || tilesOfRoadsMap[tile]!!.priority < newRoadPlan.priority)
tilesOfRoadsMap[tile] = newRoadPlan
}
}
// If and only if we have no roads to build to close-by cities then we check for a road to build to the capital
if (roadsToBuild.isEmpty() && roadToCapitalStatus < bestRoadAvailable) {
val roadToCapital = getRoadToConnectCityToCapital(workerUnit, city)
if (roadToCapital != null) {
val worstRoadStatus = getWorstRoadTypeInPath(roadToCapital.second)
var roadPriority = basePriority
roadPriority += if (worstRoadStatus == RoadStatus.None) 2f else 1f
val newRoadPlan = RoadPlan(roadToCapital.second, roadPriority + (city.population.population) / 2f, city, roadToCapital.first)
roadsToBuild.add(newRoadPlan)
for (tile in newRoadPlan.tiles) {
if (tile !in tilesOfRoadsMap || tilesOfRoadsMap[tile]!!.priority < newRoadPlan.priority)
tilesOfRoadsMap[tile] = newRoadPlan
}
}
}
workerUnit.destroy()
roadsToBuildByCitiesCache[city] = roadsToBuild
return roadsToBuild
}
private fun getWorstRoadTypeInPath(path: List<Tile>): RoadStatus {
var worstRoadTile = RoadStatus.Railroad
for (tile in path) {
if (tile.getUnpillagedRoad() < worstRoadTile) {
worstRoadTile = tile.getUnpillagedRoad()
if (worstRoadTile == RoadStatus.None)
return RoadStatus.None
}
}
return worstRoadTile
}
/**
* Returns a road that can connect this city to the capital.
* This is a very expensive function that doesn't nessesarily produce the same roads as in [getRoadsToBuildFromCity].
* So it should only be used if it is the only road that a city wants to build.
* @return a pair containing a list of tiles that resemble the road to build and the city that the road will connect to
*/
private fun getRoadToConnectCityToCapital(unit: MapUnit, city: City): Pair<City, List<Tile>>? {
val isCandidateTilePredicate: (Tile) -> Boolean = { it.isLand && unit.movement.canPassThrough(it) } val isCandidateTilePredicate: (Tile) -> Boolean = { it.isLand && unit.movement.canPassThrough(it) }
val toConnectTile = city.getCenterTile() val toConnectTile = city.getCenterTile()
val bfs: BFS = bfsCache[toConnectTile.position] ?: val bfs: BFS = bfsCache[toConnectTile.position] ?:
@ -90,38 +194,12 @@ class RoadBetweenCitiesAutomation(val civInfo: Civilization, cachedForTurn: Int,
// We have a winner! // We have a winner!
val cityTile = nextTile val cityTile = nextTile
val pathToCity = bfs.getPathTo(cityTile) val pathToCity = bfs.getPathTo(cityTile)
roadsToConnectCitiesCache[city] = pathToCity.toList().filter { it.roadStatus != bestRoadAvailable }
for (tile in pathToCity) { return Pair(cityTile.getCity()!!, pathToCity.toList())
if (tile !in tilesOfRoadsToConnectCities)
tilesOfRoadsToConnectCities[tile] = city
}
return roadsToConnectCitiesCache[city]!!
} }
nextTile = bfs.nextStep() nextTile = bfs.nextStep()
} }
return null
roadsToConnectCitiesCache[city] = listOf()
return roadsToConnectCitiesCache[city]!!
}
/** Civ-wide list of unconnected Cities, sorted by closest to capital first */
private val citiesThatNeedConnecting: List<City> by lazy {
val result = civInfo.cities.asSequence()
.filter {
civInfo.getCapital() != null
&& it.population.population > 3
&& !it.isCapital() && !it.isBeingRazed // Cities being razed should not be connected.
&& !it.cityStats.isConnectedToCapital(bestRoadAvailable)
}.sortedBy {
it.getCenterTile().aerialDistanceTo(civInfo.getCapital()!!.getCenterTile())
}.toList()
if (Log.shouldLog()) {
debug("WorkerAutomation citiesThatNeedConnecting for ${civInfo.civName} turn $cachedForTurn:")
if (result.isEmpty()) debug("\tempty")
else result.forEach { debug("\t${it.name}") }
}
result
} }
/** /**
@ -129,17 +207,13 @@ class RoadBetweenCitiesAutomation(val civInfo: Civilization, cachedForTurn: Int,
* Returns a list of all the cities close by that this worker may want to connect * Returns a list of all the cities close by that this worker may want to connect
*/ */
internal fun getNearbyCitiesToConnect(unit: MapUnit): List<City> { internal fun getNearbyCitiesToConnect(unit: MapUnit): List<City> {
if (bestRoadAvailable == RoadStatus.None || citiesThatNeedConnecting.isEmpty()) return listOf() if (bestRoadAvailable == RoadStatus.None) return listOf()
val candidateCities = citiesThatNeedConnecting.filter { val candidateCities = civInfo.cities.filter {
// Cities that are too far away make the canReach() calculations devastatingly long // Cities that are too far away make the canReach() calculations devastatingly long
it.getCenterTile().aerialDistanceTo(unit.getTile()) < 20 it.getCenterTile().aerialDistanceTo(unit.getTile()) < 20 && getRoadsToBuildFromCity(it).isNotEmpty()
} }
if (candidateCities.none()) return listOf() // do nothing. if (candidateCities.none()) return listOf() // do nothing.
// Search through ALL candidate cities to build the cache
for (toConnectCity in candidateCities) {
getRoadConnectionBetweenCities(unit, toConnectCity).filter { it.getUnpillagedRoad() < bestRoadAvailable }
}
return candidateCities return candidateCities
} }
@ -148,35 +222,30 @@ class RoadBetweenCitiesAutomation(val civInfo: Civilization, cachedForTurn: Int,
* @return whether we actually did anything * @return whether we actually did anything
*/ */
internal fun tryConnectingCities(unit: MapUnit, candidateCities: List<City>): Boolean { internal fun tryConnectingCities(unit: MapUnit, candidateCities: List<City>): Boolean {
if (bestRoadAvailable == RoadStatus.None || citiesThatNeedConnecting.isEmpty()) return false if (bestRoadAvailable == RoadStatus.None) return false
if (candidateCities.none()) return false // do nothing. if (candidateCities.none()) return false // do nothing.
val currentTile = unit.getTile() val currentTile = unit.getTile()
var bestTileToConstructRoadOn: Tile? = null
var bestTileToConstructRoadOnDist: Int = Int.MAX_VALUE
// Search through ALL candidate cities for the closest tile to build a road on // Search through ALL candidate cities for the closest tile to build a road on
for (toConnectCity in candidateCities) { for (toConnectCity in candidateCities.sortedByDescending { it.getCenterTile().aerialDistanceTo(unit.getTile()) }) {
val roadableTiles = getRoadConnectionBetweenCities(unit, toConnectCity).filter { it.getUnpillagedRoad() < bestRoadAvailable } val tilesByPriority = getRoadsToBuildFromCity(toConnectCity).flatMap { roadPlan -> roadPlan.tiles.map { tile -> Pair(tile, roadPlan.priority) } }
val reachableTile = roadableTiles.map { Pair(it, it.aerialDistanceTo(unit.getTile())) } val tilesSorted = tilesByPriority.filter { it.first.getUnpillagedRoad() < bestRoadAvailable }
.filter { it.second < bestTileToConstructRoadOnDist } .sortedBy { it.first.aerialDistanceTo(unit.getTile()) + (it.second / 10f) }
.sortedBy { it.second } val bestTile = tilesSorted.firstOrNull {
.firstOrNull {
unit.movement.canMoveTo(it.first) && unit.movement.canReach(it.first) unit.movement.canMoveTo(it.first) && unit.movement.canReach(it.first)
} ?: continue // Apparently we can't reach any of these tiles at all }?.first ?: continue // Apparently we can't reach any of these tiles at all
bestTileToConstructRoadOn = reachableTile.first
bestTileToConstructRoadOnDist = reachableTile.second
}
if (bestTileToConstructRoadOn == null) return false if (bestTile != currentTile && unit.currentMovement > 0)
unit.movement.headTowards(bestTile)
if (bestTileToConstructRoadOn != currentTile && unit.currentMovement > 0) if (unit.currentMovement > 0 && bestTile == currentTile
unit.movement.headTowards(bestTileToConstructRoadOn)
if (unit.currentMovement > 0 && bestTileToConstructRoadOn == currentTile
&& currentTile.improvementInProgress != bestRoadAvailable.name) { && currentTile.improvementInProgress != bestRoadAvailable.name) {
val improvement = bestRoadAvailable.improvement(civInfo.gameInfo.ruleset)!! val improvement = bestRoadAvailable.improvement(civInfo.gameInfo.ruleset)!!
bestTileToConstructRoadOn.startWorkingOnImprovement(improvement, civInfo, unit) bestTile.startWorkingOnImprovement(improvement, civInfo, unit)
} }
return true return true
} }
return false
}
} }

View File

@ -83,6 +83,12 @@ class WorkerAutomation(
if (tileToWork != currentTile) { if (tileToWork != currentTile) {
debug("WorkerAutomation: %s -> head towards %s", unit.toString(), tileToWork) debug("WorkerAutomation: %s -> head towards %s", unit.toString(), tileToWork)
if (unit.movement.canReachInCurrentTurn(tileToWork) && unit.movement.canMoveTo(tileToWork, canSwap = true)) {
if (!unit.movement.canMoveTo(tileToWork, canSwap = false) && unit.movement.canUnitSwapTo(tileToWork)) {
// There must be a unit on the target tile! Lets swap with it.
unit.movement.swapMoveToTile(tileToWork)
}
}
val reachedTile = unit.movement.headTowards(tileToWork) val reachedTile = unit.movement.headTowards(tileToWork)
if (reachedTile != currentTile) unit.doAction() // otherwise, we get a situation where the worker is automated, so it tries to move but doesn't, then tries to automate, then move, etc, forever. Stack overflow exception! if (reachedTile != currentTile) unit.doAction() // otherwise, we get a situation where the worker is automated, so it tries to move but doesn't, then tries to automate, then move, etc, forever. Stack overflow exception!
@ -136,7 +142,7 @@ class WorkerAutomation(
val citiesToNumberOfUnimprovedTiles = HashMap<String, Int>() val citiesToNumberOfUnimprovedTiles = HashMap<String, Int>()
for (city in unit.civ.cities) { for (city in unit.civ.cities) {
citiesToNumberOfUnimprovedTiles[city.id] = city.getTiles() citiesToNumberOfUnimprovedTiles[city.id] = city.getTiles()
.count { it.isLand && it.civilianUnit == null && (it.isPillaged() || tileHasWorkToDo(it, unit)) } .count { tile -> tile.isLand && tile.getUnits().any { unit -> unit.cache.hasUniqueToBuildImprovements } && (tile.isPillaged() || tileHasWorkToDo(tile, unit)) }
} }
val closestUndevelopedCity = unit.civ.cities.asSequence() val closestUndevelopedCity = unit.civ.cities.asSequence()
@ -176,7 +182,8 @@ class WorkerAutomation(
val workableTilesCenterFirst = currentTile.getTilesInDistance(4) val workableTilesCenterFirst = currentTile.getTilesInDistance(4)
.filter { .filter {
it !in tilesToAvoid it !in tilesToAvoid
&& (it.civilianUnit == null || it == currentTile) && (it == currentTile || (unit.isCivilian() && (it.civilianUnit == null || !it.civilianUnit!!.cache.hasUniqueToBuildImprovements))
|| (unit.isMilitary() && (it.militaryUnit == null || !it.militaryUnit!!.cache.hasUniqueToBuildImprovements)))
&& (it.owningCity == null || it.getOwner() == civInfo) && (it.owningCity == null || it.getOwner() == civInfo)
&& !it.isCityCenter() && !it.isCityCenter()
&& getBasePriority(it, unit) > 1 && getBasePriority(it, unit) > 1
@ -194,7 +201,7 @@ class WorkerAutomation(
// These are the expensive calculations (tileCanBeImproved, canReach), so we only apply these filters after everything else it done. // These are the expensive calculations (tileCanBeImproved, canReach), so we only apply these filters after everything else it done.
if (!tileHasWorkToDo(tileInGroup, unit)) continue if (!tileHasWorkToDo(tileInGroup, unit)) continue
if (unit.getTile() == tileInGroup) return unit.getTile() if (unit.getTile() == tileInGroup) return unit.getTile()
if (!unit.movement.canReach(tileInGroup) || tileInGroup.civilianUnit != null) continue if (!unit.movement.canReach(tileInGroup)) continue
if (bestTile == null || getFullPriority(tileInGroup, unit) > getFullPriority(bestTile, unit)) { if (bestTile == null || getFullPriority(tileInGroup, unit) > getFullPriority(bestTile, unit)) {
bestTile = tileInGroup bestTile = tileInGroup
} }
@ -233,7 +240,7 @@ class WorkerAutomation(
&& !civInfo.hasResource(tile.resource!!)) && !civInfo.hasResource(tile.resource!!))
priority += 2 priority += 2
} }
if (tile in roadBetweenCitiesAutomation.tilesOfRoadsToConnectCities) priority += when { if (tile in roadBetweenCitiesAutomation.tilesOfRoadsMap) priority += when {
civInfo.stats.statsForNextTurn.gold <= 5 -> 0 civInfo.stats.statsForNextTurn.gold <= 5 -> 0
civInfo.stats.statsForNextTurn.gold <= 10 -> 1 civInfo.stats.statsForNextTurn.gold <= 10 -> 1
civInfo.stats.statsForNextTurn.gold <= 30 -> 2 civInfo.stats.statsForNextTurn.gold <= 30 -> 2
@ -373,14 +380,12 @@ class WorkerAutomation(
// Add the value of roads if we want to build it here // Add the value of roads if we want to build it here
if (improvement.isRoad() && roadBetweenCitiesAutomation.bestRoadAvailable.improvement(ruleSet) == improvement if (improvement.isRoad() && roadBetweenCitiesAutomation.bestRoadAvailable.improvement(ruleSet) == improvement
&& tile in roadBetweenCitiesAutomation.tilesOfRoadsToConnectCities) { && tile in roadBetweenCitiesAutomation.tilesOfRoadsMap) {
var value = 1f val roadPlan = roadBetweenCitiesAutomation.tilesOfRoadsMap[tile]!!
val city = roadBetweenCitiesAutomation.tilesOfRoadsToConnectCities[tile]!! var value = roadPlan.priority
if (civInfo.stats.statsForNextTurn.gold >= 20) if (civInfo.stats.statsForNextTurn.gold >= 20)
// Bigger cities have a higher priority to connect
value += (city.population.population - 3) * .3f
// Higher priority if we are closer to connecting the city // Higher priority if we are closer to connecting the city
value += (5 - roadBetweenCitiesAutomation.roadsToConnectCitiesCache[city]!!.size).coerceAtLeast(0) value += (5 - roadPlan.numberOfRoadsToBuild).coerceAtLeast(0)
return value return value
} }

View File

@ -86,6 +86,11 @@ class City : IsPartOfGameInfoSerialization {
var isPuppet = false var isPuppet = false
var updateCitizens = false // flag so that on startTurn() the Governor reassigns Citizens var updateCitizens = false // flag so that on startTurn() the Governor reassigns Citizens
@delegate:Transient
val neighboringCities: List<City> by lazy {
civ.gameInfo.getCities().filter { it != this && it.getCenterTile().aerialDistanceTo(getCenterTile()) <= 8 }.toList()
}
var cityAIFocus: String = CityFocus.NoFocus.name var cityAIFocus: String = CityFocus.NoFocus.name
fun getCityFocus() = CityFocus.values().firstOrNull { it.name == cityAIFocus } ?: CityFocus.NoFocus fun getCityFocus() = CityFocus.values().firstOrNull { it.name == cityAIFocus } ?: CityFocus.NoFocus
fun setCityFocus(cityFocus: CityFocus){ cityAIFocus = cityFocus.name } fun setCityFocus(cityFocus: CityFocus){ cityAIFocus = cityFocus.name }

View File

@ -322,6 +322,12 @@ class CityStats(val city: City) {
else city.isConnectedToCapital() else city.isConnectedToCapital()
} }
fun getRoadTypeOfConnectionToCapital(): RoadStatus {
return if (isConnectedToCapital(RoadStatus.Railroad)) RoadStatus.Railroad
else if (isConnectedToCapital(RoadStatus.Road)) RoadStatus.Road
else RoadStatus.None
}
private fun getBuildingMaintenanceCosts(): Float { private fun getBuildingMaintenanceCosts(): Float {
// Same here - will have a different UI display. // Same here - will have a different UI display.
var buildingsMaintenance = city.cityConstructions.getMaintenanceCosts().toFloat() // this is AFTER the bonus calculation! var buildingsMaintenance = city.cityConstructions.getMaintenanceCosts().toFloat() // this is AFTER the bonus calculation!

View File

@ -16,30 +16,23 @@ object MapPathing {
private fun roadPreferredMovementCost(unit: MapUnit, from: Tile, to: Tile): Float{ private fun roadPreferredMovementCost(unit: MapUnit, from: Tile, to: Tile): Float{
// hasRoadConnection accounts for civs that treat jungle/forest as roads // hasRoadConnection accounts for civs that treat jungle/forest as roads
// Ignore road over river penalties. // Ignore road over river penalties.
val areConnectedByRoad = from.hasRoadConnection(unit.civ, mustBeUnpillaged = false) && to.hasRoadConnection(unit.civ, mustBeUnpillaged = false) if ((to.hasRoadConnection(unit.civ, false) || to.hasRailroadConnection(false)))
if (areConnectedByRoad){ return .5f
// 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 return 1f
} }
fun isValidRoadPathTile(unit: MapUnit, tile: Tile): Boolean { fun isValidRoadPathTile(unit: MapUnit, tile: Tile): Boolean {
val roadImprovement = tile.ruleset.roadImprovement ?: return false val roadImprovement = tile.ruleset.roadImprovement ?: return false
val railRoadImprovement = tile.ruleset.railroadImprovement ?: return false
return tile.isLand return tile.isLand
&& !tile.isImpassible() && !tile.isImpassible()
&& unit.civ.hasExplored(tile) && unit.civ.hasExplored(tile)
&& tile.canCivPassThrough(unit.civ) && tile.canCivPassThrough(unit.civ)
&& (tile.hasRoadConnection(unit.civ, true) || tile.improvementFunctions.canBuildImprovement(roadImprovement, unit.civ)) && (tile.hasRoadConnection(unit.civ, false)
|| tile.hasRailroadConnection(false)
|| tile.improvementFunctions.canBuildImprovement(roadImprovement, unit.civ))
|| tile.improvementFunctions.canBuildImprovement(railRoadImprovement, unit.civ)
} }
/** /**

View File

@ -61,6 +61,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
// Connect roads implies automated is true. It is specified by the action type. // Connect roads implies automated is true. It is specified by the action type.
var action: String? = null var action: String? = null
var automated: Boolean = false var automated: Boolean = false
// We can infer who we are escorting based on our tile // We can infer who we are escorting based on our tile
var escorting: Boolean = false var escorting: Boolean = false
@ -137,7 +138,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
* */ * */
class UnitMovementMemory(position: Vector2, val type: UnitMovementMemoryType) : IsPartOfGameInfoSerialization { class UnitMovementMemory(position: Vector2, val type: UnitMovementMemoryType) : IsPartOfGameInfoSerialization {
@Suppress("unused") // needed because this is part of a save and gets deserialized @Suppress("unused") // needed because this is part of a save and gets deserialized
constructor(): this(Vector2.Zero, UnitMovementMemoryType.UnitMoved) constructor() : this(Vector2.Zero, UnitMovementMemoryType.UnitMoved)
val position = Vector2(position) val position = Vector2(position)
fun clone() = UnitMovementMemory(position, type) fun clone() = UnitMovementMemory(position, type)
@ -258,7 +260,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
fun getMatchingUniques( fun getMatchingUniques(
uniqueType: UniqueType, uniqueType: UniqueType,
stateForConditionals: StateForConditionals = StateForConditionals(civ, unit=this), stateForConditionals: StateForConditionals = StateForConditionals(civ, unit = this),
checkCivInfoUniques: Boolean = false checkCivInfoUniques: Boolean = false
) = sequence { ) = sequence {
yieldAll( yieldAll(
@ -270,7 +272,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
fun hasUnique( fun hasUnique(
uniqueType: UniqueType, uniqueType: UniqueType,
stateForConditionals: StateForConditionals = StateForConditionals(civ, unit=this), stateForConditionals: StateForConditionals = StateForConditionals(civ, unit = this),
checkCivInfoUniques: Boolean = false checkCivInfoUniques: Boolean = false
): Boolean { ): Boolean {
return getMatchingUniques(uniqueType, stateForConditionals, checkCivInfoUniques).any() return getMatchingUniques(uniqueType, stateForConditionals, checkCivInfoUniques).any()
@ -320,7 +322,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
for (boostingUnit in currentTile.getUnits()) { for (boostingUnit in currentTile.getUnits()) {
if (boostingUnit == this) continue if (boostingUnit == this) continue
if (boostingUnit.getMatchingUniques(UniqueType.TransferMovement) if (boostingUnit.getMatchingUniques(UniqueType.TransferMovement)
.none { matchesFilter(it.params[0]) } ) continue .none { matchesFilter(it.params[0]) }) continue
movement = movement.coerceAtLeast(boostingUnit.getMaxMovement()) movement = movement.coerceAtLeast(boostingUnit.getMaxMovement())
} }
@ -411,6 +413,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
hasUnique(UniqueType.HealOnlyByPillaging, checkCivInfoUniques = true) -> 0 hasUnique(UniqueType.HealOnlyByPillaging, checkCivInfoUniques = true) -> 0
else -> rankTileForHealing(getTile()) else -> rankTileForHealing(getTile())
} }
fun canHealInCurrentTile() = getHealAmountForCurrentTile() > 0 fun canHealInCurrentTile() = getHealAmountForCurrentTile() > 0
/** Returns the health points [MapUnit] will receive if healing on [tile] */ /** Returns the health points [MapUnit] will receive if healing on [tile] */
@ -585,6 +588,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
} }
fun getOtherEscortUnit(): MapUnit? { fun getOtherEscortUnit(): MapUnit? {
if (!::currentTile.isInitialized) return null // In some cases we might not have the unit placed on the map yet
if (isCivilian()) return getTile().militaryUnit if (isCivilian()) return getTile().militaryUnit
if (isMilitary()) return getTile().civilianUnit if (isMilitary()) return getTile().civilianUnit
return null return null
@ -649,7 +653,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
/** /**
* Update this unit's cache of viewable tiles and its civ's as well. * Update this unit's cache of viewable tiles and its civ's as well.
*/ */
fun updateVisibleTiles(updateCivViewableTiles:Boolean = true, explorerPosition: Vector2? = null) { fun updateVisibleTiles(updateCivViewableTiles: Boolean = true, explorerPosition: Vector2? = null) {
val oldViewableTiles = viewableTiles val oldViewableTiles = viewableTiles
viewableTiles = when { viewableTiles = when {
@ -672,7 +676,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
} }
for (tile in newlyExploredTiles) { for (tile in newlyExploredTiles) {
// Include tile in the state for correct RNG seeding // Include tile in the state for correct RNG seeding
val state = StateForConditionals(civInfo=civ, unit=this, tile=tile) val state = StateForConditionals(civInfo = civ, unit = this, tile = tile)
for (unique in unfilteredTriggeredUniques) { for (unique in unfilteredTriggeredUniques) {
if (unique.conditionals.any { if (unique.conditionals.any {
it.type == UniqueType.TriggerUponDiscoveringTile it.type == UniqueType.TriggerUponDiscoveringTile
@ -731,7 +735,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
val gotTo = movement.headTowards(destinationTile) val gotTo = movement.headTowards(destinationTile)
if (gotTo == currentTile) { // We didn't move at all if (gotTo == currentTile) { // We didn't move at all
// pathway blocked? Are we still at the same spot as start of turn? // pathway blocked? Are we still at the same spot as start of turn?
if(movementMemories.last().position == currentTile.position) if (movementMemories.last().position == currentTile.position)
action = null action = null
return return
} }
@ -763,11 +767,12 @@ class MapUnit : IsPartOfGameInfoSerialization {
fun destroy(destroyTransportedUnit: Boolean = true) { fun destroy(destroyTransportedUnit: Boolean = true) {
stopEscorting() stopEscorting()
currentMovement = 0f
civ.units.removeUnit(this)
if (::currentTile.isInitialized) {
val currentPosition = Vector2(getTile().position) val currentPosition = Vector2(getTile().position)
civ.attacksSinceTurnStart.addAll(attacksSinceTurnStart.asSequence().map { Civilization.HistoricalAttackMemory(this.name, currentPosition, it) }) civ.attacksSinceTurnStart.addAll(attacksSinceTurnStart.asSequence().map { Civilization.HistoricalAttackMemory(this.name, currentPosition, it) })
currentMovement = 0f
removeFromTile() removeFromTile()
civ.units.removeUnit(this)
civ.cache.updateViewableTiles() civ.cache.updateViewableTiles()
if (destroyTransportedUnit) { if (destroyTransportedUnit) {
// all transported units should be destroyed as well // all transported units should be destroyed as well
@ -775,6 +780,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
.toList() // because we're changing the list .toList() // because we're changing the list
.forEach { unit -> unit.destroy() } .forEach { unit -> unit.destroy() }
} }
}
isDestroyed = true isDestroyed = true
} }
@ -871,6 +878,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
when { when {
!movement.canMoveTo(tile) -> !movement.canMoveTo(tile) ->
throw Exception("Unit $name of ${civ.civName} at $currentTile can't be put in tile $tile!") throw Exception("Unit $name of ${civ.civName} at $currentTile can't be put in tile $tile!")
baseUnit.movesLikeAirUnits() -> tile.airUnits.add(this) baseUnit.movesLikeAirUnits() -> tile.airUnits.add(this)
isCivilian() -> tile.civilianUnit = this isCivilian() -> tile.civilianUnit = this
else -> tile.militaryUnit = this else -> tile.militaryUnit = this
@ -894,6 +902,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
fun stopEscorting() { fun stopEscorting() {
getOtherEscortUnit()?.escorting = false getOtherEscortUnit()?.escorting = false
escorting = false escorting = false
if (::currentTile.isInitialized) // Can cause an error if the unit has not been placed on the map yet.
movement.clearPathfindingCache() movement.clearPathfindingCache()
} }