mirror of
https://github.com/yairm210/Unciv.git
synced 2025-01-05 21:11:35 +07:00
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:
parent
ecceb06d9f
commit
88034e6d02
@ -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"
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user