Added more unit tests for uniques; added missing unique implementations (#6886)

* Added more unit tests for uniques; added missing implementations

* This of course shouldn't go here as there is another function for it

* Stylistic changes

* This generates better unique examples

* Reviews

* Reordered for efficiency

* Reverted improvement percentage bonuses applying to tiles
This commit is contained in:
Xander Lenstra
2022-05-22 12:12:10 +02:00
committed by GitHub
parent f34b97a421
commit 3754108391
7 changed files with 224 additions and 75 deletions

View File

@ -539,6 +539,7 @@ class CityStats(val cityInfo: CityInfo) {
entry.gold *= statPercentBonusesSum.gold.toPercent() entry.gold *= statPercentBonusesSum.gold.toPercent()
entry.culture *= statPercentBonusesSum.culture.toPercent() entry.culture *= statPercentBonusesSum.culture.toPercent()
entry.food *= statPercentBonusesSum.food.toPercent() entry.food *= statPercentBonusesSum.food.toPercent()
entry.faith *= statPercentBonusesSum.faith.toPercent()
} }
// AFTER we've gotten all the gold stats figured out, only THEN do we plonk that gold into Science // AFTER we've gotten all the gold stats figured out, only THEN do we plonk that gold into Science

View File

@ -8,11 +8,9 @@ import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.civilization.PlayerType import com.unciv.logic.civilization.PlayerType
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.tile.* import com.unciv.models.ruleset.tile.*
import com.unciv.models.ruleset.unique.LocalUniqueCache import com.unciv.models.ruleset.unique.*
import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.stats.Stat
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.stats.Stats import com.unciv.models.stats.Stats
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.civilopedia.FormattedLine import com.unciv.ui.civilopedia.FormattedLine
@ -259,13 +257,13 @@ open class TileInfo {
fun getTerrainMatchingUniques(uniqueType: UniqueType, stateForConditionals: StateForConditionals = StateForConditionals(tile=this) ): Sequence<Unique> { fun getTerrainMatchingUniques(uniqueType: UniqueType, stateForConditionals: StateForConditionals = StateForConditionals(tile=this) ): Sequence<Unique> {
return getAllTerrains().flatMap { it.getMatchingUniques(uniqueType, stateForConditionals) } return getAllTerrains().flatMap { it.getMatchingUniques(uniqueType, stateForConditionals) }
} }
/** Get all uniques of this type that any part of this tile has: terrains, improvement, resource */ /** Get all uniques of this type that any part of this tile has: terrains, improvement, resource */
fun getMatchingUniques(uniqueType: UniqueType, stateForConditionals: StateForConditionals = StateForConditionals(tile=this)) = fun getMatchingUniques(uniqueType: UniqueType, stateForConditionals: StateForConditionals = StateForConditionals(tile=this)) =
getTerrainMatchingUniques(uniqueType, stateForConditionals) + getTerrainMatchingUniques(uniqueType, stateForConditionals) +
(getTileImprovement()?.getMatchingUniques(uniqueType, stateForConditionals) ?: sequenceOf()) + (getTileImprovement()?.getMatchingUniques(uniqueType, stateForConditionals) ?: sequenceOf()) +
if (resource == null) sequenceOf() else tileResource.getMatchingUniques(uniqueType, stateForConditionals) if (resource == null) sequenceOf() else tileResource.getMatchingUniques(uniqueType, stateForConditionals)
fun getWorkingCity(): CityInfo? { fun getWorkingCity(): CityInfo? {
val civInfo = getOwner() ?: return null val civInfo = getOwner() ?: return null
return civInfo.cities.firstOrNull { it.isWorked(this) } return civInfo.cities.firstOrNull { it.isWorked(this) }
@ -319,22 +317,26 @@ open class TileInfo {
tileUniques += city.getMatchingUniques(UniqueType.StatsFromObject, stateForConditionals) tileUniques += city.getMatchingUniques(UniqueType.StatsFromObject, stateForConditionals)
for (unique in localUniqueCache.get("StatsFromTilesAndObjects", tileUniques)) { for (unique in localUniqueCache.get("StatsFromTilesAndObjects", tileUniques)) {
val tileType = unique.params[1] val tileType = unique.params[1]
if (tileType == improvement) continue // This is added to the calculation in getImprovementStats. we don't want to add it twice if (!matchesTerrainFilter(tileType, observingCiv)) continue
if (matchesTerrainFilter(tileType, observingCiv)) stats.add(unique.stats)
stats.add(unique.stats) if (naturalWonder != null
if (tileType == "Natural Wonder" && naturalWonder != null && city.civInfo.hasUnique(UniqueType.DoubleStatsFromNaturalWonders)) { && tileType == "Natural Wonder"
&& city.civInfo.hasUnique(UniqueType.DoubleStatsFromNaturalWonders)
) {
stats.add(unique.stats) stats.add(unique.stats)
} }
} }
for (unique in localUniqueCache.get("StatsFromTilesWithout", for (unique in localUniqueCache.get("StatsFromTilesWithout",
city.getMatchingUniques(UniqueType.StatsFromTilesWithout, stateForConditionals))) city.getMatchingUniques(UniqueType.StatsFromTilesWithout, stateForConditionals))
) {
if ( if (
matchesTerrainFilter(unique.params[1]) && matchesTerrainFilter(unique.params[1]) &&
!matchesTerrainFilter(unique.params[2]) && !matchesTerrainFilter(unique.params[2]) &&
city.matchesFilter(unique.params[3]) city.matchesFilter(unique.params[3])
) )
stats.add(unique.stats) stats.add(unique.stats)
}
} }
if (isAdjacentToRiver()) stats.gold++ if (isAdjacentToRiver()) stats.gold++
@ -347,17 +349,61 @@ open class TileInfo {
if (improvement != null) if (improvement != null)
stats.add(getImprovementStats(improvement, observingCiv, city)) stats.add(getImprovementStats(improvement, observingCiv, city))
if (isCityCenter()) {
if (stats.food < 2) stats.food = 2f
if (stats.production < 1) stats.production = 1f
}
if (stats.gold != 0f && observingCiv.goldenAges.isGoldenAge()) if (stats.gold != 0f && observingCiv.goldenAges.isGoldenAge())
stats.gold++ stats.gold++
} }
if (isCityCenter()) {
if (stats.food < 2) stats.food = 2f
if (stats.production < 1) stats.production = 1f
}
for ((stat, value) in stats) for ((stat, value) in stats)
if (value < 0f) stats[stat] = 0f if (value < 0f) stats[stat] = 0f
for ((stat, value) in getTilePercentageStats(observingCiv, city)) {
stats[stat] *= value.toPercent()
}
return stats
}
// Only gets the tile percentage bonus, not the improvement percentage bonus
@Suppress("MemberVisibilityCanBePrivate")
fun getTilePercentageStats(observingCiv: CivilizationInfo?, city: CityInfo?): Stats {
val stats = Stats()
val stateForConditionals = StateForConditionals(civInfo = observingCiv, cityInfo = city, tile = this)
if (city != null) {
for (unique in city.getMatchingUniques(UniqueType.StatPercentFromObject, stateForConditionals)) {
val tileFilter = unique.params[2]
if (matchesTerrainFilter(tileFilter, observingCiv))
stats[Stat.valueOf(unique.params[1])] += unique.params[0].toFloat()
}
for (unique in city.getMatchingUniques(UniqueType.AllStatsPercentFromObject, stateForConditionals)) {
val tileFilter = unique.params[1]
if (!matchesTerrainFilter(tileFilter, observingCiv)) continue
val statPercentage = unique.params[0].toFloat()
for (stat in Stat.values())
stats[stat] += statPercentage
}
} else if (observingCiv != null) {
for (unique in observingCiv.getMatchingUniques(UniqueType.StatPercentFromObject, stateForConditionals)) {
val tileFilter = unique.params[2]
if (matchesTerrainFilter(tileFilter, observingCiv))
stats[Stat.valueOf(unique.params[1])] += unique.params[0].toFloat()
}
for (unique in observingCiv.getMatchingUniques(UniqueType.AllStatsPercentFromObject, stateForConditionals)) {
val tileFilter = unique.params[1]
if (!matchesTerrainFilter(tileFilter, observingCiv)) continue
val statPercentage = unique.params[0].toFloat()
for (stat in Stat.values())
stats[stat] += statPercentage
}
}
return stats return stats
} }
@ -421,6 +467,7 @@ open class TileInfo {
return fertility return fertility
} }
// Also multiplies the stats by the percentage bonus for improvements (but not for tiles)
fun getImprovementStats(improvement: TileImprovement, observingCiv: CivilizationInfo, city: CityInfo?): Stats { fun getImprovementStats(improvement: TileImprovement, observingCiv: CivilizationInfo, city: CityInfo?): Stats {
val stats = improvement.cloneStats() val stats = improvement.cloneStats()
if (hasViewableResource(observingCiv) && tileResource.isImprovedBy(improvement.name) if (hasViewableResource(observingCiv) && tileResource.isImprovedBy(improvement.name)
@ -464,17 +511,49 @@ open class TileInfo {
stats.add(unique.stats) stats.add(unique.stats)
} }
} }
for (unique in city.getMatchingUniques(UniqueType.AllStatsPercentFromObject, conditionalState)) {
if (improvement.matchesFilter(unique.params[1]))
stats.timesInPlace(unique.params[0].toPercent())
}
} }
if (city == null) { // As otherwise we already got this above for ((stat, value) in getImprovementPercentageStats(improvement, observingCiv, city)) {
stats[stat] *= value.toPercent()
}
return stats
}
@Suppress("MemberVisibilityCanBePrivate")
fun getImprovementPercentageStats(improvement: TileImprovement, observingCiv: CivilizationInfo, city: CityInfo?): Stats {
val stats = Stats()
val conditionalState = StateForConditionals(civInfo = observingCiv, cityInfo = city, tile = this)
// I would love to make an interface 'canCallMatchingUniques'
// from which both cityInfo and CivilizationInfo derive, so I don't have to duplicate all this code
// But something something too much for this PR.
if (city != null) {
for (unique in city.getMatchingUniques(UniqueType.AllStatsPercentFromObject, conditionalState)) {
if (!improvement.matchesFilter(unique.params[1])) continue
for (stat in Stat.values()) {
stats[stat] += unique.params[0].toFloat()
}
}
for (unique in city.getMatchingUniques(UniqueType.StatPercentFromObject, conditionalState)) {
if (!improvement.matchesFilter(unique.params[2])) continue
val stat = Stat.valueOf(unique.params[1])
stats[stat] += unique.params[0].toFloat()
}
} else {
for (unique in observingCiv.getMatchingUniques(UniqueType.AllStatsPercentFromObject, conditionalState)) { for (unique in observingCiv.getMatchingUniques(UniqueType.AllStatsPercentFromObject, conditionalState)) {
if (improvement.matchesFilter(unique.params[1])) if (!improvement.matchesFilter(unique.params[1])) continue
stats.timesInPlace(unique.params[0].toPercent()) for (stat in Stat.values()) {
stats[stat] += unique.params[0].toFloat()
}
}
for (unique in observingCiv.getMatchingUniques(UniqueType.StatPercentFromObject, conditionalState)) {
if (!improvement.matchesFilter(unique.params[2])) continue
val stat = Stat.valueOf(unique.params[1])
stats[stat] += unique.params[0].toFloat()
} }
} }
@ -488,10 +567,10 @@ open class TileInfo {
if (terrainFilter == Constants.freshWater && isAdjacentToRiver()) return true if (terrainFilter == Constants.freshWater && isAdjacentToRiver()) return true
return (neighbors + this).any { neighbor -> neighbor.matchesFilter(terrainFilter) } return (neighbors + this).any { neighbor -> neighbor.matchesFilter(terrainFilter) }
} }
/** Returns true if the [improvement] can be built on this [TileInfo] */ /** Returns true if the [improvement] can be built on this [TileInfo] */
fun canBuildImprovement(improvement: TileImprovement, civInfo: CivilizationInfo): Boolean { fun canBuildImprovement(improvement: TileImprovement, civInfo: CivilizationInfo): Boolean {
val stateForConditionals = StateForConditionals(civInfo, tile=this) val stateForConditionals = StateForConditionals(civInfo, tile=this)
val knownFeatureRemovals = ruleset.tileImprovements.values val knownFeatureRemovals = ruleset.tileImprovements.values
.filter { rulesetImprovement -> .filter { rulesetImprovement ->
@ -523,7 +602,7 @@ open class TileInfo {
else -> canImprovementBeBuiltHere(improvement, hasViewableResource(civInfo), knownFeatureRemovals, stateForConditionals) else -> canImprovementBeBuiltHere(improvement, hasViewableResource(civInfo), knownFeatureRemovals, stateForConditionals)
} }
} }
/** Without regards to what CivInfo it is, a lot of the checks are just for the improvement on the tile. /** Without regards to what CivInfo it is, a lot of the checks are just for the improvement on the tile.
* Doubles as a check for the map editor. * Doubles as a check for the map editor.
*/ */
@ -551,13 +630,13 @@ open class TileInfo {
clonedTile.removeTerrainFeature(topTerrain.name) clonedTile.removeTerrainFeature(topTerrain.name)
return clonedTile.canImprovementBeBuiltHere(improvement, resourceIsVisible, knownFeatureRemovals, stateForConditionals) return clonedTile.canImprovementBeBuiltHere(improvement, resourceIsVisible, knownFeatureRemovals, stateForConditionals)
} }
return when { return when {
improvement.name == this.improvement -> false improvement.name == this.improvement -> false
isCityCenter() -> false isCityCenter() -> false
// First we handle a few special improvements // First we handle a few special improvements
// Can only cancel if there is actually an improvement being built // Can only cancel if there is actually an improvement being built
improvement.name == Constants.cancelImprovementOrder -> (this.improvementInProgress != null) improvement.name == Constants.cancelImprovementOrder -> (this.improvementInProgress != null)
// Can only remove roads if that road is actually there // Can only remove roads if that road is actually there
@ -566,7 +645,7 @@ open class TileInfo {
improvement.name.startsWith(Constants.remove) -> terrainFeatures.any { it == improvement.name.removePrefix(Constants.remove) } improvement.name.startsWith(Constants.remove) -> terrainFeatures.any { it == improvement.name.removePrefix(Constants.remove) }
// Can only build roads if on land and they are better than the current road // Can only build roads if on land and they are better than the current road
RoadStatus.values().any { it.name == improvement.name } -> !isWater && RoadStatus.valueOf(improvement.name) > roadStatus RoadStatus.values().any { it.name == improvement.name } -> !isWater && RoadStatus.valueOf(improvement.name) > roadStatus
// Then we check if there is any reason to not allow this improvement to be build // Then we check if there is any reason to not allow this improvement to be build
// Can't build if there is already an irremovable improvement here // Can't build if there is already an irremovable improvement here
@ -574,7 +653,7 @@ open class TileInfo {
// Can't build if this terrain is unbuildable, except when we are specifically allowed to // Can't build if this terrain is unbuildable, except when we are specifically allowed to
getLastTerrain().unbuildable && !improvement.canBeBuildOnThisUnbuildableTerrain(knownFeatureRemovals) -> false getLastTerrain().unbuildable && !improvement.canBeBuildOnThisUnbuildableTerrain(knownFeatureRemovals) -> false
// Can't build if any terrain specifically prevents building this improvement // Can't build if any terrain specifically prevents building this improvement
getTerrainMatchingUniques(UniqueType.RestrictedBuildableImprovements, stateForConditionals).any { getTerrainMatchingUniques(UniqueType.RestrictedBuildableImprovements, stateForConditionals).any {
unique -> !improvement.matchesFilter(unique.params[0]) unique -> !improvement.matchesFilter(unique.params[0])
@ -602,20 +681,20 @@ open class TileInfo {
) -> false ) -> false
// At this point we know this is a normal improvement and that there is no reason not to allow it to be built. // At this point we know this is a normal improvement and that there is no reason not to allow it to be built.
// Lastly we check if the improvement may be built on this terrain or resource // Lastly we check if the improvement may be built on this terrain or resource
improvement.canBeBuiltOn(getLastTerrain().name) -> true improvement.canBeBuiltOn(getLastTerrain().name) -> true
isLand && improvement.canBeBuiltOn("Land") -> true isLand && improvement.canBeBuiltOn("Land") -> true
isWater && improvement.canBeBuiltOn("Water") -> true isWater && improvement.canBeBuiltOn("Water") -> true
// DO NOT reverse this &&. isAdjacentToFreshwater() is a lazy which calls a function, and reversing it breaks the tests. // DO NOT reverse this &&. isAdjacentToFreshwater() is a lazy which calls a function, and reversing it breaks the tests.
improvement.hasUnique(UniqueType.ImprovementBuildableByFreshWater, stateForConditionals) improvement.hasUnique(UniqueType.ImprovementBuildableByFreshWater, stateForConditionals)
&& isAdjacentTo(Constants.freshWater) -> true && isAdjacentTo(Constants.freshWater) -> true
// I don't particularly like this check, but it is required to build mines on non-hill resources // I don't particularly like this check, but it is required to build mines on non-hill resources
resourceIsVisible && tileResource.isImprovedBy(improvement.name) -> true resourceIsVisible && tileResource.isImprovedBy(improvement.name) -> true
// DEPRECATED since 4.0.14, REMOVE SOON: // DEPRECATED since 4.0.14, REMOVE SOON:
isLand && improvement.terrainsCanBeBuiltOn.isEmpty() && !improvement.hasUnique(UniqueType.CanOnlyImproveResource) -> true isLand && improvement.terrainsCanBeBuiltOn.isEmpty() && !improvement.hasUnique(UniqueType.CanOnlyImproveResource) -> true
// No reason this improvement should be built here, so can't build it // No reason this improvement should be built here, so can't build it
else -> false else -> false
} }
} }
@ -722,7 +801,7 @@ open class TileInfo {
if (isWater || isImpassible()) if (isWater || isImpassible())
return false return false
if (getTilesInDistance(modConstants.minimalCityDistanceOnDifferentContinents) if (getTilesInDistance(modConstants.minimalCityDistanceOnDifferentContinents)
.any { it.isCityCenter() && it.getContinent() != getContinent() } .any { it.isCityCenter() && it.getContinent() != getContinent() }
|| getTilesInDistance(modConstants.minimalCityDistance) || getTilesInDistance(modConstants.minimalCityDistance)
.any { it.isCityCenter() && it.getContinent() == getContinent() } .any { it.isCityCenter() && it.getContinent() == getContinent() }
) { ) {
@ -942,7 +1021,7 @@ open class TileInfo {
/** /**
* Sets this tile's [resource] and, if [newResource] is a Strategic resource, [resourceAmount] fields. * Sets this tile's [resource] and, if [newResource] is a Strategic resource, [resourceAmount] fields.
* *
* [resourceAmount] is determined by [MapParameters.mapResources] and [majorDeposit], and * [resourceAmount] is determined by [MapParameters.mapResources] and [majorDeposit], and
* if the latter is `null` a random choice between major and minor deposit is made, approximating * if the latter is `null` a random choice between major and minor deposit is made, approximating
* the frequency [MapRegions][com.unciv.logic.map.mapgenerator.MapRegions] would use. * the frequency [MapRegions][com.unciv.logic.map.mapgenerator.MapRegions] would use.

View File

@ -198,6 +198,13 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
if (matchesFilter(unique.params[2])) if (matchesFilter(unique.params[2]))
stats.add(Stat.valueOf(unique.params[1]), unique.params[0].toFloat()) stats.add(Stat.valueOf(unique.params[1]), unique.params[0].toFloat())
} }
for (unique in localUniqueCache.get("AllStatsPercentFromObject", civInfo.getMatchingUniques(UniqueType.AllStatsPercentFromObject))) {
if (!matchesFilter(unique.params[1])) continue
for (stat in Stat.values()) {
stats.add(stat, unique.params[0].toFloat())
}
}
return stats return stats
} }

View File

@ -84,7 +84,6 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
StatsFromCitiesOnSpecificTiles("[stats] in cities on [terrainFilter] tiles", UniqueTarget.Global, UniqueTarget.FollowerBelief), StatsFromCitiesOnSpecificTiles("[stats] in cities on [terrainFilter] tiles", UniqueTarget.Global, UniqueTarget.FollowerBelief),
StatsFromBuildings("[stats] from all [buildingFilter] buildings", UniqueTarget.Global, UniqueTarget.FollowerBelief), StatsFromBuildings("[stats] from all [buildingFilter] buildings", UniqueTarget.Global, UniqueTarget.FollowerBelief),
StatsSpendingGreatPeople("[stats] whenever a Great Person is expended", UniqueTarget.Global),
StatsFromTiles("[stats] from [tileFilter] tiles [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief), StatsFromTiles("[stats] from [tileFilter] tiles [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
StatsFromTilesWithout("[stats] from [tileFilter] tiles without [tileFilter] [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief), StatsFromTilesWithout("[stats] from [tileFilter] tiles without [tileFilter] [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
// This is a doozy // This is a doozy
@ -96,8 +95,9 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
// Stat percentage boosts // Stat percentage boosts
StatPercentBonus("[relativeAmount]% [stat]", UniqueTarget.Global, UniqueTarget.FollowerBelief), StatPercentBonus("[relativeAmount]% [stat]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
StatPercentBonusCities("[relativeAmount]% [stat] [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief), StatPercentBonusCities("[relativeAmount]% [stat] [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
StatPercentFromObject("[relativeAmount]% [stat] from every [tileFilter/specialist/buildingName]", UniqueTarget.Global, UniqueTarget.FollowerBelief), // Todo: Add support for specialist next to buildingName
AllStatsPercentFromObject("[relativeAmount]% Yield from every [tileFilter]", UniqueTarget.FollowerBelief, UniqueTarget.Global), StatPercentFromObject("[relativeAmount]% [stat] from every [tileFilter/buildingFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
AllStatsPercentFromObject("[relativeAmount]% Yield from every [tileFilter/buildingFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
StatPercentFromReligionFollowers("[relativeAmount]% [stat] from every follower, up to [relativeAmount]%", UniqueTarget.FollowerBelief), StatPercentFromReligionFollowers("[relativeAmount]% [stat] from every follower, up to [relativeAmount]%", UniqueTarget.FollowerBelief),
BonusStatsFromCityStates("[relativeAmount]% [stat] from City-States", UniqueTarget.Global), BonusStatsFromCityStates("[relativeAmount]% [stat] from City-States", UniqueTarget.Global),
StatPercentFromTradeRoutes("[relativeAmount]% [stat] from Trade Routes", UniqueTarget.Global), StatPercentFromTradeRoutes("[relativeAmount]% [stat] from Trade Routes", UniqueTarget.Global),
@ -165,6 +165,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
FreeExtraAnyBeliefs("May choose [amount] additional belief(s) of any type when [foundingOrEnhancing] a religion", UniqueTarget.Global), FreeExtraAnyBeliefs("May choose [amount] additional belief(s) of any type when [foundingOrEnhancing] a religion", UniqueTarget.Global),
StatsWhenAdoptingReligionSpeed("[stats] when a city adopts this religion for the first time (modified by game speed)", UniqueTarget.Global), StatsWhenAdoptingReligionSpeed("[stats] when a city adopts this religion for the first time (modified by game speed)", UniqueTarget.Global),
StatsWhenAdoptingReligion("[stats] when a city adopts this religion for the first time", UniqueTarget.Global), StatsWhenAdoptingReligion("[stats] when a city adopts this religion for the first time", UniqueTarget.Global),
StatsSpendingGreatPeople("[stats] whenever a Great Person is expended", UniqueTarget.Global),
UnhappinessFromPopulationTypePercentageChange("[relativeAmount]% Unhappiness from [populationFilter] [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief), UnhappinessFromPopulationTypePercentageChange("[relativeAmount]% Unhappiness from [populationFilter] [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
@Deprecated("As of 3.19.19", ReplaceWith("[relativeAmount]% Unhappiness from [Population] [cityFilter]")) @Deprecated("As of 3.19.19", ReplaceWith("[relativeAmount]% Unhappiness from [Population] [cityFilter]"))

View File

@ -98,11 +98,6 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
Applicable to: Global, FollowerBelief Applicable to: Global, FollowerBelief
??? example "[stats] whenever a Great Person is expended"
Example: "[+1 Gold, +2 Production] whenever a Great Person is expended"
Applicable to: Global
??? example "[stats] from [tileFilter] tiles [cityFilter]" ??? example "[stats] from [tileFilter] tiles [cityFilter]"
Example: "[+1 Gold, +2 Production] from [Farm] tiles [in all cities]" Example: "[+1 Gold, +2 Production] from [Farm] tiles [in all cities]"
@ -133,12 +128,12 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
Applicable to: Global, FollowerBelief Applicable to: Global, FollowerBelief
??? example "[relativeAmount]% [stat] from every [tileFilter/specialist/buildingName]" ??? example "[relativeAmount]% [stat] from every [tileFilter/buildingFilter]"
Example: "[+20]% [Culture] from every [Farm]" Example: "[+20]% [Culture] from every [Farm]"
Applicable to: Global, FollowerBelief Applicable to: Global, FollowerBelief
??? example "[relativeAmount]% Yield from every [tileFilter]" ??? example "[relativeAmount]% Yield from every [tileFilter/buildingFilter]"
Example: "[+20]% Yield from every [Farm]" Example: "[+20]% Yield from every [Farm]"
Applicable to: Global, FollowerBelief Applicable to: Global, FollowerBelief
@ -290,6 +285,11 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
Applicable to: Global Applicable to: Global
??? example "[stats] whenever a Great Person is expended"
Example: "[+1 Gold, +2 Production] whenever a Great Person is expended"
Applicable to: Global
??? example "[relativeAmount]% Unhappiness from [populationFilter] [cityFilter]" ??? example "[relativeAmount]% Unhappiness from [populationFilter] [cityFilter]"
Example: "[+20]% Unhappiness from [Followers of this Religion] [in all cities]" Example: "[+20]% Unhappiness from [Followers of this Religion] [in all cities]"

View File

@ -106,21 +106,6 @@ class GlobalUniquesTests {
Assert.assertTrue(cityInfo.cityStats.finalStatList["Buildings"]!!.gold == 0f) Assert.assertTrue(cityInfo.cityStats.finalStatList["Buildings"]!!.gold == 0f)
} }
@Test
fun statsSpendingGreatPeople() {
val civInfo = game.addCiv()
val tile = game.setTileFeatures(Vector2(0f,0f), Constants.desert)
val cityInfo = game.addCity(civInfo, tile, true)
val unit = game.addUnit("Great Engineer", civInfo, tile)
val building = game.createBuildingWithUnique("[+250 Gold] whenever a Great Person is expended")
cityInfo.cityConstructions.addBuilding(building.name)
civInfo.addGold(-civInfo.gold)
civInfo.addGold(-civInfo.gold) // reset gold just to be sure
unit.consume()
Assert.assertTrue(civInfo.gold == 250)
}
@Test @Test
fun statsFromTiles() { fun statsFromTiles() {
game.makeHexagonalMap(2) game.makeHexagonalMap(2)
@ -200,8 +185,6 @@ class GlobalUniquesTests {
inBetweenTile.roadStatus = RoadStatus.Road inBetweenTile.roadStatus = RoadStatus.Road
civInfo.transients().updateCitiesConnectedToCapital() civInfo.transients().updateCitiesConnectedToCapital()
city2.cityStats.update() city2.cityStats.update()
println(city2.isConnectedToCapital())
println(city2.cityStats.finalStatList)
Assert.assertTrue(city2.cityStats.finalStatList["Trade routes"]!!.science == 30f) Assert.assertTrue(city2.cityStats.finalStatList["Trade routes"]!!.science == 30f)
} }
@ -255,8 +238,6 @@ class GlobalUniquesTests {
civ1.updateStatsForNextTurn() civ1.updateStatsForNextTurn()
println(civ1.statsForNextTurn.science)
Assert.assertTrue(civ1.statsForNextTurn.science == 90f) Assert.assertTrue(civ1.statsForNextTurn.science == 90f)
} }
@ -273,11 +254,92 @@ class GlobalUniquesTests {
city.cityConstructions.addBuilding(building.name) city.cityConstructions.addBuilding(building.name)
city.cityStats.update() city.cityStats.update()
println(city.cityStats.finalStatList) Assert.assertTrue(city.cityStats.finalStatList["Buildings"]!!.science == 30f)
}
@Test
fun statPercentBonusCities() {
val civ = game.addCiv(uniques = listOf("[+200]% [Science] [in all cities]"))
val tile = game.getTile(Vector2(0f, 0f))
val city = game.addCity(civ, tile, true)
val building = game.createBuildingWithUniques(arrayListOf("[+10 Science]"))
city.cityConstructions.addBuilding(building.name)
city.cityStats.update()
Assert.assertTrue(city.cityStats.finalStatList["Buildings"]!!.science == 30f) Assert.assertTrue(city.cityStats.finalStatList["Buildings"]!!.science == 30f)
} }
@Test
fun statPercentFromObject() {
game.makeHexagonalMap(1)
val emptyBuilding = game.createBuildingWithUniques()
val civInfo = game.addCiv(
uniques = listOf(
"[+3 Faith] from every [Farm]",
"[+200]% [Faith] from every [${emptyBuilding.name}]",
"[+200]% [Faith] from every [Farm]",
)
)
val tile = game.setTileFeatures(Vector2(0f,0f), Constants.desert)
val city = game.addCity(civInfo, tile, true)
val faithBuilding = game.createBuildingWithUniques()
faithBuilding.faith = 3f
city.cityConstructions.addBuilding(faithBuilding.name)
val tile2 = game.setTileFeatures(Vector2(0f,1f), Constants.grassland)
tile2.improvement = "Farm"
Assert.assertTrue(tile2.getTileStats(city, civInfo).faith == 9f)
city.cityConstructions.addBuilding(emptyBuilding.name)
city.cityStats.update()
Assert.assertTrue(city.cityStats.finalStatList["Buildings"]!!.faith == 9f)
}
@Test
fun allStatsPercentFromObject() {
game.makeHexagonalMap(1)
val emptyBuilding = game.createBuildingWithUniques()
val civInfo = game.addCiv(
uniques = listOf(
"[+3 Faith] from every [Farm]",
"[+200]% Yield from every [${emptyBuilding.name}]",
"[+200]% Yield from every [Farm]",
)
)
val tile = game.setTileFeatures(Vector2(0f,0f), Constants.desert)
val city = game.addCity(civInfo, tile, true)
val faithBuilding = game.createBuildingWithUniques()
faithBuilding.faith = 3f
city.cityConstructions.addBuilding(faithBuilding.name)
val tile2 = game.setTileFeatures(Vector2(0f,1f), Constants.grassland)
tile2.improvement = "Farm"
Assert.assertTrue(tile2.getTileStats(city, civInfo).faith == 9f)
city.cityConstructions.addBuilding(emptyBuilding.name)
city.cityStats.update()
Assert.assertTrue(city.cityStats.finalStatList["Buildings"]!!.faith == 9f)
}
// endregion // endregion
@Test
fun statsSpendingGreatPeople() {
val civInfo = game.addCiv()
val tile = game.setTileFeatures(Vector2(0f,0f), Constants.desert)
val cityInfo = game.addCity(civInfo, tile, true)
val unit = game.addUnit("Great Engineer", civInfo, tile)
val building = game.createBuildingWithUnique("[+250 Gold] whenever a Great Person is expended")
cityInfo.cityConstructions.addBuilding(building.name)
civInfo.addGold(-civInfo.gold) // reset gold just to be sure
unit.consume()
Assert.assertTrue(civInfo.gold == 250)
}
} }

View File

@ -17,9 +17,6 @@ import com.unciv.models.metadata.BaseRuleset
import com.unciv.models.metadata.GameSettings import com.unciv.models.metadata.GameSettings
import com.unciv.models.ruleset.* import com.unciv.models.ruleset.*
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.utils.withItem
import kotlin.math.abs
import kotlin.math.max
class TestGame { class TestGame {
@ -61,7 +58,9 @@ class TestGame {
gameInfo.tileMap = newTileMap gameInfo.tileMap = newTileMap
} }
/** Makes a new hexagonal tileMap and sets it in gameInfo. Removes all existing tiles. All new tiles have terrain [baseTerrain] */ /** Makes a new hexagonal tileMap with radius [newRadius] and sets it in gameInfo.
* Removes all existing tiles. All new tiles have terrain [baseTerrain]
*/
fun makeHexagonalMap(newRadius: Int, baseTerrain: String = Constants.desert) { fun makeHexagonalMap(newRadius: Int, baseTerrain: String = Constants.desert) {
val newTileMap = TileMap(newRadius, ruleset, tileMap.mapParameters.worldWrap) val newTileMap = TileMap(newRadius, ruleset, tileMap.mapParameters.worldWrap)
newTileMap.mapParameters.mapSize = MapSizeNew(newRadius) newTileMap.mapParameters.mapSize = MapSizeNew(newRadius)