From 10686d1d8feefd342272ddf6a165385901d3c403 Mon Sep 17 00:00:00 2001 From: SimonCeder <63475501+SimonCeder@users.noreply.github.com> Date: Sat, 27 Nov 2021 18:59:19 +0100 Subject: [PATCH] We Love The King Day (#5705) * we love the king day * AI improvements * Don't break old saves * unused import * tutorial * proper growth when unhappy * reviews --- android/assets/jsons/Tutorials.json | 6 + .../jsons/translations/template.properties | 6 + .../logic/automation/NextTurnAutomation.kt | 2 +- core/src/com/unciv/logic/city/CityInfo.kt | 113 ++++++++++++++++-- .../logic/city/CityInfoConquestFunctions.kt | 5 +- core/src/com/unciv/logic/city/CityStats.kt | 40 ++++--- core/src/com/unciv/logic/map/TileMap.kt | 3 + .../com/unciv/logic/trade/TradeEvaluation.kt | 15 +-- core/src/com/unciv/models/Tutorial.kt | 1 + .../com/unciv/ui/cityscreen/CityStatsTable.kt | 7 +- .../com/unciv/ui/worldscreen/WorldScreen.kt | 1 + 11 files changed, 159 insertions(+), 40 deletions(-) diff --git a/android/assets/jsons/Tutorials.json b/android/assets/jsons/Tutorials.json index af6f665bd3..c567bce266 100644 --- a/android/assets/jsons/Tutorials.json +++ b/android/assets/jsons/Tutorials.json @@ -228,5 +228,11 @@ "", "The Maya measured time in days from what we would call 11th of August, 3114 BCE. A day is called K'in, 20 days are a Winal, 18 Winals are a Tun, 20 Tuns are a K'atun, 20 K'atuns are a B'ak'tun, 20 B'ak'tuns a Piktun, and so on.", "Unciv only displays ය B'ak'tuns, ඹ K'atuns and ම Tuns (from left to right) since that is enough to approximate gregorian calendar years. The Maya numerals are pretty obvious to understand. Have fun deciphering them!" + ], + "We_Love_The_King_Day": [ + "Your cities will periodically demand different luxury goods to satisfy their desire for new things in life.", + "If you manage to acquire the demanded luxury by trade, expansion, or conquest, the city will celebrate We Love The King Day for 20 turns.", + "During the We Love The King Day, the city will grow 25% faster.", + "This means exploration and trade is important to grow your cities!" ] } diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index bba0c57ff7..a6493d391d 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -638,6 +638,9 @@ Clearing a [forest] has created [amount] Production for [cityName] = [civName] no longer needs your help with the [questName] quest. = The [questName] quest for [civName] has ended. It was won by [civNames]. = The resistance in [cityName] has ended! = +[cityName] demands [resource]! = +Because they have [resource], the citizens of [cityName] are celebrating We Love The King Day! = +We Love The King Day in [cityName] has ended. = Our [name] took [tileDamage] tile damage and was destroyed = Our [name] took [tileDamage] tile damage = [civName] has adopted the [policyName] policy = @@ -722,6 +725,7 @@ Territory = Force = GOLDEN AGE = Golden Age = +We Love The King Day = [year] BC = [year] AD = Civilopedia = @@ -786,6 +790,8 @@ Food converts to production = [turnsToStarvation] turns to lose population = Stopped population growth = In resistance for another [numberOfTurns] turns = +We Love The King Day for another [numberOfTurns] turns = +Demanding [resource] = Sell for [sellAmount] gold = Are you sure you want to sell this [building]? = Free = diff --git a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/NextTurnAutomation.kt index 473ecac629..4d331739da 100644 --- a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/NextTurnAutomation.kt @@ -522,7 +522,7 @@ object NextTurnAutomation { .filter { resource -> tradeLogic.ourAvailableOffers .none { it.name == resource.name && it.type == TradeType.Luxury_Resource } - } + }.sortedBy { civInfo.cities.count { city -> city.demandedResource == it.name } } // Prioritize resources that get WLTKD val trades = ArrayList() for (i in 0..min(weHaveTheyDont.lastIndex, theyHaveWeDont.lastIndex)) { val trade = Trade() diff --git a/core/src/com/unciv/logic/city/CityInfo.kt b/core/src/com/unciv/logic/city/CityInfo.kt index 2ba67c1ac6..0409e0fb69 100644 --- a/core/src/com/unciv/logic/city/CityInfo.kt +++ b/core/src/com/unciv/logic/city/CityInfo.kt @@ -3,6 +3,7 @@ package com.unciv.logic.city import com.badlogic.gdx.math.Vector2 import com.unciv.logic.battle.CityCombatant import com.unciv.logic.civilization.CivilizationInfo +import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.civilization.Proximity import com.unciv.logic.civilization.ReligionState import com.unciv.logic.civilization.diplomacy.DiplomacyFlags @@ -25,6 +26,12 @@ import kotlin.math.min import kotlin.math.pow import kotlin.math.roundToInt +enum class CityFlags { + WeLoveTheKing, + ResourceDemand, + Resistance +} + class CityInfo { @Suppress("JoinDeclarationAndAssignment") @Transient @@ -54,6 +61,8 @@ class CityInfo { var previousOwner = "" var turnAcquired = 0 var health = 200 + + @Deprecated("As of 3.18.4", ReplaceWith("CityFlags.Resistance"), DeprecationLevel.WARNING) var resistanceCounter = 0 var religion = CityInfoReligionManager() @@ -82,6 +91,11 @@ class CityInfo { * It is important to distinguish them since the original cannot be razed and defines the Domination Victory. */ var isOriginalCapital = false + /** For We Love the King Day */ + var demandedResource = "" + + private var flagsCountdown = HashMap() + constructor() // for json parsing, we need to have a default constructor constructor(civInfo: CivilizationInfo, cityLocation: Vector2) { // new city! this.civInfo = civInfo @@ -233,11 +247,12 @@ class CityInfo { toReturn.lockedTiles = lockedTiles toReturn.isBeingRazed = isBeingRazed toReturn.attackedThisTurn = attackedThisTurn - toReturn.resistanceCounter = resistanceCounter toReturn.foundingCiv = foundingCiv toReturn.turnAcquired = turnAcquired toReturn.isPuppet = isPuppet toReturn.isOriginalCapital = isOriginalCapital + toReturn.flagsCountdown.putAll(flagsCountdown) + toReturn.demandedResource = demandedResource return toReturn } @@ -264,7 +279,11 @@ class CityInfo { } - fun isInResistance() = resistanceCounter > 0 + fun hasFlag(flag: CityFlags) = flagsCountdown.containsKey(flag.name) + fun getFlag(flag: CityFlags) = flagsCountdown[flag.name]!! + + fun isWeLoveTheKingDay() = hasFlag(CityFlags.WeLoveTheKing) + fun isInResistance() = hasFlag(CityFlags.Resistance) /** @return the number of tiles 4 out from this city that could hold a city, ie how lonely this city is */ fun getFrontierScore() = getCenterTile().getTilesAtDistance(4).count { it.canBeSettled() && (it.getOwner() == null || it.getOwner() == civInfo ) } @@ -477,6 +496,11 @@ class CityInfo { cityConstructions.cityInfo = this cityConstructions.setTransients() religion.setTransients(this) + + if (resistanceCounter > 0) { + setFlag(CityFlags.Resistance, resistanceCounter) + resistanceCounter = 0 + } } fun startTurn() { @@ -488,17 +512,52 @@ class CityInfo { cityStats.update() tryUpdateRoadStatus() attackedThisTurn = false - if (isInResistance()) { - resistanceCounter-- - if (!isInResistance()) - civInfo.addNotification( - "The resistance in [$name] has ended!", - location, - "StatIcons/Resistance" - ) - } if (isPuppet) reassignPopulation() + + // The ordering is intentional - you get a turn without WLTKD even if you have the next resource already + if (!hasFlag(CityFlags.WeLoveTheKing)) + tryWeLoveTheKing() + nextTurnFlags() + + // Seed resource demand countdown + if(demandedResource == "" && !hasFlag(CityFlags.ResourceDemand)) { + setFlag(CityFlags.ResourceDemand, + (if (isCapital()) 25 else 15) + Random().nextInt(10)) + } + } + + // cf DiplomacyManager nextTurnFlags + private fun nextTurnFlags() { + for (flag in flagsCountdown.keys.toList()) { + if (flagsCountdown[flag]!! > 0) + flagsCountdown[flag] = flagsCountdown[flag]!! - 1 + + if (flagsCountdown[flag] == 0) { + flagsCountdown.remove(flag) + + when (flag) { + CityFlags.ResourceDemand.name -> { + demandNewResource() + } + CityFlags.WeLoveTheKing.name -> { + civInfo.addNotification( + "We Love The King Day in [$name] has ended.", + location, NotificationIcon.City) + demandNewResource() + } + CityFlags.Resistance.name -> { + civInfo.addNotification( + "The resistance in [$name] has ended!", + location,"StatIcons/Resistance") + } + } + } + } + } + + fun setFlag(flag: CityFlags, amount: Int) { + flagsCountdown[flag.name] = amount } fun reassignPopulation() { @@ -624,6 +683,38 @@ class CityInfo { civInfo.updateDetailedCivResources() // this building could be a resource-requiring one } + private fun demandNewResource() { + val candidates = getRuleset().tileResources.values.filter { + it.resourceType == ResourceType.Luxury && // Must be luxury + !it.hasUnique(UniqueType.CityStateOnlyResource) && // Not a city-state only resource eg jewelry + it.name != demandedResource && // Not same as last time + !civInfo.hasResource(it.name) && // Not one we already have + it.name in tileMap.resources && // Must exist somewhere on the map + getCenterTile().getTilesInDistance(3).none { nearTile -> nearTile.resource == it.name } // Not in this city's radius + } + + val chosenResource = candidates.randomOrNull() + /* What if we had a WLTKD before but now the player has every resource in the game? We can't + pick a new resource, so the resource will stay stay the same and the city will demand it + again even if the player still has it. But we shouldn't punish success. */ + if (chosenResource != null) + demandedResource = chosenResource.name + if (demandedResource == "") // Failed to get a valid resource, try again some time later + setFlag(CityFlags.ResourceDemand, 15 + Random().nextInt(10)) + else + civInfo.addNotification("[$name] demands [$demandedResource]!", location, NotificationIcon.City, "ResourceIcons/$demandedResource") + } + + private fun tryWeLoveTheKing() { + if (demandedResource == "") return + if (civInfo.getCivResourcesByName()[demandedResource]!! > 0) { + setFlag(CityFlags.WeLoveTheKing, 20 + 1) // +1 because it will be decremented by 1 in the same startTurn() + civInfo.addNotification( + "Because they have [$demandedResource], the citizens of [$name] are celebrating We Love The King Day!", + location, NotificationIcon.City, NotificationIcon.Happiness) + } + } + /* When someone settles a city within 6 tiles of another civ, this makes the AI unhappy and it starts a rolling event. The SettledCitiesNearUs flag gets added to the AI so it knows this happened, diff --git a/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt b/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt index d15a17f173..16c5dd816c 100644 --- a/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt +++ b/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt @@ -86,7 +86,7 @@ class CityInfoConquestFunctions(val city: CityInfo){ conqueringCiv.addGold(goldPlundered) conqueringCiv.addNotification("Received [$goldPlundered] Gold for capturing [$name]", getCenterTile().position, NotificationIcon.Gold) - val reconqueredCityWhileStillInResistance = previousOwner == conqueringCiv.civName && resistanceCounter != 0 + val reconqueredCityWhileStillInResistance = previousOwner == conqueringCiv.civName && isInResistance() destroyBuildingsOnCapture() @@ -98,9 +98,10 @@ class CityInfoConquestFunctions(val city: CityInfo){ if (population.population > 1) population.addPopulation(-1 - population.population / 4) // so from 2-4 population, remove 1, from 5-8, remove 2, etc. reassignPopulation() - resistanceCounter = + setFlag(CityFlags.Resistance, if (reconqueredCityWhileStillInResistance || foundingCiv == receivingCiv.civName) 0 else population.population // I checked, and even if you puppet there's resistance for conquering + ) } conqueringCiv.updateViewableTiles() // Might see new tiles from this city } diff --git a/core/src/com/unciv/logic/city/CityStats.kt b/core/src/com/unciv/logic/city/CityStats.kt index a6f0ab5515..d53c5f13e3 100644 --- a/core/src/com/unciv/logic/city/CityStats.kt +++ b/core/src/com/unciv/logic/city/CityStats.kt @@ -532,7 +532,6 @@ class CityStats(val cityInfo: CityInfo) { baseStatList = LinkedHashMap(baseStatList).apply { put("Construction", statsFromProduction) } // concurrency-safe addition newFinalStatList["Construction"] = statsFromProduction - val isUnhappy = cityInfo.civInfo.getHappiness() < 0 for (entry in newFinalStatList.values) { entry.gold *= statPercentBonusesSum.gold.toPercent() entry.culture *= statPercentBonusesSum.culture.toPercent() @@ -550,33 +549,38 @@ class CityStats(val cityInfo: CityInfo) { entry.science *= statPercentBonusesSum.science.toPercent() } - /* Okay, food calculation is complicated. First we see how much food we generate. Then we apply production bonuses to it. Up till here, business as usual. Then, we deduct food eaten (from the total produced). - Now we have the excess food, which has its own things. If we're unhappy, cut it by 1/4. - Some policies have bonuses for excess food only, not general food production. - */ + Now we have the excess food, to which "growth" modifiers apply + Some policies have bonuses for growth only, not general food production. */ updateFoodEaten() newFinalStatList["Population"]!!.food -= foodEaten var totalFood = newFinalStatList.values.map { it.food }.sum() - if (isUnhappy && totalFood > 0) { // Reduce excess food to 1/4 per the same - val foodReducedByUnhappiness = Stats(food = totalFood * (-3 / 4f)) - baseStatList = LinkedHashMap(baseStatList).apply { put("Unhappiness", foodReducedByUnhappiness) } // concurrency-safe addition - newFinalStatList["Unhappiness"] = foodReducedByUnhappiness - } - - totalFood = newFinalStatList.values.map { it.food }.sum() // recalculate because of previous change - - // Since growth bonuses are special, (applied afterwards) they will be displayed separately in the user interface as well. - if (totalFood > 0 && !isUnhappy) { // Percentage Growth bonus revoked when unhappy per https://forums.civfanatics.com/resources/complete-guide-to-happiness-vanilla.25584/ - val foodFromGrowthBonuses = getGrowthBonusFromPoliciesAndWonders() * totalFood - newFinalStatList.add("Growth bonus", Stats(food = foodFromGrowthBonuses)) // Why Policies? Wonders can also provide this? - totalFood = newFinalStatList.values.map { it.food }.sum() // recalculate again + // Apply growth modifier only when positive food + if (totalFood > 0) { + // Since growth bonuses are special, (applied afterwards) they will be displayed separately in the user interface as well. + // All bonuses except We Love The King do apply even when unhappy + val foodFromGrowthBonuses = Stats(food = getGrowthBonusFromPoliciesAndWonders() * totalFood) + newFinalStatList.add("Growth bonus", foodFromGrowthBonuses) + val happiness = cityInfo.civInfo.getHappiness() + if (happiness < 0) { + // Unhappiness -75% to -100% + val foodReducedByUnhappiness = if (happiness <= -10) Stats(food = totalFood * -1) + else Stats(food = (totalFood * -3) / 4) + newFinalStatList.add("Unhappiness", foodReducedByUnhappiness) + } else if (cityInfo.isWeLoveTheKingDay()) { + // We Love The King Day +25%, only if not unhappy + val weLoveTheKingFood = Stats(food = totalFood / 4) + newFinalStatList.add("We Love The King Day", weLoveTheKingFood) + } + // recalculate only when all applied - growth bonuses are not multiplicative + // bonuses can allow a city to grow even with -100% unhappiness penalty, this is intended + totalFood = newFinalStatList.values.map { it.food }.sum() } val buildingsMaintenance = getBuildingMaintenanceCosts(citySpecificUniques) // this is AFTER the bonus calculation! diff --git a/core/src/com/unciv/logic/map/TileMap.kt b/core/src/com/unciv/logic/map/TileMap.kt index 4ce876dfdd..7211c0f013 100644 --- a/core/src/com/unciv/logic/map/TileMap.kt +++ b/core/src/com/unciv/logic/map/TileMap.kt @@ -72,6 +72,9 @@ class TileMap { @delegate:Transient val naturalWonders: List by lazy { tileList.asSequence().filter { it.isNaturalWonder() }.map { it.naturalWonder!! }.distinct().toList() } + @delegate:Transient + val resources: List by lazy { tileList.asSequence().filter { it.resource != null }.map { it.resource!! }.distinct().toList() } + // Excluded from Serialization by having no own backing field val values: Collection get() = tileList diff --git a/core/src/com/unciv/logic/trade/TradeEvaluation.kt b/core/src/com/unciv/logic/trade/TradeEvaluation.kt index b8b73124aa..1295573ed6 100644 --- a/core/src/com/unciv/logic/trade/TradeEvaluation.kt +++ b/core/src/com/unciv/logic/trade/TradeEvaluation.kt @@ -91,14 +91,15 @@ class TradeEvaluation { } TradeType.Luxury_Resource -> { + val weLoveTheKingPotential = civInfo.cities.count { it.demandedResource == offer.name } * 50 return if(!civInfo.hasResource(offer.name)) { // we can't trade on resources, so we are only interested in 1 copy for ourselves - when { // We're a lot more interested in luxury if low on happiness (AI is never low on happiness though) - civInfo.getHappiness() < 0 -> 450 - civInfo.getHappiness() < 10 -> 350 - else -> 300 // Higher than corresponding sell cost since a trade is mutually beneficial! - } - } else - 0 + weLoveTheKingPotential + when { // We're a lot more interested in luxury if low on happiness (AI is never low on happiness though) + civInfo.getHappiness() < 0 -> 450 + civInfo.getHappiness() < 10 -> 350 + else -> 300 // Higher than corresponding sell cost since a trade is mutually beneficial! + } + } else + 0 } TradeType.Strategic_Resource -> { diff --git a/core/src/com/unciv/models/Tutorial.kt b/core/src/com/unciv/models/Tutorial.kt index f0523f2ede..9b77d9515e 100644 --- a/core/src/com/unciv/models/Tutorial.kt +++ b/core/src/com/unciv/models/Tutorial.kt @@ -43,6 +43,7 @@ enum class Tutorial(val value: String, val isCivilopedia: Boolean = !value.start SpreadingReligion("Spreading_Religion"), Inquisitors("Inquisitors"), MayanCalendar("Maya_Long_Count_calendar_cycle"), + WeLoveTheKingDay("We_Love_The_King_Day"), ; companion object { diff --git a/core/src/com/unciv/ui/cityscreen/CityStatsTable.kt b/core/src/com/unciv/ui/cityscreen/CityStatsTable.kt index a88f582176..01eb4d6f51 100644 --- a/core/src/com/unciv/ui/cityscreen/CityStatsTable.kt +++ b/core/src/com/unciv/ui/cityscreen/CityStatsTable.kt @@ -3,6 +3,7 @@ package com.unciv.ui.cityscreen import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.utils.Align +import com.unciv.logic.city.CityFlags import com.unciv.models.stats.Stat import com.unciv.models.translations.tr import com.unciv.ui.utils.* @@ -79,7 +80,11 @@ class CityStatsTable(val cityScreen: CityScreen): Table() { innerTable.add(turnsToExpansionString.toLabel()).row() innerTable.add(turnsToPopString.toLabel()).row() if (cityInfo.isInResistance()) - innerTable.add("In resistance for another [${cityInfo.resistanceCounter}] turns".toLabel()).row() + innerTable.add("In resistance for another [${cityInfo.getFlag(CityFlags.Resistance)}] turns".toLabel()).row() + if (cityInfo.isWeLoveTheKingDay()) + innerTable.add("We Love The King Day for another [${cityInfo.getFlag(CityFlags.WeLoveTheKing)}] turns".toLabel()).row() + else if (cityInfo.demandedResource != "") + innerTable.add("Demanding [${cityInfo.demandedResource}]".toLabel()).row() } private fun addReligionInfo() { diff --git a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt index a1934c0e32..003848fa7c 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt @@ -832,6 +832,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas displayTutorial(Tutorial.SiegeUnits) { viewingCiv.getCivUnits().any { it.baseUnit.isProbablySiegeUnit() } } displayTutorial(Tutorial.Embarking) { viewingCiv.hasUnique("Enables embarkation for land units") } displayTutorial(Tutorial.NaturalWonders) { viewingCiv.naturalWonders.size > 0 } + displayTutorial(Tutorial.WeLoveTheKingDay) { viewingCiv.cities.any { it.demandedResource != "" } } } private fun backButtonAndESCHandler() {