From 7c9b0e04b4a374b559b8abf3edf5a3fe7590b5a2 Mon Sep 17 00:00:00 2001 From: Federico Luongo Date: Tue, 8 Jun 2021 05:41:02 +0200 Subject: [PATCH] Map Generation Fixes and Tweaks (#4069) * * Renamed Continents to Two Continents * Created Four Continents map size It spawns a vertical and a horizontal ocean to split the map in 4 major continents. Works properly on all sizes and with world wrap * Fixed Pangea extending to boundaries in rectangular maps * Unified Pangea for rectangular and hexagonal maps * Fixed Default map type bias on Rectangular maps. Also removed "pangea effect" on Default: land can spawn with the same probability in the whole map. * New map type: 4 Corners * Reverted name change Two Continents to Continents * Renamed Four Continents to Four Corners (to reflect original name in Civ5) * Added translation strings * New map type: 4 Corners * Reverted name change Two Continents to Continents * Renamed Four Continents to Four Corners (to reflect original name in Civ5) * Added translation strings --- .../jsons/translations/template.properties | 1 + core/src/com/unciv/logic/map/MapParameters.kt | 1 + .../map/mapgenerator/MapLandmassGenerator.kt | 112 ++++++++++-------- .../ui/newgamescreen/MapParametersTable.kt | 1 + 4 files changed, 64 insertions(+), 51 deletions(-) diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 92d302c092..bc8628e1b1 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -212,6 +212,7 @@ Default = Pangaea = Perlin = Continents = +Four Corners = Archipelago = Number of City-States = One City Challenge = diff --git a/core/src/com/unciv/logic/map/MapParameters.kt b/core/src/com/unciv/logic/map/MapParameters.kt index e5a0e5798a..eadad355bc 100644 --- a/core/src/com/unciv/logic/map/MapParameters.kt +++ b/core/src/com/unciv/logic/map/MapParameters.kt @@ -102,6 +102,7 @@ object MapShape { object MapType { const val pangaea = "Pangaea" const val continents = "Continents" + const val fourCorners = "Four Corners" const val perlin = "Perlin" const val archipelago = "Archipelago" diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapLandmassGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapLandmassGenerator.kt index 3e9a2282bb..0565c1304d 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/MapLandmassGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/MapLandmassGenerator.kt @@ -1,6 +1,5 @@ package com.unciv.logic.map.mapgenerator -import com.badlogic.gdx.math.Vector2 import com.unciv.Constants import com.unciv.logic.HexMath import com.unciv.logic.map.* @@ -23,6 +22,7 @@ class MapLandmassGenerator(val randomness: MapGenerationRandomness) { when (tileMap.mapParameters.type) { MapType.pangaea -> createPangea(tileMap) MapType.continents -> createTwoContinents(tileMap) + MapType.fourCorners -> createFourCorners(tileMap) MapType.perlin -> createPerlin(tileMap) MapType.archipelago -> createArchipelago(tileMap) MapType.default -> generateLandCellularAutomata(tileMap) @@ -36,12 +36,17 @@ class MapLandmassGenerator(val randomness: MapGenerationRandomness) { } } - private fun smooth(tileMap: TileMap, randomness: MapGenerationRandomness) { + /** + * Smoothen the map by clustering landmass and oceans with probability [threshold]. + * [TileInfo]s with more than 3 land neighbours are converted to land. + * [TileInfo]s with more than 3 water neighbours are converted to water. + */ + private fun smoothen(tileMap: TileMap, threshold: Double = 1.0) { for (tileInfo in tileMap.values) { - val numberOfLandNeighbors = tileInfo.neighbors.count { it.baseTerrain == Constants.grassland } - if (randomness.RNG.nextFloat() < 0.5f) + if (randomness.RNG.nextFloat() > threshold) continue + val numberOfLandNeighbors = tileInfo.neighbors.count { it.baseTerrain == Constants.grassland } if (numberOfLandNeighbors > 3) tileInfo.baseTerrain = Constants.grassland else if (numberOfLandNeighbors < 3) @@ -69,7 +74,7 @@ class MapLandmassGenerator(val randomness: MapGenerationRandomness) { val elevationSeed = randomness.RNG.nextInt().toDouble() for (tile in tileMap.values) { var elevation = randomness.getPerlinNoise(tile, elevationSeed) - elevation = (elevation + getCircularNoise(tile, tileMap)) / 2.0 + elevation = (elevation + getEllipticContinent(tile, tileMap)) / 2.0 spawnLandOrWater(tile, elevation, tileMap.mapParameters.waterThreshold.toDouble()) } } @@ -83,9 +88,29 @@ class MapLandmassGenerator(val randomness: MapGenerationRandomness) { } } - private fun getCircularNoise(tileInfo: TileInfo, tileMap: TileMap): Double { + private fun createFourCorners(tileMap: TileMap) { + val elevationSeed = randomness.RNG.nextInt().toDouble() + for (tile in tileMap.values) { + var elevation = randomness.getPerlinNoise(tile, elevationSeed) + elevation = (elevation + getFourCornersTransform(tile, tileMap)) / 2.0 + spawnLandOrWater(tile, elevation, tileMap.mapParameters.waterThreshold.toDouble()) + } + } + + /** + * Create an elevation map that favors a central elliptic continent spanning over 85% - 95% of + * the map size. + */ + private fun getEllipticContinent(tileInfo: TileInfo, tileMap: TileMap): Double { val randomScale = randomness.RNG.nextDouble() - val distanceFactor = percentualDistanceToCenter(tileInfo, tileMap) + val ratio = 0.85 + 0.1 * randomness.RNG.nextDouble() + + val a = ratio * tileMap.maxLongitude + val b = ratio * tileMap.maxLatitude + val x = tileInfo.longitude + val y = tileInfo.latitude + + val distanceFactor = x * x / (a * a) + y * y / (b * b) return min(0.3, 1.0 - (5.0 * distanceFactor * distanceFactor + randomScale) / 3.0) } @@ -107,19 +132,35 @@ class MapLandmassGenerator(val randomness: MapGenerationRandomness) { return min(0.2, -1.0 + (5.0 * longitudeFactor.pow(0.6f) + randomScale) / 3.0) } - private fun percentualDistanceToCenter(tileInfo: TileInfo, tileMap: TileMap): Double { - val mapRadius = tileMap.mapParameters.mapSize.radius - if (tileMap.mapParameters.shape == MapShape.hexagonal) - return HexMath.getDistance(Vector2.Zero, tileInfo.position).toDouble() / mapRadius - else { - val size = HexMath.getEquivalentRectangularSize(mapRadius) - return HexMath.getDistance(Vector2.Zero, tileInfo.position).toDouble() / HexMath.getDistance(Vector2.Zero, Vector2(size.x / 2, size.y / 2)) + private fun getFourCornersTransform(tileInfo: TileInfo, tileMap: TileMap): Double { + // The idea here is to create a water area separating the two four areas. + // So what we do it create a line of water in the middle - where longitude is close to 0. + val randomScale = randomness.RNG.nextDouble() + var longitudeFactor = abs(tileInfo.longitude) / tileMap.maxLongitude + var latitudeFactor = abs(tileInfo.latitude) / tileMap.maxLatitude + + // If this is a world wrap, we want it to be separated on both sides - + // so we make the actual strip of water thinner, but we put it both in the middle of the map and on the edges of the map + if (tileMap.mapParameters.worldWrap) { + longitudeFactor = min( + longitudeFactor, + (tileMap.maxLongitude - abs(tileInfo.longitude)) / tileMap.maxLongitude + ) * 1.5f + latitudeFactor = min( + latitudeFactor, + (tileMap.maxLatitude - abs(tileInfo.latitude)) / tileMap.maxLatitude + ) * 1.5f } + // there's nothing magical about this, it's just what we got from playing around with a lot of different options - + // the numbers can be changed if you find that something else creates better looking continents + + val landFactor = min(longitudeFactor, latitudeFactor) + + return min(0.2, -1.0 + (5.0 * landFactor.pow(0.5f) + randomScale) / 3.0) } - /** - * Generates ridged perlin noise. As for parameters see [getPerlinNoise] + * Generates ridged perlin noise. As for parameters see [MapGenerationRandomness.getPerlinNoise] */ private fun getRidgedPerlinNoise(tile: TileInfo, seed: Double, nOctaves: Int = 10, @@ -132,46 +173,15 @@ class MapLandmassGenerator(val randomness: MapGenerationRandomness) { // region Cellular automata private fun generateLandCellularAutomata(tileMap: TileMap) { - val numSmooth = 4 - // init for (tile in tileMap.values) { - val terrainType = getInitialTerrainCellularAutomata(tile, tileMap.mapParameters) - if (terrainType == TerrainType.Land) tile.baseTerrain = Constants.grassland - else tile.baseTerrain = Constants.ocean + tile.baseTerrain = + if (randomness.RNG.nextDouble() < 0.55) Constants.grassland else Constants.ocean + tile.setTransients() } - //smooth - val grassland = Constants.grassland - val ocean = Constants.ocean - - for (loop in 0..numSmooth) { - for (tileInfo in tileMap.values) { - val numberOfLandNeighbors = tileInfo.neighbors.count { it.baseTerrain == grassland } - if (tileInfo.baseTerrain == grassland) { // land tile - if (numberOfLandNeighbors < 3) - tileInfo.baseTerrain = ocean - } else { // water tile - if (numberOfLandNeighbors > 3) - tileInfo.baseTerrain = grassland - } - } - } + smoothen(tileMap) } - - private fun getInitialTerrainCellularAutomata(tileInfo: TileInfo, mapParameters: MapParameters): TerrainType { - val mapRadius = mapParameters.mapSize.radius - - // default - if (HexMath.getDistance(Vector2.Zero, tileInfo.position) > 0.9f * mapRadius) { - if (randomness.RNG.nextDouble() < 0.1) return TerrainType.Land else return TerrainType.Water - } - if (HexMath.getDistance(Vector2.Zero, tileInfo.position) > 0.85f * mapRadius) { - if (randomness.RNG.nextDouble() < 0.2) return TerrainType.Land else return TerrainType.Water - } - if (randomness.RNG.nextDouble() < 0.55) return TerrainType.Land else return TerrainType.Water - } - // endregion } \ No newline at end of file diff --git a/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt b/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt index 6529ffd680..1c6a42aaf2 100644 --- a/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt @@ -68,6 +68,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed MapType.default, MapType.pangaea, MapType.continents, + MapType.fourCorners, MapType.perlin, MapType.archipelago, if (isEmptyMapAllowed) MapType.empty else null