mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-16 02:40:41 +07:00
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
This commit is contained in:
@ -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)
|
||||
|
@ -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<Vector2> {
|
||||
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)
|
||||
|
@ -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<TileInfo>()
|
||||
val tilesToCheck = ArrayList<TileInfo>()
|
||||
|
||||
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<Area>()
|
||||
|
||||
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<Area>) {
|
||||
private fun expandAreas(areas: ArrayList<Area>) {
|
||||
val expandableAreas = ArrayList<Area>(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<String>()
|
||||
@ -511,7 +524,7 @@ class MapGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
fun chooseSpreadOutLocations(numberOfResources: Int, suitableTiles: List<TileInfo>, initialDistance: Int): ArrayList<TileInfo> {
|
||||
private fun chooseSpreadOutLocations(numberOfResources: Int, suitableTiles: List<TileInfo>, initialDistance: Int): ArrayList<TileInfo> {
|
||||
|
||||
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<TileInfo>()
|
||||
fun addTile(tileInfo: TileInfo) {
|
||||
tiles+=tileInfo
|
||||
tiles += tileInfo
|
||||
tileInfo.baseTerrain = terrain
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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]
|
||||
}
|
||||
}
|
@ -10,14 +10,14 @@ import com.unciv.models.ruleset.Ruleset
|
||||
class TileMap {
|
||||
|
||||
@Transient lateinit var gameInfo: GameInfo
|
||||
@Transient var tileMatrix=ArrayList<ArrayList<TileInfo?>>() // 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<ArrayList<TileInfo?>>() // 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<String, TileInfo>()
|
||||
|
||||
var mapParameters= MapParameters()
|
||||
var mapParameters = MapParameters()
|
||||
private var tileList = ArrayList<TileInfo>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
@ -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!!))
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
Reference in New Issue
Block a user