From d43afe38e6494e3e63ac26e088d40a9a5a248ccf Mon Sep 17 00:00:00 2001 From: Federico Luongo Date: Tue, 4 Feb 2020 22:30:35 +0100 Subject: [PATCH] Rectangular maps, maps generator, bugfixes (part 1) (#1843) * TileMap Rectangular constructor * HexMath utilities * Perlin gradients changed & utility function to combine octaves * NewGame & MapParameters UI * MapParameters & MapGenerator refactor * Advanced Map Parameters UI * Fix Hide advanced settings --- core/src/com/unciv/logic/GameStarter.kt | 6 +- core/src/com/unciv/logic/HexMath.kt | 40 ++ core/src/com/unciv/logic/map/MapGenerator.kt | 435 ++++++++++-------- core/src/com/unciv/logic/map/MapParameters.kt | 33 +- core/src/com/unciv/logic/map/Perlin.kt | 63 ++- core/src/com/unciv/logic/map/TileMap.kt | 25 +- .../com/unciv/ui/mapeditor/NewMapScreen.kt | 2 +- .../ui/newgamescreen/MapParametersTable.kt | 167 ++++++- .../unciv/ui/newgamescreen/NewGameScreen.kt | 4 +- .../NewGameScreenOptionsTable.kt | 67 +-- 10 files changed, 570 insertions(+), 272 deletions(-) diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index 3c12399646..889f9f74e4 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -11,7 +11,7 @@ import java.util.* import kotlin.collections.ArrayList import kotlin.math.max -class GameStarter{ +class GameStarter { fun startNewGame(newGameParameters: GameParameters, mapParameters: MapParameters): GameInfo { val gameInfo = GameInfo() @@ -19,9 +19,9 @@ class GameStarter{ gameInfo.gameParameters = newGameParameters val ruleset = RulesetCache.getComplexRuleset(newGameParameters.mods) - if(mapParameters.name!="") + if(mapParameters.name != "") gameInfo.tileMap = MapSaver().loadMap(mapParameters.name) - else gameInfo.tileMap = MapGenerator().generateMap(mapParameters, ruleset) + else gameInfo.tileMap = MapGenerator(ruleset).generateMap(mapParameters) gameInfo.tileMap.mapParameters = mapParameters gameInfo.tileMap.setTransients(ruleset) diff --git a/core/src/com/unciv/logic/HexMath.kt b/core/src/com/unciv/logic/HexMath.kt index e0ec200233..392e121588 100644 --- a/core/src/com/unciv/logic/HexMath.kt +++ b/core/src/com/unciv/logic/HexMath.kt @@ -14,6 +14,32 @@ object HexMath { return getVectorForAngle((2 * Math.PI * (hour / 12f)).toFloat()) } + /** returns the number of tiles in a hexagonal map of radius size*/ + fun getNumberOfTilesInHexagon(size: Int): Int { + if (size < 0) return 0 + return 1 + 6 * size * (size + 1) / 2 + } + + /* In our reference system latitude, i.e. how distant from equator we are is proportional to x + y*/ + fun getLatitude(vector: Vector2): Float { + return vector.x + vector.y + } + fun getLongitude(vector: Vector2): Float { + return vector.x - vector.y + } + + /** returns a vector containing width and height a rectangular map should have to have + * approximately the same number of tiles as an hexagonal map given a height/width ratio */ + fun getEquivalentRectangularSize(size: Int, ratio: Float = 0.65f): Vector2 { + if (size < 0) + return Vector2.Zero + + val nTiles = getNumberOfTilesInHexagon(size) + val width = round(sqrt(nTiles.toFloat()/ratio)) + val height = round(width * ratio) + return Vector2(width, height) + } + fun getAdjacentVectors(origin: Vector2): ArrayList { val vectors = arrayListOf( Vector2(1f, 0f), @@ -59,6 +85,20 @@ object HexMath { return Vector2(cubicCoord.y, -cubicCoord.z) } + fun cubic2EvenQCoords(cubicCoord: Vector3): Vector2 { + return Vector2(cubicCoord.x, cubicCoord.z + (cubicCoord.x + (cubicCoord.x.toInt() and 1)) / 2) + } + fun evenQ2CubicCoords(evenQCoord: Vector2): Vector3 { + val x = evenQCoord.x + val z = evenQCoord.y - (evenQCoord.x + (evenQCoord.x.toInt() and 1)) / 2 + val y = -x-z + return Vector3(x,y,z) + } + + fun evenQ2HexCoords(evenQCoord: Vector2): Vector2 { + return cubic2HexCoords(evenQ2CubicCoords(evenQCoord)) + } + fun roundCubicCoords(cubicCoords: Vector3): Vector3 { var rx = round(cubicCoords.x) var ry = round(cubicCoords.y) diff --git a/core/src/com/unciv/logic/map/MapGenerator.kt b/core/src/com/unciv/logic/map/MapGenerator.kt index 5020867601..4e8b3fe960 100644 --- a/core/src/com/unciv/logic/map/MapGenerator.kt +++ b/core/src/com/unciv/logic/map/MapGenerator.kt @@ -8,77 +8,66 @@ 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 java.util.* import kotlin.collections.ArrayList import kotlin.collections.HashMap import kotlin.math.* +import kotlin.random.Random -// This is no longer an Enum because there were map types that were disabled, -// and when parsing an existing map to an Enum you have to have all the options. -// So either we had to keep the old Enums forever, or change to strings. -class MapType { - companion object{ - val default="Default" // Creates a cellular automata map - val perlin="Perlin" - val continents = "Continents" - val pangaea = "Pangaea" - val custom="Custom" - val empty="Empty" - } -} +class MapGenerator(val ruleset: Ruleset) { -class MapGenerator { - - fun generateMap(mapParameters: MapParameters, ruleset: Ruleset): TileMap { + fun generateMap(mapParameters: MapParameters, seed: Long = System.currentTimeMillis()): TileMap { val mapRadius = mapParameters.size.radius val mapType = mapParameters.type + val map: TileMap + + if (mapParameters.shape == MapShape.rectangular) { + val size = HexMath.getEquivalentRectangularSize(mapRadius) + map = TileMap(size.x.toInt(), size.y.toInt(), ruleset) + } + else + map = TileMap(mapRadius, ruleset) - val map = TileMap(mapRadius, ruleset) map.mapParameters = mapParameters + map.mapParameters.seed = seed - // Is the empty map is requested, there's no need for further generation - if (mapType == MapType.empty) return map + if (mapType == MapType.empty) + return map - // Step one - separate land and water, in form of Grasslands and Oceans - if (mapType == MapType.perlin) - MapLandmassGenerator().generateLandPerlin(map) - else MapLandmassGenerator().generateLandCellularAutomata(map, mapRadius, mapType) - - divideIntoBiomes(map, 6, 0.05f, mapRadius, ruleset) - - for (tile in map.values) tile.setTransients() - - setWaterTiles(map) - - for (tile in map.values) randomizeTile(tile, mapParameters, ruleset) - - spreadResources(map, mapRadius, ruleset) - - if(!mapParameters.noRuins) - spreadAncientRuins(map) - - if (!mapParameters.noNaturalWonders) - spawnNaturalWonders(map, mapRadius, ruleset) + seedRNG(seed) + generateLand(map) + divideIntoBiomes(map) + spawnLakesAndCoasts(map) + randomizeTiles(map) + spreadResources(map) + spreadAncientRuins(map) + spawnNaturalWonders(map) return map } + private fun seedRNG(seed: Long) { + RNG = Random(seed) + println("RNG seeded with $seed") + } - fun setWaterTiles(map: TileMap) { + private fun spawnLakesAndCoasts(map: TileMap) { //define lakes var waterTiles = map.values.filter { it.isWater } + val tilesInArea = ArrayList() val tilesToCheck = ArrayList() + while (waterTiles.isNotEmpty()) { - val initialWaterTile = waterTiles.random() + val initialWaterTile = waterTiles.random(RNG) tilesInArea += initialWaterTile tilesToCheck += initialWaterTile waterTiles -= initialWaterTile + // Floodfill to cluster water tiles while (tilesToCheck.isNotEmpty()) { - val tileWeAreChecking = tilesToCheck.random() + val tileWeAreChecking = tilesToCheck.random(RNG) for (vector in tileWeAreChecking.neighbors .filter { !tilesInArea.contains(it) and waterTiles.contains(it) }) { tilesInArea += vector @@ -99,52 +88,65 @@ class MapGenerator { //Coasts for (tile in map.values.filter { it.baseTerrain == Constants.ocean }) { - if (tile.getTilesInDistance(2).any { it.isLand }) { + val coastLength = max(1, RNG.nextInt(max(1, map.mapParameters.maxCoastExtension))) + if (tile.getTilesInDistance(coastLength).any { it.isLand }) { tile.baseTerrain = Constants.coast tile.setTransients() } } } - fun randomizeTile(tileInfo: TileInfo, mapParameters: MapParameters, ruleset: Ruleset) { - if (tileInfo.getBaseTerrain().type == TerrainType.Land && Math.random() < 0.05f) { - tileInfo.baseTerrain = Constants.mountain - tileInfo.setTransients() + private fun randomizeTiles(tileMap: TileMap) { + + for (tile in tileMap.values) { + if (tile.getBaseTerrain().type == TerrainType.Land && RNG.nextDouble() < tileMap.mapParameters.mountainProbability) { + tile.baseTerrain = Constants.mountain + tile.setTransients() + } + addRandomTerrainFeature(tile, tileMap.mapParameters) } - addRandomTerrainFeature(tileInfo, ruleset) } - fun getLatitude(vector: Vector2): Float { - return abs(vector.x + vector.y) - } + private fun divideIntoBiomes(tileMap: TileMap) { + val averageTilesPerArea = tileMap.mapParameters.tilesPerBiomeArea + val waterPercent = tileMap.mapParameters.waterProbability + + val maxLatitude = tileMap.values.map { abs(HexMath.getLatitude(it.position)) }.max()!! - fun divideIntoBiomes(map: TileMap, averageTilesPerArea: Int, waterPercent: Float, distance: Int, ruleset: Ruleset) { val areas = ArrayList() val terrains = ruleset.terrains.values .filter { it.type === TerrainType.Land && it.name != Constants.lakes && it.name != Constants.mountain } - for (tile in map.values.filter { it.baseTerrain == Constants.grassland }) tile.baseTerrain = "" // So we know it's not chosen + // So we know it's not chosen + for (tile in tileMap.values.filter { it.baseTerrain == Constants.grassland }) + tile.baseTerrain = "" - while (map.values.any { it.baseTerrain == "" }) // the world could be split into lots off tiny islands, and every island deserves land types + while (tileMap.values.any { it.baseTerrain == "" }) // the world could be split into lots off tiny islands, and every island deserves land types { - val emptyTiles = map.values.filter { it.baseTerrain == "" }.toMutableList() + val emptyTiles = tileMap.values.filter { it.baseTerrain == "" }.toMutableList() val numberOfSeeds = ceil(emptyTiles.size / averageTilesPerArea.toFloat()).toInt() - val maxLatitude = abs(getLatitude(Vector2(distance.toFloat(), distance.toFloat()))) + for (i in 0 until numberOfSeeds) { - var terrain = if (Math.random() > waterPercent) terrains.random().name - else Constants.ocean - val tile = emptyTiles.random() + var terrain = if (RNG.nextDouble() < waterPercent) Constants.ocean + else terrains.random(RNG).name - //change grassland to desert or tundra based on y - if (abs(getLatitude(tile.position)) < maxLatitude * 0.1) { - if (terrain == Constants.grassland || terrain == Constants.tundra) + val tile = emptyTiles.random(RNG) + + val desertBand = maxLatitude * 0.5 * tileMap.mapParameters.temperatureExtremeness + val tundraBand = maxLatitude * (1 - 0.5 * tileMap.mapParameters.temperatureExtremeness) + + if (abs(HexMath.getLatitude(tile.position)) < desertBand) { + + if (terrain in arrayOf(Constants.grassland, Constants.tundra)) terrain = Constants.desert - } else if (abs(getLatitude(tile.position)) > maxLatitude * 0.7) { - if (terrain == Constants.grassland || terrain == Constants.plains || terrain == Constants.desert || terrain == Constants.ocean) { + + } else if (abs(HexMath.getLatitude(tile.position)) > tundraBand) { + + if (terrain in arrayOf(Constants.grassland, Constants.plains, Constants.desert, Constants.ocean)) terrain = Constants.tundra - } + } else { if (terrain == Constants.tundra) terrain = Constants.plains else if (terrain == Constants.desert) terrain = Constants.grassland @@ -159,14 +161,16 @@ class MapGenerator { expandAreas(areas) expandAreas(areas) } + + for (tile in tileMap.values) + tile.setTransients() } - - fun expandAreas(areas: ArrayList) { + private fun expandAreas(areas: ArrayList) { val expandableAreas = ArrayList(areas) while (expandableAreas.isNotEmpty()) { - val areaToExpand = expandableAreas.random() + val areaToExpand = expandableAreas.random(RNG) if (areaToExpand.tiles.size >= 20) { expandableAreas -= areaToExpand continue @@ -178,7 +182,7 @@ class MapGenerator { if (availableExpansionTiles.isEmpty()) expandableAreas -= areaToExpand else { - val expansionTile = availableExpansionTiles.random() + val expansionTile = availableExpansionTiles.random(RNG) areaToExpand.addTile(expansionTile) val areasToJoin = areas.filter { @@ -195,40 +199,48 @@ class MapGenerator { } } - - fun addRandomTerrainFeature(tileInfo: TileInfo, ruleset: Ruleset) { - if (tileInfo.getBaseTerrain().canHaveOverlay && Math.random() > 0.7f) { + private fun addRandomTerrainFeature(tileInfo: TileInfo, mapParameters: MapParameters) { + if (tileInfo.getBaseTerrain().canHaveOverlay && RNG.nextDouble() < mapParameters.terrainFeatureRichness) { val secondaryTerrains = ruleset.terrains.values - .filter { it.type === TerrainType.TerrainFeature && it.occursOn != null && it.occursOn.contains(tileInfo.baseTerrain) } - if (secondaryTerrains.any()) tileInfo.terrainFeature = secondaryTerrains.random().name + .filter { it.type === TerrainType.TerrainFeature && + it.occursOn != null && + it.occursOn.contains(tileInfo.baseTerrain) } + if (secondaryTerrains.any()) + tileInfo.terrainFeature = secondaryTerrains.random(RNG).name } } - - fun spreadAncientRuins(map: TileMap) { + private fun spreadAncientRuins(map: TileMap) { + if(map.mapParameters.noRuins) + return val suitableTiles = map.values.filter { it.isLand && !it.getBaseTerrain().impassable } val locations = chooseSpreadOutLocations(suitableTiles.size/100, suitableTiles, 10) for(tile in locations) - tile.improvement =Constants.ancientRuins + tile.improvement = Constants.ancientRuins } - - fun spreadResources(mapToReturn: TileMap, distance: Int, ruleset: Ruleset) { + private fun spreadResources(mapToReturn: TileMap) { + val distance = mapToReturn.mapParameters.size.radius for (tile in mapToReturn.values) if (tile.resource != null) tile.resource = null - spreadStrategicResources(mapToReturn, distance, ruleset) - spreadResource(mapToReturn, distance, ResourceType.Luxury, ruleset) - spreadResource(mapToReturn, distance, ResourceType.Bonus, ruleset) + spreadStrategicResources(mapToReturn, distance) + spreadResource(mapToReturn, distance, ResourceType.Luxury) + spreadResource(mapToReturn, distance, ResourceType.Bonus) } + //region natural-wonders + /* https://gaming.stackexchange.com/questions/95095/do-natural-wonders-spawn-more-closely-to-city-states/96479 https://www.reddit.com/r/civ/comments/1jae5j/information_on_the_occurrence_of_natural_wonders/ */ - fun spawnNaturalWonders(mapToReturn: TileMap, mapRadius: Int, ruleset: Ruleset) { + private fun spawnNaturalWonders(tileMap: TileMap) { + if (tileMap.mapParameters.noNaturalWonders) + return + val mapRadius = tileMap.mapParameters.size.radius // number of Natural Wonders scales linearly with mapRadius as #wonders = mapRadius * 0.13133208 - 0.56128831 val numberToSpawn = round(mapRadius * 0.13133208f - 0.56128831f).toInt() @@ -238,7 +250,7 @@ class MapGenerator { while (allNaturalWonders.isNotEmpty() && toBeSpawned.size < numberToSpawn) { val totalWeight = allNaturalWonders.map { it.weight }.sum().toFloat() - val random = Random().nextDouble() + val random = RNG.nextDouble() var sum = 0f for (wonder in allNaturalWonders) { sum += wonder.weight/totalWeight @@ -254,16 +266,16 @@ class MapGenerator { for (wonder in toBeSpawned) { when (wonder.name) { - Constants.barringerCrater -> spawnBarringerCrater(mapToReturn, ruleset) - Constants.mountFuji -> spawnMountFuji(mapToReturn, ruleset) - Constants.grandMesa -> spawnGrandMesa(mapToReturn, ruleset) - Constants.greatBarrierReef -> spawnGreatBarrierReef(mapToReturn, ruleset, mapRadius) - Constants.krakatoa -> spawnKrakatoa(mapToReturn, ruleset) - Constants.rockOfGibraltar -> spawnRockOfGibraltar(mapToReturn, ruleset) - Constants.oldFaithful -> spawnOldFaithful(mapToReturn, ruleset) - Constants.cerroDePotosi -> spawnCerroDePotosi(mapToReturn, ruleset) - Constants.elDorado -> spawnElDorado(mapToReturn, ruleset) - Constants.fountainOfYouth -> spawnFountainOfYouth(mapToReturn, ruleset) + Constants.barringerCrater -> spawnBarringerCrater(tileMap, ruleset) + Constants.mountFuji -> spawnMountFuji(tileMap, ruleset) + Constants.grandMesa -> spawnGrandMesa(tileMap, ruleset) + Constants.greatBarrierReef -> spawnGreatBarrierReef(tileMap, ruleset, mapRadius) + Constants.krakatoa -> spawnKrakatoa(tileMap, ruleset) + Constants.rockOfGibraltar -> spawnRockOfGibraltar(tileMap, ruleset) + Constants.oldFaithful -> spawnOldFaithful(tileMap, ruleset) + Constants.cerroDePotosi -> spawnCerroDePotosi(tileMap, ruleset) + Constants.elDorado -> spawnElDorado(tileMap, ruleset) + Constants.fountainOfYouth -> spawnFountainOfYouth(tileMap, ruleset) } } } @@ -339,11 +351,11 @@ class MapGenerator { */ private fun spawnGreatBarrierReef(mapToReturn: TileMap, ruleset: Ruleset, mapRadius: Int) { val wonder = ruleset.terrains[Constants.greatBarrierReef]!! - val maxLatitude = abs(getLatitude(Vector2(mapRadius.toFloat(), mapRadius.toFloat()))) + val maxLatitude = abs(HexMath.getLatitude(Vector2(mapRadius.toFloat(), mapRadius.toFloat()))) val suitableLocations = mapToReturn.values.filter { it.resource == null && it.improvement == null && wonder.occursOn!!.contains(it.getLastTerrain().name) - && abs(getLatitude(it.position)) > maxLatitude * 0.1 - && abs(getLatitude(it.position)) < maxLatitude * 0.7 + && abs(HexMath.getLatitude(it.position)) > maxLatitude * 0.1 + && abs(HexMath.getLatitude(it.position)) < maxLatitude * 0.7 && it.neighbors.all {neighbor -> neighbor.isWater} && it.neighbors.any {neighbor -> neighbor.resource == null && neighbor.improvement == null @@ -472,9 +484,10 @@ class MapGenerator { trySpawnOnSuitableLocation(suitableLocations, wonder) } + //endregion // Here, we need each specific resource to be spread over the map - it matters less if specific resources are near each other - private fun spreadStrategicResources(mapToReturn: TileMap, distance: Int, ruleset: Ruleset) { + private fun spreadStrategicResources(mapToReturn: TileMap, distance: Int) { val resourcesOfType = ruleset.tileResources.values.filter { it.resourceType == ResourceType.Strategic } for (resource in resourcesOfType) { val suitableTiles = mapToReturn.values @@ -490,12 +503,12 @@ class MapGenerator { } // Here, we need there to be some luxury/bonus resource - it matters less what - private fun spreadResource(mapToReturn: TileMap, distance: Int, resourceType: ResourceType, ruleset: Ruleset) { + private fun spreadResource(mapToReturn: TileMap, distance: Int, resourceType: ResourceType) { val resourcesOfType = ruleset.tileResources.values.filter { it.resourceType == resourceType } val suitableTiles = mapToReturn.values .filter { it.resource == null && resourcesOfType.any { r -> r.terrainsCanBeFoundOn.contains(it.getLastTerrain().name) } } - val numberOfResources = mapToReturn.values.count { it.isLand && !it.getBaseTerrain().impassable } / 15 + val numberOfResources = (mapToReturn.values.count { it.isLand && !it.getBaseTerrain().impassable } * mapToReturn.mapParameters.resourceRichness).toInt() val locations = chooseSpreadOutLocations(numberOfResources, suitableTiles, distance) val resourceToNumber = Counter() @@ -511,7 +524,7 @@ class MapGenerator { } } - fun chooseSpreadOutLocations(numberOfResources: Int, suitableTiles: List, initialDistance: Int): ArrayList { + private fun chooseSpreadOutLocations(numberOfResources: Int, suitableTiles: List, initialDistance: Int): ArrayList { for (distanceBetweenResources in initialDistance downTo 1) { var availableTiles = suitableTiles.toList() @@ -542,29 +555,125 @@ class MapGenerator { } throw Exception("Couldn't choose suitable tiles for $numberOfResources resources!") } -} -class MapLandmassGenerator { + companion object MapLandmassGenerator { + var RNG = Random(42) - fun generateLandCellularAutomata(tileMap: TileMap, mapRadius: Int, mapType: String) { - - val numSmooth = 4 - - //init - for (tile in tileMap.values) { - val terrainType = getInitialTerrainCellularAutomata(tile, mapRadius, mapType) - if (terrainType == TerrainType.Land) tile.baseTerrain = Constants.grassland - else tile.baseTerrain = Constants.ocean - tile.setTransients() + fun generateLand(tileMap: TileMap) { + when (tileMap.mapParameters.type) { + MapType.pangaea -> createPangea(tileMap) + MapType.continents -> createTwoContinents(tileMap) + MapType.perlin -> createPerlin(tileMap) + MapType.default -> generateLandCellularAutomata(tileMap) + } } - //smooth - val grassland = Constants.grassland - val ocean = Constants.ocean - - for (loop in 0..numSmooth) { + private fun smooth(tileMap: TileMap) { for (tileInfo in tileMap.values) { - if (HexMath.getDistance(Vector2.Zero, tileInfo.position) < mapRadius) { + val numberOfLandNeighbors = tileInfo.neighbors.count { it.baseTerrain == Constants.grassland } + if (RNG.nextFloat() < 0.5f) + continue + + if (numberOfLandNeighbors > 3) + tileInfo.baseTerrain = Constants.grassland + else if (numberOfLandNeighbors < 3) + tileInfo.baseTerrain = Constants.ocean + } + } + + private fun createPerlin(tileMap: TileMap) { + val elevationSeed = RNG.nextInt().toDouble() + for (tile in tileMap.values) { + var elevation = getPerlinNoise(tile, elevationSeed) + + when { + elevation < 0 -> tile.baseTerrain = Constants.ocean + else -> tile.baseTerrain = Constants.grassland + } + } + } + + private fun createPangea(tileMap: TileMap) { + val elevationSeed = RNG.nextInt().toDouble() + for (tile in tileMap.values) { + var elevation = getPerlinNoise(tile, elevationSeed) + elevation = (elevation + getCircularNoise(tile, tileMap) ) / 2.0 + + when { + elevation < 0 -> tile.baseTerrain = Constants.ocean + else -> tile.baseTerrain = Constants.grassland + } + } + } + + private fun createTwoContinents(tileMap: TileMap) { + val elevationSeed = RNG.nextInt().toDouble() + for (tile in tileMap.values) { + var elevation = getPerlinNoise(tile, elevationSeed) + elevation = (elevation + getTwoContinentsTransform(tile, tileMap)) / 2.0 + + when { + elevation < 0 -> tile.baseTerrain = Constants.ocean + else -> tile.baseTerrain = Constants.grassland + } + } + } + + private fun getCircularNoise(tileInfo: TileInfo, tileMap: TileMap): Double { + val randomScale = RNG.nextDouble() + val distanceFactor = percentualDistanceToCenter(tileInfo, tileMap) + + return min(0.3, 1.0 - (5.0 * distanceFactor * distanceFactor + randomScale) / 3.0) + } + + private fun getTwoContinentsTransform(tileInfo: TileInfo, tileMap: TileMap): Double { + val randomScale = RNG.nextDouble() + val maxLongitude = abs(tileMap.values.map { abs(HexMath.getLongitude(it.position)) }.max()!!) + val longitudeFactor = abs(HexMath.getLongitude(tileInfo.position))/maxLongitude + + return min(0.0,-1.0 + (5.0 * longitudeFactor.pow(0.7f) + randomScale) / 3.0) + } + + private fun percentualDistanceToCenter(tileInfo: TileInfo, tileMap: TileMap): Double { + val mapRadius = tileMap.mapParameters.size.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 getPerlinNoise(tile: TileInfo, seed: Double, + nOctaves: Int = 6, + persistence: Double = 0.5, + lacunarity: Double = 2.0, + scale: Double = 10.0): Double { + val worldCoords = HexMath.hex2WorldCoords(tile.position) + return Perlin.noise3d(worldCoords.x.toDouble(), worldCoords.y.toDouble(), seed, nOctaves, persistence, lacunarity, scale) + } + + // region Cellular automata + private fun generateLandCellularAutomata(tileMap: TileMap) { + val mapRadius = tileMap.mapParameters.size.radius + val mapType = tileMap.mapParameters.type + 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.setTransients() + } + + //smooth + val grassland = Constants.grassland + val ocean = Constants.ocean + + for (loop in 0..numSmooth) { + for (tileInfo in tileMap.values) { + //if (HexMath.getDistance(Vector2.Zero, tileInfo.position) < mapRadius) { val numberOfLandNeighbors = tileInfo.neighbors.count { it.baseTerrain == grassland } if (tileInfo.baseTerrain == grassland) { // land tile if (numberOfLandNeighbors < 3) @@ -573,90 +682,36 @@ class MapLandmassGenerator { if (numberOfLandNeighbors > 3) tileInfo.baseTerrain = grassland } - } else { - tileInfo.baseTerrain = ocean - } - } - - if (mapType == MapType.continents) { //keep a ocean column in the middle - for (y in -mapRadius..mapRadius) { - tileMap.get(Vector2((y / 2).toFloat(), y.toFloat())).baseTerrain = ocean - tileMap.get(Vector2((y / 2 + 1).toFloat(), y.toFloat())).baseTerrain = ocean + /*} else { + tileInfo.baseTerrain = ocean + }*/ } } } - } - private fun getInitialTerrainCellularAutomata(tileInfo: TileInfo, mapRadius: Int, mapType: String): TerrainType { + private fun getInitialTerrainCellularAutomata(tileInfo: TileInfo, mapParameters: MapParameters): TerrainType { + val landProbability = mapParameters.landProbability + val mapRadius = mapParameters.size.radius - val landProbability = 0.55f - - if (mapType == MapType.pangaea) { - val distanceFactor = (HexMath.getDistance(Vector2.Zero, tileInfo.position) * 1.8 / mapRadius).toFloat() - if (Random().nextDouble() < landProbability.pow(distanceFactor)) return TerrainType.Land - else return TerrainType.Water - } - - if (mapType == MapType.continents) { - val distanceWeight = min(getDistanceWeightForContinents(Vector2(mapRadius.toFloat() / 2, 0f), tileInfo.position), - getDistanceWeightForContinents(Vector2(-mapRadius.toFloat() / 2, 0f), tileInfo.position)) - val distanceFactor = (distanceWeight * 1.8 / mapRadius).toFloat() - if (Random().nextDouble() < landProbability.pow(distanceFactor)) return TerrainType.Land - else return TerrainType.Water - } - - // default - if (HexMath.getDistance(Vector2.Zero, tileInfo.position) > 0.9f * mapRadius) { - if (Random().nextDouble() < 0.1) return TerrainType.Land else return TerrainType.Water - } - if (HexMath.getDistance(Vector2.Zero, tileInfo.position) > 0.85f * mapRadius) { - if (Random().nextDouble() < 0.2) return TerrainType.Land else return TerrainType.Water - } - if (Random().nextDouble() < landProbability) return TerrainType.Land else return TerrainType.Water - } - - - private fun getDistanceWeightForContinents(origin: Vector2, destination: Vector2): Float { - val relative_x = 2 * (origin.x - destination.x) - val relative_y = origin.y - destination.y - if (relative_x * relative_y >= 0) - return max(abs(relative_x), abs(relative_y)) - else - return (abs(relative_x) + abs(relative_y)) - } - - - /** - * This generator simply generates Perlin noise, - * "spreads" it out according to the ratio in generateTile, - * and assigns it as the height of the various tiles. - * Tiles below a certain height threshold (determined in generateTile, currently 50%) - * are considered water tiles, the rest are land tiles - */ - fun generateLandPerlin(tileMap: TileMap) { - val mapRandomSeed = Random().nextDouble() // without this, all the "random" maps would look the same - for (tile in tileMap.values) { - val ratio = 1 / 10.0 - val vector = tile.position - val height = Perlin.noise(vector.x * ratio, vector.y * ratio, mapRandomSeed) - +Perlin.noise(vector.x * ratio * 2, vector.y * ratio * 2, mapRandomSeed) / 2 - +Perlin.noise(vector.x * ratio * 4, vector.y * ratio * 4, mapRandomSeed) / 4 - when { // If we want to change water levels, we could raise or lower the >0 - height > 0 -> tile.baseTerrain = Constants.grassland - else -> tile.baseTerrain = Constants.ocean + // default + if (HexMath.getDistance(Vector2.Zero, tileInfo.position) > 0.9f * mapRadius) { + if (RNG.nextDouble() < 0.1) return TerrainType.Land else return TerrainType.Water } + if (HexMath.getDistance(Vector2.Zero, tileInfo.position) > 0.85f * mapRadius) { + if (RNG.nextDouble() < 0.2) return TerrainType.Land else return TerrainType.Water + } + if (RNG.nextDouble() < landProbability) return TerrainType.Land else return TerrainType.Water } - } + // endregion + } } - - class Area(var terrain: String) { val tiles = ArrayList() fun addTile(tileInfo: TileInfo) { - tiles+=tileInfo + tiles += tileInfo tileInfo.baseTerrain = terrain } } diff --git a/core/src/com/unciv/logic/map/MapParameters.kt b/core/src/com/unciv/logic/map/MapParameters.kt index 8e3b80f4c9..2cb364d1ab 100644 --- a/core/src/com/unciv/logic/map/MapParameters.kt +++ b/core/src/com/unciv/logic/map/MapParameters.kt @@ -8,10 +8,41 @@ enum class MapSize(val radius: Int) { Huge(40) } +object MapShape { + const val hexagonal = "Hexagonal" + const val rectangular = "Rectangular" +} + +object MapType { + const val pangaea = "Pangaea" + const val continents = "Continents" + const val perlin = "Perlin" + + // Cellular automata + const val default = "Default" + + // Non-generated maps + const val custom = "Custom" + + // All ocean tiles + const val empty = "Empty" +} + class MapParameters { var name = "" var type = MapType.pangaea + var shape = MapShape.hexagonal var size: MapSize = MapSize.Medium var noRuins = false - var noNaturalWonders = true + var noNaturalWonders = false + + var seed: Long = 0 + var tilesPerBiomeArea = 6 + var maxCoastExtension = 2 + var mountainProbability = 0.10f + var temperatureExtremeness = 0.30f + var terrainFeatureRichness = 0.30f + var resourceRichness = 0.10f + var waterProbability = 0.05f + var landProbability = 0.55f } \ No newline at end of file diff --git a/core/src/com/unciv/logic/map/Perlin.kt b/core/src/com/unciv/logic/map/Perlin.kt index 5c7798bd93..80d6c4e3e9 100644 --- a/core/src/com/unciv/logic/map/Perlin.kt +++ b/core/src/com/unciv/logic/map/Perlin.kt @@ -1,5 +1,6 @@ package com.unciv.logic.map +import kotlin.math.floor // version 1.1.3 // From https://rosettacode.org/wiki/Perlin_noise#Kotlin @@ -25,20 +26,44 @@ object Perlin { 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180 ) + private val grad3 = arrayOf( + intArrayOf(1,1,0), intArrayOf(-1,1,0), intArrayOf(1,-1,0), intArrayOf(-1,-1,0), + intArrayOf(1,0,1), intArrayOf(-1,0,1), intArrayOf(1,0,-1), intArrayOf(-1,0,-1), + intArrayOf(0,1,1), intArrayOf(0,-1,1), intArrayOf(0,1,-1), intArrayOf(0,-1,-1), + intArrayOf(1,0,-1), intArrayOf(-1,0,-1), intArrayOf(0,-1,1), intArrayOf(0,1,1)) + private val p = IntArray(512) { if (it < 256) permutation[it] else permutation[it - 256] } + fun noise3d(x: Double, y: Double, z: Double, + nOctaves: Int = 3, + persistence: Double = 0.5, + lacunarity: Double = 2.0, + scale: Double = 10.0): Double { + var freq = 1.0 + var amp = 1.0 + var max = 0.0 + var total = 0.0 + for (i in 0 until nOctaves) { + total += amp * noise(x * freq / scale, y * freq / scale, z * freq / scale) + max += amp + freq *= lacunarity + amp *= persistence + } + return total/max + } + fun noise(x: Double, y: Double, z: Double): Double { // Find unit cube that contains point - val xi = Math.floor(x).toInt() and 255 - val yi = Math.floor(y).toInt() and 255 - val zi = Math.floor(z).toInt() and 255 + val xi = floor(x).toInt() and 255 + val yi = floor(y).toInt() and 255 + val zi = floor(z).toInt() and 255 // Find relative x, y, z of point in cube - val xx = x - Math.floor(x) - val yy = y - Math.floor(y) - val zz = z - Math.floor(z) + val xx = x - floor(x) + val yy = y - floor(y) + val zz = z - floor(z) // Compute fade curves for each of xx, yy, zz val u = fade(xx) @@ -55,26 +80,22 @@ object Perlin { val ba = p[b] + zi val bb = p[b + 1] + zi - return lerp(w, lerp(v, lerp(u, grad(p[aa], xx, yy, zz), - grad(p[ba], xx - 1, yy, zz)), - lerp(u, grad(p[ab], xx, yy - 1, zz), - grad(p[bb], xx - 1, yy - 1, zz))), - lerp(v, lerp(u, grad(p[aa + 1], xx, yy, zz - 1), - grad(p[ba + 1], xx - 1, yy, zz - 1)), - lerp(u, grad(p[ab + 1], xx, yy - 1, zz - 1), - grad(p[bb + 1], xx - 1, yy - 1, zz - 1)))) + return lerp(w, lerp(v, lerp(u, grad3(p[aa], xx, yy, zz), + grad3(p[ba], xx - 1, yy, zz)), + lerp(u, grad3(p[ab], xx, yy - 1, zz), + grad3(p[bb], xx - 1, yy - 1, zz))), + lerp(v, lerp(u, grad3(p[aa + 1], xx, yy, zz - 1), + grad3(p[ba + 1], xx - 1, yy, zz - 1)), + lerp(u, grad3(p[ab + 1], xx, yy - 1, zz - 1), + grad3(p[bb + 1], xx - 1, yy - 1, zz - 1)))) } private fun fade(t: Double) = t * t * t * (t * (t * 6 - 15) + 10) private fun lerp(t: Double, a: Double, b: Double) = a + t * (b - a) - private fun grad(hash: Int, x: Double, y: Double, z: Double): Double { - // Convert low 4 bits of hash code into 12 gradient directions - val h = hash and 15 - val u = if (h < 8) x else y - val v = if (h < 4) y else if (h == 12 || h == 14) x else z - return (if ((h and 1) == 0) u else -u) + - (if ((h and 2) == 0) v else -v) + private fun grad3(hash: Int, x: Double, y: Double, z: Double): Double { + val h = hash and 15; + return x * grad3[h][0] + y * grad3[h][1] + z * grad3[h][2] } } \ No newline at end of file diff --git a/core/src/com/unciv/logic/map/TileMap.kt b/core/src/com/unciv/logic/map/TileMap.kt index b5a118f34f..f01c007687 100644 --- a/core/src/com/unciv/logic/map/TileMap.kt +++ b/core/src/com/unciv/logic/map/TileMap.kt @@ -10,14 +10,14 @@ import com.unciv.models.ruleset.Ruleset class TileMap { @Transient lateinit var gameInfo: GameInfo - @Transient var tileMatrix=ArrayList>() // this works several times faster than a hashmap, the performance difference is really astounding - @Transient var leftX=0 - @Transient var bottomY=0 + @Transient var tileMatrix = ArrayList>() // this works several times faster than a hashmap, the performance difference is really astounding + @Transient var leftX = 0 + @Transient var bottomY = 0 @Deprecated("as of 2.7.10") private var tiles = HashMap() - var mapParameters= MapParameters() + var mapParameters = MapParameters() private var tileList = ArrayList() constructor() // for json parsing, we need to have a default constructor @@ -33,10 +33,20 @@ class TileMap { get() = tileList - + /** generates an hexagonal map of given radius */ constructor(radius:Int, ruleset: Ruleset){ for(vector in HexMath.getVectorsInDistance(Vector2.Zero, radius)) - tileList.add(TileInfo().apply { position = vector; baseTerrain= Constants.grassland }) + tileList.add(TileInfo().apply { position = vector; baseTerrain = Constants.grassland }) + setTransients(ruleset) + } + + /** generates a rectangular map of given width and height*/ + constructor(width: Int, height: Int, ruleset: Ruleset) { + for(x in -width/2..width/2) + for (y in -height/2..height/2) + tileList.add(TileInfo().apply { + position = HexMath.evenQ2HexCoords(Vector2(x.toFloat(),y.toFloat())) + baseTerrain = Constants.grassland }) setTransients(ruleset) } @@ -230,8 +240,5 @@ class TileMap { tileInfo.setTransients() } } - - - } diff --git a/core/src/com/unciv/ui/mapeditor/NewMapScreen.kt b/core/src/com/unciv/ui/mapeditor/NewMapScreen.kt index 27560ac049..81e8238267 100644 --- a/core/src/com/unciv/ui/mapeditor/NewMapScreen.kt +++ b/core/src/com/unciv/ui/mapeditor/NewMapScreen.kt @@ -46,7 +46,7 @@ class NewMapScreen : PickerScreen() { try { // Map generation can take a while and we don't want ANRs val ruleset = RulesetCache.getBaseRuleset() - generatedMap = MapGenerator().generateMap(mapParameters, ruleset) + generatedMap = MapGenerator(ruleset).generateMap(mapParameters) Gdx.app.postRunnable { UncivGame.Current.setScreen(MapEditorScreen(generatedMap!!)) diff --git a/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt b/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt index 3b2f837f6f..c353302edf 100644 --- a/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt @@ -2,13 +2,17 @@ package com.unciv.ui.newgamescreen import com.badlogic.gdx.scenes.scene2d.Actor import com.badlogic.gdx.scenes.scene2d.ui.CheckBox +import com.badlogic.gdx.scenes.scene2d.ui.Slider import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.scenes.scene2d.ui.TextButton import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener import com.unciv.logic.map.MapParameters +import com.unciv.logic.map.MapShape import com.unciv.logic.map.MapSize import com.unciv.logic.map.MapType import com.unciv.models.translations.tr import com.unciv.ui.utils.CameraStageBaseScreen +import com.unciv.ui.utils.onClick import com.unciv.ui.utils.toLabel /** Table for editing [mapParameters] @@ -17,21 +21,41 @@ import com.unciv.ui.utils.toLabel * * @param isEmptyMapAllowed whether the [MapType.empty] option should be present. Is used by the Map Editor, but should **never** be used with the New Game * */ -class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed: Boolean = false) : +class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed: Boolean = false): Table() { - + lateinit var mapTypeSelectBox: TranslatedSelectBox lateinit var noRuinsCheckbox: CheckBox lateinit var noNaturalWondersCheckbox: CheckBox init { + skin = CameraStageBaseScreen.skin + defaults().pad(5f) + addMapShapeSelectBox() addMapTypeSelectBox() addWorldSizeSelectBox() addNoRuinsCheckbox() addNoNaturalWondersCheckbox() + addAdvancedSettings() + } + + private fun addMapShapeSelectBox() { + val mapShapes = listOfNotNull( + MapShape.hexagonal, + MapShape.rectangular + ) + val mapShapeSelectBox = + TranslatedSelectBox(mapShapes, mapParameters.shape, skin) + mapShapeSelectBox.addListener(object : ChangeListener() { + override fun changed(event: ChangeEvent?, actor: Actor?) { + mapParameters.shape = mapShapeSelectBox.selected.value + } + }) + + add ("{Map shape}:".toLabel()).left() + add(mapShapeSelectBox).fillX().row() } private fun addMapTypeSelectBox() { - add("{Map generation type}:".toLabel()) val mapTypes = listOfNotNull( MapType.default, @@ -40,8 +64,8 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed MapType.perlin, if (isEmptyMapAllowed) MapType.empty else null ) - val mapTypeSelectBox = - TranslatedSelectBox(mapTypes, mapParameters.type, CameraStageBaseScreen.skin) + + mapTypeSelectBox = TranslatedSelectBox(mapTypes, mapParameters.type, skin) mapTypeSelectBox.addListener(object : ChangeListener() { override fun changed(event: ChangeEvent?, actor: Actor?) { @@ -52,17 +76,17 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed noNaturalWondersCheckbox.isVisible = mapParameters.type != MapType.empty } }) - add(mapTypeSelectBox).row() + + add("{Map generation type}:".toLabel()).left() + add(mapTypeSelectBox).fillX().row() } private fun addWorldSizeSelectBox() { - - val worldSizeLabel = "{World size}:".toLabel() val worldSizeSelectBox = TranslatedSelectBox( MapSize.values().map { it.name }, mapParameters.size.name, - CameraStageBaseScreen.skin + skin ) worldSizeSelectBox.addListener(object : ChangeListener() { @@ -71,12 +95,12 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed } }) - add(worldSizeLabel) - add(worldSizeSelectBox).pad(10f).row() + add("{World size}:".toLabel()).left() + add(worldSizeSelectBox).fillX().row() } private fun addNoRuinsCheckbox() { - noRuinsCheckbox = CheckBox("No ancient ruins".tr(), CameraStageBaseScreen.skin) + noRuinsCheckbox = CheckBox("No ancient ruins".tr(), skin) noRuinsCheckbox.isChecked = mapParameters.noRuins noRuinsCheckbox.addListener(object : ChangeListener() { override fun changed(event: ChangeEvent?, actor: Actor?) { @@ -87,7 +111,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed } private fun addNoNaturalWondersCheckbox() { - noNaturalWondersCheckbox = CheckBox("No Natural Wonders".tr(), CameraStageBaseScreen.skin) + noNaturalWondersCheckbox = CheckBox("No Natural Wonders".tr(), skin) noNaturalWondersCheckbox.isChecked = mapParameters.noNaturalWonders noNaturalWondersCheckbox.addListener(object : ChangeListener() { override fun changed(event: ChangeEvent?, actor: Actor?) { @@ -96,4 +120,121 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed }) add(noNaturalWondersCheckbox).colspan(2).row() } + + private fun addAdvancedSettings() { + val button = TextButton("Show advanced settings".tr(), skin) + val advancedSettingsTable = Table().apply {isVisible = false; defaults().pad(5f)} + + add(button).colspan(2).row() + val advancedSettingsCell = add(Table()).colspan(2) + row() + + button.onClick { + advancedSettingsTable.isVisible = !advancedSettingsTable.isVisible + + if (advancedSettingsTable.isVisible) { + button.setText("Hide advanced settings".tr()) + advancedSettingsCell.setActor(advancedSettingsTable) + } else { + button.setText("Show advanced settings".tr()) + advancedSettingsCell.setActor(Table()) + } + } + + + val averageHeightSlider = Slider(0f,1f,0.01f, false, skin).apply { + addListener(object : ChangeListener() { + override fun changed(event: ChangeEvent?, actor: Actor?) { + mapParameters.mountainProbability = this@apply.value + } + }) + } + averageHeightSlider.value = mapParameters.mountainProbability + advancedSettingsTable.add("Map Height".toLabel()).left() + advancedSettingsTable.add(averageHeightSlider).fillX().row() + + + val tempExtremeSlider = Slider(0f,1f,0.01f, false, skin).apply { + addListener(object : ChangeListener() { + override fun changed(event: ChangeEvent?, actor: Actor?) { + mapParameters.temperatureExtremeness = this@apply.value + } + }) + } + tempExtremeSlider.value = mapParameters.temperatureExtremeness + advancedSettingsTable.add("Temperature extremeness".toLabel()).left() + advancedSettingsTable.add(tempExtremeSlider).fillX().row() + + + val resourceRichnessSlider = Slider(0f,1f,0.01f, false, skin).apply { + addListener(object : ChangeListener() { + override fun changed(event: ChangeEvent?, actor: Actor?) { + mapParameters.resourceRichness = this@apply.value + } + }) + } + resourceRichnessSlider.value = mapParameters.resourceRichness + advancedSettingsTable.add("Resource richness".toLabel()).left() + advancedSettingsTable.add(resourceRichnessSlider).fillX().row() + + + val terrainFeatureRichnessSlider = Slider(0f,1f,0.01f, false, skin).apply { + addListener(object : ChangeListener() { + override fun changed(event: ChangeEvent?, actor: Actor?) { + mapParameters.terrainFeatureRichness = this@apply.value + } + }) + } + terrainFeatureRichnessSlider.value = mapParameters.terrainFeatureRichness + advancedSettingsTable.add("Terrain Features richness".toLabel()).left() + advancedSettingsTable.add(terrainFeatureRichnessSlider).fillX().row() + + + val maxCoastExtensionSlider = Slider(0f,5f,1f, false, skin).apply { + addListener(object : ChangeListener() { + override fun changed(event: ChangeEvent?, actor: Actor?) { + mapParameters.maxCoastExtension = this@apply.value.toInt() + } + }) + } + maxCoastExtensionSlider.value = mapParameters.maxCoastExtension.toFloat() + advancedSettingsTable.add("Max Coast extension".toLabel()).left() + advancedSettingsTable.add(maxCoastExtensionSlider).fillX().row() + + + val tilesPerBiomeAreaSlider = Slider(0f,15f,1f, false, skin).apply { + addListener(object : ChangeListener() { + override fun changed(event: ChangeEvent?, actor: Actor?) { + mapParameters.tilesPerBiomeArea = this@apply.value.toInt() + } + }) + } + tilesPerBiomeAreaSlider.value = mapParameters.tilesPerBiomeArea.toFloat() + advancedSettingsTable.add("Biome areas extension".toLabel()).left() + advancedSettingsTable.add(tilesPerBiomeAreaSlider).fillX().row() + + + val waterPercentSlider = Slider(0f,1f,0.01f, false, skin).apply { + addListener(object : ChangeListener() { + override fun changed(event: ChangeEvent?, actor: Actor?) { + mapParameters.waterProbability = this@apply.value + } + }) + } + waterPercentSlider.value = mapParameters.waterProbability + advancedSettingsTable.add("Water percent".toLabel()).left() + advancedSettingsTable.add(waterPercentSlider).fillX().row() + + + val landPercentSlider = Slider(0f,1f,0.01f, false, skin).apply { + addListener(object : ChangeListener() { + override fun changed(event: ChangeEvent?, actor: Actor?) { + mapParameters.landProbability = this@apply.value + } + }) + } + landPercentSlider.value = mapParameters.landProbability + advancedSettingsTable.add("Land percent".toLabel()).left() + advancedSettingsTable.add(landPercentSlider).fillX().row() + } } \ No newline at end of file diff --git a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt index 7c2905829f..058d47ee1d 100644 --- a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt +++ b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt @@ -32,7 +32,7 @@ class NewGameScreen: PickerScreen(){ val playerPickerTable = PlayerPickerTable(this, newGameParameters) val newGameScreenOptionsTable = NewGameScreenOptionsTable(this) { playerPickerTable.update() } - topTable.add(ScrollPane(newGameScreenOptionsTable)).height(topTable.parent.height) + topTable.add(ScrollPane(newGameScreenOptionsTable).apply{setOverscroll(false,false)}).height(topTable.parent.height) topTable.add(playerPickerTable).pad(10f) topTable.pack() topTable.setFillParent(true) @@ -90,6 +90,8 @@ class NewGameScreen: PickerScreen(){ cantMakeThatMapPopup.addCloseButton() cantMakeThatMapPopup.open() Gdx.input.inputProcessor = stage + rightSideButton.enable() + rightSideButton.setText("Start game!".tr()) } } } diff --git a/core/src/com/unciv/ui/newgamescreen/NewGameScreenOptionsTable.kt b/core/src/com/unciv/ui/newgamescreen/NewGameScreenOptionsTable.kt index 3e61cf813a..5de273ee8a 100644 --- a/core/src/com/unciv/ui/newgamescreen/NewGameScreenOptionsTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/NewGameScreenOptionsTable.kt @@ -1,12 +1,14 @@ package com.unciv.ui.newgamescreen import com.badlogic.gdx.scenes.scene2d.Actor +import com.badlogic.gdx.scenes.scene2d.ui.Cell import com.badlogic.gdx.scenes.scene2d.ui.CheckBox import com.badlogic.gdx.scenes.scene2d.ui.SelectBox import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener import com.badlogic.gdx.utils.Array import com.unciv.logic.MapSaver +import com.unciv.logic.map.MapType import com.unciv.models.metadata.GameSpeed import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.VictoryType @@ -22,8 +24,14 @@ class NewGameScreenOptionsTable(val newGameScreen: NewGameScreen, val updatePlay val mapParameters = newGameScreen.mapParameters val ruleset = newGameScreen.ruleset + private var mapTypeSpecificTable = Table() + private val generatedMapOptionsTable = MapParametersTable(mapParameters) + private val savedMapOptionsTable = Table() + init { pad(10f) + top() + defaults().pad(5f) add("Map options".toLabel(fontSize = 24)).colspan(2).row() addMapTypeSelection() @@ -36,8 +44,7 @@ class NewGameScreenOptionsTable(val newGameScreen: NewGameScreen, val updatePlay addBarbariansCheckbox() addOneCityChallengeCheckbox() addIsOnlineMultiplayerCheckbox() - - addModCheckboxes() + addModCheckboxes() pack() } @@ -45,33 +52,31 @@ class NewGameScreenOptionsTable(val newGameScreen: NewGameScreen, val updatePlay private fun addMapTypeSelection() { add("{Map type}:".toLabel()) val mapTypes = arrayListOf("Generated") - if (MapSaver().getMaps().isNotEmpty()) mapTypes.add("Existing") - - val mapFileLabel = "{Map file}:".toLabel() - val mapFileSelectBox = getMapFileSelectBox() - mapFileLabel.isVisible = false - mapFileSelectBox.isVisible = false - + if (MapSaver().getMaps().isNotEmpty()) mapTypes.add(MapType.custom) val mapTypeSelectBox = TranslatedSelectBox(mapTypes, "Generated", CameraStageBaseScreen.skin) - val mapParameterTable = MapParametersTable(mapParameters) + val mapFileSelectBox = getMapFileSelectBox() + savedMapOptionsTable.defaults().pad(5f) + savedMapOptionsTable.add("{Map file}:".toLabel()).left() + // because SOME people gotta give the hugest names to their maps + savedMapOptionsTable.add(mapFileSelectBox).maxWidth(newGameScreen.stage.width / 2) + .right().row() fun updateOnMapTypeChange() { - mapParameters.type = mapTypeSelectBox.selected.value - if (mapParameters.type == "Existing") { - mapParameterTable.isVisible = false - mapFileSelectBox.isVisible = true - mapFileLabel.isVisible = true + mapTypeSpecificTable.clear() + if (mapTypeSelectBox.selected.value == MapType.custom) { + mapParameters.type = MapType.custom mapParameters.name = mapFileSelectBox.selected + mapTypeSpecificTable.add(savedMapOptionsTable) } else { - mapParameterTable.isVisible = true - mapFileSelectBox.isVisible = false - mapFileLabel.isVisible = false mapParameters.name = "" + mapParameters.type = generatedMapOptionsTable.mapTypeSelectBox.selected.value + mapTypeSpecificTable.add(generatedMapOptionsTable) } } - updateOnMapTypeChange() // activate once, so when we had a file map before we'll have the right things set for another one + // activate once, so when we had a file map before we'll have the right things set for another one + updateOnMapTypeChange() mapTypeSelectBox.addListener(object : ChangeListener() { override fun changed(event: ChangeEvent?, actor: Actor?) { @@ -79,12 +84,8 @@ class NewGameScreenOptionsTable(val newGameScreen: NewGameScreen, val updatePlay } }) - add(mapTypeSelectBox).pad(10f).row() - add(mapParameterTable).colspan(2).row() - - add(mapFileLabel) - add(mapFileSelectBox).maxWidth(newGameScreen.stage.width / 2) // because SOME people gotta give the hugest names to their maps - .pad(10f).row() + add(mapTypeSelectBox).row() + add(mapTypeSpecificTable).colspan(2).row() } @@ -147,7 +148,7 @@ class NewGameScreenOptionsTable(val newGameScreen: NewGameScreen, val updatePlay (0..ruleset.nations.filter { it.value.isCityState() }.size).forEach { cityStatesArray.add(it) } cityStatesSelectBox.items = cityStatesArray cityStatesSelectBox.selected = newGameParameters.numberOfCityStates - add(cityStatesSelectBox).pad(10f).row() + add(cityStatesSelectBox).row() cityStatesSelectBox.addListener(object : ChangeListener() { override fun changed(event: ChangeEvent?, actor: Actor?) { newGameParameters.numberOfCityStates = cityStatesSelectBox.selected @@ -163,7 +164,7 @@ class NewGameScreenOptionsTable(val newGameScreen: NewGameScreen, val updatePlay newGameParameters.difficulty = difficultySelectBox.selected.value } }) - add(difficultySelectBox).pad(10f).row() + add(difficultySelectBox).fillX().row() } private fun addGameSpeedSelectBox() { @@ -174,7 +175,7 @@ class NewGameScreenOptionsTable(val newGameScreen: NewGameScreen, val updatePlay newGameParameters.gameSpeed = GameSpeed.valueOf(gameSpeedSelectBox.selected.value) } }) - add(gameSpeedSelectBox).pad(10f).row() + add(gameSpeedSelectBox).fillX().row() } private fun addEraSelectBox() { @@ -187,16 +188,16 @@ class NewGameScreenOptionsTable(val newGameScreen: NewGameScreen, val updatePlay newGameParameters.startingEra = TechEra.valueOf(eraSelectBox.selected.value.replace(" era", "")) } }) - add(eraSelectBox).pad(10f).row() + add(eraSelectBox).fillX().row() } private fun addVictoryTypeCheckboxes() { - add("{Victory conditions}:".tr()).colspan(2).row() + add("{Victory conditions}:".toLabel()).colspan(2).row() // Create a checkbox for each VictoryType existing var i = 0 - val victoryConditionsTable = Table().apply { defaults().pad(10f) } + val victoryConditionsTable = Table().apply { defaults().pad(5f) } for (victoryType in VictoryType.values()) { if (victoryType == VictoryType.Neutral) continue val victoryCheckbox = CheckBox(victoryType.name.tr(), CameraStageBaseScreen.skin) @@ -212,7 +213,7 @@ class NewGameScreenOptionsTable(val newGameScreen: NewGameScreen, val updatePlay } } }) - victoryConditionsTable.add(victoryCheckbox) + victoryConditionsTable.add(victoryCheckbox).left() if (++i % 2 == 0) victoryConditionsTable.row() } add(victoryConditionsTable).colspan(2).row() @@ -233,7 +234,7 @@ class NewGameScreenOptionsTable(val newGameScreen: NewGameScreen, val updatePlay } add("{Mods}:".tr()).colspan(2).row() - val modCheckboxTable = Table().apply { defaults().pad(10f) } + val modCheckboxTable = Table().apply { defaults().pad(5f) } for(mod in modRulesets){ val checkBox = CheckBox(mod.name,CameraStageBaseScreen.skin) checkBox.addListener(object : ChangeListener() {