mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-04 07:17:50 +07:00
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:
@ -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!"
|
||||
]
|
||||
}
|
||||
|
@ -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 =
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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!
|
||||
|
@ -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
|
||||
|
@ -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 -> {
|
||||
|
@ -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 {
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
Reference in New Issue
Block a user