diff --git a/android/assets/jsons/Civ V - Vanilla/Eras.json b/android/assets/jsons/Civ V - Vanilla/Eras.json index c06358bb56..39bd3e9488 100644 --- a/android/assets/jsons/Civ V - Vanilla/Eras.json +++ b/android/assets/jsons/Civ V - Vanilla/Eras.json @@ -16,13 +16,13 @@ "Cultured": ["Provides [3] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]"], "Mercantile": ["Provides [2] Happiness"], - "Militaristic": ["Provides military units every [20] turns"] + "Militaristic": ["Provides military units every ≈[20] turns"] }, "allyBonus": { "Cultured": ["Provides [6] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]", "Provides [1] [Food] [in all cities]"], "Mercantile": ["Provides [2] Happiness", "Provides a unique luxury"], - "Militaristic": ["Provides military units every [17] turns"] + "Militaristic": ["Provides military units every ≈[17] turns"] }, "iconRGB": [255, 87, 35] }, @@ -42,13 +42,13 @@ "Cultured": ["Provides [3] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]"], "Mercantile": ["Provides [2] Happiness"], - "Militaristic": ["Provides military units every [20] turns"] + "Militaristic": ["Provides military units every ≈[20] turns"] }, "allyBonus": { "Cultured": ["Provides [6] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]", "Provides [1] [Food] [in all cities]"], "Mercantile": ["Provides [2] Happiness", "Provides a unique luxury"], - "Militaristic": ["Provides military units every [17] turns"] + "Militaristic": ["Provides military units every ≈[17] turns"] }, "iconRGB": [233, 31, 99] }, @@ -70,13 +70,13 @@ "Cultured": ["Provides [6] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]"], "Mercantile": ["Provides [3] Happiness"], - "Militaristic": ["Provides military units every [20] turns"] + "Militaristic": ["Provides military units every ≈[20] turns"] }, "allyBonus": { "Cultured": ["Provides [12] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]", "Provides [1] [Food] [in all cities]"], "Mercantile": ["Provides [3] Happiness", "Provides a unique luxury"], - "Militaristic": ["Provides military units every [17] turns"] + "Militaristic": ["Provides military units every ≈[17] turns"] }, "iconRGB": [157, 39, 176] }, @@ -99,13 +99,13 @@ "Cultured": ["Provides [6] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]"], "Mercantile": ["Provides [3] Happiness"], - "Militaristic": ["Provides military units every [20] turns"] + "Militaristic": ["Provides military units every ≈[20] turns"] }, "allyBonus": { "Cultured": ["Provides [12] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]", "Provides [1] [Food] [in all cities]"], "Mercantile": ["Provides [3] Happiness", "Provides a unique luxury"], - "Militaristic": ["Provides military units every [17] turns"] + "Militaristic": ["Provides military units every ≈[17] turns"] }, "iconRGB": [104, 58, 183] }, @@ -129,13 +129,13 @@ "Cultured": ["Provides [13] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]"], "Mercantile": ["Provides [3] Happiness"], - "Militaristic": ["Provides military units every [20] turns"] + "Militaristic": ["Provides military units every ≈[20] turns"] }, "allyBonus": { "Cultured": ["Provides [26] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]", "Provides [1] [Food] [in all cities]"], "Mercantile": ["Provides [3] Happiness", "Provides a unique luxury"], - "Militaristic": ["Provides military units every [17] turns"] + "Militaristic": ["Provides military units every ≈[17] turns"] }, "iconRGB": [63, 81, 182], "uniques": ["May not generate great prophet equivalents naturally", @@ -164,13 +164,13 @@ "Cultured": ["Provides [13] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]"], "Mercantile": ["Provides [3] Happiness"], - "Militaristic": ["Provides military units every [20] turns"] + "Militaristic": ["Provides military units every ≈[20] turns"] }, "allyBonus": { "Cultured": ["Provides [26] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]", "Provides [1] [Food] [in all cities]"], "Mercantile": ["Provides [3] Happiness", "Provides a unique luxury"], - "Militaristic": ["Provides military units every [17] turns"] + "Militaristic": ["Provides military units every ≈[17] turns"] }, "iconRGB": [33, 150, 243], "uniques": ["May not generate great prophet equivalents naturally", @@ -200,13 +200,13 @@ "Cultured": ["Provides [13] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]"], "Mercantile": ["Provides [3] Happiness"], - "Militaristic": ["Provides military units every [20] turns"] + "Militaristic": ["Provides military units every ≈[20] turns"] }, "allyBonus": { "Cultured": ["Provides [26] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]", "Provides [1] [Food] [in all cities]"], "Mercantile": ["Provides [3] Happiness", "Provides a unique luxury"], - "Militaristic": ["Provides military units every [17] turns"] + "Militaristic": ["Provides military units every ≈[17] turns"] }, "iconRGB": [0, 150, 136], "uniques": ["May not generate great prophet equivalents naturally", @@ -240,13 +240,13 @@ "Cultured": ["Provides [13] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]"], "Mercantile": ["Provides [3] Happiness"], - "Militaristic": ["Provides military units every [20] turns"] + "Militaristic": ["Provides military units every ≈[20] turns"] }, "allyBonus": { "Cultured": ["Provides [26] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]", "Provides [1] [Food] [in all cities]"], "Mercantile": ["Provides [3] Happiness", "Provides a unique luxury"], - "Militaristic": ["Provides military units every [17] turns"] + "Militaristic": ["Provides military units every ≈[17] turns"] }, "iconRGB": [76, 176, 81], "uniques": ["May not generate great prophet equivalents naturally", @@ -279,13 +279,13 @@ "Cultured": ["Provides [13] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]"], "Mercantile": ["Provides [3] Happiness"], - "Militaristic": ["Provides military units every [20] turns"] + "Militaristic": ["Provides military units every ≈[20] turns"] }, "allyBonus": { "Cultured": ["Provides [26] [Culture] per turn"], "Maritime": ["Provides [2] [Food] [in capital]", "Provides [1] [Food] [in all cities]"], "Mercantile": ["Provides [3] Happiness", "Provides a unique luxury"], - "Militaristic": ["Provides military units every [17] turns"] + "Militaristic": ["Provides military units every ≈[17] turns"] }, "iconRGB": [76, 176, 81], "uniques": ["May not generate great prophet equivalents naturally", diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 22523d7aff..ac10799c9f 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -163,6 +163,7 @@ Pledge to protect = Declare Protection of [cityStateName]? = Build [improvementName] on [resourceName] (200 Gold) = Gift Improvement = +[civName] is able to provide [unitName] once [techName] is researched. = Diplomatic Marriage ([amount] Gold) = We have married into the ruling family of [civName], bringing them under our control. = diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index c71133ba0a..061983c4c6 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -193,23 +193,17 @@ object GameStarter { // and then all the other City-States in a random order! Because the sortedBy function is stable! availableCityStatesNames.addAll(ruleset.nations.filter { it.value.isCityState() }.keys .shuffled().sortedByDescending { it in civNamesWithStartingLocations }) - - val allMercantileResources = ruleset.tileResources.values.filter { it.unique == "Can only be created by Mercantile City-States" }.map { it.name } - val unusedMercantileResources = Stack() - unusedMercantileResources.addAll(allMercantileResources.shuffled()) - - for (cityStateName in availableCityStatesNames.take(newGameParameters.numberOfCityStates)) { + var addedCityStates = 0 + // Keep trying to add city states until we reach the target number. + while (addedCityStates < newGameParameters.numberOfCityStates) { + if (availableCityStatesNames.isEmpty()) // We ran out of city-states somehow + break + val cityStateName = availableCityStatesNames.pop() val civ = CivilizationInfo(cityStateName) - civ.cityStatePersonality = CityStatePersonality.values().random() - civ.cityStateResource = when { - ruleset.nations[cityStateName]?.cityStateType != CityStateType.Mercantile -> null - allMercantileResources.isEmpty() -> null - unusedMercantileResources.empty() -> allMercantileResources.random() // When unused luxuries exhausted, random - else -> unusedMercantileResources.pop() // First pick an unused luxury if possible + if (civ.initCityState(ruleset, newGameParameters.startingEra, availableCivNames)) { // true if successful init + gameInfo.civilizations.add(civ) + addedCityStates++ } - gameInfo.civilizations.add(civ) - for (tech in startingTechs) - civ.tech.techsResearched.add(tech.name) // can't be .addTechnology because the civInfo isn't assigned yet } } diff --git a/core/src/com/unciv/logic/civilization/CityStateFunctions.kt b/core/src/com/unciv/logic/civilization/CityStateFunctions.kt index 21bfd33f42..20c0d87132 100644 --- a/core/src/com/unciv/logic/civilization/CityStateFunctions.kt +++ b/core/src/com/unciv/logic/civilization/CityStateFunctions.kt @@ -6,6 +6,7 @@ import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomaticStatus import com.unciv.logic.civilization.diplomacy.RelationshipLevel import com.unciv.models.metadata.GameSpeed +import com.unciv.models.ruleset.Ruleset import com.unciv.models.stats.Stat import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderText @@ -18,6 +19,51 @@ 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): Boolean { + val cityStateType = ruleset.nations[civInfo.civName]?.cityStateType + if (cityStateType == null) return false + + val startingTechs = ruleset.technologies.values.filter { it.uniques.contains("Starting tech") } + 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.unique == "Can only be created by Mercantile City-States" }.map { it.name } + val allPossibleBonuses = HashSet() // We look through these to determine what kind of city state we are + for (era in ruleset.eras.values) { + val allyBonuses = era.allyBonus[cityStateType.name] + val friendBonuses = era.friendBonus[cityStateType.name] + if (allyBonuses != null) + allPossibleBonuses.addAll(allyBonuses) + if (friendBonuses != null) + allPossibleBonuses.addAll(friendBonuses) + } + + // CS Personality + civInfo.cityStatePersonality = CityStatePersonality.values().random() + + // Mercantile bonus resources + if ("Provides a unique luxury" in allPossibleBonuses + || (allPossibleBonuses.isEmpty() && cityStateType == CityStateType.Mercantile)) { // Fallback for badly defined Eras.json + civInfo.cityStateResource = allMercantileResources.random() + } + + // Unique unit for militaristic city-states + if (allPossibleBonuses.any { it.getPlaceholderText() == "Provides military units every ≈[] turns" } + || (allPossibleBonuses.isEmpty() && 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 + && it.uniqueTo != null && it.uniqueTo in unusedMajorCivs // Must be from a major civ not in the game + && ruleset.unitTypes[it.unitType]!!.isLandUnit() && ( it.strength > 0 || it.rangedStrength > 0 ) } // Must be a land military unit + if (possibleUnits.isNotEmpty()) + civInfo.cityStateUniqueUnit = possibleUnits.random().name + } + + // TODO: Return false if attempting to put a religious city-state in a game without religion + + return true + } /** Gain a random great person from the city state */ fun giveGreatPersonToPatron(receivingCiv: CivilizationInfo) { @@ -37,7 +83,12 @@ class CityStateFunctions(val civInfo: CivilizationInfo) { fun giveMilitaryUnitToPatron(receivingCiv: CivilizationInfo) { val cities = NextTurnAutomation.getClosestCities(receivingCiv, civInfo) val city = cities.city1 - val militaryUnit = city.cityConstructions.getConstructableUnits() + val uniqueUnit = civInfo.gameInfo.ruleSet.units[civInfo.cityStateUniqueUnit] + // If the receiving civ has discovered the required tech and not the obsolete tech for our unique, always give them the unique + val militaryUnit = if (uniqueUnit != null && receivingCiv.tech.isResearched(uniqueUnit.requiredTech!!) + && (uniqueUnit.obsoleteTech == null || !receivingCiv.tech.isResearched(uniqueUnit.obsoleteTech!!))) uniqueUnit + // Otherwise pick at random + else city.cityConstructions.getConstructableUnits() .filter { !it.isCivilian() && it.isLandUnit() && it.uniqueTo==null } .toList().random() // placing the unit may fail - in that case stay quiet diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index 7bc229513f..843077094a 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -174,6 +174,7 @@ class CivilizationInfo { toReturn.naturalWonders.addAll(naturalWonders) toReturn.cityStatePersonality = cityStatePersonality toReturn.cityStateResource = cityStateResource + toReturn.cityStateUniqueUnit = cityStateUniqueUnit toReturn.flagsCountdown.putAll(flagsCountdown) toReturn.temporaryUniques.addAll(temporaryUniques) toReturn.boughtConstructionsWithGloballyIncreasingPrice.putAll(boughtConstructionsWithGloballyIncreasingPrice) @@ -208,6 +209,7 @@ class CivilizationInfo { val cityStateType: CityStateType get() = nation.cityStateType!! var cityStatePersonality: CityStatePersonality = CityStatePersonality.Neutral var cityStateResource: String? = null + var cityStateUniqueUnit: String? = null // Unique unit for militaristic city state. Might still be null if there are no appropriate units fun isMajorCiv() = nation.isMajorCiv() fun isAlive(): Boolean = !isDefeated() fun hasEverBeenFriendWith(otherCiv: CivilizationInfo): Boolean = getDiplomacyManager(otherCiv).everBeenFriends() @@ -894,6 +896,9 @@ class CivilizationInfo { } //////////////////////// City State wrapper functions //////////////////////// + + fun initCityState(ruleset: Ruleset, startingEra: String, unusedMajorCivs: Collection) + = cityStateFunctions.initCityState(ruleset, startingEra, unusedMajorCivs) /** Gain a random great person from the city state */ private fun giveGreatPersonToPatron(receivingCiv: CivilizationInfo) { cityStateFunctions.giveGreatPersonToPatron(receivingCiv) diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt index cfb2ae9561..d354137863 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt @@ -541,6 +541,8 @@ class DiplomacyManager() { if (relationshipLevel() < RelationshipLevel.Friend) { if (hasFlag(DiplomacyFlags.ProvideMilitaryUnit)) removeFlag(DiplomacyFlags.ProvideMilitaryUnit) } else { + val variance = listOf(-1, 0, 1).random() + val relevantBonuses = if (relationshipLevel() == RelationshipLevel.Friend) eraInfo.friendBonus[otherCiv().cityStateType.name] @@ -549,19 +551,19 @@ class DiplomacyManager() { if (relevantBonuses == null && otherCiv().cityStateType == CityStateType.Militaristic) { // Deprecated, assume Civ V values for compatibility if (!hasFlag(DiplomacyFlags.ProvideMilitaryUnit) && relationshipLevel() == RelationshipLevel.Friend) - setFlag(DiplomacyFlags.ProvideMilitaryUnit, 20) + setFlag(DiplomacyFlags.ProvideMilitaryUnit, 20 + variance) if ((!hasFlag(DiplomacyFlags.ProvideMilitaryUnit) || getFlag(DiplomacyFlags.ProvideMilitaryUnit) > 17) && relationshipLevel() == RelationshipLevel.Ally) - setFlag(DiplomacyFlags.ProvideMilitaryUnit, 17) + setFlag(DiplomacyFlags.ProvideMilitaryUnit, 17 + variance) } if (relevantBonuses == null) return for (bonus in relevantBonuses) { // 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.getPlaceholderText() == "Provides military units every [] turns" && + if (bonus.getPlaceholderText() == "Provides military units every ≈[] turns" && (!hasFlag(DiplomacyFlags.ProvideMilitaryUnit) || getFlag(DiplomacyFlags.ProvideMilitaryUnit) > bonus.getPlaceholderParameters()[0].toInt())) - setFlag(DiplomacyFlags.ProvideMilitaryUnit, bonus.getPlaceholderParameters()[0].toInt()) + setFlag(DiplomacyFlags.ProvideMilitaryUnit, bonus.getPlaceholderParameters()[0].toInt() + variance) } } } diff --git a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt index d308d684f0..a1ef52b2c1 100644 --- a/core/src/com/unciv/ui/trade/DiplomacyScreen.kt +++ b/core/src/com/unciv/ui/trade/DiplomacyScreen.kt @@ -132,9 +132,9 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { val otherCivDiplomacyManager = otherCiv.getDiplomacyManager(viewingCiv) val diplomacyTable = Table() - diplomacyTable.defaults().pad(10f) + diplomacyTable.defaults().pad(2.5f) - diplomacyTable.add(LeaderIntroTable(otherCiv)).row() + diplomacyTable.add(LeaderIntroTable(otherCiv)).padBottom(15f).row() diplomacyTable.add("{Type}: {${otherCiv.cityStateType}}".toLabel()).row() diplomacyTable.add("{Personality}: {${otherCiv.cityStatePersonality}}".toLabel()).row() @@ -162,6 +162,7 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { } diplomacyTable.add(resourcesTable).row() } + diplomacyTable.row().padTop(15f) otherCiv.updateAllyCivForCityState() val ally = otherCiv.getAllyCiv() @@ -186,6 +187,7 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { if (nextLevelString.isNotEmpty()) { diplomacyTable.add(nextLevelString.toLabel()).row() } + diplomacyTable.row().padTop(15f) val eraInfo = viewingCiv.getEra() @@ -215,6 +217,12 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() { .apply { setAlignment(Align.center) } diplomacyTable.add(allyBonusLabel).row() + if (otherCiv.cityStateUniqueUnit != null) { + val unitName = otherCiv.cityStateUniqueUnit + val techName = viewingCiv.gameInfo.ruleSet.units[otherCiv.cityStateUniqueUnit]!!.requiredTech + diplomacyTable.add("[${otherCiv.civName}] is able to provide [${unitName}] once [${techName}] is researched.".toLabel(fontSize = 18)).row() + } + return diplomacyTable }