Expand CanOnlyBeBuiltInCertainCities to include Units and convert to use Conditionals (#11274)

* Remove Transform requirement checks

* Add back in requirement for OnlyAvailable
New BuildableOnly unique

* Instead of a new unique, expand CanOnlyBeBuiltInCertainCities to instead take conditionals

* Rename to notMetRejections and copy to BaseUnit
Add CanOnlyBeBuiltInSpecificCities to constructionRejectionReasonType

* Setup CanOnlyBeBuiltInCertainCities as depreciated (renamed CanOnlyBeBuiltInCertainCities_dep)

* Redirect Depreciation

* Quick Camel Case rename

* Function renaming and moving Unique to general Construction Uniques

* spelling

* Move Unique. Update Error message

* version
This commit is contained in:
itanasi 2024-03-13 14:12:10 -07:00 committed by GitHub
parent 1542b92e63
commit e9c3350ec5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 110 additions and 26 deletions

View File

@ -260,7 +260,7 @@
"maintenance": 4,
"hurryCostModifier": 50,
"uniques": ["Remove extra unhappiness from annexed cities",
"Can only be built [in annexed cities]"],
"Can only be built <in [Annexed] cities>"],
"requiredTech": "Mathematics"
},
{
@ -469,7 +469,7 @@
"culture": 1,
"faith": 8,
"uniques": ["Only available <if [Temple] is constructed in all [non-[Puppeted]] cities>", "Cost increases by [30] per owned city",
"[+100]% Natural religion spread [in this city]", "Hidden when religion is disabled", "Can only be built [in holy cities]"],
"[+100]% Natural religion spread [in this city]", "Hidden when religion is disabled", "Can only be built <in [Holy] cities>"],
"requiredTech": "Theology",
"isNationalWonder": true
},

View File

@ -221,7 +221,7 @@
"maintenance": 4,
"hurryCostModifier": 50,
"uniques": ["Remove extra unhappiness from annexed cities",
"Can only be built [in annexed cities]"],
"Can only be built <in [Annexed] cities>"],
"requiredTech": "Mathematics"
},
{

View File

@ -251,7 +251,10 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
yield(RejectionReasonType.AlreadyBuilt.toInstance())
for (unique in uniqueObjects) {
if (unique.type != UniqueType.OnlyAvailable &&
// skip uniques that don't have conditionals apply
// EXCEPT for [UniqueType.OnlyAvailable] and [UniqueType.CanOnlyBeBuiltInCertainCities]
// since they trigger (reject) only if conditionals ARE NOT met
if (unique.type != UniqueType.OnlyAvailable && unique.type != UniqueType.CanOnlyBeBuiltWhen &&
!unique.conditionalsApply(StateForConditionals(civ, cityConstructions.city))) continue
@Suppress("NON_EXHAUSTIVE_WHEN")
@ -262,7 +265,10 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
yield(RejectionReasonType.Unbuildable.toInstance())
UniqueType.OnlyAvailable ->
yieldAll(onlyAvailableRejections(unique, cityConstructions))
yieldAll(notMetRejections(unique, cityConstructions))
UniqueType.CanOnlyBeBuiltWhen ->
yieldAll(notMetRejections(unique, cityConstructions, true))
UniqueType.Unavailable ->
yield(RejectionReasonType.ShouldNotBeDisplayed.toInstance())
@ -433,9 +439,14 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
}
}
private fun onlyAvailableRejections(unique: Unique, cityConstructions: CityConstructions): Sequence<RejectionReason> = sequence {
/**
* Handles inverted conditional rejections and cumulative conditional reporting
* See also [com.unciv.models.ruleset.unit.BaseUnit.notMetRejections]
*/
private fun notMetRejections(unique: Unique, cityConstructions: CityConstructions, built: Boolean=false): Sequence<RejectionReason> = sequence {
val civ = cityConstructions.city.civ
for (conditional in unique.conditionals) {
// We yield a rejection only when conditionals are NOT met
if (Conditionals.conditionalApplies(unique, conditional, StateForConditionals(civ, cityConstructions.city)))
continue
when (conditional.type) {
@ -464,7 +475,10 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
}
}
else -> {
yield(RejectionReasonType.ShouldNotBeDisplayed.toInstance())
if (built)
yield(RejectionReasonType.CanOnlyBeBuiltInSpecificCities.toInstance(unique.text))
else
yield(RejectionReasonType.ShouldNotBeDisplayed.toInstance())
}
}
}

View File

@ -141,12 +141,14 @@ class RejectionReason(val type: RejectionReasonType,
RejectionReasonType.MaxNumberBuildable,
)
private val orderedImportantRejectionTypes = listOf(
RejectionReasonType.ShouldNotBeDisplayed,
RejectionReasonType.WonderBeingBuiltElsewhere,
RejectionReasonType.NationalWonderBeingBuiltElsewhere,
RejectionReasonType.RequiresBuildingInAllCities,
RejectionReasonType.RequiresBuildingInThisCity,
RejectionReasonType.RequiresBuildingInSomeCity,
RejectionReasonType.RequiresBuildingInSomeCities,
RejectionReasonType.CanOnlyBeBuiltInSpecificCities,
RejectionReasonType.CannotBeBuiltUnhappiness,
RejectionReasonType.PopulationRequirement,
RejectionReasonType.ConsumesResources,
@ -154,11 +156,12 @@ class RejectionReason(val type: RejectionReasonType,
RejectionReasonType.MaxNumberBuildable,
RejectionReasonType.NoPlaceToPutUnit,
)
// Used for units spawned, not built
// Exceptions. Used for units spawned/upgrade path, not built
private val constructionRejectionReasonType = listOf(
RejectionReasonType.Unbuildable,
RejectionReasonType.CannotBeBuiltUnhappiness,
RejectionReasonType.CannotBeBuilt,
RejectionReasonType.CanOnlyBeBuiltInSpecificCities,
)
}
@ -178,7 +181,7 @@ enum class RejectionReasonType(val shouldShow: Boolean, val errorMessage: String
MustNotBeNextToTile(false, "Must not be next to a specific tile"),
MustOwnTile(false, "Must own a specific tile close by"),
WaterUnitsInCoastalCities(false, "May only built water units in coastal cities"),
CanOnlyBeBuiltInSpecificCities(false, "Can only be built in specific cities"),
CanOnlyBeBuiltInSpecificCities(false, "Build requirements not met in this city"),
MaxNumberBuildable(false, "Maximum number have been built or are being constructed"),
UniqueToOtherNation(false, "Unique to another nation"),

View File

@ -47,7 +47,7 @@ interface IHasUniques : INamed {
fun hasUnique(uniqueType: UniqueType, stateForConditionals: StateForConditionals? = null) =
getMatchingUniques(uniqueType.placeholderText, stateForConditionals).any()
fun availabilityUniques(): Sequence<Unique> = getMatchingUniques(UniqueType.OnlyAvailable, StateForConditionals.IgnoreConditionals)
fun availabilityUniques(): Sequence<Unique> = getMatchingUniques(UniqueType.OnlyAvailable, StateForConditionals.IgnoreConditionals) + getMatchingUniques(UniqueType.CanOnlyBeBuiltWhen, StateForConditionals.IgnoreConditionals)
fun techsRequiredByUniques(): Sequence<String> {
return availabilityUniques()

View File

@ -1,6 +1,7 @@
package com.unciv.models.ruleset.unique
import com.unciv.Constants
import com.unciv.models.ruleset.RejectionReasonType
import com.unciv.models.ruleset.validation.RulesetErrorSeverity
import com.unciv.models.ruleset.validation.RulesetValidator
import com.unciv.models.ruleset.validation.Suppression
@ -257,18 +258,24 @@ enum class UniqueType(
///////////////////////////////////////// region 02 CONSTRUCTION UNIQUES /////////////////////////////////////////
Unbuildable("Unbuildable", UniqueTarget.Building, UniqueTarget.Unit, UniqueTarget.Improvement),
Unbuildable("Unbuildable", UniqueTarget.Building, UniqueTarget.Unit, UniqueTarget.Improvement,
docDescription = "Blocks from being built, possibly by conditional. However it can still appear in the menu and be bought with other means such as Gold or Faith"),
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>"
/** A special unique, as it only activates when it has conditionals that *do not* apply */
/** A special unique, as it only activates [RejectionReasonType] when it has conditionals that *do not* apply.
* Meant to be used together with conditionals, like "Buildable only <after adopting [policy]> <while the empire is happy>".
* Restricts Upgrade/Transform pathways.
* @See [CanOnlyBeBuiltWhen]
*/
OnlyAvailable("Only available", UniqueTarget.Unit, UniqueTarget.Building, UniqueTarget.Improvement,
UniqueTarget.Policy, UniqueTarget.Tech, UniqueTarget.Promotion, UniqueTarget.Ruins, UniqueTarget.FollowerBelief, UniqueTarget.FounderBelief),
UniqueTarget.Policy, UniqueTarget.Tech, UniqueTarget.Promotion, UniqueTarget.Ruins, UniqueTarget.FollowerBelief, UniqueTarget.FounderBelief,
docDescription = "Meant to be used together with conditionals, like \"Only available <after adopting [policy]> <while the empire is happy>\". Only allows Building when ALL conditionals are met. Will also block Upgrade and Transform actions. See also CanOnlyBeBuiltWhen"),
Unavailable("Unavailable", UniqueTarget.Unit, UniqueTarget.Building, UniqueTarget.Improvement,
UniqueTarget.Policy, UniqueTarget.Tech, UniqueTarget.Promotion, UniqueTarget.Ruins),
UniqueTarget.Policy, UniqueTarget.Tech, UniqueTarget.Promotion, UniqueTarget.Ruins,
docDescription = "Meant to be used together with conditionals, like \"Unavailable <after generating a Great Prophet>\"."),
ConvertFoodToProductionWhenConstructed("Excess Food converted to Production when under construction", UniqueTarget.Building, UniqueTarget.Unit),
RequiresPopulation("Requires at least [amount] population", UniqueTarget.Building, UniqueTarget.Unit),
@ -288,7 +295,14 @@ enum class UniqueType(
RequiresBuildingInAllCities("Requires a [buildingFilter] in all cities", UniqueTarget.Building),
@Deprecated("as of 4.10.17", ReplaceWith("Only available <if [buildingFilter] is constructed in at least [positiveAmount] of [All] cities>"))
RequiresBuildingInSomeCities("Requires a [buildingFilter] in at least [positiveAmount] cities", UniqueTarget.Building),
@Deprecated("as of 4.10.18", ReplaceWith("Can only be built <in [cityFilter] cities>"))
CanOnlyBeBuiltInCertainCities("Can only be built [cityFilter]", UniqueTarget.Building),
/** Triggers [RejectionReasonType] when any conditional does NOT apply.
* Doesn't restrict Upgrade/Transform pathways.
* @see [OnlyAvailable]
*/
CanOnlyBeBuiltWhen("Can only be built", UniqueTarget.Building, UniqueTarget.Unit,
docDescription = "Meant to be used together with conditionals, like \"Can only be built <after adopting [policy]> <while the empire is happy>\". Only allows Building when ALL conditionals are met. Will also NOT block Upgrade and Transform actions. See also OnlyAvailable"),
MustHaveOwnedWithinTiles("Must have an owned [tileFilter] within [amount] tiles", UniqueTarget.Building),
@ -912,7 +926,7 @@ enum class UniqueType(
StrengthWithinTilesOfTile("+[amount]% Strength if within [amount2] tiles of a [tileFilter]", UniqueTarget.Global),
@Deprecated("as of 3.19.7", ReplaceWith("[stats] <with [resource]>"), DeprecationLevel.ERROR)
StatsWithResource("[stats] with [resource]", UniqueTarget.Building),
@Deprecated("as of 3.19.16", ReplaceWith("Can only be built [in annexed cities]"), DeprecationLevel.ERROR)
@Deprecated("as of 3.19.16", ReplaceWith("Can only be built <in [Annexed] cities>"), DeprecationLevel.ERROR)
CanOnlyBeBuiltInAnnexedCities("Can only be built in annexed cities", UniqueTarget.Building),
@Deprecated("as of 4.0.3", ReplaceWith("Defense bonus when embarked <for [All] units>"), DeprecationLevel.ERROR)
DefenceBonusWhenEmbarkedCivwide("Embarked units can defend themselves", UniqueTarget.Global),

View File

@ -12,6 +12,7 @@ import com.unciv.models.ruleset.RejectionReason
import com.unciv.models.ruleset.RejectionReasonType
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetObject
import com.unciv.models.ruleset.unique.Conditionals
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueTarget
@ -170,8 +171,10 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
val civInfo = cityConstructions.city.civ
for (unique in getMatchingUniques(UniqueType.OnlyAvailable, StateForConditionals.IgnoreConditionals))
if (!unique.conditionalsApply(civInfo, cityConstructions.city))
yield(RejectionReasonType.ShouldNotBeDisplayed.toInstance())
yieldAll(notMetRejections(unique, cityConstructions))
for (unique in getMatchingUniques(UniqueType.CanOnlyBeBuiltWhen, StateForConditionals.IgnoreConditionals))
yieldAll(notMetRejections(unique, cityConstructions, true))
for (unique in getMatchingUniques(UniqueType.Unavailable, StateForConditionals(civInfo, cityConstructions.city)))
yield(RejectionReasonType.ShouldNotBeDisplayed.toInstance())
@ -238,6 +241,52 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
}
}
/**
* Copy of [com.unciv.models.ruleset.Building.notMetRejections] to handle inverted conditionals.
* Also custom handles [UniqueType.ConditionalBuildingBuiltAmount], and
* [UniqueType.ConditionalBuildingBuiltAll]
*/
private fun notMetRejections(unique: Unique, cityConstructions: CityConstructions, built: Boolean=false): Sequence<RejectionReason> = sequence {
val civ = cityConstructions.city.civ
for (conditional in unique.conditionals) {
// We yield a rejection only when conditionals are NOT met
if (Conditionals.conditionalApplies(unique, conditional, StateForConditionals(civ, cityConstructions.city)))
continue
when (conditional.type) {
UniqueType.ConditionalBuildingBuiltAmount -> {
val building = civ.getEquivalentBuilding(conditional.params[0]).name
val amount = conditional.params[1].toInt()
val cityFilter = conditional.params[2]
val numberOfCities = civ.cities.count {
it.cityConstructions.containsBuildingOrEquivalent(building) && it.matchesFilter(cityFilter)
}
if (numberOfCities < amount)
{
yield(RejectionReasonType.RequiresBuildingInSomeCities.toInstance(
"Requires a [$building] in at least [$amount] cities" +
" ($numberOfCities/$numberOfCities)"))
}
}
UniqueType.ConditionalBuildingBuiltAll -> {
val building = civ.getEquivalentBuilding(conditional.params[0]).name
val cityFilter = conditional.params[1]
if(civ.cities.any { it.matchesFilter(cityFilter)
!it.isPuppet && !it.cityConstructions.containsBuildingOrEquivalent(building)
}) {
yield(RejectionReasonType.RequiresBuildingInAllCities.toInstance(
"Requires a [${building}] in all cities"))
}
}
else -> {
if (built)
yield(RejectionReasonType.CanOnlyBeBuiltInSpecificCities.toInstance(unique.text))
else
yield(RejectionReasonType.ShouldNotBeDisplayed.toInstance())
}
}
}
}
fun isBuildable(civInfo: Civilization) = getRejectionReasons(civInfo).none()
override fun isBuildable(cityConstructions: CityConstructions): Boolean =

View File

@ -80,18 +80,18 @@ object BuildingDescriptions {
if (cityStrength != 0) translatedLines += "{City strength} +$cityStrength".tr()
if (cityHealth != 0) translatedLines += "{City health} +$cityHealth".tr()
if (maintenance != 0 && !isFree) translatedLines += "{Maintenance cost}: $maintenance {Gold}".tr()
if (showAdditionalInfo) additionalDecription(building, city, translatedLines)
if (showAdditionalInfo) additionalDescription(building, city, translatedLines)
return translatedLines.joinToString("\n").trim()
}
fun additionalDecription (building: Building, city: City, lines: ArrayList<String>) {
fun additionalDescription (building: Building, city: City, lines: ArrayList<String>) {
// Inefficient in theory. In practice, buildings seem to have only a small handful of uniques.
for (unique in building.uniqueObjects) {
if (unique.type == UniqueType.RequiresBuildingInAllCities) {
missingCityText(unique.params[0], city, "non-[Puppeted]", lines)
}
else if (unique.type == UniqueType.OnlyAvailable)
else if (unique.type == UniqueType.OnlyAvailable || unique.type == UniqueType.CanOnlyBeBuiltWhen)
for (conditional in unique.conditionals) {
if (conditional.type == UniqueType.ConditionalBuildingBuiltAll) {
missingCityText(conditional.params[0], city, conditional.params[1], lines)

View File

@ -268,6 +268,11 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
var maxButtonWidth = constructionsQueueTable.width
for (dto in constructionButtonDTOList) {
/** filter out showing buildings that have RequiresBuildingInThisCity
* rejection (eg requiredBuilding entry) which are buildable.
* The rejection for RequiresBuildingInThisCity isn't yielded if
* the prerequisite is in the queue
*/
if (dto.construction is Building
&& dto.rejectionReason?.type == RejectionReasonType.RequiresBuildingInThisCity
&& constructionButtonDTOList.any {

View File

@ -337,12 +337,11 @@ object UnitActionsFromUniques {
for (unique in unit.getMatchingUniques(UniqueType.CanTransform, stateForConditionals)) {
val unitToTransformTo = civInfo.getEquivalentUnit(unique.params[0])
// Respect OnlyAvailable criteria
if (unitToTransformTo.getMatchingUniques(
UniqueType.OnlyAvailable,
StateForConditionals.IgnoreConditionals
)
.any { !it.conditionalsApply(stateForConditionals) })
continue
UniqueType.OnlyAvailable, StateForConditionals.IgnoreConditionals
).any { !it.conditionalsApply(stateForConditionals) }
) continue
// Check _new_ resource requirements
// Using Counter to aggregate is a bit exaggerated, but - respect the mad modder.