mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-25 12:54:06 -04:00
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:
parent
ca1a2816c8
commit
783c0aa7c2
@ -6,11 +6,14 @@ import com.unciv.logic.city.City
|
||||
import com.unciv.logic.civilization.Civilization
|
||||
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
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.utils.Log
|
||||
import com.unciv.utils.debug
|
||||
import kotlin.math.max
|
||||
|
||||
private object WorkerAutomationConst {
|
||||
/** 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
|
||||
}
|
||||
|
||||
/** Cache of roads to connect cities each turn */
|
||||
internal val roadsToConnectCitiesCache: HashMap<City, List<Tile>> = HashMap()
|
||||
/** Cache of roads to connect cities each turn. Call [getRoadsToBuildFromCity] instead of using this */
|
||||
private val roadsToBuildByCitiesCache: HashMap<City, List<RoadPlan>> = HashMap()
|
||||
|
||||
/** Hashmap of all cached tiles in each list in [roadsToConnectCitiesCache] */
|
||||
internal val tilesOfRoadsToConnectCities: HashMap<Tile, City> = HashMap()
|
||||
/** Hashmap of all cached tiles in each list in [roadsToBuildByCitiesCache] */
|
||||
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.
|
||||
* May not work if the unit that originally created this cache is different from the next.
|
||||
* (Due to the difference in [UnitMovement.canPassThrough()])
|
||||
* Tries to return a list of road plans to connect this city to the surrounding cities.
|
||||
* 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.
|
||||
*
|
||||
* @return every road that we want to try and connect assosiated with this city.
|
||||
*/
|
||||
private fun getRoadConnectionBetweenCities(unit: MapUnit, city: City): List<Tile> {
|
||||
if (city in roadsToConnectCitiesCache) return roadsToConnectCitiesCache[city]!!
|
||||
fun getRoadsToBuildFromCity(city: City): List<RoadPlan> {
|
||||
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 toConnectTile = city.getCenterTile()
|
||||
val bfs: BFS = bfsCache[toConnectTile.position] ?:
|
||||
@ -90,38 +194,12 @@ class RoadBetweenCitiesAutomation(val civInfo: Civilization, cachedForTurn: Int,
|
||||
// We have a winner!
|
||||
val cityTile = nextTile
|
||||
val pathToCity = bfs.getPathTo(cityTile)
|
||||
roadsToConnectCitiesCache[city] = pathToCity.toList().filter { it.roadStatus != bestRoadAvailable }
|
||||
for (tile in pathToCity) {
|
||||
if (tile !in tilesOfRoadsToConnectCities)
|
||||
tilesOfRoadsToConnectCities[tile] = city
|
||||
}
|
||||
return roadsToConnectCitiesCache[city]!!
|
||||
|
||||
return Pair(cityTile.getCity()!!, pathToCity.toList())
|
||||
}
|
||||
nextTile = bfs.nextStep()
|
||||
}
|
||||
|
||||
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
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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
|
||||
*/
|
||||
internal fun getNearbyCitiesToConnect(unit: MapUnit): List<City> {
|
||||
if (bestRoadAvailable == RoadStatus.None || citiesThatNeedConnecting.isEmpty()) return listOf()
|
||||
val candidateCities = citiesThatNeedConnecting.filter {
|
||||
if (bestRoadAvailable == RoadStatus.None) return listOf()
|
||||
val candidateCities = civInfo.cities.filter {
|
||||
// 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.
|
||||
|
||||
// Search through ALL candidate cities to build the cache
|
||||
for (toConnectCity in candidateCities) {
|
||||
getRoadConnectionBetweenCities(unit, toConnectCity).filter { it.getUnpillagedRoad() < bestRoadAvailable }
|
||||
}
|
||||
return candidateCities
|
||||
}
|
||||
|
||||
@ -148,35 +222,30 @@ class RoadBetweenCitiesAutomation(val civInfo: Civilization, cachedForTurn: Int,
|
||||
* @return whether we actually did anything
|
||||
*/
|
||||
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.
|
||||
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
|
||||
for (toConnectCity in candidateCities) {
|
||||
val roadableTiles = getRoadConnectionBetweenCities(unit, toConnectCity).filter { it.getUnpillagedRoad() < bestRoadAvailable }
|
||||
val reachableTile = roadableTiles.map { Pair(it, it.aerialDistanceTo(unit.getTile())) }
|
||||
.filter { it.second < bestTileToConstructRoadOnDist }
|
||||
.sortedBy { it.second }
|
||||
.firstOrNull {
|
||||
for (toConnectCity in candidateCities.sortedByDescending { it.getCenterTile().aerialDistanceTo(unit.getTile()) }) {
|
||||
val tilesByPriority = getRoadsToBuildFromCity(toConnectCity).flatMap { roadPlan -> roadPlan.tiles.map { tile -> Pair(tile, roadPlan.priority) } }
|
||||
val tilesSorted = tilesByPriority.filter { it.first.getUnpillagedRoad() < bestRoadAvailable }
|
||||
.sortedBy { it.first.aerialDistanceTo(unit.getTile()) + (it.second / 10f) }
|
||||
val bestTile = tilesSorted.firstOrNull {
|
||||
unit.movement.canMoveTo(it.first) && unit.movement.canReach(it.first)
|
||||
} ?: continue // Apparently we can't reach any of these tiles at all
|
||||
bestTileToConstructRoadOn = reachableTile.first
|
||||
bestTileToConstructRoadOnDist = reachableTile.second
|
||||
}?.first ?: continue // Apparently we can't reach any of these tiles at all
|
||||
|
||||
if (bestTile != currentTile && unit.currentMovement > 0)
|
||||
unit.movement.headTowards(bestTile)
|
||||
if (unit.currentMovement > 0 && bestTile == currentTile
|
||||
&& currentTile.improvementInProgress != bestRoadAvailable.name) {
|
||||
val improvement = bestRoadAvailable.improvement(civInfo.gameInfo.ruleset)!!
|
||||
bestTile.startWorkingOnImprovement(improvement, civInfo, unit)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if (bestTileToConstructRoadOn == null) return false
|
||||
|
||||
if (bestTileToConstructRoadOn != currentTile && unit.currentMovement > 0)
|
||||
unit.movement.headTowards(bestTileToConstructRoadOn)
|
||||
if (unit.currentMovement > 0 && bestTileToConstructRoadOn == currentTile
|
||||
&& currentTile.improvementInProgress != bestRoadAvailable.name) {
|
||||
val improvement = bestRoadAvailable.improvement(civInfo.gameInfo.ruleset)!!
|
||||
bestTileToConstructRoadOn.startWorkingOnImprovement(improvement, civInfo, unit)
|
||||
}
|
||||
return true
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -83,6 +83,12 @@ class WorkerAutomation(
|
||||
|
||||
if (tileToWork != currentTile) {
|
||||
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)
|
||||
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>()
|
||||
for (city in unit.civ.cities) {
|
||||
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()
|
||||
@ -176,7 +182,8 @@ class WorkerAutomation(
|
||||
val workableTilesCenterFirst = currentTile.getTilesInDistance(4)
|
||||
.filter {
|
||||
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.isCityCenter()
|
||||
&& 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.
|
||||
if (!tileHasWorkToDo(tileInGroup, unit)) continue
|
||||
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)) {
|
||||
bestTile = tileInGroup
|
||||
}
|
||||
@ -233,7 +240,7 @@ class WorkerAutomation(
|
||||
&& !civInfo.hasResource(tile.resource!!))
|
||||
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 <= 10 -> 1
|
||||
civInfo.stats.statsForNextTurn.gold <= 30 -> 2
|
||||
@ -373,14 +380,12 @@ class WorkerAutomation(
|
||||
|
||||
// Add the value of roads if we want to build it here
|
||||
if (improvement.isRoad() && roadBetweenCitiesAutomation.bestRoadAvailable.improvement(ruleSet) == improvement
|
||||
&& tile in roadBetweenCitiesAutomation.tilesOfRoadsToConnectCities) {
|
||||
var value = 1f
|
||||
val city = roadBetweenCitiesAutomation.tilesOfRoadsToConnectCities[tile]!!
|
||||
&& tile in roadBetweenCitiesAutomation.tilesOfRoadsMap) {
|
||||
val roadPlan = roadBetweenCitiesAutomation.tilesOfRoadsMap[tile]!!
|
||||
var value = roadPlan.priority
|
||||
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
|
||||
value += (5 - roadBetweenCitiesAutomation.roadsToConnectCitiesCache[city]!!.size).coerceAtLeast(0)
|
||||
value += (5 - roadPlan.numberOfRoadsToBuild).coerceAtLeast(0)
|
||||
return value
|
||||
}
|
||||
|
||||
|
@ -86,6 +86,11 @@ class City : IsPartOfGameInfoSerialization {
|
||||
var isPuppet = false
|
||||
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
|
||||
fun getCityFocus() = CityFocus.values().firstOrNull { it.name == cityAIFocus } ?: CityFocus.NoFocus
|
||||
fun setCityFocus(cityFocus: CityFocus){ cityAIFocus = cityFocus.name }
|
||||
|
@ -322,6 +322,12 @@ class CityStats(val city: City) {
|
||||
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 {
|
||||
// Same here - will have a different UI display.
|
||||
var buildingsMaintenance = city.cityConstructions.getMaintenanceCosts().toFloat() // this is AFTER the bonus calculation!
|
||||
|
@ -16,30 +16,23 @@ object MapPathing {
|
||||
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
|
||||
if ((to.hasRoadConnection(unit.civ, false) || to.hasRailroadConnection(false)))
|
||||
return .5f
|
||||
|
||||
return 1f
|
||||
}
|
||||
|
||||
fun isValidRoadPathTile(unit: MapUnit, tile: Tile): Boolean {
|
||||
val roadImprovement = tile.ruleset.roadImprovement ?: return false
|
||||
val railRoadImprovement = tile.ruleset.railroadImprovement ?: return false
|
||||
return tile.isLand
|
||||
&& !tile.isImpassible()
|
||||
&& unit.civ.hasExplored(tile)
|
||||
&& 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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -61,6 +61,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
// Connect roads implies automated is true. It is specified by the action type.
|
||||
var action: String? = null
|
||||
var automated: Boolean = false
|
||||
|
||||
// We can infer who we are escorting based on our tile
|
||||
var escorting: Boolean = false
|
||||
|
||||
@ -137,7 +138,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
* */
|
||||
class UnitMovementMemory(position: Vector2, val type: UnitMovementMemoryType) : IsPartOfGameInfoSerialization {
|
||||
@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)
|
||||
|
||||
fun clone() = UnitMovementMemory(position, type)
|
||||
@ -157,8 +159,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
*/
|
||||
fun displayName(): String {
|
||||
val baseName =
|
||||
if (instanceName == null) "[$name]"
|
||||
else "$instanceName ([$name])"
|
||||
if (instanceName == null) "[$name]"
|
||||
else "$instanceName ([$name])"
|
||||
|
||||
return if (religion == null) baseName
|
||||
else "$baseName ([${getReligionDisplayName()}])"
|
||||
@ -201,7 +203,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
get() = baseUnit.type
|
||||
|
||||
fun getMovementString(): String =
|
||||
DecimalFormat("0.#").format(currentMovement.toDouble()) + "/" + getMaxMovement()
|
||||
DecimalFormat("0.#").format(currentMovement.toDouble()) + "/" + getMaxMovement()
|
||||
|
||||
fun getTile(): Tile = currentTile
|
||||
|
||||
@ -247,8 +249,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
if (currentMovement == 0f) return false
|
||||
val tile = getTile()
|
||||
if (tile.improvementInProgress != null &&
|
||||
canBuildImprovement(tile.getTileImprovementInProgress()!!) &&
|
||||
!tile.isMarkedForCreatesOneImprovement()
|
||||
canBuildImprovement(tile.getTileImprovementInProgress()!!) &&
|
||||
!tile.isMarkedForCreatesOneImprovement()
|
||||
) return false
|
||||
if (includeOtherEscortUnit && isEscorting() && !getOtherEscortUnit()!!.isIdle(false)) return false
|
||||
return !(isFortified() || isExploring() || isSleeping() || isAutomated() || isMoving())
|
||||
@ -257,32 +259,32 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
fun getUniques(): Sequence<Unique> = tempUniquesMap.values.asSequence().flatten()
|
||||
|
||||
fun getMatchingUniques(
|
||||
uniqueType: UniqueType,
|
||||
stateForConditionals: StateForConditionals = StateForConditionals(civ, unit=this),
|
||||
checkCivInfoUniques: Boolean = false
|
||||
uniqueType: UniqueType,
|
||||
stateForConditionals: StateForConditionals = StateForConditionals(civ, unit = this),
|
||||
checkCivInfoUniques: Boolean = false
|
||||
) = sequence {
|
||||
yieldAll(
|
||||
yieldAll(
|
||||
tempUniquesMap.getMatchingUniques(uniqueType, stateForConditionals)
|
||||
)
|
||||
)
|
||||
if (checkCivInfoUniques)
|
||||
yieldAll(civ.getMatchingUniques(uniqueType, stateForConditionals))
|
||||
}
|
||||
|
||||
fun hasUnique(
|
||||
uniqueType: UniqueType,
|
||||
stateForConditionals: StateForConditionals = StateForConditionals(civ, unit=this),
|
||||
checkCivInfoUniques: Boolean = false
|
||||
uniqueType: UniqueType,
|
||||
stateForConditionals: StateForConditionals = StateForConditionals(civ, unit = this),
|
||||
checkCivInfoUniques: Boolean = false
|
||||
): Boolean {
|
||||
return getMatchingUniques(uniqueType, stateForConditionals, checkCivInfoUniques).any()
|
||||
}
|
||||
|
||||
fun getTriggeredUniques(
|
||||
trigger: UniqueType,
|
||||
stateForConditionals: StateForConditionals = StateForConditionals(civInfo = civ, unit = this)
|
||||
trigger: UniqueType,
|
||||
stateForConditionals: StateForConditionals = StateForConditionals(civInfo = civ, unit = this)
|
||||
): Sequence<Unique> {
|
||||
return getUniques().filter { unique ->
|
||||
unique.conditionals.any { it.type == trigger }
|
||||
&& unique.conditionalsApply(stateForConditionals)
|
||||
&& unique.conditionalsApply(stateForConditionals)
|
||||
}
|
||||
}
|
||||
|
||||
@ -306,11 +308,11 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
|
||||
fun getMaxMovement(): Int {
|
||||
var movement =
|
||||
if (isEmbarked()) 2
|
||||
else baseUnit.movement
|
||||
if (isEmbarked()) 2
|
||||
else baseUnit.movement
|
||||
|
||||
movement += getMatchingUniques(UniqueType.Movement, checkCivInfoUniques = true)
|
||||
.sumOf { it.params[0].toInt() }
|
||||
.sumOf { it.params[0].toInt() }
|
||||
|
||||
if (movement < 1) movement = 1
|
||||
|
||||
@ -320,7 +322,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
for (boostingUnit in currentTile.getUnits()) {
|
||||
if (boostingUnit == this) continue
|
||||
if (boostingUnit.getMatchingUniques(UniqueType.TransferMovement)
|
||||
.none { matchesFilter(it.params[0]) } ) continue
|
||||
.none { matchesFilter(it.params[0]) }) continue
|
||||
movement = movement.coerceAtLeast(boostingUnit.getMaxMovement())
|
||||
}
|
||||
|
||||
@ -352,7 +354,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
|
||||
fun maxAttacksPerTurn(): Int {
|
||||
return 1 + getMatchingUniques(UniqueType.AdditionalAttacks, checkCivInfoUniques = true)
|
||||
.sumOf { it.params[0].toInt() }
|
||||
.sumOf { it.params[0].toInt() }
|
||||
}
|
||||
|
||||
fun canAttack(): Boolean {
|
||||
@ -365,7 +367,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
if (baseUnit.isMelee()) return 1
|
||||
var range = baseUnit.range
|
||||
range += getMatchingUniques(UniqueType.Range, checkCivInfoUniques = true)
|
||||
.sumOf { it.params[0].toInt() }
|
||||
.sumOf { it.params[0].toInt() }
|
||||
return range
|
||||
}
|
||||
|
||||
@ -411,6 +413,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
hasUnique(UniqueType.HealOnlyByPillaging, checkCivInfoUniques = true) -> 0
|
||||
else -> rankTileForHealing(getTile())
|
||||
}
|
||||
|
||||
fun canHealInCurrentTile() = getHealAmountForCurrentTile() > 0
|
||||
|
||||
/** Returns the health points [MapUnit] will receive if healing on [tile] */
|
||||
@ -445,8 +448,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
}
|
||||
|
||||
val maxAdjacentHealingBonus = currentTile.neighbors
|
||||
.flatMap { it.getUnits() }.filter { it.civ == civ }
|
||||
.map { it.adjacentHealingBonus() }.maxOrNull()
|
||||
.flatMap { it.getUnits() }.filter { it.civ == civ }
|
||||
.map { it.adjacentHealingBonus() }.maxOrNull()
|
||||
if (maxAdjacentHealingBonus != null)
|
||||
healing += maxAdjacentHealingBonus
|
||||
|
||||
@ -467,7 +470,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
|
||||
fun getInterceptionRange(): Int {
|
||||
val rangeFromUniques = getMatchingUniques(UniqueType.AirInterceptionRange, checkCivInfoUniques = true)
|
||||
.sumOf { it.params[0].toInt() }
|
||||
.sumOf { it.params[0].toInt() }
|
||||
return baseUnit.interceptRange + rangeFromUniques
|
||||
}
|
||||
|
||||
@ -476,8 +479,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
// Air Units can only Intercept if they didn't move this turn
|
||||
if (baseUnit.isAirUnit() && currentMovement == 0f) return false
|
||||
val maxAttacksPerTurn = 1 +
|
||||
getMatchingUniques(UniqueType.ExtraInterceptionsPerTurn)
|
||||
.sumOf { it.params[0].toInt() }
|
||||
getMatchingUniques(UniqueType.ExtraInterceptionsPerTurn)
|
||||
.sumOf { it.params[0].toInt() }
|
||||
if (attacksThisTurn >= maxAttacksPerTurn) return false
|
||||
return true
|
||||
}
|
||||
@ -488,7 +491,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
|
||||
fun interceptDamagePercentBonus(): Int {
|
||||
return getMatchingUniques(UniqueType.DamageWhenIntercepting)
|
||||
.sumOf { it.params[0].toInt() }
|
||||
.sumOf { it.params[0].toInt() }
|
||||
}
|
||||
|
||||
fun receivedInterceptDamageFactor(): Float {
|
||||
@ -499,7 +502,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
}
|
||||
|
||||
fun getDamageFromTerrain(tile: Tile = currentTile): Int {
|
||||
return tile.allTerrains.sumOf { it.damagePerTurn }
|
||||
return tile.allTerrains.sumOf { it.damagePerTurn }
|
||||
}
|
||||
|
||||
fun isTransportTypeOf(mapUnit: MapUnit): Boolean {
|
||||
@ -510,9 +513,9 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
|
||||
private fun carryCapacity(unit: MapUnit): Int {
|
||||
return (getMatchingUniques(UniqueType.CarryAirUnits)
|
||||
+ getMatchingUniques(UniqueType.CarryExtraAirUnits))
|
||||
.filter { unit.matchesFilter(it.params[1]) }
|
||||
.sumOf { it.params[0].toInt() }
|
||||
+ getMatchingUniques(UniqueType.CarryExtraAirUnits))
|
||||
.filter { unit.matchesFilter(it.params[1]) }
|
||||
.sumOf { it.params[0].toInt() }
|
||||
}
|
||||
|
||||
fun canTransport(unit: MapUnit): Boolean {
|
||||
@ -525,13 +528,13 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
|
||||
/** Gets a Nuke's blast radius from the BlastRadius unique, defaulting to 2. No check whether the unit actually is a Nuke. */
|
||||
fun getNukeBlastRadius() = getMatchingUniques(UniqueType.BlastRadius)
|
||||
// Don't check conditionals as these are not supported
|
||||
.firstOrNull()?.params?.get(0)?.toInt() ?: 2
|
||||
// Don't check conditionals as these are not supported
|
||||
.firstOrNull()?.params?.get(0)?.toInt() ?: 2
|
||||
|
||||
private fun isAlly(otherCiv: Civilization): Boolean {
|
||||
return otherCiv == civ
|
||||
|| (otherCiv.isCityState() && otherCiv.getAllyCiv() == civ.civName)
|
||||
|| (civ.isCityState() && civ.getAllyCiv() == otherCiv.civName)
|
||||
|| (otherCiv.isCityState() && otherCiv.getAllyCiv() == civ.civName)
|
||||
|| (civ.isCityState() && civ.getAllyCiv() == otherCiv.civName)
|
||||
}
|
||||
|
||||
/** Implements [UniqueParameterType.MapUnitFilter][com.unciv.models.ruleset.unique.UniqueParameterType.MapUnitFilter] */
|
||||
@ -559,8 +562,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
// Workers (and similar) should never be able to (instantly) construct things, only build them
|
||||
// HOWEVER, they should be able to repair such things if they are pillaged
|
||||
if (improvement.turnsToBuild == -1
|
||||
&& improvement.name != Constants.cancelImprovementOrder
|
||||
&& tile.improvementInProgress != improvement.name
|
||||
&& improvement.name != Constants.cancelImprovementOrder
|
||||
&& tile.improvementInProgress != improvement.name
|
||||
) return false
|
||||
val buildImprovementUniques = getMatchingUniques(UniqueType.BuildImprovements)
|
||||
if (tile.improvementInProgress == Constants.repair) {
|
||||
@ -568,7 +571,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
return buildImprovementUniques.any()
|
||||
}
|
||||
return buildImprovementUniques
|
||||
.any { improvement.matchesFilter(it.params[0]) || tile.matchesTerrainFilter(it.params[0]) }
|
||||
.any { improvement.matchesFilter(it.params[0]) || tile.matchesTerrainFilter(it.params[0]) }
|
||||
}
|
||||
|
||||
fun getReligionDisplayName(): String? {
|
||||
@ -585,6 +588,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
}
|
||||
|
||||
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 (isMilitary()) return getTile().civilianUnit
|
||||
return null
|
||||
@ -613,7 +617,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
fun setTransients(ruleset: Ruleset) {
|
||||
promotions.setTransients(this)
|
||||
baseUnit = ruleset.units[name]
|
||||
?: throw java.lang.Exception("Unit $name is not found!")
|
||||
?: throw java.lang.Exception("Unit $name is not found!")
|
||||
|
||||
updateUniques()
|
||||
if (action == UnitActionType.Automate.value) automated = true
|
||||
@ -621,9 +625,9 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
|
||||
fun updateUniques() {
|
||||
val uniqueSources =
|
||||
baseUnit.uniqueObjects.asSequence() +
|
||||
type.uniqueObjects +
|
||||
promotions.getPromotions().flatMap { it.uniqueObjects }
|
||||
baseUnit.uniqueObjects.asSequence() +
|
||||
type.uniqueObjects +
|
||||
promotions.getPromotions().flatMap { it.uniqueObjects }
|
||||
tempUniquesMap = UniqueMap(uniqueSources)
|
||||
cache.updateUniques()
|
||||
}
|
||||
@ -649,7 +653,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
/**
|
||||
* 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
|
||||
|
||||
viewableTiles = when {
|
||||
@ -661,9 +665,9 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
|
||||
// Set equality automatically determines if anything changed - https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-abstract-set/equals.html
|
||||
if (updateCivViewableTiles && oldViewableTiles != viewableTiles
|
||||
// Don't bother updating if all previous and current viewable tiles are within our borders
|
||||
&& (oldViewableTiles.any { it !in civ.cache.ourTilesAndNeighboringTiles }
|
||||
|| viewableTiles.any { it !in civ.cache.ourTilesAndNeighboringTiles })) {
|
||||
// Don't bother updating if all previous and current viewable tiles are within our borders
|
||||
&& (oldViewableTiles.any { it !in civ.cache.ourTilesAndNeighboringTiles }
|
||||
|| viewableTiles.any { it !in civ.cache.ourTilesAndNeighboringTiles })) {
|
||||
|
||||
val unfilteredTriggeredUniques = getTriggeredUniques(UniqueType.TriggerUponDiscoveringTile, StateForConditionals.IgnoreConditionals).toList()
|
||||
if (unfilteredTriggeredUniques.isNotEmpty()) {
|
||||
@ -672,12 +676,12 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
}
|
||||
for (tile in newlyExploredTiles) {
|
||||
// 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) {
|
||||
if (unique.conditionals.any {
|
||||
it.type == UniqueType.TriggerUponDiscoveringTile
|
||||
&& tile.matchesFilter(it.params[0], civ)
|
||||
} && unique.conditionalsApply(state)
|
||||
it.type == UniqueType.TriggerUponDiscoveringTile
|
||||
&& tile.matchesFilter(it.params[0], civ)
|
||||
} && unique.conditionalsApply(state)
|
||||
)
|
||||
UniqueTriggerActivation.triggerUnique(unique, this)
|
||||
}
|
||||
@ -712,7 +716,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
if (isEscorting() && getOtherEscortUnit()!!.currentMovement == 0f) return
|
||||
|
||||
val enemyUnitsInWalkingDistance = movement.getDistanceToTiles().keys
|
||||
.filter { it.militaryUnit != null && civ.isAtWarWith(it.militaryUnit!!.civ) }
|
||||
.filter { it.militaryUnit != null && civ.isAtWarWith(it.militaryUnit!!.civ) }
|
||||
if (enemyUnitsInWalkingDistance.isNotEmpty()) {
|
||||
if (isMoving()) // stop on enemy in sight
|
||||
action = null
|
||||
@ -731,7 +735,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
val gotTo = movement.headTowards(destinationTile)
|
||||
if (gotTo == currentTile) { // We didn't move at all
|
||||
// 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
|
||||
return
|
||||
}
|
||||
@ -747,8 +751,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
|
||||
fun healBy(amount: Int) {
|
||||
health += amount *
|
||||
if (hasUnique(UniqueType.HealingEffectsDoubled, checkCivInfoUniques = true)) 2
|
||||
else 1
|
||||
if (hasUnique(UniqueType.HealingEffectsDoubled, checkCivInfoUniques = true)) 2
|
||||
else 1
|
||||
if (health > 100) health = 100
|
||||
cache.updateUniques()
|
||||
}
|
||||
@ -763,18 +767,21 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
|
||||
fun destroy(destroyTransportedUnit: Boolean = true) {
|
||||
stopEscorting()
|
||||
val currentPosition = Vector2(getTile().position)
|
||||
civ.attacksSinceTurnStart.addAll(attacksSinceTurnStart.asSequence().map { Civilization.HistoricalAttackMemory(this.name, currentPosition, it) })
|
||||
currentMovement = 0f
|
||||
removeFromTile()
|
||||
civ.units.removeUnit(this)
|
||||
civ.cache.updateViewableTiles()
|
||||
if (destroyTransportedUnit) {
|
||||
// all transported units should be destroyed as well
|
||||
currentTile.getUnits().filter { it.isTransported && isTransportTypeOf(it) }
|
||||
.toList() // because we're changing the list
|
||||
.forEach { unit -> unit.destroy() }
|
||||
if (::currentTile.isInitialized) {
|
||||
val currentPosition = Vector2(getTile().position)
|
||||
civ.attacksSinceTurnStart.addAll(attacksSinceTurnStart.asSequence().map { Civilization.HistoricalAttackMemory(this.name, currentPosition, it) })
|
||||
removeFromTile()
|
||||
civ.cache.updateViewableTiles()
|
||||
if (destroyTransportedUnit) {
|
||||
// all transported units should be destroyed as well
|
||||
currentTile.getUnits().filter { it.isTransported && isTransportTypeOf(it) }
|
||||
.toList() // because we're changing the list
|
||||
.forEach { unit -> unit.destroy() }
|
||||
}
|
||||
}
|
||||
|
||||
isDestroyed = true
|
||||
}
|
||||
|
||||
@ -784,7 +791,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
civ.cache.updateViewableTiles()
|
||||
// all transported units should be gift as well
|
||||
currentTile.getUnits().filter { it.isTransported && isTransportTypeOf(it) }
|
||||
.forEach { unit -> unit.gift(recipient) }
|
||||
.forEach { unit -> unit.gift(recipient) }
|
||||
assignOwner(recipient)
|
||||
recipient.cache.updateViewableTiles()
|
||||
}
|
||||
@ -817,7 +824,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
civ.addStat(stat.key, stat.value.toInt())
|
||||
|
||||
civ.addNotification("By expending your [$name] you gained [${gainedStats.toStringForNotifications()}]!",
|
||||
getTile().position, NotificationCategory.Units, name)
|
||||
getTile().position, NotificationCategory.Units, name)
|
||||
}
|
||||
|
||||
fun removeFromTile() = currentTile.removeUnit(this)
|
||||
@ -855,8 +862,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
}
|
||||
|
||||
val promotionUniques = tile.neighbors
|
||||
.flatMap { it.allTerrains }
|
||||
.flatMap { it.getMatchingUniques(UniqueType.TerrainGrantsPromotion) }
|
||||
.flatMap { it.allTerrains }
|
||||
.flatMap { it.getMatchingUniques(UniqueType.TerrainGrantsPromotion) }
|
||||
for (unique in promotionUniques) {
|
||||
if (!this.matchesFilter(unique.params[2])) continue
|
||||
val promotion = unique.params[0]
|
||||
@ -871,6 +878,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
when {
|
||||
!movement.canMoveTo(tile) ->
|
||||
throw Exception("Unit $name of ${civ.civName} at $currentTile can't be put in tile $tile!")
|
||||
|
||||
baseUnit.movesLikeAirUnits() -> tile.airUnits.add(this)
|
||||
isCivilian() -> tile.civilianUnit = this
|
||||
else -> tile.militaryUnit = this
|
||||
@ -894,7 +902,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
fun stopEscorting() {
|
||||
getOtherEscortUnit()?.escorting = false
|
||||
escorting = false
|
||||
movement.clearPathfindingCache()
|
||||
if (::currentTile.isInitialized) // Can cause an error if the unit has not been placed on the map yet.
|
||||
movement.clearPathfindingCache()
|
||||
}
|
||||
|
||||
private fun clearEncampment(tile: Tile) {
|
||||
@ -902,27 +911,27 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
|
||||
// Notify City-States that this unit cleared a Barbarian Encampment, required for quests
|
||||
civ.gameInfo.getAliveCityStates()
|
||||
.forEach { it.questManager.barbarianCampCleared(civ, tile.position) }
|
||||
.forEach { it.questManager.barbarianCampCleared(civ, tile.position) }
|
||||
|
||||
var goldGained =
|
||||
civ.getDifficulty().clearBarbarianCampReward * civ.gameInfo.speed.goldCostModifier
|
||||
civ.getDifficulty().clearBarbarianCampReward * civ.gameInfo.speed.goldCostModifier
|
||||
if (civ.hasUnique(UniqueType.TripleGoldFromEncampmentsAndCities))
|
||||
goldGained *= 3f
|
||||
|
||||
civ.addGold(goldGained.toInt())
|
||||
civ.addNotification(
|
||||
"We have captured a barbarian encampment and recovered [${goldGained.toInt()}] gold!",
|
||||
tile.position,
|
||||
NotificationCategory.War,
|
||||
NotificationIcon.Gold
|
||||
"We have captured a barbarian encampment and recovered [${goldGained.toInt()}] gold!",
|
||||
tile.position,
|
||||
NotificationCategory.War,
|
||||
NotificationIcon.Gold
|
||||
)
|
||||
}
|
||||
|
||||
fun disband() {
|
||||
// evacuation of transported units before disbanding, if possible. toListed because we're modifying the unit list.
|
||||
for (unit in currentTile.getUnits()
|
||||
.filter { it.isTransported && isTransportTypeOf(it) }
|
||||
.toList()
|
||||
.filter { it.isTransported && isTransportTypeOf(it) }
|
||||
.toList()
|
||||
) {
|
||||
// if we disbanded a unit carrying other units in a city, the carried units can still stay in the city
|
||||
if (currentTile.isCityCenter() && unit.movement.canMoveTo(currentTile)) {
|
||||
@ -934,7 +943,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
||||
unit.disband()
|
||||
// let's find closest city or another carrier where it can be evacuated
|
||||
val tileCanMoveTo = unit.currentTile.getTilesInDistance(unit.getMaxMovementForAirUnits())
|
||||
.filterNot { it == currentTile }.firstOrNull { unit.movement.canMoveTo(it) }
|
||||
.filterNot { it == currentTile }.firstOrNull { unit.movement.canMoveTo(it) }
|
||||
|
||||
if (tileCanMoveTo != null)
|
||||
unit.movement.moveToTile(tileCanMoveTo)
|
||||
|
Loading…
x
Reference in New Issue
Block a user