From 2d5536e8be23ee86164ed47b8e7276f968996779 Mon Sep 17 00:00:00 2001 From: Yair Morgenstern Date: Sat, 19 Nov 2022 19:16:24 +0200 Subject: [PATCH] Allowed adding arbitrary global uniques to city state bonuses (#8028) * Allowed adding arbitrary global uniques to city state bonuses * Added conditional support for city-state uniques --- core/src/com/unciv/logic/GameInfo.kt | 7 +++ core/src/com/unciv/logic/city/CityStats.kt | 5 +- .../logic/civilization/CityStateFunctions.kt | 58 +++++++++---------- .../unciv/logic/civilization/CivInfoStats.kt | 17 +----- .../logic/civilization/CivilizationInfo.kt | 1 + .../diplomacy/DiplomacyManager.kt | 6 +- core/src/com/unciv/models/ruleset/Era.kt | 28 +++++---- .../src/com/unciv/ui/trade/DiplomacyScreen.kt | 2 +- 8 files changed, 58 insertions(+), 66 deletions(-) diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index 49c0f8d63a..8339f7f51a 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -491,6 +491,13 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion for (religion in religions.values) religion.setTransients(this) + // must be done before updating tech manager bools, since that depends on allied city-states + for (civInfo in civilizations) + for (diplomacyManager in civInfo.diplomacy.values) { + diplomacyManager.civInfo = civInfo + diplomacyManager.updateHasOpenBorders() + } + for (civInfo in civilizations) civInfo.setTransients() for (civInfo in civilizations) { civInfo.thingsToFocusOnForVictory = diff --git a/core/src/com/unciv/logic/city/CityStats.kt b/core/src/com/unciv/logic/city/CityStats.kt index afc86180e7..4ea2d94ddb 100644 --- a/core/src/com/unciv/logic/city/CityStats.kt +++ b/core/src/com/unciv/logic/city/CityStats.kt @@ -156,9 +156,8 @@ class CityStats(val cityInfo: CityInfo) { if (otherCiv.cityStateType == CityStateType.Maritime && cityInfo.isCapital()) stats.food += 2 } else { - for (bonus in eraInfo.getCityStateBonuses(otherCiv.cityStateType, relationshipLevel)) { - if (bonus.isOfType(UniqueType.CityStateStatsPerCity) - && cityInfo.matchesFilter(bonus.params[1]) + for (bonus in eraInfo.getCityStateBonuses(otherCiv.cityStateType, relationshipLevel, UniqueType.CityStateStatsPerCity)) { + if (cityInfo.matchesFilter(bonus.params[1]) && bonus.conditionalsApply(otherCiv, cityInfo) ) stats.add(bonus.stats) } diff --git a/core/src/com/unciv/logic/civilization/CityStateFunctions.kt b/core/src/com/unciv/logic/civilization/CityStateFunctions.kt index a69cfc79f8..e9da3f95fb 100644 --- a/core/src/com/unciv/logic/civilization/CityStateFunctions.kt +++ b/core/src/com/unciv/logic/civilization/CityStateFunctions.kt @@ -2,18 +2,19 @@ package com.unciv.logic.civilization import com.unciv.Constants import com.unciv.logic.automation.civilization.NextTurnAutomation -import com.unciv.logic.civilization.diplomacy.* +import com.unciv.logic.civilization.diplomacy.DiplomacyFlags +import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers +import com.unciv.logic.civilization.diplomacy.DiplomaticStatus +import com.unciv.logic.civilization.diplomacy.RelationshipLevel import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.tile.ResourceSupplyList +import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.stats.Stat import com.unciv.ui.victoryscreen.RankingType import java.util.* -import kotlin.collections.HashMap -import kotlin.collections.HashSet -import kotlin.collections.LinkedHashMap import kotlin.math.min import kotlin.math.pow @@ -22,40 +23,29 @@ class CityStateFunctions(val civInfo: CivilizationInfo) { /** Attempts to initialize the city state, returning true if successful. */ fun initCityState(ruleset: Ruleset, startingEra: String, unusedMajorCivs: Collection): Boolean { - val cityStateType = ruleset.nations[civInfo.civName]?.cityStateType - ?: return false - val startingTechs = ruleset.technologies.values.filter { it.hasUnique(UniqueType.StartingTech) } for (tech in startingTechs) civInfo.tech.techsResearched.add(tech.name) // can't be .addTechnology because the civInfo isn't assigned yet val allMercantileResources = ruleset.tileResources.values.filter { it.hasUnique(UniqueType.CityStateOnlyResource) }.map { it.name } - val allPossibleBonuses = HashSet() // We look through these to determine what kind of city state we are - var fallback = false + val uniqueTypes = HashSet() // We look through these to determine what kind of city state we are for (era in ruleset.eras.values) { - if (era.undefinedCityStateBonuses()) { - fallback = true - break - } - val allyBonuses = era.getCityStateBonuses(cityStateType, RelationshipLevel.Ally) - val friendBonuses = era.getCityStateBonuses(cityStateType, RelationshipLevel.Friend) - allPossibleBonuses.addAll(allyBonuses) - allPossibleBonuses.addAll(friendBonuses) + if (era.undefinedCityStateBonuses()) continue + for (unique in era.friendBonusObjects.values.map { it.getAllUniques() } + era.allyBonusObjects.values.map { it.getAllUniques() }) + uniqueTypes.addAll(unique.mapNotNull { it.type }) } // CS Personality civInfo.cityStatePersonality = CityStatePersonality.values().random() // Mercantile bonus resources - if (allPossibleBonuses.any { it.isOfType(UniqueType.CityStateUniqueLuxury) } - || fallback && cityStateType == CityStateType.Mercantile) { // Fallback for badly defined Eras.json + + if (uniqueTypes.contains(UniqueType.CityStateUniqueLuxury)) { // Fallback for badly defined Eras.json civInfo.cityStateResource = allMercantileResources.randomOrNull() } // Unique unit for militaristic city-states - if (allPossibleBonuses.any { it.isOfType(UniqueType.CityStateMilitaryUnits) } - || fallback && cityStateType == CityStateType.Militaristic // Fallback for badly defined Eras.json - ) { + if (uniqueTypes.contains(UniqueType.CityStateMilitaryUnits)) { 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 @@ -438,15 +428,6 @@ class CityStateFunctions(val civInfo: CivilizationInfo) { if (bonus.stats[statType] > 0 || (bonus.isOfType(UniqueType.CityStateHappiness) && statType == Stat.Happiness)) return true } - } else { - // compatibility mode - return when { - civInfo.cityStateType == CityStateType.Mercantile && statType == Stat.Happiness -> true - civInfo.cityStateType == CityStateType.Cultured && statType == Stat.Culture -> true - civInfo.cityStateType == CityStateType.Maritime && statType == Stat.Food -> true - civInfo.cityStateType == CityStateType.Religious && statType == Stat.Faith ->true - else -> false - } } return false @@ -668,4 +649,19 @@ class CityStateFunctions(val civInfo: CivilizationInfo) { addPositiveByResource(city.getCityResources(), Constants.cityStates) } } + + // TODO: Optimize, update whenever status changes, otherwise retain the same list + fun getUniquesProvidedByCityStates( + uniqueType: UniqueType, + stateForConditionals: StateForConditionals + ):Sequence { + if (civInfo.isCityState()) return emptySequence() + val era = civInfo.getEra() + + if (era.undefinedCityStateBonuses()) return emptySequence() + + return civInfo.getKnownCivs().asSequence().filter { it.isCityState() } + .flatMap { era.getCityStateBonuses(it.cityStateType, civInfo.getDiplomacyManager(it).relationshipLevel(), uniqueType) } + .filter { it.conditionalsApply(stateForConditionals) } + } } diff --git a/core/src/com/unciv/logic/civilization/CivInfoStats.kt b/core/src/com/unciv/logic/civilization/CivInfoStats.kt index fc26eff242..bb3d4e9163 100644 --- a/core/src/com/unciv/logic/civilization/CivInfoStats.kt +++ b/core/src/com/unciv/logic/civilization/CivInfoStats.kt @@ -147,21 +147,8 @@ class CivInfoStats(val civInfo: CivilizationInfo) { val eraInfo = civInfo.getEra() if (!eraInfo.undefinedCityStateBonuses()) { - for (bonus in eraInfo.getCityStateBonuses(otherCiv.cityStateType, relationshipLevel)) { - if (bonus.isOfType(UniqueType.CityStateStatsPerTurn) && bonus.conditionalsApply(otherCiv)) - cityStateBonus.add(bonus.stats) - } - } else { - // Deprecated, assume Civ V values for compatibility - if (otherCiv.cityStateType == CityStateType.Cultured) { - cityStateBonus.culture = - when { - civInfo.getEraNumber() in 0..1 -> 3f - civInfo.getEraNumber() in 2..3 -> 6f - else -> 13f - } - if (relationshipLevel == RelationshipLevel.Ally) - cityStateBonus.culture *= 2f + for (bonus in eraInfo.getCityStateBonuses(otherCiv.cityStateType, relationshipLevel, UniqueType.CityStateStatsPerTurn)) { + cityStateBonus.add(bonus.stats) } } diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index 0811ed5e28..7a807b5fdc 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -463,6 +463,7 @@ class CivilizationInfo : IsPartOfGameInfoSerialization { .filter { it.isOfType(uniqueType) && it.conditionalsApply(stateForConditionals) } ) yieldAll(getEra().getMatchingUniques(uniqueType, stateForConditionals)) + yieldAll(cityStateFunctions.getUniquesProvidedByCityStates(uniqueType, stateForConditionals)) if (religionManager.religion != null) yieldAll(religionManager.religion!!.getFounderUniques() .filter { it.isOfType(uniqueType) && it.conditionalsApply(stateForConditionals) }) diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt index bc0a007603..1be5c61e6e 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt @@ -646,11 +646,9 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization { if (eraInfo.undefinedCityStateBonuses()) return - for (bonus in eraInfo.getCityStateBonuses(otherCiv().cityStateType, relationshipLevel())) { + for (bonus in eraInfo.getCityStateBonuses(otherCiv().cityStateType, relationshipLevel(), UniqueType.CityStateMilitaryUnits)) { // 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()) - ) { + if (!hasFlag(DiplomacyFlags.ProvideMilitaryUnit) || getFlag(DiplomacyFlags.ProvideMilitaryUnit) > bonus.params[0].toInt()) { setFlag(DiplomacyFlags.ProvideMilitaryUnit, bonus.params[0].toInt() + variance) } } diff --git a/core/src/com/unciv/models/ruleset/Era.kt b/core/src/com/unciv/models/ruleset/Era.kt index 47f9736e89..44c339c8ae 100644 --- a/core/src/com/unciv/models/ruleset/Era.kt +++ b/core/src/com/unciv/models/ruleset/Era.kt @@ -3,9 +3,9 @@ package com.unciv.models.ruleset import com.badlogic.gdx.graphics.Color import com.unciv.logic.civilization.CityStateType import com.unciv.logic.civilization.diplomacy.RelationshipLevel -import com.unciv.models.ruleset.unique.IHasUniques import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.Unique +import com.unciv.models.ruleset.unique.UniqueMap import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueType import com.unciv.ui.civilopedia.FormattedLine @@ -34,9 +34,9 @@ class Era : RulesetObject() { var friendBonus = HashMap>() var allyBonus = HashMap>() @Suppress("MemberVisibilityCanBePrivate") - val friendBonusObjects: Map> by lazy { initBonuses(friendBonus) } + val friendBonusObjects: Map by lazy { initBonuses(friendBonus) } @Suppress("MemberVisibilityCanBePrivate") - val allyBonusObjects: Map> by lazy { initBonuses(allyBonus) } + val allyBonusObjects: Map by lazy { initBonuses(allyBonus) } private var iconRGB: List? = null @@ -87,20 +87,24 @@ class Era : RulesetObject() { }.map { it.first }.distinct() } - private fun initBonuses(bonusMap: Map>): Map> { - val objectMap = HashMap>() + private fun initBonuses(bonusMap: Map>): Map { + val objectMap = HashMap() for ((cityStateType, bonusList) in bonusMap) { - objectMap[CityStateType.valueOf(cityStateType)] = bonusList.map { Unique(it, UniqueTarget.CityState) } + val cityStateTypeUniqueMap = UniqueMap() + cityStateTypeUniqueMap.addUniques(bonusList.map { Unique(it, UniqueTarget.CityState)}) + objectMap[CityStateType.valueOf(cityStateType)] = cityStateTypeUniqueMap } return objectMap } - fun getCityStateBonuses(cityStateType: CityStateType, relationshipLevel: RelationshipLevel): List { - return when (relationshipLevel) { - RelationshipLevel.Ally -> allyBonusObjects[cityStateType] ?: emptyList() - RelationshipLevel.Friend -> friendBonusObjects[cityStateType] ?: emptyList() - else -> emptyList() - } + fun getCityStateBonuses(cityStateType: CityStateType, relationshipLevel: RelationshipLevel, uniqueType:UniqueType?=null): Sequence { + val cityStateUniqueMap = when (relationshipLevel) { + RelationshipLevel.Ally -> allyBonusObjects[cityStateType] + RelationshipLevel.Friend -> friendBonusObjects[cityStateType] + else -> null + } ?: return emptySequence() + return if (uniqueType == null) cityStateUniqueMap.getAllUniques() + else cityStateUniqueMap.getUniques(uniqueType) } /** Since 3.19.5 we have a warning for mods without bonuses, eventually we should treat such mods as providing no bonus */ diff --git a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt index b1b717cac2..9fcd8342cf 100644 --- a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt +++ b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt @@ -288,7 +288,7 @@ class DiplomacyScreen( /** Given a list of [bonuses], returns a list of pretty strings with updated values for Siam-like uniques * Assumes that each bonus contains only one stat type */ - private fun getAdjustedBonuses(bonuses: List): List { + private fun getAdjustedBonuses(bonuses: Sequence): List { val bonusStrings = ArrayList() for (bonus in bonuses) { var improved = false