Flat Earth Hexagonal (#8140)

* Add new map shape: Flat Earth Hexagonal.

* Make flat earth logic easier to read.

* Make flat earth set temperature based on tile distance from center.

* Make flat earth waste less space on the ice ring.

* Add variety to the flat earth ice ring.

* Use baseTerrain for snow on flat earth edge instead of addTerrainFeature.

* Ensure flat earth center tiles and edge tiles are ocean not coast.

* Give flat earth ice ring some random juts and dips.

* Make 3rd continent smaller when flat earth.

* Enable more continent positions for three continents when flat earth.

* Add parens around or statement.

* Refactor flat earth temperature code into functions.

* Invert some flat earth if statements to reduce nesting.

* Allow coast near flat earth ice walls.

* Make flat earth ice wall generation more efficient.

* Stop adding ice to flat earth mountains.

* Move flat earth ice wall generation to its own function.

* Improve flat earth water placement comments.

* Move flat earth extra water generation to its own function.

* Move flat earth center ice spawn to its own function.

* Move flat earth edge ice spawn to its own function.

* Minor efficiency tweak.

* Add Flat Earth Hexagonal to template.
This commit is contained in:
Philip Keiter
2022-12-16 03:08:04 -06:00
committed by GitHub
parent 20062c1c3a
commit 02f9176f78
7 changed files with 280 additions and 14 deletions

View File

@ -377,6 +377,7 @@ Time =
Map Shape =
Hexagonal =
Flat Earth Hexagonal =
Rectangular =
Height =
Width =

View File

@ -124,6 +124,7 @@ class MapSizeNew : IsPartOfGameInfoSerialization {
object MapShape : IsPartOfGameInfoSerialization {
const val hexagonal = "Hexagonal"
const val flatEarth = "Flat Earth Hexagonal"
const val rectangular = "Rectangular"
}
@ -229,12 +230,12 @@ class MapParameters : IsPartOfGameInfoSerialization {
}
fun getArea() = when {
shape == MapShape.hexagonal -> getNumberOfTilesInHexagon(mapSize.radius)
shape == MapShape.hexagonal || shape == MapShape.flatEarth -> getNumberOfTilesInHexagon(mapSize.radius)
worldWrap && mapSize.width % 2 != 0 -> (mapSize.width - 1) * mapSize.height
else -> mapSize.width * mapSize.height
}
fun displayMapDimensions() = mapSize.run {
(if (shape == MapShape.hexagonal) "R$radius" else "${width}x$height") +
(if (shape == MapShape.hexagonal || shape == MapShape.flatEarth) "R$radius" else "${width}x$height") +
(if (worldWrap) "w" else "")
}
@ -266,7 +267,7 @@ class MapParameters : IsPartOfGameInfoSerialization {
}.joinToString("")
fun numberOfTiles() =
if (shape == MapShape.hexagonal) {
if (shape == MapShape.hexagonal || shape == MapShape.flatEarth) {
1 + 3 * mapSize.radius * (mapSize.radius - 1)
} else {
mapSize.width * mapSize.height

View File

@ -26,6 +26,7 @@ import kotlin.math.max
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.sign
import kotlin.math.sqrt
import kotlin.math.ulp
import kotlin.random.Random
@ -35,6 +36,14 @@ class MapGenerator(val ruleset: Ruleset) {
private const val consoleTimings = false
}
private val landTerrainName =
MapLandmassGenerator.getInitializationTerrain(ruleset, TerrainType.Land)
private val waterTerrainName: String = try {
MapLandmassGenerator.getInitializationTerrain(ruleset, TerrainType.Water)
} catch (_: Exception) {
landTerrainName
}
private var randomness = MapGenerationRandomness()
private val firstLandTerrain = ruleset.terrains.values.first { it.type==TerrainType.Land }
@ -503,9 +512,19 @@ class MapGenerator(val ruleset: Ruleset) {
val humidityRandom = randomness.getPerlinNoise(tile, humiditySeed, scale = scale, nOctaves = 1)
val humidity = ((humidityRandom + 1.0) / 2.0 + humidityShift).coerceIn(0.0..1.0)
val randomTemperature = randomness.getPerlinNoise(tile, temperatureSeed, scale = scale, nOctaves = 1)
val expectedTemperature = if (tileMap.mapParameters.shape === MapShape.flatEarth) {
// Flat Earth uses radius because North is center of map
val radius = getTileRadius(tile, tileMap)
val radiusTemperature = getTemperatureAtRadius(radius)
radiusTemperature
} else {
// Globe Earth uses latitude because North is top of map
val latitudeTemperature = 1.0 - 2.0 * abs(tile.latitude) / tileMap.maxLatitude
var temperature = (5.0 * latitudeTemperature + randomTemperature) / 6.0
latitudeTemperature
}
val randomTemperature = randomness.getPerlinNoise(tile, temperatureSeed, scale = scale, nOctaves = 1)
var temperature = (5.0 * expectedTemperature + randomTemperature) / 6.0
temperature = abs(temperature).pow(1.0 - temperatureExtremeness) * temperature.sign
temperature = (temperature + temperatureShift).coerceIn(-1.0..1.0)
@ -538,6 +557,65 @@ class MapGenerator(val ruleset: Ruleset) {
}
}
private fun getTileRadius(tile: TileInfo, tileMap: TileMap): Float {
val latitudeRatio = abs(tile.latitude) / tileMap.maxLatitude
val longitudeRatio = abs(tile.longitude) / tileMap.maxLongitude
return sqrt(latitudeRatio.pow(2) + longitudeRatio.pow(2))
}
private fun getTemperatureAtRadius(radius: Float): Double {
/*
Radius is in the range of 0.0 to 1.0
Temperature is in the range of -1.0 to 1.0
Radius of 0.0 (arctic) is -1.0 (cold)
Radius of 0.25 (mid North) is 0.0 (temperate)
Radius of 0.5 (equator) is 1.0 (hot)
Radius of 0.75 (mid South) is 0.0 (temperate)
Radius of 1.0 (antarctic) is -1.0 (cold)
Scale the radius range to the temperature range
*/
return when {
/*
North Zone
Starts cold at arctic and gets hotter as it goes South to equator
x1 is set to 0.05 instead of 0.0 to offset the ice in the center of the map
*/
radius < 0.5 -> scaleToRange(0.05, 0.5, -1.0, 1.0, radius)
/*
South Zone
Starts hot at equator and gets colder as it goes South to antarctic
x2 is set to 0.95 instead of 1.0 to offset the ice on the edges of the map
*/
radius > 0.5 -> scaleToRange(0.5, 0.95, 1.0, -1.0, radius)
/*
Equator
Always hot
radius == 0.5
*/
else -> 1.0
}
}
/**
* @x1 start of the original range
* @x2 end of the original range
* @y1 start of the new range
* @y2 end of the new range
* @value value to be scaled from the original range to the new range
*
* @returns value in new scale
* special thanks to @letstalkaboutdune for the math
*/
private fun scaleToRange(x1: Double, x2: Double, y1: Double, y2: Double, value: Float): Double {
val gain = (y2 - y1) / (x2 - x1)
val offset = y2 - (gain * x2)
return (gain * value) + offset
}
/**
* [MapParameters.vegetationRichness] is the threshold for vegetation spawn
*/
@ -596,6 +674,11 @@ class MapGenerator(val ruleset: Ruleset) {
-1f, ruleset.modOptions.constants.spawnIceBelowTemperature,
0f, 1f))
}.toList()
if (tileMap.mapParameters.shape === MapShape.flatEarth) {
spawnFlatEarthIceWalls(tileMap, iceEquivalents)
}
if (iceEquivalents.isEmpty()) return
tileMap.setTransients(ruleset)
@ -621,6 +704,145 @@ class MapGenerator(val ruleset: Ruleset) {
}
}
}
private fun spawnFlatEarthIceWalls(tileMap: TileMap, iceEquivalents: List<TerrainOccursRange>) {
val iceCandidates = iceEquivalents.filter {
it.matches(-1.0, 1.0)
}.map {
it.terrain.name
}
val iceTerrainName =
when (iceCandidates.size) {
1 -> iceCandidates.first()
!in 0..1 -> iceCandidates.random(randomness.RNG)
else -> null
}
val snowCandidates = ruleset.terrains.values.asSequence().filter {
it.type == TerrainType.Land
}.flatMap { terrain ->
val conditions = terrain.getGenerationConditions()
if (conditions.any()) conditions
else sequenceOf(
TerrainOccursRange(
terrain,
-1f, ruleset.modOptions.constants.spawnIceBelowTemperature,
0f, 1f
)
)
}.toList().filter {
it.matches(-1.0, 1.0)
}.map {
it.terrain.name
}
val snowTerrainName =
when (snowCandidates.size) {
1 -> snowCandidates.first()
!in 0..1 -> snowCandidates.random(randomness.RNG)
else -> null
}
val mountainTerrainName =
ruleset.terrains.values.firstOrNull { it.hasUnique(UniqueType.OccursInChains) }?.name
val bestArcticTileName = when {
iceTerrainName != null -> iceTerrainName
snowTerrainName != null -> snowTerrainName
mountainTerrainName != null -> mountainTerrainName
else -> null
}
val arcticTileNameList =
arrayOf(iceTerrainName, snowTerrainName, mountainTerrainName).filterNotNull()
// Skip the tile loop if nothing can be done in it
if (bestArcticTileName == null && arcticTileNameList.isEmpty()) return
// Flat Earth needs a 1 tile wide perimeter of ice/mountain/snow and a 2 radius cluster of ice in the center.
for (tile in tileMap.values) {
val isCenterTile = tile.latitude == 0f && tile.longitude == 0f
val isEdgeTile = tile.neighbors.count() < 6
// Make center tiles ice or snow or mountain depending on availability
if (isCenterTile && bestArcticTileName != null) {
spawnFlatEarthCenterIceWall(tile, bestArcticTileName, iceTerrainName)
}
// Make edge tiles randomly ice or snow or mountain if available
if (isEdgeTile && arcticTileNameList.isNotEmpty()) {
spawnFlatEarthEdgeIceWall(tile, arcticTileNameList, iceTerrainName, mountainTerrainName)
}
}
}
private fun spawnFlatEarthCenterIceWall(tile: TileInfo, bestArcticTileName: String, iceTerrainName: String?) {
// Spawn ice on center tile
if (bestArcticTileName == iceTerrainName) {
tile.baseTerrain = waterTerrainName
tile.addTerrainFeature(iceTerrainName)
} else {
tile.baseTerrain = bestArcticTileName
}
// Spawn circle of ice around center tile
for (neighbor in tile.neighbors) {
if (bestArcticTileName == iceTerrainName) {
neighbor.baseTerrain = waterTerrainName
neighbor.addTerrainFeature(iceTerrainName)
} else {
neighbor.baseTerrain = bestArcticTileName
}
// Spawn partial circle of ice around circle of ice
for (neighbor2 in neighbor.neighbors) {
if (randomness.RNG.nextDouble() < 0.75) {
// Do nothing most of the time at random.
} else if (bestArcticTileName == iceTerrainName) {
neighbor2.baseTerrain = waterTerrainName
neighbor2.addTerrainFeature(iceTerrainName)
} else {
neighbor2.baseTerrain = bestArcticTileName
}
}
}
}
private fun spawnFlatEarthEdgeIceWall(tile: TileInfo, arcticTileNameList: List<String>, iceTerrainName: String?, mountainTerrainName: String?) {
// Select one of the arctic tiles at random
val arcticTileName = when (arcticTileNameList.size) {
1 -> arcticTileNameList.first()
else -> arcticTileNameList.random(randomness.RNG)
}
// Spawn arctic tiles on edge tile
if (arcticTileName == iceTerrainName) {
tile.baseTerrain = waterTerrainName
tile.addTerrainFeature(iceTerrainName)
} else if (iceTerrainName != null && arcticTileName != mountainTerrainName) {
tile.baseTerrain = arcticTileName
tile.addTerrainFeature(iceTerrainName)
} else {
tile.baseTerrain = arcticTileName
}
// Spawn partial circle of arctic tiles next to the edge
for (neighbor in tile.neighbors) {
val neighborIsEdgeTile = neighbor.neighbors.count() < 6
if (neighborIsEdgeTile) {
// Do not redo edge tile. It is already done.
} else if (randomness.RNG.nextDouble() < 0.75) {
// Do nothing most of the time at random.
} else if (arcticTileName == iceTerrainName) {
neighbor.baseTerrain = waterTerrainName
neighbor.addTerrainFeature(iceTerrainName)
} else if (iceTerrainName != null && arcticTileName != mountainTerrainName) {
neighbor.baseTerrain = arcticTileName
neighbor.addTerrainFeature(iceTerrainName)
} else {
neighbor.baseTerrain = arcticTileName
}
}
}
}
class MapGenerationRandomness {

View File

@ -48,6 +48,36 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
MapType.archipelago -> createArchipelago(tileMap)
MapType.default -> createPerlin(tileMap)
}
if (tileMap.mapParameters.shape === MapShape.flatEarth) {
generateFlatEarthExtraWater(tileMap)
}
}
private fun generateFlatEarthExtraWater(tileMap: TileMap) {
for (tile in tileMap.values) {
val isCenterTile = tile.latitude == 0f && tile.longitude == 0f
val isEdgeTile = tile.neighbors.count() < 6
if (!isCenterTile && !isEdgeTile) continue
/*
Flat Earth needs a 3 tile wide water perimeter and a 4 tile radius water center.
This helps map generators to not place important things there which would be destroyed
when the ice walls are placed there.
*/
tile.baseTerrain = waterTerrainName
for (neighbor in tile.neighbors) {
neighbor.baseTerrain = waterTerrainName
for (neighbor2 in neighbor.neighbors) {
neighbor2.baseTerrain = waterTerrainName
if (!isCenterTile) continue
for (neighbor3 in neighbor2.neighbors) {
neighbor3.baseTerrain = waterTerrainName
}
}
}
}
}
private fun spawnLandOrWater(tile: TileInfo, elevation: Double) {
@ -109,7 +139,7 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
private fun createTwoContinents(tileMap: TileMap) {
val isLatitude =
if (tileMap.mapParameters.shape === MapShape.hexagonal) randomness.RNG.nextDouble() > 0.5f
if (tileMap.mapParameters.shape === MapShape.hexagonal || tileMap.mapParameters.shape === MapShape.flatEarth) randomness.RNG.nextDouble() > 0.5f
else if (tileMap.mapParameters.mapSize.height > tileMap.mapParameters.mapSize.width) true
else if (tileMap.mapParameters.mapSize.width > tileMap.mapParameters.mapSize.height) false
else randomness.RNG.nextDouble() > 0.5f
@ -123,12 +153,14 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
}
private fun createThreeContinents(tileMap: TileMap) {
val isNorth = randomness.RNG.nextDouble() < 0.5f
val isNorth = randomness.RNG.nextDouble() < 0.5
// On flat earth maps we can randomly do East or West instead of North or South
val isEastWest = tileMap.mapParameters.shape === MapShape.flatEarth && randomness.RNG.nextDouble() > 0.5
val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed)
elevation = (elevation + getThreeContinentsTransform(tile, tileMap, isNorth)) / 2.0
elevation = (elevation + getThreeContinentsTransform(tile, tileMap, isNorth, isEastWest)) / 2.0
spawnLandOrWater(tile, elevation)
}
}
@ -180,16 +212,25 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
return min(0.2, -1.0 + (5.0 * factor.pow(0.6f) + randomScale) / 3.0)
}
private fun getThreeContinentsTransform(tileInfo: TileInfo, tileMap: TileMap, isNorth: Boolean): Double {
private fun getThreeContinentsTransform(tileInfo: TileInfo, tileMap: TileMap, isNorth: Boolean, isEastWest: Boolean): Double {
// The idea here is to create a water area separating the two four areas.
// So what we do it create a line of water in the middle - where longitude is close to 0.
val randomScale = randomness.RNG.nextDouble()
var longitudeFactor = abs(tileInfo.longitude) / tileMap.maxLongitude
var latitudeFactor = abs(tileInfo.latitude) / tileMap.maxLatitude
// 3rd continent should use only half the map width, or if flat earth, only a third
val sizeReductionFactor = if (tileMap.mapParameters.shape === MapShape.flatEarth) 3f else 2f
// We then pick one side to be merged into one centered continent instead of two cornered.
if (isEastWest) {
// In EastWest mode North represents West
if (isNorth && tileInfo.longitude < 0 || !isNorth && tileInfo.longitude > 0)
latitudeFactor = max(0f, tileMap.maxLatitude - abs(tileInfo.latitude * sizeReductionFactor)) / tileMap.maxLatitude
} else {
if (isNorth && tileInfo.latitude < 0 || !isNorth && tileInfo.latitude > 0)
longitudeFactor = max(0f, tileMap.maxLongitude - abs(tileInfo.longitude * 2f)) / tileMap.maxLongitude
longitudeFactor = max(0f, tileMap.maxLongitude - abs(tileInfo.longitude * sizeReductionFactor)) / tileMap.maxLongitude
}
// If this is a world wrap, we want it to be separated on both sides -
// so we make the actual strip of water thinner, but we put it both in the middle of the map and on the edges of the map

View File

@ -76,7 +76,7 @@ class MapRegions (val ruleset: Ruleset){
val totalLand = tileMap.continentSizes.values.sum().toFloat()
val largestContinent = tileMap.continentSizes.values.maxOf { it }.toFloat()
val radius = if (tileMap.mapParameters.shape == MapShape.hexagonal)
val radius = if (tileMap.mapParameters.shape == MapShape.hexagonal || tileMap.mapParameters.shape == MapShape.flatEarth)
tileMap.mapParameters.mapSize.radius.toFloat()
else
(max(tileMap.mapParameters.mapSize.width / 2, tileMap.mapParameters.mapSize.height / 2)).toFloat()

View File

@ -230,7 +230,7 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen(), RecreateOnResize {
ToastPopup(message, this@MapEditorScreen, 4000L )
}
if (params.shape == MapShape.hexagonal) {
if (params.shape == MapShape.hexagonal || params.shape == MapShape.flatEarth) {
params.mapSize = MapSizeNew(HexMath.getHexagonalRadiusForArea(areaFromTiles).toInt())
return
}

View File

@ -76,6 +76,7 @@ class MapParametersTable(
private fun addMapShapeSelectBox() {
val mapShapes = listOfNotNull(
MapShape.hexagonal,
MapShape.flatEarth,
MapShape.rectangular
)
val mapShapeSelectBox =
@ -176,7 +177,7 @@ class MapParametersTable(
private fun updateWorldSizeTable() {
customWorldSizeTable.clear()
if (mapParameters.shape == MapShape.hexagonal && worldSizeSelectBox.selected.value == MapSize.custom)
if ((mapParameters.shape == MapShape.hexagonal || mapParameters.shape == MapShape.flatEarth) && worldSizeSelectBox.selected.value == MapSize.custom)
customWorldSizeTable.add(hexagonalSizeTable).grow().row()
else if (mapParameters.shape == MapShape.rectangular && worldSizeSelectBox.selected.value == MapSize.custom)
customWorldSizeTable.add(rectangularSizeTable).grow().row()