Terraforming! (#11152)

* Terraforming!

* comment

* whoops

* And better tests, that actually output text.

* SomeTrog caught this doc error :)

* Fix natural wonder placement, it better belongs with base terrain
Also, normalize tiles to ruleset after plonking a new terrain

* Out Of Cheese Error, Redo From Start

- Only terrains, no improvements - one problem at a time
- Fit new functional trigger format
- Respect occursOn
- Teleport units out of tiles if they can't pass through (nat wonders for example)
- No need for terrainFeature unique parameter
- Tested with "Turn this tile into a [Grand Mesa] tile <for [0] movement>", "Turn this tile into a [Forest] tile <for [0] movement>" uniques

* Don't allow city tiles to turn into water/wonders etc kudos @SomeTrog
This commit is contained in:
Yair Morgenstern
2024-02-20 23:38:23 +02:00
committed by GitHub
parent 2c96c6f244
commit 23edf07a41
7 changed files with 87 additions and 37 deletions

View File

@ -151,18 +151,10 @@ class NaturalWonderGenerator(val ruleset: Ruleset, val randomness: MapGeneration
val targetGroupSize = if (minGroupSize == maxGroupSize) maxGroupSize val targetGroupSize = if (minGroupSize == maxGroupSize) maxGroupSize
else (minGroupSize..maxGroupSize).random(randomness.RNG) else (minGroupSize..maxGroupSize).random(randomness.RNG)
var convertNeighborsExcept: String? = null
var convertUnique = wonder.getMatchingUniques(UniqueType.NaturalWonderConvertNeighbors).firstOrNull()
var convertNeighborsTo = convertUnique?.params?.get(0)
if (convertNeighborsTo == null) {
convertUnique = wonder.getMatchingUniques(UniqueType.NaturalWonderConvertNeighborsExcept).firstOrNull()
convertNeighborsExcept = convertUnique?.params?.get(0)
convertNeighborsTo = convertUnique?.params?.get(1)
}
if (suitableLocations.size >= minGroupSize) { if (suitableLocations.size >= minGroupSize) {
val location = suitableLocations.random(randomness.RNG) val location = suitableLocations.random(randomness.RNG)
val list = mutableListOf(location) val list = mutableListOf(location)
while (list.size < targetGroupSize) { while (list.size < targetGroupSize) {
val allNeighbors = list.flatMap { it.neighbors }.minus(list).toHashSet() val allNeighbors = list.flatMap { it.neighbors }.minus(list).toHashSet()
val candidates = suitableLocations.filter { it in allNeighbors } val candidates = suitableLocations.filter { it in allNeighbors }
@ -171,30 +163,11 @@ class NaturalWonderGenerator(val ruleset: Ruleset, val randomness: MapGeneration
} }
if (list.size >= minGroupSize) { if (list.size >= minGroupSize) {
list.forEach { list.forEach {
clearTile(it) placeNaturalWonder(wonder, location)
it.naturalWonder = wonder.name
if (wonder.turnsInto != null)
it.baseTerrain = wonder.turnsInto!!
// Add all tiles within a certain distance to a blacklist so NW:s don't cluster // Add all tiles within a certain distance to a blacklist so NW:s don't cluster
blockedTiles.addAll(it.getTilesInDistance(it.tileMap.mapParameters.mapSize.height / 5)) blockedTiles.addAll(it.getTilesInDistance(it.tileMap.mapParameters.mapSize.height / 5))
} }
if (convertNeighborsTo != null) {
for (tile in location.neighbors) {
if (tile.baseTerrain == convertNeighborsTo) continue
if (tile.baseTerrain == convertNeighborsExcept) continue
if (convertNeighborsTo == Constants.coast)
for (neighbor in tile.neighbors) {
// This is so we don't have this tile turn into Coast, and then it's touching a Lake tile.
// We just turn the lake tiles into this kind of tile.
if (neighbor.baseTerrain == Constants.lakes) {
neighbor.baseTerrain = tile.baseTerrain
neighbor.setTerrainTransients()
}
}
tile.baseTerrain = convertNeighborsTo
clearTile(tile)
}
}
debug("Natural Wonder %s @%s", wonder.name, location.position) debug("Natural Wonder %s @%s", wonder.name, location.position)
return true return true
@ -205,11 +178,47 @@ class NaturalWonderGenerator(val ruleset: Ruleset, val randomness: MapGeneration
return false return false
} }
private fun clearTile(tile: Tile) { companion object {
tile.setTerrainFeatures(listOf()) fun placeNaturalWonder(wonder: Terrain, location: Tile) {
tile.resource = null clearTile(location)
tile.removeImprovement() location.naturalWonder = wonder.name
tile.setTerrainTransients() if (wonder.turnsInto != null)
location.baseTerrain = wonder.turnsInto!!
var convertNeighborsExcept: String? = null
var convertUnique = wonder.getMatchingUniques(UniqueType.NaturalWonderConvertNeighbors).firstOrNull()
var convertNeighborsTo = convertUnique?.params?.get(0)
if (convertNeighborsTo == null) {
convertUnique = wonder.getMatchingUniques(UniqueType.NaturalWonderConvertNeighborsExcept).firstOrNull()
convertNeighborsExcept = convertUnique?.params?.get(0)
convertNeighborsTo = convertUnique?.params?.get(1)
}
if (convertNeighborsTo != null) {
for (tile in location.neighbors) {
if (tile.baseTerrain == convertNeighborsTo) continue
if (tile.baseTerrain == convertNeighborsExcept) continue
if (convertNeighborsTo == Constants.coast)
for (neighbor in tile.neighbors) {
// This is so we don't have this tile turn into Coast, and then it's touching a Lake tile.
// We just turn the lake tiles into this kind of tile.
if (neighbor.baseTerrain == Constants.lakes) {
neighbor.baseTerrain = tile.baseTerrain
neighbor.setTerrainTransients()
}
}
tile.baseTerrain = convertNeighborsTo
clearTile(tile)
}
}
}
private fun clearTile(tile: Tile) {
tile.setTerrainFeatures(listOf())
tile.resource = null
tile.removeImprovement()
tile.setTerrainTransients()
}
} }
/** Implements [UniqueParameterType.SimpleTerrain][com.unciv.models.ruleset.unique.UniqueParameterType.SimpleTerrain] */ /** Implements [UniqueParameterType.SimpleTerrain][com.unciv.models.ruleset.unique.UniqueParameterType.SimpleTerrain] */

View File

@ -26,6 +26,7 @@ import com.unciv.models.ruleset.unique.UniqueMap
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.components.extensions.withItem import com.unciv.ui.components.extensions.withItem
import com.unciv.ui.components.extensions.withoutItem import com.unciv.ui.components.extensions.withoutItem
import com.unciv.ui.screens.mapeditorscreen.TileInfoNormalizer
import com.unciv.utils.DebugUtils import com.unciv.utils.DebugUtils
import com.unciv.utils.Log import com.unciv.utils.Log
import kotlin.math.abs import kotlin.math.abs
@ -832,6 +833,14 @@ open class Tile : IsPartOfGameInfoSerialization {
else unitHeight else unitHeight
} }
fun setBaseTerrain(baseTerrainObject: Terrain){
baseTerrain = baseTerrainObject.name
this.baseTerrainObject = baseTerrainObject
TileInfoNormalizer.normalizeToRuleset(this, ruleset)
setTerrainFeatures(terrainFeatures)
setTerrainTransients()
}
private fun updateUniqueMap() { private fun updateUniqueMap() {
if (!::tileMap.isInitialized) return // This tile is a fake tile, for visual display only (e.g. map editor, civilopedia) if (!::tileMap.isInitialized) return // This tile is a fake tile, for visual display only (e.g. map editor, civilopedia)
val terrainNameList = allTerrains.map { it.name }.toList() val terrainNameList = allTerrains.map { it.name }.toList()

View File

@ -375,7 +375,7 @@ enum class UniqueParameterType(
/** Used by [NaturalWonderGenerator.trySpawnOnSuitableLocation][com.unciv.logic.map.mapgenerator.NaturalWonderGenerator.trySpawnOnSuitableLocation], only tests base terrain */ /** Used by [NaturalWonderGenerator.trySpawnOnSuitableLocation][com.unciv.logic.map.mapgenerator.NaturalWonderGenerator.trySpawnOnSuitableLocation], only tests base terrain */
BaseTerrain("baseTerrain", Constants.grassland, "The name of any terrain that is a base terrain according to the json file") { BaseTerrain("baseTerrain", Constants.grassland, "The name of any terrain that is a base terrain according to the json file") {
override fun getErrorSeverity(parameterText: String, ruleset: Ruleset): override fun getErrorSeverity(parameterText: String, ruleset: Ruleset):
UniqueType.UniqueParameterErrorSeverity? { UniqueType.UniqueParameterErrorSeverity? {
if (ruleset.terrains[parameterText]?.type?.isBaseTerrain == true) return null if (ruleset.terrains[parameterText]?.type?.isBaseTerrain == true) return null
return UniqueType.UniqueParameterErrorSeverity.RulesetSpecific return UniqueType.UniqueParameterErrorSeverity.RulesetSpecific
} }

View File

@ -16,15 +16,18 @@ import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PolicyAction import com.unciv.logic.civilization.PolicyAction
import com.unciv.logic.civilization.TechAction import com.unciv.logic.civilization.TechAction
import com.unciv.logic.civilization.managers.ReligionState import com.unciv.logic.civilization.managers.ReligionState
import com.unciv.logic.map.mapgenerator.NaturalWonderGenerator
import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile import com.unciv.logic.map.tile.Tile
import com.unciv.models.UpgradeUnitAction import com.unciv.models.UpgradeUnitAction
import com.unciv.models.ruleset.BeliefType import com.unciv.models.ruleset.BeliefType
import com.unciv.models.ruleset.tile.TerrainType
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats import com.unciv.models.stats.Stats
import com.unciv.models.translations.fillPlaceholders import com.unciv.models.translations.fillPlaceholders
import com.unciv.models.translations.hasPlaceholderParameters import com.unciv.models.translations.hasPlaceholderParameters
import com.unciv.ui.components.extensions.addToMapOfSets import com.unciv.ui.components.extensions.addToMapOfSets
import com.unciv.ui.screens.mapeditorscreen.TileInfoNormalizer
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.random.Random import kotlin.random.Random
@ -926,6 +929,28 @@ object UniqueTriggerActivation {
true true
} }
} }
UniqueType.OneTimeChangeTerrain -> {
if (tile == null) return null
val terrain = ruleSet.terrains[unique.params[0]] ?: return null
if (terrain.type == TerrainType.TerrainFeature && !terrain.occursOn.contains(tile.lastTerrain.name))
return null
if (tile.terrainFeatures.contains(terrain.name)) return null
if (tile.isCityCenter() && terrain.type != TerrainType.Land) return null
if (terrain.type.isBaseTerrain && tile.baseTerrain == terrain.name) return null
return {
when (terrain.type) {
TerrainType.Land, TerrainType.Water -> tile.setBaseTerrain(terrain)
TerrainType.TerrainFeature -> tile.addTerrainFeature(terrain.name)
TerrainType.NaturalWonder -> NaturalWonderGenerator.placeNaturalWonder(terrain, tile)
}
TileInfoNormalizer.normalizeToRuleset(tile, ruleSet)
tile.getUnits().filter { !it.movement.canPassThrough(tile) }.toList()
.forEach { it.movement.teleportToClosestMoveableTile() }
true
}
}
else -> return null else -> return null
} }
} }

View File

@ -761,6 +761,8 @@ enum class UniqueType(
SkipPromotion("Doing so will consume this opportunity to choose a Promotion", UniqueTarget.Promotion), SkipPromotion("Doing so will consume this opportunity to choose a Promotion", UniqueTarget.Promotion),
FreePromotion("This Promotion is free", UniqueTarget.Promotion), FreePromotion("This Promotion is free", UniqueTarget.Promotion),
OneTimeChangeTerrain("Turn this tile into a [terrainName] tile", UniqueTarget.UnitTriggerable),
UnitsGainPromotion("[mapUnitFilter] units gain the [promotion] promotion", UniqueTarget.Triggerable), // Not used in Vanilla UnitsGainPromotion("[mapUnitFilter] units gain the [promotion] promotion", UniqueTarget.Triggerable), // Not used in Vanilla
FreeStatBuildings("Provides the cheapest [stat] building in your first [positiveAmount] cities for free", UniqueTarget.Triggerable), // used in Policy FreeStatBuildings("Provides the cheapest [stat] building in your first [positiveAmount] cities for free", UniqueTarget.Triggerable), // used in Policy
FreeSpecificBuildings("Provides a [buildingName] in your first [positiveAmount] cities for free", UniqueTarget.Triggerable), // used in Policy FreeSpecificBuildings("Provides a [buildingName] in your first [positiveAmount] cities for free", UniqueTarget.Triggerable), // used in Policy

View File

@ -190,6 +190,11 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
Applicable to: UnitTriggerable Applicable to: UnitTriggerable
??? example "Turn this tile into a [terrain] tile"
Example: "Turn this tile into a [Unknown] tile"
Applicable to: UnitTriggerable
## Global uniques ## Global uniques
!!! note "" !!! note ""

View File

@ -110,7 +110,7 @@ class BasicTests {
for (paramType in entry.value) { for (paramType in entry.value) {
if (paramType == UniqueParameterType.Unknown) { if (paramType == UniqueParameterType.Unknown) {
val badParam = uniqueType.text.getPlaceholderParameters()[entry.index] val badParam = uniqueType.text.getPlaceholderParameters()[entry.index]
debug("%s param[%s] type \"%s\" is unknown", uniqueType.name, entry.index, badParam) println("${uniqueType.name} param[${entry.index}] type \"$badParam\" is unknown")
noUnknownParameters = false noUnknownParameters = false
} }
} }