diff --git a/core/src/com/unciv/UnCivGame.kt b/core/src/com/unciv/UnCivGame.kt index c0bd7113d7..08109ee1de 100644 --- a/core/src/com/unciv/UnCivGame.kt +++ b/core/src/com/unciv/UnCivGame.kt @@ -23,7 +23,7 @@ class UnCivGame(val version: String) : Game() { * This exists so that when debugging we can see the entire map. * Remember to turn this to false before commit and upload! */ - var viewEntireMapForDebug = true + var viewEntireMapForDebug = false /** For when you need to test something in an advanced game and don't have time to faff around */ val superchargedForDebug = false diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index fd4a2e98b8..61fbf2fd9d 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -3,9 +3,7 @@ package com.unciv.logic import com.badlogic.gdx.math.Vector2 import com.unciv.Constants import com.unciv.logic.civilization.CivilizationInfo -import com.unciv.logic.map.BFS -import com.unciv.logic.map.TileInfo -import com.unciv.logic.map.TileMap +import com.unciv.logic.map.* import com.unciv.models.gamebasics.GameBasics import com.unciv.models.metadata.GameParameters import java.util.* @@ -18,7 +16,13 @@ class GameStarter{ val gameInfo = GameInfo() gameInfo.gameParameters = newGameParameters - gameInfo.tileMap = TileMap(newGameParameters) + + if(newGameParameters.mapType==MapType.file) + gameInfo.tileMap = MapSaver().loadMap(newGameParameters.mapFileName!!) + else gameInfo.tileMap = MapGenerator().generateMap(newGameParameters) + + gameInfo.tileMap.setTransients() + gameInfo.tileMap.gameInfo = gameInfo // need to set this transient before placing units in the map gameInfo.difficulty = newGameParameters.difficulty @@ -74,7 +78,7 @@ class GameStarter{ .map { it.improvement!!.replace("StartingLocation ", "") } val availableCityStatesNames = Stack() - // since we shuffle and then order by, we end up with all the city states with starting locations first in a random order, + // since we shuffle and then order by, we end up with all the city states with starting tiles first in a random order, // and then all the other city states in a random order! Because the sortedBy function is stable! availableCityStatesNames.addAll(GameBasics.Nations.filter { it.value.isCityState() }.keys .shuffled().sortedByDescending { it in cityStatesWithStartingLocations }) @@ -154,7 +158,7 @@ class GameStarter{ val presetStartingLocation = tilesWithStartingLocations.firstOrNull { it.improvement=="StartingLocation "+civ.civName } if(presetStartingLocation!=null) startingLocation = presetStartingLocation else { - if (freeTiles.isEmpty()) break // we failed to get all the starting locations with this minimum distance + if (freeTiles.isEmpty()) break // we failed to get all the starting tiles with this minimum distance var preferredTiles = freeTiles.toList() for (startBias in civ.nation.startBias) { @@ -175,7 +179,7 @@ class GameStarter{ for(tile in tilesWithStartingLocations) tile.improvement=null // get rid of the starting location improvements return startingLocations } - throw Exception("Didn't manage to get starting locations even with distance of 1?") + throw Exception("Didn't manage to get starting tiles even with distance of 1?") } private fun vectorIsAtLeastNTilesAwayFromEdge(vector: Vector2, n:Int, tileMap: TileMap): Boolean { diff --git a/core/src/com/unciv/logic/civilization/Notification.kt b/core/src/com/unciv/logic/civilization/Notification.kt index a7deaaf57a..8f1fb52364 100644 --- a/core/src/com/unciv/logic/civilization/Notification.kt +++ b/core/src/com/unciv/logic/civilization/Notification.kt @@ -23,7 +23,7 @@ interface NotificationAction { fun execute(worldScreen: WorldScreen) } -/** cycle through locations */ +/** cycle through tiles */ data class LocationAction(var locations: ArrayList = ArrayList()) : NotificationAction { constructor(locations: List): this(ArrayList(locations)) @@ -31,7 +31,7 @@ data class LocationAction(var locations: ArrayList = ArrayList()) : Not override fun execute(worldScreen: WorldScreen) { if (locations.isNotEmpty()) { var index = locations.indexOf(worldScreen.tileMapHolder.selectedTile?.position) - index = ++index % locations.size // cycle through locations + index = ++index % locations.size // cycle through tiles worldScreen.tileMapHolder.setCenterPosition(locations[index], selectUnit = false) } } diff --git a/core/src/com/unciv/logic/map/MapGenerator.kt b/core/src/com/unciv/logic/map/MapGenerator.kt new file mode 100644 index 0000000000..6551ae4a1a --- /dev/null +++ b/core/src/com/unciv/logic/map/MapGenerator.kt @@ -0,0 +1,389 @@ +package com.unciv.logic.map + +import com.badlogic.gdx.math.Vector2 +import com.unciv.Constants +import com.unciv.logic.HexMath +import com.unciv.models.Counter +import com.unciv.models.gamebasics.GameBasics +import com.unciv.models.gamebasics.tile.ResourceType +import com.unciv.models.gamebasics.tile.TerrainType +import com.unciv.models.metadata.GameParameters +import java.util.* +import kotlin.math.* + +// 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 file = "File" + } +} + +class MapGenerator() { + + fun generateMap(gameParameters: GameParameters): TileMap { + val mapRadius = gameParameters.mapRadius + val mapType = gameParameters.mapType + + val map = TileMap(mapRadius) + + // 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) + + for(tile in map.values) tile.setTransients() + + setWaterTiles(map) + + for(tile in map.values) randomizeTile(tile) + + randomizeResources(map, mapRadius) + + return map + } + + + + fun setWaterTiles(map: TileMap) { + + //define lakes + var waterTiles = map.values.filter { it.isWater } + val tilesInArea = ArrayList() + val tilesToCheck = ArrayList() + while (waterTiles.isNotEmpty()) { + val initialWaterTile = waterTiles.random() + tilesInArea += initialWaterTile + tilesToCheck += initialWaterTile + waterTiles -= initialWaterTile + + while (tilesToCheck.isNotEmpty()) { + val tileWeAreChecking = tilesToCheck.random() + for (vector in tileWeAreChecking.neighbors + .filter { !tilesInArea.contains(it) and waterTiles.contains(it) }) { + tilesInArea += vector + tilesToCheck += vector + waterTiles -= vector + } + tilesToCheck -= tileWeAreChecking + } + + if (tilesInArea.size <= 10) { + for (tile in tilesInArea) { + tile.baseTerrain = Constants.lakes + tile.setTransients() + } + } + tilesInArea.clear() + } + + //Coasts + for (tile in map.values.filter { it.baseTerrain == Constants.ocean }) { + if (tile.getTilesInDistance(2).any { it.isLand }) { + tile.baseTerrain = Constants.coast + tile.setTransients() + } + } + } + + fun randomizeTile(tileInfo: TileInfo){ + if(tileInfo.getBaseTerrain().type==TerrainType.Land && Math.random()<0.05f){ + tileInfo.baseTerrain = Constants.mountain + tileInfo.setTransients() + } + addRandomTerrainFeature(tileInfo) + maybeAddAncientRuins(tileInfo) + } + + fun getLatitude(vector: Vector2): Float { + return (sin(3.1416/3) * vector.y).toFloat() + } + + fun divideIntoBiomes(map: TileMap, averageTilesPerArea: Int, waterPercent: Float, distance: Int) { + val areas = ArrayList() + + val terrains = GameBasics.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 + + while(map.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 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() + + //change grassland to desert or tundra based on y + if (abs(getLatitude(tile.position)) < maxLatitude * 0.1) { + if (terrain == Constants.grassland || terrain == 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) { + terrain = Constants.tundra + } + } else { + if (terrain == Constants.tundra) terrain = Constants.plains + else if (terrain == Constants.desert) terrain = Constants.grassland + } + + val area = Area(terrain) + emptyTiles -= tile + area.addTile(tile) + areas += area + } + + expandAreas(areas) + expandAreas(areas) + } + } + + + fun expandAreas(areas: ArrayList) { + val expandableAreas = ArrayList(areas) + + while (expandableAreas.isNotEmpty()) { + val areaToExpand = expandableAreas.random() + if(areaToExpand.tiles.size>=20){ + expandableAreas -= areaToExpand + continue + } + + val availableExpansionTiles = areaToExpand.tiles + .flatMap { it.neighbors }.distinct() + .filter { it.baseTerrain=="" } + + if (availableExpansionTiles.isEmpty()) expandableAreas -= areaToExpand + else { + val expansionTile = availableExpansionTiles.random() + areaToExpand.addTile(expansionTile) + + val areasToJoin = areas.filter { + it.terrain == areaToExpand.terrain + && it != areaToExpand + && it.tiles.any { tile -> tile in expansionTile.neighbors } + } + for (area in areasToJoin) { + areaToExpand.tiles += area.tiles + areas.remove(area) + expandableAreas.remove(area) + } + } + } + } + + + fun addRandomTerrainFeature(tileInfo: TileInfo) { + if (tileInfo.getBaseTerrain().canHaveOverlay && Math.random() > 0.7f) { + val secondaryTerrains = GameBasics.Terrains.values + .filter { it.type === TerrainType.TerrainFeature && it.occursOn!!.contains(tileInfo.baseTerrain) } + if (secondaryTerrains.any()) tileInfo.terrainFeature = secondaryTerrains.random().name + } + } + + + fun maybeAddAncientRuins(tile: TileInfo) { + val baseTerrain = tile.getBaseTerrain() + if(baseTerrain.type!=TerrainType.Water && !baseTerrain.impassable && Random().nextDouble() < 1f/100) + tile.improvement = Constants.ancientRuins + } + + + fun randomizeResources(mapToReturn: TileMap, distance: Int) { + for(tile in mapToReturn.values) + if(tile.resource!=null) + tile.resource=null + + randomizeStrategicResources(mapToReturn, distance) + randomizeResource(mapToReturn, distance, ResourceType.Luxury) + randomizeResource(mapToReturn, distance, ResourceType.Bonus) + } + + // Here, we need each specific resource to be spread over the map - it matters less if specific resources are near each other + private fun randomizeStrategicResources(mapToReturn: TileMap, distance: Int) { + val resourcesOfType = GameBasics.TileResources.values.filter { it.resourceType == ResourceType.Strategic } + for (resource in resourcesOfType) { + val suitableTiles = mapToReturn.values + .filter { it.resource == null && resource.terrainsCanBeFoundOn.contains(it.getLastTerrain().name) } + + val averageTilesPerResource = 15 * resourcesOfType.count() + val numberOfResources = mapToReturn.values.count { it.isLand && !it.getBaseTerrain().impassable } / averageTilesPerResource + + val locations = chooseSpreadOutLocations(numberOfResources, suitableTiles, distance) + + for (location in locations) location.resource = resource.name + } + } + + // Here, we need there to be some luxury/bonus resource - it matters less what + private fun randomizeResource(mapToReturn: TileMap, distance: Int, resourceType: ResourceType) { + val resourcesOfType = GameBasics.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 locations = chooseSpreadOutLocations(numberOfResources, suitableTiles, distance) + + val resourceToNumber = Counter() + + for(tile in locations){ + val possibleResources = resourcesOfType + .filter { it.terrainsCanBeFoundOn.contains(tile.getLastTerrain().name) } + .map { it.name } + if(possibleResources.isEmpty()) continue + val resourceWithLeastAssignments = possibleResources.minBy { resourceToNumber[it]!! }!! + resourceToNumber.add(resourceWithLeastAssignments, 1) + tile.resource = resourceWithLeastAssignments + } + } + + fun chooseSpreadOutLocations(numberOfResources: Int, suitableTiles: List, initialDistance:Int): ArrayList { + + for(distanceBetweenResources in initialDistance downTo 1){ + var availableTiles = suitableTiles.toList() + val chosenTiles = ArrayList() + + for(i in 1..numberOfResources){ + if(availableTiles.isEmpty()) break + val chosenTile = availableTiles.random() + availableTiles = availableTiles.filter { it.arialDistanceTo(chosenTile)>distanceBetweenResources } + chosenTiles.add(chosenTile) + } + // Either we got them all, or we're not going to get anything better + if(chosenTiles.size == numberOfResources || distanceBetweenResources==1) return chosenTiles + } + throw Exception("Couldn't choose suitable tiles for $numberOfResources resources!") + } +} + +class MapLandmassGenerator(){ + + 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() + } + + //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) + tileInfo.baseTerrain = ocean + } else { // water tile + 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 + } + } + } + } + + + private fun getInitialTerrainCellularAutomata(tileInfo: TileInfo, mapRadius: Int, mapType: String):TerrainType { + + 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 + } + } + } + +} + + + +class Area(var terrain: String) { + val tiles = ArrayList() + fun addTile(tileInfo: TileInfo) { + tiles+=tileInfo + tileInfo.baseTerrain = terrain + } +} + diff --git a/core/src/com/unciv/logic/map/RandomMapGenerator.kt b/core/src/com/unciv/logic/map/RandomMapGenerator.kt deleted file mode 100644 index a24c2399b3..0000000000 --- a/core/src/com/unciv/logic/map/RandomMapGenerator.kt +++ /dev/null @@ -1,589 +0,0 @@ -package com.unciv.logic.map - -import com.badlogic.gdx.math.Vector2 -import com.unciv.Constants -import com.unciv.logic.HexMath -import com.unciv.models.Counter -import com.unciv.models.gamebasics.GameBasics -import com.unciv.models.gamebasics.tile.ResourceType -import com.unciv.models.gamebasics.tile.TerrainType -import com.unciv.models.gamebasics.tile.TileResource -import java.util.* -import kotlin.collections.HashMap -import kotlin.math.* - -enum class MapType { - Perlin, - Default, - Continents, - Pangaea, - File -} - - -class CelluarAutomataRandomMapGenerator(): SeedRandomMapGenerator() { - var landProb = 0.55f - var numSmooth = 4 - var mapType = MapType.Default - - constructor(type: MapType): this() { - mapType = type - if (mapType != MapType.Default && mapType !=MapType.Pangaea && mapType !=MapType.Continents) { - mapType = MapType.Default - } - } - - override fun generateMap(distance: Int): HashMap { - val mapVectors = HexMath().getVectorsInDistance(Vector2.Zero, distance) - val landscape = HashMap() - - //init - for (vector in mapVectors) { - landscape[vector] = generateInitTerrain(vector, distance) - } - - //smooth - for (loop in 0..numSmooth) { - for (vector in mapVectors) { - if (HexMath().getDistance(Vector2.Zero, vector) < distance) { - val neighborLands = HexMath().getAdjacentVectors(vector).count {landscape[it] == TerrainType.Land} - if (landscape[vector] == TerrainType.Land) { - if (neighborLands < 3) - landscape[vector] = TerrainType.Water - } else { - if (neighborLands > 3) - landscape[vector] = TerrainType.Land - } - } - else { - landscape[vector] = TerrainType.Water - } - } - if (mapType == MapType.Continents) { //keep a ocean column in the middle - for (y in -distance..distance) { - landscape[Vector2((y/2).toFloat(), y.toFloat())] = TerrainType.Water - landscape[Vector2((y/2+1).toFloat(), y.toFloat())] = TerrainType.Water - } - } - } - - val map = HashMap() - for (vector in mapVectors) - map[vector] = generateTile(vector,landscape[vector]!!) - - divideIntoAreas2(6, 0.05f, distance, map) - - val mapToReturn = HashMap() - for(tile in map) { - tile.value.setTransients() - mapToReturn[tile.key.toString()] = tile.value - } - - setWaterTiles(mapToReturn) - - for(tile in mapToReturn.values) randomizeTile(tile,mapToReturn) - - randomizeResources(mapToReturn,distance) - - return mapToReturn - } - - 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)) - } - - private fun generateInitTerrain(vector: Vector2, distance: Int): TerrainType { - val type: TerrainType - if (mapType == MapType.Pangaea) { - val distanceFactor = (HexMath().getDistance(Vector2.Zero, vector) * 1.8 / distance).toFloat() - type = if (Random().nextDouble() < landProb.pow(distanceFactor)) TerrainType.Land else TerrainType.Water - } else if (mapType == MapType.Continents) { - val distanceWeight = min(getDistanceWeightForContinents(Vector2(distance.toFloat()/2, 0f), vector), - getDistanceWeightForContinents(Vector2(-distance.toFloat()/2, 0f), vector)) - val distanceFactor = (distanceWeight * 1.8 / distance).toFloat() - type = if (Random().nextDouble() < landProb.pow(distanceFactor)) TerrainType.Land else TerrainType.Water - } else { //default - if (HexMath().getDistance(Vector2.Zero, vector) > 0.9f * distance) - type = if (Random().nextDouble() < 0.1) TerrainType.Land else TerrainType.Water - else if (HexMath().getDistance(Vector2.Zero, vector) > 0.85f * distance) - type = if (Random().nextDouble() < 0.2) TerrainType.Land else TerrainType.Water - else - type = if (Random().nextDouble() < landProb) TerrainType.Land else TerrainType.Water - } - return type - } - - private fun generateTile(vector: Vector2, type: TerrainType): TileInfo { - val tile=TileInfo() - tile.position=vector - if (type == TerrainType.Land) tile.baseTerrain = "" - else tile.baseTerrain = Constants.ocean - return tile - } - - override fun setWaterTiles(map: HashMap) { - //define lakes - var waterTiles = map.values.filter { it.isWater }.map { it.position } - val tilesInArea = ArrayList() - val tilesToCheck = ArrayList() - while (waterTiles.isNotEmpty()) { - val initialWaterTile = waterTiles.random() - tilesInArea += initialWaterTile - tilesToCheck += initialWaterTile - waterTiles -= initialWaterTile - - while (tilesToCheck.isNotEmpty()) { - val tileChecking = tilesToCheck.random() - for (vector in HexMath().getVectorsAtDistance(tileChecking,1) - .filter { !tilesInArea.contains(it) and waterTiles.contains(it) }) { - tilesInArea += vector - tilesToCheck += vector - waterTiles -= vector - } - tilesToCheck -= tileChecking - } - - if (tilesInArea.size <= 10) { - for (vector in tilesInArea) { - val tile = map[vector.toString()]!! - tile.baseTerrain = Constants.lakes - tile.setTransients() - } - } - tilesInArea.clear() - } - - //Coasts - for (tile in map.values.filter { it.baseTerrain == Constants.ocean }) { - if (HexMath().getVectorsInDistance(tile.position,2).any { hasLandTile(map,it) }) { - tile.baseTerrain = Constants.coast - tile.setTransients() - } - } - } - - override fun randomizeTile(tileInfo: TileInfo, map: HashMap){ - if(tileInfo.getBaseTerrain().type==TerrainType.Land && Math.random()<0.05f){ - tileInfo.baseTerrain = Constants.mountain - tileInfo.setTransients() - } - addRandomTerrainFeature(tileInfo) - addRandomResourceToTile(tileInfo) - maybeAddAncientRuins(tileInfo) - } - - fun getLatitude(vector: Vector2): Float { - return (sin(3.1416/3) * vector.y).toFloat() - } - - fun divideIntoAreas2(averageTilesPerArea: Int, waterPercent: Float, distance: Int, map: HashMap) { - val areas = ArrayList() - - val terrains = GameBasics.Terrains.values.filter { it.type === TerrainType.Land && it.name != Constants.lakes - && it.name != Constants.mountain} - - while(map.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 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() - - //change grassland to desert or tundra based on y - if (abs(getLatitude(tile.position)) < maxLatitude * 0.1) { - if (terrain == Constants.grassland || terrain == 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) { - terrain = Constants.tundra - } - } else { - if (terrain == Constants.tundra) terrain = Constants.plains - else if (terrain == Constants.desert) terrain = Constants.grassland - } - - val area = Area(terrain) - emptyTiles -= tile - area.addTile(tile) - areas += area - } - - expandAreas(areas, map) - expandAreas(areas, map) - } - } -} - -/** - * 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 - */ -class PerlinNoiseRandomMapGenerator:SeedRandomMapGenerator(){ - override fun generateMap(distance: Int): HashMap { - val map = HashMap() - val mapRandomSeed = Random().nextDouble() // without this, all the "random" maps would look the same - for (vector in HexMath().getVectorsInDistance(Vector2.Zero, distance)) - map[vector] = generateTile(vector,mapRandomSeed) - - divideIntoAreas(6, 0f, map) - - val mapToReturn = HashMap() - for(tile in map) { - tile.value.setTransients() - mapToReturn[tile.key.toString()] = tile.value - } - - setWaterTiles(mapToReturn) - - for(tile in mapToReturn.values) randomizeTile(tile,mapToReturn) - - randomizeResources(mapToReturn,distance) - - return mapToReturn - } - - private fun generateTile(vector: Vector2, mapRandomSeed: Double): TileInfo { - val tile=TileInfo() - tile.position=vector - val ratio = 1/10.0 - 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 { - height>0.8 -> tile.baseTerrain = Constants.mountain - height>0 -> tile.baseTerrain = "" // we'll leave this to the area division - else -> tile.baseTerrain = Constants.ocean - } - return tile - } -} - -/** - * This generator uses the algorithm from the game "Alexander", outlined here: - * http://www.cartania.com/alexander/generation.html - */ -class AlexanderRandomMapGenerator:RandomMapGenerator(){ - fun generateMap(distance: Int, landExpansionChance:Float): HashMap { - val map = HashMap() - - for (vector in HexMath().getVectorsInDistance(Vector2.Zero, distance)) - map[vector] = null - - val sparkList = ArrayList() - for(i in 0..distance*distance/6){ - val location = map.filter { it.value==null }.map { it.key }.random() - map[location] = TileInfo().apply { baseTerrain= Constants.grassland} - sparkList.add(location) - } - - while(sparkList.any()){ - val currentSpark = sparkList.random() - val emptyTilesAroundSpark = HexMath().getAdjacentVectors(currentSpark) - .filter { map.containsKey(it) && map[it]==null } - if(map[currentSpark]!!.baseTerrain==Constants.grassland){ - for(tile in emptyTilesAroundSpark){ - if(Math.random()() - for(entry in map){ - entry.value!!.position = entry.key - if(entry.value!!.baseTerrain==Constants.ocean - && HexMath().getAdjacentVectors(entry.key).all { !map.containsKey(it) || map[it]!!.baseTerrain==Constants.grassland }) - entry.value!!.baseTerrain=Constants.grassland - - newmap[entry.key.toString()] = entry.value!! - } - - setWaterTiles(newmap) - - return newmap - // now that we've divided them into land and not-land, stage 2 - seeding areas the way we did with the seed generator! - - } -} - -class Area(var terrain: String) { - val locations = ArrayList() - fun addTile(tileInfo: TileInfo) { - locations+=tileInfo.position - tileInfo.baseTerrain = terrain - } -} - -/** - * This generator works by creating a number of seeds of different terrain types in random places, - * and choosing a random one each time to expand in a random direction, until the map is filled. - * With water, this creates canal-like structures. - */ -open class SeedRandomMapGenerator : RandomMapGenerator() { - - fun generateMap(distance: Int, waterPercent:Float): HashMap { - - val map = HashMap() - - for (vector in HexMath().getVectorsInDistance(Vector2.Zero, distance)) - map[vector] = TileInfo().apply { position=vector; baseTerrain="" } - - - divideIntoAreas(6, waterPercent, map) - - val mapToReturn = HashMap() - - for (entry in map) mapToReturn[entry.key.toString()] = entry.value - for (entry in map) randomizeTile(entry.value, mapToReturn) - - setWaterTiles(mapToReturn) - randomizeResources(mapToReturn,distance) - return mapToReturn - } - - open fun divideIntoAreas(averageTilesPerArea: Int, waterPercent: Float, map: HashMap) { - val areas = ArrayList() - - val terrains = GameBasics.Terrains.values - .filter { it.type === TerrainType.Land && it.name != Constants.lakes && it.name != Constants.mountain } - - while(map.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 numberOfSeeds = ceil(emptyTiles.size / averageTilesPerArea.toFloat()).toInt() - - for (i in 0 until numberOfSeeds) { - val terrain = if (Math.random() > waterPercent) terrains.random().name - else Constants.ocean - val area = Area(terrain) - val tile = emptyTiles.random() - emptyTiles -= tile - area.addTile(tile) - areas += area - } - - expandAreas(areas, map) - expandAreas(areas, map) - } - - - for (area in areas.filter { it.terrain == Constants.ocean && it.locations.size <= 10 }) { - // areas with 10 or less tiles are lakes. - for (location in area.locations) - map[location]!!.baseTerrain = Constants.lakes - } - } - - fun expandAreas(areas: ArrayList, map: HashMap) { - val expandableAreas = ArrayList(areas) - while (expandableAreas.isNotEmpty()) { - val areaToExpand = expandableAreas.random() - if(areaToExpand.locations.size>=20){ - expandableAreas -= areaToExpand - continue - } - val availableExpansionVectors = areaToExpand.locations - .flatMap { HexMath().getAdjacentVectors(it) }.asSequence().distinct() - .filter { map.containsKey(it) && map[it]!!.baseTerrain=="" }.toList() - if (availableExpansionVectors.isEmpty()) expandableAreas -= areaToExpand - else { - val expansionVector = availableExpansionVectors.random() - areaToExpand.addTile(map[expansionVector]!!) - - val neighbors = HexMath().getAdjacentVectors(expansionVector) - val areasToJoin = areas.filter { - it.terrain == areaToExpand.terrain - && it != areaToExpand - && it.locations.any { location -> location in neighbors } - } - for (area in areasToJoin) { - areaToExpand.locations += area.locations - areas.remove(area) - expandableAreas.remove(area) - } - } - } - } - -} - - -/** - * This contains the basic randomizing tasks (add random terrain feature/resource) - * and a basic map generator where every single tile is individually randomized. - * Doesn't look very good TBH. - */ -open class RandomMapGenerator { - - private fun addRandomTile(position: Vector2): TileInfo { - val tileInfo = TileInfo() - tileInfo.position = position - val terrains = GameBasics.Terrains.values - - val baseTerrain = terrains.filter { it.type === TerrainType.Land }.random() - tileInfo.baseTerrain = baseTerrain.name - - addRandomTerrainFeature(tileInfo) - addRandomResourceToTile(tileInfo) - - return tileInfo - } - - fun addRandomTerrainFeature(tileInfo: TileInfo) { - if (tileInfo.getBaseTerrain().canHaveOverlay && Math.random() > 0.7f) { - val secondaryTerrains = GameBasics.Terrains.values - .filter { it.type === TerrainType.TerrainFeature && it.occursOn!!.contains(tileInfo.baseTerrain) } - if (secondaryTerrains.any()) tileInfo.terrainFeature = secondaryTerrains.random().name - } - } - - - internal fun addRandomResourceToTile(tileInfo: TileInfo) { - - var tileResources = GameBasics.TileResources.values.toList() - - // Resources are placed according to TerrainFeature, if exists, otherwise according to BaseLayer. - tileResources = tileResources.filter { it.terrainsCanBeFoundOn.contains(tileInfo.getLastTerrain().name) } - - var resource: TileResource? = null - when { - Math.random() < 1 / 15f -> resource = getRandomResource(tileResources, ResourceType.Bonus) - Math.random() < 1 / 15f -> resource = getRandomResource(tileResources, ResourceType.Strategic) - Math.random() < 1 / 15f -> resource = getRandomResource(tileResources, ResourceType.Luxury) - } - if (resource != null) tileInfo.resource = resource.name - } - - private fun getRandomResource(resources: List, resourceType: ResourceType): TileResource? { - val filtered = resources.filter { it.resourceType == resourceType } - if (filtered.isEmpty()) return null - else return filtered.random() - } - - open fun generateMap(distance: Int): HashMap { - val map = HashMap() - for (vector in HexMath().getVectorsInDistance(Vector2.Zero, distance)) - map[vector.toString()] = addRandomTile(vector) - return map - } - - fun maybeAddAncientRuins(tile: TileInfo) { - val baseTerrain = tile.getBaseTerrain() - if(baseTerrain.type!=TerrainType.Water && !baseTerrain.impassable && Random().nextDouble() < 1f/100) - tile.improvement = Constants.ancientRuins - } - - - fun hasLandTile(map: HashMap, vector: Vector2): Boolean { - return map.containsKey(vector.toString()) && map[vector.toString()]!!.getBaseTerrain().type == TerrainType.Land - } - - open fun setWaterTiles(map: HashMap) { - for (tile in map.values.filter { it.baseTerrain == Constants.ocean }) { - if (HexMath().getVectorsInDistance(tile.position,2).any { hasLandTile(map,it) }) { - tile.baseTerrain = Constants.coast - tile.setTransients() - } - } - } - - open fun randomizeTile(tileInfo: TileInfo, map: HashMap){ - if(tileInfo.getBaseTerrain().type==TerrainType.Land && Math.random()<0.05f){ - tileInfo.baseTerrain = Constants.mountain - tileInfo.setTransients() - } - if(tileInfo.getBaseTerrain().type==TerrainType.Land && Math.random()<0.05f - && HexMath().getVectorsInDistance(tileInfo.position,1).all { hasLandTile(map,it) }){ - tileInfo.baseTerrain = Constants.lakes - tileInfo.setTransients() - } - addRandomTerrainFeature(tileInfo) - addRandomResourceToTile(tileInfo) - maybeAddAncientRuins(tileInfo) - } - - - fun randomizeResources(mapToReturn: HashMap, distance: Int) { - for(tile in mapToReturn.values) - if(tile.resource!=null) - tile.resource=null - - randomizeStrategicResources(mapToReturn, distance, ResourceType.Strategic) - randomizeResource(mapToReturn, distance, ResourceType.Luxury) - randomizeResource(mapToReturn, distance, ResourceType.Bonus) - } - - // Here, we need each specific resource to be spread over the map - it matters less if specific resources are near each other - private fun randomizeStrategicResources(mapToReturn: HashMap, distance: Int, resourceType: ResourceType) { - val resourcesOfType = GameBasics.TileResources.values.filter { it.resourceType == resourceType } - for (resource in resourcesOfType) { - val suitableTiles = mapToReturn.values - .filter { it.resource == null && resource.terrainsCanBeFoundOn.contains(it.getLastTerrain().name) } - - val averageTilesPerResource = 15 * resourcesOfType.count() - val numberOfResources = mapToReturn.values.count { it.isLand && !it.getBaseTerrain().impassable } / averageTilesPerResource - - val locations = chooseSpreadOutLocations(numberOfResources, suitableTiles, distance) - - for (location in locations) location.resource = resource.name - } - } - - // Here, we need there to be some luxury/bonus resource - it matters less what - private fun randomizeResource(mapToReturn: HashMap, distance: Int, resourceType: ResourceType) { - val resourcesOfType = GameBasics.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 locations = chooseSpreadOutLocations(numberOfResources, suitableTiles, distance) - - val resourceToNumber = Counter() - - for(tile in locations){ - val possibleResources = resourcesOfType - .filter { it.terrainsCanBeFoundOn.contains(tile.getLastTerrain().name) } - .map { it.name } - if(possibleResources.isEmpty()) continue - val resourceWithLeastAssignments = possibleResources.minBy { resourceToNumber[it]!! }!! - resourceToNumber.add(resourceWithLeastAssignments, 1) - tile.resource = resourceWithLeastAssignments - } - } - - fun chooseSpreadOutLocations(numberOfResources: Int, suitableTiles: List, initialDistance:Int): ArrayList { - - for(distanceBetweenResources in initialDistance downTo 1){ - var availableTiles = suitableTiles.toList() - val chosenTiles = ArrayList() - - for(i in 1..numberOfResources){ - if(availableTiles.isEmpty()) break - val chosenTile = availableTiles.random() - availableTiles = availableTiles.filter { it.arialDistanceTo(chosenTile)>distanceBetweenResources } - chosenTiles.add(chosenTile) - } - if(chosenTiles.size == numberOfResources) return chosenTiles - } - throw Exception("ArgleBargle") - } - -} \ 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 28870c3734..6eab928a52 100644 --- a/core/src/com/unciv/logic/map/TileMap.kt +++ b/core/src/com/unciv/logic/map/TileMap.kt @@ -1,12 +1,11 @@ package com.unciv.logic.map import com.badlogic.gdx.math.Vector2 +import com.unciv.Constants import com.unciv.logic.GameInfo import com.unciv.logic.HexMath -import com.unciv.logic.MapSaver import com.unciv.logic.civilization.CivilizationInfo import com.unciv.models.gamebasics.GameBasics -import com.unciv.models.metadata.GameParameters class TileMap { @@ -32,18 +31,10 @@ class TileMap { get() = tileList - constructor(newGameParameters: GameParameters) { - val mapValues:Collection - - if(newGameParameters.mapType == MapType.File) - mapValues = MapSaver().loadMap(newGameParameters.mapFileName!!).values - else if(newGameParameters.mapType==MapType.Perlin) - mapValues = PerlinNoiseRandomMapGenerator().generateMap(newGameParameters.mapRadius).values - else - mapValues = CelluarAutomataRandomMapGenerator(newGameParameters.mapType).generateMap(newGameParameters.mapRadius).values - - tileList.addAll(mapValues) + constructor(radius:Int){ + for(vector in HexMath().getVectorsInDistance(Vector2.Zero, radius)) + tileList.add(TileInfo().apply { position = vector; baseTerrain= Constants.grassland }) setTransients() } diff --git a/core/src/com/unciv/models/metadata/GameParameters.kt b/core/src/com/unciv/models/metadata/GameParameters.kt index a5c8c682d6..c0960ea701 100644 --- a/core/src/com/unciv/models/metadata/GameParameters.kt +++ b/core/src/com/unciv/models/metadata/GameParameters.kt @@ -14,7 +14,7 @@ class GameParameters { // Default values are the default new game for (i in 1..3) add(Player()) } var numberOfCityStates = 0 - var mapType = MapType.Perlin + var mapType = MapType.pangaea var noBarbarians = false var mapFileName: String? = null var victoryTypes: ArrayList = VictoryType.values().toCollection(ArrayList()) // By default, all victory types diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt b/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt index c7f40fdc3e..8a9a71e152 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt @@ -6,7 +6,6 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextButton import com.unciv.logic.MapSaver import com.unciv.logic.map.TileMap import com.unciv.models.gamebasics.tr -import com.unciv.models.metadata.GameParameters import com.unciv.ui.tilegroups.TileGroup import com.unciv.ui.tilegroups.TileSetStrings import com.unciv.ui.utils.CameraStageBaseScreen @@ -15,7 +14,7 @@ import com.unciv.ui.utils.setFontSize import com.unciv.ui.worldscreen.TileGroupMap class MapEditorScreen(): CameraStageBaseScreen(){ - var tileMap = TileMap(GameParameters()) + var tileMap = TileMap() var mapName = "My first map" lateinit var mapHolder: TileGroupMap private val tileEditorOptions = TileEditorOptionsTable(this) diff --git a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt index 62ddeb0d78..bbda249083 100644 --- a/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt +++ b/core/src/com/unciv/ui/newgamescreen/NewGameScreen.kt @@ -85,6 +85,7 @@ class NewGameScreen: PickerScreen(){ cantMakeThatMapPopup.addGoodSizedLabel("Maybe you put too many players into too small a map?".tr()).row() cantMakeThatMapPopup.addCloseButton() cantMakeThatMapPopup.open() + Gdx.input.inputProcessor = stage } } } diff --git a/core/src/com/unciv/ui/newgamescreen/NewGameScreenOptionsTable.kt b/core/src/com/unciv/ui/newgamescreen/NewGameScreenOptionsTable.kt index 06593466bb..e4f6e6bdbf 100644 --- a/core/src/com/unciv/ui/newgamescreen/NewGameScreenOptionsTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/NewGameScreenOptionsTable.kt @@ -60,26 +60,23 @@ class NewGameScreenOptionsTable(val newGameParameters: GameParameters, val onMul private fun addMapTypeSizeAndFile() { add("{Map type}:".tr()) - val mapTypes = LinkedHashMap() - for (type in MapType.values()) { - if (type == MapType.File && MapSaver().getMaps().isEmpty()) continue - mapTypes[type.toString()] = type - } + val mapTypes = arrayListOf(MapType.default,MapType.continents,MapType.perlin,MapType.pangaea) + if(MapSaver().getMaps().isNotEmpty()) mapTypes.add(MapType.file) val mapFileLabel = "{Map file}:".toLabel() val mapFileSelectBox = getMapFileSelectBox() mapFileLabel.isVisible = false mapFileSelectBox.isVisible = false - val mapTypeSelectBox = TranslatedSelectBox(mapTypes.keys, newGameParameters.mapType.toString(), CameraStageBaseScreen.skin) + val mapTypeSelectBox = TranslatedSelectBox(mapTypes, newGameParameters.mapType, CameraStageBaseScreen.skin) val worldSizeSelectBox = getWorldSizeSelectBox() val worldSizeLabel = "{World size}:".toLabel() mapTypeSelectBox.addListener(object : ChangeListener() { override fun changed(event: ChangeEvent?, actor: Actor?) { - newGameParameters.mapType = mapTypes[mapTypeSelectBox.selected.value]!! - if (newGameParameters.mapType == MapType.File) { + newGameParameters.mapType = mapTypeSelectBox.selected.value + if (newGameParameters.mapType == MapType.file) { worldSizeSelectBox.isVisible = false worldSizeLabel.isVisible = false mapFileSelectBox.isVisible = true