diff --git a/android/assets/jsons/Civ V - Gods & Kings/Terrains.json b/android/assets/jsons/Civ V - Gods & Kings/Terrains.json index 54c98a808f..c735512021 100644 --- a/android/assets/jsons/Civ V - Gods & Kings/Terrains.json +++ b/android/assets/jsons/Civ V - Gods & Kings/Terrains.json @@ -270,7 +270,8 @@ "impassable": true, "overrideStats": true, "occursOn": ["Ocean", "Coast"], - "uniques": ["[-1] to Fertility for Map Generation", + "uniques": ["Occurs at temperature between [-1] and [-0.8] and humidity between [0] and [1]", + "[-1] to Fertility for Map Generation", "Considered [Undesirable] when determining start locations"] }, { diff --git a/android/assets/jsons/Civ V - Vanilla/Terrains.json b/android/assets/jsons/Civ V - Vanilla/Terrains.json index 1936493f70..355d0ef11d 100644 --- a/android/assets/jsons/Civ V - Vanilla/Terrains.json +++ b/android/assets/jsons/Civ V - Vanilla/Terrains.json @@ -272,7 +272,8 @@ "impassable": true, "overrideStats": true, "occursOn": ["Ocean", "Coast"], - "uniques": ["[-1] to Fertility for Map Generation", + "uniques": ["Occurs at temperature between [-1] and [-0.8] and humidity between [0] and [1]", + "[-1] to Fertility for Map Generation", "Considered [Undesirable] when determining start locations"] }, { diff --git a/core/src/com/unciv/Constants.kt b/core/src/com/unciv/Constants.kt index 45890fca93..637d95b52a 100644 --- a/core/src/com/unciv/Constants.kt +++ b/core/src/com/unciv/Constants.kt @@ -28,7 +28,6 @@ object Constants { const val jungle = "Jungle" const val ice = "Ice" val vegetation = arrayOf(forest, jungle) - val sea = arrayOf(ocean, coast) // Note the difference in case. **Not** interchangeable! /** The "Fresh water" terrain _unique_ */ diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt index 0f1aeefe58..41349c88c5 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt @@ -10,6 +10,7 @@ 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.unique.Unique import kotlin.math.* import com.unciv.models.ruleset.unique.UniqueType import kotlin.random.Random @@ -25,6 +26,27 @@ class MapGenerator(val ruleset: Ruleset) { private var randomness = MapGenerationRandomness() private val firstLandTerrain = ruleset.terrains.values.first { it.type==TerrainType.Land } + /** Associates [terrain] with a range of temperatures and a range of humidities (both open to closed) */ + private class TerrainOccursRange( + val terrain: Terrain, + val tempFrom: Float, val tempTo: Float, + val humidFrom: Float, val humidTo: Float + ) { + /** builds a [TerrainOccursRange] for [terrain] from a [unique] (type [UniqueType.TileGenerationConditions]) */ + constructor(terrain: Terrain, unique: Unique) + : this(terrain, + unique.params[0].toFloat(), unique.params[1].toFloat(), + unique.params[2].toFloat(), unique.params[3].toFloat()) + /** checks if both [temperature] and [humidity] satisfy their ranges (>From, <=To) */ + // Yes this does implicit conversions Float/Double + fun matches(temperature: Double, humidity: Double) = + tempFrom < temperature && temperature <= tempTo && + humidFrom < humidity && humidity <= humidTo + } + private fun Terrain.getGenerationConditions() = + getMatchingUniques(UniqueType.TileGenerationConditions) + .map { unique -> TerrainOccursRange(this, unique) } + fun generateMap(mapParameters: MapParameters, civilizations: List = emptyList()): TileMap { val mapSize = mapParameters.mapSize val mapType = mapParameters.type @@ -407,21 +429,11 @@ class MapGenerator(val ruleset: Ruleset) { val scale = tileMap.mapParameters.tilesPerBiomeArea.toDouble() val temperatureExtremeness = tileMap.mapParameters.temperatureExtremeness - - class TerrainOccursRange( - val terrain: Terrain, - val tempFrom: Float, val tempTo: Float, - val humidFrom: Float, val humidTo: Float - ) + // List is OK here as it's only sequentially scanned val limitsMap: List = - ruleset.terrains.values.flatMap { terrain -> - terrain.getMatchingUniques(UniqueType.TileGenerationConditions) - .map { unique -> - TerrainOccursRange(terrain, - unique.params[0].toFloat(), unique.params[1].toFloat(), - unique.params[2].toFloat(), unique.params[3].toFloat()) - } + ruleset.terrains.values.flatMap { + it.getGenerationConditions() } val noTerrainUniques = limitsMap.isEmpty() val elevationTerrains = arrayOf(Constants.mountain, Constants.hill) @@ -453,10 +465,7 @@ class MapGenerator(val ruleset: Ruleset) { continue } - val matchingTerrain = limitsMap.firstOrNull { - it.tempFrom < temperature && temperature <= it.tempTo - && it.humidFrom < humidity && humidity <= it.humidTo - } + val matchingTerrain = limitsMap.firstOrNull { it.matches(temperature, humidity) } if (matchingTerrain != null) tile.baseTerrain = matchingTerrain.terrain.name else { @@ -505,19 +514,43 @@ class MapGenerator(val ruleset: Ruleset) { * [MapParameters.temperatureExtremeness] as in [applyHumidityAndTemperature] */ private fun spawnIce(tileMap: TileMap) { - if (!ruleset.terrains.containsKey(Constants.ice)) return // I can't think of how to make this nicely moddable + val waterTerrain: Set = + ruleset.terrains.values.asSequence() + .filter { it.type == TerrainType.Water } + .map { it.name }.toSet() + val iceEquivalents: List = + ruleset.terrains.values.asSequence() + .filter { terrain -> + terrain.type == TerrainType.TerrainFeature && + terrain.impassable && + terrain.occursOn.all { it in waterTerrain } + }.flatMap { terrain -> + val conditions = terrain.getGenerationConditions() + if (conditions.any()) conditions + else sequenceOf(TerrainOccursRange(terrain, -1f, -0.8f, 0f, 1f)) + }.toList() + if (iceEquivalents.isEmpty()) return + tileMap.setTransients(ruleset) val temperatureSeed = randomness.RNG.nextInt().toDouble() for (tile in tileMap.values) { - if (tile.baseTerrain !in Constants.sea || tile.terrainFeatures.isNotEmpty()) + if (tile.baseTerrain !in waterTerrain || tile.terrainFeatures.isNotEmpty()) continue val randomTemperature = randomness.getPerlinNoise(tile, temperatureSeed, scale = tileMap.mapParameters.tilesPerBiomeArea.toDouble(), nOctaves = 1) val latitudeTemperature = 1.0 - 2.0 * abs(tile.latitude) / tileMap.maxLatitude var temperature = ((latitudeTemperature + randomTemperature) / 2.0) temperature = abs(temperature).pow(1.0 - tileMap.mapParameters.temperatureExtremeness) * temperature.sign - if (temperature < -0.8f) - tile.addTerrainFeature(Constants.ice) + + val candidates = iceEquivalents + .filter { + it.matches(temperature, 1.0) && + tile.getLastTerrain().name in it.terrain.occursOn + }.map { it.terrain.name } + when (candidates.size) { + 1 -> tile.addTerrainFeature(candidates.first()) + !in 0..1 -> tile.addTerrainFeature(candidates.random(randomness.RNG)) + } } } } diff --git a/docs/Other/Map-related-JSON-files.md b/docs/Other/Map-related-JSON-files.md index 6e8b4aed2e..daa57eafdd 100644 --- a/docs/Other/Map-related-JSON-files.md +++ b/docs/Other/Map-related-JSON-files.md @@ -18,11 +18,9 @@ Each terrain entry can have the following properties: | movementCost | Integer | Default 1 | base movement cost | | defenceBonus | Float | Default 0 | combat bonus for units being attacked here | | RGB | List Integer * 3 | Default 'Gold' | RGB color for 'Default' tileset display | -| uniques | List | Default empty | List of effects, [see here](../Modders/Unique-parameter-types.md#terrain-uniques) | +| uniques | List | Default empty | List of effects, [see here](../Modders/uniques.md#terrain-uniques) | | civilopediaText | List | Default empty | see [civilopediaText chapter](Miscellaneous-JSON-files.md#civilopedia-text) | -Note that many Natural Wonders have hardcoded routines for their placement and are recognized by name (e.g. Great Barrier Reef being more than one tile). - ## TileImprovements.json This file lists the improvements that can be constructed or created on a map tile by a unit (any unit having the appropriate unique).