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.culture *= statPercentBonusesSum.culture.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

View File

@ -8,11 +8,9 @@ import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.civilization.PlayerType
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.tile.*
import com.unciv.models.ruleset.unique.LocalUniqueCache
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.*
import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats
import com.unciv.models.translations.tr
import com.unciv.ui.civilopedia.FormattedLine
@ -319,22 +317,26 @@ open class TileInfo {
tileUniques += city.getMatchingUniques(UniqueType.StatsFromObject, stateForConditionals)
for (unique in localUniqueCache.get("StatsFromTilesAndObjects", tileUniques)) {
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))
stats.add(unique.stats)
if (tileType == "Natural Wonder" && naturalWonder != null && city.civInfo.hasUnique(UniqueType.DoubleStatsFromNaturalWonders)) {
if (!matchesTerrainFilter(tileType, observingCiv)) continue
stats.add(unique.stats)
if (naturalWonder != null
&& tileType == "Natural Wonder"
&& city.civInfo.hasUnique(UniqueType.DoubleStatsFromNaturalWonders)
) {
stats.add(unique.stats)
}
}
for (unique in localUniqueCache.get("StatsFromTilesWithout",
city.getMatchingUniques(UniqueType.StatsFromTilesWithout, stateForConditionals)))
city.getMatchingUniques(UniqueType.StatsFromTilesWithout, stateForConditionals))
) {
if (
matchesTerrainFilter(unique.params[1]) &&
!matchesTerrainFilter(unique.params[2]) &&
city.matchesFilter(unique.params[3])
)
stats.add(unique.stats)
}
}
if (isAdjacentToRiver()) stats.gold++
@ -347,17 +349,61 @@ open class TileInfo {
if (improvement != null)
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())
stats.gold++
}
if (isCityCenter()) {
if (stats.food < 2) stats.food = 2f
if (stats.production < 1) stats.production = 1f
}
for ((stat, value) in stats)
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
}
@ -421,6 +467,7 @@ open class TileInfo {
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 {
val stats = improvement.cloneStats()
if (hasViewableResource(observingCiv) && tileResource.isImprovedBy(improvement.name)
@ -464,17 +511,49 @@ open class TileInfo {
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)) {
if (improvement.matchesFilter(unique.params[1]))
stats.timesInPlace(unique.params[0].toPercent())
if (!improvement.matchesFilter(unique.params[1])) continue
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()
}
}

View File

@ -199,6 +199,13 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
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
}

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),
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),
StatsFromTilesWithout("[stats] from [tileFilter] tiles without [tileFilter] [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
// This is a doozy
@ -96,8 +95,9 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
// Stat percentage boosts
StatPercentBonus("[relativeAmount]% [stat]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
StatPercentBonusCities("[relativeAmount]% [stat] [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
StatPercentFromObject("[relativeAmount]% [stat] from every [tileFilter/specialist/buildingName]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
AllStatsPercentFromObject("[relativeAmount]% Yield from every [tileFilter]", UniqueTarget.FollowerBelief, UniqueTarget.Global),
// Todo: Add support for specialist next to buildingName
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),
BonusStatsFromCityStates("[relativeAmount]% [stat] from City-States", 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),
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),
StatsSpendingGreatPeople("[stats] whenever a Great Person is expended", UniqueTarget.Global),
UnhappinessFromPopulationTypePercentageChange("[relativeAmount]% Unhappiness from [populationFilter] [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
@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
??? 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: "[+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
??? example "[relativeAmount]% [stat] from every [tileFilter/specialist/buildingName]"
??? example "[relativeAmount]% [stat] from every [tileFilter/buildingFilter]"
Example: "[+20]% [Culture] from every [Farm]"
Applicable to: Global, FollowerBelief
??? example "[relativeAmount]% Yield from every [tileFilter]"
??? example "[relativeAmount]% Yield from every [tileFilter/buildingFilter]"
Example: "[+20]% Yield from every [Farm]"
Applicable to: Global, FollowerBelief
@ -290,6 +285,11 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
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: "[+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)
}
@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
fun statsFromTiles() {
game.makeHexagonalMap(2)
@ -200,8 +185,6 @@ class GlobalUniquesTests {
inBetweenTile.roadStatus = RoadStatus.Road
civInfo.transients().updateCitiesConnectedToCapital()
city2.cityStats.update()
println(city2.isConnectedToCapital())
println(city2.cityStats.finalStatList)
Assert.assertTrue(city2.cityStats.finalStatList["Trade routes"]!!.science == 30f)
}
@ -255,8 +238,6 @@ class GlobalUniquesTests {
civ1.updateStatsForNextTurn()
println(civ1.statsForNextTurn.science)
Assert.assertTrue(civ1.statsForNextTurn.science == 90f)
}
@ -273,11 +254,92 @@ class GlobalUniquesTests {
city.cityConstructions.addBuilding(building.name)
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)
}
@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
@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.ruleset.*
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.utils.withItem
import kotlin.math.abs
import kotlin.math.max
class TestGame {
@ -61,7 +58,9 @@ class TestGame {
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) {
val newTileMap = TileMap(newRadius, ruleset, tileMap.mapParameters.worldWrap)
newTileMap.mapParameters.mapSize = MapSizeNew(newRadius)