mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-13 17:28:57 +07:00
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:
@ -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"))
|
||||
|
@ -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
|
||||
|
@ -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"] =
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user