mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-20 12:48:56 +07:00
Enhance modding freedom for Natural Wonders (#12062)
* Allow omitting `turnsInto` on Natural Wonders * Support a wider range of parameters on NaturalWonderConvertNeighbors* * Extend docDescription * Good suggestion Co-authored-by: Yair Morgenstern <yairm210@hotmail.com> --------- Co-authored-by: Yair Morgenstern <yairm210@hotmail.com>
This commit is contained in:
@ -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<Tile>()
|
||||
|
||||
/*
|
||||
@ -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<String> = 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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -25,7 +25,9 @@ class Terrain : RulesetStatsObject() {
|
||||
/** For terrain features */
|
||||
val occursOn = ArrayList<String>()
|
||||
|
||||
/** 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
|
||||
|
@ -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<String> =
|
||||
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) */
|
||||
|
@ -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 `<with [n]% chance>`, 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 `<with [n]% chance>`, 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
|
||||
|
@ -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() }
|
||||
|
@ -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 |
|
||||
| [`<stats>`](#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
|
||||
|
||||
|
@ -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 `<with [n]% chance>`, 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 `<with [n]% chance>`, 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.
|
||||
|
Reference in New Issue
Block a user