diff --git a/core/src/com/unciv/logic/map/mapgenerator/NaturalWonderGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/NaturalWonderGenerator.kt index 1098ce38e2..c0418daf9f 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/NaturalWonderGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/NaturalWonderGenerator.kt @@ -6,6 +6,7 @@ import com.unciv.logic.map.tile.Tile import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.tile.Terrain import com.unciv.models.ruleset.tile.TerrainType +import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueType import com.unciv.utils.debug @@ -14,10 +15,6 @@ import kotlin.math.roundToInt class NaturalWonderGenerator(val ruleset: Ruleset, val randomness: MapGenerationRandomness) { - private val allTerrainFeatures = ruleset.terrains.values - .filter { it.type == TerrainType.TerrainFeature } - .map { it.name }.toSet() - private val blockedTiles = HashSet() /* @@ -64,7 +61,7 @@ class NaturalWonderGenerator(val ruleset: Ruleset, val randomness: MapGeneration } chosenWonders.sortBy { wonderCandidateTiles[it]!!.size } for (wonder in chosenWonders) { - if (trySpawnOnSuitableLocation(wonderCandidateTiles[wonder]!!.filter { it !in blockedTiles }.toList(), wonder)) + if (trySpawnOnSuitableLocation(wonderCandidateTiles[wonder]!!.filter { it !in blockedTiles }, wonder)) spawned.add(wonder) } @@ -180,59 +177,78 @@ class NaturalWonderGenerator(val ruleset: Ruleset, val randomness: MapGeneration companion object { fun placeNaturalWonder(wonder: Terrain, location: Tile) { - clearTile(location) location.naturalWonder = wonder.name - if (wonder.turnsInto != null) + if (wonder.turnsInto != null) { + clearTile(location) 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) + } else { + clearTile(location, wonder.occursOn) } - 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() - } - } - location.setConnectedByRiver(tile, false) + val conversionUniques = wonder.getMatchingUniques(UniqueType.NaturalWonderConvertNeighbors, StateForConditionals.IgnoreConditionals) + + wonder.getMatchingUniques(UniqueType.NaturalWonderConvertNeighborsExcept, StateForConditionals.IgnoreConditionals) + if (conversionUniques.none()) return + + for (tile in location.neighbors) { + val state = StateForConditionals(tile = tile) + for (unique in conversionUniques) { + if (!unique.conditionalsApply(state)) continue + val convertTo = if (unique.type == UniqueType.NaturalWonderConvertNeighborsExcept) { + if (tile.matchesWonderFilter(unique.params[0])) continue + unique.params[1] + } else unique.params[0] + if (tile.baseTerrain == convertTo || convertTo in tile.terrainFeatures) continue + if (convertTo == Constants.lakes && tile.isCoastalTile()) continue + val terrainObject = location.ruleset.terrains[convertTo] ?: continue + if (terrainObject.type == TerrainType.TerrainFeature && tile.baseTerrain !in terrainObject.occursOn) continue + if (convertTo == Constants.coast) + removeLakesNextToFutureCoast(location, tile) + if (terrainObject.type.isBaseTerrain) { + clearTile(tile) + tile.baseTerrain = convertTo + } + if (terrainObject.type == TerrainType.TerrainFeature) { + clearTile(tile, tile.terrainFeatures) + tile.addTerrainFeature(convertTo) } - tile.baseTerrain = convertNeighborsTo - clearTile(tile) } } } - private fun clearTile(tile: Tile) { - tile.setTerrainFeatures(listOf()) + // location is being converted to a NW, tile is a neighbor to be converted to coast: Ensure that coast won't show invalid rivers or coast touching lakes + private fun removeLakesNextToFutureCoast(location: Tile, tile: Tile) { + 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) { + clearTile(neighbor) + neighbor.baseTerrain = tile.baseTerrain + neighbor.setTerrainTransients() + } + } + location.setConnectedByRiver(tile, false) + } + + /** Implements [UniqueParameterType.SimpleTerrain][com.unciv.models.ruleset.unique.UniqueParameterType.SimpleTerrain] */ + private fun Tile.matchesWonderFilter(filter: String) = when (filter) { + "Elevated" -> baseTerrain == Constants.mountain || isHill() + "Water" -> isWater + "Land" -> isLand + Constants.hill -> isHill() + naturalWonder -> true + lastTerrain.name -> true + else -> baseTerrain == filter + } + + private fun clearTile(tile: Tile, exceptFeatures: List = listOf()) { + if (tile.terrainFeatures.isNotEmpty() && exceptFeatures != tile.terrainFeatures) + tile.setTerrainFeatures(tile.terrainFeatures.filter { it in exceptFeatures }) tile.resource = null - tile.improvement = null + tile.removeImprovement() tile.setTerrainTransients() } } - /** Implements [UniqueParameterType.SimpleTerrain][com.unciv.models.ruleset.unique.UniqueParameterType.SimpleTerrain] */ - private fun Tile.matchesWonderFilter(filter: String) = when (filter) { - "Elevated" -> baseTerrain == Constants.mountain || isHill() - "Water" -> isWater - "Land" -> isLand - Constants.hill -> isHill() - naturalWonder -> true - in allTerrainFeatures -> lastTerrain.name == filter - else -> baseTerrain == filter - } /* Barringer Crater: Must be in tundra or desert; cannot be adjacent to grassland; can be adjacent to a maximum diff --git a/core/src/com/unciv/logic/map/tile/TileNormalizer.kt b/core/src/com/unciv/logic/map/tile/TileNormalizer.kt index 4b756a904c..c2f600e1a4 100644 --- a/core/src/com/unciv/logic/map/tile/TileNormalizer.kt +++ b/core/src/com/unciv/logic/map/tile/TileNormalizer.kt @@ -10,9 +10,13 @@ object TileNormalizer { if (tile.naturalWonder != null && !ruleset.terrains.containsKey(tile.naturalWonder)) tile.naturalWonder = null if (tile.naturalWonder != null) { - if (tile.getNaturalWonder().turnsInto != null) - tile.baseTerrain = tile.getNaturalWonder().turnsInto!! - tile.setTerrainFeatures(listOf()) + val wonderTerrain = tile.getNaturalWonder() + if (wonderTerrain.turnsInto != null) { + tile.baseTerrain = wonderTerrain.turnsInto!! + tile.removeTerrainFeatures() + } else { + tile.setTerrainFeatures(tile.terrainFeatures.filter { it in wonderTerrain.occursOn }) + } tile.resource = null tile.clearImprovement() } diff --git a/core/src/com/unciv/models/ruleset/tile/Terrain.kt b/core/src/com/unciv/models/ruleset/tile/Terrain.kt index e8b868418f..7bb3bea728 100644 --- a/core/src/com/unciv/models/ruleset/tile/Terrain.kt +++ b/core/src/com/unciv/models/ruleset/tile/Terrain.kt @@ -25,7 +25,9 @@ class Terrain : RulesetStatsObject() { /** For terrain features */ val occursOn = ArrayList() - /** Used by Natural Wonders: it is the baseTerrain on top of which the Natural Wonder is placed */ + /** Used by Natural Wonders: it is the baseTerrain on top of which the Natural Wonder is placed + * Omitting it means the Natural Wonder is placed on whatever baseTerrain the Tile already had (limited by occursOn) + */ var turnsInto: String? = null override fun getUniqueTarget() = UniqueTarget.Terrain diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt index 409ec5ec28..7b0e7d6634 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt @@ -7,6 +7,7 @@ import com.unciv.models.ruleset.BeliefType import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.tile.ResourceType +import com.unciv.models.ruleset.tile.TerrainType import com.unciv.models.ruleset.unique.UniqueParameterType.Companion.guessTypeForTranslationWriter import com.unciv.models.ruleset.validation.Suppression import com.unciv.models.stats.Stat @@ -366,11 +367,20 @@ enum class UniqueParameterType( staticKnownValues + ruleset.terrains.keys }, - /** Used by [NaturalWonderGenerator.trySpawnOnSuitableLocation][com.unciv.logic.map.mapgenerator.NaturalWonderGenerator.trySpawnOnSuitableLocation], only tests base terrain */ + /** Used by [NaturalWonderGenerator][com.unciv.logic.map.mapgenerator.NaturalWonderGenerator], 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 getKnownValuesForAutocomplete(ruleset: Ruleset): Set = ruleset.terrains.filter { it.value.type.isBaseTerrain }.keys }, + /** Used by [UniqueType.NaturalWonderConvertNeighbors], only tests base terrain. + * - See [NaturalWonderGenerator.trySpawnOnSuitableLocation][com.unciv.logic.map.mapgenerator.NaturalWonderGenerator.trySpawnOnSuitableLocation] */ + TerrainFeature("terrainFeature", Constants.hill, "The name of any terrain that is a terrain feature according to the json file") { + override fun getErrorSeverity(parameterText: String, ruleset: Ruleset): + UniqueType.UniqueParameterErrorSeverity? { + if (ruleset.terrains[parameterText]?.type == TerrainType.TerrainFeature) return null + return UniqueType.UniqueParameterErrorSeverity.RulesetSpecific + } + }, /** Used by: [UniqueType.LandUnitsCrossTerrainAfterUnitGained] (CivilizationInfo.addUnit), * [UniqueType.ChangesTerrain] (MapGenerator.convertTerrains) */ diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index 826eb37420..ff9176e413 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -534,9 +534,13 @@ enum class UniqueType( NaturalWonderLargerLandmass("Must be on [amount] largest landmasses", UniqueTarget.Terrain, flags = UniqueFlag.setOfHiddenToUsers), NaturalWonderLatitude("Occurs on latitudes from [amount] to [amount] percent of distance equator to pole", UniqueTarget.Terrain, flags = UniqueFlag.setOfHiddenToUsers), NaturalWonderGroups("Occurs in groups of [amount] to [amount] tiles", UniqueTarget.Terrain, flags = UniqueFlag.setOfHiddenToUsers), - NaturalWonderConvertNeighbors("Neighboring tiles will convert to [baseTerrain]", UniqueTarget.Terrain, flags = UniqueFlag.setOfHiddenToUsers), + NaturalWonderConvertNeighbors("Neighboring tiles will convert to [baseTerrain/terrainFeature]", UniqueTarget.Terrain, flags = UniqueFlag.setOfHiddenToUsers, + docDescription = "Supports conditionals that need only a Tile as context and nothing else, like ``, and applies them per neighbor." + + "\nIf your mod renames Coast or Lakes, do not use this with one of these as parameter, as the code preventing artifacts won't work."), // The "Except [terrainFilter]" could theoretically be implemented with a conditional - NaturalWonderConvertNeighborsExcept("Neighboring tiles except [baseTerrain] will convert to [baseTerrain]", UniqueTarget.Terrain, flags = UniqueFlag.setOfHiddenToUsers), + NaturalWonderConvertNeighborsExcept("Neighboring tiles except [simpleTerrain] will convert to [baseTerrain/terrainFeature]", UniqueTarget.Terrain, flags = UniqueFlag.setOfHiddenToUsers, + docDescription = "Supports conditionals that need only a Tile as context and nothing else, like ``, and applies them per neighbor." + + "\nIf your mod renames Coast or Lakes, do not use this with one of these as parameter, as the code preventing artifacts won't work."), GrantsStatsToFirstToDiscover("Grants [stats] to the first civilization to discover it", UniqueTarget.Terrain), // General terrain diff --git a/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt b/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt index 2f5b3a4227..a9c8a2c260 100644 --- a/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt +++ b/core/src/com/unciv/models/ruleset/validation/RulesetValidator.kt @@ -479,9 +479,7 @@ class RulesetValidator(val ruleset: Ruleset) { else if (baseTerrain.type == TerrainType.NaturalWonder) lines.add("${terrain.name} occurs on natural wonder $baseTerrainName: Unsupported.", RulesetErrorSeverity.WarningOptionsOnly, terrain) } - if (terrain.type == TerrainType.NaturalWonder) { - if (terrain.turnsInto == null) - lines.add("Natural Wonder ${terrain.name} is missing the turnsInto attribute!", sourceObject = terrain) + if (terrain.type == TerrainType.NaturalWonder && terrain.turnsInto != null) { val baseTerrain = ruleset.terrains[terrain.turnsInto] if (baseTerrain == null) lines.add("${terrain.name} turns into terrain ${terrain.turnsInto} which does not exist!", sourceObject = terrain) @@ -740,8 +738,8 @@ class RulesetValidator(val ruleset: Ruleset) { if (techColumn.columnNumber < 0) lines.add("Tech Column number ${techColumn.columnNumber} is negative", sourceObject = null) - val buildingsWithoutAssignedCost = ruleset.buildings.values.filter { - it.cost == -1 && techColumn.techs.map { it.name }.contains(it.requiredTech) }.toList() + val buildingsWithoutAssignedCost = ruleset.buildings.values.filter { building -> + building.cost == -1 && techColumn.techs.map { it.name }.contains(building.requiredTech) }.toList() val nonWondersWithoutAssignedCost = buildingsWithoutAssignedCost.filter { !it.isAnyWonder() } diff --git a/docs/Modders/Mod-file-structure/3-Map-related-JSON-files.md b/docs/Modders/Mod-file-structure/3-Map-related-JSON-files.md index c4736b2b7a..b892d1a1aa 100644 --- a/docs/Modders/Mod-file-structure/3-Map-related-JSON-files.md +++ b/docs/Modders/Mod-file-structure/3-Map-related-JSON-files.md @@ -13,7 +13,7 @@ Each terrain entry has the following structure: | name | String | Required | [^A] | | type | Enum | Required | Land, Water, TerrainFeature, NaturalWonder [^B] | | occursOn | List of Strings | none | Only for terrain features and Natural Wonders: The baseTerrain it can be placed on | -| turnsInto | String | none | Only for NaturalWonder: the base terrain is changed to this after placing the Natural Wonder | +| turnsInto | String | none | Only for NaturalWonder: optional mandatory base terrain [^C] | | weight | Integer | 10 | Only for NaturalWonder: _relative_ weight of being picked by the map generator | | [``](#general-stat) | Float | 0 | Per-turn yield or bonus yield for the tile | | overrideStats | Boolean | false | If true, a feature's yields replace any yield from underlying terrain instead of adding to it | @@ -29,6 +29,7 @@ Each terrain entry has the following structure: `River` is hardcoded to be used to look up a [Stats](../../uniques.md#global-uniques) unique to determine the bonuses an actual River provides (remember, rivers live on the edges not as terrain). River should always be a TerrainFeature and have the same uniques the one in the vanilla rulesets has - if you change that, expect surprises. [^B]: A base ruleset mod is always expected to provide at least one Land and at least one Water terrain. We do not support Land-only or Water-only mods, even if they might be possible to pull off. +[^C]: If set, the base terrain is changed to this after placing the Natural Wonder, and terrain features cleared. Otherwise, terrain features are reduced to only those present in occursOn. ## TileImprovements.json diff --git a/docs/Modders/uniques.md b/docs/Modders/uniques.md index 280f178996..dbf4e17af6 100644 --- a/docs/Modders/uniques.md +++ b/docs/Modders/uniques.md @@ -2155,13 +2155,15 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl Applicable to: Terrain -??? example "Neighboring tiles will convert to [baseTerrain]" +??? example "Neighboring tiles will convert to [baseTerrain/terrainFeature]" + Supports conditionals that need only a Tile as context and nothing else, like ``, and applies them per neighbor. Example: "Neighboring tiles will convert to [Grassland]" Applicable to: Terrain -??? example "Neighboring tiles except [baseTerrain] will convert to [baseTerrain]" - Example: "Neighboring tiles except [Grassland] will convert to [Grassland]" +??? example "Neighboring tiles except [simpleTerrain] will convert to [baseTerrain/terrainFeature]" + Supports conditionals that need only a Tile as context and nothing else, like ``, and applies them per neighbor. + Example: "Neighboring tiles except [Elevated] will convert to [Grassland]" Applicable to: Terrain @@ -3304,6 +3306,7 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl *[stats]: For example: `+2 Production, +3 Food`. Note that the stat names need to be capitalized! *[stockpiledResource]: The name of any stockpiled resource. *[tech]: The name of any tech. +*[terrainFeature]: The name of any terrain that is a terrain feature according to the json file. *[tileFilter]: Anything that can be used either in an improvementFilter or in a terrainFilter can be used here, plus 'unimproved' *[unitType]: Can be 'Land', 'Water', 'Air', any unit type, a filtering Unique on a unit type, or a multi-filter of these. *[validationWarning]: Suppresses one specific Ruleset validation warning. This can specify the full text verbatim including correct upper/lower case, or it can be a wildcard case-insensitive simple pattern starting and ending in an asterisk ('*'). If the suppression unique is used within an object or as modifier (not ModOptions), the wildcard symbols can be omitted, as selectivity is better due to the limited scope.