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
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.",
"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. =
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 =

View File

@ -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<Trade>()
for (i in 0..min(weHaveTheyDont.lastIndex, theyHaveWeDont.lastIndex)) {
val trade = Trade()

View File

@ -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<String, Int>()
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,

View File

@ -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
}

View File

@ -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!

View File

@ -72,6 +72,9 @@ class TileMap {
@delegate:Transient
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
val values: Collection<TileInfo>
get() = tileList

View File

@ -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 -> {

View File

@ -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 {

View File

@ -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() {

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.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() {