diff --git a/android/assets/jsons/Civ V - Vanilla/Beliefs.json b/android/assets/jsons/Civ V - Vanilla/Beliefs.json index 22f4dd87d9..c1bf98b564 100644 --- a/android/assets/jsons/Civ V - Vanilla/Beliefs.json +++ b/android/assets/jsons/Civ V - Vanilla/Beliefs.json @@ -151,7 +151,11 @@ "type": "Follower", "uniques": ["[+2 Production] "] }, - // ToDo: Holy Warriors (I have to look up the faith costs for these sometime) + { + "name": "Holy Warriors", + "type": "Follower", + "uniques": ["May buy [{Military} {Land}] units with [Faith] for [2] times their normal Production cost "] + }, { "name": "Liturgical Drama", "type": "Follower", diff --git a/android/assets/jsons/Civ V - Vanilla/Policies.json b/android/assets/jsons/Civ V - Vanilla/Policies.json index aadb5940fb..910cfa8a74 100644 --- a/android/assets/jsons/Civ V - Vanilla/Policies.json +++ b/android/assets/jsons/Civ V - Vanilla/Policies.json @@ -267,7 +267,7 @@ { "name": "Commerce Complete", "uniques": ["[+1 Gold] from every [Trading post]", "Double gold from Great Merchant trade missions", - "May buy [Great Merchant] units for [1000] [Faith] [in all cities in which the majority religion is a major religion] starting from the [Industrial era] at an increasing price ([500])" + "May buy [Great Merchant] units for [1000] [Faith] [in all cities in which the majority religion is a major religion] at an increasing price ([500]) " ] } ] @@ -314,7 +314,7 @@ { "name": "Rationalism Complete", "uniques": ["[2] Free Technologies", - "May buy [Great Scientist] units for [1000] [Faith] [in all cities in which the majority religion is a major religion] starting from the [Industrial era] at an increasing price ([500])" + "May buy [Great Scientist] units for [1000] [Faith] [in all cities in which the majority religion is a major religion] at an increasing price ([500]) " ] } ] @@ -359,7 +359,7 @@ { "name": "Freedom Complete", "uniques": ["+[100]% yield from every [Great Improvement]", "Golden Age length increased by [50]%", - "May buy [Great Artist] units for [1000] [Faith] [in all cities in which the majority religion is a major religion] starting from the [Industrial era] at an increasing price ([500])" + "May buy [Great Artist] units for [1000] [Faith] [in all cities in which the majority religion is a major religion] at an increasing price ([500]) " ] } ] @@ -407,8 +407,8 @@ { "name": "Autocracy Complete", "uniques": ["+[25]% attack strength to all [Military] units for [50] turns", - "May buy [Great General] units for [1000] [Faith] [in all cities in which the majority religion is a major religion] starting from the [Industrial era] at an increasing price ([500])", - "May buy [Great Admiral] units for [1000] [Faith] [in all cities in which the majority religion is a major religion] starting from the [Industrial era] at an increasing price ([500])" + "May buy [Great General] units for [1000] [Faith] [in all cities in which the majority religion is a major religion] at an increasing price ([500]) ", + "May buy [Great Admiral] units for [1000] [Faith] [in all cities in which the majority religion is a major religion] at an increasing price ([500]) " ] } ] @@ -453,7 +453,7 @@ { "name": "Order Complete", "uniques": ["[+2 Food, +2 Production, +2 Science, +2 Gold, +2 Culture] [in all cities]", - "May buy [Great Engineer] units for [1000] [Faith] [in all cities in which the majority religion is a major religion] starting from the [Industrial era] at an increasing price ([500])" + "May buy [Great Engineer] units for [1000] [Faith] [in all cities in which the majority religion is a major religion] at an increasing price ([500]) " ] } ] diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index f5adadb3b1..5887898828 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -507,7 +507,6 @@ Check extension mods based on vanilla = Checking mods for errors... = Show experimental world wrap for maps = HIGHLY EXPERIMENTAL - YOU HAVE BEEN WARNED! = -HIGHLY EXPERIMENTAL - UPDATES WILL BREAK SAVES! = Enable portrait orientation = Generate translation files = Translation files are generated successfully. = diff --git a/core/src/com/unciv/logic/automation/ChooseBeliefsAutomation.kt b/core/src/com/unciv/logic/automation/ChooseBeliefsAutomation.kt index 4d94a356d1..508dfd34f7 100644 --- a/core/src/com/unciv/logic/automation/ChooseBeliefsAutomation.kt +++ b/core/src/com/unciv/logic/automation/ChooseBeliefsAutomation.kt @@ -150,6 +150,8 @@ object ChooseBeliefsAutomation { // This is something completely different from the original, but I have no idea // what happens over there else civInfo.statsForNextTurn[Stat.valueOf(unique.params[1])] * 10f / civInfo.getEra().baseUnitBuyCost + UniqueType.BuyUnitsByProductionCost.placeholderText -> + 15f * if (civInfo.victoryType() == VictoryType.Domination) 2f else 1f "when a city adopts this religion for the first time (modified by game speed)" -> // Modified by personality unique.stats.values.sum() * 10f "When spreading religion to a city, gain [] times the amount of followers of other religions as []" -> @@ -160,7 +162,7 @@ object ChooseBeliefsAutomation { unique.params[0].toInt() / 7f "[] for each global city following this religion" -> 50f / unique.stats.values.sum() - "whenever a Great Person is expended" -> + UniqueType.StatsSpendingGreatPeople.placeholderText -> unique.stats.values.sum() / 2f "[]% Natural religion spread to []" -> unique.params[0].toFloat() / 4f diff --git a/core/src/com/unciv/logic/city/CityInfo.kt b/core/src/com/unciv/logic/city/CityInfo.kt index a48e2a482f..c813e9538a 100644 --- a/core/src/com/unciv/logic/city/CityInfo.kt +++ b/core/src/com/unciv/logic/city/CityInfo.kt @@ -716,18 +716,10 @@ class CityInfo { // Finds matching uniques provided from both local and non-local sources. fun getMatchingUniques( uniqueType: UniqueType, - // We might have this cached to avoid concurrency problems. If we don't, just get it directly - localUniques: Sequence = getLocalMatchingUniques(uniqueType), stateForConditionals: StateForConditionals? = null, ): Sequence { - // 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(uniqueType, stateForConditionals, this) + - localUniques.filter { - it.isOfType(uniqueType) - && it.conditionalsApply(stateForConditionals) - && it.params.none { param -> param == "in other cities" } - } + getLocalMatchingUniques(uniqueType, stateForConditionals) } // Matching uniques provided by sources in the city itself diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index 53592f6273..642cb4b95e 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -349,7 +349,7 @@ class MapUnit { visibilityRange += (getMatchingUniques(UniqueType.Sight, StateForConditionals(civInfo = civInfo, unit = this)) + civInfo.getMatchingUniques(UniqueType.Sight, StateForConditionals(civInfo = civInfo, unit = this)) ).sumOf { it.params[0].toInt() } - + // Deprecated since 3.17.5 for (unique in getMatchingUniques(UniqueType.SightUnits)) if (matchesFilter(unique.params[1])) @@ -1051,6 +1051,7 @@ class MapUnit { if (filter.contains('{')) // multiple types at once - AND logic. Looks like:"{Military} {Land}" return filter.removePrefix("{").removeSuffix("}").split("} {") .all { matchesFilter(it) } + return when (filter) { // todo: unit filters should be adjectives, fitting "[filterType] units" // This means converting "wounded units" to "Wounded", "Barbarians" to "Barbarian" diff --git a/core/src/com/unciv/models/ruleset/Ruleset.kt b/core/src/com/unciv/models/ruleset/Ruleset.kt index ea8d10c42f..5b05d38010 100644 --- a/core/src/com/unciv/models/ruleset/Ruleset.kt +++ b/core/src/com/unciv/models/ruleset/Ruleset.kt @@ -564,7 +564,10 @@ class Ruleset { } for (nation in nations.values) { checkUniques(nation, lines, UniqueType.UniqueComplianceErrorSeverity.RulesetSpecific) + if (nation.favoredReligion != null && nation.favoredReligion !in religions) + lines += "${nation.name} has ${nation.favoredReligion} as their favored religion, which does not exist!" } + for (policy in policies.values) { if (policy.requires != null) for (prereq in policy.requires!!) diff --git a/core/src/com/unciv/models/ruleset/unique/Unique.kt b/core/src/com/unciv/models/ruleset/unique/Unique.kt index cc1517f7f4..8d0efb72e8 100644 --- a/core/src/com/unciv/models/ruleset/unique/Unique.kt +++ b/core/src/com/unciv/models/ruleset/unique/Unique.kt @@ -51,6 +51,12 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s state.civInfo != null && state.civInfo.statsForNextTurn.happiness >= 0 UniqueType.ConditionalGoldenAge -> state.civInfo != null && state.civInfo.goldenAges.isGoldenAge() + UniqueType.ConditionalBeforeEra -> + state.civInfo != null && state.civInfo.getEraNumber() < state.civInfo.gameInfo.ruleSet.eras[condition.params[0]]!!.eraNumber + UniqueType.ConditionalStartingFromEra -> + state.civInfo != null && state.civInfo.getEraNumber() >= state.civInfo.gameInfo.ruleSet.eras[condition.params[0]]!!.eraNumber + UniqueType.ConditionalDuringEra -> + state.civInfo != null && state.civInfo.getEraNumber() == state.civInfo.gameInfo.ruleSet.eras[condition.params[0]]!!.eraNumber UniqueType.ConditionalSpecialistCount -> state.cityInfo != null && state.cityInfo.population.getNumberOfSpecialists() >= condition.params[0].toInt() diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index 7ff59baa53..5b90abb8a9 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -77,6 +77,8 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget) { StatsFromSpecialistDeprecated("[stats] from every specialist", UniqueTarget.Global), StatsPerPopulation("[stats] per [amount] population [cityFilter]", UniqueTarget.Global), + + StatsSpendingGreatPeople("[stats] whenever a Great Person is expended", UniqueTarget.Global), /////// City-State related uniques @@ -104,10 +106,8 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget) { @Deprecated("As of 3.16.16", ReplaceWith("[amount]% maintenance costs for [mapUnitFilter] units"), DeprecationLevel.WARNING) DecreasedUnitMaintenanceCostsGlobally("-[amount]% unit upkeep costs", UniqueTarget.Global), - ConsumesResources("Consumes [amount] [resource]", - UniqueTarget.Improvement, UniqueTarget.Building, UniqueTarget.Unit), - ProvidesResources("Provides [amount] [resource]", - UniqueTarget.Improvement, UniqueTarget.Building), + ConsumesResources("Consumes [amount] [resource]", UniqueTarget.Improvement, UniqueTarget.Building, UniqueTarget.Unit), + ProvidesResources("Provides [amount] [resource]", UniqueTarget.Improvement, UniqueTarget.Building), GrowthPercentBonus("[amount]% growth [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief), @Deprecated("As of 3.16.14", ReplaceWith("[amount]% growth [cityFilter]"), DeprecationLevel.WARNING) @@ -122,6 +122,11 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget) { FreeExtraBeliefs("May choose [amount] additional [beliefType] beliefs when [foundingOrEnhancing] a religion", UniqueTarget.Global), FreeExtraAnyBeliefs("May choose [amount] additional belief(s) of any type when [foundingOrEnhancing] a religion", UniqueTarget.Global), + BuyUnitsIncreasingCost("May buy [baseUnitFilter] units for [amount] [stat] [cityFilter] at an increasing price ([amount])", UniqueTarget.Global), + @Deprecated("As of 3.17.9", ReplaceWith ("May buy [baseUnitFilter] units for [amount] [stat] [cityFilter] at an increasing price ([amount]) ")) + BuyUnitsIncreasingCostEra("May buy [baseUnitFilter] units for [amount] [stat] [cityFilter] starting from the [era] at an increasing price ([amount])", UniqueTarget.Global), + BuyUnitsByProductionCost("May buy [baseUnitFilter] units with [stat] for [amount] times their normal Production cost", UniqueTarget.FollowerBelief, UniqueTarget.Global), + MayanGainGreatPerson("Receive a free Great Person at the end of every [comment] (every 394 years), after researching [tech]. Each bonus person can only be chosen once.", UniqueTarget.Nation), MayanCalendarDisplay("Once The Long Count activates, the year on the world screen displays as the traditional Mayan Long Count.", UniqueTarget.Nation), @@ -213,7 +218,7 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget) { HiddenWithoutReligion("Hidden when religion is disabled", UniqueTarget.Unit, UniqueTarget.Building, UniqueTarget.Ruins), - //////////////////////////////////////// TERRAIN UNIQUES //////////////////////////////////////// + ///////////////////////////////////////// TILE UNIQUES ///////////////////////////////////////// NaturalWonderNeighborCount("Must be adjacent to [amount] [simpleTerrain] tiles", UniqueTarget.Terrain), @@ -245,6 +250,10 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget) { ConditionalNotWar("when not at war", UniqueTarget.Conditional), ConditionalHappy("while the empire is happy", UniqueTarget.Conditional), ConditionalGoldenAge("during a Golden Age", UniqueTarget.Conditional), + + ConditionalDuringEra("during the [era]", UniqueTarget.Conditional), + ConditionalBeforeEra("before the [era]", UniqueTarget.Conditional), + ConditionalStartingFromEra("starting from the [era]", UniqueTarget.Conditional), // city conditionals ConditionalSpecialistCount("if this city has at least [amount] specialists", UniqueTarget.Conditional), @@ -265,6 +274,7 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget) { ///////////////////////////////////////// TRIGGERED ONE-TIME ///////////////////////////////////////// + OneTimeFreeUnit("Free [baseUnitFilter] appears", UniqueTarget.Global), // used in Policies, Buildings OneTimeAmountFreeUnits("[amount] free [baseUnitFilter] units appear", UniqueTarget.Global), // used in Buildings OneTimeFreeUnitRuins("Free [baseUnitFilter] found in the ruins", UniqueTarget.Ruins), // Differs from "Free [] appears" in that it spawns near the ruins instead of in a city diff --git a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt index 122b3c2fd8..32fa8b0b2d 100644 --- a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt +++ b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt @@ -6,6 +6,7 @@ import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.MapUnit 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.UniqueTarget import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.stats.Stat @@ -250,7 +251,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { override fun canBePurchasedWithStat(cityInfo: CityInfo?, stat: Stat): Boolean { // May buy [unitFilter] units for [amount] [Stat] [cityFilter] starting from the [eraName] at an increasing price ([amount]) - if (cityInfo != null && cityInfo.civInfo.getMatchingUniques("May buy [] units for [] [] [] starting from the [] at an increasing price ([])") + if (cityInfo != null && cityInfo.getMatchingUniques("May buy [] units for [] [] [] starting from the [] at an increasing price ([])") .any { matchesFilter(it.params[0]) && cityInfo.matchesFilter(it.params[3]) @@ -260,13 +261,22 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { ) return true // May buy [unitFilter] units for [amount] [Stat] [cityFilter] at an increasing price ([amount]) - if (cityInfo != null && cityInfo.civInfo.getMatchingUniques("May buy [] units for [] [] [] at an increasing price ([])") + if (cityInfo != null && cityInfo.getMatchingUniques("May buy [] units for [] [] [] at an increasing price ([])") .any { matchesFilter(it.params[0]) && cityInfo.matchesFilter(it.params[3]) && it.params[2] == stat.name } ) return true + + if (cityInfo != null && cityInfo.getMatchingUniques( + UniqueType.BuyUnitsByProductionCost, + stateForConditionals = StateForConditionals(civInfo = cityInfo.civInfo, cityInfo = cityInfo) + ).any { + matchesFilter(it.params[0]) + && it.params[1] == stat.name + } + ) return true return super.canBePurchasedWithStat(cityInfo, stat) } @@ -277,24 +287,26 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { override fun getBaseBuyCost(cityInfo: CityInfo, stat: Stat): Int? { if (stat == Stat.Gold) return getBaseGoldCost(cityInfo.civInfo).toInt() + val conditionalState = StateForConditionals(civInfo = cityInfo.civInfo, cityInfo = cityInfo) return ( sequenceOf(super.getBaseBuyCost(cityInfo, stat)).filterNotNull() - // May buy [unitFilter] units for [amount] [Stat] starting from the [eraName] at an increasing price ([amount]) - + (cityInfo.civInfo.getMatchingUniques("May buy [] units for [] [] [] starting from the [] at an increasing price ([])") - .filter { - matchesFilter(it.params[0]) - && cityInfo.matchesFilter(it.params[3]) - && cityInfo.civInfo.getEraNumber() >= ruleset.eras[it.params[4]]!!.eraNumber - && it.params[2] == stat.name - }.map { - getCostForConstructionsIncreasingInPrice( - it.params[1].toInt(), - it.params[5].toInt(), - cityInfo.civInfo.civConstructions.boughtItemsWithIncreasingPrice[name] ?: 0 - ) - } - ) - + (cityInfo.civInfo.getMatchingUniques("May buy [] units for [] [] [] at an increasing price ([])") + // Deprecated since 3.17.9 + + (cityInfo.getMatchingUniques(UniqueType.BuyUnitsIncreasingCostEra, conditionalState) + .filter { + matchesFilter(it.params[0]) + && cityInfo.matchesFilter(it.params[3]) + && cityInfo.civInfo.getEraNumber() >= ruleset.eras[it.params[4]]!!.eraNumber + && it.params[2] == stat.name + }.map { + getCostForConstructionsIncreasingInPrice( + it.params[1].toInt(), + it.params[5].toInt(), + cityInfo.civInfo.civConstructions.boughtItemsWithIncreasingPrice[name] ?: 0 + ) + } + ) + // + + (cityInfo.getMatchingUniques(UniqueType.BuyUnitsIncreasingCost, conditionalState) .filter { matchesFilter(it.params[0]) && cityInfo.matchesFilter(it.params[3]) @@ -307,6 +319,15 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { ) } ) + + (cityInfo.getMatchingUniques( + UniqueType.BuyUnitsByProductionCost, conditionalState + ).filter { + it.params[1] == stat.name + && matchesFilter(it.params[0]) + }.map { + getProductionCost(cityInfo.civInfo) * it.params[2].toInt() + } + ) ).minOrNull() } @@ -493,6 +514,9 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { } fun matchesFilter(filter: String): Boolean { + if (filter.contains('{')) // multiple types at once - AND logic. Looks like:"{Military} {Land}" + return filter.removePrefix("{").removeSuffix("}").split("} {") + .all { matchesFilter(it) } return when (filter) { unitType -> true diff --git a/core/src/com/unciv/models/translations/Translations.kt b/core/src/com/unciv/models/translations/Translations.kt index 6d4c450d6f..a2381c2ba7 100644 --- a/core/src/com/unciv/models/translations/Translations.kt +++ b/core/src/com/unciv/models/translations/Translations.kt @@ -213,7 +213,7 @@ class Translations : LinkedHashMap(){ // Whenever this string is changed, it should also be changed in the translation files! // It is mostly used as the template for translating the order of conditionals const val englishConditionalOrderingString = - " " + " " const val conditionalUniqueOrderString = "ConditionalsPlacement" const val shouldCapitalizeString = "StartWithCapitalLetter" }