From dea3256c930298cefbb8163805bf69ce5c69b95c Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Sun, 16 Apr 2023 13:21:58 +0200 Subject: [PATCH] Unhappiness effects (#9188) * Block growth if Unique says so - even with full stores * Display unhappiness rejection reason for Settlers * Display unhappiness effects in Overview Stats * Display unhappiness effects in Overview Stats - simplify --- core/src/com/unciv/logic/city/CityStats.kt | 2 +- .../city/managers/CityPopulationManager.kt | 34 +++++++++------- .../com/unciv/models/ruleset/IConstruction.kt | 2 + .../com/unciv/models/ruleset/unique/Unique.kt | 3 +- .../com/unciv/models/ruleset/unit/BaseUnit.kt | 22 +++++------ .../overviewscreen/StatsOverviewTable.kt | 39 +++++++++++++++++++ 6 files changed, 74 insertions(+), 28 deletions(-) diff --git a/core/src/com/unciv/logic/city/CityStats.kt b/core/src/com/unciv/logic/city/CityStats.kt index 1ef1b8403a..89d4085b7e 100644 --- a/core/src/com/unciv/logic/city/CityStats.kt +++ b/core/src/com/unciv/logic/city/CityStats.kt @@ -593,7 +593,7 @@ class CityStats(val city: City) { val amountToRemove = -newFinalStatList.values.sumOf { it[Stat.Food].toDouble() } newFinalStatList.add( getSourceNameForUnique(growthNullifyingUnique), - Stats().apply { this[Stat.Food] = amountToRemove.toFloat() } + Stats(food = amountToRemove.toFloat()) ) } diff --git a/core/src/com/unciv/logic/city/managers/CityPopulationManager.kt b/core/src/com/unciv/logic/city/managers/CityPopulationManager.kt index 1573d2578e..5d05f114f1 100644 --- a/core/src/com/unciv/logic/city/managers/CityPopulationManager.kt +++ b/core/src/com/unciv/logic/city/managers/CityPopulationManager.kt @@ -102,20 +102,26 @@ class CityPopulationManager : IsPartOfGameInfoSerialization { if (population > 1) addPopulation(-1) foodStored = 0 } - if (foodStored >= getFoodToNextPopulation()) { // growth! - foodStored -= getFoodToNextPopulation() - var percentOfFoodCarriedOver = - city.getMatchingUniques(UniqueType.CarryOverFood) - .filter { city.matchesFilter(it.params[1]) } - .sumOf { it.params[0].toInt() } - // Try to avoid runaway food gain in mods, just in case - if (percentOfFoodCarriedOver > 95) percentOfFoodCarriedOver = 95 - foodStored += (getFoodToNextPopulation() * percentOfFoodCarriedOver / 100f).toInt() - addPopulation(1) - city.updateCitizens = true - city.civ.addNotification("[${city.name}] has grown!", city.location, - NotificationCategory.Cities, NotificationIcon.Growth) - } + val foodNeededToGrow = getFoodToNextPopulation() + if (foodStored < foodNeededToGrow) return + + // What if the stores are already over foodNeededToGrow but NullifiesGrowth is in effect? + // We could simply test food==0 - but this way NullifiesStat(food) will still allow growth: + if (city.getMatchingUniques(UniqueType.NullifiesGrowth).any()) + return + + // growth! + foodStored -= foodNeededToGrow + val percentOfFoodCarriedOver = + city.getMatchingUniques(UniqueType.CarryOverFood) + .filter { city.matchesFilter(it.params[1]) } + .sumOf { it.params[0].toInt() } + .coerceAtMost(95) // Try to avoid runaway food gain in mods, just in case + foodStored += (foodNeededToGrow * percentOfFoodCarriedOver / 100f).toInt() + addPopulation(1) + city.updateCitizens = true + city.civ.addNotification("[${city.name}] has grown!", city.location, + NotificationCategory.Cities, NotificationIcon.Growth) } fun addPopulation(count: Int) { diff --git a/core/src/com/unciv/models/ruleset/IConstruction.kt b/core/src/com/unciv/models/ruleset/IConstruction.kt index a477d6ff4c..94fae917c5 100644 --- a/core/src/com/unciv/models/ruleset/IConstruction.kt +++ b/core/src/com/unciv/models/ruleset/IConstruction.kt @@ -130,6 +130,7 @@ class RejectionReason(val type: RejectionReasonType, RejectionReasonType.RequiresBuildingInAllCities, RejectionReasonType.RequiresBuildingInThisCity, RejectionReasonType.RequiresBuildingInSomeCity, + RejectionReasonType.CannotBeBuiltUnhappiness, RejectionReasonType.PopulationRequirement, RejectionReasonType.ConsumesResources, RejectionReasonType.CanOnlyBePurchased, @@ -160,6 +161,7 @@ enum class RejectionReasonType(val shouldShow: Boolean, val errorMessage: String UniqueToOtherNation(false, "Unique to another nation"), ReplacedByOurUnique(false, "Our unique replaces this"), CannotBeBuilt(false, "Cannot be built by this nation"), + CannotBeBuiltUnhappiness(true, "Unhappiness"), Obsoleted(false, "Obsolete"), RequiresTech(false, "Required tech not researched"), diff --git a/core/src/com/unciv/models/ruleset/unique/Unique.kt b/core/src/com/unciv/models/ruleset/unique/Unique.kt index 280e145342..c037b06d99 100644 --- a/core/src/com/unciv/models/ruleset/unique/Unique.kt +++ b/core/src/com/unciv/models/ruleset/unique/Unique.kt @@ -162,7 +162,7 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s UniqueType.ConditionalWhenBelowAmountResource -> state.civInfo != null && state.civInfo.getCivResourcesByName()[condition.params[1]]!! < condition.params[0].toInt() UniqueType.ConditionalHappy -> - state.civInfo != null && state.civInfo.stats.statsForNextTurn.happiness >= 0 + state.civInfo != null && state.civInfo.stats.happiness >= 0 UniqueType.ConditionalBetweenHappiness -> state.civInfo != null && condition.params[0].toInt() <= state.civInfo.stats.happiness @@ -379,4 +379,3 @@ fun ArrayList.getMatchingUniques(uniqueType: UniqueType, stateF .map { it.uniqueObject } .filter { it.isOfType(uniqueType) && it.conditionalsApply(stateForConditionals) } } - diff --git a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt index d39ff38096..99fe119022 100644 --- a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt +++ b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt @@ -9,6 +9,7 @@ import com.unciv.logic.civilization.Civilization import com.unciv.logic.map.mapunit.MapUnit import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetObject +import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.stats.Stat @@ -141,7 +142,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { yieldAll(getRejectionReasons(civInfo, cityConstructions.city)) } - fun getRejectionReasons(civ: Civilization, city:City?=null): Sequence { + fun getRejectionReasons(civ: Civilization, city: City? = null): Sequence { val result = mutableListOf() if (requiredTech != null && !civ.tech.isResearched(requiredTech!!)) result.add(RejectionReasonType.RequiresTech.toInstance("$requiredTech not researched")) @@ -189,16 +190,15 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { } } - for (unique in civ.getMatchingUniques(UniqueType.CannotBuildUnits)) - if (this@BaseUnit.matchesFilter(unique.params[0])) { - if (unique.conditionals.any { it.type == UniqueType.ConditionalBelowHappiness }) { - result.add( - RejectionReasonType.CannotBeBuilt.toInstance( - unique.text, - true - ) - ) - } else result.add(RejectionReasonType.CannotBeBuilt.toInstance()) + val stateForConditionals = StateForConditionals(civ, city) + for (unique in civ.getMatchingUniques(UniqueType.CannotBuildUnits, stateForConditionals)) + if (this.matchesFilter(unique.params[0])) { + val hasHappinessCondition = unique.conditionals.any { + it.type == UniqueType.ConditionalBelowHappiness || it.type == UniqueType.ConditionalBetweenHappiness + } + if (hasHappinessCondition) + result.add(RejectionReasonType.CannotBeBuiltUnhappiness.toInstance(unique.text)) + else result.add(RejectionReasonType.CannotBeBuilt.toInstance()) } return result.asSequence() } diff --git a/core/src/com/unciv/ui/screens/overviewscreen/StatsOverviewTable.kt b/core/src/com/unciv/ui/screens/overviewscreen/StatsOverviewTable.kt index 0da1544ae6..7eb37ea2c9 100644 --- a/core/src/com/unciv/ui/screens/overviewscreen/StatsOverviewTable.kt +++ b/core/src/com/unciv/ui/screens/overviewscreen/StatsOverviewTable.kt @@ -6,6 +6,8 @@ import com.unciv.Constants import com.unciv.GUI import com.unciv.logic.civilization.Civilization import com.unciv.models.ruleset.ModOptionsConstants +import com.unciv.models.ruleset.unique.Unique +import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.stats.Stat import com.unciv.models.stats.StatMap import com.unciv.ui.components.TabbedPager @@ -13,6 +15,8 @@ import com.unciv.ui.components.UncivSlider import com.unciv.ui.components.extensions.addSeparator import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.images.ImageGetter +import com.unciv.ui.screens.civilopediascreen.FormattedLine +import com.unciv.ui.screens.civilopediascreen.MarkupRenderer import kotlin.math.roundToInt class StatsOverviewTab( @@ -20,6 +24,7 @@ class StatsOverviewTab( overviewScreen: EmpireOverviewScreen ) : EmpireOverviewTab(viewingPlayer, overviewScreen) { private val happinessTable = Table() + private val unhappinessTable = UnhappinessTable() private val goldAndSliderTable = Table() private val goldTable = Table() private val scienceTable = Table() @@ -45,6 +50,7 @@ class StatsOverviewTab( faithTable.defaults().pad(5f) greatPeopleTable.defaults().pad(5f) scoreTable.defaults().pad(5f) + unhappinessTable.update() goldAndSliderTable.add(goldTable).row() if (gameInfo.ruleset.modOptions.uniques.contains(ModOptionsConstants.convertGoldToScience)) @@ -54,6 +60,7 @@ class StatsOverviewTab( val allStatTables = sequence { yield(happinessTable) + if (unhappinessTable.show) yield(unhappinessTable) yield(goldAndSliderTable) yield(scienceTable) yield(cultureTable) @@ -121,6 +128,38 @@ class StatsOverviewTab( addTotal(happinessBreakdown.values.sum()) } + inner class UnhappinessTable : Table() { + val show: Boolean + + private val uniques: Set + + init { + defaults().pad(5f) + uniques = sequenceOf( + UniqueType.ConditionalBetweenHappiness, + UniqueType.ConditionalBelowHappiness + ).flatMap { conditional -> + viewingPlayer.getTriggeredUniques(conditional) + .sortedBy { it.type } // otherwise order might change as a HashMap is involved + }.toSet() + show = uniques.isNotEmpty() + } + + fun update() { + add(ImageGetter.getStatIcon("Malcontent")) + .size(Constants.headingFontSize.toFloat()) + .right().padRight(1f) + add("Unhappiness".toLabel(fontSize = Constants.headingFontSize)).left() + addSeparator() + + add(MarkupRenderer.render( + uniques.map { FormattedLine(it) }, + labelWidth = (overviewScreen.stage.width * 0.25f).coerceAtLeast(greatPeopleTable.width * 0.8f), + iconDisplay = FormattedLine.IconDisplay.NoLink + )).colspan(2) + } + } + private fun Table.updateStatTable(stat: Stat, statMap: StatMap) { addHeading(stat.name) var total = 0f