mirror of
https://github.com/yairm210/Unciv.git
synced 2025-01-13 08:14:29 +07:00
River terraform (#11256)
* Allow terraformers to place Rivers * Someone said Ruleset is not a Set * Turn setConnectedByRiver into a public Tile API * Follow review suggestions
This commit is contained in:
parent
44528d26d0
commit
41b29256fe
@ -220,20 +220,23 @@ class MapGenerator(val ruleset: Ruleset, private val coroutineScope: CoroutineSc
|
||||
debug("MapGenerator.%s took %s.%sms", text, delta/1000000L, (delta/10000L).rem(100))
|
||||
}
|
||||
|
||||
fun convertTerrains(tiles: Iterable<Tile>) {
|
||||
for (tile in tiles) {
|
||||
val conversionUnique =
|
||||
tile.getBaseTerrain().getMatchingUniques(UniqueType.ChangesTerrain)
|
||||
.firstOrNull { tile.isAdjacentTo(it.params[1]) }
|
||||
?: continue
|
||||
val terrain = ruleset.terrains[conversionUnique.params[0]] ?: continue
|
||||
fun convertTerrains(tiles: Iterable<Tile>) = Helpers.convertTerrains(ruleset, tiles)
|
||||
object Helpers {
|
||||
fun convertTerrains(ruleset: Ruleset, tiles: Iterable<Tile>) {
|
||||
for (tile in tiles) {
|
||||
val conversionUnique =
|
||||
tile.getBaseTerrain().getMatchingUniques(UniqueType.ChangesTerrain)
|
||||
.firstOrNull { tile.isAdjacentTo(it.params[1]) }
|
||||
?: continue
|
||||
val terrain = ruleset.terrains[conversionUnique.params[0]] ?: continue
|
||||
|
||||
if (terrain.type == TerrainType.TerrainFeature) {
|
||||
if (!terrain.occursOn.contains(tile.lastTerrain.name)) continue
|
||||
tile.addTerrainFeature(terrain.name)
|
||||
} else
|
||||
tile.baseTerrain = terrain.name
|
||||
tile.setTerrainTransients()
|
||||
if (terrain.type != TerrainType.TerrainFeature)
|
||||
tile.baseTerrain = terrain.name
|
||||
else if (!terrain.occursOn.contains(tile.lastTerrain.name)) continue
|
||||
else
|
||||
tile.addTerrainFeature(terrain.name)
|
||||
tile.setTerrainTransients()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import com.unciv.Constants
|
||||
import com.unciv.logic.map.TileMap
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
import com.unciv.utils.debug
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@ -189,4 +190,80 @@ class RiverGenerator(
|
||||
}
|
||||
}.count { it }
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** [UniqueType.OneTimeChangeTerrain] tries to place a "River" feature.
|
||||
*
|
||||
* Operates on *edges* - while [spawnRiver] hops from [vertex][RiverCoordinate] to vertex!
|
||||
* Placed here to make comparison easier, even though the implementation has nothing else in common.
|
||||
* @return success - one edge of [tile] has a new river
|
||||
*/
|
||||
fun continueRiverOn(tile: Tile): Boolean {
|
||||
if (!tile.isLand) return false
|
||||
val tileMap = tile.tileMap
|
||||
|
||||
/** Helper to prioritize a tile edge for river placement - accesses [tile] as closure,
|
||||
* and considers the edge common with [otherTile] in direction [clockPosition].
|
||||
*
|
||||
* Will consider two additional tiles - those that are neighbor to both [tile] and [otherTile],
|
||||
* and four other edges - those connecting to "our" edge.
|
||||
*/
|
||||
class NeighborData(val otherTile: Tile) {
|
||||
val clockPosition = tileMap.getNeighborTileClockPosition(tile, otherTile)
|
||||
// Accesses `tile` as closure
|
||||
val isConnectedByRiver = tile.isConnectedByRiver(otherTile)
|
||||
val edgeLeadsToSea: Boolean
|
||||
val connectedRiverCount: Int
|
||||
val verticesFormYCount: Int
|
||||
|
||||
init {
|
||||
// Similar: private fun Tile.getLeftSharedNeighbor in TileLayerBorders
|
||||
val leftSharedNeighbor = tileMap.getClockPositionNeighborTile(tile, (clockPosition - 2) % 12)
|
||||
val rightSharedNeighbor = tileMap.getClockPositionNeighborTile(tile, (clockPosition + 2) % 12)
|
||||
edgeLeadsToSea = leftSharedNeighbor?.isWater == true || rightSharedNeighbor?.isWater == true
|
||||
connectedRiverCount = sequence {
|
||||
yield(leftSharedNeighbor?.isConnectedByRiver(tile))
|
||||
yield(leftSharedNeighbor?.isConnectedByRiver(otherTile))
|
||||
yield(rightSharedNeighbor?.isConnectedByRiver(tile))
|
||||
yield(rightSharedNeighbor?.isConnectedByRiver(otherTile))
|
||||
}.count { it == true }
|
||||
verticesFormYCount = sequence {
|
||||
yield(leftSharedNeighbor?.run { isConnectedByRiver(tile) && isConnectedByRiver(otherTile) })
|
||||
yield(rightSharedNeighbor?.run { isConnectedByRiver(tile) && isConnectedByRiver(otherTile) })
|
||||
}.count { it == true }
|
||||
}
|
||||
|
||||
fun getPriority(edgeToSeaPriority: Int) =
|
||||
// choose a priority - only order matters, not magnitude
|
||||
when {
|
||||
isConnectedByRiver -> -9 // ensures this isn't chosen, otherwise "cannot place another river" would have bailed
|
||||
edgeLeadsToSea -> edgeToSeaPriority + connectedRiverCount - 3 * verticesFormYCount
|
||||
// Just 6 possible cases left:
|
||||
// * Connect two bends = -2
|
||||
// * Connect a bend to nothing = -1 // debatable!
|
||||
// * Connect nothing = 0
|
||||
// * Connect a bend with an open end = 1
|
||||
// * Connect to one open end = 2
|
||||
// * Connect two open ends = 3 (make that 4 to simplify)
|
||||
verticesFormYCount == 2 -> -2
|
||||
verticesFormYCount == 1 -> connectedRiverCount * 2 - 5
|
||||
else -> connectedRiverCount * 2
|
||||
}
|
||||
}
|
||||
|
||||
// Collect data (includes tiles we already have a river edge with - need the stats)
|
||||
val viableNeighbors = tile.neighbors.filter { it.isLand }.map { NeighborData(it) }.toList()
|
||||
if (viableNeighbors.all { it.isConnectedByRiver }) return false // cannot place another river
|
||||
|
||||
// Greatly encourage connecting to sea unless the tile already has a river to sea, in which case slightly discourage another one
|
||||
val edgeToSeaPriority = if (viableNeighbors.none { it.isConnectedByRiver && it.edgeLeadsToSea }) 9 else -1
|
||||
|
||||
val choice = viableNeighbors
|
||||
.groupBy { it.getPriority(edgeToSeaPriority) } // Assign and group by priorities
|
||||
.maxBy { it.key }.value // Get the List with best priority - can't be empty
|
||||
.random()
|
||||
|
||||
return tile.setConnectedByRiver(choice.otherTile, newValue = true, convertTerrains = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import com.unciv.logic.map.HexMath
|
||||
import com.unciv.logic.map.MapParameters
|
||||
import com.unciv.logic.map.MapResources
|
||||
import com.unciv.logic.map.TileMap
|
||||
import com.unciv.logic.map.mapgenerator.MapGenerator
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.logic.map.mapunit.movement.UnitMovement
|
||||
import com.unciv.models.ruleset.Ruleset
|
||||
@ -644,13 +645,58 @@ class Tile : IsPartOfGameInfoSerialization {
|
||||
}
|
||||
}
|
||||
|
||||
@delegate:Transient
|
||||
private val isAdjacentToRiverLazy by lazy {
|
||||
// These are so if you add a river at the bottom of the map (no neighboring tile to be connected to)
|
||||
// that tile is still considered adjacent to river
|
||||
hasBottomLeftRiver || hasBottomRiver || hasBottomRightRiver
|
||||
|| neighbors.any { isConnectedByRiver(it) } }
|
||||
fun isAdjacentToRiver() = isAdjacentToRiverLazy
|
||||
@Transient
|
||||
private var isAdjacentToRiver = false
|
||||
@Transient
|
||||
private var isAdjacentToRiverKnown = false
|
||||
fun isAdjacentToRiver(): Boolean {
|
||||
if (!isAdjacentToRiverKnown) {
|
||||
isAdjacentToRiver =
|
||||
// These are so if you add a river at the bottom of the map (no neighboring tile to be connected to)
|
||||
// that tile is still considered adjacent to river
|
||||
hasBottomLeftRiver || hasBottomRiver || hasBottomRightRiver
|
||||
|| neighbors.any { isConnectedByRiver(it) }
|
||||
isAdjacentToRiverKnown = true
|
||||
}
|
||||
return isAdjacentToRiver
|
||||
}
|
||||
|
||||
/** Allows resetting the cached value [isAdjacentToRiver] will return
|
||||
* @param isKnownTrue Set this to indicate you need to update the cache due to **adding** a river edge
|
||||
* (removing would need to look at other edges, and that is what isAdjacentToRiver will do)
|
||||
*/
|
||||
private fun resetAdjacentToRiverTransient(isKnownTrue: Boolean = false) {
|
||||
isAdjacentToRiver = isKnownTrue
|
||||
isAdjacentToRiverKnown = isKnownTrue
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the "has river" state of one edge of this Tile. Works for all six directions.
|
||||
* @param otherTile The neighbor tile in the direction the river we wish to change is (If it's not a neighbor, this does nothing).
|
||||
* @param newValue The new river edge state: `true` to create a river, `false` to remove one.
|
||||
* @param convertTerrains If true, calls MapGenerator's convertTerrains to apply UniqueType.ChangesTerrain effects.
|
||||
* @return The state did change (`false`: the edge already had the `newValue`)
|
||||
*/
|
||||
fun setConnectedByRiver(otherTile: Tile, newValue: Boolean, convertTerrains: Boolean = false): Boolean {
|
||||
//todo synergy potential with [MapEditorEditRiversTab]?
|
||||
val field = when (tileMap.getNeighborTileClockPosition(this, otherTile)) {
|
||||
2 -> otherTile::hasBottomLeftRiver // we're to the bottom-left of it
|
||||
4 -> ::hasBottomRightRiver // we're to the top-left of it
|
||||
6 -> ::hasBottomRiver // we're directly above it
|
||||
8 -> ::hasBottomLeftRiver // we're to the top-right of it
|
||||
10 -> otherTile::hasBottomRightRiver // we're to the bottom-right of it
|
||||
12 -> otherTile::hasBottomRiver // we're directly below it
|
||||
else -> return false
|
||||
}
|
||||
if (field.get() == newValue) return false
|
||||
field.set(newValue)
|
||||
val affectedTiles = listOf(this, otherTile)
|
||||
for (tile in affectedTiles)
|
||||
tile.resetAdjacentToRiverTransient(newValue)
|
||||
if (convertTerrains)
|
||||
MapGenerator.Helpers.convertTerrains(ruleset, affectedTiles)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns whether units of [civInfo] can pass through this tile, considering only civ-wide filters.
|
||||
|
@ -18,6 +18,7 @@ import com.unciv.logic.civilization.PopupAlert
|
||||
import com.unciv.logic.civilization.TechAction
|
||||
import com.unciv.logic.civilization.managers.ReligionState
|
||||
import com.unciv.logic.map.mapgenerator.NaturalWonderGenerator
|
||||
import com.unciv.logic.map.mapgenerator.RiverGenerator
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.logic.map.tile.Tile
|
||||
import com.unciv.models.UpgradeUnitAction
|
||||
@ -99,7 +100,7 @@ object UniqueTriggerActivation {
|
||||
val tileBasedRandom =
|
||||
if (tile != null) Random(tile.position.toString().hashCode())
|
||||
else Random(-550) // Very random indeed
|
||||
val ruleSet = civInfo.gameInfo.ruleset
|
||||
val ruleset = civInfo.gameInfo.ruleset
|
||||
|
||||
when (unique.type) {
|
||||
UniqueType.TriggerEvent -> {
|
||||
@ -115,7 +116,7 @@ object UniqueTriggerActivation {
|
||||
|
||||
UniqueType.OneTimeFreeUnit -> {
|
||||
val unitName = unique.params[0]
|
||||
val baseUnit = ruleSet.units[unitName] ?: return null
|
||||
val baseUnit = ruleset.units[unitName] ?: return null
|
||||
val civUnit = civInfo.getEquivalentUnit(baseUnit)
|
||||
if (civUnit.isCityFounder() && civInfo.isOneCityChallenger())
|
||||
return null
|
||||
@ -156,7 +157,7 @@ object UniqueTriggerActivation {
|
||||
|
||||
UniqueType.OneTimeAmountFreeUnits -> {
|
||||
val unitName = unique.params[1]
|
||||
val baseUnit = ruleSet.units[unitName] ?: return null
|
||||
val baseUnit = ruleset.units[unitName] ?: return null
|
||||
val civUnit = civInfo.getEquivalentUnit(baseUnit)
|
||||
if (civUnit.isCityFounder() && civInfo.isOneCityChallenger())
|
||||
return null
|
||||
@ -212,7 +213,7 @@ object UniqueTriggerActivation {
|
||||
UniqueType.OneTimeFreeUnitRuins -> {
|
||||
var civUnit = civInfo.getEquivalentUnit(unique.params[0])
|
||||
if ( civUnit.isCityFounder() && civInfo.isOneCityChallenger()) {
|
||||
val replacementUnit = ruleSet.units.values
|
||||
val replacementUnit = ruleset.units.values
|
||||
.firstOrNull {
|
||||
it.getMatchingUniques(UniqueType.BuildImprovements)
|
||||
.any { unique -> unique.params[0] == "Land" }
|
||||
@ -383,7 +384,7 @@ object UniqueTriggerActivation {
|
||||
}
|
||||
}
|
||||
UniqueType.OneTimeFreeTechRuins -> {
|
||||
val researchableTechsFromThatEra = ruleSet.technologies.values
|
||||
val researchableTechsFromThatEra = ruleset.technologies.values
|
||||
.filter {
|
||||
(it.column!!.era == unique.params[1] || unique.params[1] == "any era")
|
||||
&& civInfo.tech.canBeResearched(it.name)
|
||||
@ -445,7 +446,7 @@ object UniqueTriggerActivation {
|
||||
|
||||
UniqueType.OneTimeProvideResources -> {
|
||||
val resourceName = unique.params[1]
|
||||
val resource = ruleSet.tileResources[resourceName] ?: return null
|
||||
val resource = ruleset.tileResources[resourceName] ?: return null
|
||||
if (!resource.isStockpiled()) return null
|
||||
|
||||
return {
|
||||
@ -464,7 +465,7 @@ object UniqueTriggerActivation {
|
||||
|
||||
UniqueType.OneTimeConsumeResources -> {
|
||||
val resourceName = unique.params[1]
|
||||
val resource = ruleSet.tileResources[resourceName] ?: return null
|
||||
val resource = ruleset.tileResources[resourceName] ?: return null
|
||||
if (!resource.isStockpiled()) return null
|
||||
|
||||
return {
|
||||
@ -498,7 +499,7 @@ object UniqueTriggerActivation {
|
||||
|
||||
val unitsToPromote = civInfo.units.getCivUnits().filter { it.matchesFilter(filter) }
|
||||
.filter { unitToPromote ->
|
||||
ruleSet.unitPromotions.values.any {
|
||||
ruleset.unitPromotions.values.any {
|
||||
it.name == promotion && unitToPromote.type.name in it.unitTypes
|
||||
}
|
||||
}.toList()
|
||||
@ -828,7 +829,7 @@ object UniqueTriggerActivation {
|
||||
}
|
||||
}
|
||||
UniqueType.FreeSpecificBuildings ->{
|
||||
val building = ruleSet.buildings[unique.params[0]] ?: return null
|
||||
val building = ruleset.buildings[unique.params[0]] ?: return null
|
||||
return {
|
||||
civInfo.civConstructions.addFreeBuildings(building, unique.params[1].toInt())
|
||||
true
|
||||
@ -946,7 +947,9 @@ object UniqueTriggerActivation {
|
||||
|
||||
UniqueType.OneTimeChangeTerrain -> {
|
||||
if (tile == null) return null
|
||||
val terrain = ruleSet.terrains[unique.params[0]] ?: return null
|
||||
val terrain = ruleset.terrains[unique.params[0]] ?: return null
|
||||
if (terrain.name == Constants.river)
|
||||
return getOneTimeChangeRiverTriggerFunction(tile)
|
||||
if (terrain.type == TerrainType.TerrainFeature && !terrain.occursOn.contains(tile.lastTerrain.name))
|
||||
return null
|
||||
if (tile.terrainFeatures.contains(terrain.name)) return null
|
||||
@ -959,7 +962,7 @@ object UniqueTriggerActivation {
|
||||
TerrainType.TerrainFeature -> tile.addTerrainFeature(terrain.name)
|
||||
TerrainType.NaturalWonder -> NaturalWonderGenerator.placeNaturalWonder(terrain, tile)
|
||||
}
|
||||
TileInfoNormalizer.normalizeToRuleset(tile, ruleSet)
|
||||
TileInfoNormalizer.normalizeToRuleset(tile, ruleset)
|
||||
tile.getUnits().filter { !it.movement.canPassThrough(tile) }.toList()
|
||||
.forEach { it.movement.teleportToClosestMoveableTile() }
|
||||
true
|
||||
@ -980,4 +983,10 @@ object UniqueTriggerActivation {
|
||||
}
|
||||
else null
|
||||
}
|
||||
|
||||
private fun getOneTimeChangeRiverTriggerFunction(tile: Tile): (()->Boolean)? {
|
||||
if (tile.neighbors.none { it.isLand && !tile.isConnectedByRiver(it) })
|
||||
return null // no place for another river
|
||||
return { RiverGenerator.continueRiverOn(tile) }
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user