Regions part 2 - City state placements, start normalization (#5663)

* start position normalization

* assignLuxuries

* City states placement

* city state normalization

* don't consider tiny islands

* also modify the other json since they are duplicated now
This commit is contained in:
SimonCeder 2021-11-11 11:11:48 +01:00 committed by GitHub
parent bc5ea2d90a
commit e4f686964e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 685 additions and 85 deletions

View File

@ -292,7 +292,8 @@
"gold": 2, "gold": 2,
"improvement": "Quarry", "improvement": "Quarry",
"improvementStats": {"production": 1}, "improvementStats": {"production": 1},
"uniques": ["[+15]% Production when constructing [All] wonders [in all cities]"] "uniques": ["[+15]% Production when constructing [All] wonders [in all cities]",
"Special placement during map generation"]
}, },
{ {
"name": "Whales", "name": "Whales",

View File

@ -292,7 +292,8 @@
"gold": 2, "gold": 2,
"improvement": "Quarry", "improvement": "Quarry",
"improvementStats": {"production": 1}, "improvementStats": {"production": 1},
"uniques": ["[+15]% Production when constructing [All] wonders [in all cities]"] "uniques": ["[+15]% Production when constructing [All] wonders [in all cities]",
"Special placement during map generation"]
}, },
{ {
"name": "Whales", "name": "Whales",

View File

@ -225,15 +225,7 @@ object GameStarter {
!it.value.hasUnique(UniqueType.CityStateDeprecated) !it.value.hasUnique(UniqueType.CityStateDeprecated)
}.keys }.keys
.shuffled() .shuffled()
.sortedByDescending { it in civNamesWithStartingLocations } ) .sortedBy { it in civNamesWithStartingLocations } ) // pop() gets the last item, so sort ascending
val allMercantileResources = ruleset.tileResources.values.filter {
it.hasUnique(UniqueType.CityStateOnlyResource) }.map { it.name }
val unusedMercantileResources = Stack<String>()
unusedMercantileResources.addAll(allMercantileResources.shuffled())
var addedCityStates = 0 var addedCityStates = 0
// Keep trying to add city states until we reach the target number. // Keep trying to add city states until we reach the target number.
@ -286,11 +278,6 @@ object GameStarter {
for (civ in gameInfo.civilizations.filter { !it.isBarbarian() && !it.isSpectator() }) { for (civ in gameInfo.civilizations.filter { !it.isBarbarian() && !it.isSpectator() }) {
val startingLocation = startingLocations[civ]!! val startingLocation = startingLocations[civ]!!
if(civ.isMajorCiv() && startScores[startingLocation]!! < 45) {
// An unusually bad spawning location
addConsolationPrize(gameInfo, startingLocation, 45 - startingLocation.getTileStartScore().toInt())
}
if(civ.isCityState()) if(civ.isCityState())
addCityStateLuxury(gameInfo, startingLocation) addCityStateLuxury(gameInfo, startingLocation)
@ -465,29 +452,6 @@ object GameStarter {
return preferredTiles.lastOrNull() ?: freeTiles.last() return preferredTiles.lastOrNull() ?: freeTiles.last()
} }
private fun addConsolationPrize(gameInfo: GameInfo, spawn: TileInfo, points: Int) {
val relevantTiles = spawn.getTilesInDistanceRange(1..2).shuffled()
var addedPoints = 0
var addedBonuses = 0
for (tile in relevantTiles) {
if (addedPoints >= points || addedBonuses >= 4) // At some point enough is enough
break
if (tile.resource != null || tile.baseTerrain == Constants.snow) // Snow is quite irredeemable
continue
val bonusToAdd = gameInfo.ruleSet.tileResources.values
.filter { it.terrainsCanBeFoundOn.contains(tile.getLastTerrain().name) && it.resourceType == ResourceType.Bonus }
.randomOrNull()
if (bonusToAdd != null) {
tile.resource = bonusToAdd.name
addedPoints += (bonusToAdd.food + bonusToAdd.production + bonusToAdd.gold + 1).toInt() // +1 because resources can be improved
addedBonuses++
}
}
}
private fun addCityStateLuxury(gameInfo: GameInfo, spawn: TileInfo) { private fun addCityStateLuxury(gameInfo: GameInfo, spawn: TileInfo) {
// Every city state should have at least one luxury to trade // Every city state should have at least one luxury to trade
val relevantTiles = spawn.getTilesInDistance(2).shuffled() val relevantTiles = spawn.getTilesInDistance(2).shuffled()

View File

@ -237,9 +237,9 @@ open class TileInfo {
return workingCity != null && workingCity.lockedTiles.contains(position) return workingCity != null && workingCity.lockedTiles.contains(position)
} }
fun getTileStats(observingCiv: CivilizationInfo): Stats = getTileStats(getCity(), observingCiv) fun getTileStats(observingCiv: CivilizationInfo?): Stats = getTileStats(getCity(), observingCiv)
fun getTileStats(city: CityInfo?, observingCiv: CivilizationInfo): Stats { fun getTileStats(city: CityInfo?, observingCiv: CivilizationInfo?): Stats {
var stats = getBaseTerrain().cloneStats() var stats = getBaseTerrain().cloneStats()
for (terrainFeatureBase in getTerrainFeatures()) { for (terrainFeatureBase in getTerrainFeatures()) {
@ -288,6 +288,9 @@ open class TileInfo {
stats.add(unique.stats) stats.add(unique.stats)
} }
if (isAdjacentToRiver()) stats.gold++
if (observingCiv != null) {
// resource base // resource base
if (hasViewableResource(observingCiv)) stats.add(tileResource) if (hasViewableResource(observingCiv)) stats.add(tileResource)
@ -300,11 +303,9 @@ open class TileInfo {
if (stats.production < 1) stats.production = 1f if (stats.production < 1) stats.production = 1f
} }
if (isAdjacentToRiver()) stats.gold++
if (stats.gold != 0f && observingCiv.goldenAges.isGoldenAge()) if (stats.gold != 0f && observingCiv.goldenAges.isGoldenAge())
stats.gold++ stats.gold++
}
for ((stat, value) in stats) for ((stat, value) in stats)
if (value < 0f) stats[stat] = 0f if (value < 0f) stats[stat] = 0f

View File

@ -78,6 +78,8 @@ class MapGenerator(val ruleset: Ruleset) {
runAndMeasure("RiverGenerator") { runAndMeasure("RiverGenerator") {
RiverGenerator(map, randomness).spawnRivers() RiverGenerator(map, randomness).spawnRivers()
} }
// Region based map generation - not used when generating maps in worldbuilder
if (civilizations.isNotEmpty()) {
val regions = MapRegions(ruleset) val regions = MapRegions(ruleset)
runAndMeasure("generateRegions") { runAndMeasure("generateRegions") {
regions.generateRegions(map, civilizations.count { ruleset.nations[it.civName]!!.isMajorCiv() }) regions.generateRegions(map, civilizations.count { ruleset.nations[it.civName]!!.isMajorCiv() })
@ -85,6 +87,10 @@ class MapGenerator(val ruleset: Ruleset) {
runAndMeasure("assignRegions") { runAndMeasure("assignRegions") {
regions.assignRegions(map, civilizations.filter { ruleset.nations[it.civName]!!.isMajorCiv() }) regions.assignRegions(map, civilizations.filter { ruleset.nations[it.civName]!!.isMajorCiv() })
} }
runAndMeasure("placeResourcesAndMinorCivs") {
regions.placeResourcesAndMinorCivs(map, civilizations.filter { ruleset.nations[it.civName]!!.isCityState() })
}
}
runAndMeasure("NaturalWonderGenerator") { runAndMeasure("NaturalWonderGenerator") {
NaturalWonderGenerator(ruleset, randomness).spawnNaturalWonders(map) NaturalWonderGenerator(ruleset, randomness).spawnNaturalWonders(map)
} }
@ -169,8 +175,10 @@ class MapGenerator(val ruleset: Ruleset) {
private fun spreadResources(tileMap: TileMap) { private fun spreadResources(tileMap: TileMap) {
val mapRadius = tileMap.mapParameters.mapSize.radius val mapRadius = tileMap.mapParameters.mapSize.radius
for (tile in tileMap.values) // Commenting this out for now not to interfere with start normalization - will be restored when
tile.resource = null // region-based resource placement is implemented, then this function will be map editor only.
/*for (tile in tileMap.values)
tile.resource = null*/
spreadStrategicResources(tileMap, mapRadius) spreadStrategicResources(tileMap, mapRadius)
spreadResources(tileMap, mapRadius, ResourceType.Luxury) spreadResources(tileMap, mapRadius, ResourceType.Luxury)

View File

@ -2,21 +2,24 @@ package com.unciv.logic.map.mapgenerator
import com.badlogic.gdx.math.Rectangle import com.badlogic.gdx.math.Rectangle
import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.logic.HexMath import com.unciv.logic.HexMath
import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.MapShape import com.unciv.logic.map.MapShape
import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap import com.unciv.logic.map.TileMap
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.Terrain
import com.unciv.models.ruleset.tile.TerrainType import com.unciv.models.ruleset.tile.TerrainType
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.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 kotlin.math.abs import com.unciv.ui.utils.randomWeighted
import kotlin.math.max import kotlin.math.*
import kotlin.math.min
import kotlin.math.roundToInt
class MapRegions (val ruleset: Ruleset){ class MapRegions (val ruleset: Ruleset){
companion object { companion object {
@ -37,7 +40,10 @@ class MapRegions (val ruleset: Ruleset){
} }
private val regions = ArrayList<Region>() private val regions = ArrayList<Region>()
private var usingArchipelagoRegions = false
private val tileData = HashMap<Vector2, MapGenTileData>() private val tileData = HashMap<Vector2, MapGenTileData>()
private val cityStateLuxuries = ArrayList<String>()
private val randomLuxuries = ArrayList<String>()
/** Creates [numRegions] number of balanced regions for civ starting locations. */ /** Creates [numRegions] number of balanced regions for civ starting locations. */
fun generateRegions(tileMap: TileMap, numRegions: Int) { fun generateRegions(tileMap: TileMap, numRegions: Int) {
@ -56,6 +62,7 @@ class MapRegions (val ruleset: Ruleset){
// Lots of small islands - just split ut the map in rectangles while ignoring Continents // Lots of small islands - just split ut the map in rectangles while ignoring Continents
// 25% is chosen as limit so Four Corners maps don't fall in this category // 25% is chosen as limit so Four Corners maps don't fall in this category
if (largestContinent / totalLand < 0.25f) { if (largestContinent / totalLand < 0.25f) {
usingArchipelagoRegions = true
// Make a huge rectangle covering the entire map // Make a huge rectangle covering the entire map
val hugeRect = Region(tileMap, mapRect, -1) // -1 meaning ignore continent data val hugeRect = Region(tileMap, mapRect, -1) // -1 meaning ignore continent data
hugeRect.affectedByWorldWrap = false // Might as well start at the seam hugeRect.affectedByWorldWrap = false // Might as well start at the seam
@ -178,12 +185,8 @@ class MapRegions (val ruleset: Ruleset){
if (civilizations.isEmpty()) return if (civilizations.isEmpty()) return
// first assign region types // first assign region types
val regionTypes = ruleset.terrains.values.filter { it.hasUnique(UniqueType.RegionRequirePercentSingleType) || val regionTypes = ruleset.terrains.values.filter { getRegionPriority(it) != null }
it.hasUnique(UniqueType.RegionRequirePercentTwoTypes) } .sortedBy { getRegionPriority(it) }
.sortedBy { if (it.hasUnique(UniqueType.RegionRequirePercentSingleType))
it.getMatchingUniques(UniqueType.RegionRequirePercentSingleType).first().params[2].toInt()
else
it.getMatchingUniques(UniqueType.RegionRequirePercentTwoTypes).first().params[3].toInt() }
for (region in regions) { for (region in regions) {
region.countTerrains() region.countTerrains()
@ -219,6 +222,10 @@ class MapRegions (val ruleset: Ruleset){
for (region in sortedRegions) { for (region in sortedRegions) {
findStart(region) findStart(region)
} }
// Normalize starts
for (region in regions) {
normalizeStart(tileMap[region.startPosition!!], minorCiv = false)
}
val coastBiasCivs = civilizations.filter { ruleset.nations[it.civName]!!.startBias.contains("Coast") } val coastBiasCivs = civilizations.filter { ruleset.nations[it.civName]!!.startBias.contains("Coast") }
val negativeBiasCivs = civilizations.filter { ruleset.nations[it.civName]!!.startBias.any { bias -> bias.equalsPlaceholderText("Avoid []") } } val negativeBiasCivs = civilizations.filter { ruleset.nations[it.civName]!!.startBias.any { bias -> bias.equalsPlaceholderText("Avoid []") } }
@ -228,35 +235,40 @@ class MapRegions (val ruleset: Ruleset){
val positiveBiasCivs = civilizations.filterNot { it in coastBiasCivs || it in negativeBiasCivs || it in randomCivs } val positiveBiasCivs = civilizations.filterNot { it in coastBiasCivs || it in negativeBiasCivs || it in randomCivs }
.sortedBy { ruleset.nations[it.civName]!!.startBias.count() } // civs with only one desired region go first .sortedBy { ruleset.nations[it.civName]!!.startBias.count() } // civs with only one desired region go first
val positiveBiasFallbackCivs = ArrayList<CivilizationInfo>() // Civs who couln't get their desired region at first pass val positiveBiasFallbackCivs = ArrayList<CivilizationInfo>() // Civs who couln't get their desired region at first pass
val unpickedRegions = regions.toMutableList()
// First assign coast bias civs // First assign coast bias civs
for (civ in coastBiasCivs) { for (civ in coastBiasCivs) {
// Try to find a coastal start, preferably a really coastal one // Try to find a coastal start, preferably a really coastal one
var startRegion = regions.filter { tileMap[it.startPosition!!].isCoastalTile() } var startRegion = unpickedRegions.filter { tileMap[it.startPosition!!].isCoastalTile() }
.maxByOrNull { it.terrainCounts["Coastal"] ?: 0 } .maxByOrNull { it.terrainCounts["Coastal"] ?: 0 }
if (startRegion != null) { if (startRegion != null) {
assignCivToRegion(civ, startRegion) assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion)
continue continue
} }
// Else adjacent to a lake // Else adjacent to a lake
startRegion = regions.filter { tileMap[it.startPosition!!].neighbors.any { neighbor -> neighbor.getBaseTerrain().hasUnique(UniqueType.FreshWater) } } startRegion = unpickedRegions.filter { tileMap[it.startPosition!!].neighbors.any { neighbor -> neighbor.getBaseTerrain().hasUnique(UniqueType.FreshWater) } }
.maxByOrNull { it.terrainCounts["Coastal"] ?: 0 } .maxByOrNull { it.terrainCounts["Coastal"] ?: 0 }
if (startRegion != null) { if (startRegion != null) {
assignCivToRegion(civ, startRegion) assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion)
continue continue
} }
// Else adjacent to a river // Else adjacent to a river
startRegion = regions.filter { tileMap[it.startPosition!!].isAdjacentToRiver() } startRegion = unpickedRegions.filter { tileMap[it.startPosition!!].isAdjacentToRiver() }
.maxByOrNull { it.terrainCounts["Coastal"] ?: 0 } .maxByOrNull { it.terrainCounts["Coastal"] ?: 0 }
if (startRegion != null) { if (startRegion != null) {
assignCivToRegion(civ, startRegion) assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion)
continue continue
} }
// Else at least close to a river ???? // Else at least close to a river ????
startRegion = regions.filter { tileMap[it.startPosition!!].neighbors.any { neighbor -> neighbor.isAdjacentToRiver() } } startRegion = unpickedRegions.filter { tileMap[it.startPosition!!].neighbors.any { neighbor -> neighbor.isAdjacentToRiver() } }
.maxByOrNull { it.terrainCounts["Coastal"] ?: 0 } .maxByOrNull { it.terrainCounts["Coastal"] ?: 0 }
if (startRegion != null) { if (startRegion != null) {
assignCivToRegion(civ, startRegion) assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion)
continue continue
} }
// Else pick a random region at the end // Else pick a random region at the end
@ -267,10 +279,11 @@ class MapRegions (val ruleset: Ruleset){
for (civ in positiveBiasCivs) { for (civ in positiveBiasCivs) {
// Try to find a start that matches any of the desired regions, ideally with lots of desired terrain // Try to find a start that matches any of the desired regions, ideally with lots of desired terrain
val preferred = ruleset.nations[civ.civName]!!.startBias val preferred = ruleset.nations[civ.civName]!!.startBias
val startRegion = regions.filter { it.type in preferred } val startRegion = unpickedRegions.filter { it.type in preferred }
.maxByOrNull { it.terrainCounts.filterKeys { terrain -> terrain in preferred }.values.sum() } .maxByOrNull { it.terrainCounts.filterKeys { terrain -> terrain in preferred }.values.sum() }
if (startRegion != null) { if (startRegion != null) {
assignCivToRegion(civ, startRegion) assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion)
continue continue
} else if (ruleset.nations[civ.civName]!!.startBias.count() == 1) { // Civs with a single bias (only) get to look for a fallback region } else if (ruleset.nations[civ.civName]!!.startBias.count() == 1) { // Civs with a single bias (only) get to look for a fallback region
positiveBiasFallbackCivs.add(civ) positiveBiasFallbackCivs.add(civ)
@ -281,17 +294,20 @@ class MapRegions (val ruleset: Ruleset){
// Do a second pass for fallback civs, choosing the region most similar to the desired type // Do a second pass for fallback civs, choosing the region most similar to the desired type
for (civ in positiveBiasFallbackCivs) { for (civ in positiveBiasFallbackCivs) {
assignCivToRegion(civ, getFallbackRegion(ruleset.nations[civ.civName]!!.startBias.first())) val startRegion = getFallbackRegion(ruleset.nations[civ.civName]!!.startBias.first(), unpickedRegions)
assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion)
} }
// Next do negative bias ones (ie "Avoid []") // Next do negative bias ones (ie "Avoid []")
for (civ in negativeBiasCivs) { for (civ in negativeBiasCivs) {
val avoided = ruleset.nations[civ.civName]!!.startBias.map { it.getPlaceholderParameters()[0] } val avoided = ruleset.nations[civ.civName]!!.startBias.map { it.getPlaceholderParameters()[0] }
// Try to find a region not of the avoided types, secondary sort by least number of undesired terrains // Try to find a region not of the avoided types, secondary sort by least number of undesired terrains
val startRegion = regions.filterNot { it.type in avoided } val startRegion = unpickedRegions.filterNot { it.type in avoided }
.minByOrNull { it.terrainCounts.filterKeys { terrain -> terrain in avoided }.values.sum() } .minByOrNull { it.terrainCounts.filterKeys { terrain -> terrain in avoided }.values.sum() }
if (startRegion != null) { if (startRegion != null) {
assignCivToRegion(civ, startRegion) assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion)
continue continue
} else } else
randomCivs.add(civ) // else pick a random region at the end randomCivs.add(civ) // else pick a random region at the end
@ -299,14 +315,38 @@ class MapRegions (val ruleset: Ruleset){
// Finally assign the remaining civs randomly // Finally assign the remaining civs randomly
for (civ in randomCivs) { for (civ in randomCivs) {
val startRegion = regions.random() val startRegion = unpickedRegions.random()
assignCivToRegion(civ, startRegion) assignCivToRegion(civ, startRegion)
unpickedRegions.remove(startRegion)
} }
} }
private fun getRegionPriority(terrain: Terrain?): Int? {
if (terrain == null) // ie "hybrid"
return 99999 // a big number
if (!terrain.hasUnique(UniqueType.RegionRequirePercentSingleType) &&
!terrain.hasUnique(UniqueType.RegionRequirePercentTwoTypes))
return null
else
return if (terrain.hasUnique(UniqueType.RegionRequirePercentSingleType))
terrain.getMatchingUniques(UniqueType.RegionRequirePercentSingleType).first().params[2].toInt()
else
terrain.getMatchingUniques(UniqueType.RegionRequirePercentTwoTypes).first().params[3].toInt()
}
private fun assignCivToRegion(civInfo: CivilizationInfo, region: Region) { private fun assignCivToRegion(civInfo: CivilizationInfo, region: Region) {
region.tileMap.addStartingLocation(civInfo.civName, region.tileMap[region.startPosition!!]) val tile = region.tileMap[region.startPosition!!]
regions.remove(region) // This region can no longer be picked region.tileMap.addStartingLocation(civInfo.civName, tile)
// Place impacts to keep city states etc at appropriate distance
placeImpact(ImpactType.MinorCiv,tile, 6)
/* lets leave these commented until resource placement is actually implemented
placeImpact(ImpactType.Luxury, tile, 3)
placeImpact(ImpactType.Strategic,tile, 0)
placeImpact(ImpactType.Bonus, tile, 3)
placeImpact(ImpactType.Fish, tile, 3)
placeImpact(ImpactType.NaturalWonder, tile, 4)
*/
} }
/** Attempts to find a good start close to the center of [region]. Calls setRegionStart with the position*/ /** Attempts to find a good start close to the center of [region]. Calls setRegionStart with the position*/
@ -422,9 +462,214 @@ 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 [minorCiv] is true, different weightings will be used. */
private fun normalizeStart(startTile: TileInfo, minorCiv: Boolean) {
// Remove ice-like features adjacent to start
for (tile in startTile.neighbors) {
val lastTerrain = tile.getTerrainFeatures().lastOrNull { it.impassable }
if (lastTerrain != null) {
tile.terrainFeatures.remove(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) || (minorCiv && innerProduction < 4)) {
val hillSpot = startTile.neighbors
.filter { it.isLand && it.terrainFeatures.isEmpty() && !it.isAdjacentToFreshwater }
.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.terrainFeatures.add(hillEquivalent)
}
}
// TODO: Strategic Balance Resources
// If bad early production, add a small strategic resource to SECOND ring (not for minors)
if (!minorCiv && 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)
}
if (validResources.isNotEmpty()) {
for (tile in startTile.getTilesAtDistance(2).shuffled()) {
val resourceToPlace = validResources.filter { tile.getLastTerrain().name in it.terrainsCanBeFoundOn }.randomOrNull()
if (resourceToPlace != null) {
tile.setTileResource(resourceToPlace, majorDeposit = false)
break
}
}
}
}
// Now 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
}
}
// TODO: Legendary start? +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
}
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.getTerrainFeatures() }
.shuffled().sortedBy { it.aerialDistanceTo(startTile) }.toMutableList()
// Place food bonuses (and oases) as able
while (bonusesNeeded > 0 && candidatePlots.isNotEmpty()) {
val plot = candidatePlots.first()
candidatePlots.remove(plot) // remove the plot as it has now been tried, whether successfully or not
val validBonuses = ruleset.tileResources.values.filter {
it.resourceType == ResourceType.Bonus &&
it.food >= 1 &&
plot.getLastTerrain().name in it.terrainsCanBeFoundOn
}
val goodPlotForOasis = canPlaceOasis && plot.getLastTerrain().name in oasisEquivalent!!.occursOn
if (validBonuses.isNotEmpty() || goodPlotForOasis) {
if (goodPlotForOasis) {
plot.terrainFeatures.add(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) }
}
bonusesNeeded--
}
}
// Minor civs are done, go on with grassiness checks for major civs
if (minorCiv) return
// Check for very grass-heavy starts that might still need some stone to help with production
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 stoneNeeded = when {
grassTypePlots.count() >= 9 && plainsTypePlots.isEmpty() -> 2
grassTypePlots.count() >= 6 && plainsTypePlots.count() <= 4 -> 1
else -> 0
}
val stoneTypeBonuses = ruleset.tileResources.values.filter { it.resourceType == ResourceType.Bonus && it.production > 0 }
if(stoneTypeBonuses.isNotEmpty()) {
while (stoneNeeded > 0 && grassTypePlots.isNotEmpty()) {
val plot = grassTypePlots.random()
grassTypePlots.remove(plot)
if (plot.resource != null) continue
val bonusToPlace = stoneTypeBonuses.filter { plot.getLastTerrain().name in it.terrainsCanBeFoundOn }.randomOrNull()
if (bonusToPlace != null) {
plot.resource = bonusToPlace.name
stoneNeeded--
}
}
}
}
private fun getPotentialYield(tile: TileInfo, stat: Stat, unimproved: Boolean = false): Float {
val baseYield = tile.getTileStats(null)[stat]
if (unimproved) return baseYield
val bestImprovementYield = tile.tileMap.ruleset!!.tileImprovements.values
.filter { !it.hasUnique(UniqueType.GreatImprovement) &&
it.uniqueTo == null &&
tile.getLastTerrain().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): Region { private fun getFallbackRegion(type: String, candidates: List<Region>): Region {
return regions.maxByOrNull { it.terrainCounts[type] ?: 0 }!! return candidates.maxByOrNull { it.terrainCounts[type] ?: 0 }!!
} }
private fun setRegionStart(region: Region, position: Vector2) { private fun setRegionStart(region: Region, position: Vector2) {
@ -528,9 +773,386 @@ class MapRegions (val ruleset: Ruleset){
localData.startScore = totalScore localData.startScore = totalScore
} }
fun placeResourcesAndMinorCivs(tileMap: TileMap, minorCivs: List<CivilizationInfo>) {
assignLuxuries()
placeMinorCivs(tileMap, minorCivs)
// TODO: place luxuries
// TODO: place strategic and bonus resources
}
/** Assigns a luxury to each region. No luxury can be assigned to too many regions.
* Some luxuries are earmarked for city states. The rest are randomly distributed or
* don't occur att all in the map */
private fun assignLuxuries() {
// If there are any weightings defined in json, assume they are complete. If there are none, use flat weightings instead
val fallbackWeightings = ruleset.tileResources.values.none {
it.resourceType == ResourceType.Luxury &&
(it.hasUnique(UniqueType.LuxuryWeighting) || it.hasUnique(UniqueType.LuxuryWeightingForCityStates)) }
val maxRegionsWithLuxury = if (regions.count() > 12) 3 else 2
val targetCityStateLuxuries = 3 // was probably intended to be "if (tileData.size > 5000) 4 else 3"
val disabledPercent = 100 - min(tileData.size.toFloat().pow(0.2f) * 16, 100f).toInt() // Approximately
val targetDisabledLuxuries = (ruleset.tileResources.values
.count { it.resourceType == ResourceType.Luxury } * disabledPercent) / 100
val amountRegionsWithLuxury = HashMap<String, Int>()
// Init map
ruleset.tileResources.values
.forEach { amountRegionsWithLuxury[it.name] = 0 }
for (region in regions.sortedBy { getRegionPriority(ruleset.terrains[it.type]) } ) {
var candidateLuxuries = ruleset.tileResources.values.filter {
it.resourceType == ResourceType.Luxury &&
amountRegionsWithLuxury[it.name]!! < maxRegionsWithLuxury &&
// Check that it has a weight for this region type
(fallbackWeightings ||
it.getMatchingUniques(UniqueType.LuxuryWeighting).any { unique -> unique.params[0] == region.type } ) &&
// Check that there is enough coast if it is a water based resource
((region.terrainCounts["Coastal"] ?: 0) >= 12 ||
it.terrainsCanBeFoundOn.any { terrain -> ruleset.terrains[terrain]!!.type != TerrainType.Water } )
}
// If we couldn't find any options, pick from all luxuries. First try to not pick water luxuries on land regions
if (candidateLuxuries.isEmpty()) {
candidateLuxuries = ruleset.tileResources.values.filter {
it.resourceType == ResourceType.Luxury &&
amountRegionsWithLuxury[it.name]!! < maxRegionsWithLuxury &&
// Ignore weightings for this pass
// Check that there is enough coast if it is a water based resource
((region.terrainCounts["Coastal"] ?: 0) >= 12 ||
it.terrainsCanBeFoundOn.any { terrain -> ruleset.terrains[terrain]!!.type != TerrainType.Water })
}
}
// If there are still no candidates, ignore water restrictions
if (candidateLuxuries.isEmpty()) {
candidateLuxuries = ruleset.tileResources.values.filter {
it.resourceType == ResourceType.Luxury &&
amountRegionsWithLuxury[it.name]!! < maxRegionsWithLuxury
// Ignore weightings and water for this pass
}
}
// If there are still no candidates (mad modders???) just skip this region
if (candidateLuxuries.isEmpty()) continue
// Pick a luxury at random. Weight is reduced if the luxury has been picked before
val modifiedWeights = candidateLuxuries.map {
val weightingUnique = it.getMatchingUniques(UniqueType.LuxuryWeighting)
.filter { unique -> unique.params[0] == region.type }.firstOrNull()
if (weightingUnique == null)
1f / (1f + amountRegionsWithLuxury[it.name]!!)
else
weightingUnique.params[1].toFloat() / (1f + amountRegionsWithLuxury[it.name]!!)
}
region.luxury = candidateLuxuries.randomWeighted(modifiedWeights).name
amountRegionsWithLuxury[region.luxury!!] = amountRegionsWithLuxury[region.luxury]!! + 1
}
// Assign luxuries to City States
for (i in 1..targetCityStateLuxuries) {
val candidateLuxuries = ruleset.tileResources.values.filter {
it.resourceType == ResourceType.Luxury &&
amountRegionsWithLuxury[it.name] == 0 &&
(fallbackWeightings || it.hasUnique(UniqueType.LuxuryWeightingForCityStates))
}
if (candidateLuxuries.isEmpty()) continue
val weights = candidateLuxuries.map {
val weightingUnique = it.getMatchingUniques(UniqueType.LuxuryWeightingForCityStates).firstOrNull()
if (weightingUnique == null)
1f
else
weightingUnique.params[0].toFloat()
}
val luxury = candidateLuxuries.randomWeighted(weights).name
cityStateLuxuries.add(luxury)
amountRegionsWithLuxury[luxury] = 1
}
// Assign some resources as random placement. Marble is never random.
val remainingLuxuries = ruleset.tileResources.values.filter {
it.resourceType == ResourceType.Luxury &&
amountRegionsWithLuxury[it.name] == 0 &&
!it.hasUnique(UniqueType.LuxurySpecialPlacement)
}.map { it.name }.shuffled()
randomLuxuries.addAll(remainingLuxuries.drop(targetDisabledLuxuries))
}
/** Assigns [civs] to regions or "uninhabited" land and places them. Depends on
* assignLuxuries having been called previously.
* Note: can silently fail to place all city states if there is too little room.
* Currently our GameStarter fills out with random city states, Civ V behavior is to
* forget about the discarded city states entirely. */
private fun placeMinorCivs(tileMap: TileMap, civs: List<CivilizationInfo>) {
if (civs.isEmpty()) return
// Some but not all city states are assigned to regions directly. Determine the CS density.
val minorCivRatio = civs.count().toFloat() / regions.count()
val minorCivPerRegion = when {
minorCivRatio > 14f -> 10 // lol
minorCivRatio > 11f -> 8
minorCivRatio > 8f -> 6
minorCivRatio > 5.7f -> 4
minorCivRatio > 4.35f -> 3
minorCivRatio > 2.7f -> 2
minorCivRatio > 1.35f -> 1
else -> 0
}
val unassignedCivs = civs.shuffled().toMutableList()
if (minorCivPerRegion > 0) {
regions.forEach {
val civsToAssign = unassignedCivs.take(minorCivPerRegion)
it.assignedMinorCivs.addAll(civsToAssign)
unassignedCivs.removeAll(civsToAssign)
}
}
// Some city states are assigned to "uninhabited" continents - unless it's an archipelago type map
// (Because then every continent will have been assigned to a region anyway)
val uninhabitedCoastal = ArrayList<TileInfo>()
val uninhabitedHinterland = ArrayList<TileInfo>()
val uninhabitedContinents = tileMap.continentSizes.filter {
it.value >= 4 && // Don't bother with tiny islands
regions.none { region -> region.continentID == it.key }
}.keys
val civAssignedToUninhabited = ArrayList<CivilizationInfo>()
var numUninhabitedTiles = 0
var numInhabitedTiles = 0
if (!usingArchipelagoRegions) {
// Go through the entire map to build the data
for (tile in tileMap.values) {
if (!canPlaceMinorCiv(tile)) continue
val continent = tile.getContinent()
if (continent in uninhabitedContinents) {
if(tile.isCoastalTile())
uninhabitedCoastal.add(tile)
else
uninhabitedHinterland.add(tile)
numUninhabitedTiles++
} else
numInhabitedTiles++
}
// Determine how many minor civs to put on uninhabited continents.
val maxByUninhabited = (3 * civs.count() * numUninhabitedTiles) / (numInhabitedTiles + numUninhabitedTiles)
val maxByRatio = (civs.count() + 1) / 2
val targetForUninhabited = min(maxByRatio, maxByUninhabited)
val civsToAssign = unassignedCivs.take(targetForUninhabited)
unassignedCivs.removeAll(civsToAssign)
civAssignedToUninhabited.addAll(civsToAssign)
}
// If there are still unassigned minor civs, assign extra ones to regions that share their
// luxury type with two others, as compensation. Because starting close to a city state is good??
if (unassignedCivs.isNotEmpty()) {
val regionsWithCommonLuxuries = regions.filter {
regions.count { other -> other.luxury == it.luxury } >= 3
}
// assign one civ each to regions with common luxuries if there are enough to go around
if (regionsWithCommonLuxuries.count() > 0 &&
regionsWithCommonLuxuries.count() <= unassignedCivs.count()) {
regionsWithCommonLuxuries.forEach {
val civToAssign = unassignedCivs.first()
unassignedCivs.remove(civToAssign)
it.assignedMinorCivs.add(civToAssign)
}
}
}
// Still unassigned civs??
if (unassignedCivs.isNotEmpty()) {
// Add one extra to each region as long as there are enough to go around
while (unassignedCivs.count() >= regions.count()) {
regions.forEach {
val civToAssign = unassignedCivs.first()
unassignedCivs.remove(civToAssign)
it.assignedMinorCivs.add(civToAssign)
}
}
// STILL unassigned civs??
if (unassignedCivs.isNotEmpty()) {
// At this point there is at least for sure less remaining city states than regions
// Sort regions by fertility and put extra city states in the worst ones.
val worstRegions = regions.sortedBy { it.totalFertility }.take(unassignedCivs.count())
worstRegions.forEach {
val civToAssign = unassignedCivs.first()
unassignedCivs.remove(civToAssign)
it.assignedMinorCivs.add(civToAssign)
}
}
}
// All minor civs are assigned - now place them
// First place the "uninhabited continent" ones, preferring coastal starts
tryPlaceMinorCivsInTiles(civAssignedToUninhabited, tileMap, uninhabitedCoastal)
tryPlaceMinorCivsInTiles(civAssignedToUninhabited, tileMap, uninhabitedHinterland)
// Fallback to a random region for civs that couldn't be placed in the wilderness
for (unplacedCiv in civAssignedToUninhabited) {
regions.random().assignedMinorCivs.add(unplacedCiv)
}
// Fallback lists for minor civs that can't be placed with any other method
val fallbackTiles = ArrayList<TileInfo>()
val fallbackMinors = ArrayList<CivilizationInfo>()
// Now place the ones assigned to specific regions.
for (region in regions) {
// Check the outer edges of the region, working inwards
val section = Rectangle(region.rect)
val unprocessedTiles = ArrayList<TileInfo>()
val regionCoastal = ArrayList<TileInfo>()
val regionHinterland = ArrayList<TileInfo>()
while (section.width >= 4 && section.height >= 4 && region.assignedMinorCivs.isNotEmpty()) {
// Clear the tile lists
unprocessedTiles.clear()
regionCoastal.clear()
regionHinterland.clear()
if (section.height > section.width) {
// Check top and bottom
unprocessedTiles.addAll(
tileMap.getTilesInRectangle(
Rectangle(section.x, section.y, section.width, 1f),
evenQ = true)
)
unprocessedTiles.addAll(
tileMap.getTilesInRectangle(
Rectangle(section.x, section.y + section.height - 1, section.width, 1f),
evenQ = true)
)
// Narrow the remaining section
section.y += 1
section.height -= 2
} else {
// Check left and right
unprocessedTiles.addAll(
tileMap.getTilesInRectangle(
Rectangle(section.x, section.y, 1f, section.height),
evenQ = true)
)
unprocessedTiles.addAll(
tileMap.getTilesInRectangle(
Rectangle(section.x + section.width - 1, section.y, 1f, section.height),
evenQ = true)
)
// Narrow the remaining section
section.x += 1
section.width -= 2
}
// Now process the tiles
for (tile in unprocessedTiles) {
if (!canPlaceMinorCiv(tile)) continue
if (!usingArchipelagoRegions && tile.getContinent() != region.continentID) continue
if(tile.isCoastalTile())
regionCoastal.add(tile)
else
regionHinterland.add(tile)
}
// Now attempt to place as many minor civs as possible, trying coastal tiles first
tryPlaceMinorCivsInTiles(region.assignedMinorCivs, tileMap, regionCoastal)
tryPlaceMinorCivsInTiles(region.assignedMinorCivs, tileMap, regionHinterland)
}
// In case we went through the entire region without finding spots for all assigned civs
if(region.assignedMinorCivs.isNotEmpty()) {
fallbackMinors.addAll(region.assignedMinorCivs)
} else {
// If we did find spots for all civs, there might be more eligible tiles left in the region
// Add them to the fallback list
fallbackTiles.addAll(regionCoastal)
fallbackTiles.addAll(regionHinterland)
fallbackTiles.addAll(tileMap.getTilesInRectangle(section, evenQ = true)
.filter { canPlaceMinorCiv(it) }
)
}
}
// Finally attempt to place the fallback lists - the rest will be silently discarded
if (fallbackMinors.isNotEmpty()) {
// Throw in the uninhabited lists as well
fallbackTiles.addAll(uninhabitedCoastal)
fallbackTiles.addAll(uninhabitedHinterland)
tryPlaceMinorCivsInTiles(fallbackMinors, tileMap, fallbackTiles)
}
}
/** Attempts to randomly place civs from [civsToPlace] in tiles from [tileList]. Assumes that
* [tileList] is pre-vetted and only contains habitable land tiles.
* Will modify both [civsToPlace] and [tileList] as it goes! */
private fun tryPlaceMinorCivsInTiles(civsToPlace: MutableList<CivilizationInfo>, tileMap: TileMap, tileList: MutableList<TileInfo>) {
while (tileList.isNotEmpty() && civsToPlace.isNotEmpty()) {
val chosenTile = tileList.random()
tileList.remove(chosenTile)
val data = tileData[chosenTile.position]!!
// If the randomly chosen tile is too close to a player or a city state, discard it
if (data.impacts.containsKey(ImpactType.MinorCiv))
continue
// Otherwise, go ahead and place the minor civ
val civToAdd = civsToPlace.first()
civsToPlace.remove(civToAdd)
placeMinorCiv(civToAdd, tileMap, chosenTile)
}
}
private fun canPlaceMinorCiv(tile: TileInfo) = !tile.isWater && !tile.isImpassible() &&
!tileData[tile.position]!!.isJunk &&
tile.getBaseTerrain().getMatchingUniques(UniqueType.HasQuality).none { it.params[0] == "Undesirable" } && // So we don't get snow hills
tile.neighbors.count() == 6 // Avoid map edges
private fun placeMinorCiv(civ: CivilizationInfo, tileMap: TileMap, tile: TileInfo) {
tileMap.addStartingLocation(civ.civName, tile)
placeImpact(ImpactType.MinorCiv,tile, 4)
/* lets leave these commented until resource placement is actually implemented
placeImpact(ImpactType.Luxury, tile, 3)
placeImpact(ImpactType.Strategic,tile, 0)
placeImpact(ImpactType.Bonus, tile, 3)
placeImpact(ImpactType.Fish, tile, 3)
placeImpact(ImpactType.Marble, tile, 4) */
normalizeStart(tile, minorCiv = true)
}
/** Adds numbers to tileData in a similar way to closeStartPenalty, but for different types */
private fun placeImpact(type: ImpactType, tile: TileInfo, radius: Int) {
// Epicenter
if (type == ImpactType.Fish || type == ImpactType.Marble)
tileData[tile.position]!!.impacts[type] = 1 // These use different values
else
tileData[tile.position]!!.impacts[type] = 99
if (radius <= 0) return
for (ring in 1..radius) {
val ringValue = radius - ring + 1
for (outerTile in tile.getTilesAtDistance(ring)) {
val data = tileData[outerTile.position]!!
when (type) {
ImpactType.Marble,
ImpactType.MinorCiv -> data.impacts[type] = 1
ImpactType.Fish -> {
if (data.impacts.containsKey(type))
data.impacts[type] = min(10, max(ringValue, data.impacts[type]!!) + 1)
else
data.impacts[type] = ringValue
}
else -> {
if (data.impacts.containsKey(type))
data.impacts[type] = min(50, max(ringValue, data.impacts[type]!!) + 2)
else
data.impacts[type] = ringValue
}
}
}
}
}
enum class ImpactType {
Strategic,
Luxury,
Bonus,
Fish,
MinorCiv,
NaturalWonder,
Marble,
}
// Holds a bunch of tile info that is only interesting during map gen // Holds a bunch of tile info that is only interesting during map gen
class MapGenTileData(val tile: TileInfo, val region: Region?) { class MapGenTileData(val tile: TileInfo, val region: Region?) {
var closeStartPenalty = 0 var closeStartPenalty = 0
val impacts = HashMap<ImpactType, Int>()
var isFood = false var isFood = false
var isProd = false var isProd = false
var isGood = false var isGood = false
@ -606,7 +1228,9 @@ class Region (val tileMap: TileMap, val rect: Rectangle, val continentID: Int =
val terrainCounts = HashMap<String, Int>() val terrainCounts = HashMap<String, Int>()
var totalFertility = 0 var totalFertility = 0
var type = "Hybrid" // being an undefined or indeterminate type var type = "Hybrid" // being an undefined or indeterminate type
var luxury: String? = null
var startPosition: Vector2? = null var startPosition: Vector2? = null
val assignedMinorCivs = ArrayList<CivilizationInfo>()
var affectedByWorldWrap = false var affectedByWorldWrap = false

View File

@ -320,6 +320,7 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget, val flags:
LuxuryWeighting("Appears in [regionType] regions with weight [amount]", UniqueTarget.Resource, flags = listOf(UniqueFlag.HideInCivilopedia)), LuxuryWeighting("Appears in [regionType] regions with weight [amount]", UniqueTarget.Resource, flags = listOf(UniqueFlag.HideInCivilopedia)),
LuxuryWeightingForCityStates("Appears near City States with weight [amount]", UniqueTarget.Resource, flags = listOf(UniqueFlag.HideInCivilopedia)), LuxuryWeightingForCityStates("Appears near City States with weight [amount]", UniqueTarget.Resource, flags = listOf(UniqueFlag.HideInCivilopedia)),
LuxurySpecialPlacement("Special placement during map generation", UniqueTarget.Resource, flags = listOf(UniqueFlag.HideInCivilopedia)),
OverrideDepositAmountOnTileFilter("Deposits in [tileFilter] tiles always provide [amount] resources", UniqueTarget.Resource), OverrideDepositAmountOnTileFilter("Deposits in [tileFilter] tiles always provide [amount] resources", UniqueTarget.Resource),