Adds conditionals to most of the uniques currently in the enum (#5270)

* Moved uniques to their own folder

* Added support for conditionals to most of the uniques in the current enum

* Deprecation > removal, of course

* Fixed tests & added `.removeConditionals` before checking for placeholders
This commit is contained in:
Xander Lenstra
2021-09-19 17:43:32 +02:00
committed by GitHub
parent 62e3dbe014
commit f47f427b05
11 changed files with 83 additions and 53 deletions

View File

@ -124,7 +124,10 @@ class CityStats(val cityInfo: CityInfo) {
stats.food += 2
} else {
for (bonus in eraInfo.getCityStateBonuses(otherCiv.cityStateType, relationshipLevel)) {
if (bonus.isOfType(UniqueType.CityStateStatsPerCity) && cityInfo.matchesFilter(bonus.params[1])) {
if (bonus.isOfType(UniqueType.CityStateStatsPerCity)
&& cityInfo.matchesFilter(bonus.params[1])
&& bonus.conditionalsApply(otherCiv, cityInfo)
) {
stats.add(bonus.stats)
}
}
@ -168,7 +171,7 @@ class CityStats(val cityInfo: CityInfo) {
var bonus = 0f
// "[amount]% growth [cityFilter]"
for (unique in cityInfo.getMatchingUniques("[]% growth []")) {
if (!unique.conditionalsApply(cityInfo.civInfo)) continue
if (!unique.conditionalsApply(cityInfo.civInfo, cityInfo)) continue
if (cityInfo.matchesFilter(unique.params[1]))
bonus += unique.params[0].toFloat()
}
@ -216,8 +219,12 @@ class CityStats(val cityInfo: CityInfo) {
private fun getStatsFromUniques(uniques: Sequence<Unique>): Stats {
val stats = Stats()
for (unique in uniques.toList()) { // Should help mitigate getConstructionButtonDTOs concurrency problems.
if (unique.isOfType(UniqueType.StatsPerCity) && cityInfo.matchesFilter(unique.params[1]))
for (unique in uniques.toList()) { // Should help mitigate getConstructionButtonDTOs concurrency problems.
if (unique.isOfType(UniqueType.Stats) && unique.conditionalsApply(cityInfo.civInfo, cityInfo)) {
stats.add(unique.stats)
}
if (unique.isOfType(UniqueType.StatsPerCity) && cityInfo.matchesFilter(unique.params[1]) && unique.conditionalsApply(cityInfo.civInfo))
stats.add(unique.stats)
// "[stats] per [amount] population [cityFilter]"
@ -233,11 +240,14 @@ class CityStats(val cityInfo: CityInfo) {
// "[stats] in cities on [tileFilter] tiles"
if (unique.placeholderText == "[] in cities on [] tiles" && cityInfo.getCenterTile().matchesTerrainFilter(unique.params[1]))
stats.add(unique.stats)
// "[stats] if this city has at least [amount] specialists"
if (unique.matches(UniqueType.StatBonusForNumberOfSpecialists, cityInfo.getRuleset())
&& cityInfo.population.getNumberOfSpecialists() >= unique.params[1].toInt())
stats.add(unique.stats)
// Deprecated since 3.16.16
// "[stats] if this city has at least [amount] specialists"
if (unique.matches(UniqueType.StatBonusForNumberOfSpecialists, cityInfo.getRuleset())
&& cityInfo.population.getNumberOfSpecialists() >= unique.params[1].toInt()
)
stats.add(unique.stats)
//
// Deprecated since a very long time ago, moved here from another code section
if (unique.placeholderText == "+2 Culture per turn from cities before discovering Steam Power" && !cityInfo.civInfo.tech.isResearched("Steam Power"))

View File

@ -19,6 +19,7 @@ import kotlin.math.pow
/** Class containing city-state-specific functions */
class CityStateFunctions(val civInfo: CivilizationInfo) {
/** Attempts to initialize the city state, returning true if successful. */
fun initCityState(ruleset: Ruleset, startingEra: String, unusedMajorCivs: Collection<String>): Boolean {
val cityStateType = ruleset.nations[civInfo.civName]?.cityStateType
@ -53,7 +54,8 @@ class CityStateFunctions(val civInfo: CivilizationInfo) {
// Unique unit for militaristic city-states
if (allPossibleBonuses.any { it.isOfType(UniqueType.CityStateMilitaryUnits) }
|| (fallback && cityStateType == CityStateType.Militaristic)) { // Fallback for badly defined Eras.json
|| (fallback && cityStateType == CityStateType.Militaristic) // Fallback for badly defined Eras.json
) {
val possibleUnits = ruleset.units.values.filter { it.requiredTech != null
&& ruleset.eras[ruleset.technologies[it.requiredTech!!]!!.era()]!!.eraNumber > ruleset.eras[startingEra]!!.eraNumber // Not from the start era or before

View File

@ -22,6 +22,7 @@ class CivInfoStats(val civInfo: CivilizationInfo) {
val baseUnitCost = 0.5f
var freeUnits = 3
for (unique in civInfo.getMatchingUniquesByEnum(UniqueType.FreeUnits)) {
if (!unique.conditionalsApply(civInfo)) continue
freeUnits += unique.params[0].toInt()
}
@ -36,6 +37,7 @@ class CivInfoStats(val civInfo: CivilizationInfo) {
var numberOfUnitsToPayFor = max(0f, unitsToPayFor.count().toFloat() - freeUnits)
for (unique in civInfo.getMatchingUniquesByEnum(UniqueType.UnitMaintenanceDiscount)) {
if (!unique.conditionalsApply(civInfo)) continue
val numberOfUnitsWithDiscount = min(
numberOfUnitsToPayFor,
unitsToPayFor.count { it.matchesFilter(unique.params[1]) }.toFloat()
@ -127,7 +129,7 @@ class CivInfoStats(val civInfo: CivilizationInfo) {
if (!eraInfo.undefinedCityStateBonuses()) {
for (bonus in eraInfo.getCityStateBonuses(otherCiv.cityStateType, relationshipLevel)) {
if (bonus.isOfType(UniqueType.CityStateStatsPerTurn))
if (bonus.isOfType(UniqueType.CityStateStatsPerTurn) && bonus.conditionalsApply(otherCiv))
cityStateBonus.add(bonus.stats)
}
} else {
@ -320,6 +322,7 @@ class CivInfoStats(val civInfo: CivilizationInfo) {
if (!eraInfo.undefinedCityStateBonuses()) {
for (bonus in eraInfo.getCityStateBonuses(otherCiv.cityStateType, relationshipLevel)) {
if (!bonus.conditionalsApply(otherCiv)) continue
if (bonus.isOfType(UniqueType.CityStateHappiness)) {
if (statMap.containsKey("City-States"))
statMap["City-States"] =

View File

@ -578,31 +578,36 @@ class DiplomacyManager() {
if (!hasFlag(DiplomacyFlags.DeclarationOfFriendship))
revertToZero(DiplomaticModifiers.DeclarationOfFriendship, 1 / 2f) //decreases slowly and will revert to full if it is declared later
if (otherCiv().isCityState()) {
val eraInfo = civInfo.getEra()
if (!otherCiv().isCityState()) return
val eraInfo = civInfo.getEra()
if (relationshipLevel() < RelationshipLevel.Friend) {
if (hasFlag(DiplomacyFlags.ProvideMilitaryUnit)) removeFlag(DiplomacyFlags.ProvideMilitaryUnit)
} else {
val variance = listOf(-1, 0, 1).random()
if (eraInfo.undefinedCityStateBonuses() && otherCiv().cityStateType == CityStateType.Militaristic) {
// Deprecated, assume Civ V values for compatibility
if (!hasFlag(DiplomacyFlags.ProvideMilitaryUnit) && relationshipLevel() == RelationshipLevel.Friend)
setFlag(DiplomacyFlags.ProvideMilitaryUnit, 20 + variance)
if (relationshipLevel() < RelationshipLevel.Friend) {
if (hasFlag(DiplomacyFlags.ProvideMilitaryUnit))
removeFlag(DiplomacyFlags.ProvideMilitaryUnit)
return
}
val variance = listOf(-1, 0, 1).random()
if (eraInfo.undefinedCityStateBonuses() && otherCiv().cityStateType == CityStateType.Militaristic) {
// Deprecated, assume Civ V values for compatibility
if (!hasFlag(DiplomacyFlags.ProvideMilitaryUnit) && relationshipLevel() == RelationshipLevel.Friend)
setFlag(DiplomacyFlags.ProvideMilitaryUnit, 20 + variance)
if ((!hasFlag(DiplomacyFlags.ProvideMilitaryUnit) || getFlag(DiplomacyFlags.ProvideMilitaryUnit) > 17)
&& relationshipLevel() == RelationshipLevel.Ally)
setFlag(DiplomacyFlags.ProvideMilitaryUnit, 17 + variance)
}
if (eraInfo.undefinedCityStateBonuses()) return
if ((!hasFlag(DiplomacyFlags.ProvideMilitaryUnit) || getFlag(DiplomacyFlags.ProvideMilitaryUnit) > 17)
&& relationshipLevel() == RelationshipLevel.Ally)
setFlag(DiplomacyFlags.ProvideMilitaryUnit, 17 + variance)
}
if (eraInfo.undefinedCityStateBonuses()) return
for (bonus in eraInfo.getCityStateBonuses(otherCiv().cityStateType, relationshipLevel())) {
// Reset the countdown if it has ended, or if we have longer to go than the current maximum (can happen when going from friend to ally)
if (bonus.isOfType(UniqueType.CityStateMilitaryUnits) &&
(!hasFlag(DiplomacyFlags.ProvideMilitaryUnit) || getFlag(DiplomacyFlags.ProvideMilitaryUnit) > bonus.params[0].toInt()))
setFlag(DiplomacyFlags.ProvideMilitaryUnit, bonus.params[0].toInt() + variance)
}
for (bonus in eraInfo.getCityStateBonuses(otherCiv().cityStateType, relationshipLevel())) {
// Reset the countdown if it has ended, or if we have longer to go than the current maximum (can happen when going from friend to ally)
if (bonus.isOfType(UniqueType.CityStateMilitaryUnits) &&
(!hasFlag(DiplomacyFlags.ProvideMilitaryUnit) || getFlag(DiplomacyFlags.ProvideMilitaryUnit) > bonus.params[0].toInt())
) {
setFlag(DiplomacyFlags.ProvideMilitaryUnit, bonus.params[0].toInt() + variance)
}
}
}

View File

@ -1,5 +1,6 @@
package com.unciv.models.ruleset.unique
import com.unciv.logic.city.CityInfo
import com.unciv.models.stats.Stats
import com.unciv.models.translations.*
import com.unciv.logic.civilization.CivilizationInfo
@ -10,7 +11,7 @@ class Unique(val text:String) {
/** This is so the heavy regex-based parsing is only activated once per unique, instead of every time it's called
* - for instance, in the city screen, we call every tile unique for every tile, which can lead to ANRs */
val placeholderText = text.getPlaceholderText()
val params = text.getPlaceholderParameters()
val params = text.removeConditionals().getPlaceholderParameters()
val type = UniqueType.values().firstOrNull { it.placeholderText == placeholderText }
val stats: Stats by lazy {
@ -30,20 +31,22 @@ class Unique(val text:String) {
// This will require a lot of parameters to be passed (attacking unit, tile, defending unit, civInfo, cityInfo, ...)
// I'm open for better ideas, but this was the first thing that I could think of that would
// work in all cases.
fun conditionalsApply(civInfo: CivilizationInfo? = null): Boolean {
fun conditionalsApply(civInfo: CivilizationInfo? = null, city: CityInfo? = null): Boolean {
for (condition in conditionals) {
if (!conditionalApplies(condition, civInfo)) return false
if (!conditionalApplies(condition, civInfo, city)) return false
}
return true
}
private fun conditionalApplies(
condition: Unique,
civInfo: CivilizationInfo? = null
civInfo: CivilizationInfo? = null,
city: CityInfo? = null
): Boolean {
return when (condition.placeholderText) {
"when not at war" -> civInfo?.isAtWar() == false
"when at war" -> civInfo?.isAtWar() == true
"if this city has at least [] specialists" -> city != null && city.population.getNumberOfSpecialists() >= condition.params[0].toInt()
else -> false
}
}

View File

@ -13,21 +13,27 @@ enum class UniqueTarget{
}
enum class UniqueType(val text:String, val replacedBy: UniqueType? = null) {
Stats("[stats]"),
StatsPerCity("[stats] [cityFilter]"),
ConsumesResources("Consumes [amount] [resource]"),
ConsumesResources("Consumes [amount] [resource]"), // No conditional support as of yet
FreeUnits("[amount] units cost no maintenance"),
UnitMaintenanceDiscount("[amount]% maintenance costs for [mapUnitFilter] units"),
@Deprecated("As of 3.16.16", ReplaceWith("UnitMaintenanceDiscount"))
DecreasedUnitMaintenanceCostsByFilter("-[amount]% [mapUnitFilter] unit maintenance costs", UnitMaintenanceDiscount),
@Deprecated("As of 3.16.16")
DecreasedUnitMaintenanceCostsGlobally("-[amount]% unit upkeep costs", UnitMaintenanceDiscount),
StatBonusForNumberOfSpecialists("[stats] if this city has at least [amount] specialists"),
StatsPerCity("[stats] [cityFilter]"),
DecreasedUnitMaintenanceCostsByFilter("-[amount]% [mapUnitFilter] unit maintenance costs", UnitMaintenanceDiscount), // No conditional support
@Deprecated("As of 3.16.16", ReplaceWith("UnitMaintenanceDiscount"))
DecreasedUnitMaintenanceCostsGlobally("-[amount]% unit upkeep costs", UnitMaintenanceDiscount), // No conditional support
@Deprecated("As of 3.16.16", ReplaceWith("Stats <>"))
StatBonusForNumberOfSpecialists("[stats] if this city has at least [amount] specialists"), // No conditional support
CityStateStatsPerTurn("Provides [stats] per turn"), // Should not be Happiness!
CityStateStatsPerCity("Provides [stats] [cityFilter]"),
CityStateHappiness("Provides [amount] Happiness"),
CityStateMilitaryUnits("Provides military units every ≈[amount] turns"),
CityStateUniqueLuxury("Provides a unique luxury"),
CityStateMilitaryUnits("Provides military units every ≈[amount] turns"), // No conditional support as of yet
CityStateUniqueLuxury("Provides a unique luxury"), // No conditional support as of yet
;
/** For uniques that have "special" parameters that can accept multiple types, we can override them manually

View File

@ -116,7 +116,7 @@ class Translations : LinkedHashMap<String, TranslationEntry>(){
private fun createTranslations(language: String, languageTranslations: HashMap<String,String>) {
for (translation in languageTranslations) {
val hashKey = if (translation.key.contains('['))
val hashKey = if (translation.key.contains('[') && !translation.key.contains('<'))
translation.key.getPlaceholderText()
else translation.key
var entry = this[hashKey]
@ -212,7 +212,7 @@ class Translations : LinkedHashMap<String, TranslationEntry>(){
companion object {
// 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 = "<when at war> <when not at war>"
const val englishConditionalOrderingString = "<if this city has at least [amount] specialists> <when at war> <when not at war>"
const val conditionalUniqueOrderString = "ConditionalsPlacement"
const val shouldCapitalizeString = "StartWithCapitalLetter"
}
@ -366,17 +366,17 @@ fun String.tr(): String {
}
fun String.getPlaceholderText() = this
.replace(squareBraceRegex, "[]")
.removeConditionals()
.replace(squareBraceRegex, "[]")
fun String.equalsPlaceholderText(str:String): Boolean {
if (first() != str.first()) return false // for quick negative return 95% of the time
return this.getPlaceholderText() == str
}
fun String.hasPlaceholderParameters() = squareBraceRegex.containsMatchIn(this)
fun String.hasPlaceholderParameters() = squareBraceRegex.containsMatchIn(this.removeConditionals())
fun String.getPlaceholderParameters() = squareBraceRegex.findAll(this).map { it.groups[1]!!.value }.toList()
fun String.getPlaceholderParameters() = squareBraceRegex.findAll(this.removeConditionals()).map { it.groups[1]!!.value }.toList()
/** Substitutes placeholders with [strings], respecting order of appearance. */
fun String.fillPlaceholders(vararg strings: String): String {