mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-26 21:35:14 -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.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 (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
|
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 }
|
||||||
|
@ -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!
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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)
|
||||||
@ -157,8 +159,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
*/
|
*/
|
||||||
fun displayName(): String {
|
fun displayName(): String {
|
||||||
val baseName =
|
val baseName =
|
||||||
if (instanceName == null) "[$name]"
|
if (instanceName == null) "[$name]"
|
||||||
else "$instanceName ([$name])"
|
else "$instanceName ([$name])"
|
||||||
|
|
||||||
return if (religion == null) baseName
|
return if (religion == null) baseName
|
||||||
else "$baseName ([${getReligionDisplayName()}])"
|
else "$baseName ([${getReligionDisplayName()}])"
|
||||||
@ -201,7 +203,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
get() = baseUnit.type
|
get() = baseUnit.type
|
||||||
|
|
||||||
fun getMovementString(): String =
|
fun getMovementString(): String =
|
||||||
DecimalFormat("0.#").format(currentMovement.toDouble()) + "/" + getMaxMovement()
|
DecimalFormat("0.#").format(currentMovement.toDouble()) + "/" + getMaxMovement()
|
||||||
|
|
||||||
fun getTile(): Tile = currentTile
|
fun getTile(): Tile = currentTile
|
||||||
|
|
||||||
@ -247,8 +249,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
if (currentMovement == 0f) return false
|
if (currentMovement == 0f) return false
|
||||||
val tile = getTile()
|
val tile = getTile()
|
||||||
if (tile.improvementInProgress != null &&
|
if (tile.improvementInProgress != null &&
|
||||||
canBuildImprovement(tile.getTileImprovementInProgress()!!) &&
|
canBuildImprovement(tile.getTileImprovementInProgress()!!) &&
|
||||||
!tile.isMarkedForCreatesOneImprovement()
|
!tile.isMarkedForCreatesOneImprovement()
|
||||||
) return false
|
) return false
|
||||||
if (includeOtherEscortUnit && isEscorting() && !getOtherEscortUnit()!!.isIdle(false)) return false
|
if (includeOtherEscortUnit && isEscorting() && !getOtherEscortUnit()!!.isIdle(false)) return false
|
||||||
return !(isFortified() || isExploring() || isSleeping() || isAutomated() || isMoving())
|
return !(isFortified() || isExploring() || isSleeping() || isAutomated() || isMoving())
|
||||||
@ -257,32 +259,32 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
fun getUniques(): Sequence<Unique> = tempUniquesMap.values.asSequence().flatten()
|
fun getUniques(): Sequence<Unique> = tempUniquesMap.values.asSequence().flatten()
|
||||||
|
|
||||||
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(
|
||||||
tempUniquesMap.getMatchingUniques(uniqueType, stateForConditionals)
|
tempUniquesMap.getMatchingUniques(uniqueType, stateForConditionals)
|
||||||
)
|
)
|
||||||
if (checkCivInfoUniques)
|
if (checkCivInfoUniques)
|
||||||
yieldAll(civ.getMatchingUniques(uniqueType, stateForConditionals))
|
yieldAll(civ.getMatchingUniques(uniqueType, stateForConditionals))
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getTriggeredUniques(
|
fun getTriggeredUniques(
|
||||||
trigger: UniqueType,
|
trigger: UniqueType,
|
||||||
stateForConditionals: StateForConditionals = StateForConditionals(civInfo = civ, unit = this)
|
stateForConditionals: StateForConditionals = StateForConditionals(civInfo = civ, unit = this)
|
||||||
): Sequence<Unique> {
|
): Sequence<Unique> {
|
||||||
return getUniques().filter { unique ->
|
return getUniques().filter { unique ->
|
||||||
unique.conditionals.any { it.type == trigger }
|
unique.conditionals.any { it.type == trigger }
|
||||||
&& unique.conditionalsApply(stateForConditionals)
|
&& unique.conditionalsApply(stateForConditionals)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,11 +308,11 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
|
|
||||||
fun getMaxMovement(): Int {
|
fun getMaxMovement(): Int {
|
||||||
var movement =
|
var movement =
|
||||||
if (isEmbarked()) 2
|
if (isEmbarked()) 2
|
||||||
else baseUnit.movement
|
else baseUnit.movement
|
||||||
|
|
||||||
movement += getMatchingUniques(UniqueType.Movement, checkCivInfoUniques = true)
|
movement += getMatchingUniques(UniqueType.Movement, checkCivInfoUniques = true)
|
||||||
.sumOf { it.params[0].toInt() }
|
.sumOf { it.params[0].toInt() }
|
||||||
|
|
||||||
if (movement < 1) movement = 1
|
if (movement < 1) movement = 1
|
||||||
|
|
||||||
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,7 +354,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
|
|
||||||
fun maxAttacksPerTurn(): Int {
|
fun maxAttacksPerTurn(): Int {
|
||||||
return 1 + getMatchingUniques(UniqueType.AdditionalAttacks, checkCivInfoUniques = true)
|
return 1 + getMatchingUniques(UniqueType.AdditionalAttacks, checkCivInfoUniques = true)
|
||||||
.sumOf { it.params[0].toInt() }
|
.sumOf { it.params[0].toInt() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun canAttack(): Boolean {
|
fun canAttack(): Boolean {
|
||||||
@ -365,7 +367,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
if (baseUnit.isMelee()) return 1
|
if (baseUnit.isMelee()) return 1
|
||||||
var range = baseUnit.range
|
var range = baseUnit.range
|
||||||
range += getMatchingUniques(UniqueType.Range, checkCivInfoUniques = true)
|
range += getMatchingUniques(UniqueType.Range, checkCivInfoUniques = true)
|
||||||
.sumOf { it.params[0].toInt() }
|
.sumOf { it.params[0].toInt() }
|
||||||
return range
|
return range
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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] */
|
||||||
@ -445,8 +448,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val maxAdjacentHealingBonus = currentTile.neighbors
|
val maxAdjacentHealingBonus = currentTile.neighbors
|
||||||
.flatMap { it.getUnits() }.filter { it.civ == civ }
|
.flatMap { it.getUnits() }.filter { it.civ == civ }
|
||||||
.map { it.adjacentHealingBonus() }.maxOrNull()
|
.map { it.adjacentHealingBonus() }.maxOrNull()
|
||||||
if (maxAdjacentHealingBonus != null)
|
if (maxAdjacentHealingBonus != null)
|
||||||
healing += maxAdjacentHealingBonus
|
healing += maxAdjacentHealingBonus
|
||||||
|
|
||||||
@ -467,7 +470,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
|
|
||||||
fun getInterceptionRange(): Int {
|
fun getInterceptionRange(): Int {
|
||||||
val rangeFromUniques = getMatchingUniques(UniqueType.AirInterceptionRange, checkCivInfoUniques = true)
|
val rangeFromUniques = getMatchingUniques(UniqueType.AirInterceptionRange, checkCivInfoUniques = true)
|
||||||
.sumOf { it.params[0].toInt() }
|
.sumOf { it.params[0].toInt() }
|
||||||
return baseUnit.interceptRange + rangeFromUniques
|
return baseUnit.interceptRange + rangeFromUniques
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -476,8 +479,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
// Air Units can only Intercept if they didn't move this turn
|
// Air Units can only Intercept if they didn't move this turn
|
||||||
if (baseUnit.isAirUnit() && currentMovement == 0f) return false
|
if (baseUnit.isAirUnit() && currentMovement == 0f) return false
|
||||||
val maxAttacksPerTurn = 1 +
|
val maxAttacksPerTurn = 1 +
|
||||||
getMatchingUniques(UniqueType.ExtraInterceptionsPerTurn)
|
getMatchingUniques(UniqueType.ExtraInterceptionsPerTurn)
|
||||||
.sumOf { it.params[0].toInt() }
|
.sumOf { it.params[0].toInt() }
|
||||||
if (attacksThisTurn >= maxAttacksPerTurn) return false
|
if (attacksThisTurn >= maxAttacksPerTurn) return false
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -488,7 +491,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
|
|
||||||
fun interceptDamagePercentBonus(): Int {
|
fun interceptDamagePercentBonus(): Int {
|
||||||
return getMatchingUniques(UniqueType.DamageWhenIntercepting)
|
return getMatchingUniques(UniqueType.DamageWhenIntercepting)
|
||||||
.sumOf { it.params[0].toInt() }
|
.sumOf { it.params[0].toInt() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun receivedInterceptDamageFactor(): Float {
|
fun receivedInterceptDamageFactor(): Float {
|
||||||
@ -499,7 +502,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getDamageFromTerrain(tile: Tile = currentTile): Int {
|
fun getDamageFromTerrain(tile: Tile = currentTile): Int {
|
||||||
return tile.allTerrains.sumOf { it.damagePerTurn }
|
return tile.allTerrains.sumOf { it.damagePerTurn }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isTransportTypeOf(mapUnit: MapUnit): Boolean {
|
fun isTransportTypeOf(mapUnit: MapUnit): Boolean {
|
||||||
@ -510,9 +513,9 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
|
|
||||||
private fun carryCapacity(unit: MapUnit): Int {
|
private fun carryCapacity(unit: MapUnit): Int {
|
||||||
return (getMatchingUniques(UniqueType.CarryAirUnits)
|
return (getMatchingUniques(UniqueType.CarryAirUnits)
|
||||||
+ getMatchingUniques(UniqueType.CarryExtraAirUnits))
|
+ getMatchingUniques(UniqueType.CarryExtraAirUnits))
|
||||||
.filter { unit.matchesFilter(it.params[1]) }
|
.filter { unit.matchesFilter(it.params[1]) }
|
||||||
.sumOf { it.params[0].toInt() }
|
.sumOf { it.params[0].toInt() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun canTransport(unit: MapUnit): Boolean {
|
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. */
|
/** 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)
|
fun getNukeBlastRadius() = getMatchingUniques(UniqueType.BlastRadius)
|
||||||
// Don't check conditionals as these are not supported
|
// Don't check conditionals as these are not supported
|
||||||
.firstOrNull()?.params?.get(0)?.toInt() ?: 2
|
.firstOrNull()?.params?.get(0)?.toInt() ?: 2
|
||||||
|
|
||||||
private fun isAlly(otherCiv: Civilization): Boolean {
|
private fun isAlly(otherCiv: Civilization): Boolean {
|
||||||
return otherCiv == civ
|
return otherCiv == civ
|
||||||
|| (otherCiv.isCityState() && otherCiv.getAllyCiv() == civ.civName)
|
|| (otherCiv.isCityState() && otherCiv.getAllyCiv() == civ.civName)
|
||||||
|| (civ.isCityState() && civ.getAllyCiv() == otherCiv.civName)
|
|| (civ.isCityState() && civ.getAllyCiv() == otherCiv.civName)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Implements [UniqueParameterType.MapUnitFilter][com.unciv.models.ruleset.unique.UniqueParameterType.MapUnitFilter] */
|
/** 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
|
// 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
|
// HOWEVER, they should be able to repair such things if they are pillaged
|
||||||
if (improvement.turnsToBuild == -1
|
if (improvement.turnsToBuild == -1
|
||||||
&& improvement.name != Constants.cancelImprovementOrder
|
&& improvement.name != Constants.cancelImprovementOrder
|
||||||
&& tile.improvementInProgress != improvement.name
|
&& tile.improvementInProgress != improvement.name
|
||||||
) return false
|
) return false
|
||||||
val buildImprovementUniques = getMatchingUniques(UniqueType.BuildImprovements)
|
val buildImprovementUniques = getMatchingUniques(UniqueType.BuildImprovements)
|
||||||
if (tile.improvementInProgress == Constants.repair) {
|
if (tile.improvementInProgress == Constants.repair) {
|
||||||
@ -568,7 +571,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
return buildImprovementUniques.any()
|
return buildImprovementUniques.any()
|
||||||
}
|
}
|
||||||
return buildImprovementUniques
|
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? {
|
fun getReligionDisplayName(): String? {
|
||||||
@ -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
|
||||||
@ -613,7 +617,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
fun setTransients(ruleset: Ruleset) {
|
fun setTransients(ruleset: Ruleset) {
|
||||||
promotions.setTransients(this)
|
promotions.setTransients(this)
|
||||||
baseUnit = ruleset.units[name]
|
baseUnit = ruleset.units[name]
|
||||||
?: throw java.lang.Exception("Unit $name is not found!")
|
?: throw java.lang.Exception("Unit $name is not found!")
|
||||||
|
|
||||||
updateUniques()
|
updateUniques()
|
||||||
if (action == UnitActionType.Automate.value) automated = true
|
if (action == UnitActionType.Automate.value) automated = true
|
||||||
@ -621,9 +625,9 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
|
|
||||||
fun updateUniques() {
|
fun updateUniques() {
|
||||||
val uniqueSources =
|
val uniqueSources =
|
||||||
baseUnit.uniqueObjects.asSequence() +
|
baseUnit.uniqueObjects.asSequence() +
|
||||||
type.uniqueObjects +
|
type.uniqueObjects +
|
||||||
promotions.getPromotions().flatMap { it.uniqueObjects }
|
promotions.getPromotions().flatMap { it.uniqueObjects }
|
||||||
tempUniquesMap = UniqueMap(uniqueSources)
|
tempUniquesMap = UniqueMap(uniqueSources)
|
||||||
cache.updateUniques()
|
cache.updateUniques()
|
||||||
}
|
}
|
||||||
@ -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 {
|
||||||
@ -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
|
// Set equality automatically determines if anything changed - https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-abstract-set/equals.html
|
||||||
if (updateCivViewableTiles && oldViewableTiles != viewableTiles
|
if (updateCivViewableTiles && oldViewableTiles != viewableTiles
|
||||||
// Don't bother updating if all previous and current viewable tiles are within our borders
|
// Don't bother updating if all previous and current viewable tiles are within our borders
|
||||||
&& (oldViewableTiles.any { it !in civ.cache.ourTilesAndNeighboringTiles }
|
&& (oldViewableTiles.any { it !in civ.cache.ourTilesAndNeighboringTiles }
|
||||||
|| viewableTiles.any { it !in civ.cache.ourTilesAndNeighboringTiles })) {
|
|| viewableTiles.any { it !in civ.cache.ourTilesAndNeighboringTiles })) {
|
||||||
|
|
||||||
val unfilteredTriggeredUniques = getTriggeredUniques(UniqueType.TriggerUponDiscoveringTile, StateForConditionals.IgnoreConditionals).toList()
|
val unfilteredTriggeredUniques = getTriggeredUniques(UniqueType.TriggerUponDiscoveringTile, StateForConditionals.IgnoreConditionals).toList()
|
||||||
if (unfilteredTriggeredUniques.isNotEmpty()) {
|
if (unfilteredTriggeredUniques.isNotEmpty()) {
|
||||||
@ -672,12 +676,12 @@ 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
|
||||||
&& tile.matchesFilter(it.params[0], civ)
|
&& tile.matchesFilter(it.params[0], civ)
|
||||||
} && unique.conditionalsApply(state)
|
} && unique.conditionalsApply(state)
|
||||||
)
|
)
|
||||||
UniqueTriggerActivation.triggerUnique(unique, this)
|
UniqueTriggerActivation.triggerUnique(unique, this)
|
||||||
}
|
}
|
||||||
@ -712,7 +716,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
if (isEscorting() && getOtherEscortUnit()!!.currentMovement == 0f) return
|
if (isEscorting() && getOtherEscortUnit()!!.currentMovement == 0f) return
|
||||||
|
|
||||||
val enemyUnitsInWalkingDistance = movement.getDistanceToTiles().keys
|
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 (enemyUnitsInWalkingDistance.isNotEmpty()) {
|
||||||
if (isMoving()) // stop on enemy in sight
|
if (isMoving()) // stop on enemy in sight
|
||||||
action = null
|
action = null
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
@ -747,8 +751,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
|
|
||||||
fun healBy(amount: Int) {
|
fun healBy(amount: Int) {
|
||||||
health += amount *
|
health += amount *
|
||||||
if (hasUnique(UniqueType.HealingEffectsDoubled, checkCivInfoUniques = true)) 2
|
if (hasUnique(UniqueType.HealingEffectsDoubled, checkCivInfoUniques = true)) 2
|
||||||
else 1
|
else 1
|
||||||
if (health > 100) health = 100
|
if (health > 100) health = 100
|
||||||
cache.updateUniques()
|
cache.updateUniques()
|
||||||
}
|
}
|
||||||
@ -763,18 +767,21 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
|
|
||||||
fun destroy(destroyTransportedUnit: Boolean = true) {
|
fun destroy(destroyTransportedUnit: Boolean = true) {
|
||||||
stopEscorting()
|
stopEscorting()
|
||||||
val currentPosition = Vector2(getTile().position)
|
|
||||||
civ.attacksSinceTurnStart.addAll(attacksSinceTurnStart.asSequence().map { Civilization.HistoricalAttackMemory(this.name, currentPosition, it) })
|
|
||||||
currentMovement = 0f
|
currentMovement = 0f
|
||||||
removeFromTile()
|
|
||||||
civ.units.removeUnit(this)
|
civ.units.removeUnit(this)
|
||||||
civ.cache.updateViewableTiles()
|
if (::currentTile.isInitialized) {
|
||||||
if (destroyTransportedUnit) {
|
val currentPosition = Vector2(getTile().position)
|
||||||
// all transported units should be destroyed as well
|
civ.attacksSinceTurnStart.addAll(attacksSinceTurnStart.asSequence().map { Civilization.HistoricalAttackMemory(this.name, currentPosition, it) })
|
||||||
currentTile.getUnits().filter { it.isTransported && isTransportTypeOf(it) }
|
removeFromTile()
|
||||||
.toList() // because we're changing the list
|
civ.cache.updateViewableTiles()
|
||||||
.forEach { unit -> unit.destroy() }
|
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
|
isDestroyed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -784,7 +791,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
civ.cache.updateViewableTiles()
|
civ.cache.updateViewableTiles()
|
||||||
// all transported units should be gift as well
|
// all transported units should be gift as well
|
||||||
currentTile.getUnits().filter { it.isTransported && isTransportTypeOf(it) }
|
currentTile.getUnits().filter { it.isTransported && isTransportTypeOf(it) }
|
||||||
.forEach { unit -> unit.gift(recipient) }
|
.forEach { unit -> unit.gift(recipient) }
|
||||||
assignOwner(recipient)
|
assignOwner(recipient)
|
||||||
recipient.cache.updateViewableTiles()
|
recipient.cache.updateViewableTiles()
|
||||||
}
|
}
|
||||||
@ -817,7 +824,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
civ.addStat(stat.key, stat.value.toInt())
|
civ.addStat(stat.key, stat.value.toInt())
|
||||||
|
|
||||||
civ.addNotification("By expending your [$name] you gained [${gainedStats.toStringForNotifications()}]!",
|
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)
|
fun removeFromTile() = currentTile.removeUnit(this)
|
||||||
@ -855,8 +862,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val promotionUniques = tile.neighbors
|
val promotionUniques = tile.neighbors
|
||||||
.flatMap { it.allTerrains }
|
.flatMap { it.allTerrains }
|
||||||
.flatMap { it.getMatchingUniques(UniqueType.TerrainGrantsPromotion) }
|
.flatMap { it.getMatchingUniques(UniqueType.TerrainGrantsPromotion) }
|
||||||
for (unique in promotionUniques) {
|
for (unique in promotionUniques) {
|
||||||
if (!this.matchesFilter(unique.params[2])) continue
|
if (!this.matchesFilter(unique.params[2])) continue
|
||||||
val promotion = unique.params[0]
|
val promotion = unique.params[0]
|
||||||
@ -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,7 +902,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
fun stopEscorting() {
|
fun stopEscorting() {
|
||||||
getOtherEscortUnit()?.escorting = false
|
getOtherEscortUnit()?.escorting = false
|
||||||
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) {
|
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
|
// Notify City-States that this unit cleared a Barbarian Encampment, required for quests
|
||||||
civ.gameInfo.getAliveCityStates()
|
civ.gameInfo.getAliveCityStates()
|
||||||
.forEach { it.questManager.barbarianCampCleared(civ, tile.position) }
|
.forEach { it.questManager.barbarianCampCleared(civ, tile.position) }
|
||||||
|
|
||||||
var goldGained =
|
var goldGained =
|
||||||
civ.getDifficulty().clearBarbarianCampReward * civ.gameInfo.speed.goldCostModifier
|
civ.getDifficulty().clearBarbarianCampReward * civ.gameInfo.speed.goldCostModifier
|
||||||
if (civ.hasUnique(UniqueType.TripleGoldFromEncampmentsAndCities))
|
if (civ.hasUnique(UniqueType.TripleGoldFromEncampmentsAndCities))
|
||||||
goldGained *= 3f
|
goldGained *= 3f
|
||||||
|
|
||||||
civ.addGold(goldGained.toInt())
|
civ.addGold(goldGained.toInt())
|
||||||
civ.addNotification(
|
civ.addNotification(
|
||||||
"We have captured a barbarian encampment and recovered [${goldGained.toInt()}] gold!",
|
"We have captured a barbarian encampment and recovered [${goldGained.toInt()}] gold!",
|
||||||
tile.position,
|
tile.position,
|
||||||
NotificationCategory.War,
|
NotificationCategory.War,
|
||||||
NotificationIcon.Gold
|
NotificationIcon.Gold
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun disband() {
|
fun disband() {
|
||||||
// evacuation of transported units before disbanding, if possible. toListed because we're modifying the unit list.
|
// evacuation of transported units before disbanding, if possible. toListed because we're modifying the unit list.
|
||||||
for (unit in currentTile.getUnits()
|
for (unit in currentTile.getUnits()
|
||||||
.filter { it.isTransported && isTransportTypeOf(it) }
|
.filter { it.isTransported && isTransportTypeOf(it) }
|
||||||
.toList()
|
.toList()
|
||||||
) {
|
) {
|
||||||
// if we disbanded a unit carrying other units in a city, the carried units can still stay in the city
|
// 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)) {
|
if (currentTile.isCityCenter() && unit.movement.canMoveTo(currentTile)) {
|
||||||
@ -934,7 +943,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
unit.disband()
|
unit.disband()
|
||||||
// let's find closest city or another carrier where it can be evacuated
|
// let's find closest city or another carrier where it can be evacuated
|
||||||
val tileCanMoveTo = unit.currentTile.getTilesInDistance(unit.getMaxMovementForAirUnits())
|
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)
|
if (tileCanMoveTo != null)
|
||||||
unit.movement.moveToTile(tileCanMoveTo)
|
unit.movement.moveToTile(tileCanMoveTo)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user