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
This commit is contained in:
Yair Morgenstern 2022-02-12 19:03:30 +02:00 committed by GitHub
parent 4fb4722e3a
commit e72dcc8b0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 74 additions and 33 deletions

View File

@ -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 <before adopting [Rationalism]>"],
"policies": [
{
"name": "Organized Religion",
@ -276,7 +276,7 @@
{
"name": "Rationalism",
"era": "Renaissance era",
"uniques": ["[+15]% [Science] <while the empire is happy>", "Incompatible with [Piety]"],
"uniques": ["[+15]% [Science] <while the empire is happy>", "Only available <before adopting [Piety]>"],
"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 <before adopting [Autocracy]>", "Only available <before adopting [Order]>"],
"policies": [
{
"name": "Constitution",
@ -369,7 +369,7 @@
"name": "Autocracy",
"era": "Industrial era",
"uniques": ["[-33]% maintenance costs <for [All] units>", "Upon capturing a city, receive [10] times its [Culture] production as [Culture] immediately",
"Incompatible with [Order]", "Incompatible with [Freedom]"],
"Only available <before adopting [Order]>", "Only available <before adopting [Freedom]>"],
"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 <before adopting [Autocracy]>", "Only available <before adopting [Freedom]>"],
"policies": [
{
"name": "United Front",

View File

@ -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 <before adopting [Rationalism]>"],
"policies": [
{
"name": "Organized Religion",
@ -231,8 +231,7 @@
{
"name": "Naval Tradition",
"uniques": ["[+1] Movement <for [{Military} {Water}] units>", "[+1] Sight <for [{Military} {Water}] units>",
"Free [Great General] appears", "[+2] Movement <for [Great Admiral] units>"
// ToDo: Should be "Free [Great Admiral] appears"
"Free [Great General] appears" //, "[+2] Movement <for [Great Admiral] units>", "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 <before adopting [Piety]>"],
"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 <before adopting [Autocracy]>", "Only available <before adopting [Order]>"],
"policies": [
{
"name": "Constitution",
@ -363,7 +362,7 @@
"name": "Autocracy",
"era": "Industrial era",
"uniques": ["[-33]% maintenance costs <for [All] units>", "Upon capturing a city, receive [10] times its [Culture] production as [Culture] immediately",
"Incompatible with [Order]", "Incompatible with [Freedom]"],
"Only available <before adopting [Order]>", "Only available <before adopting [Freedom]>"],
"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 <before adopting [Autocracy]>", "Only available <before adopting [Freedom]>"],
"policies": [
{
"name": "United Front",

View File

@ -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
}

View File

@ -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())

View File

@ -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

View File

@ -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)) }
}
}

View File

@ -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])

View File

@ -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 ->

View File

@ -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) {

View File

@ -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] <after discovering [tech]> OR [amount]% natural religion spread [cityFilter] <after adopting [policy]>"))
@Deprecated("as of 3.19.3", ReplaceWith("[amount]% Natural religion spread [cityFilter] <after discovering [tech/policy]> OR [amount]% natural religion spread [cityFilter] <after adopting [tech/policy]>"))
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 <before adopting [policy/tech/promotion]> OR <before discovering [policy/tech/promotion]> OR <for units without [policy/tech/promotion]>"))
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 <after adopting [policy]> <while the empire is happy>"
OnlyAvailableWhen("Only available", UniqueTarget.Unit, UniqueTarget.Building, UniqueTarget.Improvement,
UniqueTarget.Policy, UniqueTarget.Tech, UniqueTarget.Promotion),
@Deprecated("as of 3.19.8", ReplaceWith("Only available <after adopting [buildingName/tech/resource/policy]> OR <with [buildingName/tech/resource/policy]> OR <after discovering [buildingName/tech/resource/policy]>"))
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),

View File

@ -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)

View File

@ -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 })

View File

@ -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: "<for [Wounded] units>"
Applicable to: Conditional
#### <for units with [promotion]>
Example: "<for units with [Shock I]>"
Applicable to: Conditional
#### <for units without [promotion]>
Example: "<for units without [Shock I]>"
Applicable to: Conditional
#### <vs cities>
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 <when attacking>"
- "+[amount]% attacking strength for cities with garrisoned units" - Deprecated as of 3.19.1, replace with "[+amount]% Strength for cities <with a garrison> <when attacking>"
- "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] <after discovering [tech]> OR [amount]% natural religion spread [cityFilter] <after adopting [policy]>"
- "[amount]% Natural religion spread [cityFilter] with [tech/policy]" - Deprecated as of 3.19.3, replace with "[amount]% Natural religion spread [cityFilter] <after discovering [tech/policy]> OR [amount]% natural religion spread [cityFilter] <after adopting [tech/policy]>"
- "[amount] HP when healing in [tileFilter] tiles" - Deprecated as of 3.19.4, replace with "[amount] HP when healing <in [tileFilter] tiles>"
- "Melee units pay no movement cost to pillage" - Deprecated as of 3.18.17, replace with "No movement cost to pillage <for [Melee] units>"
- "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 <for [All] units>"
- "-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 <with a garrison> <when attacking>"
- "Incompatible with [policy/tech/promotion]" - Deprecated as of 3.19.8, replace with "Only available <before adopting [policy/tech/promotion]> OR <before discovering [policy/tech/promotion]> OR <for units without [policy/tech/promotion]>"
- "Not displayed as an available construction without [buildingName/tech/resource/policy]" - Deprecated as of 3.19.8, replace with "Only available <after adopting [buildingName/tech/resource/policy]> OR <with [buildingName/tech/resource/policy]> OR <after discovering [buildingName/tech/resource/policy]>"
- "[stats] with [resource]" - Deprecated as of 3.19.7, replace with "[stats] <with [resource]>"
- "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] <after discovering [tech]>"

View File

@ -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){