From e72dcc8b0dd6cac78fee1d0c416023dd5712a790 Mon Sep 17 00:00:00 2001 From: Yair Morgenstern Date: Sat, 12 Feb 2022 19:03:30 +0200 Subject: [PATCH] Unified "X is only available under Y conditions" into a single unique (#6133) * Unified "X is only available under Y conditions" into a single unique There were a few problems with existing uniques - they weren't really composable, the offered things they didn't keep, etc For example, "Incompatible with [policy/tech/promotion]", UniqueTarget.Policy, UniqueTarget.Tech, UniqueTarget.Promotion. In fact, promotions only checked promotion incompatibility, promotions - promotion incompat, etc Additionally, with a few more changes, this could cover several other uniques - "Hidden until [amount] social policy branches have been completed", "Requires at least [amount] population", perhaps others I have to say I think conditionals are the best thing ever and they make amazing composability possible :) * Autoupdate correctly recognizes parameters Updated ruleset jsons * Deprecation texts should be allowed to forward to other deprecated uniques so we only need to change the leaves when introducing new uniques, not go through the whole tree --- .../jsons/Civ V - Gods & Kings/Policies.json | 12 ++++----- .../jsons/Civ V - Vanilla/Policies.json | 13 +++++---- .../unciv/logic/civilization/PolicyManager.kt | 3 +++ .../unciv/logic/civilization/TechManager.kt | 1 + core/src/com/unciv/logic/map/TileInfo.kt | 2 ++ .../src/com/unciv/logic/map/UnitPromotions.kt | 7 ++++- core/src/com/unciv/models/ruleset/Building.kt | 4 +++ .../com/unciv/models/ruleset/unique/Unique.kt | 5 ++-- .../ruleset/unique/UniqueTriggerActivation.kt | 2 +- .../unciv/models/ruleset/unique/UniqueType.kt | 12 +++++++-- .../com/unciv/models/ruleset/unit/BaseUnit.kt | 6 +++++ .../ui/pickerscreens/TechPickerScreen.kt | 9 ++++++- docs/uniques.md | 27 +++++++++++-------- tests/src/com/unciv/testing/BasicTests.kt | 4 +-- 14 files changed, 74 insertions(+), 33 deletions(-) diff --git a/android/assets/jsons/Civ V - Gods & Kings/Policies.json b/android/assets/jsons/Civ V - Gods & Kings/Policies.json index 31b6edc79b..c927fe578f 100644 --- a/android/assets/jsons/Civ V - Gods & Kings/Policies.json +++ b/android/assets/jsons/Civ V - Gods & Kings/Policies.json @@ -135,8 +135,8 @@ },{ "name": "Piety", "era": "Classical era", - "uniques": ["[+100]% Production when constructing [Shrine] buildings [in all cities]", "[+100]% Production when constructing [Temple] buildings [in all cities]", - "Incompatible with [Rationalism]"], + "uniques": ["[+100]% Production when constructing [Shrine] buildings [in all cities]", "[+100]% Production when constructing [Temple] buildings [in all cities]", + "Only available "], "policies": [ { "name": "Organized Religion", @@ -276,7 +276,7 @@ { "name": "Rationalism", "era": "Renaissance era", - "uniques": ["[+15]% [Science] ", "Incompatible with [Piety]"], + "uniques": ["[+15]% [Science] ", "Only available "], "policies": [ { "name": "Secularism", @@ -323,7 +323,7 @@ { "name": "Freedom", "era": "Renaissance era", - "uniques": ["[+25]% great person generation [in all cities]", "Incompatible with [Autocracy]", "Incompatible with [Order]"], + "uniques": ["[+25]% great person generation [in all cities]", "Only available ", "Only available "], "policies": [ { "name": "Constitution", @@ -369,7 +369,7 @@ "name": "Autocracy", "era": "Industrial era", "uniques": ["[-33]% maintenance costs ", "Upon capturing a city, receive [10] times its [Culture] production as [Culture] immediately", - "Incompatible with [Order]", "Incompatible with [Freedom]"], + "Only available ", "Only available "], "policies": [ { "name": "Populism", @@ -418,7 +418,7 @@ { "name": "Order", "era": "Industrial era", - "uniques": ["[+1 Happiness] [in all cities]", "Incompatible with [Autocracy]", "Incompatible with [Freedom]"], + "uniques": ["[+1 Happiness] [in all cities]", "Only available ", "Only available "], "policies": [ { "name": "United Front", diff --git a/android/assets/jsons/Civ V - Vanilla/Policies.json b/android/assets/jsons/Civ V - Vanilla/Policies.json index ee445ecc8a..c4726007f5 100644 --- a/android/assets/jsons/Civ V - Vanilla/Policies.json +++ b/android/assets/jsons/Civ V - Vanilla/Policies.json @@ -136,7 +136,7 @@ },{ "name": "Piety", "era": "Classical era", - "uniques": ["[+15]% Production when constructing [Culture] buildings [in all cities]", "Incompatible with [Rationalism]"], + "uniques": ["[+15]% Production when constructing [Culture] buildings [in all cities]", "Only available "], "policies": [ { "name": "Organized Religion", @@ -231,8 +231,7 @@ { "name": "Naval Tradition", "uniques": ["[+1] Movement ", "[+1] Sight ", - "Free [Great General] appears", "[+2] Movement " - // ToDo: Should be "Free [Great Admiral] appears" + "Free [Great General] appears" //, "[+2] Movement ", "Free [Great Admiral] appears" - todo ], "row": 1, "column": 2 @@ -274,7 +273,7 @@ { "name": "Rationalism", "era": "Renaissance era", - "uniques": ["Science gained from research agreements [+50]%", "Incompatible with [Piety]"], + "uniques": ["Science gained from research agreements [+50]%", "Only available "], "policies": [ { "name": "Secularism", @@ -319,7 +318,7 @@ { "name": "Freedom", "era": "Renaissance era", - "uniques": ["[+25]% great person generation [in all cities]", "Incompatible with [Autocracy]", "Incompatible with [Order]"], + "uniques": ["[+25]% great person generation [in all cities]", "Only available ", "Only available "], "policies": [ { "name": "Constitution", @@ -363,7 +362,7 @@ "name": "Autocracy", "era": "Industrial era", "uniques": ["[-33]% maintenance costs ", "Upon capturing a city, receive [10] times its [Culture] production as [Culture] immediately", - "Incompatible with [Order]", "Incompatible with [Freedom]"], + "Only available ", "Only available "], "policies": [ { "name": "Populism", @@ -407,7 +406,7 @@ { "name": "Order", "era": "Industrial era", - "uniques": ["[+1 Happiness] [in all cities]", "Incompatible with [Autocracy]", "Incompatible with [Freedom]"], + "uniques": ["[+1 Happiness] [in all cities]", "Only available ", "Only available "], "policies": [ { "name": "United Front", diff --git a/core/src/com/unciv/logic/civilization/PolicyManager.kt b/core/src/com/unciv/logic/civilization/PolicyManager.kt index 434ee5d5c7..a81497af6c 100644 --- a/core/src/com/unciv/logic/civilization/PolicyManager.kt +++ b/core/src/com/unciv/logic/civilization/PolicyManager.kt @@ -3,6 +3,7 @@ package com.unciv.logic.civilization import com.unciv.logic.map.MapSize import com.unciv.models.ruleset.Policy import com.unciv.models.ruleset.Policy.PolicyBranchType +import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.UniqueMap import com.unciv.models.ruleset.unique.UniqueTriggerActivation import com.unciv.models.ruleset.unique.UniqueType @@ -118,6 +119,8 @@ class PolicyManager { if (!getAdoptedPolicies().containsAll(policy.requires!!)) return false if (checkEra && civInfo.gameInfo.ruleSet.eras[policy.branch.era]!!.eraNumber > civInfo.getEraNumber()) return false if (policy.getMatchingUniques(UniqueType.IncompatibleWith).any { adoptedPolicies.contains(it.params[0]) }) return false + if (policy.uniqueObjects.filter { it.type == UniqueType.OnlyAvailableWhen } + .any { !it.conditionalsApply(civInfo) }) return false return true } diff --git a/core/src/com/unciv/logic/civilization/TechManager.kt b/core/src/com/unciv/logic/civilization/TechManager.kt index ab6e051d38..70e214294e 100644 --- a/core/src/com/unciv/logic/civilization/TechManager.kt +++ b/core/src/com/unciv/logic/civilization/TechManager.kt @@ -132,6 +132,7 @@ class TechManager { fun canBeResearched(techName: String): Boolean { val tech = getRuleset().technologies[techName]!! + if (tech.uniqueObjects.any { it.type == UniqueType.OnlyAvailableWhen && !it.conditionalsApply(civInfo) }) if (tech.getMatchingUniques(UniqueType.IncompatibleWith).any { isResearched(it.params[0]) }) return false if (isResearched(tech.name) && !tech.isContinuallyResearchable()) diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index b19b8fe188..76d3d6e90a 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -454,6 +454,8 @@ open class TileInfo { && neighbors.any { it.getOwner() == civInfo } && civInfo.cities.isNotEmpty() ) ) -> false + improvement.uniqueObjects.filter { it.type == UniqueType.OnlyAvailableWhen } + .any { !it.conditionalsApply(StateForConditionals(civInfo)) } -> false improvement.getMatchingUniques(UniqueType.ObsoleteWith).any { civInfo.tech.isResearched(it.params[0]) } -> return false diff --git a/core/src/com/unciv/logic/map/UnitPromotions.kt b/core/src/com/unciv/logic/map/UnitPromotions.kt index 94714ba64d..d898467f60 100644 --- a/core/src/com/unciv/logic/map/UnitPromotions.kt +++ b/core/src/com/unciv/logic/map/UnitPromotions.kt @@ -1,5 +1,6 @@ package com.unciv.logic.map +import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.UniqueTriggerActivation import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.Promotion @@ -95,7 +96,11 @@ class UnitPromotions { .filter { it.getMatchingUniques(UniqueType.IncompatibleWith).all { unique -> !promotions.contains(unique.params[0]) - } + } + } + .filter { promotion -> promotion.uniqueObjects + .none { it.type == UniqueType.OnlyAvailableWhen + && !it.conditionalsApply(StateForConditionals(unit.civInfo, unit = unit)) } } } diff --git a/core/src/com/unciv/models/ruleset/Building.kt b/core/src/com/unciv/models/ruleset/Building.kt index b7280e1169..ba3dac29dd 100644 --- a/core/src/com/unciv/models/ruleset/Building.kt +++ b/core/src/com/unciv/models/ruleset/Building.kt @@ -469,6 +469,10 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction { for (unique in uniqueObjects) { when (unique.placeholderText) { // TODO: Lots of typification… + UniqueType.OnlyAvailableWhen.placeholderText-> + if (!unique.conditionalsApply(civInfo, cityConstructions.cityInfo)) + rejectionReasons.add(RejectionReason.ShouldNotBeDisplayed) + UniqueType.NotDisplayedWithout.placeholderText -> if (unique.params[0] in ruleSet.tileResources && !civInfo.hasResource(unique.params[0]) || unique.params[0] in ruleSet.buildings && !cityConstructions.containsBuildingOrEquivalent(unique.params[0]) diff --git a/core/src/com/unciv/models/ruleset/unique/Unique.kt b/core/src/com/unciv/models/ruleset/unique/Unique.kt index 1b41928493..b34cf2101f 100644 --- a/core/src/com/unciv/models/ruleset/unique/Unique.kt +++ b/core/src/com/unciv/models/ruleset/unique/Unique.kt @@ -124,8 +124,9 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s UniqueType.ConditionalVsCity -> state.theirCombatant?.matchesCategory("City") == true UniqueType.ConditionalVsUnits -> state.theirCombatant?.matchesCategory(condition.params[0]) == true UniqueType.ConditionalOurUnit -> - state.ourCombatant?.matchesCategory(condition.params[0]) == true - || state.unit?.matchesFilter(condition.params[0]) == true + relevantUnit?.matchesFilter(condition.params[0]) == true + UniqueType.ConditionalUnitWithPromotion -> relevantUnit?.promotions?.promotions?.contains(params[0]) == true + UniqueType.ConditionalUnitWithoutPromotion -> relevantUnit?.promotions?.promotions?.contains(params[0]) == false UniqueType.ConditionalAttacking -> state.combatAction == CombatAction.Attack UniqueType.ConditionalDefending -> state.combatAction == CombatAction.Defend UniqueType.ConditionalAboveHP -> diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt index f2a97a8d68..627172360f 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt @@ -29,7 +29,7 @@ object UniqueTriggerActivation { if (tile != null) Random(tile.position.toString().hashCode()) else Random(-550) // Very random indeed - if (!unique.conditionalsApply(StateForConditionals(civInfo, cityInfo))) return false + if (!unique.conditionalsApply(civInfo, cityInfo)) return false val timingConditional = unique.conditionals.firstOrNull{it.type == ConditionalTimedUnique} if (timingConditional!=null) { diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index 52b8998f0e..161b98e52b 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -279,10 +279,11 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags: PopulationLossFromNukesDeprecated("Population loss from nuclear attacks -[amount]%", UniqueTarget.Global), NaturalReligionSpreadStrength("[amount]% Natural religion spread [cityFilter]", UniqueTarget.FollowerBelief, UniqueTarget.Global), - @Deprecated("as of 3.19.3", ReplaceWith("[amount]% Natural religion spread [cityFilter] OR [amount]% natural religion spread [cityFilter] ")) + @Deprecated("as of 3.19.3", ReplaceWith("[amount]% Natural religion spread [cityFilter] OR [amount]% natural religion spread [cityFilter] ")) NaturalReligionSpreadStrengthWith("[amount]% Natural religion spread [cityFilter] with [tech/policy]", UniqueTarget.Global, UniqueTarget.FollowerBelief), ReligionSpreadDistance("Religion naturally spreads to cities [amount] tiles away", UniqueTarget.Global, UniqueTarget.FollowerBelief), - + + @Deprecated("as of 3.19.8", ReplaceWith("Only available OR OR ")) IncompatibleWith("Incompatible with [policy/tech/promotion]", UniqueTarget.Policy, UniqueTarget.Tech, UniqueTarget.Promotion), StartingTech("Starting tech", UniqueTarget.Tech), StartsWithTech("Starts with [tech]", UniqueTarget.Nation), @@ -296,12 +297,17 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags: ///////////////////////////////////////// region CONSTRUCTION UNIQUES ///////////////////////////////////////// + Unbuildable("Unbuildable", UniqueTarget.Building, UniqueTarget.Unit), CannotBePurchased("Cannot be purchased", UniqueTarget.Building, UniqueTarget.Unit), CanBePurchasedWithStat("Can be purchased with [stat] [cityFilter]", UniqueTarget.Building, UniqueTarget.Unit), CanBePurchasedForAmountStat("Can be purchased for [amount] [stat] [cityFilter]", UniqueTarget.Building, UniqueTarget.Unit), MaxNumberBuildable("Limited to [amount] per Civilization", UniqueTarget.Building, UniqueTarget.Unit), HiddenBeforeAmountPolicies("Hidden until [amount] social policy branches have been completed", UniqueTarget.Building, UniqueTarget.Unit), + // Meant to be used together with conditionals, like "Only available " + OnlyAvailableWhen("Only available", UniqueTarget.Unit, UniqueTarget.Building, UniqueTarget.Improvement, + UniqueTarget.Policy, UniqueTarget.Tech, UniqueTarget.Promotion), + @Deprecated("as of 3.19.8", ReplaceWith("Only available OR OR ")) NotDisplayedWithout("Not displayed as an available construction without [buildingName/tech/resource/policy]", UniqueTarget.Building, UniqueTarget.Unit), ConvertFoodToProductionWhenConstructed("Excess Food converted to Production when under construction", UniqueTarget.Building, UniqueTarget.Unit), RequiresPopulation("Requires at least [amount] population", UniqueTarget.Building, UniqueTarget.Unit), @@ -557,6 +563,8 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags: /////// unit conditionals ConditionalOurUnit("for [mapUnitFilter] units", UniqueTarget.Conditional), + ConditionalUnitWithPromotion("for units with [promotion]", UniqueTarget.Conditional), + ConditionalUnitWithoutPromotion("for units without [promotion]", UniqueTarget.Conditional), ConditionalVsCity("vs cities", UniqueTarget.Conditional), ConditionalVsUnits("vs [mapUnitFilter] units", UniqueTarget.Conditional), ConditionalVsLargerCiv("when fighting units from a Civilization with more Cities than you", UniqueTarget.Conditional), diff --git a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt index 34302c89e9..bdac28e902 100644 --- a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt +++ b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt @@ -351,6 +351,12 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { if (isWaterUnit() && !cityConstructions.cityInfo.isCoastal()) rejectionReasons.add(RejectionReason.WaterUnitsInCoastalCities) val civInfo = cityConstructions.cityInfo.civInfo + + for (unique in uniqueObjects.filter { it.type == UniqueType.OnlyAvailableWhen }){ + if (!unique.conditionalsApply(civInfo, cityConstructions.cityInfo)) + rejectionReasons.add(RejectionReason.ShouldNotBeDisplayed) + } + for (unique in getMatchingUniques(UniqueType.NotDisplayedWithout)) { val filter = unique.params[0] if (filter in civInfo.gameInfo.ruleSet.tileResources && !civInfo.hasResource(filter) diff --git a/core/src/com/unciv/ui/pickerscreens/TechPickerScreen.kt b/core/src/com/unciv/ui/pickerscreens/TechPickerScreen.kt index 93a36ae172..69d4e5db54 100644 --- a/core/src/com/unciv/ui/pickerscreens/TechPickerScreen.kt +++ b/core/src/com/unciv/ui/pickerscreens/TechPickerScreen.kt @@ -265,13 +265,20 @@ class TechPickerScreen( } val pathToTech = civTech.getRequiredTechsToDestination(tech) - for (requiredTech in pathToTech) + for (requiredTech in pathToTech) { for (unique in requiredTech.getMatchingUniques(UniqueType.IncompatibleWith)) if (civTech.isResearched(unique.params[0])) { rightSideButton.setText(unique.text.tr()) rightSideButton.disable() return } + for (unique in requiredTech.uniqueObjects + .filter { it.type == UniqueType.OnlyAvailableWhen && !it.conditionalsApply(civInfo) }) { + rightSideButton.setText(unique.text.tr()) + rightSideButton.disable() + return + } + } tempTechsToResearch.clear() tempTechsToResearch.addAll(pathToTech.map { it.name }) diff --git a/docs/uniques.md b/docs/uniques.md index a4906289cb..27ab512bde 100644 --- a/docs/uniques.md +++ b/docs/uniques.md @@ -647,14 +647,12 @@ Example: "Starts with [Agriculture]" Applicable to: Nation ## Tech uniques -#### Incompatible with [policy/tech/promotion] -Example: "Incompatible with [policy/tech/promotion]" - -Applicable to: Tech, Policy, Promotion - #### Starting tech Applicable to: Tech +#### Only available +Applicable to: Tech, Policy, Building, Unit, Promotion, Improvement + ## FollowerBelief uniques #### [amount]% [stat] from every follower, up to [amount]% Example: "[20]% [Culture] from every follower, up to [20]%" @@ -703,11 +701,6 @@ Example: "Hidden until [20] social policy branches have been completed" Applicable to: Building, Unit -#### Not displayed as an available construction without [buildingName/tech/resource/policy] -Example: "Not displayed as an available construction without [buildingName/tech/resource/policy]" - -Applicable to: Building, Unit - #### Excess Food converted to Production when under construction Applicable to: Building, Unit @@ -1369,6 +1362,16 @@ Example: "" Applicable to: Conditional +#### +Example: "" + +Applicable to: Conditional + +#### +Example: "" + +Applicable to: Conditional + #### Applicable to: Conditional @@ -1477,7 +1480,7 @@ Applicable to: Conditional - "[amount]% Attacking Strength for cities" - Deprecated as of 3.18.17, replace with "[+amount]% Strength for cities " - "+[amount]% attacking strength for cities with garrisoned units" - Deprecated as of 3.19.1, replace with "[+amount]% Strength for cities " - "Population loss from nuclear attacks -[amount]%" - Deprecated as of 3.19.2, replace with "Population loss from nuclear attacks [-amount]% [in this city]" - - "[amount]% Natural religion spread [cityFilter] with [tech/policy]" - Deprecated as of 3.19.3, replace with "[amount]% Natural religion spread [cityFilter] OR [amount]% natural religion spread [cityFilter] " + - "[amount]% Natural religion spread [cityFilter] with [tech/policy]" - Deprecated as of 3.19.3, replace with "[amount]% Natural religion spread [cityFilter] OR [amount]% natural religion spread [cityFilter] " - "[amount] HP when healing in [tileFilter] tiles" - Deprecated as of 3.19.4, replace with "[amount] HP when healing " - "Melee units pay no movement cost to pillage" - Deprecated as of 3.18.17, replace with "No movement cost to pillage " - "Heal adjacent units for an additional 15 HP per turn" - Deprecated as of 3.19.3, replace with "All adjacent units heal [+15] HP when healing" @@ -1521,6 +1524,8 @@ Applicable to: Conditional - "-33% unit upkeep costs" - Deprecated Extremely old - used for auto-updates only, replace with "[-33]% maintenance costs " - "-50% food consumption by specialists" - Deprecated Extremely old - used for auto-updates only, replace with "[-50]% Food consumption by specialists [in all cities]" - "+50% attacking strength for cities with garrisoned units" - Deprecated Extremely old - used for auto-updates only, replace with "[+50]% Strength for cities " + - "Incompatible with [policy/tech/promotion]" - Deprecated as of 3.19.8, replace with "Only available OR OR " + - "Not displayed as an available construction without [buildingName/tech/resource/policy]" - Deprecated as of 3.19.8, replace with "Only available OR OR " - "[stats] with [resource]" - Deprecated as of 3.19.7, replace with "[stats] " - "Not displayed as an available construction unless [buildingName] is built" - Deprecated as of 3.16.11, replace with "Not displayed as an available construction without [buildingName]" - "[stats] once [tech] is discovered" - Deprecated as of 3.17.10 - removed 3.18.19, replace with "[stats] " diff --git a/tests/src/com/unciv/testing/BasicTests.kt b/tests/src/com/unciv/testing/BasicTests.kt index d320b60fb8..55bdee345f 100644 --- a/tests/src/com/unciv/testing/BasicTests.kt +++ b/tests/src/com/unciv/testing/BasicTests.kt @@ -134,8 +134,8 @@ class BasicTests { println("${uniqueType.name}'s deprecation text does not match any existing type!'") allOK = false } - if (replacementTextUnique.getDeprecationAnnotation() != null){ - println("${uniqueType.name}'s deprecation text references another deprecated unique!'") + if (replacementTextUnique.type == uniqueType){ + println("${uniqueType.name}'s deprecation text references itself!'") allOK = false } for (conditional in replacementTextUnique.conditionals){