Refactored the way cities determine what uniques should apply when (#4393)

* I'm so sorry for the size of this PR

* Uniques are better generalized

* Code now compiles

* Missed a line

* Some more cleaning up
This commit is contained in:
Xander Lenstra
2021-07-07 21:01:14 +02:00
committed by GitHub
parent ecd5ae5128
commit fbd64b9520
11 changed files with 209 additions and 114 deletions

View File

@ -26,9 +26,9 @@
"food": 2,
"maintenance": 1,
"hurryCostModifier": 25,
"uniques": ["[+1 Food] from [Deer] tiles in this city",
"[+1 Food] from [Bananas] tiles in this city",
"[+1 Food] from [Wheat] tiles in this city"],
"uniques": ["[+1 Food] from [Deer] tiles [in this city]",
"[+1 Food] from [Bananas] tiles [in this city]",
"[+1 Food] from [Wheat] tiles [in this city]"],
"requiredTech": "Pottery"
},
{
@ -57,8 +57,8 @@
"maintenance": 1,
"hurryCostModifier": 0,
"uniques": ["Must not be on [Plains]",
"[+1 Production] from [Stone] tiles in this city",
"[+1 Production] from [Marble] tiles in this city"],
"[+1 Production] from [Stone] tiles [in this city]",
"[+1 Production] from [Marble] tiles [in this city]"],
"requiredTech": "Calendar"
},
{
@ -118,7 +118,7 @@
"uniqueTo": "Aztecs",
"food": 2,
"production": 1,
"uniques": ["[+2 Food] from [Lakes] tiles in this city", "Must be next to [Fresh water]"],
"uniques": ["[+2 Food] from [Lakes] tiles [in this city]", "Must be next to [Fresh water]"],
"hurryCostModifier": 25,
"maintenance": 1,
"percentStatBonus": {"food": 15},
@ -156,7 +156,7 @@
"greatPersonPoints": {"gold": 1},
"isWonder": true,
"uniques": ["Provides a sum of gold each time you spend a Great Person",
"[+2 Gold] from [Marble] tiles in this city", "[+2 Gold] from [Stone] tiles in this city"],
"[+2 Gold] from [Marble] tiles [in this city]", "[+2 Gold] from [Stone] tiles [in this city]"],
"requiredTech": "Masonry",
"quote": "'The whole earth is the tomb of heroic men and their story is not given only on stone over their clay but abides everywhere without visible symbol woven into the stuff of other men's lives.' - Pericles"
},
@ -195,8 +195,8 @@
"cost": 75,
"hurryCostModifier": 25,
"maintenance": 1,
"uniques": ["Must be next to [Coast]", "[+1 Food] from [Ocean] tiles in this city",
"[+1 Food] from [Coast] tiles in this city", "[+1 Food] from [Fish] tiles in this city" ],
"uniques": ["Must be next to [Coast]", "[+1 Food] from [Ocean] tiles [in this city]",
"[+1 Food] from [Coast] tiles [in this city]", "[+1 Food] from [Fish] tiles [in this city]" ],
"requiredTech": "Optics"
},
{
@ -217,9 +217,9 @@
"requiredNearbyImprovedResources": ["Horses","Sheep","Cattle"],
"hurryCostModifier": 25,
"uniques": ["+[15]% Production when constructing [Mounted] units [in this city]",
"[+1 Production] from [Cattle] tiles in this city",
"[+1 Production] from [Horses] tiles in this city",
"[+1 Production] from [Sheep] tiles in this city"],
"[+1 Production] from [Cattle] tiles [in this city]",
"[+1 Production] from [Horses] tiles [in this city]",
"[+1 Production] from [Sheep] tiles [in this city]"],
"requiredTech": "Horseback Riding"
},
{
@ -349,7 +349,7 @@
"cost": 125,
"culture": 1,
"isNationalWonder": true,
"uniques": ["Requires a [Monument] in all cities", "+[25]% great person generation in this city", "Cost increases by [30] per owned city"],
"uniques": ["Requires a [Monument] in all cities", "[+25]% great person generation [in this city]", "Cost increases by [30] per owned city"],
"requiredTech": "Drama and Poetry"
},
{
@ -369,7 +369,7 @@
"hurryCostModifier": 25,
"percentStatBonus": {"gold": 25},
"uniques": ["Provides 1 extra copy of each improved luxury resource near this City",
"[+2 Gold] from [Oil] tiles in this city", "[+2 Gold] from [Oasis] tiles in this city"],
"[+2 Gold] from [Oil] tiles [in this city]", "[+2 Gold] from [Oasis] tiles [in this city]"],
"requiredTech": "Currency"
},
{
@ -377,7 +377,7 @@
"maintenance": 0,
"requiredNearbyImprovedResources": ["Gold Ore", "Silver"],
"hurryCostModifier": 25,
"uniques": ["[+2 Gold] from [Gold Ore] tiles in this city", "[+2 Gold] from [Silver] tiles in this city"],
"uniques": ["[+2 Gold] from [Gold Ore] tiles [in this city]", "[+2 Gold] from [Silver] tiles [in this city]"],
"requiredTech": "Currency"
},
{ // Flood plains are not supposed to be affected by this wonder. Add stats to all desert tiles and then add negative stats to flood plains to undo the effect on them for now
@ -385,7 +385,7 @@
"culture": 1,
"isWonder": true,
"greatPersonPoints": {"production": 1},
"uniques": ["Must be next to [Desert]", "[+1 Food, +1 Production] from [Desert] tiles in this city", "[-1 Food, -1 Production] from [Flood plains] tiles in this city", "[+6 Culture] once [Archaeology] is discovered"],
"uniques": ["Must be next to [Desert]", "[+1 Food, +1 Production] from [Desert] tiles [in this city]", "[-1 Food, -1 Production] from [Flood plains] tiles [in this city]", "[+6 Culture] once [Archaeology] is discovered"],
"requiredTech": "Currency",
"quote": "'...who drinks the water I shall give him, says the Lord, will have a spring inside him welling up for eternal life. Let them bring me to your holy mountain in the place where you dwell. Across the desert and through the mountain to the Canyon of the Crescent Moon...' - Indiana Jones"
},
@ -411,7 +411,7 @@
"cost": 125,
"culture": 1,
"isNationalWonder": true,
"uniques": ["Requires a [Barracks] in all cities", "All newly-trained [non-air] units in this city receive the [Morale] promotion",
"uniques": ["Requires a [Barracks] in all cities", "All newly-trained [non-air] units [in this city] receive the [Morale] promotion",
"Cost increases by [30] per owned city"],
"requiredTech": "Iron Working"
},
@ -422,7 +422,7 @@
"gold": 5,
"greatPersonPoints": {"gold": 1},
"isWonder": true,
"uniques": ["Must be next to [Coast]", "[+1 Gold] from [Water] tiles in this city"],
"uniques": ["Must be next to [Coast]", "[+1 Gold] from [Water] tiles [in this city]"],
"requiredTech": "Iron Working",
"quote": "'Why man, he doth bestride the narrow world like a colossus, and we petty men walk under his huge legs, and peep about to find ourselves dishonorable graves.' - William Shakespeare, Julius Caesar"
},
@ -432,7 +432,7 @@
// Column 5
{
"name": "Garden",
"uniques": ["+[25]% great person generation in this city", "Must be next to [Fresh water]"],
"uniques": ["[+25]% great person generation [in this city]", "Must be next to [Fresh water]"],
"hurryCostModifier": 25,
"maintenance": 1,
"requiredTech": "Theology"
@ -441,7 +441,7 @@
"name": "Monastery",
"maintenance": 0,
"requiredNearbyImprovedResources": ["Wine","Incense"],
"uniques": ["[+2 Culture] from [Wine] tiles in this city", "[+2 Culture] from [Incense] tiles in this city"],
"uniques": ["[+2 Culture] from [Wine] tiles [in this city]", "[+2 Culture] from [Incense] tiles [in this city]"],
"hurryCostModifier": 25,
"requiredTech": "Theology"
},
@ -450,7 +450,7 @@
"culture": 1,
"greatPersonPoints": {"culture": 1},
"isWonder": true,
"uniques": ["+[25]% great person generation in all cities"],
"uniques": ["[+25]% great person generation [in all cities]"],
"requiredTech": "Theology",
"quote": "'For it soars to a height to match the sky, and as if surging up from among the other buildings it stands on high and looks down upon the remainder of the city, adorning it, because it is a part of it, but glorying in its own beauty' - Procopius, De Aedificis"
},
@ -501,7 +501,7 @@
"production": 2,
"specialistSlots": {"Engineer": 1},
"hurryCostModifier": 25,
"uniques": ["[+1 Production] from [Forest] tiles in this city"],
"uniques": ["[+1 Production] from [Forest] tiles [in this city]"],
"requiredTech": "Metal Casting"
},
{
@ -514,7 +514,7 @@
// "+[15]% Production when constructing [Spaceship part] units [in this city]"
"uniques": ["+[15]% Production when constructing [Spaceship part] [in this city]",
"+[15]% Production when constructing [Land] units [in this city]",
"[+1 Production] from [Iron] tiles in this city"]
"[+1 Production] from [Iron] tiles [in this city]"]
},
// Column 6
{
@ -522,7 +522,7 @@
"cost": 120,
"maintenance": 2,
"hurryCostModifier": 25,
"uniques": ["[+1 Production] from [Water resource] tiles in this city",
"uniques": ["[+1 Production] from [Water resource] tiles [in this city]",
"Connects trade routes over water","Must be next to [Coast]"],
"requiredTech": "Compass"
},
@ -533,7 +533,7 @@
"percentStatBonus": {"science": 33},
"specialistSlots": {"Scientist": 2},
"requiredBuilding": "Library",
"uniques": ["[+2 Science] from [Jungle] tiles in this city"],
"uniques": ["[+2 Science] from [Jungle] tiles [in this city]"],
"requiredTech": "Education"
},
{
@ -546,7 +546,7 @@
"specialistSlots": {"Scientist": 2},
"culture": 3,
"requiredBuilding": "Library",
"uniques": ["[+2 Science] from [Jungle] tiles in this city"],
"uniques": ["[+2 Science] from [Jungle] tiles [in this city]"],
"requiredTech": "Education"
},
{
@ -596,7 +596,7 @@
"isWonder": true,
"providesFreeBuilding": "Castle",
"percentStatBonus": {"culture": 20},
"uniques": ["All newly-trained [relevant] units in this city receive the [Drill I] promotion"],
"uniques": ["All newly-trained [relevant] units [in this city] receive the [Drill I] promotion"],
"requiredTech": "Chivalry",
"quote": "'Justice is an unassailable fortress, built on the brow of a mountain which cannot be overthrown by the violence of torrents, nor demolished by the force of armies.' - Joseph Addison"
},
@ -713,7 +713,7 @@
"culture": 1,
"isWonder": true,
"greatPersonPoints": {"culture": 1},
"uniques": ["+[25]% great person generation in all cities", "Free Great Person"],
"uniques": ["[+25]% great person generation [in all cities]", "Free Great Person"],
"requiredTech": "Printing Press",
"quote": "'Don't clap too hard - it's a very old building.' - John Osbourne"
},
@ -733,7 +733,7 @@
"hurryCostModifier": 25,
"maintenance": 3,
"requiredBuilding": "Harbor",
"uniques": ["[+1 Production, +1 Gold] from [Water resource] tiles in this city",
"uniques": ["[+1 Production, +1 Gold] from [Water resource] tiles [in this city]",
"Must be next to [Coast]", "+[15]% Production when constructing [Water] units [in this city]"],
"requiredTech": "Navigation"
},
@ -888,7 +888,7 @@
"requiredResource": "Aluminum",
"hurryCostModifier": 0,
"maintenance": 3,
"uniques": ["Must be on [River]","[+1 Production] from [River] tiles in this city"],
"uniques": ["Must be on [River]","[+1 Production] from [River] tiles [in this city]"],
"requiredTech": "Electricity"
},
*/

View File

@ -327,7 +327,7 @@
{
"name": "Freedom",
"era": "Renaissance era",
"uniques": ["+[25]% great person generation in all cities", "Incompatible with [Autocracy]", "Incompatible with [Order]"],
"uniques": ["[+25]% great person generation [in all cities]", "Incompatible with [Autocracy]", "Incompatible with [Order]"],
"policies": [
{
"name": "Constitution",

View File

@ -79,13 +79,40 @@ class CityConstructions {
val stats = Stats()
for (building in getBuiltBuildings())
stats.add(building.getStats(cityInfo))
for (unique in builtBuildingUniqueMap.getAllUniques()) when (unique.placeholderText) {
"[] per [] population []" -> if (cityInfo.matchesFilter(unique.params[2]))
stats.add(unique.stats.times(cityInfo.population.population / unique.params[1].toFloat()))
"[] once [] is discovered" -> if (cityInfo.civInfo.tech.isResearched(unique.params[1])) stats.add(unique.stats)
// Why only the local matching uniques you ask? Well, the non-local uniques are evaluated in
// CityStats.getStatsFromUniques(uniques). This function gets a list of uniques and one of
// the placeholderTexts it filters for is "[] per [] population []". Why doesn't that function
// then not also handle the local (i.e. cityFilter == "in this city") versions?
// This is because of what it is used for. The only time time a unique with this placeholderText
// is actually contained in `uniques`, is when `getStatsFromUniques` is called for determining
// the stats a city receives from wonders. It is then called with `unique` being the list
// of all specifically non-local uniques of all cities.
//
// This averts the problem, albeit it barely, and it might change in the future without
// anyone noticing, which might lead to further bugs. So why can't these two unique checks
// just be merged then? Because of another problem.
//
// As noted earlier, `getStatsFromUniques` is called in that case to calculate the stats
// this city received from wonders, both local and non-local. This function, `getStats`,
// is only called to calculate the stats the city receives from local buildings.
// In the current codebase with the current JSON objects it just so happens to be that
// all non-local uniques with this placeholderText from other cities belong to wonders,
// while the local uniques with this placeholderText are from buildings, but this is in no
// way a given. In reality, there should be functions getBuildingStats and getWonderStats,
// to solve this, with getStats merely adding these two together. Implementing this is on
// my ToDoList, but this PR is already large enough as it is.
for (unique in cityInfo.getLocalMatchingUniques("[] per [] population []")
.filter { cityInfo.matchesFilter(it.params[2])}
) {
stats.add(unique.stats.times(cityInfo.population.population / unique.params[1].toFloat()))
}
for (unique in cityInfo.getLocalMatchingUniques("[] once [] is discovered")) {
if (cityInfo.civInfo.tech.isResearched(unique.params[1]))
stats.add(unique.stats)
}
return stats
}

View File

@ -6,6 +6,7 @@ import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.map.RoadStatus
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
import com.unciv.models.ruleset.Unique
import com.unciv.models.ruleset.tile.ResourceSupplyList
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.unit.BaseUnit
@ -253,7 +254,7 @@ class CityInfo {
cityResources.add(resource, -amount, "Buildings")
}
}
for (unique in cityConstructions.builtBuildingUniqueMap.getUniques("Provides [] []")) { // E.G "Provides [1] [Iron]"
for (unique in getLocalMatchingUniques("Provides [] []")) { // E.G "Provides [1] [Iron]"
val resource = getRuleset().tileResources[unique.params[1]]
if (resource != null) {
cityResources.add(resource, unique.params[0].toInt() * civInfo.getResourceModifier(resource), "Tiles")
@ -337,10 +338,20 @@ class CityInfo {
// this is not very efficient, and if it causes problems we can try and think of a way of improving it
if (stat != null) entry.value.add(stat, entry.value.get(stat) * unique.params[1].toFloat() / 100)
}
for (unique in civInfo.getMatchingUniques("+[]% great person generation in all cities")
+ cityConstructions.builtBuildingUniqueMap.getUniques("+[]% great person generation in this city"))
stats[entry.key] = stats[entry.key]!!.times(1 + (unique.params[0].toFloat() / 100))
for (unique in getMatchingUniques("[]% great person generation []")) {
if (!matchesFilter(unique.params[1])) continue
stats[entry.key]!!.timesInPlace(1 + unique.params[0].toFloat() / 100f)
}
// Deprecated since 3.15.9
for (unique in getMatchingUniques("+[]% great person generation in this city")
+ getMatchingUniques("+[]% great person generation in all cities")
) {
stats[entry.key]!!.timesInPlace(1 + unique.params[0].toFloat() / 100f)
}
//
}
return stats
@ -532,6 +543,54 @@ class CityInfo {
else -> false
}
}
// So everywhere in the codebase there were continuous calls to either
// `cityConstructions.builtBuildingUniqueMap.getUniques()` or `cityConstructions.builtBuildingMap.getAllUniques()`,
// which was fine as long as those were the only uniques that cities could provide.
// However, with the introduction of religion, cities might also get uniques from the religion the city follows.
// Adding both calls to `builtBuildingsUniqueMap` and `Religion` every time is not really modular and also ugly, so something had to be done.
// Looking at all the use cases, the following functions were written to handle all findMatchingUniques() problems.
// Sadly, due to the large disparity between use cases, there needed to be lots of functions.
// Finds matching uniques provided from both local and non-local sources.
fun getMatchingUniques(
placeholderText: String,
// We might have this cached to avoid concurrency problems. If we don't, just get it directly
localUniques: Sequence<Unique> = getLocalMatchingUniques(placeholderText),
): Sequence<Unique> {
// The localUniques might not be filtered when passed as a parameter, so we filter it anyway
// The time loss shouldn't be that large I don't think
return civInfo.getMatchingUniques(placeholderText, this) +
localUniques.filter { it.placeholderText == placeholderText }
}
// Matching uniques provided by sources in the city itself
fun getLocalMatchingUniques(placeholderText: String): Sequence<Unique> {
return (
cityConstructions.builtBuildingUniqueMap.getUniques(placeholderText) +
religion.getMatchingUniques(placeholderText)
).asSequence()
}
// Get all uniques that originate from this city
fun getAllLocalUniques(): Sequence<Unique> {
return cityConstructions.builtBuildingUniqueMap.getAllUniques() + religion.getUniques()
}
// Get all matching uniques that don't apply to only this city
fun getMatchingUniquesWithNonLocalEffects(placeholderText: String): Sequence<Unique> {
return cityConstructions.builtBuildingUniqueMap.getUniques(placeholderText).asSequence()
.filter { it.params.none { param -> param == "in this city" } }
// Note that we don't query religion here, as those only have local effects (for now at least)
}
// Get all uniques that don't apply to only this city
fun getAllUniquesWithNonLocalEffects(): Sequence<Unique> {
return cityConstructions.builtBuildingUniqueMap.getAllUniques()
.filter { it.params.none { param -> param == "in this city" } }
// Note that we don't query religion here, as those only have local effects (for now at least)
}
//endregion
}

View File

@ -8,14 +8,14 @@ class CityInfoReligionManager: Counter<String>() {
@Transient
lateinit var cityInfo: CityInfo
fun getUniques(): List<Unique> {
fun getUniques(): Sequence<Unique> {
val majorityReligion = getMajorityReligion()
if (majorityReligion == null) return listOf()
if (majorityReligion == null) return sequenceOf()
// This should later be changed when religions can have multiple beliefs
return cityInfo.civInfo.gameInfo.ruleSet.beliefs[majorityReligion]!!.uniqueObjects
return cityInfo.civInfo.gameInfo.ruleSet.beliefs[majorityReligion]!!.uniqueObjects.asSequence()
}
fun getMatchingUniques(unique: String): List<Unique> {
fun getMatchingUniques(unique: String): Sequence<Unique> {
return getUniques().filter { it.placeholderText == unique }
}

View File

@ -373,22 +373,24 @@ class CityStats {
newBaseStatList["National ability"] = getStatsFromNationUnique()
newBaseStatList["Wonders"] = getStatsFromUniques(civInfo.getCivWideBuildingUniques())
newBaseStatList["City-States"] = getStatsFromCityStates()
newBaseStatList["Religion"] = getStatsFromUniques(cityInfo.religion.getUniques())
baseStatList = newBaseStatList
}
private fun updateStatPercentBonusList(currentConstruction: IConstruction, citySpecificUniques: Sequence<Unique>) {
private fun updateStatPercentBonusList(currentConstruction: IConstruction, localBuildingUniques: Sequence<Unique>) {
val newStatPercentBonusList = LinkedHashMap<String, Stats>()
newStatPercentBonusList["Golden Age"] = getStatPercentBonusesFromGoldenAge(cityInfo.civInfo.goldenAges.isGoldenAge())
newStatPercentBonusList["Policies"] = getStatPercentBonusesFromUniques(currentConstruction, cityInfo.civInfo.policies.policyUniques.getAllUniques())
newStatPercentBonusList["Buildings"] = getStatPercentBonusesFromUniques(currentConstruction, citySpecificUniques)
newStatPercentBonusList["Buildings"] = getStatPercentBonusesFromUniques(currentConstruction, localBuildingUniques)
.plus(cityInfo.cityConstructions.getStatPercentBonuses()) // This function is to be deprecated but it'll take a while.
newStatPercentBonusList["Wonders"] = getStatPercentBonusesFromUniques(currentConstruction, cityInfo.civInfo.getCivWideBuildingUniques())
newStatPercentBonusList["Railroad"] = getStatPercentBonusesFromRailroad()
newStatPercentBonusList["Resources"] = getStatPercentBonusesFromResources(currentConstruction)
newStatPercentBonusList["National ability"] = getStatPercentBonusesFromNationUnique(currentConstruction)
newStatPercentBonusList["Puppet City"] = getStatPercentBonusesFromPuppetCity()
newStatPercentBonusList["Religion"] = getStatPercentBonusesFromUniques(currentConstruction, cityInfo.religion.getUniques())
if (UncivGame.Current.superchargedForDebug) {
val stats = Stats()
@ -402,12 +404,13 @@ class CityStats {
fun update(currentConstruction: IConstruction = cityInfo.cityConstructions.getCurrentConstruction()) {
// We calculate this here for concurrency reasons
// If something needs this, we pass this through as a parameter
val citySpecificUniques = getCitySpecificUniques()
val localBuildingUniques = cityInfo.cityConstructions.builtBuildingUniqueMap.getAllUniques()
val citySpecificUniques = cityInfo.getAllLocalUniques()
// We need to compute Tile yields before happiness
updateBaseStatList()
updateCityHappiness()
updateStatPercentBonusList(currentConstruction, citySpecificUniques)
updateStatPercentBonusList(currentConstruction, localBuildingUniques)
updateFinalStatList(currentConstruction, citySpecificUniques) // again, we don't edit the existing currentCityStats directly, in order to avoid concurrency exceptions
@ -498,21 +501,6 @@ class CityStats {
finalStatList = newFinalStatList
}
private fun getCitySpecificUniques(): Sequence<Unique> {
return cityInfo.cityConstructions.builtBuildingUniqueMap.getAllUniques()
.filter { it.params.isNotEmpty() && it.params.last() == "in this city" }
}
private fun getUniquesForThisCity(
unique: String,
// We might have to cached to avoid concurrency problems, so if we don't, just get it directly
citySpecificUniques: Sequence<Unique> = getCitySpecificUniques()
): Sequence<Unique> {
return citySpecificUniques.filter { it.placeholderText == unique } +
cityInfo.civInfo.getMatchingUniques(unique).filter { cityInfo.matchesFilter(it.params[1]) } +
cityInfo.religion.getMatchingUniques(unique)
}
private fun getBuildingMaintenanceCosts(citySpecificUniques: Sequence<Unique>): Float {
// Same here - will have a different UI display.
var buildingsMaintenance = cityInfo.cityConstructions.getMaintenanceCosts().toFloat() // this is AFTER the bonus calculation!
@ -521,12 +509,12 @@ class CityStats {
}
// e.g. "-[50]% maintenance costs for buildings [in this city]"
for (unique in getUniquesForThisCity("-[]% maintenance cost for buildings []", citySpecificUniques)) {
for (unique in cityInfo.getMatchingUniques("-[]% maintenance cost for buildings []", citySpecificUniques)) {
buildingsMaintenance *= (1f - unique.params[0].toFloat() / 100)
}
// Deprecated since 3.15
for (unique in getUniquesForThisCity("-[]% building maintenance costs []", citySpecificUniques)) {
for (unique in cityInfo.getMatchingUniques("-[]% building maintenance costs []", citySpecificUniques)) {
buildingsMaintenance *= (1f - unique.params[0].toFloat() / 100)
}
//

View File

@ -59,9 +59,11 @@ class PopulationManager {
}
if (foodStored >= getFoodToNextPopulation()) { // growth!
foodStored -= getFoodToNextPopulation()
val percentOfFoodCarriedOver = cityInfo.cityConstructions.builtBuildingUniqueMap
.getUniques("[]% of food is carried over after population increases")
var percentOfFoodCarriedOver = cityInfo
.getMatchingUniques("[]% of food is carried over after population increases")
.sumBy { it.params[0].toInt() }
// Try to avoid runaway food gain in mods, just in case mod makes don't notice it
if (percentOfFoodCarriedOver > 95) percentOfFoodCarriedOver = 95
foodStored += (getFoodToNextPopulation() * percentOfFoodCarriedOver / 100f).toInt()
population++
autoAssignPopulation()

View File

@ -242,18 +242,16 @@ class CivilizationInfo {
fun hasResource(resourceName: String): Boolean = getCivResourcesByName()[resourceName]!! > 0
fun getCivWideBuildingUniques(): Sequence<Unique> = cities.asSequence().flatMap {
city -> city.cityConstructions.builtBuildingUniqueMap.getAllUniques()
.filter { it.params.isEmpty() || it.params.last() != "in this city" }
city -> city.getAllUniquesWithNonLocalEffects()
}
fun hasUnique(unique: String) = getMatchingUniques(unique).any()
// Does not return local uniques, only global ones.
fun getMatchingUniques(uniqueTemplate: String): Sequence<Unique> {
fun getMatchingUniques(uniqueTemplate: String, cityToIgnore: CityInfo? = null): Sequence<Unique> {
return nation.uniqueObjects.asSequence().filter { it.placeholderText == uniqueTemplate } +
cities.asSequence().flatMap {
city -> city.cityConstructions.builtBuildingUniqueMap.getUniques(uniqueTemplate).asSequence()
.filter { it.params.isEmpty() || it.params.last() != "in this city" }
cities.filter { it != cityToIgnore}.flatMap {
city -> city.getMatchingUniquesWithNonLocalEffects(uniqueTemplate)
} +
policies.policyUniques.getUniques(uniqueTemplate) +
tech.getTechUniques().filter { it.placeholderText == uniqueTemplate } +

View File

@ -225,11 +225,13 @@ open class TileInfo {
}
if (city != null) {
val cityWideUniques = city.cityConstructions.builtBuildingUniqueMap.getUniques("[] from [] tiles in this city")
val civWideUniques = city.civInfo.getMatchingUniques("[] from every []")
val religionUniques = city.religion.getMatchingUniques( "[] from every []")
// Should be refactored to use city.getUniquesForThisCity(), probably
for (unique in cityWideUniques + civWideUniques + religionUniques) {
var tileUniques = city.getMatchingUniques("[] from [] tiles []")
.filter { city.matchesFilter(it.params[2]) }
// Deprecated since 3.15.9
tileUniques += city.getLocalMatchingUniques("[] from [] tiles in this city")
//
tileUniques += city.getMatchingUniques("[] from every []")
for (unique in 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))
@ -278,12 +280,16 @@ open class TileInfo {
stats.add(unique.stats)
if (city != null) {
val cityWideUniques = city.cityConstructions.builtBuildingUniqueMap.getUniques("[] from [] tiles in this city")
var tileUniques = city.getMatchingUniques("[] from [] tiles []")
.filter { city.matchesFilter(it.params[2]) }
// Deprecated since 3.15.9
tileUniques += city.getLocalMatchingUniques("[] from [] tiles in this city")
//
val improvementUniques = improvement.uniqueObjects.filter {
it.placeholderText == "[] on [] tiles once [] is discovered"
&& observingCiv.tech.isResearched(it.params[2])
}
for (unique in cityWideUniques + improvementUniques) {
for (unique in tileUniques + improvementUniques) {
if (improvement.matchesFilter(unique.params[1])
// Freshwater and non-freshwater cannot be moved to matchesUniqueFilter since that creates an endless feedback.
// If you're attempting that, check that it works!
@ -292,7 +298,7 @@ open class TileInfo {
stats.add(unique.stats)
}
for (unique in city.civInfo.getMatchingUniques("[] from every []") + city.religion.getMatchingUniques("[] from every []")) {
for (unique in city.getMatchingUniques("[] from every []")) {
if (improvement.matchesFilter(unique.params[1])) {
stats.add(unique.stats)
}

View File

@ -88,7 +88,7 @@ class Building : NamedStats(), IConstruction {
val tileBonusHashmap = HashMap<String, ArrayList<String>>()
val finalUniques = ArrayList<String>()
for (unique in uniqueObjects)
if (unique.placeholderText == "[] from [] tiles in this city") {
if (unique.placeholderText == "[] from [] tiles []" && unique.params[2] == "in this city") {
val stats = unique.params[0]
if (!tileBonusHashmap.containsKey(stats)) tileBonusHashmap[stats] = ArrayList()
tileBonusHashmap[stats]!!.add(unique.params[1])
@ -155,30 +155,27 @@ class Building : NamedStats(), IConstruction {
fun getStats(city: CityInfo?): Stats {
val stats = this.clone()
val civInfo = city?.civInfo
if (civInfo != null) {
val baseBuildingName = getBaseBuilding(civInfo.gameInfo.ruleSet).name
if (city == null) return stats
val civInfo = city.civInfo
// We don't have to check whether 'city' is null, as if it was, cityInfo would also be null, and we wouldn't be here.
for (unique in civInfo.getMatchingUniques("[] from every []") + city.religion.getMatchingUniques("[] from every []")) {
if (unique.params[1] != baseBuildingName) continue
stats.add(unique.stats)
}
for (unique in uniqueObjects)
if (unique.placeholderText == "[] with []" && civInfo.hasResource(unique.params[1])
&& Stats.isStats(unique.params[0]))
stats.add(unique.stats)
if (!isWonder)
for (unique in civInfo.getMatchingUniques("[] from all [] buildings")) {
if (isStatRelated(Stat.valueOf(unique.params[1])))
stats.add(unique.stats)
}
else
for (unique in civInfo.getMatchingUniques("[] from every Wonder"))
stats.add(unique.stats)
for (unique in city.getMatchingUniques("[] from every []")) {
if (!matchesFilter(unique.params[1])) continue
stats.add(unique.stats)
}
for (unique in uniqueObjects)
if (unique.placeholderText == "[] with []" && civInfo.hasResource(unique.params[1])
&& Stats.isStats(unique.params[0]))
stats.add(unique.stats)
if (!isWonder)
for (unique in city.getMatchingUniques("[] from all [] buildings")) {
if (isStatRelated(Stat.valueOf(unique.params[1])))
stats.add(unique.stats)
}
else
for (unique in city.getMatchingUniques("[] from every Wonder"))
stats.add(unique.stats)
return stats
}

View File

@ -286,22 +286,40 @@ class BaseUnit : INamed, IConstruction, CivilopediaText() {
var XP = cityConstructions.getBuiltBuildings().sumBy { it.xpForNewUnits }
for (unique in cityConstructions.builtBuildingUniqueMap
.getUniques("New [] units start with [] Experience in this city")
+ civInfo.getMatchingUniques("New [] units start with [] Experience")) {
for (unique in
cityConstructions.cityInfo.getMatchingUniques("New [] units start with [] Experience") +
cityConstructions.cityInfo.getMatchingUniques("New [] units start with [] Experience []")
.filter { cityConstructions.cityInfo.matchesFilter(it.params[2]) } +
// Deprecated since 3.15.9
cityConstructions.cityInfo.getLocalMatchingUniques("New [] units start with [] Experience in this city")
//
) {
if (unit.matchesFilter(unique.params[0]))
XP += unique.params[1].toInt()
}
unit.promotions.XP = XP
for (unique in cityConstructions.builtBuildingUniqueMap
.getUniques("All newly-trained [] units in this city receive the [] promotion")) {
for (unique in
cityConstructions.cityInfo.getMatchingUniques("All newly-trained [] units [] receive the [] promotion")
.filter { cityConstructions.cityInfo.matchesFilter(it.params[1]) } +
// Deprecated since 3.15.9
cityConstructions.cityInfo.getLocalMatchingUniques("All newly-trained [] units in this city receive the [] promotion")
//
) {
val filter = unique.params[0]
val promotion = unique.params[1]
val promotion = unique.params.last()
if (unit.matchesFilter(filter) || (filter == "relevant" && civInfo.gameInfo.ruleSet.unitPromotions.values
.any { unit.type.name in it.unitTypes && it.name == promotion }))
if (unit.matchesFilter(filter) ||
(
filter == "relevant" &&
civInfo.gameInfo.ruleSet.unitPromotions.values
.any {
it.name == promotion && unit.type.name in it.unitTypes
}
)
) {
unit.promotions.addPromotion(promotion, isFree = true)
}
}
return true