From b66443574e4a50481d8af972a190f4902fc3319e Mon Sep 17 00:00:00 2001 From: Xander Lenstra <71121390+xlenstra@users.noreply.github.com> Date: Sun, 24 Oct 2021 19:47:29 +0200 Subject: [PATCH] Enumified all remaining resource & improvement uniques (#5523) * Added conditionals & enumified improvement stat uniques * Enumified all the other uniques * Fixed bug where improvemen stat icons didn't show up in tech tree & reviews --- .../jsons/Civ V - Vanilla/Buildings.json | 4 +- .../Civ V - Vanilla/TileImprovements.json | 37 +++++----- .../jsons/Civ V - Vanilla/TileResources.json | 2 +- core/src/com/unciv/logic/GameStarter.kt | 2 +- core/src/com/unciv/logic/battle/Battle.kt | 4 +- .../com/unciv/logic/city/CityConstructions.kt | 10 +-- .../logic/civilization/CityStateFunctions.kt | 2 +- core/src/com/unciv/logic/map/MapUnit.kt | 13 ++-- core/src/com/unciv/logic/map/TileInfo.kt | 71 +++++++++++-------- core/src/com/unciv/models/ruleset/Nation.kt | 5 +- .../unciv/models/ruleset/tech/Technology.kt | 41 +++++++++-- .../models/ruleset/tile/TileImprovement.kt | 12 +--- .../com/unciv/models/ruleset/unique/Unique.kt | 13 +++- .../ruleset/unique/UniqueParameterType.kt | 11 +++ .../unciv/models/ruleset/unique/UniqueType.kt | 49 ++++++++++--- core/src/com/unciv/models/stats/NamedStats.kt | 4 ++ .../unciv/models/translations/Translations.kt | 2 +- .../unciv/ui/civilopedia/CivilopediaText.kt | 2 +- .../com/unciv/ui/pickerscreens/TechButton.kt | 14 ++-- .../unciv/ui/worldscreen/unit/UnitActions.kt | 2 +- 20 files changed, 195 insertions(+), 105 deletions(-) diff --git a/android/assets/jsons/Civ V - Vanilla/Buildings.json b/android/assets/jsons/Civ V - Vanilla/Buildings.json index b49c1d1cd4..9c52775998 100644 --- a/android/assets/jsons/Civ V - Vanilla/Buildings.json +++ b/android/assets/jsons/Civ V - Vanilla/Buildings.json @@ -390,7 +390,7 @@ "culture": 1, "isWonder": true, "greatPersonPoints": {"Great Engineer": 1}, - "uniques": ["Must be next to [Desert]", "[+1 Food, +1 Production, +1 Gold] from [Desert] tiles without [Flood plains] [in this city]", "Gain a free [Amphitheater] [in this city]", "[+6 Culture] once [Archaeology] is discovered"], + "uniques": ["Must be next to [Desert]", "[+1 Food, +1 Production, +1 Gold] from [Desert] tiles without [Flood plains] [in this city]", "Gain a free [Amphitheater] [in this city]", "[+6 Culture] "], "requiredTech": "Currency", "quote": "'...who drinks the water I shall give him, says the Lord, will have a spring inside him welling up for eternal life. Let them bring me to your holy mountain in the place where you dwell. Across the desert and through the mountain to the Canyon of the Crescent Moon...' - Indiana Jones" }, @@ -603,7 +603,7 @@ "culture": 2, "hurryCostModifier": 25, "requiredBuilding": "Walls", - "uniques": ["[+1 Gold] once [Flight] is discovered", "Destroyed when the city is captured"], + "uniques": ["[+1 Gold] ", "Destroyed when the city is captured"], "requiredTech": "Chivalry" }, { diff --git a/android/assets/jsons/Civ V - Vanilla/TileImprovements.json b/android/assets/jsons/Civ V - Vanilla/TileImprovements.json index 4e4f39b7ba..6a16d9a737 100644 --- a/android/assets/jsons/Civ V - Vanilla/TileImprovements.json +++ b/android/assets/jsons/Civ V - Vanilla/TileImprovements.json @@ -7,8 +7,8 @@ "turnsToBuild": 7, "techRequired": "Agriculture", "uniques": ["Can also be built on tiles adjacent to fresh water", - "[+1 Food] on [Fresh water] tiles once [Civil Service] is discovered", - "[+1 Food] on [non-fresh water] tiles once [Fertilizer] is discovered"], + "[+1 Food] from [Fresh water] tiles ", + "[+1 Food] from [non-fresh water] tiles "], "shortcutKey": "F" }, { @@ -17,7 +17,7 @@ "production": 1, "turnsToBuild": 7, "techRequired": "Construction", - "uniques": ["[+1 Production] once [Scientific Theory] is discovered"], + "uniques": ["[+1 Production] "], "shortcutKey": "L" }, { @@ -26,7 +26,7 @@ "production": 1, "turnsToBuild": 7, "techRequired": "Mining", - "uniques": ["[+1 Production] once [Chemistry] is discovered"], + "uniques": ["[+1 Production] "], "shortcutKey": "M" }, { @@ -35,7 +35,7 @@ "gold": 1, "turnsToBuild": 7, "techRequired": "Guilds", - "uniques": ["[+1 Gold] once [Economics] is discovered"], + "uniques": ["[+1 Gold] "], "shortcutKey": "T" }, @@ -44,7 +44,7 @@ "name": "Camp", "turnsToBuild": 7, "techRequired": "Trapping", - "uniques": ["Does not need removal of [Forest]","Does not need removal of [Jungle]","[+1 Gold] once [Economics] is discovered"], + "uniques": ["Does not need removal of [Forest]","Does not need removal of [Jungle]","[+1 Gold] "], "shortcutKey": "C" }, { @@ -59,7 +59,7 @@ "name": "Pasture", "turnsToBuild": 8, "techRequired": "Animal Husbandry", - "uniques": ["[+1 Food] once [Fertilizer] is discovered"], + "uniques": ["[+1 Food] "], "shortcutKey": "P" }, { @@ -67,14 +67,14 @@ "turnsToBuild": 6, "gold": 1, "techRequired": "Calendar", - "uniques": ["[+1 Food] once [Fertilizer] is discovered"], + "uniques": ["[+1 Food] "], "shortcutKey": "P" }, { "name": "Quarry", "turnsToBuild": 8, "techRequired": "Masonry", - "uniques": ["[+1 Production] once [Chemistry] is discovered"], + "uniques": ["[+1 Production] "], "shortcutKey": "Q" }, { @@ -82,7 +82,7 @@ "terrainsCanBeBuiltOn": ["Coast"], "food": 1, "techRequired": "Sailing", - "uniques": ["[+1 Gold] once [Compass] is discovered"] + "uniques": ["[+1 Gold] "] }, // Military improvement @@ -100,6 +100,7 @@ "name": "Road", "turnsToBuild": 4, "techRequired": "The Wheel", + // "Costs [1] gold per turn when in your territory" does nothing and is only to inform the user "uniques": ["Can be built outside your borders", "Costs [1] gold per turn when in your territory"], "shortcutKey": "R", "civilopediaText": [ @@ -173,7 +174,7 @@ { "name": "Academy", "science": 8, - "uniques": ["Great Improvement", "[+2 Science] once [Scientific Theory] is discovered", "[+2 Science] once [Atomic Theory] is discovered"] + "uniques": ["Great Improvement", "[+2 Science] ", "[+2 Science] "] }, { "name": "Landmark", @@ -183,12 +184,12 @@ { "name": "Manufactory", "production": 4, - "uniques": ["Great Improvement", "[+1 Production] once [Chemistry] is discovered"] + "uniques": ["Great Improvement", "[+1 Production] "] }, { "name": "Customs house", "gold": 4, - "uniques": ["Great Improvement", "[+1 Gold] once [Economics] is discovered"] + "uniques": ["Great Improvement", "[+1 Gold] "] }, { "name": "Holy site", @@ -197,7 +198,7 @@ }, { "name": "Citadel", - "uniques": ["Great Improvement", "Gives a defensive bonus of [100]%", "Deal 30 damage to adjacent enemy units", "Can be built just outside your borders"], + "uniques": ["Great Improvement", "Gives a defensive bonus of [100]%", "Adjacent enemy units ending their turn take [30] damage", "Can be built just outside your borders"], "civilopediaText": [{"text":"Constructing it will take over the tiles around it and assign them to your closest city"}] }, @@ -207,7 +208,7 @@ "uniqueTo": "Polynesia", "culture": 1, "turnsToBuild": 4, - "uniques": ["Can only be built on [Coastal] tiles", "[+1 Culture] for each adjacent [Moai]", "[+1 Gold] once [Flight] is discovered"], + "uniques": ["Can only be built on [Coastal] tiles", "[+1 Culture] for each adjacent [Moai]", "[+1 Gold] "], "techRequired": "Construction", "shortcutKey": "M" }, @@ -219,8 +220,8 @@ "turnsToBuild": 7, "uniques": ["Cannot be built on [Bonus resource] tiles", "[+1 Food] for each adjacent [Mountain]", - "[+1 Food] on [Fresh water] tiles once [Civil Service] is discovered", - "[+1 Food] on [non-fresh water] tiles once [Fertilizer] is discovered"], + "[+1 Food] on [Fresh water] tiles ", + "[+1 Food] on [non-fresh water] tiles "], "techRequired": "Construction", "shortcutKey": "F" }, @@ -230,7 +231,7 @@ "food": 3, "terrainsCanBeBuiltOn": ["Marsh", "Flood plains"], "turnsToBuild": 7, - "uniques": ["[+1 Production, +2 Gold] once [Economics] is discovered"], + "uniques": ["[+1 Production, +2 Gold] "], "techRequired": "Guilds", "shortcutKey": "F" }, diff --git a/android/assets/jsons/Civ V - Vanilla/TileResources.json b/android/assets/jsons/Civ V - Vanilla/TileResources.json index be19954fb6..7c67eaabad 100644 --- a/android/assets/jsons/Civ V - Vanilla/TileResources.json +++ b/android/assets/jsons/Civ V - Vanilla/TileResources.json @@ -324,5 +324,5 @@ "gold": 2, "improvement": "Camp", "improvementStats": {"gold": 1} - }, + } ] diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index 40416ec757..02588b78a9 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -212,7 +212,7 @@ object GameStarter { val allMercantileResources = ruleset.tileResources.values.filter { it.unique == "Can only be created by Mercantile City-States" // Deprecated as of 3.16.16 - || it.uniques.contains("Can only be created by Mercantile City-States") }.map { it.name } + || it.hasUnique(UniqueType.CityStateOnlyResource) }.map { it.name } val unusedMercantileResources = Stack() diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index e6d52ba972..e8d90905e1 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -695,7 +695,7 @@ object Battle { } // Remove improvements, add fallout - if (tile.improvement != null && !tile.getTileImprovement()!!.hasUnique("Indestructible")) { + if (tile.improvement != null && !tile.getTileImprovement()!!.hasUnique(UniqueType.Indestructible)) { tile.improvement = null } tile.improvementInProgress = null @@ -758,7 +758,7 @@ object Battle { } // Remove improvements - if (tile.improvement != null && !tile.getTileImprovement()!!.hasUnique("Indestructible")) { + if (tile.improvement != null && !tile.getTileImprovement()!!.hasUnique(UniqueType.Indestructible)) { tile.improvement = null } tile.improvementInProgress = null diff --git a/core/src/com/unciv/logic/city/CityConstructions.kt b/core/src/com/unciv/logic/city/CityConstructions.kt index dc2f3fb5f8..1eceb82912 100644 --- a/core/src/com/unciv/logic/city/CityConstructions.kt +++ b/core/src/com/unciv/logic/city/CityConstructions.kt @@ -116,10 +116,12 @@ class CityConstructions { stats.add(unique.stats.times(cityInfo.population.population / unique.params[1].toFloat())) } - for (unique in cityInfo.getLocalMatchingUniques("[] once [] is discovered")) { - if (cityInfo.civInfo.tech.isResearched(unique.params[1])) - stats.add(unique.stats) - } + // Deprecated since 3.17.11 + for (unique in cityInfo.getLocalMatchingUniques(UniqueType.StatsWithTech)) { + if (cityInfo.civInfo.tech.isResearched(unique.params[1])) + stats.add(unique.stats) + } + // return stats } diff --git a/core/src/com/unciv/logic/civilization/CityStateFunctions.kt b/core/src/com/unciv/logic/civilization/CityStateFunctions.kt index 5d531ebc1e..a2a6ee7109 100644 --- a/core/src/com/unciv/logic/civilization/CityStateFunctions.kt +++ b/core/src/com/unciv/logic/civilization/CityStateFunctions.kt @@ -28,7 +28,7 @@ class CityStateFunctions(val civInfo: CivilizationInfo) { for (tech in startingTechs) civInfo.tech.techsResearched.add(tech.name) // can't be .addTechnology because the civInfo isn't assigned yet - val allMercantileResources = ruleset.tileResources.values.filter { it.hasUnique("Can only be created by Mercantile City-States") }.map { it.name } + val allMercantileResources = ruleset.tileResources.values.filter { it.hasUnique(UniqueType.CityStateOnlyResource) }.map { it.name } val allPossibleBonuses = HashSet() // We look through these to determine what kind of city state we are var fallback = false for (era in ruleset.eras.values) { diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index f0c960084a..9601af6d65 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -1014,15 +1014,16 @@ class MapUnit { private fun doCitadelDamage() { // Check for Citadel damage - note: 'Damage does not stack with other Citadels' val citadelTile = currentTile.neighbors - .firstOrNull { - it.getOwner() != null && civInfo.isAtWarWith(it.getOwner()!!) && - with(it.getTileImprovement()) { - this != null && this.hasUnique("Deal 30 damage to adjacent enemy units") - } + .filter { + it.getOwner() != null && civInfo.isAtWarWith(it.getOwner()!!) && it.improvement != null + }.maxByOrNull { tile -> + tile.getTileImprovement()!! + .getMatchingUniques(UniqueType.DamagesAdjacentEnemyUnits) + .sumOf { it.params[0].toInt() } } if (citadelTile != null) { - health -= 30 + health -= citadelTile.getTileImprovement()!!.getMatchingUniques(UniqueType.DamagesAdjacentEnemyUnits).sumOf { it.params[0].toInt() } val locations = LocationAction(listOf(citadelTile.position, currentTile.position)) if (health <= 0) { civInfo.addNotification( diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index 40af10c0df..5b649429ee 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -10,6 +10,7 @@ import com.unciv.logic.civilization.PlayerType import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.tile.* +import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.stats.Stats import com.unciv.models.translations.tr import com.unciv.ui.civilopedia.FormattedLine @@ -354,21 +355,31 @@ open class TileInfo { } fun getImprovementStats(improvement: TileImprovement, observingCiv: CivilizationInfo, city: CityInfo?): Stats { - val stats = improvement.clone() // clones the stats of the improvement, not the improvement itself + val stats = improvement.cloneStats() if (hasViewableResource(observingCiv) && tileResource.improvement == improvement.name) stats.add(tileResource.improvementStats!!.clone()) // resource-specific improvement - for (unique in improvement.uniqueObjects) - if (unique.placeholderText == "[] once [] is discovered" && observingCiv.tech.isResearched(unique.params[1])) - stats.add(unique.stats) + // Deprecated since 3.17.10 + for (unique in improvement.getMatchingUniques(UniqueType.StatsWithTech)) { + if (observingCiv.tech.isResearched(unique.params[1])) + stats.add(unique.stats) + } + // + + for (unique in improvement.getMatchingUniques(UniqueType.Stats, StateForConditionals(civInfo = observingCiv, cityInfo = city))) { + stats.add(unique.stats) + } if (city != null) { - val tileUniques = city.getMatchingUniques(UniqueType.StatsFromTiles) + val tileUniques = city.getMatchingUniques(UniqueType.StatsFromTiles, StateForConditionals(civInfo = observingCiv, cityInfo = city)) .filter { city.matchesFilter(it.params[2]) } - val improvementUniques = improvement.uniqueObjects.filter { - it.placeholderText == "[] on [] tiles once [] is discovered" - && observingCiv.tech.isResearched(it.params[2]) - } + val improvementUniques = + // Deprecated since 3.17.10 + improvement.getMatchingUniques(UniqueType.StatsOnTileWithTech) + .filter { observingCiv.tech.isResearched(it.params[2]) } + + // + improvement.getMatchingUniques(UniqueType.ImprovementStatsOnTile, StateForConditionals(civInfo = observingCiv, cityInfo = city)) + for (unique in tileUniques + improvementUniques) { if (improvement.matchesFilter(unique.params[1]) // Freshwater and non-freshwater cannot be moved to matchesUniqueFilter since that creates an endless feedback. @@ -385,15 +396,14 @@ open class TileInfo { } } - for (unique in improvement.uniqueObjects) - if (unique.placeholderText == "[] for each adjacent []") { - val adjacent = unique.params[1] - val numberOfBonuses = neighbors.count { - it.matchesFilter(adjacent, observingCiv) - || it.roadStatus.name == adjacent - } - stats.add(unique.stats.times(numberOfBonuses.toFloat())) + for (unique in improvement.getMatchingUniques(UniqueType.ImprovementStatsForAdjacencies)) { + val adjacent = unique.params[1] + val numberOfBonuses = neighbors.count { + it.matchesFilter(adjacent, observingCiv) + || it.roadStatus.name == adjacent } + stats.add(unique.stats.times(numberOfBonuses.toFloat())) + } for (unique in observingCiv.getMatchingUniques("+[]% yield from every []")) if (improvement.matchesFilter(unique.params[1])) @@ -408,16 +418,16 @@ open class TileInfo { improvement.uniqueTo != null && improvement.uniqueTo != civInfo.civName -> false improvement.techRequired != null && !civInfo.tech.isResearched(improvement.techRequired!!) -> false getOwner() != civInfo && !( - improvement.hasUnique("Can be built outside your borders") - // citadel can be built only next to or within own borders - || improvement.hasUnique("Can be built just outside your borders") - && neighbors.any { it.getOwner() == civInfo } && civInfo.cities.isNotEmpty() - ) -> false + improvement.hasUnique(UniqueType.CanBuildOutsideBorders) + || ( // citadel can be built only next to or within own borders + improvement.hasUnique(UniqueType.CanBuildJustOutsideBorders) + && neighbors.any { it.getOwner() == civInfo } && civInfo.cities.isNotEmpty() + ) + ) -> false improvement.uniqueObjects.any { it.placeholderText == "Obsolete with []" && civInfo.tech.isResearched(it.params[0]) } -> return false - improvement.uniqueObjects.any { - it.placeholderText == "Cannot be built on [] tiles until [] is discovered" && + improvement.getMatchingUniques(UniqueType.RequiresTechToBuildOnTile).any { matchesTerrainFilter(it.params[0]) && !civInfo.tech.isResearched(it.params[1]) } -> false improvement.uniqueObjects.any { @@ -437,8 +447,8 @@ open class TileInfo { return when { improvement.name == this.improvement -> false isCityCenter() -> false - improvement.uniqueObjects.filter { it.placeholderText == "Cannot be built on [] tiles" }.any { - unique -> matchesTerrainFilter(unique.params[0]) + improvement.getMatchingUniques(UniqueType.CannotBuildOnTile).any { + unique -> matchesTerrainFilter(unique.params[0]) } -> false // Road improvements can change on tiles with irremovable improvements - nothing else can, though. @@ -463,7 +473,7 @@ open class TileInfo { improvement.name == roadStatus.removeAction -> true topTerrain.unbuildable && !improvement.isAllowedOnFeature(topTerrain.name) -> false // DO NOT reverse this &&. isAdjacentToFreshwater() is a lazy which calls a function, and reversing it breaks the tests. - improvement.hasUnique("Can also be built on tiles adjacent to fresh water") && isAdjacentToFreshwater -> true + improvement.hasUnique(UniqueType.ImprovementBuildableByFreshWater) && isAdjacentToFreshwater -> true // If an unique of this type exists, we want all to match (e.g. Hill _and_ Forest would be meaningful). improvement.uniqueObjects.filter { it.placeholderText == "Can only be built on [] tiles" }.let { @@ -544,9 +554,8 @@ open class TileInfo { var bonus = getLastTerrain().defenceBonus val tileImprovement = getTileImprovement() if (tileImprovement != null) { - for (unique in tileImprovement.uniqueObjects) - if (unique.placeholderText == "Gives a defensive bonus of []%") - bonus += unique.params[0].toFloat() / 100 + for (unique in tileImprovement.getMatchingUniques(UniqueType.DefensiveBonus)) + bonus += unique.params[0].toFloat() / 100 } return bonus } @@ -761,7 +770,7 @@ open class TileInfo { if (newResource.resourceType != ResourceType.Strategic) return - for (unique in newResource.getMatchingUniques(UniqueType.OverrideDepositAmountOnTileFilter)) { + for (unique in newResource.getMatchingUniques(UniqueType.ResourceAmountOnTiles)) { if (matchesTerrainFilter(unique.params[0])) { resourceAmount = unique.params[1].toInt() return diff --git a/core/src/com/unciv/models/ruleset/Nation.kt b/core/src/com/unciv/models/ruleset/Nation.kt index cfd30c576a..51132aaf63 100644 --- a/core/src/com/unciv/models/ruleset/Nation.kt +++ b/core/src/com/unciv/models/ruleset/Nation.kt @@ -6,11 +6,10 @@ import com.unciv.UncivGame import com.unciv.logic.civilization.CityStateType import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueTarget -import com.unciv.models.stats.INamed +import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.translations.squareBraceRegex import com.unciv.models.translations.tr import com.unciv.ui.civilopedia.FormattedLine -import com.unciv.ui.civilopedia.ICivilopediaText import com.unciv.ui.utils.Fonts import com.unciv.ui.utils.colorFromRGB @@ -177,7 +176,7 @@ class Nation : RulesetObject() { if (showResources) { val allMercantileResources = ruleset.tileResources.values .filter { it.unique == "Can only be created by Mercantile City-States" // Deprecated 3.16.16 - || it.uniques.contains("Can only be created by Mercantile City-States") } + || it.hasUnique(UniqueType.CityStateOnlyResource) } if (allMercantileResources.isNotEmpty()) { textList += FormattedLine() diff --git a/core/src/com/unciv/models/ruleset/tech/Technology.kt b/core/src/com/unciv/models/ruleset/tech/Technology.kt index ec8f8f7336..f7a0f08096 100644 --- a/core/src/com/unciv/models/ruleset/tech/Technology.kt +++ b/core/src/com/unciv/models/ruleset/tech/Technology.kt @@ -36,13 +36,26 @@ class Technology: RulesetObject() { val lineList = ArrayList() // more readable than StringBuilder, with same performance for our use-case for (unique in uniques) lineList += unique.tr() - for (improvement in ruleset.tileImprovements.values) + for (improvement in ruleset.tileImprovements.values) { for (unique in improvement.uniqueObjects) { - if (unique.placeholderText == "[] once [] is discovered" && unique.params.last() == name) + // Deprecated since 3.17.10 + if (unique.isOfType(UniqueType.StatsWithTech) && unique.params.last() == name) + lineList += "[${unique.params[0]}] from every [${improvement.name}]" + else if (unique.isOfType(UniqueType.StatsOnTileWithTech) && unique.params.last() == name) + lineList += "[${unique.params[0]}] from every [${improvement.name}] on [${unique.params[1]}] tiles" + else + // + if (unique.isOfType(UniqueType.Stats)) { + val requiredTech = unique.conditionals.firstOrNull { it.isOfType(UniqueType.ConditionalTech) }?.params?.get(0) + if (requiredTech != name) continue lineList += "[${unique.params[0]}] from every [${improvement.name}]" - else if (unique.placeholderText == "[] on [] tiles once [] is discovered" && unique.params.last() == name) + } else if (unique.isOfType(UniqueType.ImprovementStatsOnTile)) { + val requiredTech = unique.conditionals.firstOrNull { it.isOfType(UniqueType.ConditionalTech) }?.params?.get(0) + if (requiredTech != name) continue lineList += "[${unique.params[0]}] from every [${improvement.name}] on [${unique.params[1]}] tiles" + } } + } val viewingCiv = UncivGame.Current.worldScreen.viewingCiv val enabledUnits = getEnabledUnits(viewingCiv) @@ -176,12 +189,26 @@ class Technology: RulesetObject() { var wantEmpty = true for (improvement in ruleset.tileImprovements.values) for (unique in improvement.uniqueObjects) { - if (unique.placeholderText == "[] once [] is discovered" && unique.params.last() == name) { - if (wantEmpty) { lineList += FormattedLine(); wantEmpty = false } + // Deprecated since 3.17.10 + if (unique.isOfType(UniqueType.StatsWithTech) && unique.params.last() == name) { + if (wantEmpty) { lineList += FormattedLine(); wantEmpty = false } + lineList += FormattedLine("[${unique.params[0]}] from every [${improvement.name}]", + link = improvement.makeLink()) + } else if (unique.isOfType(UniqueType.StatsOnTileWithTech) && unique.params.last() == name) { + if (wantEmpty) { lineList += FormattedLine(); wantEmpty = false } + lineList += FormattedLine("[${unique.params[0]}] from every [${improvement.name}] on [${unique.params[1]}] tiles", + link = improvement.makeLink()) + } + else + // + if (unique.isOfType(UniqueType.Stats)) { + val requiredTech = unique.conditionals.firstOrNull { it.isOfType(UniqueType.ConditionalTech) }?.params?.get(0) + if (requiredTech != name) continue lineList += FormattedLine("[${unique.params[0]}] from every [${improvement.name}]", link = improvement.makeLink()) - } else if (unique.placeholderText == "[] on [] tiles once [] is discovered" && unique.params.last() == name) { - if (wantEmpty) { lineList += FormattedLine(); wantEmpty = false } + } else if (unique.placeholderText == "[] on [] tiles") { + val requiredTech = unique.conditionals.firstOrNull { it.isOfType(UniqueType.ConditionalTech) }?.params?.get(0) + if (requiredTech != name) continue lineList += FormattedLine("[${unique.params[0]}] from every [${improvement.name}] on [${unique.params[1]}] tiles", link = improvement.makeLink()) } diff --git a/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt b/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt index bbc0b96bac..33a7bd6d43 100644 --- a/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt +++ b/core/src/com/unciv/models/ruleset/tile/TileImprovement.kt @@ -1,13 +1,11 @@ package com.unciv.models.ruleset.tile -import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.RoadStatus import com.unciv.models.ruleset.Belief import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetStatsObject -import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.translations.tr @@ -67,9 +65,9 @@ class TileImprovement : RulesetStatsObject() { return lines.joinToString("\n") } - fun isGreatImprovement() = hasUnique("Great Improvement") + fun isGreatImprovement() = hasUnique(UniqueType.GreatImprovement) fun isRoad() = RoadStatus.values().any { it != RoadStatus.None && it.name == this.name } - fun isAncientRuinsEquivalent() = hasUnique("Provides a random bonus when entered") + fun isAncientRuinsEquivalent() = hasUnique(UniqueType.IsAncientRuinsEquivalent) /** * Check: Is this improvement allowed on a [given][name] terrain feature? @@ -81,11 +79,7 @@ class TileImprovement : RulesetStatsObject() { * so this check is done in conjunction - for the user, success means he does not need to remove * a terrain feature, thus the unique name. */ - fun isAllowedOnFeature(name: String): Boolean { - return uniqueObjects.filter { it.placeholderText == "Does not need removal of []" - && it.params[0] == name - }.any() - } + fun isAllowedOnFeature(name: String) = getMatchingUniques(UniqueType.NoFeatureRemovalNeeded).any { it.params[0] == name } fun matchesFilter(filter: String): Boolean { return when (filter) { diff --git a/core/src/com/unciv/models/ruleset/unique/Unique.kt b/core/src/com/unciv/models/ruleset/unique/Unique.kt index ba4f893deb..266ebf7d1a 100644 --- a/core/src/com/unciv/models/ruleset/unique/Unique.kt +++ b/core/src/com/unciv/models/ruleset/unique/Unique.kt @@ -5,7 +5,6 @@ import com.unciv.logic.city.CityInfo import com.unciv.models.stats.Stats import com.unciv.models.translations.* import com.unciv.logic.civilization.CivilizationInfo -import com.unciv.models.ruleset.Ruleset class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val sourceObjectName: String? = null) { @@ -22,6 +21,8 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s } val conditionals: List = text.getConditionals() + val allParams = params + conditionals.flatMap { it.params } + fun isOfType(uniqueType: UniqueType) = uniqueType == type fun conditionalsApply(civInfo: CivilizationInfo? = null, city: CityInfo? = null): Boolean { @@ -53,7 +54,15 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s state.civInfo != null && state.civInfo.getEraNumber() >= state.civInfo.gameInfo.ruleSet.eras[condition.params[0]]!!.eraNumber UniqueType.ConditionalDuringEra -> state.civInfo != null && state.civInfo.getEraNumber() == state.civInfo.gameInfo.ruleSet.eras[condition.params[0]]!!.eraNumber - + UniqueType.ConditionalTech -> + state.civInfo != null && state.civInfo.tech.isResearched(condition.params[0]) + UniqueType.ConditionalNoTech -> + state.civInfo != null && !state.civInfo.tech.isResearched(condition.params[0]) + UniqueType.ConditionalPolicy -> + state.civInfo != null && state.civInfo.policies.isAdopted(condition.params[0]) + UniqueType.ConditionalNoPolicy -> + state.civInfo != null && !state.civInfo.policies.isAdopted(condition.params[0]) + UniqueType.ConditionalSpecialistCount -> state.cityInfo != null && state.cityInfo.population.getNumberOfSpecialists() >= condition.params[0].toInt() diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt index a115b74053..a4353788f7 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt @@ -222,6 +222,17 @@ enum class UniqueParameterType(val parameterName:String) { else -> UniqueType.UniqueComplianceErrorSeverity.RulesetSpecific } }, + Policy("policy") { + override fun getErrorSeverity( + parameterText: String, + ruleset: Ruleset + ): UniqueType.UniqueComplianceErrorSeverity? { + return when (parameterText) { + in ruleset.policies -> null + else -> UniqueType.UniqueComplianceErrorSeverity.RulesetSpecific + } + } + }, /** Behaves like [Unknown], but states explicitly the parameter is OK and its contents are ignored */ Comment("comment") { override fun getErrorSeverity(parameterText: String, ruleset: Ruleset): diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index e1f69051ac..fcd6c70a39 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -62,7 +62,7 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget) { /////// Stat providing uniques - Stats("[stats]", UniqueTarget.Global, UniqueTarget.FollowerBelief), + Stats("[stats]", UniqueTarget.Global, UniqueTarget.FollowerBelief, UniqueTarget.Improvement), StatsPerCity("[stats] [cityFilter]", UniqueTarget.Global), @Deprecated("As of 3.16.16 - removed as of 3.17.11", ReplaceWith("[stats] "), DeprecationLevel.ERROR) StatBonusForNumberOfSpecialists("[stats] if this city has at least [amount] specialists", UniqueTarget.Global), @@ -175,7 +175,6 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget) { RequiresAnotherBuilding("Requires a [buildingName] in this city", UniqueTarget.Building), - ///////////////////////////////////////// UNIT UNIQUES ///////////////////////////////////////// FoundCity("Founds a new city", UniqueTarget.Unit), @@ -306,13 +305,41 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget) { FreshWater("Fresh water", UniqueTarget.Terrain), RoughTerrain("Rough terrain", UniqueTarget.Terrain), - // Resource uniques - OverrideDepositAmountOnTileFilter("Deposits in [tileFilter] tiles always provide [amount] resources", UniqueTarget.Resource), + /////// Resource uniques + ResourceAmountOnTiles("Deposits in [tileFilter] tiles always provide [amount] resources", UniqueTarget.Resource), + CityStateOnlyResource("Can only be created by Mercantile City-States", UniqueTarget.Resource), + ////// Improvement uniques + ImprovementBuildableByFreshWater("Can also be built on tiles adjacent to fresh water", UniqueTarget.Improvement), + ImprovementStatsOnTile("[stats] from [tileFilter] tiles", UniqueTarget.Improvement), + @Deprecated("As of 3.17.10", ReplaceWith("[stats] from [tileFilter] tiles "), DeprecationLevel.WARNING) + StatsOnTileWithTech("[stats] on [tileFilter] tiles once [tech] is discovered", UniqueTarget.Improvement), + @Deprecated("As of 3.17.10", ReplaceWith("[stats] "), DeprecationLevel.WARNING) + StatsWithTech("[stats] once [tech] is discovered", UniqueTarget.Improvement, UniqueTarget.Building), + ImprovementStatsForAdjacencies("[stats] for each adjacent [tileFilter]", UniqueTarget.Improvement), + + CanBuildOutsideBorders("Can be built outside your borders", UniqueTarget.Improvement), + CanBuildJustOutsideBorders("Can be built just outside your borders", UniqueTarget.Improvement), + RequiresTechToBuildOnTile("Cannot be built on [tileFilter] tiles until [tech] is discovered", UniqueTarget.Improvement), + CannotBuildOnTile("Cannot be built on [tileFilter] tiles", UniqueTarget.Improvement), + NoFeatureRemovalNeeded("Does not need removal of [tileFilter]", UniqueTarget.Improvement), + + DefensiveBonus("Gives a defensive bonus of [amount]%", UniqueTarget.Improvement), + ImprovementMaintenance("Costs [amount] gold per turn when in your territory", UniqueTarget.Improvement), // Unused + DamagesAdjacentEnemyUnits("Deal [amount] damage to adjacent enemy units", UniqueTarget.Improvement), + @Deprecated("As of 3.17.10", ReplaceWith("Adjacent enemy units ending their turn take [30] damage"), DeprecationLevel.WARNING) + DamagesAdjacentEnemyUnitsForExactlyThirtyDamage("Deal 30 damage to adjacent enemy units", UniqueTarget.Improvement), + + GreatImprovement("Great Improvement", UniqueTarget.Improvement), + IsAncientRuinsEquivalent("Provides a random bonus when entered", UniqueTarget.Improvement), + + Unpillagable("Unpillagable", UniqueTarget.Improvement), + Indestructible("Indestructible", UniqueTarget.Improvement), + ///////////////////////////////////////// CONDITIONALS ///////////////////////////////////////// - // civ conditionals + /////// civ conditionals ConditionalWar("when at war", UniqueTarget.Conditional), ConditionalNotWar("when not at war", UniqueTarget.Conditional), ConditionalHappy("while the empire is happy", UniqueTarget.Conditional), @@ -321,11 +348,16 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget) { ConditionalDuringEra("during the [era]", UniqueTarget.Conditional), ConditionalBeforeEra("before the [era]", UniqueTarget.Conditional), ConditionalStartingFromEra("starting from the [era]", UniqueTarget.Conditional), + + ConditionalTech("after discovering [tech]", UniqueTarget.Conditional), + ConditionalNoTech("before discovering [tech]", UniqueTarget.Conditional), + ConditionalPolicy("after adopting [policy]", UniqueTarget.Conditional), + ConditionalNoPolicy("before adopting [policy]", UniqueTarget.Conditional), - // city conditionals + /////// city conditionals ConditionalSpecialistCount("if this city has at least [amount] specialists", UniqueTarget.Conditional), - // unit conditionals + /////// unit conditionals ConditionalOurUnit("for [mapUnitFilter] units", UniqueTarget.Conditional), ConditionalVsCity("vs cities", UniqueTarget.Conditional), ConditionalVsUnits("vs [mapUnitFilter] units", UniqueTarget.Conditional), @@ -333,9 +365,8 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget) { ConditionalAttacking("when attacking", UniqueTarget.Conditional), ConditionalDefending("when defending", UniqueTarget.Conditional), ConditionalInTiles("when fighting in [tileFilter] tiles", UniqueTarget.Conditional), -// ConditionalIntercepting("when intercepting", UniqueTarget.Conditional), - // tile conditionals + /////// tile conditionals ConditionalNeighborTiles("with [amount] to [amount] neighboring [tileFilter] tiles", UniqueTarget.Conditional), ConditionalNeighborTilesAnd("with [amount] to [amount] neighboring [tileFilter] [tileFilter] tiles", UniqueTarget.Conditional), diff --git a/core/src/com/unciv/models/stats/NamedStats.kt b/core/src/com/unciv/models/stats/NamedStats.kt index ccd7b5f5d5..af9111cbde 100644 --- a/core/src/com/unciv/models/stats/NamedStats.kt +++ b/core/src/com/unciv/models/stats/NamedStats.kt @@ -7,4 +7,8 @@ open class NamedStats : Stats(), INamed { override fun toString(): String { return name } + + fun cloneStats(): Stats { + return clone() + } } diff --git a/core/src/com/unciv/models/translations/Translations.kt b/core/src/com/unciv/models/translations/Translations.kt index a2381c2ba7..7a6b6c7348 100644 --- a/core/src/com/unciv/models/translations/Translations.kt +++ b/core/src/com/unciv/models/translations/Translations.kt @@ -213,7 +213,7 @@ class Translations : LinkedHashMap(){ // Whenever this string is changed, it should also be changed in the translation files! // It is mostly used as the template for translating the order of conditionals const val englishConditionalOrderingString = - " " + " " const val conditionalUniqueOrderString = "ConditionalsPlacement" const val shouldCapitalizeString = "StartWithCapitalLetter" } diff --git a/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt b/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt index 1b319d189d..914192cd0c 100644 --- a/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt +++ b/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt @@ -154,7 +154,7 @@ class FormattedLine ( val ruleSet = getCurrentRuleset() if (allObjectNamesCategoryMap == null || rulesetCachedInNameMap !== ruleSet) allObjectNamesCategoryMap = initNamesCategoryMap(ruleSet) - for (parameter in unique.params) { + for (parameter in unique.params + unique.conditionals.flatMap { it.params }) { val category = allObjectNamesCategoryMap!![parameter] ?: continue return category.name + "/" + parameter } diff --git a/core/src/com/unciv/ui/pickerscreens/TechButton.kt b/core/src/com/unciv/ui/pickerscreens/TechButton.kt index f437b9214e..1ddf01a90e 100644 --- a/core/src/com/unciv/ui/pickerscreens/TechButton.kt +++ b/core/src/com/unciv/ui/pickerscreens/TechButton.kt @@ -65,12 +65,14 @@ class TechButton(techName:String, private val techManager: TechManager, isWorldS }) for (improvement in ruleset.tileImprovements.values - .filter { - it.techRequired == techName || it.uniqueObjects.any { u -> u.params.contains(techName) } - || it.uniqueObjects.any { it.placeholderText == "[] once [] is discovered" && it.params[1] == techName } - } - .filter { it.uniqueTo == null || it.uniqueTo == civName }) + .filter { + it.techRequired == techName + || it.uniqueObjects.any { u -> u.allParams.contains(techName) } + } + .filter { it.uniqueTo == null || it.uniqueTo == civName } + ) { techEnabledIcons.add(ImageGetter.getImprovementIcon(improvement.name, techIconSize)) + } for (resource in ruleset.tileResources.values.filter { it.revealedBy == techName }) @@ -78,7 +80,7 @@ class TechButton(techName:String, private val techManager: TechManager, isWorldS for (unique in tech.uniques) techEnabledIcons.add(ImageGetter.getImage("OtherIcons/Star") - .apply { color = Color.BLACK }.surroundWithCircle(techIconSize)) + .apply { color = Color.BLACK }.surroundWithCircle(techIconSize)) if (isWorldScreen) rightSide.add(techEnabledIcons) else rightSide.add(techEnabledIcons) diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt index e81f2480b1..7851871d18 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt @@ -748,7 +748,7 @@ object UnitActions { fun canPillage(unit: MapUnit, tile: TileInfo): Boolean { val tileImprovement = tile.getTileImprovement() // City ruins, Ancient Ruins, Barbarian Camp, City Center marked in json - if (tileImprovement == null || tileImprovement.hasUnique("Unpillagable")) return false + if (tileImprovement == null || tileImprovement.hasUnique(UniqueType.Unpillagable)) return false val tileOwner = tile.getOwner() // Can't pillage friendly tiles, just like you can't attack them - it's an 'act of war' thing return tileOwner == null || unit.civInfo.isAtWarWith(tileOwner)