Allow filter uniques to have conditionals and work with modifiers (#12404)

* Allow filter uniques to have conditionals

* Move cache to inside the multifilter

* Move Civ caching to nation

* Might as well remove a use of let

* Forgot to remove check

* Might as well edit these for consistency

* Avoid checking unit filters twice where necessary

* Probably the wrong way to fix this, Idk where I should put the fix instead

* Remove extra parenthesis

* Don't multiFilter if we know it won't do anything

* Apparently, issues for initialized tiles can exist for cities too

* Lower cpu cost for changes - use consistent StateForConditionals, set once when loading map and again every time there is an ownership change

* Resolves #12483 - add gameInfo to tile state so game-wide conditionals will work

---------

Co-authored-by: yairm210 <yairm210@hotmail.com>
This commit is contained in:
SeventhM 2024-11-17 01:37:47 -08:00 committed by GitHub
parent 06f6449c6c
commit 405655d51d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 257 additions and 125 deletions

View File

@ -39,10 +39,11 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) {
private val constructionsToAvoid = personality.getMatchingUniques(UniqueType.WillNotBuild, StateForConditionals(city))
.map{ it.params[0] }
private fun shouldAvoidConstruction (construction: IConstruction): Boolean {
val stateForConditionals = StateForConditionals(city)
for (toAvoid in constructionsToAvoid) {
if (construction is Building && construction.matchesFilter(toAvoid))
if (construction is Building && construction.matchesFilter(toAvoid, stateForConditionals))
return true
if (construction is BaseUnit && construction.matchesFilter(toAvoid))
if (construction is BaseUnit && construction.matchesFilter(toAvoid, stateForConditionals))
return true
}
return false

View File

@ -539,7 +539,7 @@ object NextTurnAutomation {
val settlerUnits = civInfo.gameInfo.ruleset.units.values
.filter { it.isCityFounder() && it.isBuildable(civInfo) &&
personality.getMatchingUniques(UniqueType.WillNotBuild, StateForConditionals(civInfo))
.none { unique -> it.matchesFilter(unique.params[0]) } }
.none { unique -> it.matchesFilter(unique.params[0], StateForConditionals(civInfo)) } }
if (settlerUnits.isEmpty()) return
if (civInfo.units.getCivUnits().count { it.isMilitary() } < civInfo.cities.size) return // We need someone to defend them first

View File

@ -17,6 +17,7 @@ import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.Terrain
import com.unciv.models.ruleset.tile.TileImprovement
import com.unciv.models.ruleset.unique.LocalUniqueCache
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats
@ -337,7 +338,7 @@ class WorkerAutomation(
if (tile.improvementInProgress != null) return ruleSet.tileImprovements[tile.improvementInProgress!!]
val potentialTileImprovements = ruleSet.tileImprovements.filter {
(it.value.uniqueTo == null || unit.civ.matchesFilter(it.value.uniqueTo!!))
(it.value.uniqueTo == null || unit.civ.matchesFilter(it.value.uniqueTo!!, StateForConditionals(unit = unit, tile = tile)))
&& unit.canBuildImprovement(it.value, tile)
&& tile.improvementFunctions.canBuildImprovement(it.value, civInfo)
}

View File

@ -533,7 +533,7 @@ object Battle {
for (unit in greatGeneralUnits) {
val greatGeneralPointsBonus = thisCombatant
.getMatchingUniques(UniqueType.GreatPersonEarnedFaster, stateForConditionals, true)
.filter { unit.matchesFilter(it.params[0]) }
.filter { unit.matchesFilter(it.params[0], stateForConditionals) }
.sumOf { it.params[1].toDouble() }
val greatGeneralPointsModifier = 1.0 + greatGeneralPointsBonus / 100

View File

@ -25,7 +25,9 @@ class CityCombatant(val city: City) : ICombatant {
override fun isDefeated(): Boolean = city.health == 1
override fun isInvisible(to: Civilization): Boolean = false
override fun canAttack(): Boolean = city.canBombard()
override fun matchesFilter(filter: String) = MultiFilter.multiFilter(filter, {it == "City" || it in Constants.all || city.matchesFilter(it)})
override fun matchesFilter(filter: String, multiFilter: Boolean) =
if (multiFilter) MultiFilter.multiFilter(filter, { it == "City" || it in Constants.all || city.matchesFilter(it, multiFilter = false) })
else filter == "City" || filter in Constants.all || city.matchesFilter(filter, multiFilter = false)
override fun getAttackSound() = UncivSound.Bombard
override fun takeDamage(damage: Int) {

View File

@ -19,7 +19,7 @@ interface ICombatant {
fun isInvisible(to: Civilization): Boolean
fun canAttack(): Boolean
/** Implements [UniqueParameterType.CombatantFilter][com.unciv.models.ruleset.unique.UniqueParameterType.CombatantFilter] */
fun matchesFilter(filter: String): Boolean
fun matchesFilter(filter: String, multiFilter: Boolean = true): Boolean
fun getAttackSound(): UncivSound
fun isMelee(): Boolean = !isRanged()

View File

@ -18,7 +18,7 @@ class MapUnitCombatant(val unit: MapUnit) : ICombatant {
override fun isDefeated(): Boolean = unit.health <= 0
override fun isInvisible(to: Civilization): Boolean = unit.isInvisible(to)
override fun canAttack(): Boolean = unit.canAttack()
override fun matchesFilter(filter: String) = unit.matchesFilter(filter)
override fun matchesFilter(filter: String, multiFilter: Boolean) = unit.matchesFilter(filter, multiFilter)
override fun getAttackSound() = unit.baseUnit.attackSound.let {
if (it == null) UncivSound.Click else UncivSound(it)
}

View File

@ -149,6 +149,7 @@ class City : IsPartOfGameInfoSerialization, INamed {
fun canBombard() = !attackedThisTurn && !isInResistance()
fun getCenterTile(): Tile = centerTile
fun getCenterTileOrNull(): Tile? = if (::centerTile.isInitialized) centerTile else null
fun getTiles(): Sequence<Tile> = tiles.asSequence().map { tileMap[it] }
fun getWorkableTiles() = tilesInRange.asSequence().filter { it.getOwner() == civ }
fun isWorked(tile: Tile) = workedTiles.contains(tile.position)
@ -164,7 +165,7 @@ class City : IsPartOfGameInfoSerialization, INamed {
val indicatorBuildings = getRuleset().buildings.values.asSequence()
.filter { it.hasUnique(UniqueType.IndicatesCapital) }
val civSpecificBuilding = indicatorBuildings.firstOrNull { it.uniqueTo != null && civ.matchesFilter(it.uniqueTo!!) }
val civSpecificBuilding = indicatorBuildings.firstOrNull { it.uniqueTo != null && civ.matchesFilter(it.uniqueTo!!, StateForConditionals(this)) }
return civSpecificBuilding ?: indicatorBuildings.firstOrNull()
}
@ -425,8 +426,10 @@ class City : IsPartOfGameInfoSerialization, INamed {
}
/** Implements [UniqueParameterType.CityFilter][com.unciv.models.ruleset.unique.UniqueParameterType.CityFilter] */
fun matchesFilter(filter: String, viewingCiv: Civilization? = civ): Boolean {
return MultiFilter.multiFilter(filter, { matchesSingleFilter(it, viewingCiv) })
fun matchesFilter(filter: String, viewingCiv: Civilization? = civ, multiFilter: Boolean = true): Boolean {
return if (multiFilter)
MultiFilter.multiFilter(filter, { matchesSingleFilter(it, viewingCiv) })
else matchesSingleFilter(filter, viewingCiv)
}
private fun matchesSingleFilter(filter: String, viewingCiv: Civilization? = civ): Boolean {
@ -464,7 +467,7 @@ class City : IsPartOfGameInfoSerialization, INamed {
// this will always be true when checked.
"in cities following this religion" -> true
"in cities following our religion" -> viewingCiv?.religionManager?.religion == religion.getMajorityReligion()
else -> civ.matchesFilter(filter)
else -> civ.matchesFilter(filter, StateForConditionals(this), false)
}
}

View File

@ -241,7 +241,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
fun getBuiltBuildings(): Sequence<Building> = builtBuildingObjects.asSequence()
fun containsBuildingOrEquivalent(buildingNameOrUnique: String): Boolean =
isBuilt(buildingNameOrUnique) || getBuiltBuildings().any { it.replaces == buildingNameOrUnique || it.hasTagUnique(buildingNameOrUnique) }
isBuilt(buildingNameOrUnique) || getBuiltBuildings().any { it.replaces == buildingNameOrUnique || it.hasUnique(buildingNameOrUnique, StateForConditionals(city)) }
fun getWorkDone(constructionName: String): Int {
return if (inProgressConstructions.containsKey(constructionName)) inProgressConstructions[constructionName]!!
@ -565,11 +565,11 @@ class CityConstructions : IsPartOfGameInfoSerialization {
UniqueTriggerActivation.triggerUnique(unique, city, triggerNotificationText = triggerNotificationText)
for (unique in city.civ.getTriggeredUniques(UniqueType.TriggerUponConstructingBuilding, stateForConditionals)
{ building.matchesFilter(it.params[0]) })
{ building.matchesFilter(it.params[0], stateForConditionals) })
UniqueTriggerActivation.triggerUnique(unique, city, triggerNotificationText = triggerNotificationText)
for (unique in city.civ.getTriggeredUniques(UniqueType.TriggerUponConstructingBuildingCityFilter, stateForConditionals)
{ building.matchesFilter(it.params[0]) && city.matchesFilter(it.params[1]) })
{ building.matchesFilter(it.params[0], stateForConditionals) && city.matchesFilter(it.params[1]) })
UniqueTriggerActivation.triggerUnique(unique, city, triggerNotificationText = triggerNotificationText)
}
@ -674,15 +674,15 @@ class CityConstructions : IsPartOfGameInfoSerialization {
?: return false // We should never end up here anyway, so things have already gone _way_ wrong
city.addStat(stat, -1 * constructionCost)
val conditionalState = StateForConditionals(civInfo = city.civ, city = city)
val conditionalState = StateForConditionals(city)
if ((
city.civ.getMatchingUniques(UniqueType.BuyUnitsIncreasingCost, conditionalState) +
city.civ.getMatchingUniques(UniqueType.BuyBuildingsIncreasingCost, conditionalState)
).any {
(
construction is BaseUnit && construction.matchesFilter(it.params[0]) ||
construction is Building && construction.matchesFilter(it.params[0])
construction is BaseUnit && construction.matchesFilter(it.params[0], conditionalState) ||
construction is Building && construction.matchesFilter(it.params[0], conditionalState)
)
&& city.matchesFilter(it.params[3])
&& it.params[2] == stat.name

View File

@ -304,8 +304,9 @@ class CityStats(val city: City) {
}
private fun constructionMatchesFilter(construction: IConstruction, filter: String): Boolean {
if (construction is Building) return construction.matchesFilter(filter)
if (construction is BaseUnit) return construction.matchesFilter(filter)
val state = StateForConditionals(city)
if (construction is Building) return construction.matchesFilter(filter, state)
if (construction is BaseUnit) return construction.matchesFilter(filter, state)
return false
}

View File

@ -551,19 +551,16 @@ class Civilization : IsPartOfGameInfoSerialization {
yieldAll(gameInfo.ruleset.globalUniques.uniqueMap.getTriggeredUniques(trigger, stateForConditionals, triggerFilter))
}.toList() // Triggers can e.g. add buildings which contain triggers, causing concurrent modification errors
@Transient
private val cachedMatchesFilterResult = HashMap<String, Boolean>()
/** Implements [UniqueParameterType.CivFilter][com.unciv.models.ruleset.unique.UniqueParameterType.CivFilter] */
fun matchesFilter(filter: String): Boolean =
cachedMatchesFilterResult.getOrPut(filter) { MultiFilter.multiFilter(filter, ::matchesSingleFilter ) }
fun matchesFilter(filter: String, state: StateForConditionals? = StateForConditionals(this), multiFilter: Boolean = true): Boolean =
if (multiFilter) MultiFilter.multiFilter(filter, { matchesSingleFilter(it, state) })
else matchesSingleFilter(filter, state)
fun matchesSingleFilter(filter: String): Boolean {
fun matchesSingleFilter(filter: String, state: StateForConditionals? = StateForConditionals(this)): Boolean {
return when (filter) {
"Human player" -> isHuman()
"AI player" -> isAI()
else -> nation.matchesFilter(filter)
else -> nation.matchesFilter(filter, state, false)
}
}

View File

@ -316,7 +316,7 @@ class TechManager : IsPartOfGameInfoSerialization {
if (!unique.hasTriggerConditional() && unique.conditionalsApply(StateForConditionals(civInfo)))
UniqueTriggerActivation.triggerUnique(unique, civInfo, triggerNotificationText = triggerNotificationText)
for (unique in civInfo.getTriggeredUniques(UniqueType.TriggerUponResearch) { newTech.matchesFilter(it.params[0]) })
for (unique in civInfo.getTriggeredUniques(UniqueType.TriggerUponResearch) { newTech.matchesFilter(it.params[0], StateForConditionals(civInfo)) })
UniqueTriggerActivation.triggerUnique(unique, civInfo, triggerNotificationText = triggerNotificationText)

View File

@ -128,6 +128,10 @@ class MapUnit : IsPartOfGameInfoSerialization {
@Transient
private var tempUniquesMap = UniqueMap()
@Transient
/** Temp map excluding unit uniques*/
private var nonUnitUniquesMap = UniqueMap()
@Transient
val movement = UnitMovement(this)
@ -563,8 +567,8 @@ class MapUnit : IsPartOfGameInfoSerialization {
}
/** Implements [UniqueParameterType.MapUnitFilter][com.unciv.models.ruleset.unique.UniqueParameterType.MapUnitFilter] */
fun matchesFilter(filter: String): Boolean {
return MultiFilter.multiFilter(filter, ::matchesSingleFilter)
fun matchesFilter(filter: String, multiFilter: Boolean = true): Boolean {
return if (multiFilter) MultiFilter.multiFilter(filter, ::matchesSingleFilter) else matchesSingleFilter(filter)
}
private fun matchesSingleFilter(filter: String): Boolean {
@ -575,9 +579,10 @@ class MapUnit : IsPartOfGameInfoSerialization {
Constants.embarked -> isEmbarked()
"Non-City" -> true
else -> {
if (baseUnit.matchesFilter(filter)) return true
if (civ.matchesFilter(filter)) return true
if (tempUniquesMap.hasTagUnique(filter)) return true
val state = StateForConditionals(this)
if (baseUnit.matchesFilter(filter, state, false)) return true
if (civ.matchesFilter(filter, state, false)) return true
if (nonUnitUniquesMap.hasUnique(filter, state))
if (promotions.promotions.contains(filter)) return true
return false
}
@ -597,7 +602,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
return buildImprovementUniques.any()
}
return buildImprovementUniques
.any { improvement.matchesFilter(it.params[0]) || tile.matchesTerrainFilter(it.params[0]) }
.any { improvement.matchesFilter(it.params[0], StateForConditionals(this)) || tile.matchesTerrainFilter(it.params[0]) }
}
fun getReligionDisplayName(): String? {
@ -654,13 +659,15 @@ class MapUnit : IsPartOfGameInfoSerialization {
}
fun updateUniques() {
val uniqueSources =
baseUnit.uniqueObjects.asSequence() +
type.uniqueObjects +
promotions.getPromotions().flatMap { it.uniqueObjects } +
statuses.flatMap { it.uniques }
val unitUniqueSources =
baseUnit.uniqueObjects.asSequence() +
type.uniqueObjects
val otherUniqueSources = promotions.getPromotions().flatMap { it.uniqueObjects } +
statuses.flatMap { it.uniques }
val uniqueSources = unitUniqueSources + otherUniqueSources
tempUniquesMap = UniqueMap(uniqueSources)
nonUnitUniquesMap = UniqueMap(otherUniqueSources)
cache.updateUniques()
}

View File

@ -366,8 +366,8 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
fun isRoughTerrain() = allTerrains.any { it.isRough() }
@delegate:Transient
private val stateThisTile: StateForConditionals by lazy { StateForConditionals(tile = this) }
@Transient
private var stateThisTile: StateForConditionals = StateForConditionals.EmptyState
/** Checks whether any of the TERRAINS of this tile has a certain unique */
fun terrainHasUnique(uniqueType: UniqueType, state: StateForConditionals = stateThisTile) =
terrainUniqueMap.getMatchingUniques(uniqueType, state).any()
@ -476,13 +476,14 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
if ((improvement == null || improvementIsPillaged) && filter == "unimproved") return true
if (improvement != null && !improvementIsPillaged && filter == "improved") return true
if (ignoreImprovement) return false
if (getUnpillagedTileImprovement()?.matchesFilter(filter) == true) return true
return getUnpillagedRoadImprovement()?.matchesFilter(filter) == true
if (getUnpillagedTileImprovement()?.matchesFilter(filter, stateThisTile, false) == true) return true
return getUnpillagedRoadImprovement()?.matchesFilter(filter, stateThisTile, false) == true
}
/** Implements [UniqueParameterType.TerrainFilter][com.unciv.models.ruleset.unique.UniqueParameterType.TerrainFilter] */
fun matchesTerrainFilter(filter: String, observingCiv: Civilization? = null): Boolean {
return MultiFilter.multiFilter(filter, { matchesSingleTerrainFilter(it, observingCiv) })
fun matchesTerrainFilter(filter: String, observingCiv: Civilization? = null, multiFilter: Boolean = true): Boolean {
return if (multiFilter) MultiFilter.multiFilter(filter, { matchesSingleTerrainFilter(it, observingCiv) })
else matchesSingleTerrainFilter(filter, observingCiv)
}
private fun matchesSingleTerrainFilter(filter: String, observingCiv: Civilization? = null): Boolean {
@ -508,8 +509,9 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
Constants.freshWaterFilter -> isAdjacentTo(Constants.freshWater, observingCiv)
else -> {
if (allTerrains.any { it.matchesFilter(filter) }) return true
if (getOwner()?.matchesFilter(filter) == true) return true
val owner = getOwner()
if (allTerrains.any { it.matchesFilter(filter, stateThisTile, false) }) return true
if (owner != null && owner.matchesFilter(filter, stateThisTile, false)) return true
// Resource type check is last - cannot succeed if no resource here
if (resource == null) return false
@ -520,7 +522,7 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
val resourceObject = tileResource
val hasResourceWithFilter =
tileResource.name == filter
|| tileResource.hasTagUnique(filter)
|| tileResource.hasUnique(filter, stateThisTile)
|| filter.removeSuffix(" resource") == tileResource.resourceType.name
if (!hasResourceWithFilter) return false
@ -733,6 +735,10 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
}
fun setOwnerTransients() {
// If it has an owning city, the state was already set in setOwningCity
if (owningCity == null) stateThisTile = StateForConditionals(tile = this,
// When generating maps we call this function but there's no gameinfo
gameInfo = if (tileMap.hasGameInfo()) tileMap.gameInfo else null)
if (owningCity == null && roadOwner != "")
getRoadOwner()!!.neutralRoads.add(this.position)
}
@ -749,6 +755,7 @@ class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
roadOwner = ""
}
owningCity = city
stateThisTile = StateForConditionals(tile = this, city = city, gameInfo = tileMap.gameInfo)
isCityCenterInternal = getCity()?.location == position
}

View File

@ -150,7 +150,7 @@ class TileImprovementFunctions(val tile: Tile) {
// Can't build if any terrain specifically prevents building this improvement
tile.getTerrainMatchingUniques(UniqueType.RestrictedBuildableImprovements, stateForConditionals).any {
unique -> !improvement.matchesFilter(unique.params[0])
unique -> !improvement.matchesFilter(unique.params[0], StateForConditionals(tile = tile))
} -> false
// Can't build if the improvement specifically prevents building on some present feature
@ -260,12 +260,12 @@ class TileImprovementFunctions(val tile: Tile) {
UniqueTriggerActivation.triggerUnique(unique, civ, unit = unit, tile = tile)
for (unique in civ.getTriggeredUniques(UniqueType.TriggerUponBuildingImprovement, stateForConditionals)
{ improvement.matchesFilter(it.params[0]) })
{ improvement.matchesFilter(it.params[0], StateForConditionals(civ, unit = unit, tile = tile)) })
UniqueTriggerActivation.triggerUnique(unique, civ, unit = unit, tile = tile)
if (unit == null) return
for (unique in unit.getTriggeredUniques(UniqueType.TriggerUponBuildingImprovement, stateForConditionals)
{ improvement.matchesFilter(it.params[0]) })
{ improvement.matchesFilter(it.params[0], StateForConditionals(civ, unit = unit, tile = tile)) })
UniqueTriggerActivation.triggerUnique(unique, civ, unit = unit, tile = tile)
}

View File

@ -78,9 +78,9 @@ class TileStatFunctions(val tile: Tile) {
val tileType = unique.params[1]
if (tile.matchesFilter(tileType, observingCiv, true))
listOfStats.add("{${unique.sourceObjectName}} ({${unique.getDisplayText()}})" to unique.stats)
else if (improvement != null && improvement.matchesFilter(tileType))
else if (improvement != null && improvement.matchesFilter(tileType, StateForConditionals(city = city, tile = tile)))
improvementStats.add(unique.stats)
else if (road != null && road.matchesFilter(tileType))
else if (road != null && road.matchesFilter(tileType, StateForConditionals(city = city, tile = tile)))
roadStats.add(unique.stats)
}
}
@ -186,9 +186,9 @@ class TileStatFunctions(val tile: Tile) {
fun addStats(filter: String, stat: Stat, amount: Float) {
if (tile.matchesFilter(filter, observingCiv, true))
terrainStats.add(stat, amount)
else if (improvement != null && improvement.matchesFilter(filter))
else if (improvement != null && improvement.matchesFilter(filter, StateForConditionals(city = city, tile = tile)))
improvementStats.add(stat, amount)
else if (road != null && road.matchesFilter(filter))
else if (road != null && road.matchesFilter(filter, StateForConditionals(city = city, tile = tile)))
roadStats.add(stat, amount)
}

View File

@ -82,18 +82,20 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
localUniqueCache: LocalUniqueCache = LocalUniqueCache(false)): Stats {
// Calls the clone function of the NamedStats this class is derived from, not a clone function of this class
val stats = cloneStats()
val conditionalState = StateForConditionals(city)
for (unique in localUniqueCache.forCityGetMatchingUniques(city, UniqueType.StatsFromObject)) {
if (!matchesFilter(unique.params[1])) continue
if (!matchesFilter(unique.params[1], conditionalState)) continue
stats.add(unique.stats)
}
for (unique in getMatchingUniques(UniqueType.Stats, StateForConditionals(city.civ, city)))
for (unique in getMatchingUniques(UniqueType.Stats, conditionalState))
stats.add(unique.stats)
if (!isWonder)
for (unique in localUniqueCache.forCityGetMatchingUniques(city, UniqueType.StatsFromBuildings)) {
if (matchesFilter(unique.params[1]))
if (matchesFilter(unique.params[1], conditionalState))
stats.add(unique.stats)
}
return stats
@ -102,14 +104,16 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
fun getStatPercentageBonuses(city: City?, localUniqueCache: LocalUniqueCache = LocalUniqueCache(false)): Stats {
val stats = percentStatBonus?.clone() ?: Stats()
val civInfo = city?.civ ?: return stats // initial stats
val conditionalState = StateForConditionals(city)
for (unique in localUniqueCache.forCivGetMatchingUniques(civInfo, UniqueType.StatPercentFromObject)) {
if (matchesFilter(unique.params[2]))
if (matchesFilter(unique.params[2], conditionalState))
stats.add(Stat.valueOf(unique.params[1]), unique.params[0].toFloat())
}
for (unique in localUniqueCache.forCivGetMatchingUniques(civInfo, UniqueType.AllStatsPercentFromObject)) {
if (!matchesFilter(unique.params[1])) continue
if (!matchesFilter(unique.params[1], conditionalState)) continue
for (stat in Stat.entries) {
stats.add(stat, unique.params[0].toFloat())
}
@ -159,21 +163,21 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
city.getMatchingUniques(UniqueType.BuyBuildingsIncreasingCost, conditionalState)
.any {
it.params[2] == stat.name
&& matchesFilter(it.params[0])
&& matchesFilter(it.params[0], conditionalState)
&& city.matchesFilter(it.params[3])
}
|| city.getMatchingUniques(UniqueType.BuyBuildingsByProductionCost, conditionalState)
.any { it.params[1] == stat.name && matchesFilter(it.params[0]) }
.any { it.params[1] == stat.name && matchesFilter(it.params[0], conditionalState) }
|| city.getMatchingUniques(UniqueType.BuyBuildingsWithStat, conditionalState)
.any {
it.params[1] == stat.name
&& matchesFilter(it.params[0])
&& matchesFilter(it.params[0], conditionalState)
&& city.matchesFilter(it.params[2])
}
|| city.getMatchingUniques(UniqueType.BuyBuildingsForAmountStat, conditionalState)
.any {
it.params[2] == stat.name
&& matchesFilter(it.params[0])
&& matchesFilter(it.params[0], conditionalState)
&& city.matchesFilter(it.params[3])
}
|| super.canBePurchasedWithStat(city, stat)
@ -190,7 +194,7 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
yieldAll(city.getMatchingUniques(UniqueType.BuyBuildingsIncreasingCost, conditionalState)
.filter {
it.params[2] == stat.name
&& matchesFilter(it.params[0])
&& matchesFilter(it.params[0], conditionalState)
&& city.matchesFilter(it.params[3])
}.map {
getCostForConstructionsIncreasingInPrice(
@ -201,13 +205,13 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
}
)
yieldAll(city.getMatchingUniques(UniqueType.BuyBuildingsByProductionCost, conditionalState)
.filter { it.params[1] == stat.name && matchesFilter(it.params[0]) }
.filter { it.params[1] == stat.name && matchesFilter(it.params[0], conditionalState) }
.map { (getProductionCost(city.civ, city) * it.params[2].toInt()).toFloat() }
)
if (city.getMatchingUniques(UniqueType.BuyBuildingsWithStat, conditionalState)
.any {
it.params[1] == stat.name
&& matchesFilter(it.params[0])
&& matchesFilter(it.params[0], conditionalState)
&& city.matchesFilter(it.params[2])
}
) {
@ -216,7 +220,7 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
yieldAll(city.getMatchingUniques(UniqueType.BuyBuildingsForAmountStat, conditionalState)
.filter {
it.params[2] == stat.name
&& matchesFilter(it.params[0])
&& matchesFilter(it.params[0], conditionalState)
&& city.matchesFilter(it.params[3])
}.map { it.params[1].toInt() * city.civ.gameInfo.speed.statCostModifiers[stat]!! }
)
@ -225,13 +229,14 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
override fun getStatBuyCost(city: City, stat: Stat): Int? {
var cost = getBaseBuyCost(city, stat)?.toDouble() ?: return null
val conditionalState = StateForConditionals(city)
for (unique in city.getMatchingUniques(UniqueType.BuyItemsDiscount))
if (stat.name == unique.params[0])
cost *= unique.params[1].toPercent()
for (unique in city.getMatchingUniques(UniqueType.BuyBuildingsDiscount)) {
if (stat.name == unique.params[0] && matchesFilter(unique.params[1]))
if (stat.name == unique.params[0] && matchesFilter(unique.params[1], conditionalState))
cost *= unique.params[2].toPercent()
}
@ -348,7 +353,7 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
}
}
if (uniqueTo != null && !civ.matchesFilter(uniqueTo!!))
if (uniqueTo != null && !civ.matchesFilter(uniqueTo!!, stateForConditionals))
yield(RejectionReasonType.UniqueToOtherNation.toInstance("Unique to $uniqueTo"))
if (civ.cache.uniqueBuildings.any { it.replaces == name })
@ -480,8 +485,12 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
private val cachedMatchesFilterResult = HashMap<String, Boolean>()
/** Implements [UniqueParameterType.BuildingFilter] */
fun matchesFilter(filter: String): Boolean =
cachedMatchesFilterResult.getOrPut(filter) { MultiFilter.multiFilter(filter, ::matchesSingleFilter ) }
fun matchesFilter(filter: String, state: StateForConditionals? = null): Boolean =
MultiFilter.multiFilter(filter, {
cachedMatchesFilterResult.getOrPut(it) { matchesSingleFilter(it) } ||
state != null && hasUnique(it, state) ||
state == null && hasTagUnique(it)
})
private fun matchesSingleFilter(filter: String): Boolean {
return when (filter) {
@ -493,10 +502,9 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
"World Wonder", "World" -> isWonder
replaces -> true
else -> {
if (uniques.contains(filter)) return true
if (::ruleset.isInitialized) // False when loading ruleset and checking buildingsToRemove
for (requiredTech: String in requiredTechs())
if (ruleset.technologies[requiredTech]?.matchesFilter(filter) == true) return true
if (ruleset.technologies[requiredTech]?.matchesFilter(filter, multiFilter = false) == true) return true
val stat = Stat.safeValueOf(filter)
return (stat != null && isStatRelated(stat))
}

View File

@ -35,15 +35,18 @@ open class Policy : RulesetObject() {
fun isBranchCompleteByName(name: String) = name.endsWith(branchCompleteSuffix)
}
fun matchesFilter(filter: String): Boolean {
return MultiFilter.multiFilter(filter, ::matchesSingleFilter)
}
fun matchesFilter(filter: String, state: StateForConditionals? = null): Boolean =
MultiFilter.multiFilter(filter, {
matchesSingleFilter(filter) ||
state != null && hasUnique(filter, state) ||
state == null && hasTagUnique(filter)
})
fun matchesSingleFilter(filter: String): Boolean {
return when(filter) {
in Constants.all -> true
name -> true
"[${branch.name}] branch" -> true
in uniques -> true
else -> false
}
}

View File

@ -5,6 +5,7 @@ import com.unciv.Constants
import com.unciv.logic.MultiFilter
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetObject
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.UniqueMap
import com.unciv.models.ruleset.unique.UniqueTarget
import com.unciv.models.ruleset.unique.UniqueType
@ -257,9 +258,19 @@ class Nation : RulesetObject() {
}
}
}
fun matchesFilter(filter: String): Boolean {
return MultiFilter.multiFilter(filter, ::matchesSingleFilter)
@Transient
private val cachedMatchesFilterResult = HashMap<String, Boolean>()
fun matchesFilter(filter: String, state: StateForConditionals? = null, multiFilter: Boolean = true): Boolean {
return if (multiFilter) MultiFilter.multiFilter(filter, {
cachedMatchesFilterResult.getOrPut(it) { matchesSingleFilter(it) } ||
state != null && hasUnique(it, state) ||
state == null && hasTagUnique(it)
})
else cachedMatchesFilterResult.getOrPut(filter) { matchesSingleFilter(filter) } ||
state != null && hasUnique(filter, state) ||
state == null && hasTagUnique(filter)
}
private fun matchesSingleFilter(filter: String): Boolean {
@ -268,7 +279,7 @@ class Nation : RulesetObject() {
name -> true
"Major" -> isMajorCiv
Constants.cityStates, "City-State" -> isCityState
else -> uniques.contains(filter)
else -> false
}
}
}

View File

@ -1,9 +1,11 @@
package com.unciv.models.ruleset.tech
import com.unciv.Constants
import com.unciv.logic.MultiFilter
import com.unciv.logic.civilization.Civilization
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetObject
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueTarget
import com.unciv.models.ruleset.unique.UniqueType
@ -35,12 +37,23 @@ class Technology: RulesetObject() {
override fun era(ruleset: Ruleset) = ruleset.eras[era()]
fun matchesFilter(filter: String): Boolean {
fun matchesFilter(filter: String, state: StateForConditionals? = null, multiFilter: Boolean = true): Boolean {
return if (multiFilter) MultiFilter.multiFilter(filter, {
matchesSingleFilter(filter) ||
state != null && hasUnique(filter, state) ||
state == null && hasTagUnique(filter)
})
else matchesSingleFilter(filter) ||
state != null && hasUnique(filter, state) ||
state == null && hasTagUnique(filter)
}
fun matchesSingleFilter(filter: String): Boolean {
return when (filter) {
in Constants.all -> true
name -> true
era() -> true
else -> uniques.contains(filter)
else -> false
}
}

View File

@ -6,6 +6,7 @@ import com.unciv.logic.MultiFilter
import com.unciv.models.ruleset.Belief
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetStatsObject
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.UniqueTarget
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.components.extensions.colorFromRGB
@ -159,8 +160,16 @@ class Terrain : RulesetStatsObject() {
/** Terrain filter matching is "pure" - input always returns same output, and it's called a bajillion times */
val cachedMatchesFilterResult = HashMap<String, Boolean>()
fun matchesFilter(filter: String): Boolean =
cachedMatchesFilterResult.getOrPut(filter) { MultiFilter.multiFilter(filter, ::matchesSingleFilter ) }
fun matchesFilter(filter: String, state: StateForConditionals? = null, multiFilter: Boolean = true): Boolean {
return if (multiFilter) MultiFilter.multiFilter(filter, {
cachedMatchesFilterResult.getOrPut(it) { matchesSingleFilter(it) } ||
state != null && hasUnique(it, state) ||
state == null && hasTagUnique(it)
})
else cachedMatchesFilterResult.getOrPut(filter) { matchesSingleFilter(filter) } ||
state != null && hasUnique(filter, state) ||
state == null && hasTagUnique(filter)
}
/** Implements [UniqueParameterType.TerrainFilter][com.unciv.models.ruleset.unique.UniqueParameterType.TerrainFilter] */
fun matchesSingleFilter(filter: String): Boolean {
@ -168,14 +177,13 @@ class Terrain : RulesetStatsObject() {
in Constants.all -> true
name -> true
"Terrain" -> true
in Constants.all -> true
"Open terrain" -> !isRough()
"Rough terrain" -> isRough()
type.name -> true
"Natural Wonder" -> type == TerrainType.NaturalWonder
"Terrain Feature" -> type == TerrainType.TerrainFeature
else -> uniques.contains(filter)
else -> false
}
}

View File

@ -32,7 +32,7 @@ class TileImprovement : RulesetStatsObject() {
fun getTurnsToBuild(civInfo: Civilization, unit: MapUnit): Int {
val state = StateForConditionals(civInfo, unit = unit)
val buildSpeedUniques = unit.getMatchingUniques(UniqueType.SpecificImprovementTime, state, checkCivInfoUniques = true)
.filter { matchesFilter(it.params[1]) }
.filter { matchesFilter(it.params[1], state) }
return buildSpeedUniques
.fold(turnsToBuild.toFloat() * civInfo.gameInfo.speed.improvementBuildLengthModifier) { calculatedTurnsToBuild, unique ->
calculatedTurnsToBuild * unique.params[0].toPercent()
@ -72,8 +72,16 @@ class TileImprovement : RulesetStatsObject() {
private val cachedMatchesFilterResult = HashMap<String, Boolean>()
/** Implements [UniqueParameterType.ImprovementFilter][com.unciv.models.ruleset.unique.UniqueParameterType.ImprovementFilter] */
fun matchesFilter(filter: String): Boolean =
cachedMatchesFilterResult.getOrPut(filter) { MultiFilter.multiFilter(filter, ::matchesSingleFilter ) }
fun matchesFilter(filter: String, tileState: StateForConditionals? = null, multiFilter: Boolean = true): Boolean {
return if (multiFilter) MultiFilter.multiFilter(filter, {
cachedMatchesFilterResult.getOrPut(it) { matchesSingleFilter(it) } ||
tileState != null && hasUnique(it, tileState) ||
tileState == null && hasTagUnique(it)
})
else cachedMatchesFilterResult.getOrPut(filter) { matchesSingleFilter(filter) } ||
tileState != null && hasUnique(filter, tileState) ||
tileState == null && hasTagUnique(filter)
}
private fun matchesSingleFilter(filter: String): Boolean {
return when (filter) {
@ -83,7 +91,7 @@ class TileImprovement : RulesetStatsObject() {
"Improvement" -> true // For situations involving tileFilter
"All Road" -> isRoad()
"Great Improvement", "Great" -> isGreatImprovement()
else -> uniqueMap.hasTagUnique(filter)
else -> false
}
}

View File

@ -1,5 +1,6 @@
package com.unciv.models.ruleset.tile
import com.unciv.logic.MultiFilter
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.Belief
@ -179,11 +180,17 @@ class TileResource : RulesetStatsObject() {
}
}
fun matchesFilter(filter: String) = when (filter) {
fun matchesFilter(filter: String, state: StateForConditionals? = null): Boolean =
MultiFilter.multiFilter(filter, {
matchesSingleFilter(filter) ||
state != null && hasUnique(filter, state) ||
state == null && hasTagUnique(filter)
})
fun matchesSingleFilter(filter: String) = when (filter) {
name -> true
"any" -> true
resourceType.name -> true
in uniques -> true
else -> improvementStats?.any { filter == it.key.name } == true
}

View File

@ -110,7 +110,7 @@ object Conditionals {
UniqueType.ConditionalTutorialsEnabled -> UncivGame.Current.settings.showTutorials
UniqueType.ConditionalTutorialCompleted -> conditional.params[0] in UncivGame.Current.settings.tutorialTasksCompleted
UniqueType.ConditionalCivFilter, UniqueType.ConditionalCivFilterOld -> checkOnCiv { matchesFilter(conditional.params[0]) }
UniqueType.ConditionalCivFilter, UniqueType.ConditionalCivFilterOld -> checkOnCiv { matchesFilter(conditional.params[0], state) }
UniqueType.ConditionalWar -> checkOnCiv { isAtWar() }
UniqueType.ConditionalNotWar -> checkOnCiv { !isAtWar() }
UniqueType.ConditionalWithResource -> state.getResourceAmount(conditional.params[0]) > 0
@ -207,7 +207,7 @@ object Conditionals {
UniqueType.ConditionalWhenGarrisoned ->
checkOnCity { getCenterTile().militaryUnit?.canGarrison() == true }
UniqueType.ConditionalVsCity -> state.theirCombatant?.matchesFilter("City") == true
UniqueType.ConditionalVsCity -> state.theirCombatant?.matchesFilter("City", false) == true
UniqueType.ConditionalVsUnits, UniqueType.ConditionalVsCombatant -> state.theirCombatant?.matchesFilter(conditional.params[0]) == true
UniqueType.ConditionalOurUnit, UniqueType.ConditionalOurUnitOnUnit ->
state.relevantUnit?.matchesFilter(conditional.params[0]) == true

View File

@ -38,10 +38,16 @@ interface IHasUniques : INamed {
fun getMatchingUniques(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) =
uniqueMap.getMatchingUniques(uniqueType, state)
fun getMatchingUniques(uniqueTag: String, state: StateForConditionals = StateForConditionals.EmptyState) =
uniqueMap.getMatchingUniques(uniqueTag, state)
fun hasUnique(uniqueType: UniqueType, state: StateForConditionals? = null) =
uniqueMap.hasMatchingUnique(uniqueType, state ?: StateForConditionals.EmptyState)
fun hasUnique(uniqueTag: String, state: StateForConditionals? = null) =
uniqueMap.hasMatchingUnique(uniqueTag, state ?: StateForConditionals.EmptyState)
fun hasTagUnique(tagUnique: String) =
uniqueMap.hasTagUnique(tagUnique)

View File

@ -28,8 +28,8 @@ data class StateForConditionals(
val ignoreConditionals: Boolean = false,
) {
constructor(city: City) : this(city.civ, city, tile = city.getCenterTile())
constructor(unit: MapUnit) : this(unit.civ, unit = unit, tile = unit.currentTile)
constructor(city: City) : this(city.civ, city, tile = city.getCenterTileOrNull())
constructor(unit: MapUnit) : this(unit.civ, unit = unit, tile = if (unit.hasTile()) unit.getTile() else null)
constructor(ourCombatant: ICombatant, theirCombatant: ICombatant? = null,
attackedTile: Tile? = null, combatAction: CombatAction? = null) : this(
ourCombatant.getCivInfo(),
@ -52,7 +52,7 @@ data class StateForConditionals(
?: 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()
?: city?.getCenterTileOrNull()
}
val relevantCity by lazy {

View File

@ -298,6 +298,9 @@ open class UniqueMap() {
fun hasUnique(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueType).any { it.conditionalsApply(state) && !it.isTimedTriggerable }
fun hasUnique(uniqueTag: String, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueTag).any { it.conditionalsApply(state) && !it.isTimedTriggerable }
fun hasTagUnique(tagUnique: String) =
innerUniqueMap.containsKey(tagUnique)
@ -307,6 +310,10 @@ open class UniqueMap() {
?.asSequence()
?: emptySequence()
fun getUniques(uniqueTag: String) = innerUniqueMap[uniqueTag]
?.asSequence()
?: emptySequence()
fun getMatchingUniques(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueType)
// Same as .filter | .flatMap, but more cpu/mem performant (7.7 GB vs ?? for test)
@ -317,10 +324,24 @@ open class UniqueMap() {
else -> it.getMultiplied(state)
}
}
fun getMatchingUniques(uniqueTag: String, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueTag)
// Same as .filter | .flatMap, but more cpu/mem performant (7.7 GB vs ?? for test)
.flatMap {
when {
it.isTimedTriggerable -> emptySequence()
!it.conditionalsApply(state) -> emptySequence()
else -> it.getMultiplied(state)
}
}
fun hasMatchingUnique(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueType).any { it.conditionalsApply(state) }
fun hasMatchingUnique(uniqueTag: String, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueTag).any { it.conditionalsApply(state) }
fun getAllUniques() = innerUniqueMap.values.asSequence().flatten()
fun getTriggeredUniques(trigger: UniqueType, stateForConditionals: StateForConditionals,

View File

@ -327,7 +327,7 @@ object UniqueTriggerActivation {
val policyFilter = unique.params[0]
val policiesToRemove = civInfo.policies.adoptedPolicies
.mapNotNull { civInfo.gameInfo.ruleset.policies[it] }
.filter { it.matchesFilter(policyFilter) }
.filter { it.matchesFilter(policyFilter, stateForConditionals) }
if (policiesToRemove.isEmpty()) return null
return {
@ -350,7 +350,7 @@ object UniqueTriggerActivation {
val refundPercentage = unique.params[1].toInt()
val policiesToRemove = civInfo.policies.adoptedPolicies
.mapNotNull { civInfo.gameInfo.ruleset.policies[it] }
.filter { it.matchesFilter(policyFilter) }
.filter { it.matchesFilter(policyFilter, stateForConditionals) }
if (policiesToRemove.isEmpty()) return null
val policiesToRemoveMap = civInfo.policies.getCultureRefundMap(policiesToRemove, refundPercentage)
@ -907,7 +907,7 @@ object UniqueTriggerActivation {
return {
for (applicableCity in applicableCities) {
val buildingsToRemove = applicableCity.cityConstructions.getBuiltBuildings().filter {
it.matchesFilter(unique.params[0])
it.matchesFilter(unique.params[0], StateForConditionals(applicableCity))
}.toSet()
applicableCity.cityConstructions.removeBuildings(buildingsToRemove)
}
@ -924,7 +924,7 @@ object UniqueTriggerActivation {
return {
for (applicableCity in applicableCities) {
val buildingsToSell = applicableCity.cityConstructions.getBuiltBuildings().filter {
it.matchesFilter(unique.params[0]) && it.isSellable()
it.matchesFilter(unique.params[0], StateForConditionals(applicableCity)) && it.isSellable()
}
for (building in buildingsToSell) applicableCity.sellBuilding(building)

View File

@ -120,6 +120,10 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
return super<RulesetObject>.hasUnique(uniqueType, state) || ::ruleset.isInitialized && type.hasUnique(uniqueType, state)
}
override fun hasUnique(uniqueTag: String, state: StateForConditionals?): Boolean {
return super<RulesetObject>.hasUnique(uniqueTag, state) || ::ruleset.isInitialized && type.hasUnique(uniqueTag, state)
}
override fun hasTagUnique(tagUnique: String): Boolean {
return super<RulesetObject>.hasTagUnique(tagUnique) || ::ruleset.isInitialized && type.hasTagUnique(tagUnique)
}
@ -138,6 +142,20 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
return ourUniques + type.getMatchingUniques(uniqueType, state)
}
/** Allows unique functions (getMatchingUniques, hasUnique) to "see" uniques from the UnitType */
override fun getMatchingUniques(uniqueTag: String, state: StateForConditionals): Sequence<Unique> {
val ourUniques = super<RulesetObject>.getMatchingUniques(uniqueTag, state)
if (! ::ruleset.isInitialized) { // Not sure if this will ever actually happen, but better safe than sorry
return ourUniques
}
val typeUniques = type.getMatchingUniques(uniqueTag, state)
// Memory optimization - very rarely do we actually get uniques from both sources,
// and sequence addition is expensive relative to the rare case that we'll actually need it
if (ourUniques.none()) return typeUniques
if (typeUniques.none()) return ourUniques
return ourUniques + type.getMatchingUniques(uniqueTag, state)
}
override fun getProductionCost(civInfo: Civilization, city: City?): Int = costFunctions.getProductionCost(civInfo, city)
override fun canBePurchasedWithStat(city: City?, stat: Stat): Boolean {
@ -211,7 +229,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
if (civ.tech.isResearched(obsoleteTech))
yield(RejectionReasonType.Obsoleted.toInstance("Obsolete by $obsoleteTech"))
if (uniqueTo != null && !civ.matchesFilter(uniqueTo!!))
if (uniqueTo != null && !civ.matchesFilter(uniqueTo!!, stateForConditionals))
yield(RejectionReasonType.UniqueToOtherNation.toInstance("Unique to $uniqueTo"))
if (civ.cache.uniqueUnits.any { it.replaces == name })
yield(RejectionReasonType.ReplacedByOurUnique.toInstance("Our unique unit replaces this"))
@ -250,7 +268,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
}
for (unique in civ.getMatchingUniques(UniqueType.CannotBuildUnits, stateForConditionals))
if (this@BaseUnit.matchesFilter(unique.params[0])) {
if (this@BaseUnit.matchesFilter(unique.params[0], stateForConditionals)) {
val hasHappinessCondition = unique.hasModifier(UniqueType.ConditionalBelowHappiness)
|| unique.hasModifier(UniqueType.ConditionalBetweenHappiness)
if (hasHappinessCondition)
@ -384,8 +402,17 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
private val cachedMatchesFilterResult = HashMap<String, Boolean>()
/** Implements [UniqueParameterType.BaseUnitFilter][com.unciv.models.ruleset.unique.UniqueParameterType.BaseUnitFilter] */
fun matchesFilter(filter: String): Boolean =
cachedMatchesFilterResult.getOrPut(filter) { MultiFilter.multiFilter(filter, ::matchesSingleFilter ) }
fun matchesFilter(filter: String, state: StateForConditionals? = null, multiFilter: Boolean = true): Boolean {
return if (multiFilter) MultiFilter.multiFilter(filter, {
cachedMatchesFilterResult.getOrPut(it) { matchesSingleFilter(it) } ||
state != null && hasUnique(it, state) ||
state == null && hasTagUnique(it)
})
else cachedMatchesFilterResult.getOrPut(filter) { matchesSingleFilter(filter) } ||
state != null && hasUnique(filter, state) ||
state == null && hasTagUnique(filter)
}
fun matchesSingleFilter(filter: String): Boolean {
return when (filter) {
@ -408,16 +435,16 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
"Religious" -> hasUnique(UniqueType.ReligiousUnit)
else -> {
if (type.matchesFilter(filter)) return true
for (requiredTech: String in requiredTechs())
if (ruleset.technologies[requiredTech]?.matchesFilter(filter) == true) return true
if (ruleset.technologies[requiredTech]?.matchesFilter(filter, multiFilter = false) == true) return true
if (
// Uniques using these kinds of filters should be deprecated and replaced with adjective-only parameters
filter.endsWith(" units")
// "military units" --> "Military", using invariant locale
&& matchesFilter(filter.removeSuffix(" units").lowercase().replaceFirstChar { it.uppercaseChar() })
&& matchesFilter(filter.removeSuffix(" units").lowercase().replaceFirstChar { it.uppercaseChar() },
multiFilter = false)
) return true
return uniqueMap.hasTagUnique(filter)
return false
}
}
}

View File

@ -36,25 +36,25 @@ class BaseUnitCost(val baseUnit: BaseUnit) {
/** Contains only unit-specific uniques that allow purchasing with stat */
fun canBePurchasedWithStat(city: City, stat: Stat): Boolean {
val conditionalState = StateForConditionals(civInfo = city.civ, city = city)
val conditionalState = StateForConditionals(city)
if (city.getMatchingUniques(UniqueType.BuyUnitsIncreasingCost, conditionalState)
.any {
it.params[2] == stat.name
&& baseUnit.matchesFilter(it.params[0])
&& baseUnit.matchesFilter(it.params[0], conditionalState)
&& city.matchesFilter(it.params[3])
}
) return true
if (city.getMatchingUniques(UniqueType.BuyUnitsByProductionCost, conditionalState)
.any { it.params[1] == stat.name && baseUnit.matchesFilter(it.params[0]) }
.any { it.params[1] == stat.name && baseUnit.matchesFilter(it.params[0], conditionalState) }
)
return true
if (city.getMatchingUniques(UniqueType.BuyUnitsWithStat, conditionalState)
.any {
it.params[1] == stat.name
&& baseUnit.matchesFilter(it.params[0])
&& baseUnit.matchesFilter(it.params[0], conditionalState)
&& city.matchesFilter(it.params[2])
}
)
@ -63,7 +63,7 @@ class BaseUnitCost(val baseUnit: BaseUnit) {
if (city.getMatchingUniques(UniqueType.BuyUnitsForAmountStat, conditionalState)
.any {
it.params[2] == stat.name
&& baseUnit.matchesFilter(it.params[0])
&& baseUnit.matchesFilter(it.params[0], conditionalState)
&& city.matchesFilter(it.params[3])
}
)
@ -75,9 +75,10 @@ class BaseUnitCost(val baseUnit: BaseUnit) {
fun getStatBuyCost(city: City, stat: Stat): Int? {
var cost = baseUnit.getBaseBuyCost(city, stat)?.toDouble() ?: return null
val conditionalState = StateForConditionals(city)
for (unique in city.getMatchingUniques(UniqueType.BuyUnitsDiscount)) {
if (stat.name == unique.params[0] && baseUnit.matchesFilter(unique.params[1]))
if (stat.name == unique.params[0] && baseUnit.matchesFilter(unique.params[1], conditionalState))
cost *= unique.params[2].toPercent()
}
for (unique in city.getMatchingUniques(UniqueType.BuyItemsDiscount))
@ -94,7 +95,7 @@ class BaseUnitCost(val baseUnit: BaseUnit) {
yieldAll(city.getMatchingUniques(UniqueType.BuyUnitsIncreasingCost, conditionalState)
.filter {
it.params[2] == stat.name
&& baseUnit.matchesFilter(it.params[0])
&& baseUnit.matchesFilter(it.params[0], conditionalState)
&& city.matchesFilter(it.params[3])
}.map {
baseUnit.getCostForConstructionsIncreasingInPrice(
@ -105,14 +106,14 @@ class BaseUnitCost(val baseUnit: BaseUnit) {
}
)
yieldAll(city.getMatchingUniques(UniqueType.BuyUnitsByProductionCost, conditionalState)
.filter { it.params[1] == stat.name && baseUnit.matchesFilter(it.params[0]) }
.filter { it.params[1] == stat.name && baseUnit.matchesFilter(it.params[0], conditionalState) }
.map { (getProductionCost(city.civ, city) * it.params[2].toInt()).toFloat() }
)
if (city.getMatchingUniques(UniqueType.BuyUnitsWithStat, conditionalState)
.any {
it.params[1] == stat.name
&& baseUnit.matchesFilter(it.params[0])
&& baseUnit.matchesFilter(it.params[0], conditionalState)
&& city.matchesFilter(it.params[2])
}
) yield(city.civ.getEra().baseUnitBuyCost * city.civ.gameInfo.speed.statCostModifiers[stat]!!)
@ -120,7 +121,7 @@ class BaseUnitCost(val baseUnit: BaseUnit) {
yieldAll(city.getMatchingUniques(UniqueType.BuyUnitsForAmountStat, conditionalState)
.filter {
it.params[2] == stat.name
&& baseUnit.matchesFilter(it.params[0])
&& baseUnit.matchesFilter(it.params[0], conditionalState)
&& city.matchesFilter(it.params[3])
}.map { it.params[1].toInt() * city.civ.gameInfo.speed.statCostModifiers[stat]!! }
)

View File

@ -36,7 +36,7 @@ class UnitType() : RulesetObject() {
"Land" -> isLandUnit()
"Water" -> isWaterUnit()
"Air" -> isAirUnit()
else -> uniqueMap.hasTagUnique(filter)
else -> hasTagUnique(filter)
}
}

View File

@ -269,7 +269,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
&& constructionButtonDTOList.any {
(it.construction is Building) && (it.construction.name == dto.construction.requiredBuilding
|| it.construction.replaces == dto.construction.requiredBuilding
|| it.construction.hasTagUnique(dto.construction.requiredBuilding!!))
|| it.construction.hasUnique(dto.construction.requiredBuilding!!, StateForConditionals(cityScreen.city)))
})
continue

View File

@ -296,7 +296,7 @@ object UnitActionsFromUniques {
for (unique in uniquesToCheck) {
val improvementFilter = unique.params[0]
val improvements = tile.ruleset.tileImprovements.values.filter { it.matchesFilter(improvementFilter) }
val improvements = tile.ruleset.tileImprovements.values.filter { it.matchesFilter(improvementFilter, StateForConditionals(unit = unit, tile = tile)) }
for (improvement in improvements) {
// Try to skip Improvements we can never build