mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-28 22:37:02 -04:00
chore: Separated start normalization into a separate object
This commit is contained in:
parent
f52e7d37f4
commit
e1a33f2116
@ -18,7 +18,6 @@ import com.unciv.models.ruleset.tile.TileResource
|
|||||||
import com.unciv.models.ruleset.unique.StateForConditionals
|
import com.unciv.models.ruleset.unique.StateForConditionals
|
||||||
import com.unciv.models.ruleset.unique.Unique
|
import com.unciv.models.ruleset.unique.Unique
|
||||||
import com.unciv.models.ruleset.unique.UniqueType
|
import com.unciv.models.ruleset.unique.UniqueType
|
||||||
import com.unciv.models.stats.Stat
|
|
||||||
import com.unciv.models.translations.equalsPlaceholderText
|
import com.unciv.models.translations.equalsPlaceholderText
|
||||||
import com.unciv.models.translations.getPlaceholderParameters
|
import com.unciv.models.translations.getPlaceholderParameters
|
||||||
import com.unciv.ui.components.extensions.randomWeighted
|
import com.unciv.ui.components.extensions.randomWeighted
|
||||||
@ -240,30 +239,7 @@ class MapRegions (val ruleset: Ruleset){
|
|||||||
fun assignRegions(tileMap: TileMap, civilizations: List<Civilization>, gameParameters: GameParameters) {
|
fun assignRegions(tileMap: TileMap, civilizations: List<Civilization>, gameParameters: GameParameters) {
|
||||||
if (civilizations.isEmpty()) return
|
if (civilizations.isEmpty()) return
|
||||||
|
|
||||||
// first assign region types
|
assignRegionTypes()
|
||||||
val regionTypes = ruleset.terrains.values.filter { getRegionPriority(it) != null }
|
|
||||||
.sortedBy { getRegionPriority(it) }
|
|
||||||
|
|
||||||
for (region in regions) {
|
|
||||||
region.countTerrains()
|
|
||||||
|
|
||||||
for (type in regionTypes) {
|
|
||||||
// Test exclusion criteria first
|
|
||||||
if (type.getMatchingUniques(UniqueType.RegionRequireFirstLessThanSecond).any {
|
|
||||||
region.getTerrainAmount(it.params[0]) >= region.getTerrainAmount(it.params[1]) } ) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Test inclusion criteria
|
|
||||||
if (type.getMatchingUniques(UniqueType.RegionRequirePercentSingleType).any {
|
|
||||||
region.getTerrainAmount(it.params[1]) >= (it.params[0].toInt() * region.tiles.size) / 100 }
|
|
||||||
|| type.getMatchingUniques(UniqueType.RegionRequirePercentTwoTypes).any {
|
|
||||||
region.getTerrainAmount(it.params[1]) + region.getTerrainAmount(it.params[2]) >= (it.params[0].toInt() * region.tiles.size) / 100 }
|
|
||||||
) {
|
|
||||||
region.type = type.name
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate tile data for all tiles
|
// Generate tile data for all tiles
|
||||||
for (tile in tileMap.values) {
|
for (tile in tileMap.values) {
|
||||||
@ -273,13 +249,9 @@ class MapRegions (val ruleset: Ruleset){
|
|||||||
|
|
||||||
// Sort regions by fertility so the worse regions get to pick first
|
// Sort regions by fertility so the worse regions get to pick first
|
||||||
val sortedRegions = regions.sortedBy { it.totalFertility }
|
val sortedRegions = regions.sortedBy { it.totalFertility }
|
||||||
// Find a start for each region
|
for (region in sortedRegions) findStart(region)
|
||||||
for (region in sortedRegions) {
|
|
||||||
findStart(region)
|
|
||||||
}
|
|
||||||
// Normalize starts
|
|
||||||
for (region in regions) {
|
for (region in regions) {
|
||||||
normalizeStart(tileMap[region.startPosition!!], tileMap, isMinorCiv = false)
|
StartNormalizer.normalizeStart(tileMap[region.startPosition!!], tileMap, tileData, ruleset, isMinorCiv = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val civBiases = civilizations.associateWith { ruleset.nations[it.civName]!!.startBias }
|
val civBiases = civilizations.associateWith { ruleset.nations[it.civName]!!.startBias }
|
||||||
@ -415,6 +387,36 @@ class MapRegions (val ruleset: Ruleset){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sets region.type */
|
||||||
|
private fun assignRegionTypes() {
|
||||||
|
val regionTypes = ruleset.terrains.values.filter { getRegionPriority(it) != null }
|
||||||
|
.sortedBy { getRegionPriority(it) }
|
||||||
|
|
||||||
|
for (region in regions) {
|
||||||
|
region.countTerrains()
|
||||||
|
|
||||||
|
for (type in regionTypes) {
|
||||||
|
// Test exclusion criteria first
|
||||||
|
if (type.getMatchingUniques(UniqueType.RegionRequireFirstLessThanSecond).any {
|
||||||
|
region.getTerrainAmount(it.params[0]) >= region.getTerrainAmount(it.params[1])
|
||||||
|
}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Test inclusion criteria
|
||||||
|
if (type.getMatchingUniques(UniqueType.RegionRequirePercentSingleType).any {
|
||||||
|
region.getTerrainAmount(it.params[1]) >= (it.params[0].toInt() * region.tiles.size) / 100
|
||||||
|
}
|
||||||
|
|| type.getMatchingUniques(UniqueType.RegionRequirePercentTwoTypes).any {
|
||||||
|
region.getTerrainAmount(it.params[1]) + region.getTerrainAmount(it.params[2]) >= (it.params[0].toInt() * region.tiles.size) / 100
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
region.type = type.name
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun logAssignRegion(success: Boolean, startBiasType: BiasTypes, civ: Civilization, region: Region? = null) {
|
private fun logAssignRegion(success: Boolean, startBiasType: BiasTypes, civ: Civilization, region: Region? = null) {
|
||||||
if (Log.backend.isRelease()) return
|
if (Log.backend.isRelease()) return
|
||||||
|
|
||||||
@ -561,260 +563,6 @@ class MapRegions (val ruleset: Ruleset){
|
|||||||
setRegionStart(region, panicPosition)
|
setRegionStart(region, panicPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Attempts to improve the start on [startTile] as needed to make it decent.
|
|
||||||
* Relies on startPosition having been set previously.
|
|
||||||
* Assumes unchanged baseline values ie citizens eat 2 food each, similar production costs
|
|
||||||
* If [isMinorCiv] is true, different weightings will be used. */
|
|
||||||
private fun normalizeStart(startTile: Tile, tileMap: TileMap, isMinorCiv: Boolean) {
|
|
||||||
// Remove ice-like features adjacent to start
|
|
||||||
for (tile in startTile.neighbors) {
|
|
||||||
val lastTerrain = tile.terrainFeatureObjects.lastOrNull { it.impassable }
|
|
||||||
if (lastTerrain != null) {
|
|
||||||
tile.removeTerrainFeature(lastTerrain.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// evaluate production potential
|
|
||||||
val innerProduction = startTile.neighbors.sumOf { getPotentialYield(it, Stat.Production).toInt() }
|
|
||||||
val outerProduction = startTile.getTilesAtDistance(2).sumOf { getPotentialYield(it, Stat.Production).toInt() }
|
|
||||||
// for very early production we ideally want tiles that also give food
|
|
||||||
val earlyProduction = startTile.getTilesInDistanceRange(1..2).sumOf {
|
|
||||||
if (getPotentialYield(it, Stat.Food, unimproved = true) > 0f) getPotentialYield(it, Stat.Production, unimproved = true).toInt()
|
|
||||||
else 0 }
|
|
||||||
|
|
||||||
// If terrible, try adding a hill to a dry flat tile
|
|
||||||
if (innerProduction == 0 || (innerProduction < 2 && outerProduction < 8) || (isMinorCiv && innerProduction < 4)) {
|
|
||||||
val hillSpot = startTile.neighbors
|
|
||||||
.filter { it.isLand && it.terrainFeatures.isEmpty() && !it.isAdjacentTo(Constants.freshWater) && !it.isImpassible() }
|
|
||||||
.toList().randomOrNull()
|
|
||||||
val hillEquivalent = ruleset.terrains.values
|
|
||||||
.firstOrNull { it.type == TerrainType.TerrainFeature && it.production >= 2 && !it.hasUnique(UniqueType.RareFeature) }?.name
|
|
||||||
if (hillSpot != null && hillEquivalent != null) {
|
|
||||||
hillSpot.addTerrainFeature(hillEquivalent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Place Strategic Balance Resources
|
|
||||||
if (tileMap.mapParameters.mapResources == MapResources.strategicBalance) {
|
|
||||||
val candidateTiles = startTile.getTilesInDistanceRange(1..2).shuffled() + startTile.getTilesAtDistance(3).shuffled()
|
|
||||||
for (resource in ruleset.tileResources.values.filter { it.hasUnique(UniqueType.StrategicBalanceResource) }) {
|
|
||||||
if (MapRegionResources.tryAddingResourceToTiles(tileData, resource, 1, candidateTiles, majorDeposit = true) == 0) {
|
|
||||||
// Fallback mode - force placement, even on an otherwise inappropriate terrain. Do still respect water and impassible tiles!
|
|
||||||
val resourceTiles =
|
|
||||||
if (isWaterOnlyResource(resource)) candidateTiles.filter { it.isWater && !it.isImpassible() }.toList()
|
|
||||||
else candidateTiles.filter { it.isLand && !it.isImpassible() }.toList()
|
|
||||||
MapRegionResources.placeResourcesInTiles(tileData, 999, resourceTiles, listOf(resource), majorDeposit = true, forcePlacement = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If bad early production, add a small strategic resource to SECOND ring (not for minors)
|
|
||||||
if (!isMinorCiv && innerProduction < 3 && earlyProduction < 6) {
|
|
||||||
val lastEraNumber = ruleset.eras.values.maxOf { it.eraNumber }
|
|
||||||
val earlyEras = ruleset.eras.filterValues { it.eraNumber <= lastEraNumber / 3 }
|
|
||||||
val validResources = ruleset.tileResources.values.filter {
|
|
||||||
it.resourceType == ResourceType.Strategic &&
|
|
||||||
(it.revealedBy == null ||
|
|
||||||
ruleset.technologies[it.revealedBy]!!.era() in earlyEras)
|
|
||||||
}.shuffled()
|
|
||||||
val candidateTiles = startTile.getTilesAtDistance(2).shuffled()
|
|
||||||
for (resource in validResources) {
|
|
||||||
if (MapRegionResources.tryAddingResourceToTiles(tileData, resource, 1, candidateTiles, majorDeposit = false) > 0)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val foodBonusesNeeded = calculateFoodBonusesNeeded(startTile, isMinorCiv, tileMap)
|
|
||||||
placeFoodBonuses(isMinorCiv, startTile, foodBonusesNeeded)
|
|
||||||
|
|
||||||
// Minor civs are done, go on with grassiness checks for major civs
|
|
||||||
if (isMinorCiv) return
|
|
||||||
|
|
||||||
addProductionBonuses(startTile)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Check for very food-heavy starts that might still need some stone to help with production */
|
|
||||||
private fun addProductionBonuses(startTile: Tile) {
|
|
||||||
val grassTypePlots = startTile.getTilesInDistanceRange(1..2).filter {
|
|
||||||
it.isLand &&
|
|
||||||
getPotentialYield(it, Stat.Food, unimproved = true) >= 2f && // Food neutral natively
|
|
||||||
getPotentialYield(it, Stat.Production) == 0f // Production can't even be improved
|
|
||||||
}.toMutableList()
|
|
||||||
val plainsTypePlots = startTile.getTilesInDistanceRange(1..2).filter {
|
|
||||||
it.isLand &&
|
|
||||||
getPotentialYield(it, Stat.Food) >= 2f && // Something that can be improved to food neutral
|
|
||||||
getPotentialYield(it, Stat.Production, unimproved = true) >= 1f // Some production natively
|
|
||||||
}.toList()
|
|
||||||
var productionBonusesNeeded = when {
|
|
||||||
grassTypePlots.size >= 9 && plainsTypePlots.isEmpty() -> 2
|
|
||||||
grassTypePlots.size >= 6 && plainsTypePlots.size <= 4 -> 1
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
val productionBonuses =
|
|
||||||
ruleset.tileResources.values.filter { it.resourceType == ResourceType.Bonus && it.production > 0 }
|
|
||||||
|
|
||||||
if (productionBonuses.isNotEmpty()) {
|
|
||||||
while (productionBonusesNeeded > 0 && grassTypePlots.isNotEmpty()) {
|
|
||||||
val plot = grassTypePlots.random()
|
|
||||||
grassTypePlots.remove(plot)
|
|
||||||
|
|
||||||
if (plot.resource != null) continue
|
|
||||||
|
|
||||||
val bonusToPlace =
|
|
||||||
productionBonuses.filter { plot.lastTerrain.name in it.terrainsCanBeFoundOn }
|
|
||||||
.randomOrNull()
|
|
||||||
if (bonusToPlace != null) {
|
|
||||||
plot.resource = bonusToPlace.name
|
|
||||||
productionBonusesNeeded--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateFoodBonusesNeeded(
|
|
||||||
startTile: Tile,
|
|
||||||
minorCiv: Boolean,
|
|
||||||
tileMap: TileMap
|
|
||||||
): Int {
|
|
||||||
// evaluate food situation
|
|
||||||
// Food²/4 because excess food is really good and lets us work other tiles or run specialists!
|
|
||||||
// 2F is worth 1, 3F is worth 2, 4F is worth 4, 5F is worth 6 and so on
|
|
||||||
val innerFood =
|
|
||||||
startTile.neighbors.sumOf { (getPotentialYield(it, Stat.Food).pow(2) / 4).toInt() }
|
|
||||||
val outerFood = startTile.getTilesAtDistance(2)
|
|
||||||
.sumOf { (getPotentialYield(it, Stat.Food).pow(2) / 4).toInt() }
|
|
||||||
val totalFood = innerFood + outerFood
|
|
||||||
// we want at least some two-food tiles to keep growing
|
|
||||||
val innerNativeTwoFood =
|
|
||||||
startTile.neighbors.count { getPotentialYield(it, Stat.Food, unimproved = true) >= 2f }
|
|
||||||
val outerNativeTwoFood = startTile.getTilesAtDistance(2)
|
|
||||||
.count { getPotentialYield(it, Stat.Food, unimproved = true) >= 2f }
|
|
||||||
val totalNativeTwoFood = innerNativeTwoFood + outerNativeTwoFood
|
|
||||||
|
|
||||||
// Determine number of needed bonuses. Different weightings for minor and major civs.
|
|
||||||
var bonusesNeeded = if (minorCiv) {
|
|
||||||
when { // From 2 to 0
|
|
||||||
totalFood < 12 || innerFood < 4 -> 2
|
|
||||||
totalFood < 16 || innerFood < 9 -> 1
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when { // From 5 to 0
|
|
||||||
innerFood == 0 && totalFood < 4 -> 5
|
|
||||||
totalFood < 6 -> 4
|
|
||||||
totalFood < 8 ||
|
|
||||||
(totalFood < 12 && innerFood < 5) -> 3
|
|
||||||
|
|
||||||
(totalFood < 17 && innerFood < 9) ||
|
|
||||||
totalNativeTwoFood < 2 -> 2
|
|
||||||
|
|
||||||
(totalFood < 24 && innerFood < 11) ||
|
|
||||||
totalNativeTwoFood == 2 ||
|
|
||||||
innerNativeTwoFood == 0 ||
|
|
||||||
totalFood < 20 -> 1
|
|
||||||
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (tileMap.mapParameters.mapResources == MapResources.legendaryStart)
|
|
||||||
bonusesNeeded += 2
|
|
||||||
|
|
||||||
// Attempt to place one grassland at a plains-only spot (nor for minors)
|
|
||||||
if (!minorCiv && bonusesNeeded < 3 && totalNativeTwoFood == 0) {
|
|
||||||
val twoFoodTerrain =
|
|
||||||
ruleset.terrains.values.firstOrNull { it.type == TerrainType.Land && it.food >= 2 }?.name
|
|
||||||
val candidateInnerSpots = startTile.neighbors
|
|
||||||
.filter { it.isLand && !it.isImpassible() && it.terrainFeatures.isEmpty() && it.resource == null }
|
|
||||||
val candidateOuterSpots = startTile.getTilesAtDistance(2)
|
|
||||||
.filter { it.isLand && !it.isImpassible() && it.terrainFeatures.isEmpty() && it.resource == null }
|
|
||||||
val spot =
|
|
||||||
candidateInnerSpots.shuffled().firstOrNull() ?: candidateOuterSpots.shuffled()
|
|
||||||
.firstOrNull()
|
|
||||||
if (twoFoodTerrain != null && spot != null) {
|
|
||||||
spot.baseTerrain = twoFoodTerrain
|
|
||||||
} else
|
|
||||||
bonusesNeeded = 3 // Irredeemable plains situation
|
|
||||||
}
|
|
||||||
return bonusesNeeded
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun placeFoodBonuses(
|
|
||||||
minorCiv: Boolean,
|
|
||||||
startTile: Tile,
|
|
||||||
foodBonusesNeeded: Int
|
|
||||||
) {
|
|
||||||
var bonusesStillNeeded = foodBonusesNeeded
|
|
||||||
val oasisEquivalent = ruleset.terrains.values.firstOrNull {
|
|
||||||
it.type == TerrainType.TerrainFeature &&
|
|
||||||
it.hasUnique(UniqueType.RareFeature) &&
|
|
||||||
it.food >= 2 &&
|
|
||||||
it.food + it.production + it.gold >= 3 &&
|
|
||||||
it.occursOn.any { base -> ruleset.terrains[base]!!.type == TerrainType.Land }
|
|
||||||
}
|
|
||||||
var canPlaceOasis =
|
|
||||||
oasisEquivalent != null // One oasis per start is enough. Don't bother finding a place if there is no good oasis equivalent
|
|
||||||
var placedInFirst = 0 // Attempt to put first 2 in inner ring and next 3 in second ring
|
|
||||||
var placedInSecond = 0
|
|
||||||
val rangeForBonuses = if (minorCiv) 2 else 3
|
|
||||||
|
|
||||||
// Start with list of candidate plots sorted in ring order 1,2,3
|
|
||||||
val candidatePlots = startTile.getTilesInDistanceRange(1..rangeForBonuses)
|
|
||||||
.filter { it.resource == null && oasisEquivalent !in it.terrainFeatureObjects }
|
|
||||||
.shuffled().sortedBy { it.aerialDistanceTo(startTile) }.toMutableList()
|
|
||||||
|
|
||||||
// Place food bonuses (and oases) as able
|
|
||||||
while (bonusesStillNeeded > 0 && candidatePlots.isNotEmpty()) {
|
|
||||||
val plot = candidatePlots.first()
|
|
||||||
candidatePlots.remove(plot) // remove the plot as it has now been tried, whether successfully or not
|
|
||||||
if (plot.getBaseTerrain().hasUnique(
|
|
||||||
UniqueType.BlocksResources,
|
|
||||||
StateForConditionals(attackedTile = plot)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
continue // Don't put bonuses on snow hills
|
|
||||||
|
|
||||||
val validBonuses = ruleset.tileResources.values.filter {
|
|
||||||
it.resourceType == ResourceType.Bonus &&
|
|
||||||
it.food >= 1 &&
|
|
||||||
plot.lastTerrain.name in it.terrainsCanBeFoundOn
|
|
||||||
}
|
|
||||||
val goodPlotForOasis =
|
|
||||||
canPlaceOasis && plot.lastTerrain.name in oasisEquivalent!!.occursOn
|
|
||||||
|
|
||||||
if (validBonuses.isNotEmpty() || goodPlotForOasis) {
|
|
||||||
if (goodPlotForOasis) {
|
|
||||||
plot.addTerrainFeature(oasisEquivalent!!.name)
|
|
||||||
canPlaceOasis = false
|
|
||||||
} else {
|
|
||||||
plot.setTileResource(validBonuses.random())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plot.aerialDistanceTo(startTile) == 1) {
|
|
||||||
placedInFirst++
|
|
||||||
if (placedInFirst == 2) // Resort the list in ring order 2,3,1
|
|
||||||
candidatePlots.sortBy { abs(it.aerialDistanceTo(startTile) * 10 - 22) }
|
|
||||||
} else if (plot.aerialDistanceTo(startTile) == 2) {
|
|
||||||
placedInSecond++
|
|
||||||
if (placedInSecond == 3) // Resort the list in ring order 3,1,2
|
|
||||||
candidatePlots.sortByDescending { abs(it.aerialDistanceTo(startTile) * 10 - 17) }
|
|
||||||
}
|
|
||||||
bonusesStillNeeded--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPotentialYield(tile: Tile, stat: Stat, unimproved: Boolean = false): Float {
|
|
||||||
val baseYield = tile.stats.getTileStats(null)[stat]
|
|
||||||
if (unimproved) return baseYield
|
|
||||||
|
|
||||||
val bestImprovementYield = tile.tileMap.ruleset!!.tileImprovements.values
|
|
||||||
.filter { !it.hasUnique(UniqueType.GreatImprovement) &&
|
|
||||||
it.uniqueTo == null &&
|
|
||||||
tile.lastTerrain.name in it.terrainsCanBeBuiltOn }
|
|
||||||
.maxOfOrNull { it[stat] }
|
|
||||||
return baseYield + (bestImprovementYield ?: 0f)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @returns the region most similar to a region of [type] */
|
/** @returns the region most similar to a region of [type] */
|
||||||
private fun getFallbackRegion(type: String, candidates: List<Region>): Region {
|
private fun getFallbackRegion(type: String, candidates: List<Region>): Region {
|
||||||
return candidates.maxByOrNull { it.terrainCounts[type] ?: 0 }!!
|
return candidates.maxByOrNull { it.terrainCounts[type] ?: 0 }!!
|
||||||
@ -1183,7 +931,7 @@ class MapRegions (val ruleset: Ruleset){
|
|||||||
tileData.placeImpact(ImpactType.Strategic,tile, 0)
|
tileData.placeImpact(ImpactType.Strategic,tile, 0)
|
||||||
tileData.placeImpact(ImpactType.Bonus, tile, 3)
|
tileData.placeImpact(ImpactType.Bonus, tile, 3)
|
||||||
|
|
||||||
normalizeStart(tile, tileMap, isMinorCiv = true)
|
StartNormalizer.normalizeStart(tile, tileMap, tileData, ruleset, isMinorCiv = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Places all Luxuries onto [tileMap]. Assumes that assignLuxuries and placeMinorCivs have been called. */
|
/** Places all Luxuries onto [tileMap]. Assumes that assignLuxuries and placeMinorCivs have been called. */
|
||||||
@ -1247,7 +995,7 @@ class MapRegions (val ruleset: Ruleset){
|
|||||||
for (region in regions) {
|
for (region in regions) {
|
||||||
val resource = ruleset.tileResources[region.luxury] ?: continue
|
val resource = ruleset.tileResources[region.luxury] ?: continue
|
||||||
fun Tile.isShoreOfContinent(continent: Int) = isWater && neighbors.any { it.getContinent() == continent }
|
fun Tile.isShoreOfContinent(continent: Int) = isWater && neighbors.any { it.getContinent() == continent }
|
||||||
val candidates = if (isWaterOnlyResource(resource))
|
val candidates = if (isWaterOnlyResource(resource, ruleset))
|
||||||
tileMap.getTilesInRectangle(region.rect).filter { it.isShoreOfContinent(region.continentID) }
|
tileMap.getTilesInRectangle(region.rect).filter { it.isShoreOfContinent(region.continentID) }
|
||||||
else region.tiles.asSequence()
|
else region.tiles.asSequence()
|
||||||
MapRegionResources.tryAddingResourceToTiles(tileData, resource, regionTargetNumber, candidates.shuffled(), 0.4f, true, 4, 2)
|
MapRegionResources.tryAddingResourceToTiles(tileData, resource, regionTargetNumber, candidates.shuffled(), 0.4f, true, 4, 2)
|
||||||
@ -1460,7 +1208,7 @@ class MapRegions (val ruleset: Ruleset){
|
|||||||
for (resource in strategicResources) {
|
for (resource in strategicResources) {
|
||||||
val extraNeeded = min(2, regions.size - totalPlaced[resource]!!)
|
val extraNeeded = min(2, regions.size - totalPlaced[resource]!!)
|
||||||
if (extraNeeded > 0) {
|
if (extraNeeded > 0) {
|
||||||
if (isWaterOnlyResource(resource))
|
if (isWaterOnlyResource(resource, ruleset))
|
||||||
MapRegionResources.tryAddingResourceToTiles(tileData, resource, extraNeeded, tileMap.values.asSequence().filter { it.isWater }.shuffled(), respectImpacts = true)
|
MapRegionResources.tryAddingResourceToTiles(tileData, resource, extraNeeded, tileMap.values.asSequence().filter { it.isWater }.shuffled(), respectImpacts = true)
|
||||||
else
|
else
|
||||||
MapRegionResources.tryAddingResourceToTiles(tileData, resource, extraNeeded, landList.asSequence(), respectImpacts = true)
|
MapRegionResources.tryAddingResourceToTiles(tileData, resource, extraNeeded, landList.asSequence(), respectImpacts = true)
|
||||||
@ -1472,9 +1220,9 @@ class MapRegions (val ruleset: Ruleset){
|
|||||||
|
|
||||||
// Sixth place bonus resources (and other resources that might have been assigned frequency-based generation).
|
// Sixth place bonus resources (and other resources that might have been assigned frequency-based generation).
|
||||||
// Water-based bonuses go last and have extra impact, because coasts are very common and we don't want too much clustering
|
// Water-based bonuses go last and have extra impact, because coasts are very common and we don't want too much clustering
|
||||||
val sortedResourceList = ruleset.tileResources.values.sortedBy { isWaterOnlyResource(it) }
|
val sortedResourceList = ruleset.tileResources.values.sortedBy { isWaterOnlyResource(it, ruleset) }
|
||||||
for (resource in sortedResourceList) {
|
for (resource in sortedResourceList) {
|
||||||
val extraImpact = if (isWaterOnlyResource(resource)) 1 else 0
|
val extraImpact = if (isWaterOnlyResource(resource, ruleset)) 1 else 0
|
||||||
for (rule in resource.uniqueObjects.filter { it.type == UniqueType.ResourceFrequency }) {
|
for (rule in resource.uniqueObjects.filter { it.type == UniqueType.ResourceFrequency }) {
|
||||||
// Figure out which list applies, if any
|
// Figure out which list applies, if any
|
||||||
val simpleRule = anonymizeUnique(rule)
|
val simpleRule = anonymizeUnique(rule)
|
||||||
@ -1518,9 +1266,6 @@ class MapRegions (val ruleset: Ruleset){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isWaterOnlyResource(resource: TileResource) = resource.terrainsCanBeFoundOn
|
|
||||||
.all { terrainName -> ruleset.terrains[terrainName]!!.type == TerrainType.Water }
|
|
||||||
|
|
||||||
enum class ImpactType {
|
enum class ImpactType {
|
||||||
Strategic,
|
Strategic,
|
||||||
Luxury,
|
Luxury,
|
||||||
@ -1535,6 +1280,9 @@ class MapRegions (val ruleset: Ruleset){
|
|||||||
internal fun anonymizeUnique(unique: Unique) = Unique(
|
internal fun anonymizeUnique(unique: Unique) = Unique(
|
||||||
"RULE" + unique.conditionals.sortedBy { it.text }.joinToString(prefix = " ", separator = " ") { "<" + it.text + ">" })
|
"RULE" + unique.conditionals.sortedBy { it.text }.joinToString(prefix = " ", separator = " ") { "<" + it.text + ">" })
|
||||||
|
|
||||||
|
internal fun isWaterOnlyResource(resource: TileResource, ruleset: Ruleset) = resource.terrainsCanBeFoundOn
|
||||||
|
.all { terrainName -> ruleset.terrains[terrainName]!!.type == TerrainType.Water }
|
||||||
|
|
||||||
|
|
||||||
/** @return a fake unique with conditionals that will satisfy the same conditions as terrainsCanBeFoundOn */
|
/** @return a fake unique with conditionals that will satisfy the same conditions as terrainsCanBeFoundOn */
|
||||||
internal fun getTerrainRule(terrain: Terrain, ruleset: Ruleset): Unique {
|
internal fun getTerrainRule(terrain: Terrain, ruleset: Ruleset): Unique {
|
||||||
|
@ -0,0 +1,322 @@
|
|||||||
|
package com.unciv.logic.map.mapgenerator.mapregions
|
||||||
|
|
||||||
|
import com.unciv.Constants
|
||||||
|
import com.unciv.logic.map.MapResources
|
||||||
|
import com.unciv.logic.map.TileMap
|
||||||
|
import com.unciv.logic.map.tile.Tile
|
||||||
|
import com.unciv.models.ruleset.Ruleset
|
||||||
|
import com.unciv.models.ruleset.tile.ResourceType
|
||||||
|
import com.unciv.models.ruleset.tile.TerrainType
|
||||||
|
import com.unciv.models.ruleset.unique.StateForConditionals
|
||||||
|
import com.unciv.models.ruleset.unique.UniqueType
|
||||||
|
import com.unciv.models.stats.Stat
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
/** Ensures that starting positions of civs have enough yield that they aren't at a disadvantage */
|
||||||
|
object StartNormalizer {
|
||||||
|
|
||||||
|
/** Attempts to improve the start on [startTile] as needed to make it decent.
|
||||||
|
* Relies on startPosition having been set previously.
|
||||||
|
* Assumes unchanged baseline values ie citizens eat 2 food each, similar production costs
|
||||||
|
* If [isMinorCiv] is true, different weightings will be used. */
|
||||||
|
fun normalizeStart(startTile: Tile, tileMap: TileMap, tileData: TileDataMap, ruleset: Ruleset, isMinorCiv: Boolean) {
|
||||||
|
// Remove ice-like features adjacent to start
|
||||||
|
for (tile in startTile.neighbors) {
|
||||||
|
val lastTerrain = tile.terrainFeatureObjects.lastOrNull { it.impassable }
|
||||||
|
if (lastTerrain != null) {
|
||||||
|
tile.removeTerrainFeature(lastTerrain.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tileMap.mapParameters.mapResources == MapResources.strategicBalance)
|
||||||
|
placeStrategicBalanceResources(startTile, ruleset, tileData)
|
||||||
|
|
||||||
|
normalizeProduction(startTile, isMinorCiv, ruleset, tileData)
|
||||||
|
|
||||||
|
val foodBonusesNeeded = calculateFoodBonusesNeeded(startTile, isMinorCiv, ruleset, tileMap)
|
||||||
|
placeFoodBonuses(isMinorCiv, startTile, ruleset, foodBonusesNeeded)
|
||||||
|
|
||||||
|
// Minor civs are done, go on with grassiness checks for major civs
|
||||||
|
if (isMinorCiv) return
|
||||||
|
|
||||||
|
addProductionBonuses(startTile, ruleset)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizeProduction(
|
||||||
|
startTile: Tile,
|
||||||
|
isMinorCiv: Boolean,
|
||||||
|
ruleset: Ruleset,
|
||||||
|
tileData: TileDataMap
|
||||||
|
) {
|
||||||
|
// evaluate production potential
|
||||||
|
val innerProduction =
|
||||||
|
startTile.neighbors.sumOf { getPotentialYield(it, Stat.Production).toInt() }
|
||||||
|
val outerProduction =
|
||||||
|
startTile.getTilesAtDistance(2).sumOf { getPotentialYield(it, Stat.Production).toInt() }
|
||||||
|
// for very early production we ideally want tiles that also give food
|
||||||
|
val earlyProduction = startTile.getTilesInDistanceRange(1..2).sumOf {
|
||||||
|
if (getPotentialYield(it, Stat.Food, unimproved = true) > 0f) getPotentialYield(
|
||||||
|
it,
|
||||||
|
Stat.Production,
|
||||||
|
unimproved = true
|
||||||
|
).toInt()
|
||||||
|
else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// If terrible, try adding a hill to a dry flat tile
|
||||||
|
if (innerProduction == 0 || (innerProduction < 2 && outerProduction < 8) || (isMinorCiv && innerProduction < 4)) {
|
||||||
|
val hillSpot = startTile.neighbors
|
||||||
|
.filter { it.isLand && it.terrainFeatures.isEmpty() && !it.isAdjacentTo(Constants.freshWater) && !it.isImpassible() }
|
||||||
|
.toList().randomOrNull()
|
||||||
|
val hillEquivalent = ruleset.terrains.values
|
||||||
|
.firstOrNull {
|
||||||
|
it.type == TerrainType.TerrainFeature && it.production >= 2 && !it.hasUnique(
|
||||||
|
UniqueType.RareFeature
|
||||||
|
)
|
||||||
|
}?.name
|
||||||
|
if (hillSpot != null && hillEquivalent != null) {
|
||||||
|
hillSpot.addTerrainFeature(hillEquivalent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If bad early production, add a small strategic resource to SECOND ring (not for minors)
|
||||||
|
if (!isMinorCiv && innerProduction < 3 && earlyProduction < 6) {
|
||||||
|
val lastEraNumber = ruleset.eras.values.maxOf { it.eraNumber }
|
||||||
|
val earlyEras = ruleset.eras.filterValues { it.eraNumber <= lastEraNumber / 3 }
|
||||||
|
val validResources = ruleset.tileResources.values.filter {
|
||||||
|
it.resourceType == ResourceType.Strategic &&
|
||||||
|
(it.revealedBy == null ||
|
||||||
|
ruleset.technologies[it.revealedBy]!!.era() in earlyEras)
|
||||||
|
}.shuffled()
|
||||||
|
val candidateTiles = startTile.getTilesAtDistance(2).shuffled()
|
||||||
|
for (resource in validResources) {
|
||||||
|
val resourcesAdded = MapRegionResources.tryAddingResourceToTiles(
|
||||||
|
tileData, resource, 1, candidateTiles, majorDeposit = false)
|
||||||
|
if (resourcesAdded > 0) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun placeStrategicBalanceResources(
|
||||||
|
startTile: Tile,
|
||||||
|
ruleset: Ruleset,
|
||||||
|
tileData: TileDataMap
|
||||||
|
) {
|
||||||
|
val candidateTiles =
|
||||||
|
startTile.getTilesInDistanceRange(1..2).shuffled() + startTile.getTilesAtDistance(3)
|
||||||
|
.shuffled()
|
||||||
|
for (resource in ruleset.tileResources.values.filter { it.hasUnique(UniqueType.StrategicBalanceResource) }) {
|
||||||
|
if (MapRegionResources.tryAddingResourceToTiles(
|
||||||
|
tileData,
|
||||||
|
resource,
|
||||||
|
1,
|
||||||
|
candidateTiles,
|
||||||
|
majorDeposit = true
|
||||||
|
) == 0
|
||||||
|
) {
|
||||||
|
// Fallback mode - force placement, even on an otherwise inappropriate terrain. Do still respect water and impassible tiles!
|
||||||
|
val resourceTiles =
|
||||||
|
if (isWaterOnlyResource(
|
||||||
|
resource,
|
||||||
|
ruleset
|
||||||
|
)
|
||||||
|
) candidateTiles.filter { it.isWater && !it.isImpassible() }.toList()
|
||||||
|
else candidateTiles.filter { it.isLand && !it.isImpassible() }.toList()
|
||||||
|
MapRegionResources.placeResourcesInTiles(
|
||||||
|
tileData,
|
||||||
|
999,
|
||||||
|
resourceTiles,
|
||||||
|
listOf(resource),
|
||||||
|
majorDeposit = true,
|
||||||
|
forcePlacement = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check for very food-heavy starts that might still need some stone to help with production */
|
||||||
|
private fun addProductionBonuses(startTile: Tile, ruleset: Ruleset) {
|
||||||
|
val grassTypePlots = startTile.getTilesInDistanceRange(1..2).filter {
|
||||||
|
it.isLand &&
|
||||||
|
getPotentialYield(it, Stat.Food, unimproved = true) >= 2f && // Food neutral natively
|
||||||
|
getPotentialYield(it, Stat.Production) == 0f // Production can't even be improved
|
||||||
|
}.toMutableList()
|
||||||
|
val plainsTypePlots = startTile.getTilesInDistanceRange(1..2).filter {
|
||||||
|
it.isLand &&
|
||||||
|
getPotentialYield(it, Stat.Food) >= 2f && // Something that can be improved to food neutral
|
||||||
|
getPotentialYield(it, Stat.Production, unimproved = true) >= 1f // Some production natively
|
||||||
|
}.toList()
|
||||||
|
var productionBonusesNeeded = when {
|
||||||
|
grassTypePlots.size >= 9 && plainsTypePlots.isEmpty() -> 2
|
||||||
|
grassTypePlots.size >= 6 && plainsTypePlots.size <= 4 -> 1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
val productionBonuses =
|
||||||
|
ruleset.tileResources.values.filter { it.resourceType == ResourceType.Bonus && it.production > 0 }
|
||||||
|
|
||||||
|
if (productionBonuses.isNotEmpty()) {
|
||||||
|
while (productionBonusesNeeded > 0 && grassTypePlots.isNotEmpty()) {
|
||||||
|
val plot = grassTypePlots.random()
|
||||||
|
grassTypePlots.remove(plot)
|
||||||
|
|
||||||
|
if (plot.resource != null) continue
|
||||||
|
|
||||||
|
val bonusToPlace =
|
||||||
|
productionBonuses.filter { plot.lastTerrain.name in it.terrainsCanBeFoundOn }
|
||||||
|
.randomOrNull()
|
||||||
|
if (bonusToPlace != null) {
|
||||||
|
plot.resource = bonusToPlace.name
|
||||||
|
productionBonusesNeeded--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateFoodBonusesNeeded(
|
||||||
|
startTile: Tile,
|
||||||
|
minorCiv: Boolean,
|
||||||
|
ruleset: Ruleset,
|
||||||
|
tileMap: TileMap
|
||||||
|
): Int {
|
||||||
|
// evaluate food situation
|
||||||
|
// Food²/4 because excess food is really good and lets us work other tiles or run specialists!
|
||||||
|
// 2F is worth 1, 3F is worth 2, 4F is worth 4, 5F is worth 6 and so on
|
||||||
|
val innerFood =
|
||||||
|
startTile.neighbors.sumOf { (getPotentialYield(it, Stat.Food).pow(2) / 4).toInt() }
|
||||||
|
val outerFood = startTile.getTilesAtDistance(2)
|
||||||
|
.sumOf { (getPotentialYield(it, Stat.Food).pow(2) / 4).toInt() }
|
||||||
|
val totalFood = innerFood + outerFood
|
||||||
|
// we want at least some two-food tiles to keep growing
|
||||||
|
val innerNativeTwoFood =
|
||||||
|
startTile.neighbors.count { getPotentialYield(it, Stat.Food, unimproved = true) >= 2f }
|
||||||
|
val outerNativeTwoFood = startTile.getTilesAtDistance(2)
|
||||||
|
.count { getPotentialYield(it, Stat.Food, unimproved = true) >= 2f }
|
||||||
|
val totalNativeTwoFood = innerNativeTwoFood + outerNativeTwoFood
|
||||||
|
|
||||||
|
// Determine number of needed bonuses. Different weightings for minor and major civs.
|
||||||
|
var bonusesNeeded = if (minorCiv) {
|
||||||
|
when { // From 2 to 0
|
||||||
|
totalFood < 12 || innerFood < 4 -> 2
|
||||||
|
totalFood < 16 || innerFood < 9 -> 1
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when { // From 5 to 0
|
||||||
|
innerFood == 0 && totalFood < 4 -> 5
|
||||||
|
totalFood < 6 -> 4
|
||||||
|
totalFood < 8 ||
|
||||||
|
(totalFood < 12 && innerFood < 5) -> 3
|
||||||
|
|
||||||
|
(totalFood < 17 && innerFood < 9) ||
|
||||||
|
totalNativeTwoFood < 2 -> 2
|
||||||
|
|
||||||
|
(totalFood < 24 && innerFood < 11) ||
|
||||||
|
totalNativeTwoFood == 2 ||
|
||||||
|
innerNativeTwoFood == 0 ||
|
||||||
|
totalFood < 20 -> 1
|
||||||
|
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tileMap.mapParameters.mapResources == MapResources.legendaryStart)
|
||||||
|
bonusesNeeded += 2
|
||||||
|
|
||||||
|
// Attempt to place one grassland at a plains-only spot (nor for minors)
|
||||||
|
if (!minorCiv && bonusesNeeded < 3 && totalNativeTwoFood == 0) {
|
||||||
|
val twoFoodTerrain =
|
||||||
|
ruleset.terrains.values.firstOrNull { it.type == TerrainType.Land && it.food >= 2 }?.name
|
||||||
|
val candidateInnerSpots = startTile.neighbors
|
||||||
|
.filter { it.isLand && !it.isImpassible() && it.terrainFeatures.isEmpty() && it.resource == null }
|
||||||
|
val candidateOuterSpots = startTile.getTilesAtDistance(2)
|
||||||
|
.filter { it.isLand && !it.isImpassible() && it.terrainFeatures.isEmpty() && it.resource == null }
|
||||||
|
val spot =
|
||||||
|
candidateInnerSpots.shuffled().firstOrNull() ?: candidateOuterSpots.shuffled()
|
||||||
|
.firstOrNull()
|
||||||
|
if (twoFoodTerrain != null && spot != null) {
|
||||||
|
spot.baseTerrain = twoFoodTerrain
|
||||||
|
} else
|
||||||
|
bonusesNeeded = 3 // Irredeemable plains situation
|
||||||
|
}
|
||||||
|
return bonusesNeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun placeFoodBonuses(
|
||||||
|
minorCiv: Boolean,
|
||||||
|
startTile: Tile,
|
||||||
|
ruleset: Ruleset,
|
||||||
|
foodBonusesNeeded: Int
|
||||||
|
) {
|
||||||
|
var bonusesStillNeeded = foodBonusesNeeded
|
||||||
|
val oasisEquivalent = ruleset.terrains.values.firstOrNull {
|
||||||
|
it.type == TerrainType.TerrainFeature &&
|
||||||
|
it.hasUnique(UniqueType.RareFeature) &&
|
||||||
|
it.food >= 2 &&
|
||||||
|
it.food + it.production + it.gold >= 3 &&
|
||||||
|
it.occursOn.any { base -> ruleset.terrains[base]!!.type == TerrainType.Land }
|
||||||
|
}
|
||||||
|
var canPlaceOasis =
|
||||||
|
oasisEquivalent != null // One oasis per start is enough. Don't bother finding a place if there is no good oasis equivalent
|
||||||
|
var placedInFirst = 0 // Attempt to put first 2 in inner ring and next 3 in second ring
|
||||||
|
var placedInSecond = 0
|
||||||
|
val rangeForBonuses = if (minorCiv) 2 else 3
|
||||||
|
|
||||||
|
// Start with list of candidate plots sorted in ring order 1,2,3
|
||||||
|
val candidatePlots = startTile.getTilesInDistanceRange(1..rangeForBonuses)
|
||||||
|
.filter { it.resource == null && oasisEquivalent !in it.terrainFeatureObjects }
|
||||||
|
.shuffled().sortedBy { it.aerialDistanceTo(startTile) }.toMutableList()
|
||||||
|
|
||||||
|
// Place food bonuses (and oases) as able
|
||||||
|
while (bonusesStillNeeded > 0 && candidatePlots.isNotEmpty()) {
|
||||||
|
val plot = candidatePlots.first()
|
||||||
|
candidatePlots.remove(plot) // remove the plot as it has now been tried, whether successfully or not
|
||||||
|
if (plot.getBaseTerrain().hasUnique(
|
||||||
|
UniqueType.BlocksResources,
|
||||||
|
StateForConditionals(attackedTile = plot)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue // Don't put bonuses on snow hills
|
||||||
|
|
||||||
|
val validBonuses = ruleset.tileResources.values.filter {
|
||||||
|
it.resourceType == ResourceType.Bonus &&
|
||||||
|
it.food >= 1 &&
|
||||||
|
plot.lastTerrain.name in it.terrainsCanBeFoundOn
|
||||||
|
}
|
||||||
|
val goodPlotForOasis =
|
||||||
|
canPlaceOasis && plot.lastTerrain.name in oasisEquivalent!!.occursOn
|
||||||
|
|
||||||
|
if (validBonuses.isNotEmpty() || goodPlotForOasis) {
|
||||||
|
if (goodPlotForOasis) {
|
||||||
|
plot.addTerrainFeature(oasisEquivalent!!.name)
|
||||||
|
canPlaceOasis = false
|
||||||
|
} else {
|
||||||
|
plot.setTileResource(validBonuses.random())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plot.aerialDistanceTo(startTile) == 1) {
|
||||||
|
placedInFirst++
|
||||||
|
if (placedInFirst == 2) // Resort the list in ring order 2,3,1
|
||||||
|
candidatePlots.sortBy { abs(it.aerialDistanceTo(startTile) * 10 - 22) }
|
||||||
|
} else if (plot.aerialDistanceTo(startTile) == 2) {
|
||||||
|
placedInSecond++
|
||||||
|
if (placedInSecond == 3) // Resort the list in ring order 3,1,2
|
||||||
|
candidatePlots.sortByDescending { abs(it.aerialDistanceTo(startTile) * 10 - 17) }
|
||||||
|
}
|
||||||
|
bonusesStillNeeded--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPotentialYield(tile: Tile, stat: Stat, unimproved: Boolean = false): Float {
|
||||||
|
val baseYield = tile.stats.getTileStats(null)[stat]
|
||||||
|
if (unimproved) return baseYield
|
||||||
|
|
||||||
|
val bestImprovementYield = tile.tileMap.ruleset!!.tileImprovements.values
|
||||||
|
.filter { !it.hasUnique(UniqueType.GreatImprovement) &&
|
||||||
|
it.uniqueTo == null &&
|
||||||
|
tile.lastTerrain.name in it.terrainsCanBeBuiltOn }
|
||||||
|
.maxOfOrNull { it[stat] }
|
||||||
|
return baseYield + (bestImprovementYield ?: 0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user