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
This commit is contained in:
SimonCeder 2021-11-27 18:59:19 +01:00 committed by GitHub
parent 4107ff3386
commit 10686d1d8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 159 additions and 40 deletions

View File

@ -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.", "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!" "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!"
] ]
} }

View File

@ -638,6 +638,9 @@ Clearing a [forest] has created [amount] Production for [cityName] =
[civName] no longer needs your help with the [questName] quest. = [civName] no longer needs your help with the [questName] quest. =
The [questName] quest for [civName] has ended. It was won by [civNames]. = The [questName] quest for [civName] has ended. It was won by [civNames]. =
The resistance in [cityName] has ended! = 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 and was destroyed =
Our [name] took [tileDamage] tile damage = Our [name] took [tileDamage] tile damage =
[civName] has adopted the [policyName] policy = [civName] has adopted the [policyName] policy =
@ -722,6 +725,7 @@ Territory =
Force = Force =
GOLDEN AGE = GOLDEN AGE =
Golden Age = Golden Age =
We Love The King Day =
[year] BC = [year] BC =
[year] AD = [year] AD =
Civilopedia = Civilopedia =
@ -786,6 +790,8 @@ Food converts to production =
[turnsToStarvation] turns to lose population = [turnsToStarvation] turns to lose population =
Stopped population growth = Stopped population growth =
In resistance for another [numberOfTurns] turns = In resistance for another [numberOfTurns] turns =
We Love The King Day for another [numberOfTurns] turns =
Demanding [resource] =
Sell for [sellAmount] gold = Sell for [sellAmount] gold =
Are you sure you want to sell this [building]? = Are you sure you want to sell this [building]? =
Free = Free =

View File

@ -522,7 +522,7 @@ object NextTurnAutomation {
.filter { resource -> .filter { resource ->
tradeLogic.ourAvailableOffers tradeLogic.ourAvailableOffers
.none { it.name == resource.name && it.type == TradeType.Luxury_Resource } .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<Trade>() val trades = ArrayList<Trade>()
for (i in 0..min(weHaveTheyDont.lastIndex, theyHaveWeDont.lastIndex)) { for (i in 0..min(weHaveTheyDont.lastIndex, theyHaveWeDont.lastIndex)) {
val trade = Trade() val trade = Trade()

View File

@ -3,6 +3,7 @@ package com.unciv.logic.city
import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector2
import com.unciv.logic.battle.CityCombatant import com.unciv.logic.battle.CityCombatant
import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.Proximity import com.unciv.logic.civilization.Proximity
import com.unciv.logic.civilization.ReligionState import com.unciv.logic.civilization.ReligionState
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
@ -25,6 +26,12 @@ import kotlin.math.min
import kotlin.math.pow import kotlin.math.pow
import kotlin.math.roundToInt import kotlin.math.roundToInt
enum class CityFlags {
WeLoveTheKing,
ResourceDemand,
Resistance
}
class CityInfo { class CityInfo {
@Suppress("JoinDeclarationAndAssignment") @Suppress("JoinDeclarationAndAssignment")
@Transient @Transient
@ -54,6 +61,8 @@ class CityInfo {
var previousOwner = "" var previousOwner = ""
var turnAcquired = 0 var turnAcquired = 0
var health = 200 var health = 200
@Deprecated("As of 3.18.4", ReplaceWith("CityFlags.Resistance"), DeprecationLevel.WARNING)
var resistanceCounter = 0 var resistanceCounter = 0
var religion = CityInfoReligionManager() 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. */ * It is important to distinguish them since the original cannot be razed and defines the Domination Victory. */
var isOriginalCapital = false var isOriginalCapital = false
/** For We Love the King Day */
var demandedResource = ""
private var flagsCountdown = HashMap<String, Int>()
constructor() // for json parsing, we need to have a default constructor constructor() // for json parsing, we need to have a default constructor
constructor(civInfo: CivilizationInfo, cityLocation: Vector2) { // new city! constructor(civInfo: CivilizationInfo, cityLocation: Vector2) { // new city!
this.civInfo = civInfo this.civInfo = civInfo
@ -233,11 +247,12 @@ class CityInfo {
toReturn.lockedTiles = lockedTiles toReturn.lockedTiles = lockedTiles
toReturn.isBeingRazed = isBeingRazed toReturn.isBeingRazed = isBeingRazed
toReturn.attackedThisTurn = attackedThisTurn toReturn.attackedThisTurn = attackedThisTurn
toReturn.resistanceCounter = resistanceCounter
toReturn.foundingCiv = foundingCiv toReturn.foundingCiv = foundingCiv
toReturn.turnAcquired = turnAcquired toReturn.turnAcquired = turnAcquired
toReturn.isPuppet = isPuppet toReturn.isPuppet = isPuppet
toReturn.isOriginalCapital = isOriginalCapital toReturn.isOriginalCapital = isOriginalCapital
toReturn.flagsCountdown.putAll(flagsCountdown)
toReturn.demandedResource = demandedResource
return toReturn 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 */ /** @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 ) } fun getFrontierScore() = getCenterTile().getTilesAtDistance(4).count { it.canBeSettled() && (it.getOwner() == null || it.getOwner() == civInfo ) }
@ -477,6 +496,11 @@ class CityInfo {
cityConstructions.cityInfo = this cityConstructions.cityInfo = this
cityConstructions.setTransients() cityConstructions.setTransients()
religion.setTransients(this) religion.setTransients(this)
if (resistanceCounter > 0) {
setFlag(CityFlags.Resistance, resistanceCounter)
resistanceCounter = 0
}
} }
fun startTurn() { fun startTurn() {
@ -488,17 +512,52 @@ class CityInfo {
cityStats.update() cityStats.update()
tryUpdateRoadStatus() tryUpdateRoadStatus()
attackedThisTurn = false attackedThisTurn = false
if (isInResistance()) {
resistanceCounter--
if (!isInResistance())
civInfo.addNotification(
"The resistance in [$name] has ended!",
location,
"StatIcons/Resistance"
)
}
if (isPuppet) reassignPopulation() 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() { fun reassignPopulation() {
@ -624,6 +683,38 @@ class CityInfo {
civInfo.updateDetailedCivResources() // this building could be a resource-requiring one 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. 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, The SettledCitiesNearUs flag gets added to the AI so it knows this happened,

View File

@ -86,7 +86,7 @@ class CityInfoConquestFunctions(val city: CityInfo){
conqueringCiv.addGold(goldPlundered) conqueringCiv.addGold(goldPlundered)
conqueringCiv.addNotification("Received [$goldPlundered] Gold for capturing [$name]", getCenterTile().position, NotificationIcon.Gold) 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() 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. if (population.population > 1) population.addPopulation(-1 - population.population / 4) // so from 2-4 population, remove 1, from 5-8, remove 2, etc.
reassignPopulation() reassignPopulation()
resistanceCounter = setFlag(CityFlags.Resistance,
if (reconqueredCityWhileStillInResistance || foundingCiv == receivingCiv.civName) 0 if (reconqueredCityWhileStillInResistance || foundingCiv == receivingCiv.civName) 0
else population.population // I checked, and even if you puppet there's resistance for conquering else population.population // I checked, and even if you puppet there's resistance for conquering
)
} }
conqueringCiv.updateViewableTiles() // Might see new tiles from this city conqueringCiv.updateViewableTiles() // Might see new tiles from this city
} }

View File

@ -532,7 +532,6 @@ class CityStats(val cityInfo: CityInfo) {
baseStatList = LinkedHashMap(baseStatList).apply { put("Construction", statsFromProduction) } // concurrency-safe addition baseStatList = LinkedHashMap(baseStatList).apply { put("Construction", statsFromProduction) } // concurrency-safe addition
newFinalStatList["Construction"] = statsFromProduction newFinalStatList["Construction"] = statsFromProduction
val isUnhappy = cityInfo.civInfo.getHappiness() < 0
for (entry in newFinalStatList.values) { for (entry in newFinalStatList.values) {
entry.gold *= statPercentBonusesSum.gold.toPercent() entry.gold *= statPercentBonusesSum.gold.toPercent()
entry.culture *= statPercentBonusesSum.culture.toPercent() entry.culture *= statPercentBonusesSum.culture.toPercent()
@ -550,33 +549,38 @@ class CityStats(val cityInfo: CityInfo) {
entry.science *= statPercentBonusesSum.science.toPercent() entry.science *= statPercentBonusesSum.science.toPercent()
} }
/* Okay, food calculation is complicated. /* Okay, food calculation is complicated.
First we see how much food we generate. Then we apply production bonuses to it. First we see how much food we generate. Then we apply production bonuses to it.
Up till here, business as usual. Up till here, business as usual.
Then, we deduct food eaten (from the total produced). 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. Now we have the excess food, to which "growth" modifiers apply
Some policies have bonuses for excess food only, not general food production. Some policies have bonuses for growth only, not general food production. */
*/
updateFoodEaten() updateFoodEaten()
newFinalStatList["Population"]!!.food -= foodEaten newFinalStatList["Population"]!!.food -= foodEaten
var totalFood = newFinalStatList.values.map { it.food }.sum() var totalFood = newFinalStatList.values.map { it.food }.sum()
if (isUnhappy && totalFood > 0) { // Reduce excess food to 1/4 per the same // Apply growth modifier only when positive food
val foodReducedByUnhappiness = Stats(food = totalFood * (-3 / 4f)) if (totalFood > 0) {
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. // 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/ // All bonuses except We Love The King do apply even when unhappy
val foodFromGrowthBonuses = getGrowthBonusFromPoliciesAndWonders() * totalFood val foodFromGrowthBonuses = Stats(food = getGrowthBonusFromPoliciesAndWonders() * totalFood)
newFinalStatList.add("Growth bonus", Stats(food = foodFromGrowthBonuses)) // Why Policies? Wonders can also provide this? newFinalStatList.add("Growth bonus", foodFromGrowthBonuses)
totalFood = newFinalStatList.values.map { it.food }.sum() // recalculate again 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! val buildingsMaintenance = getBuildingMaintenanceCosts(citySpecificUniques) // this is AFTER the bonus calculation!

View File

@ -72,6 +72,9 @@ class TileMap {
@delegate:Transient @delegate:Transient
val naturalWonders: List<String> by lazy { tileList.asSequence().filter { it.isNaturalWonder() }.map { it.naturalWonder!! }.distinct().toList() } val naturalWonders: List<String> by lazy { tileList.asSequence().filter { it.isNaturalWonder() }.map { it.naturalWonder!! }.distinct().toList() }
@delegate:Transient
val resources: List<String> by lazy { tileList.asSequence().filter { it.resource != null }.map { it.resource!! }.distinct().toList() }
// Excluded from Serialization by having no own backing field // Excluded from Serialization by having no own backing field
val values: Collection<TileInfo> val values: Collection<TileInfo>
get() = tileList get() = tileList

View File

@ -91,8 +91,9 @@ class TradeEvaluation {
} }
TradeType.Luxury_Resource -> { 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 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) 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() < 0 -> 450
civInfo.getHappiness() < 10 -> 350 civInfo.getHappiness() < 10 -> 350
else -> 300 // Higher than corresponding sell cost since a trade is mutually beneficial! else -> 300 // Higher than corresponding sell cost since a trade is mutually beneficial!

View File

@ -43,6 +43,7 @@ enum class Tutorial(val value: String, val isCivilopedia: Boolean = !value.start
SpreadingReligion("Spreading_Religion"), SpreadingReligion("Spreading_Religion"),
Inquisitors("Inquisitors"), Inquisitors("Inquisitors"),
MayanCalendar("Maya_Long_Count_calendar_cycle"), MayanCalendar("Maya_Long_Count_calendar_cycle"),
WeLoveTheKingDay("We_Love_The_King_Day"),
; ;
companion object { companion object {

View File

@ -3,6 +3,7 @@ package com.unciv.ui.cityscreen
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.logic.city.CityFlags
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
@ -79,7 +80,11 @@ class CityStatsTable(val cityScreen: CityScreen): Table() {
innerTable.add(turnsToExpansionString.toLabel()).row() innerTable.add(turnsToExpansionString.toLabel()).row()
innerTable.add(turnsToPopString.toLabel()).row() innerTable.add(turnsToPopString.toLabel()).row()
if (cityInfo.isInResistance()) 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() { private fun addReligionInfo() {

View File

@ -832,6 +832,7 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Bas
displayTutorial(Tutorial.SiegeUnits) { viewingCiv.getCivUnits().any { it.baseUnit.isProbablySiegeUnit() } } displayTutorial(Tutorial.SiegeUnits) { viewingCiv.getCivUnits().any { it.baseUnit.isProbablySiegeUnit() } }
displayTutorial(Tutorial.Embarking) { viewingCiv.hasUnique("Enables embarkation for land units") } displayTutorial(Tutorial.Embarking) { viewingCiv.hasUnique("Enables embarkation for land units") }
displayTutorial(Tutorial.NaturalWonders) { viewingCiv.naturalWonders.size > 0 } displayTutorial(Tutorial.NaturalWonders) { viewingCiv.naturalWonders.size > 0 }
displayTutorial(Tutorial.WeLoveTheKingDay) { viewingCiv.cities.any { it.demandedResource != "" } }
} }
private fun backButtonAndESCHandler() { private fun backButtonAndESCHandler() {