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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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
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) {
val location = suitableLocations.random(randomness.RNG)
val list = mutableListOf(location)
while (list.size < targetGroupSize) {
val allNeighbors = list.flatMap { it.neighbors }.minus(list).toHashSet()
val candidates = suitableLocations.filter { it in allNeighbors }
@ -171,30 +163,11 @@ class NaturalWonderGenerator(val ruleset: Ruleset, val randomness: MapGeneration
}
if (list.size >= minGroupSize) {
list.forEach {
clearTile(it)
it.naturalWonder = wonder.name
if (wonder.turnsInto != null)
it.baseTerrain = wonder.turnsInto!!
placeNaturalWonder(wonder, location)
// 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))
}
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)
return true
@ -205,11 +178,47 @@ class NaturalWonderGenerator(val ruleset: Ruleset, val randomness: MapGeneration
return false
}
private fun clearTile(tile: Tile) {
tile.setTerrainFeatures(listOf())
tile.resource = null
tile.removeImprovement()
tile.setTerrainTransients()
companion object {
fun placeNaturalWonder(wonder: Terrain, location: Tile) {
clearTile(location)
location.naturalWonder = wonder.name
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] */

View File

@ -26,6 +26,7 @@ import com.unciv.models.ruleset.unique.UniqueMap
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.components.extensions.withItem
import com.unciv.ui.components.extensions.withoutItem
import com.unciv.ui.screens.mapeditorscreen.TileInfoNormalizer
import com.unciv.utils.DebugUtils
import com.unciv.utils.Log
import kotlin.math.abs
@ -832,6 +833,14 @@ open class Tile : IsPartOfGameInfoSerialization {
else unitHeight
}
fun setBaseTerrain(baseTerrainObject: Terrain){
baseTerrain = baseTerrainObject.name
this.baseTerrainObject = baseTerrainObject
TileInfoNormalizer.normalizeToRuleset(this, ruleset)
setTerrainFeatures(terrainFeatures)
setTerrainTransients()
}
private fun updateUniqueMap() {
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()

View File

@ -375,7 +375,7 @@ enum class UniqueParameterType(
/** 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") {
override fun getErrorSeverity(parameterText: String, ruleset: Ruleset):
UniqueType.UniqueParameterErrorSeverity? {
UniqueType.UniqueParameterErrorSeverity? {
if (ruleset.terrains[parameterText]?.type?.isBaseTerrain == true) return null
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.TechAction
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.tile.Tile
import com.unciv.models.UpgradeUnitAction
import com.unciv.models.ruleset.BeliefType
import com.unciv.models.ruleset.tile.TerrainType
import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats
import com.unciv.models.translations.fillPlaceholders
import com.unciv.models.translations.hasPlaceholderParameters
import com.unciv.ui.components.extensions.addToMapOfSets
import com.unciv.ui.screens.mapeditorscreen.TileInfoNormalizer
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade
import kotlin.math.roundToInt
import kotlin.random.Random
@ -926,6 +929,28 @@ object UniqueTriggerActivation {
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
}
}

View File

@ -761,6 +761,8 @@ enum class UniqueType(
SkipPromotion("Doing so will consume this opportunity to choose a Promotion", 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
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

View File

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

View File

@ -110,7 +110,7 @@ class BasicTests {
for (paramType in entry.value) {
if (paramType == UniqueParameterType.Unknown) {
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
}
}