Mods can use the Hills and mountains distribution uniques on Land or Feature terrains (#11020)

* Refactor and rewrite raiseMountainsAndHills to allow hill and mountain uniques on land+feature terrain types

* Optimize chooseSpreadOutLocations

* Optimize MapLandmassGenerator's retries for water proportion / large continent count
This commit is contained in:
SomeTroglodyte 2024-01-28 10:05:50 +01:00 committed by GitHub
parent ecceb06d9f
commit 88034e6d02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 325 additions and 231 deletions

View File

@ -83,9 +83,6 @@ object Constants {
const val embarked = "Embarked"
const val wounded = "Wounded"
const val rising = "Rising"
const val lowering = "Lowering"
const val remove = "Remove "
const val repair = "Repair"

View File

@ -0,0 +1,212 @@
package com.unciv.logic.map.mapgenerator
import com.unciv.logic.map.MapParameters
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.TerrainType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.utils.Log
import kotlin.math.abs
import kotlin.math.pow
import kotlin.math.sign
class MapElevationGenerator(
private val tileMap: TileMap,
private val ruleset: Ruleset,
private val randomness: MapGenerationRandomness
) {
companion object {
private const val rising = "~Raising~"
private const val lowering = "~Lowering~"
}
private val flat = ruleset.terrains.values.firstOrNull {
!it.impassable && it.type == TerrainType.Land && !it.hasUnique(UniqueType.RoughTerrain)
}?.name
private val hillMutator: ITileMutator
private val mountainMutator: ITileMutator
private val dummyMutator by lazy { TileDummyMutator() }
init {
mountainMutator = getTileMutator(UniqueType.OccursInChains, flat)
hillMutator = getTileMutator(UniqueType.OccursInGroups, flat)
}
private fun getTileMutator(type: UniqueType, flat: String?): ITileMutator {
if (flat == null) return dummyMutator
val terrain = ruleset.terrains.values.firstOrNull { it.hasUnique(type) }
?: return dummyMutator
return if (terrain.type == TerrainType.TerrainFeature)
TileFeatureMutator(terrain.name)
else TileBaseMutator(flat, terrain.name)
}
/**
* [MapParameters.elevationExponent] favors high elevation
*/
fun raiseMountainsAndHills() {
if (flat == null) {
Log.debug("Ruleset seems to contain no flat terrain - can't generate heightmap")
return
}
val elevationSeed = randomness.RNG.nextInt().toDouble()
val exponent = 1.0 - tileMap.mapParameters.elevationExponent.toDouble()
fun Double.powSigned(exponent: Double) = abs(this).pow(exponent) * sign(this)
tileMap.setTransients(ruleset)
for (tile in tileMap.values) {
if (tile.isWater) continue
val elevation = randomness.getPerlinNoise(tile, elevationSeed, scale = 2.0).powSigned(exponent)
tile.baseTerrain = flat // in case both mutators are TileFeatureMutator
hillMutator.setElevated(tile, elevation > 0.5 && elevation <= 0.7)
mountainMutator.setElevated(tile, elevation > 0.7)
tile.setTerrainTransients()
}
cellularMountainRanges()
cellularHills()
}
private fun cellularMountainRanges() {
if (mountainMutator is TileDummyMutator) return
Log.debug("Mountain-like generation for %s", mountainMutator.name)
val targetMountains = mountainMutator.count(tileMap.values) * 2
val impassableTerrains = ruleset.terrains.values.filter { it.impassable }.map { it.name }.toSet()
for (i in 1..5) {
var totalMountains = mountainMutator.count(tileMap.values)
for (tile in tileMap.values) {
if (tile.isWater) continue
val adjacentMountains = mountainMutator.count(tile.neighbors)
val adjacentImpassible = tile.neighbors.count { it.baseTerrain in impassableTerrains }
if (adjacentMountains == 0 && mountainMutator.isElevated(tile)) {
if (randomness.RNG.nextInt(until = 4) == 0)
tile.addTerrainFeature(lowering)
} else if (adjacentMountains == 1) {
if (randomness.RNG.nextInt(until = 10) == 0)
tile.addTerrainFeature(rising)
} else if (adjacentImpassible == 3) {
if (randomness.RNG.nextInt(until = 2) == 0)
tile.addTerrainFeature(lowering)
} else if (adjacentImpassible > 3) {
tile.addTerrainFeature(lowering)
}
}
for (tile in tileMap.values) {
if (tile.isWater) continue
if (tile.terrainFeatures.contains(rising)) {
tile.removeTerrainFeature(rising)
if (totalMountains >= targetMountains) continue
if (!mountainMutator.isElevated(tile)) totalMountains++
hillMutator.lower(tile)
mountainMutator.raise(tile)
}
if (tile.terrainFeatures.contains(lowering)) {
tile.removeTerrainFeature(lowering)
if (totalMountains * 2 <= targetMountains) continue
if (mountainMutator.isElevated(tile)) totalMountains--
mountainMutator.lower(tile)
hillMutator.raise(tile)
}
}
}
}
private fun cellularHills() {
if (hillMutator is TileDummyMutator) return
Log.debug("Hill-like generation for %s", hillMutator.name)
val targetHills = hillMutator.count(tileMap.values)
for (i in 1..5) {
var totalHills = hillMutator.count(tileMap.values)
for (tile in tileMap.values) {
if (tile.isWater || mountainMutator.isElevated(tile)) continue
val adjacentMountains = mountainMutator.count(tile.neighbors)
val adjacentHills = hillMutator.count(tile.neighbors)
if (adjacentHills <= 1 && adjacentMountains == 0 && randomness.RNG.nextInt(until = 2) == 0) {
tile.addTerrainFeature(lowering)
} else if (adjacentHills > 3 && adjacentMountains == 0 && randomness.RNG.nextInt(until = 2) == 0) {
tile.addTerrainFeature(lowering)
} else if (adjacentHills + adjacentMountains in 2..3 && randomness.RNG.nextInt(until = 2) == 0) {
tile.addTerrainFeature(rising)
}
}
for (tile in tileMap.values) {
if (tile.isWater || mountainMutator.isElevated(tile)) continue
if (tile.terrainFeatures.contains(rising)) {
tile.removeTerrainFeature(rising)
if (totalHills > targetHills && i != 1) continue
if (!hillMutator.isElevated(tile)) {
hillMutator.raise(tile)
totalHills++
}
}
if (tile.terrainFeatures.contains(lowering)) {
tile.removeTerrainFeature(lowering)
if (totalHills >= targetHills * 0.9f || i == 1) {
if (hillMutator.isElevated(tile)) {
hillMutator.lower(tile)
totalHills--
}
}
}
}
}
}
private interface ITileMutator {
val name: String // logging only
fun lower(tile: Tile)
fun raise(tile: Tile)
fun isElevated(tile: Tile): Boolean
fun setElevated(tile: Tile, value: Boolean) = if (value) raise(tile) else lower(tile)
fun count(tiles: Iterable<Tile>) = tiles.count { isElevated(it) }
fun count(tiles: Sequence<Tile>) = tiles.count { isElevated(it) }
}
private class TileDummyMutator : ITileMutator {
override val name get() = ""
override fun lower(tile: Tile) {}
override fun raise(tile: Tile) {}
override fun isElevated(tile: Tile) = false
}
private class TileBaseMutator(
private val flat: String,
private val elevated: String
) : ITileMutator {
override val name get() = elevated
override fun lower(tile: Tile) {
tile.baseTerrain = flat
}
override fun raise(tile: Tile) {
tile.baseTerrain = elevated
}
override fun isElevated(tile: Tile) = tile.baseTerrain == elevated
}
private class TileFeatureMutator(
val elevated: String
) : ITileMutator {
override val name get() = elevated
override fun lower(tile: Tile) {
tile.removeTerrainFeature(elevated)
}
override fun raise(tile: Tile) {
tile.addTerrainFeature(elevated)
}
override fun isElevated(tile: Tile) = elevated in tile.terrainFeatures
}
}

View File

@ -2,10 +2,9 @@ package com.unciv.logic.map.mapgenerator
import com.unciv.logic.map.HexMath
import com.unciv.logic.map.tile.Tile
import com.unciv.utils.Log
import com.unciv.utils.debug
import kotlin.math.max
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.random.Random
class MapGenerationRandomness {
@ -47,39 +46,54 @@ class MapGenerationRandomness {
// The `if` means if we need to fill 60% or more of the available tiles, no sense starting with minimum distance 2.
val sparsityFactor = (HexMath.getHexagonalRadiusForArea(suitableTiles.size) / mapRadius).pow(0.333f)
val initialDistance = if (number == 1 || number * 5 >= suitableTiles.size * 3) 1
else max(1, (mapRadius * 0.666f / HexMath.getHexagonalRadiusForArea(number).pow(0.9f) * sparsityFactor + 0.5).toInt())
else (mapRadius * 0.666f / HexMath.getHexagonalRadiusForArea(number).pow(0.9f) * sparsityFactor).roundToInt().coerceAtLeast(1)
// If possible, we want to equalize the base terrains upon which
// the resources are found, so we save how many have been
// found for each base terrain and try to get one from the lowest
val baseTerrainsToChosenTiles = HashMap<String, Int>()
for (tileInfo in suitableTiles) {
if (tileInfo.baseTerrain !in baseTerrainsToChosenTiles)
baseTerrainsToChosenTiles[tileInfo.baseTerrain] = 0
// Once we have a preference to choose from a specific base terrain, we want quick lookup of the available candidates
val suitableTilesGrouped = LinkedHashMap<String, MutableSet<Tile>>(8) // 8 is > number of base terrains in vanilla
// Prefill both with all existing base terrains as keys, and group suitableTiles into base terrain buckets
for (tile in suitableTiles) {
val terrain = tile.baseTerrain
if (terrain !in baseTerrainsToChosenTiles)
baseTerrainsToChosenTiles[terrain] = 0
suitableTilesGrouped.getOrPut(terrain) { mutableSetOf() }.add(tile)
}
fun LinkedHashMap<String, MutableSet<Tile>>.deepClone(): LinkedHashMap<String, MutableSet<Tile>> {
// map { it.key to it.value.toMutableSet() }.toMap() is marginally less efficient
val result = LinkedHashMap<String, MutableSet<Tile>>(size)
for ((key, value) in this)
result[key] = value.toMutableSet()
return result
}
for (distanceBetweenResources in initialDistance downTo 1) {
var availableTiles = suitableTiles
val availableTiles = suitableTilesGrouped.deepClone()
val chosenTiles = ArrayList<Tile>(number)
for (terrain in baseTerrainsToChosenTiles.keys)
baseTerrainsToChosenTiles[terrain] = 0
for (i in 1..number) {
if (availableTiles.isEmpty()) break
val orderedKeys = baseTerrainsToChosenTiles.entries
.sortedBy { it.value }.map { it.key }
.sortedBy { it.value }.map { it.key }
val firstKeyWithTilesLeft = orderedKeys
.first { availableTiles.any { tile -> tile.baseTerrain == it} }
val chosenTile = availableTiles.filter { it.baseTerrain == firstKeyWithTilesLeft }.random(RNG)
availableTiles = availableTiles.filter { it.aerialDistanceTo(chosenTile) > distanceBetweenResources }
.firstOrNull { availableTiles[it]!!.isNotEmpty() }
?: break
val chosenTile = availableTiles[firstKeyWithTilesLeft]!!.random(RNG)
val closeTiles = chosenTile.getTilesInDistance(distanceBetweenResources).toSet()
for (availableSet in availableTiles.values)
availableSet.removeAll(closeTiles)
chosenTiles.add(chosenTile)
baseTerrainsToChosenTiles[firstKeyWithTilesLeft] = baseTerrainsToChosenTiles[firstKeyWithTilesLeft]!! + 1
}
if (chosenTiles.size == number || distanceBetweenResources == 1) {
// Either we got them all, or we're not going to get anything better
if (Log.shouldLog() && distanceBetweenResources < initialDistance)
debug("chooseSpreadOutLocations: distance $distanceBetweenResources < initial $initialDistance")
if (distanceBetweenResources < initialDistance)
debug("chooseSpreadOutLocations: distance %d < initial %d", distanceBetweenResources, initialDistance)
return chosenTiles
}
}

View File

@ -116,10 +116,10 @@ class MapGenerator(val ruleset: Ruleset, private val coroutineScope: CoroutineSc
if (consoleTimings) debug("\nMapGenerator run with parameters %s", mapParameters)
runAndMeasure("MapLandmassGenerator") {
MapLandmassGenerator(ruleset, randomness).generateLand(map)
MapLandmassGenerator(map, ruleset, randomness).generateLand()
}
runAndMeasure("raiseMountainsAndHills") {
raiseMountainsAndHills(map)
MapElevationGenerator(map, ruleset, randomness).raiseMountainsAndHills()
}
runAndMeasure("applyHumidityAndTemperature") {
applyHumidityAndTemperature(map)
@ -190,8 +190,8 @@ class MapGenerator(val ruleset: Ruleset, private val coroutineScope: CoroutineSc
when (step) {
MapGeneratorSteps.None -> Unit
MapGeneratorSteps.All -> throw IllegalArgumentException("MapGeneratorSteps.All cannot be used in generateSingleStep")
MapGeneratorSteps.Landmass -> MapLandmassGenerator(ruleset, randomness).generateLand(map)
MapGeneratorSteps.Elevation -> raiseMountainsAndHills(map)
MapGeneratorSteps.Landmass -> MapLandmassGenerator(map, ruleset, randomness).generateLand()
MapGeneratorSteps.Elevation -> MapElevationGenerator(map, ruleset, randomness).raiseMountainsAndHills()
MapGeneratorSteps.HumidityAndTemperature -> applyHumidityAndTemperature(map)
MapGeneratorSteps.LakesAndCoast -> spawnLakesAndCoasts(map)
MapGeneratorSteps.Vegetation -> spawnVegetation(map)
@ -379,158 +379,6 @@ class MapGenerator(val ruleset: Ruleset, private val coroutineScope: CoroutineSc
}
/**
* [MapParameters.elevationExponent] favors high elevation
*/
private fun raiseMountainsAndHills(tileMap: TileMap) {
val mountain = ruleset.terrains.values.firstOrNull { it.hasUnique(UniqueType.OccursInChains) }?.name
val hill = ruleset.terrains.values.firstOrNull { it.hasUnique(UniqueType.OccursInGroups) }?.name
val flat = ruleset.terrains.values.firstOrNull {
!it.impassable && it.type == TerrainType.Land && !it.hasUnique(UniqueType.RoughTerrain)
}?.name
if (flat == null) {
debug("Ruleset seems to contain no flat terrain - can't generate heightmap")
return
}
if (mountain != null)
debug("Mountain-like generation for %s", mountain)
if (hill != null)
debug("Hill-like generation for %s", mountain)
val elevationSeed = randomness.RNG.nextInt().toDouble()
tileMap.setTransients(ruleset)
for (tile in tileMap.values.asSequence().filter { !it.isWater }) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed, scale = 2.0)
elevation = abs(elevation).pow(1.0 - tileMap.mapParameters.elevationExponent.toDouble()) * elevation.sign
when {
elevation <= 0.5 -> {
tile.baseTerrain = flat
if (hill != null && tile.terrainFeatures.contains(hill)) {
tile.removeTerrainFeature(hill)
}
}
elevation <= 0.7 && hill != null -> {
tile.addTerrainFeature(hill)
tile.baseTerrain = flat
}
elevation <= 0.7 && hill == null -> tile.baseTerrain = flat // otherwise would be hills become mountains
elevation > 0.7 && mountain != null -> {
tile.baseTerrain = mountain
if (hill != null && tile.terrainFeatures.contains(hill)) {
tile.removeTerrainFeature(hill)
}
}
else -> {
tile.baseTerrain = flat
if (hill != null && tile.terrainFeatures.contains(hill)) {
tile.removeTerrainFeature(hill)
}
}
}
tile.setTerrainTransients()
}
if (mountain != null)
cellularMountainRanges(tileMap, mountain, hill, flat)
if (hill != null)
cellularHills(tileMap, mountain, hill)
}
private fun cellularMountainRanges(tileMap: TileMap, mountain: String, hill: String?, flat: String) {
val targetMountains = tileMap.values.count { it.baseTerrain == mountain } * 2
for (i in 1..5) {
var totalMountains = tileMap.values.count { it.baseTerrain == mountain }
for (tile in tileMap.values.filter { !it.isWater }) {
val adjacentMountains =
tile.neighbors.count { it.baseTerrain == mountain }
val adjacentImpassible =
tile.neighbors.count { ruleset.terrains[it.baseTerrain]?.impassable == true }
if (adjacentMountains == 0 && tile.baseTerrain == mountain) {
if (randomness.RNG.nextInt(until = 4) == 0)
tile.addTerrainFeature(Constants.lowering)
} else if (adjacentMountains == 1) {
if (randomness.RNG.nextInt(until = 10) == 0)
tile.addTerrainFeature(Constants.rising)
} else if (adjacentImpassible == 3) {
if (randomness.RNG.nextInt(until = 2) == 0)
tile.addTerrainFeature(Constants.lowering)
} else if (adjacentImpassible > 3) {
tile.addTerrainFeature(Constants.lowering)
}
}
for (tile in tileMap.values.filter { !it.isWater }) {
if (tile.terrainFeatures.contains(Constants.rising)) {
tile.removeTerrainFeature(Constants.rising)
if (totalMountains >= targetMountains) continue
if (hill != null)
tile.removeTerrainFeature(hill)
tile.baseTerrain = mountain
totalMountains++
}
if (tile.terrainFeatures.contains(Constants.lowering)) {
tile.removeTerrainFeature(Constants.lowering)
if (totalMountains <= targetMountains * 0.5f) continue
if (tile.baseTerrain == mountain) {
if (hill != null && !tile.terrainFeatures.contains(hill))
tile.addTerrainFeature(hill)
totalMountains--
}
tile.baseTerrain = flat
}
}
}
}
private fun cellularHills(tileMap: TileMap, mountain: String?, hill: String) {
val targetHills = tileMap.values.count { it.terrainFeatures.contains(hill) }
for (i in 1..5) {
var totalHills = tileMap.values.count { it.terrainFeatures.contains(hill) }
for (tile in tileMap.values.asSequence().filter { !it.isWater && (mountain == null || it.baseTerrain != mountain) }) {
val adjacentMountains = if (mountain == null) 0 else
tile.neighbors.count { it.baseTerrain == mountain }
val adjacentHills =
tile.neighbors.count { it.terrainFeatures.contains(hill) }
if (adjacentHills <= 1 && adjacentMountains == 0 && randomness.RNG.nextInt(until = 2) == 0) {
tile.addTerrainFeature(Constants.lowering)
} else if (adjacentHills > 3 && adjacentMountains == 0 && randomness.RNG.nextInt(until = 2) == 0) {
tile.addTerrainFeature(Constants.lowering)
} else if (adjacentHills + adjacentMountains in 2..3 && randomness.RNG.nextInt(until = 2) == 0) {
tile.addTerrainFeature(Constants.rising)
}
}
for (tile in tileMap.values.asSequence().filter { !it.isWater && (mountain == null || it.baseTerrain != mountain) }) {
if (tile.terrainFeatures.contains(Constants.rising)) {
tile.removeTerrainFeature(Constants.rising)
if (totalHills > targetHills && i != 1) continue
if (!tile.terrainFeatures.contains(hill)) {
tile.addTerrainFeature(hill)
totalHills++
}
}
if (tile.terrainFeatures.contains(Constants.lowering)) {
tile.removeTerrainFeature(Constants.lowering)
if (totalHills >= targetHills * 0.9f || i == 1) {
if (tile.terrainFeatures.contains(hill))
tile.removeTerrainFeature(hill)
totalHills--
}
}
}
}
}
/**
* [MapParameters.tilesPerBiomeArea] to set biomes size
* [MapParameters.temperatureExtremeness] to favor very high and very low temperatures
@ -931,4 +779,3 @@ class MapGenerator(val ruleset: Ruleset, private val coroutineScope: CoroutineSc
}
}
}

View File

@ -14,8 +14,12 @@ import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt
class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRandomness) {
//region _Fields
class MapLandmassGenerator(
private val tileMap: TileMap,
ruleset: Ruleset,
private val randomness: MapGenerationRandomness
) {
//region Fields
private val landTerrainName = getInitializationTerrain(ruleset, TerrainType.Land)
private val waterTerrainName: String = try {
getInitializationTerrain(ruleset, TerrainType.Water)
@ -33,7 +37,7 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
?: throw Exception("Cannot create map - no $type terrains found!")
}
fun generateLand(tileMap: TileMap) {
fun generateLand() {
// This is to accommodate land-only mods
if (landOnlyMod) {
for (tile in tileMap.values)
@ -44,25 +48,25 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
waterThreshold = tileMap.mapParameters.waterThreshold.toDouble()
when (tileMap.mapParameters.type) {
MapType.pangaea -> createPangaea(tileMap)
MapType.innerSea -> createInnerSea(tileMap)
MapType.continentAndIslands -> createContinentAndIslands(tileMap)
MapType.twoContinents -> createTwoContinents(tileMap)
MapType.threeContinents -> createThreeContinents(tileMap)
MapType.fourCorners -> createFourCorners(tileMap)
MapType.archipelago -> createArchipelago(tileMap)
MapType.perlin -> createPerlin(tileMap)
MapType.fractal -> createFractal(tileMap)
MapType.lakes -> createLakes(tileMap)
MapType.smallContinents -> createSmallContinents(tileMap)
MapType.pangaea -> createPangaea()
MapType.innerSea -> createInnerSea()
MapType.continentAndIslands -> createContinentAndIslands()
MapType.twoContinents -> createTwoContinents()
MapType.threeContinents -> createThreeContinents()
MapType.fourCorners -> createFourCorners()
MapType.archipelago -> createArchipelago()
MapType.perlin -> createPerlin()
MapType.fractal -> createFractal()
MapType.lakes -> createLakes()
MapType.smallContinents -> createSmallContinents()
}
if (tileMap.mapParameters.shape === MapShape.flatEarth) {
generateFlatEarthExtraWater(tileMap)
generateFlatEarthExtraWater()
}
}
private fun generateFlatEarthExtraWater(tileMap: TileMap) {
private fun generateFlatEarthExtraWater() {
for (tile in tileMap.values) {
val isCenterTile = tile.latitude == 0f && tile.longitude == 0f
val isEdgeTile = tile.neighbors.count() < 6
@ -92,7 +96,27 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
tile.baseTerrain = if (elevation < waterThreshold) waterTerrainName else landTerrainName
}
private fun createPerlin(tileMap: TileMap) {
/** Repeat [function] until [predicate] is `true`, lowering [waterThreshold] on each retry, preventing an endless loop.
* The [predicate] receives the proportion of water on the map, the default accepts <= 70%.
*/
private fun retryLoweringWaterLevel(predicate: (waterPercent: Float) -> Boolean = { it <= 0.7f }, function: () -> Unit) {
var retries = 0
while (++retries <= 30) { // 28 is enough to go from +1 to -1 with only the retries acceleration below
function()
val waterPercent = tileMap.values.count { it.baseTerrain == waterTerrainName }.toFloat() / tileMap.values.size
if (waterThreshold < -1f || predicate(waterPercent)) break
// lower water table to reduce water percentage, with empiric base step and acceleration
// (tweaked to acceptable performance on huge maps - but feel free to improve)
waterThreshold -= 0.02 *
(waterPercent / 0.7f).coerceAtLeast(1f) *
retries.toFloat().pow(0.5f)
//Log.debug("retry %d with waterPercent=%f, waterThreshold=%f", retries, waterPercent, waterThreshold)
}
}
//region Type-specific generators
private fun createPerlin() {
val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
val elevation = randomness.getPerlinNoise(tile, elevationSeed)
@ -100,8 +124,8 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
}
}
private fun createFractal(tileMap: TileMap) {
do {
private fun createFractal() {
retryLoweringWaterLevel {
val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
val maxdim = max(tileMap.maxLatitude, tileMap.maxLongitude)
@ -112,15 +136,14 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
var elevation = randomness.getPerlinNoise(tile, elevationSeed, persistence=0.8, lacunarity=1.5, scale=ratio*30.0)
elevation += getOceanEdgesTransform(tile, tileMap)
elevation += getOceanEdgesTransform(tile)
spawnLandOrWater(tile, elevation)
}
waterThreshold -= 0.01
} while (tileMap.values.count { it.baseTerrain == waterTerrainName } > tileMap.values.size * 0.7f) // Over 70% water
}
}
private fun createLakes(tileMap: TileMap) {
private fun createLakes() {
val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
val elevation = 0.3 - getRidgedPerlinNoise(tile, elevationSeed, persistence=0.7, lacunarity=1.5)
@ -129,20 +152,19 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
}
}
private fun createSmallContinents(tileMap: TileMap) {
private fun createSmallContinents() {
val elevationSeed = randomness.RNG.nextInt().toDouble()
waterThreshold += 0.25
do {
retryLoweringWaterLevel {
for (tile in tileMap.values) {
var elevation = getRidgedPerlinNoise(tile, elevationSeed, scale = 22.0)
elevation += getOceanEdgesTransform(tile, tileMap)
elevation += getOceanEdgesTransform(tile)
spawnLandOrWater(tile, elevation)
}
waterThreshold -= 0.01
} while (tileMap.values.count { it.baseTerrain == waterTerrainName } > tileMap.values.size * 0.7f) // Over 70%
}
}
private fun createArchipelago(tileMap: TileMap) {
private fun createArchipelago() {
val elevationSeed = randomness.RNG.nextInt().toDouble()
waterThreshold += 0.25
for (tile in tileMap.values) {
@ -151,38 +173,36 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
}
}
private fun createPangaea(tileMap: TileMap) {
val largeContinentThreshold = (tileMap.values.size / 4).coerceAtMost(25)
var retryCount = 200 // A bit much but when relevant (tiny map) an iteration is relatively cheap
while(--retryCount >= 0) {
private fun createPangaea() {
val largeContinentThreshold = 25
.coerceAtMost(tileMap.values.size / 4) // Or really tiny maps will exhaust retries
.coerceAtLeast(tileMap.values.size.toFloat().pow(0.333f).toInt()) // kicks in on really large maps: 130k tiles -> 50
retryLoweringWaterLevel(predicate = { waterPercent ->
val largeContinents = tileMap.continentSizes.values.count { it > largeContinentThreshold }
largeContinents == 1 && waterPercent <= 0.7f
}) {
val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed)
elevation = elevation * (3 / 4f) + getEllipticContinent(tile, tileMap) / 4
elevation = elevation * (3 / 4f) + getEllipticContinent(tile) / 4
spawnLandOrWater(tile, elevation)
tile.setTerrainTransients() // necessary for assignContinents
}
tileMap.assignContinents(TileMap.AssignContinentsMode.Reassign)
if ( tileMap.continentSizes.values.count { it > largeContinentThreshold } == 1 // Only one large continent
&& tileMap.values.count { it.baseTerrain == waterTerrainName } <= tileMap.values.size * 0.7f // And at most 70% water
) break
waterThreshold -= 0.01
tileMap.assignContinents(TileMap.AssignContinentsMode.Reassign) // to support largeContinents above
}
tileMap.assignContinents(TileMap.AssignContinentsMode.Clear)
}
private fun createInnerSea(tileMap: TileMap) {
private fun createInnerSea() {
val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed)
elevation -= getEllipticContinent(tile, tileMap, 0.6) * 0.3
elevation -= getEllipticContinent(tile, 0.6) * 0.3
spawnLandOrWater(tile, elevation)
}
}
private fun createContinentAndIslands(tileMap: TileMap) {
private fun createContinentAndIslands() {
val isNorth = randomness.RNG.nextDouble() < 0.5
val isLatitude =
if (tileMap.mapParameters.shape === MapShape.hexagonal || tileMap.mapParameters.shape === MapShape.flatEarth) randomness.RNG.nextDouble() > 0.5f
@ -193,12 +213,12 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed)
elevation = (elevation + getContinentAndIslandsTransform(tile, tileMap, isNorth, isLatitude)) / 2.0
elevation = (elevation + getContinentAndIslandsTransform(tile, isNorth, isLatitude)) / 2.0
spawnLandOrWater(tile, elevation)
}
}
private fun createTwoContinents(tileMap: TileMap) {
private fun createTwoContinents() {
val isLatitude =
if (tileMap.mapParameters.shape === MapShape.hexagonal || tileMap.mapParameters.shape === MapShape.flatEarth) randomness.RNG.nextDouble() > 0.5f
else if (tileMap.mapParameters.mapSize.height > tileMap.mapParameters.mapSize.width) true
@ -208,12 +228,12 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed)
elevation = (elevation + getTwoContinentsTransform(tile, tileMap, isLatitude)) / 2.0
elevation = (elevation + getTwoContinentsTransform(tile, isLatitude)) / 2.0
spawnLandOrWater(tile, elevation)
}
}
private fun createThreeContinents(tileMap: TileMap) {
private fun createThreeContinents() {
val isNorth = randomness.RNG.nextDouble() < 0.5
// On flat earth maps we can randomly do East or West instead of North or South
val isEastWest = tileMap.mapParameters.shape === MapShape.flatEarth && randomness.RNG.nextDouble() > 0.5
@ -221,25 +241,28 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed)
elevation = (elevation + getThreeContinentsTransform(tile, tileMap, isNorth, isEastWest)) / 2.0
elevation = (elevation + getThreeContinentsTransform(tile, isNorth, isEastWest)) / 2.0
spawnLandOrWater(tile, elevation)
}
}
private fun createFourCorners(tileMap: TileMap) {
private fun createFourCorners() {
val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed)
elevation = elevation/2 + getFourCornersTransform(tile, tileMap)/2
elevation = elevation / 2 + getFourCornersTransform(tile) / 2
spawnLandOrWater(tile, elevation)
}
}
//endregion
//region Shaping helpers
/**
* Create an elevation map that favors a central elliptic continent spanning over 85% - 95% of
* the map size.
*/
private fun getEllipticContinent(tile: Tile, tileMap: TileMap, percentOfMap: Double = 0.85): Double {
private fun getEllipticContinent(tile: Tile, percentOfMap: Double = 0.85): Double {
val randomScale = randomness.RNG.nextDouble()
val ratio = percentOfMap + 0.1 * randomness.RNG.nextDouble()
@ -253,7 +276,7 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
return min(0.3, 1.0 - (5.0 * distanceFactor * distanceFactor + randomScale) / 3.0)
}
private fun getContinentAndIslandsTransform(tile: Tile, tileMap: TileMap, isNorth: Boolean, isLatitude: Boolean): Double {
private fun getContinentAndIslandsTransform(tile: Tile, isNorth: Boolean, isLatitude: Boolean): Double {
// The idea here is to create a water area separating the two land areas.
// So what we do it create a line of water in the middle - where latitude or longitude is close to 0.
val randomScale = randomness.RNG.nextDouble()
@ -283,7 +306,7 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
return min(0.2, -1.0 + (5.0 * factor.pow(0.6f) + randomScale) / 3.0)
}
private fun getTwoContinentsTransform(tile: Tile, tileMap: TileMap, isLatitude: Boolean): Double {
private fun getTwoContinentsTransform(tile: Tile, isLatitude: Boolean): Double {
// The idea here is to create a water area separating the two land areas.
// So what we do it create a line of water in the middle - where latitude or longitude is close to 0.
val randomScale = randomness.RNG.nextDouble()
@ -303,7 +326,7 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
return min(0.2, -1.0 + (5.0 * factor.pow(0.6f) + randomScale) / 3.0)
}
private fun getThreeContinentsTransform(tile: Tile, tileMap: TileMap, isNorth: Boolean, isEastWest: Boolean): Double {
private fun getThreeContinentsTransform(tile: Tile, isNorth: Boolean, isEastWest: Boolean): Double {
// The idea here is to create a water area separating the three land areas.
// So what we do it create a line of water in the middle - where latitude or longitude is close to 0.
val randomScale = randomness.RNG.nextDouble()
@ -339,7 +362,7 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
return min(0.2, -1.0 + (5.0 * factor.pow(0.5f) + randomScale) / 3.0)
}
private fun getFourCornersTransform(tile: Tile, tileMap: TileMap): Double {
private fun getFourCornersTransform(tile: Tile): Double {
// The idea here is to create a water area separating the four land areas.
// So what we do it create a line of water in the middle - where latitude or longitude is close to 0.
val randomScale = randomness.RNG.nextDouble()
@ -364,7 +387,7 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
return 1.0 - (5.0 * shouldBeWater*shouldBeWater + randomScale) / 3.0
}
private fun getOceanEdgesTransform(tile: Tile, tileMap: TileMap): Double {
private fun getOceanEdgesTransform(tile: Tile): Double {
// The idea is to reduce elevation at the border of the map, so that we have mostly ocean there.
val maxX = tileMap.maxLongitude
val maxY = tileMap.maxLatitude
@ -425,4 +448,5 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
val worldCoords = HexMath.hex2WorldCoords(tile.position)
return Perlin.ridgedNoise3d(worldCoords.x.toDouble(), worldCoords.y.toDouble(), seed, nOctaves, persistence, lacunarity, scale)
}
//endregion
}