diff --git a/core/src/com/unciv/logic/city/City.kt b/core/src/com/unciv/logic/city/City.kt index 3c0247d865..1cbec6819c 100644 --- a/core/src/com/unciv/logic/city/City.kt +++ b/core/src/com/unciv/logic/city/City.kt @@ -483,7 +483,7 @@ class City : IsPartOfGameInfoSerialization { + religion.getUniques().filter { it.type == uniqueType } ).filter { !it.isTimedTriggerable && it.conditionalsApply(stateForConditionals) - } + }.flatMap { it.getMultiplied(stateForConditionals) } } // Uniques special to this city @@ -491,6 +491,7 @@ class City : IsPartOfGameInfoSerialization { val uniques = cityConstructions.builtBuildingUniqueMap.getUniques(uniqueType).filter { it.isLocalEffect } + religion.getUniques().filter { it.type == uniqueType } return if (uniques.any()) uniques.filter { !it.isTimedTriggerable && it.conditionalsApply(stateForConditionals) } + .flatMap { it.getMultiplied(stateForConditionals) } else uniques } @@ -499,7 +500,7 @@ class City : IsPartOfGameInfoSerialization { val uniques = cityConstructions.builtBuildingUniqueMap.getUniques(uniqueType) // Memory performance showed that this function was very memory intensive, thus we only create the filter if needed return if (uniques.any()) uniques.filter { !it.isLocalEffect && !it.isTimedTriggerable - && it.conditionalsApply(stateForConditionals) } + && it.conditionalsApply(stateForConditionals) }.flatMap { it.getMultiplied(stateForConditionals) } else uniques } diff --git a/core/src/com/unciv/models/ruleset/unique/Conditionals.kt b/core/src/com/unciv/models/ruleset/unique/Conditionals.kt index 8d66d1d8f0..21e2a26a3a 100644 --- a/core/src/com/unciv/models/ruleset/unique/Conditionals.kt +++ b/core/src/com/unciv/models/ruleset/unique/Conditionals.kt @@ -2,7 +2,6 @@ package com.unciv.models.ruleset.unique import com.unciv.logic.GameInfo import com.unciv.logic.battle.CombatAction -import com.unciv.logic.battle.MapUnitCombatant import com.unciv.logic.city.City import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.managers.ReligionState @@ -20,62 +19,31 @@ object Conditionals { if (condition.type?.targetTypes?.any { it.modifierType == UniqueTarget.ModifierType.Other } == true) return true // not a filtering condition, includes e.g. ModifierHiddenFromUsers - val relevantUnit by lazy { - if (state.ourCombatant != null && state.ourCombatant is MapUnitCombatant) state.ourCombatant.unit - else state.unit - } - - val relevantTile by lazy { state.attackedTile - ?: state.tile - // We need to protect against conditionals checking tiles for units pre-placement - see #10425, #10512 - ?: relevantUnit?.run { if (hasTile()) getTile() else null } - ?: state.city?.getCenterTile() - } - - val relevantCity by lazy { - state.city - ?: relevantTile?.getCity() - } - - val relevantCiv by lazy { - state.civInfo ?: - relevantCity?.civ ?: - relevantUnit?.civ - } - - val gameInfo by lazy { relevantCiv?.gameInfo } - - val stateBasedRandom by lazy { Random(state.hashCode() * 31 + (gameInfo?.turns?.hashCode() ?: 0)) } - - fun getResourceAmount(resourceName: String): Int { - if (relevantCity != null) return relevantCity!!.getAvailableResourceAmount(resourceName) - if (relevantCiv != null) return relevantCiv!!.getResourceAmount(resourceName) - return 0 - } + val stateBasedRandom by lazy { Random(state.hashCode() * 31 + (state.gameInfo?.turns?.hashCode() ?: 0)) } /** Helper to simplify conditional tests requiring gameInfo */ fun checkOnGameInfo(predicate: (GameInfo.() -> Boolean)): Boolean { - if (gameInfo == null) return false - return gameInfo!!.predicate() + if (state.gameInfo == null) return false + return state.gameInfo!!.predicate() } /** Helper to simplify conditional tests requiring a Civilization */ fun checkOnCiv(predicate: (Civilization.() -> Boolean)): Boolean { - if (relevantCiv == null) return false - return relevantCiv!!.predicate() + if (state.relevantCiv == null) return false + return state.relevantCiv!!.predicate() } /** Helper to simplify conditional tests requiring a City */ fun checkOnCity(predicate: (City.() -> Boolean)): Boolean { - if (relevantCity == null) return false - return relevantCity!!.predicate() + if (state.relevantCity == null) return false + return state.relevantCity!!.predicate() } /** Helper to simplify the "compare civ's current era with named era" conditions */ fun compareEra(eraParam: String, compare: (civEra: Int, paramEra: Int) -> Boolean): Boolean { - if (gameInfo == null) return false - val era = gameInfo!!.ruleset.eras[eraParam] ?: return false - return compare(relevantCiv!!.getEraNumber(), era.eraNumber) + if (state.gameInfo == null) return false + val era = state.gameInfo!!.ruleset.eras[eraParam] ?: return false + return compare(state.relevantCiv!!.getEraNumber(), era.eraNumber) } /** Helper for ConditionalWhenAboveAmountStatResource and its below counterpart */ @@ -86,50 +54,27 @@ object Conditionals { modifyByGameSpeed: Boolean = false, compare: (current: Int, lowerLimit: Float, upperLimit: Float) -> Boolean ): Boolean { - if (gameInfo == null) return false - var gameSpeedModifier = if (modifyByGameSpeed) gameInfo!!.speed.modifier else 1f + if (state.gameInfo == null) return false + var gameSpeedModifier = if (modifyByGameSpeed) state.gameInfo!!.speed.modifier else 1f - if (gameInfo!!.ruleset.tileResources.containsKey(resourceOrStatName)) - return compare(getResourceAmount(resourceOrStatName), lowerLimit * gameSpeedModifier, upperLimit * gameSpeedModifier) + if (state.gameInfo!!.ruleset.tileResources.containsKey(resourceOrStatName)) + return compare(state.getResourceAmount(resourceOrStatName), lowerLimit * gameSpeedModifier, upperLimit * gameSpeedModifier) val stat = Stat.safeValueOf(resourceOrStatName) ?: return false - val statReserve = if (relevantCity != null) relevantCity!!.getStatReserve(stat) else relevantCiv!!.getStatReserve(stat) + val statReserve = if (state.relevantCity != null) state.relevantCity!!.getStatReserve(stat) else state.relevantCiv!!.getStatReserve(stat) - gameSpeedModifier = if (modifyByGameSpeed) gameInfo!!.speed.statCostModifiers[stat]!! else 1f + gameSpeedModifier = if (modifyByGameSpeed) state.gameInfo!!.speed.statCostModifiers[stat]!! else 1f return compare(statReserve, lowerLimit * gameSpeedModifier, upperLimit * gameSpeedModifier) } - fun getCountableAmount(countable: String): Float? { - if (countable.toFloatOrNull() != null) return countable.toFloat() - - val relevantStat = Stat.safeValueOf(countable) - - if (relevantStat != null) { - return if (relevantCity != null) { - relevantCity!!.getStatReserve(relevantStat).toFloat() - } else if (relevantStat in Stat.statsWithCivWideField && relevantCiv != null) { - relevantCiv!!.getStatReserve(relevantStat).toFloat() - } else { - null - } - } - - if (gameInfo == null) return null - - if (countable == "year") return gameInfo!!.getYear(gameInfo!!.turns).toFloat() - if (gameInfo!!.ruleset.tileResources.containsKey(countable)) - return getResourceAmount(countable).toFloat() - - return null - } fun compareCountables( first: String, second: String, - compare: (first: Float, second: Float) -> Boolean): Boolean { + compare: (first: Int, second: Int) -> Boolean): Boolean { - val firstNumber = getCountableAmount(first) - val secondNumber = getCountableAmount(second) + val firstNumber = Countables.getCountableAmount(first, state) + val secondNumber = Countables.getCountableAmount(second, state) return if (firstNumber != null && secondNumber != null) compare(firstNumber, secondNumber) @@ -138,11 +83,11 @@ object Conditionals { } fun compareCountables(first: String, second: String, third: String, - compare: (first: Float, second: Float, third: Float) -> Boolean): Boolean { + compare: (first: Int, second: Int, third: Int) -> Boolean): Boolean { - val firstNumber = getCountableAmount(first) - val secondNumber = getCountableAmount(second) - val thirdNumber = getCountableAmount(third) + val firstNumber = Countables.getCountableAmount(first, state) + val secondNumber = Countables.getCountableAmount(second, state) + val thirdNumber = Countables.getCountableAmount(third, state) return if (firstNumber != null && secondNumber != null && thirdNumber != null) compare(firstNumber, secondNumber, thirdNumber) @@ -162,8 +107,8 @@ object Conditionals { UniqueType.ConditionalCivFilter -> checkOnCiv { matchesFilter(condition.params[0]) } UniqueType.ConditionalWar -> checkOnCiv { isAtWar() } UniqueType.ConditionalNotWar -> checkOnCiv { !isAtWar() } - UniqueType.ConditionalWithResource -> getResourceAmount(condition.params[0]) > 0 - UniqueType.ConditionalWithoutResource -> getResourceAmount(condition.params[0]) <= 0 + UniqueType.ConditionalWithResource -> state.getResourceAmount(condition.params[0]) > 0 + UniqueType.ConditionalWithoutResource -> state.getResourceAmount(condition.params[0]) <= 0 UniqueType.ConditionalWhenAboveAmountStatResource -> checkResourceOrStatAmount(condition.params[1], condition.params[0].toFloat(), Float.MAX_VALUE) @@ -233,15 +178,15 @@ object Conditionals { checkOnGameInfo { getCities().any { it.cityConstructions.containsBuildingOrEquivalent(condition.params[0]) } } // Filtered via city.getMatchingUniques - UniqueType.ConditionalInThisCity -> relevantCity != null - UniqueType.ConditionalCityFilter -> checkOnCity { matchesFilter(condition.params[0], relevantCiv) } + UniqueType.ConditionalInThisCity -> state.relevantCity != null + UniqueType.ConditionalCityFilter -> checkOnCity { matchesFilter(condition.params[0], state.relevantCiv) } UniqueType.ConditionalCityConnected -> checkOnCity { isConnectedToCapital() } UniqueType.ConditionalCityMajorReligion -> checkOnCity { religion.getMajorityReligion()?.isMajorReligion() == true } UniqueType.ConditionalCityEnhancedReligion -> checkOnCity { religion.getMajorityReligion()?.isEnhancedReligion() == true } UniqueType.ConditionalCityThisReligion -> checkOnCity { - religion.getMajorityReligion() == relevantCiv?.religionManager?.religion } + religion.getMajorityReligion() == state.relevantCiv?.religionManager?.religion } UniqueType.ConditionalWLTKD -> checkOnCity { isWeLoveTheKingDayActive() } UniqueType.ConditionalCityWithBuilding -> checkOnCity { cityConstructions.containsBuildingOrEquivalent(condition.params[0]) } @@ -257,58 +202,58 @@ object Conditionals { UniqueType.ConditionalVsCity -> state.theirCombatant?.matchesFilter("City") == true UniqueType.ConditionalVsUnits, UniqueType.ConditionalVsCombatant -> state.theirCombatant?.matchesFilter(condition.params[0]) == true UniqueType.ConditionalOurUnit, UniqueType.ConditionalOurUnitOnUnit -> - relevantUnit?.matchesFilter(condition.params[0]) == true - UniqueType.ConditionalUnitWithPromotion -> relevantUnit?.promotions?.promotions?.contains(condition.params[0]) == true - UniqueType.ConditionalUnitWithoutPromotion -> relevantUnit?.promotions?.promotions?.contains(condition.params[0]) == false + state.relevantUnit?.matchesFilter(condition.params[0]) == true + UniqueType.ConditionalUnitWithPromotion -> state.relevantUnit?.promotions?.promotions?.contains(condition.params[0]) == true + UniqueType.ConditionalUnitWithoutPromotion -> state.relevantUnit?.promotions?.promotions?.contains(condition.params[0]) == false UniqueType.ConditionalAttacking -> state.combatAction == CombatAction.Attack UniqueType.ConditionalDefending -> state.combatAction == CombatAction.Defend - UniqueType.ConditionalAboveHP -> relevantUnit != null && relevantUnit!!.health > condition.params[0].toInt() + UniqueType.ConditionalAboveHP -> state.relevantUnit != null && state.relevantUnit!!.health > condition.params[0].toInt() || state.ourCombatant != null && state.ourCombatant.getHealth() > condition.params[0].toInt() - UniqueType.ConditionalBelowHP -> relevantUnit != null && relevantUnit!!.health < condition.params[0].toInt() + UniqueType.ConditionalBelowHP -> state.relevantUnit != null && state.relevantUnit!!.health < condition.params[0].toInt() ||state.ourCombatant != null && state.ourCombatant.getHealth() < condition.params[0].toInt() UniqueType.ConditionalHasNotUsedOtherActions -> state.unit == null || // So we get the action as a valid action in BaseUnit.hasUnique() state.unit.abilityToTimesUsed.isEmpty() UniqueType.ConditionalInTiles -> - relevantTile?.matchesFilter(condition.params[0], relevantCiv) == true + state.relevantTile?.matchesFilter(condition.params[0], state.relevantCiv) == true UniqueType.ConditionalInTilesNot -> - relevantTile?.matchesFilter(condition.params[0], relevantCiv) == false - UniqueType.ConditionalAdjacentTo -> relevantTile?.isAdjacentTo(condition.params[0], relevantCiv) == true - UniqueType.ConditionalNotAdjacentTo -> relevantTile?.isAdjacentTo(condition.params[0], relevantCiv) == false + state.relevantTile?.matchesFilter(condition.params[0], state.relevantCiv) == false + UniqueType.ConditionalAdjacentTo -> state.relevantTile?.isAdjacentTo(condition.params[0], state.relevantCiv) == true + UniqueType.ConditionalNotAdjacentTo -> state.relevantTile?.isAdjacentTo(condition.params[0], state.relevantCiv) == false UniqueType.ConditionalFightingInTiles -> - state.attackedTile?.matchesFilter(condition.params[0], relevantCiv) == true + state.attackedTile?.matchesFilter(condition.params[0], state.relevantCiv) == true UniqueType.ConditionalNearTiles -> - relevantTile != null && relevantTile!!.getTilesInDistance(condition.params[0].toInt()).any { + state.relevantTile != null && state.relevantTile!!.getTilesInDistance(condition.params[0].toInt()).any { it.matchesFilter(condition.params[1]) } UniqueType.ConditionalVsLargerCiv -> { - val yourCities = relevantCiv?.cities?.size ?: 1 + val yourCities = state.relevantCiv?.cities?.size ?: 1 val theirCities = state.theirCombatant?.getCivInfo()?.cities?.size ?: 0 yourCities < theirCities } UniqueType.ConditionalForeignContinent -> checkOnCiv { - relevantTile != null && ( + state.relevantTile != null && ( cities.isEmpty() || getCapital() == null - || getCapital()!!.getCenterTile().getContinent() != relevantTile!!.getContinent() + || getCapital()!!.getCenterTile().getContinent() != state.relevantTile!!.getContinent() ) } UniqueType.ConditionalAdjacentUnit -> - relevantCiv != null && - relevantUnit != null && - relevantTile!!.neighbors.any { + state.relevantCiv != null && + state.relevantUnit != null && + state.relevantTile!!.neighbors.any { it.getUnits().any { - it != relevantUnit && - it.civ == relevantCiv && + it != state.relevantUnit && + it.civ == state.relevantCiv && it.matchesFilter(condition.params[0]) } } UniqueType.ConditionalNeighborTiles -> - relevantTile != null - && relevantTile!!.neighbors.count { - it.matchesFilter(condition.params[2], relevantCiv) + state.relevantTile != null + && state.relevantTile!!.neighbors.count { + it.matchesFilter(condition.params[2], state.relevantCiv) } in condition.params[0].toInt()..condition.params[1].toInt() UniqueType.ConditionalOnWaterMaps -> state.region?.continentID == -1 @@ -319,7 +264,7 @@ object Conditionals { unique != null && unique.sourceObjectType == UniqueTarget.Tech && checkOnGameInfo { civilizations.none { - it != relevantCiv && it.isMajorCiv() + it != state.relevantCiv && it.isMajorCiv() && it.tech.isResearched(unique.sourceObjectName!!) // guarded by the sourceObjectType check } } @@ -327,7 +272,7 @@ object Conditionals { unique != null && unique.sourceObjectType == UniqueTarget.Policy && checkOnGameInfo { civilizations.none { - it != relevantCiv && it.isMajorCiv() + it != state.relevantCiv && it.isMajorCiv() && it.policies.isAdopted(unique.sourceObjectName!!) // guarded by the sourceObjectType check } } diff --git a/core/src/com/unciv/models/ruleset/unique/Countables.kt b/core/src/com/unciv/models/ruleset/unique/Countables.kt new file mode 100644 index 0000000000..a3043aa5ca --- /dev/null +++ b/core/src/com/unciv/models/ruleset/unique/Countables.kt @@ -0,0 +1,30 @@ +package com.unciv.models.ruleset.unique + +import com.unciv.models.stats.Stat + +object Countables { + + fun getCountableAmount(countable: String, stateForConditionals: StateForConditionals): Int? { + if (countable.toIntOrNull() != null) return countable.toInt() + + val relevantStat = Stat.safeValueOf(countable) + + if (relevantStat != null) { + return if (stateForConditionals.relevantCity != null) { + stateForConditionals.relevantCity!!.getStatReserve(relevantStat) + } else if (relevantStat in Stat.statsWithCivWideField && stateForConditionals.relevantCiv != null) { + stateForConditionals.relevantCiv!!.getStatReserve(relevantStat) + } else { + null + } + } + + if (stateForConditionals.gameInfo == null) return null + + if (countable == "year") return stateForConditionals.gameInfo!!.getYear(stateForConditionals.gameInfo!!.turns) + if (stateForConditionals.gameInfo!!.ruleset.tileResources.containsKey(countable)) + return stateForConditionals.getResourceAmount(countable) + + return null + } +} diff --git a/core/src/com/unciv/models/ruleset/unique/IHasUniques.kt b/core/src/com/unciv/models/ruleset/unique/IHasUniques.kt index babc8927db..2b49dd23e2 100644 --- a/core/src/com/unciv/models/ruleset/unique/IHasUniques.kt +++ b/core/src/com/unciv/models/ruleset/unique/IHasUniques.kt @@ -37,7 +37,10 @@ interface IHasUniques : INamed { fun getMatchingUniques(uniqueTemplate: String, stateForConditionals: StateForConditionals? = null): Sequence { val matchingUniques = uniqueMap[uniqueTemplate] ?: return sequenceOf() - return matchingUniques.asSequence().filter { it.conditionalsApply(stateForConditionals ?: StateForConditionals()) } + + val actualStateForConditionals = stateForConditionals ?: StateForConditionals() + val uniques = matchingUniques.asSequence().filter { it.conditionalsApply(actualStateForConditionals) } + return uniques.flatMap { it.getMultiplied(actualStateForConditionals) } } fun getMatchingUniques(uniqueType: UniqueType, stateForConditionals: StateForConditionals? = null) = diff --git a/core/src/com/unciv/models/ruleset/unique/StateForConditionals.kt b/core/src/com/unciv/models/ruleset/unique/StateForConditionals.kt index 3de770a774..6de48d5cd4 100644 --- a/core/src/com/unciv/models/ruleset/unique/StateForConditionals.kt +++ b/core/src/com/unciv/models/ruleset/unique/StateForConditionals.kt @@ -39,6 +39,38 @@ data class StateForConditionals( combatAction ) + + val relevantUnit by lazy { + if (ourCombatant != null && ourCombatant is MapUnitCombatant) ourCombatant.unit + else unit + } + + val relevantTile by lazy { attackedTile + ?: tile + // We need to protect against conditionals checking tiles for units pre-placement - see #10425, #10512 + ?: relevantUnit?.run { if (hasTile()) getTile() else null } + ?: city?.getCenterTile() + } + + val relevantCity by lazy { + city + ?: relevantTile?.getCity() + } + + val relevantCiv by lazy { + civInfo ?: + relevantCity?.civ ?: + relevantUnit?.civ + } + + val gameInfo by lazy { relevantCiv?.gameInfo } + + fun getResourceAmount(resourceName: String): Int { + if (relevantCity != null) return relevantCity!!.getAvailableResourceAmount(resourceName) + if (relevantCiv != null) return relevantCiv!!.getResourceAmount(resourceName) + return 0 + } + companion object { val IgnoreConditionals = StateForConditionals(ignoreConditionals = true) } @@ -67,4 +99,7 @@ data class StateForConditionals( result = 31 * result + ignoreConditionals.hashCode() return result } + + } + diff --git a/core/src/com/unciv/models/ruleset/unique/Unique.kt b/core/src/com/unciv/models/ruleset/unique/Unique.kt index 2191fc8646..8677866c81 100644 --- a/core/src/com/unciv/models/ruleset/unique/Unique.kt +++ b/core/src/com/unciv/models/ruleset/unique/Unique.kt @@ -68,6 +68,30 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s return true } + private fun getUniqueMultiplier(stateForConditionals: StateForConditionals = StateForConditionals()): Int { + val multiplierConditionals = conditionals.filter { it.type == UniqueType.ForEveryCountable } + if (multiplierConditionals.isEmpty()) return 1 + var amount = 1 + for (conditional in multiplierConditionals) { // multiple multipliers DO multiply. + val multiplier = Countables.getCountableAmount(conditional.params[0], stateForConditionals) + if (multiplier != null) amount *= multiplier + } + return amount.coerceAtLeast(0) + } + + /** Multiplies the unique according to the multiplication conditionals */ + fun getMultiplied(stateForConditionals: StateForConditionals = StateForConditionals()): Sequence { + val multiplier = getUniqueMultiplier(stateForConditionals) + return EndlessSequenceOf(this).take(multiplier) + } + + private class EndlessSequenceOf(private val value: T) : Sequence { + override fun iterator(): Iterator = object : Iterator { + override fun next() = value + override fun hasNext() = true + } + } + fun getDeprecationAnnotation(): Deprecated? = type?.getDeprecationAnnotation() fun getSourceNameForUser(): String { @@ -244,9 +268,6 @@ class UniqueMap() : HashMap>() { && unique.conditionalsApply(stateForConditionals) } } - - /** This is an alias for [containsKey] to clarify when a pure string-based check is legitimate. */ - fun containsFilteringUnique(filter: String) = containsKey(filter) } diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt index d290e2b875..40330995a4 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt @@ -87,7 +87,12 @@ object UniqueTriggerActivation { val timingConditional = unique.conditionals.firstOrNull { it.type == UniqueType.ConditionalTimedUnique } if (timingConditional != null) { - return { civInfo.temporaryUniques.add(TemporaryUnique(unique, timingConditional.params[0].toInt())) } + return { + civInfo.temporaryUniques.add(TemporaryUnique(unique, timingConditional.params[0].toInt())) + if (unique.type in setOf(UniqueType.ProvidesResources, UniqueType.ConsumesResources)) + civInfo.cache.updateCivResources() + true + } } val stateForConditionals = StateForConditionals(civInfo, city, unit, tile) diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index e22475e622..93823b9eb9 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -870,6 +870,7 @@ enum class UniqueType( HiddenWithoutVictoryType("Hidden when [victoryType] Victory is disabled", UniqueTarget.Building, UniqueTarget.Unit, flags = UniqueFlag.setOfHiddenToUsers), HiddenFromCivilopedia("Will not be displayed in Civilopedia", *UniqueTarget.Displayable, flags = UniqueFlag.setOfHiddenToUsers), ModifierHiddenFromUsers("hidden from users", UniqueTarget.MetaModifier), + ForEveryCountable("for every [countable]", UniqueTarget.MetaModifier), Comment("Comment [comment]", *UniqueTarget.Displayable, docDescription = "Allows displaying arbitrary text in a Unique listing. Only the text within the '[]' brackets will be displayed, the rest serves to allow Ruleset validation to recognize the intent."), diff --git a/docs/Modders/uniques.md b/docs/Modders/uniques.md index 57a7f82546..0c38586650 100644 --- a/docs/Modders/uniques.md +++ b/docs/Modders/uniques.md @@ -2479,6 +2479,11 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl ??? example "<hidden from users>" Applicable to: MetaModifier +??? example "<for every [countable]>" + Example: "<for every [1000]>" + + Applicable to: MetaModifier + *[amount]: This indicates a whole number, possibly with a + or - sign, such as `2`, `+13`, or `-3`. *[baseTerrain]: The name of any terrain that is a base terrain according to the json file. diff --git a/tests/src/com/unciv/uniques/ResourceTests.kt b/tests/src/com/unciv/uniques/ResourceTests.kt index 9c49d84333..3d58b616a1 100644 --- a/tests/src/com/unciv/uniques/ResourceTests.kt +++ b/tests/src/com/unciv/uniques/ResourceTests.kt @@ -1,5 +1,7 @@ package com.unciv.uniques +import com.unciv.models.ruleset.unique.Unique +import com.unciv.models.ruleset.unique.UniqueTriggerActivation import com.unciv.testing.GdxTestRunner import com.unciv.testing.TestGame import org.junit.Assert @@ -121,4 +123,16 @@ class ResourceTests { city.cityConstructions.addBuilding(doubleStrategic) Assert.assertTrue(civInfo.getCivResourcesByName()["Coal"] == 4) } + + @Test + fun testPerCountableForGlobalAndLocalResources() { + // one coal provided locally + val consumesCoal = game.createBuilding("Provides [1] [Coal]") + city.cityConstructions.addBuilding(consumesCoal) + // one globally + UniqueTriggerActivation.triggerUnique(Unique("Provides [1] [Coal] "), civInfo) + val providesFaithPerCoal = game.createBuilding("[+1 Faith] [in this city] ") + city.cityConstructions.addBuilding(providesFaithPerCoal) + Assert.assertEquals(2f, city.cityStats.currentCityStats.faith) + } }