diff --git a/core/src/com/unciv/logic/BarbarianManager.kt b/core/src/com/unciv/logic/BarbarianManager.kt index 68be5a7998..db65fe7416 100644 --- a/core/src/com/unciv/logic/BarbarianManager.kt +++ b/core/src/com/unciv/logic/BarbarianManager.kt @@ -6,6 +6,7 @@ import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileMap import com.unciv.models.metadata.GameSpeed +import com.unciv.models.ruleset.unique.UniqueType import java.util.* import kotlin.collections.HashMap import kotlin.math.max @@ -210,7 +211,7 @@ class Encampment { || it.isCityCenter() || it.getFirstUnit() != null || (it.isWater && !canSpawnBoats) - || (it.hasUnique("Fresh water") && it.isWater) // No Lakes + || (it.hasUnique(UniqueType.FreshWater) && it.isWater) // No Lakes } if (validTiles.isEmpty()) return false diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index 3d4150c00b..e49eaed1d1 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -704,13 +704,13 @@ object Battle { tile.turnsToImprovement = 0 tile.roadStatus = RoadStatus.None if (tile.isLand && !tile.isImpassible() && !tile.terrainFeatures.contains("Fallout")) { - if (tile.terrainFeatures.any { attacker.getCivInfo().gameInfo.ruleSet.terrains[it]!!.uniques.contains("Resistant to nukes") }) { + if (tile.terrainFeatures.any { attacker.getCivInfo().gameInfo.ruleSet.terrains[it]!!.hasUnique(UniqueType.ResistsNukes) }) { if (Random().nextFloat() < 0.25f) { - tile.terrainFeatures.removeAll { attacker.getCivInfo().gameInfo.ruleSet.terrains[it]!!.uniques.contains("Can be destroyed by nukes") } + tile.terrainFeatures.removeAll { attacker.getCivInfo().gameInfo.ruleSet.terrains[it]!!.hasUnique(UniqueType.DestroyableByNukes) } tile.terrainFeatures.add("Fallout") } } else if (Random().nextFloat() < 0.5f) { - tile.terrainFeatures.removeAll { attacker.getCivInfo().gameInfo.ruleSet.terrains[it]!!.uniques.contains("Can be destroyed by nukes") } + tile.terrainFeatures.removeAll { attacker.getCivInfo().gameInfo.ruleSet.terrains[it]!!.hasUnique(UniqueType.DestroyableByNukes) } tile.terrainFeatures.add("Fallout") } } @@ -767,13 +767,13 @@ object Battle { tile.turnsToImprovement = 0 tile.roadStatus = RoadStatus.None if (tile.isLand && !tile.isImpassible() && !tile.terrainFeatures.contains("Fallout")) { - if (tile.terrainFeatures.any { attacker.getCivInfo().gameInfo.ruleSet.terrains[it]!!.uniques.contains("Resistant to nukes") }) { + if (tile.terrainFeatures.any { attacker.getCivInfo().gameInfo.ruleSet.terrains[it]!!.hasUnique(UniqueType.ResistsNukes) }) { if (Random().nextFloat() < 0.25f) { - tile.terrainFeatures.removeAll { attacker.getCivInfo().gameInfo.ruleSet.terrains[it]!!.uniques.contains("Can be destroyed by nukes") } + tile.terrainFeatures.removeAll { attacker.getCivInfo().gameInfo.ruleSet.terrains[it]!!.hasUnique(UniqueType.DestroyableByNukes) } tile.terrainFeatures.add("Fallout") } } else if (Random().nextFloat() < 0.5f) { - tile.terrainFeatures.removeAll { attacker.getCivInfo().gameInfo.ruleSet.terrains[it]!!.uniques.contains("Can be destroyed by nukes") } + tile.terrainFeatures.removeAll { attacker.getCivInfo().gameInfo.ruleSet.terrains[it]!!.hasUnique(UniqueType.DestroyableByNukes) } tile.terrainFeatures.add("Fallout") } } diff --git a/core/src/com/unciv/logic/battle/CityCombatant.kt b/core/src/com/unciv/logic/battle/CityCombatant.kt index 1fad1b63c1..9343d898c7 100644 --- a/core/src/com/unciv/logic/battle/CityCombatant.kt +++ b/core/src/com/unciv/logic/battle/CityCombatant.kt @@ -4,6 +4,7 @@ import com.unciv.logic.city.CityInfo import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.TileInfo import com.unciv.models.UncivSound +import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.UnitType import kotlin.math.pow import kotlin.math.roundToInt @@ -39,9 +40,8 @@ class CityCombatant(val city: CityInfo) : ICombatant { var strength = 8f strength += (city.population.population / 5) * 2 // Each 5 pop gives 2 defence val cityTile = city.getCenterTile() - for (unique in cityTile.getAllTerrains().flatMap { it.uniqueObjects }) - if (unique.placeholderText == "[] Strength for cities built on this terrain") - strength += unique.params[0].toInt() + for (unique in cityTile.getAllTerrains().flatMap { it.getMatchingUniques(UniqueType.GrantsCityStrength) }) + strength += unique.params[0].toInt() // as tech progresses so does city strength val techCount = getCivInfo().gameInfo.ruleSet.technologies.count() val techsPercentKnown: Float = if (techCount > 0) city.civInfo.tech.techsResearched.count().toFloat() / techCount else 0.5f // for mods with no tech diff --git a/core/src/com/unciv/logic/civilization/CityStateFunctions.kt b/core/src/com/unciv/logic/civilization/CityStateFunctions.kt index bb962c3307..5d531ebc1e 100644 --- a/core/src/com/unciv/logic/civilization/CityStateFunctions.kt +++ b/core/src/com/unciv/logic/civilization/CityStateFunctions.kt @@ -294,6 +294,7 @@ class CityStateFunctions(val civInfo: CivilizationInfo) { city.moveToCiv(otherCiv) city.isPuppet = true // Human players get a popup that allows them to annex instead city.foundingCiv = "" // This is no longer a city-state + city.isOriginalCapital = false // It's now an ordinary city and can be razed in later conquests } civInfo.destroy() } diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index d42fca4fb7..2fcf3dc835 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -1,7 +1,6 @@ package com.unciv.logic.civilization import com.badlogic.gdx.math.Vector2 -import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.GameInfo import com.unciv.logic.UncivShowableException @@ -343,7 +342,7 @@ class CivilizationInfo { cities.asSequence().flatMap { city -> if (cityItIsFor != null && city == cityItIsFor) - city.getAllUniquesWithNonLocalEffects().filter { it.params.none { param -> param == "in other cities" } } + city.getAllUniquesWithNonLocalEffects().filter { it.params.none { param -> param == "in other cities" || param == "in all cities" } } else city.getAllUniquesWithNonLocalEffects() } @@ -714,7 +713,7 @@ class CivilizationInfo { passThroughImpassableUnlocked = passableImpassables.isNotEmpty() // Cache whether this civ gets nonstandard terrain damage for performance reasons. - nonStandardTerrainDamage = getMatchingUniques("Units ending their turn on [] tiles take [] damage") + nonStandardTerrainDamage = getMatchingUniques(UniqueType.DamagesContainingUnits) .any { gameInfo.ruleSet.terrains[it.params[0]]!!.damagePerTurn != it.params[1].toInt() } // Cache the last era each resource is used for buildings or units respectively for AI building evaluation diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index c1289c5568..4388693aa7 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -614,9 +614,7 @@ class MapUnit { tile.roadStatus = RoadStatus.None else { val removedFeatureObject = tile.ruleset.terrains[removedFeatureName] - if (removedFeatureObject != null && removedFeatureObject.uniques - .contains("Provides a one-time Production bonus to the closest city when cut down") - ) { + if (removedFeatureObject != null && removedFeatureObject.hasUnique(UniqueType.ProductionBonusWhenRemoved)) { tryProvideProductionToClosestCity(removedFeatureName) } tile.terrainFeatures.remove(removedFeatureName) @@ -1006,7 +1004,7 @@ class MapUnit { fun getDamageFromTerrain(tile: TileInfo = currentTile): Int { if (civInfo.nonStandardTerrainDamage) { - for (unique in getMatchingUniques("Units ending their turn on [] tiles take [] damage")) { + for (unique in getMatchingUniques(UniqueType.DamagesContainingUnits)) { if (unique.params[0] in tile.getAllTerrains().map { it.name }) { return unique.params[1].toInt() // Use the damage from the unique } diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index 88c767f234..3ba18ee778 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -436,7 +436,7 @@ open class TileInfo { && getTileImprovement().let { it != null && it.hasUnique("Irremovable") } -> false // Terrain blocks most improvements - getAllTerrains().any { it.getMatchingUniques("Only [] improvements may be built on this tile") + getAllTerrains().any { it.getMatchingUniques(UniqueType.RestrictedBuildableImprovements) .any { unique -> !improvement.matchesFilter(unique.params[0]) } } -> false // Decide cancelImprovementOrder earlier, otherwise next check breaks it @@ -630,6 +630,17 @@ open class TileInfo { FormattedLine("{$resource} ($resourceAmount)", link="Resource/$resource") else FormattedLine(resource!!, link="Resource/$resource") + if (resource != null && viewingCiv != null && hasViewableResource(viewingCiv)) { + val tileImprovement = ruleset.tileImprovements[getTileResource().improvement] + if (tileImprovement?.techRequired != null + && !viewingCiv.tech.isResearched(tileImprovement.techRequired!!)) { + lineList += FormattedLine( + "Requires [${tileImprovement.techRequired}]", + link="Technology/${tileImprovement.techRequired}", + color= "#FAA" + ) + } + } if (naturalWonder != null) lineList += FormattedLine(naturalWonder!!, link="Terrain/$naturalWonder") if (roadStatus !== RoadStatus.None && !isCityCenter()) diff --git a/core/src/com/unciv/logic/map/TileMap.kt b/core/src/com/unciv/logic/map/TileMap.kt index 16e6e0871a..749d8e7a98 100644 --- a/core/src/com/unciv/logic/map/TileMap.kt +++ b/core/src/com/unciv/logic/map/TileMap.kt @@ -317,14 +317,17 @@ class TileMap { This can all be summed up as "I can see c if a>b || c>b || (a==b && b !blocks same-elevation view)" */ - val containsViewableNeighborThatCanSeeOver = cTile.neighbors.any { - bNeighbor: TileInfo -> + val containsViewableNeighborThatCanSeeOver = cTile.neighbors.any { bNeighbor: TileInfo -> val bNeighborHeight = bNeighbor.height - viewableTiles.contains(bNeighbor) && ( - currentTileHeight > bNeighborHeight // a>b - || cTileHeight > bNeighborHeight // c>b - || currentTileHeight == bNeighborHeight // a==b - && !bNeighbor.hasUnique("Blocks line-of-sight from tiles at same elevation")) + viewableTiles.contains(bNeighbor) + && ( + currentTileHeight > bNeighborHeight // a>b + || cTileHeight > bNeighborHeight // c>b + || ( + currentTileHeight == bNeighborHeight // a==b + && !bNeighbor.hasUnique(UniqueType.BlocksLineOfSightAtSameElevation) + ) + ) } if (containsViewableNeighborThatCanSeeOver) tilesToAddInDistanceI.add(cTile) } diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt index 2a6ee2ce34..28a568744b 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt @@ -9,6 +9,7 @@ import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.tile.Terrain import com.unciv.models.ruleset.tile.TerrainType +import com.unciv.models.ruleset.unique.UniqueType import kotlin.math.abs import kotlin.math.max import kotlin.math.pow @@ -223,9 +224,11 @@ class MapGenerator(val ruleset: Ruleset) { * [MapParameters.elevationExponent] favors high elevation */ private fun raiseMountainsAndHills(tileMap: TileMap) { - val mountain = ruleset.terrains.values.firstOrNull { it.uniques.contains("Occurs in chains at high elevations") }?.name - val hill = ruleset.terrains.values.firstOrNull { it.uniques.contains("Occurs in groups around high elevations") }?.name - val flat = ruleset.terrains.values.firstOrNull { !it.impassable && it.type == TerrainType.Land && !it.uniques.contains("Rough Terrain") }?.name + val mountain = ruleset.terrains.values.firstOrNull { it.hasUnique(UniqueType.OccursInChains) }?.name + val hill = ruleset.terrains.values.firstOrNull { it.hasUnique(UniqueType.OccursInGroups) }?.name + val flat = ruleset.terrains.values.firstOrNull { + !it.impassable && it.type == TerrainType.Land && !it.hasUnique(UniqueType.RoughTerrain) + }?.name if (flat == null) { println("Ruleset seems to contain no flat terrain - can't generate heightmap") @@ -358,12 +361,11 @@ class MapGenerator(val ruleset: Ruleset) { val tempFrom: Float, val tempTo: Float, val humidFrom: Float, val humidTo: Float ) + // List is OK here as it's only sequentially scanned val limitsMap: List = - // List is OK here as it's only sequentially scanned - ruleset.terrains.values.flatMap { terrain -> - terrain.uniqueObjects.filter { - it.placeholderText == "Occurs at temperature between [] and [] and humidity between [] and []" - }.map { unique -> + ruleset.terrains.values.flatMap { terrain -> + terrain.getMatchingUniques(UniqueType.TileGenerationConditions) + .map { unique -> TerrainOccursRange(terrain, unique.params[0].toFloat(), unique.params[1].toFloat(), unique.params[2].toFloat(), unique.params[3].toFloat()) @@ -432,7 +434,7 @@ class MapGenerator(val ruleset: Ruleset) { */ private fun spawnRareFeatures(tileMap: TileMap) { val rareFeatures = ruleset.terrains.values.filter { - it.type == TerrainType.TerrainFeature && it.uniques.contains("Rare feature") + it.type == TerrainType.TerrainFeature && it.hasUnique(UniqueType.RareFeature) } for (tile in tileMap.values.asSequence().filter { it.terrainFeatures.isEmpty() }) { if (randomness.RNG.nextDouble() <= tileMap.mapParameters.rareFeaturesRichness) { diff --git a/core/src/com/unciv/models/ruleset/tile/Terrain.kt b/core/src/com/unciv/models/ruleset/tile/Terrain.kt index 091e8348da..de02bdf421 100644 --- a/core/src/com/unciv/models/ruleset/tile/Terrain.kt +++ b/core/src/com/unciv/models/ruleset/tile/Terrain.kt @@ -40,7 +40,8 @@ class Terrain : RulesetStatsObject() { @Transient var damagePerTurn = 0 - fun isRough(): Boolean = uniques.contains("Rough terrain") + // Shouldn't this just be a lazy property so it's automatically cached? + fun isRough(): Boolean = hasUnique(UniqueType.RoughTerrain) /** Tests base terrains, features and natural wonders whether they should be treated as Land/Water. * Currently only used for civilopedia display, as other code can test the tile itself. @@ -141,8 +142,6 @@ class Terrain : RulesetStatsObject() { } fun setTransients() { - damagePerTurn = uniqueObjects.sumOf { - if (it.placeholderText == "Units ending their turn on this terrain take [] damage") it.params[0].toInt() else 0 - } + damagePerTurn = getMatchingUniques(UniqueType.DamagesContainingUnits).sumOf { it.params[0].toInt() } } } diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index d03eedcae8..dbff14e5c0 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -261,16 +261,33 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget) { // The "Except [terrainFilter]" could theoretically be implemented with a conditional NaturalWonderConvertNeighborsExcept("Neighboring tiles except [baseTerrain] will convert to [baseTerrain]", UniqueTarget.Terrain), + DamagesContainingUnits("Units ending their turn on this terrain take [amount] damage", UniqueTarget.Terrain), TerrainGrantsPromotion("Grants [promotion] ([comment]) to adjacent [mapUnitFilter] units for the rest of the game", UniqueTarget.Terrain), + GrantsCityStrength("[amount] Strength for cities built on this terrain", UniqueTarget.Terrain), + ProductionBonusWhenRemoved("Provides a one-time Production bonus to the closest city when cut down", UniqueTarget.Terrain), TileProvidesYieldWithoutPopulation("Tile provides yield without assigned population", UniqueTarget.Terrain, UniqueTarget.Improvement), NullifyYields("Nullifies all other stats this tile provides", UniqueTarget.Terrain), + RestrictedBuildableImprovements("Only [improvementFilter] improvements may be built on this tile", UniqueTarget.Terrain), + + BlocksLineOfSightAtSameElevation("Blocks line-of-sight from tiles at same elevation", UniqueTarget.Terrain), VisibilityElevation("Has an elevation of [amount] for visibility calculations", UniqueTarget.Terrain), NoNaturalGeneration("Doesn't generate naturally", UniqueTarget.Terrain), - + TileGenerationConditions("Occurs at temperature between [amount] and [amount] and humidity between [amount] and [amount]", UniqueTarget.Terrain), + OccursInChains("Occurs in chains at high elevations", UniqueTarget.Terrain), + OccursInGroups("Occurs in groups around high elevations", UniqueTarget.Terrain), + RareFeature("Rare feature", UniqueTarget.Terrain), + + ResistsNukes("Resistant to nukes", UniqueTarget.Terrain), + DestroyableByNukes("Can be destroyed by nukes", UniqueTarget.Terrain), + + FreshWater("Fresh water", UniqueTarget.Terrain), + RoughTerrain("Rough terrain", UniqueTarget.Terrain), + + // Resource uniques OverrideDepositAmountOnTileFilter("Deposits in [tileFilter] tiles always provide [amount] resources", UniqueTarget.Resource), - + ///////////////////////////////////////// CONDITIONALS /////////////////////////////////////////