diff --git a/core/src/com/unciv/Constants.kt b/core/src/com/unciv/Constants.kt index c2e77c2486..f9053ce038 100644 --- a/core/src/com/unciv/Constants.kt +++ b/core/src/com/unciv/Constants.kt @@ -24,6 +24,7 @@ object Constants { const val oasis = "Oasis" const val atoll = "Atoll" const val ice = "Ice" + const val floodPlains = "Flood plains" val vegetation = arrayOf(forest, jungle) val sea = arrayOf(ocean, coast) diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt index 02b924f22b..cdd67db1d7 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt @@ -44,6 +44,7 @@ class MapGenerator(val ruleset: Ruleset) { spawnVegetation(map) spawnRareFeatures(map) spawnIce(map) + RiverGenerator(randomness).spawnRivers(map) spreadResources(map) spreadAncientRuins(map) NaturalWonderGenerator(ruleset).spawnNaturalWonders(map, randomness) @@ -104,7 +105,7 @@ class MapGenerator(val ruleset: Ruleset) { if(map.mapParameters.noRuins) return val suitableTiles = map.values.filter { it.isLand && !it.getBaseTerrain().impassable } - val locations = chooseSpreadOutLocations(suitableTiles.size/100, + val locations = randomness.chooseSpreadOutLocations(suitableTiles.size/100, suitableTiles, 10) for(tile in locations) tile.improvement = Constants.ancientRuins @@ -135,7 +136,7 @@ class MapGenerator(val ruleset: Ruleset) { && resource.terrainsCanBeFoundOn.contains(it.getBaseTerrain().name) && (it.terrainFeature==null || ruleset.tileImprovements.containsKey("Remove "+it.terrainFeature)) } - val locations = chooseSpreadOutLocations(resourcesPerType, suitableTiles, distance) + val locations = randomness.chooseSpreadOutLocations(resourcesPerType, suitableTiles, distance) for (location in locations) location.resource = resource.name } @@ -152,7 +153,7 @@ class MapGenerator(val ruleset: Ruleset) { .filter { it.resource == null && resourcesOfType.any { r -> r.terrainsCanBeFoundOn.contains(it.getLastTerrain().name) } } val numberOfResources = tileMap.values.count { it.isLand && !it.getBaseTerrain().impassable } * tileMap.mapParameters.resourceRichness - val locations = chooseSpreadOutLocations(numberOfResources.toInt(), suitableTiles, distance) + val locations = randomness.chooseSpreadOutLocations(numberOfResources.toInt(), suitableTiles, distance) val resourceToNumber = Counter() @@ -167,37 +168,6 @@ class MapGenerator(val ruleset: Ruleset) { } } - private fun chooseSpreadOutLocations(numberOfResources: Int, suitableTiles: List, initialDistance: Int): ArrayList { - - for (distanceBetweenResources in initialDistance downTo 1) { - var availableTiles = suitableTiles.toList() - val chosenTiles = ArrayList() - - // If possible, we want to equalize the base terrains upon which - // the resources are found, so we save how many have been - // found for each base terrain and try to get one from the lowerst - val baseTerrainsToChosenTiles = HashMap() - for(tileInfo in availableTiles){ - if(tileInfo.baseTerrain !in baseTerrainsToChosenTiles) - baseTerrainsToChosenTiles[tileInfo.baseTerrain] = 0 - } - - for (i in 1..numberOfResources) { - if (availableTiles.isEmpty()) break - val orderedKeys = baseTerrainsToChosenTiles.entries - .sortedBy { it.value }.map { it.key } - val firstKeyWithTilesLeft = orderedKeys - .first { availableTiles.any { tile -> tile.baseTerrain== it} } - val chosenTile = availableTiles.filter { it.baseTerrain==firstKeyWithTilesLeft }.random() - availableTiles = availableTiles.filter { it.aerialDistanceTo(chosenTile) > distanceBetweenResources } - chosenTiles.add(chosenTile) - baseTerrainsToChosenTiles[firstKeyWithTilesLeft] = baseTerrainsToChosenTiles[firstKeyWithTilesLeft]!!+1 - } - // 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!") - } /** * [MapParameters.elevationExponent] favors high elevation @@ -280,6 +250,7 @@ class MapGenerator(val ruleset: Ruleset) { val rareFeatures = ruleset.terrains.values.filter { it.type == TerrainType.TerrainFeature && it.name !in Constants.vegetation && + it.name != Constants.floodPlains && it.name != Constants.ice } for (tile in tileMap.values.asSequence().filter { it.terrainFeature == null }) { @@ -331,29 +302,63 @@ class MapGenerationRandomness{ val worldCoords = HexMath.hex2WorldCoords(tile.position) return Perlin.noise3d(worldCoords.x.toDouble(), worldCoords.y.toDouble(), seed, nOctaves, persistence, lacunarity, scale) } + + + fun chooseSpreadOutLocations(number: Int, suitableTiles: List, initialDistance: Int): ArrayList { + for (distanceBetweenResources in initialDistance downTo 1) { + var availableTiles = suitableTiles.toList() + val chosenTiles = ArrayList() + + // If possible, we want to equalize the base terrains upon which + // the resources are found, so we save how many have been + // found for each base terrain and try to get one from the lowerst + val baseTerrainsToChosenTiles = HashMap() + for(tileInfo in availableTiles){ + if(tileInfo.baseTerrain !in baseTerrainsToChosenTiles) + baseTerrainsToChosenTiles[tileInfo.baseTerrain] = 0 + } + + for (i in 1..number) { + if (availableTiles.isEmpty()) break + val orderedKeys = baseTerrainsToChosenTiles.entries + .sortedBy { it.value }.map { it.key } + val firstKeyWithTilesLeft = orderedKeys + .first { availableTiles.any { tile -> tile.baseTerrain== it} } + val chosenTile = availableTiles.filter { it.baseTerrain==firstKeyWithTilesLeft }.random() + availableTiles = availableTiles.filter { it.aerialDistanceTo(chosenTile) > distanceBetweenResources } + chosenTiles.add(chosenTile) + baseTerrainsToChosenTiles[firstKeyWithTilesLeft] = baseTerrainsToChosenTiles[firstKeyWithTilesLeft]!!+1 + } + // Either we got them all, or we're not going to get anything better + if (chosenTiles.size == number || distanceBetweenResources == 1) return chosenTiles + } + throw Exception("Couldn't choose suitable tiles for $number resources!") + } } -class RiverGenerator(){ - public class RiverCoordinate(val position: Vector2, val bottomRightOrLeft: BottomRightOrLeft){ - enum class BottomRightOrLeft{ - BottomLeft, BottomRight - } +class RiverCoordinate(val position: Vector2, val bottomRightOrLeft: BottomRightOrLeft){ + enum class BottomRightOrLeft{ + /** 7 O'Clock of the tile */ + BottomLeft, + /** 5 O'Clock of the tile */ + BottomRight + } - fun getAdjacentPositions(): Sequence { - // What's nice is that adjacents are always the OPPOSITE in terms of right-left - rights are adjacent to only lefts, and vice-versa - // This means that a lot of obviously-wrong assignments are simple to spot - if (bottomRightOrLeft == BottomRightOrLeft.BottomLeft) { - return sequenceOf(RiverCoordinate(position, BottomRightOrLeft.BottomRight), // same tile, other side - RiverCoordinate(position.cpy().add(1f, 0f), BottomRightOrLeft.BottomRight), // tile to MY top-left, take its bottom right corner - RiverCoordinate(position.cpy().add(0f, -1f), BottomRightOrLeft.BottomRight) // Tile to MY bottom-left, take its bottom right - ) - } else { - return sequenceOf(RiverCoordinate(position, BottomRightOrLeft.BottomLeft), // same tile, other side - RiverCoordinate(position.cpy().add(0f, 1f), BottomRightOrLeft.BottomLeft), // tile to MY top-right, take its bottom left - RiverCoordinate(position.cpy().add(-1f, 0f), BottomRightOrLeft.BottomLeft) // tile to MY bottom-right, take its bottom left - ) - } + fun getAdjacentPositions(): Sequence { + // What's nice is that adjacents are always the OPPOSITE in terms of right-left - rights are adjacent to only lefts, and vice-versa + // This means that a lot of obviously-wrong assignments are simple to spot + if (bottomRightOrLeft == BottomRightOrLeft.BottomLeft) { + return sequenceOf(RiverCoordinate(position, BottomRightOrLeft.BottomRight), // same tile, other side + RiverCoordinate(position.cpy().add(1f, 0f), BottomRightOrLeft.BottomRight), // tile to MY top-left, take its bottom right corner + RiverCoordinate(position.cpy().add(0f, -1f), BottomRightOrLeft.BottomRight) // Tile to MY bottom-left, take its bottom right + ) + } else { + return sequenceOf(RiverCoordinate(position, BottomRightOrLeft.BottomLeft), // same tile, other side + RiverCoordinate(position.cpy().add(0f, 1f), BottomRightOrLeft.BottomLeft), // tile to MY top-right, take its bottom left + RiverCoordinate(position.cpy().add(-1f, 0f), BottomRightOrLeft.BottomLeft) // tile to MY bottom-right, take its bottom left + ) } } -} \ No newline at end of file +} + diff --git a/core/src/com/unciv/logic/map/mapgenerator/RiverGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/RiverGenerator.kt new file mode 100644 index 0000000000..9f65f66d3e --- /dev/null +++ b/core/src/com/unciv/logic/map/mapgenerator/RiverGenerator.kt @@ -0,0 +1,110 @@ +package com.unciv.logic.map.mapgenerator + +import com.unciv.Constants +import com.unciv.logic.map.TileInfo +import com.unciv.logic.map.TileMap + +class RiverGenerator(val randomness: MapGenerationRandomness){ + + fun spawnRivers(map: TileMap){ + val numberOfRivers = map.values.count { it.isLand } / 100 + + var optionalTiles = map.values + .filter { it.baseTerrain== Constants.mountain && it.aerialDistanceTo(getClosestWaterTile(it)) > 4 } + if(optionalTiles.size < numberOfRivers) + optionalTiles += map.values.filter { it.baseTerrain== Constants.hill && it.aerialDistanceTo(getClosestWaterTile(it)) > 4 } + if(optionalTiles.size < numberOfRivers) + optionalTiles = map.values.filter { it.isLand && it.aerialDistanceTo(getClosestWaterTile(it)) > 4 } + + + val riverStarts = randomness.chooseSpreadOutLocations(numberOfRivers, optionalTiles, 10) + for(tile in riverStarts) spawnRiver(tile, map) + + for(tile in map.values){ + if(tile.isAdjacentToRiver()){ + if(tile.baseTerrain== Constants.desert) tile.terrainFeature= Constants.floodPlains + else if(tile.baseTerrain== Constants.snow) tile.baseTerrain = Constants.tundra + else if(tile.baseTerrain== Constants.tundra) tile.baseTerrain = Constants.plains + tile.setTerrainTransients() + } + } + } + + private fun getClosestWaterTile(tile: TileInfo): TileInfo { + var distance = 1 + while(true){ + val waterTiles = tile.getTilesAtDistance(distance).filter { it.isWater } + if(waterTiles.none()) { + distance++ + continue + } + return waterTiles.toList().random(randomness.RNG) + } + } + + private fun spawnRiver(initialPosition: TileInfo, map: TileMap) { + // Recommendation: Draw a bunch of hexagons on paper before trying to understand this, it's super helpful! + val endPosition = getClosestWaterTile(initialPosition) + + var riverCoordinate = RiverCoordinate(initialPosition.position, + RiverCoordinate.BottomRightOrLeft.values().random(randomness.RNG)) + + + while(getAdjacentTiles(riverCoordinate,map).none { it.isWater }){ + val possibleCoordinates = riverCoordinate.getAdjacentPositions() + if(possibleCoordinates.none()) return // end of the line + val newCoordinate = possibleCoordinates +// .sortedBy { numberOfConnectedRivers(it,map) } + .groupBy { getAdjacentTiles(it,map).map { it.aerialDistanceTo(endPosition) }.min()!! } + .minBy { it.key }!! + .component2().random(randomness.RNG) +// .minBy { getAdjacentTiles(it,map).map { it.aerialDistanceTo(endPosition) }.min()!! }!! + + // set new rivers in place + val riverCoordinateTile = map[riverCoordinate.position] + if(newCoordinate.position == riverCoordinate.position) // same tile, switched right-to-left + riverCoordinateTile.hasBottomRiver=true + else if(riverCoordinate.bottomRightOrLeft== RiverCoordinate.BottomRightOrLeft.BottomRight){ + if(getAdjacentTiles(newCoordinate,map).contains(riverCoordinateTile)) // moved from our 5 O'Clock to our 3 O'Clock + riverCoordinateTile.hasBottomRightRiver = true + else // moved from our 5 O'Clock down in the 5 O'Clock direction - this is the 8 O'Clock river of the tile to our 4 O'Clock! + map[newCoordinate.position].hasBottomLeftRiver = true + } + else { // riverCoordinate.bottomRightOrLeft==RiverCoordinate.BottomRightOrLeft.Left + if(getAdjacentTiles(newCoordinate,map).contains(riverCoordinateTile)) // moved from our 7 O'Clock to our 9 O'Clock + riverCoordinateTile.hasBottomLeftRiver = true + else // moved from our 7 O'Clock down in the 7 O'Clock direction + map[newCoordinate.position].hasBottomRightRiver = true + } + riverCoordinate = newCoordinate + } + + } + + fun getAdjacentTiles(riverCoordinate: RiverCoordinate, map: TileMap): Sequence { + val potentialPositions = sequenceOf( + riverCoordinate.position, + riverCoordinate.position.cpy().add(-1f, -1f), // tile directly below us, + if (riverCoordinate.bottomRightOrLeft == RiverCoordinate.BottomRightOrLeft.BottomLeft) + riverCoordinate.position.cpy().add(0f, -1f) // tile to our bottom-left + else riverCoordinate.position.cpy().add(-1f, 0f) // tile to our bottom-right + ) + return potentialPositions.map { if (map.contains(it)) map[it] else null }.filterNotNull() + } + + fun numberOfConnectedRivers(riverCoordinate: RiverCoordinate, map: TileMap): Int { + var sum = 0 + if (map.contains(riverCoordinate.position) && map[riverCoordinate.position].hasBottomRiver) sum += 1 + if (riverCoordinate.bottomRightOrLeft == RiverCoordinate.BottomRightOrLeft.BottomLeft) { + if (map.contains(riverCoordinate.position) && map[riverCoordinate.position].hasBottomLeftRiver) sum += 1 + val bottomLeftTilePosition = riverCoordinate.position.cpy().add(0f, -1f) + if (map.contains(bottomLeftTilePosition) && map[bottomLeftTilePosition].hasBottomRightRiver) sum += 1 + } else { + if (map.contains(riverCoordinate.position) && map[riverCoordinate.position].hasBottomRightRiver) sum += 1 + val bottomLeftTilePosition = riverCoordinate.position.cpy().add(-1f, 0f) + if (map.contains(bottomLeftTilePosition) && map[bottomLeftTilePosition].hasBottomLeftRiver) sum += 1 + } + return sum + } + +} \ No newline at end of file