Add unified unique for gaining stats or stockpiles (#12642)

* Add unified unique for gaining stats or stockpiles

* Use IgnoreConditionals instead of EmptyState
This commit is contained in:
SeventhM 2024-12-16 00:54:47 -08:00 committed by GitHub
parent 2963d47295
commit ae28dca570
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 162 additions and 33 deletions

View File

@ -17,12 +17,15 @@ import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.RoadStatus
import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.stats.GameResource
import com.unciv.models.stats.INamed
import com.unciv.models.stats.Stat
import com.unciv.models.stats.SubStat
import java.util.UUID
import kotlin.math.roundToInt
@ -212,6 +215,19 @@ class City : IsPartOfGameInfoSerialization, INamed {
}
}
fun addGameResource(stat: GameResource, amount: Int) {
if (stat is TileResource) {
if (!stat.isStockpiled) return
if (!stat.isCityWide) civ.gainStockpiledResource(stat.name, amount)
else { /*TODO*/ }
}
when (stat) {
Stat.Production -> cityConstructions.addProductionPoints(amount)
Stat.Food, SubStat.StoredFood -> population.foodStored += amount
else -> civ.addGameResource(stat, amount)
}
}
fun getStatReserve(stat: Stat): Int {
return when (stat) {
Stat.Production -> cityConstructions.getWorkDone(cityConstructions.getCurrentConstruction().name)
@ -220,6 +236,20 @@ class City : IsPartOfGameInfoSerialization, INamed {
}
}
fun getReserve(stat: GameResource): Int {
if (stat is TileResource && stat.isCityWide) {
return if (stat.isStockpiled) {
// TODO
0
} else 0
}
return when (stat) {
Stat.Production -> cityConstructions.getWorkDone(cityConstructions.getCurrentConstruction().name)
Stat.Food, SubStat.StoredFood -> population.foodStored
else -> civ.getReserve(stat)
}
}
fun hasStatToBuy(stat: Stat, price: Int): Boolean {
return when {
civ.gameInfo.gameParameters.godMode -> true

View File

@ -435,7 +435,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
for (unique in costUniques) {
val amount = unique.params[0].toInt()
val resourceName = unique.params[1]
city.civ.resourceStockpiles.add(resourceName, -amount)
city.civ.gainStockpiledResource(resourceName, -amount)
}
if (construction !is Building) return
@ -705,7 +705,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
for (unique in costUniques) {
val amount = unique.params[0].toInt()
val resourceName = unique.params[1]
city.civ.resourceStockpiles.add(resourceName, -amount)
city.civ.gainStockpiledResource(resourceName, -amount)
}
}
}

View File

@ -28,7 +28,7 @@ object CityResources {
// This way we get them once, but it is ugly, I welcome other ideas :/
getCityResourcesFromCiv(city, cityResources, resourceModifers)
cityResources.removeAll { !it.resource.hasUnique(UniqueType.CityResource) }
cityResources.removeAll { !it.resource.isCityWide }
return cityResources
}
@ -69,7 +69,7 @@ object CityResources {
fun getAvailableResourceAmount(city: City, resourceName: String): Int {
val resource = city.getRuleset().tileResources[resourceName] ?: return 0
if (resource.hasUnique(UniqueType.CityResource))
if (resource.isCityWide)
return getCityResourcesAvailableToCity(city).asSequence().filter { it.resource == resource }.sumOf { it.amount }
return city.civ.getResourceAmount(resourceName)
}

View File

@ -48,8 +48,10 @@ import com.unciv.models.ruleset.tile.TileImprovement
import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.ruleset.unique.*
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.stats.GameResource
import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats
import com.unciv.models.stats.SubStat
import com.unciv.models.translations.tr
import com.unciv.ui.components.extensions.toPercent
import com.unciv.ui.screens.victoryscreen.RankingType
@ -451,7 +453,7 @@ class Civilization : IsPartOfGameInfoSerialization {
val newResourceSupplyList = ResourceSupplyList(keepZeroAmounts = true)
for (resourceSupply in detailedCivResources) {
if (resourceSupply.resource.isStockpiled()) continue
if (resourceSupply.resource.isStockpiled) continue
if (resourceSupply.resource.hasUnique(UniqueType.CannotBeTraded, state)) continue
// If we got it from another trade or from a CS, preserve the origin
if (resourceSupply.isCityStateOrTradeOrigin()) {
@ -475,7 +477,7 @@ class Civilization : IsPartOfGameInfoSerialization {
val hashMap = HashMap<String, Int>(gameInfo.ruleset.tileResources.size)
for (resource in gameInfo.ruleset.tileResources.keys) hashMap[resource] = 0
for (entry in getCivResourceSupply())
if (!entry.resource.isStockpiled())
if (!entry.resource.isStockpiled)
hashMap[entry.resource.name] = entry.amount
for ((key, value) in resourceStockpiles)
hashMap[key] = value
@ -863,6 +865,26 @@ class Civilization : IsPartOfGameInfoSerialization {
}
}
fun addGameResource(stat: GameResource, amount: Int) {
if (stat is TileResource && !stat.isCityWide && stat.isStockpiled) gainStockpiledResource(stat.name, amount)
when (stat) {
Stat.Culture -> { policies.addCulture(amount)
if (amount > 0) totalCultureForContests += amount }
Stat.Science -> tech.addScience(amount)
Stat.Gold -> addGold(amount)
Stat.Faith -> { religionManager.storedFaith += amount
if (amount > 0) totalFaithForContests += amount }
SubStat.GoldenAgePoints -> goldenAges.addHappiness(amount)
else -> {}
// Food and Production wouldn't make sense to be added nationwide
// Happiness cannot be added as it is recalculated again, use a unique instead
}
}
fun gainStockpiledResource(resourceName: String, amount: Int) {
resourceStockpiles.add(resourceName, amount)
}
fun getStatReserve(stat: Stat): Int {
return when (stat) {
Stat.Culture -> policies.storedCulture
@ -876,6 +898,22 @@ class Civilization : IsPartOfGameInfoSerialization {
}
}
fun getReserve(stat: GameResource): Int {
if (stat is TileResource && !stat.isCityWide && stat.isStockpiled)
return resourceStockpiles[stat.name]
return when (stat) {
Stat.Culture -> policies.storedCulture
Stat.Science -> {
if (tech.currentTechnology() == null) 0
else tech.researchOfTech(tech.currentTechnology()!!.name)
}
Stat.Gold -> gold
Stat.Faith -> religionManager.storedFaith
SubStat.GoldenAgePoints -> goldenAges.storedHappiness
else -> 0
}
}
// region addNotification
fun addNotification(text: String, category: NotificationCategory, vararg notificationIcons: String) =
addNotification(text, null, category, *notificationIcons)

View File

@ -432,7 +432,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
val isResourceFilter: (TradeOffer) -> Boolean = {
(it.type == TradeOfferType.Strategic_Resource || it.type == TradeOfferType.Luxury_Resource)
&& resourcesMap.containsKey(it.name)
&& !resourcesMap[it.name]!!.isStockpiled()
&& !resourcesMap[it.name]!!.isStockpiled
}
for (trade in trades) {
for (offer in trade.ourOffers.filter(isResourceFilter))

View File

@ -31,7 +31,7 @@ object DiplomacyTurnManager {
// Every cancelled trade can change this - if 1 resource is missing,
// don't cancel all trades of that resource, only cancel one (the first one, as it happens, since they're added chronologically)
val negativeCivResources = civInfo.getCivResourceSupply()
.filter { it.amount < 0 && !it.resource.isStockpiled() }.map { it.resource.name }
.filter { it.amount < 0 && !it.resource.isStockpiled }.map { it.resource.name }
for (offer in trade.ourOffers) {
if (offer.type in listOf(TradeOfferType.Luxury_Resource, TradeOfferType.Strategic_Resource)

View File

@ -9,7 +9,6 @@ import com.unciv.logic.civilization.PopupAlert
import com.unciv.models.ruleset.unique.UniqueTriggerActivation
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.components.extensions.toPercent
import kotlin.math.max
class GoldenAgeManager : IsPartOfGameInfoSerialization {
@Transient
@ -28,6 +27,10 @@ class GoldenAgeManager : IsPartOfGameInfoSerialization {
}
fun isGoldenAge(): Boolean = turnsLeftForCurrentGoldenAge > 0
fun addHappiness(amount: Int) {
storedHappiness += amount
}
fun happinessRequiredForNextGoldenAge(): Int {
var cost = (500 + numberOfGoldenAges * 250).toFloat()

View File

@ -40,8 +40,8 @@ class TurnManager(val civInfo: Civilization) {
civInfo.tech.updateResearchProgress()
civInfo.cache.updateCivResources() // If you offered a trade last turn, this turn it will have been accepted/declined
for (stockpiledResource in civInfo.getCivResourceSupply().filter { it.resource.isStockpiled() })
civInfo.resourceStockpiles.add(stockpiledResource.resource.name, stockpiledResource.amount)
for (stockpiledResource in civInfo.getCivResourceSupply().filter { it.resource.isStockpiled })
civInfo.gainStockpiledResource(stockpiledResource.resource.name, stockpiledResource.amount)
civInfo.civConstructions.startTurn()
civInfo.attacksSinceTurnStart.clear()

View File

@ -9,11 +9,12 @@ import com.unciv.models.ruleset.RulesetStatsObject
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.GameResource
import com.unciv.models.stats.Stats
import com.unciv.ui.objectdescriptions.uniquesToCivilopediaTextLines
import com.unciv.ui.screens.civilopediascreen.FormattedLine
class TileResource : RulesetStatsObject() {
class TileResource : RulesetStatsObject(), GameResource {
var resourceType: ResourceType = ResourceType.Bonus
var terrainsCanBeFoundOn: List<String> = listOf()
@ -35,6 +36,10 @@ class TileResource : RulesetStatsObject() {
var majorDepositAmount: DepositAmount = DepositAmount()
var minorDepositAmount: DepositAmount = DepositAmount()
val isCityWide by lazy { hasUnique(UniqueType.CityResource, StateForConditionals.IgnoreConditionals) }
val isStockpiled by lazy { hasUnique(UniqueType.Stockpiled, StateForConditionals.IgnoreConditionals) }
private var improvementsInitialized = false
/** Cache collecting [improvement], [improvedBy] and [UniqueType.ImprovesResources] uniques on the improvements themselves. */
@ -209,8 +214,6 @@ class TileResource : RulesetStatsObject() {
return true
}
fun isStockpiled() = hasUnique(UniqueType.Stockpiled)
class DepositAmount {
var sparse: Int = 1
var default: Int = 2

View File

@ -11,6 +11,7 @@ import com.unciv.models.ruleset.tile.TerrainType
import com.unciv.models.ruleset.unique.UniqueParameterType.Companion.guessTypeForTranslationWriter
import com.unciv.models.ruleset.validation.Suppression
import com.unciv.models.stats.Stat
import com.unciv.models.stats.SubStat
import com.unciv.models.translations.TranslationFileWriter
import com.unciv.models.translations.equalsPlaceholderText
@ -468,7 +469,15 @@ enum class UniqueParameterType(
/** Used by [UniqueType.OneTimeConsumeResources], [UniqueType.OneTimeProvideResources], [UniqueType.CostsResources], [UniqueType.UnitActionStockpileCost], implementation not centralized */
StockpiledResource("stockpiledResource", "Mana", "The name of any stockpiled resource") {
override fun getKnownValuesForAutocomplete(ruleset: Ruleset) = ruleset.tileResources.filter { it.value.isStockpiled() }.keys
override fun getKnownValuesForAutocomplete(ruleset: Ruleset) = ruleset.tileResources.filter { it.value.isStockpiled }.keys
},
/** Used by [UniqueType.OneTimeGainResource], implementation not centralized */
Stockpile("stockpile", "Mana", "The name of any stockpiled resource") {
override fun getKnownValuesForAutocomplete(ruleset: Ruleset): Set<String> {
return ruleset.tileResources.filter { it.value.isStockpiled }.keys +
Stat.entries.map { it.name } + SubStat.StoredFood.name + SubStat.GoldenAgePoints.name
}
},
/** Used by [UniqueType.ImprovesResources], implemented by [com.unciv.models.ruleset.tile.TileResource.matchesFilter] */

View File

@ -27,8 +27,10 @@ import com.unciv.models.UpgradeUnitAction
import com.unciv.models.ruleset.BeliefType
import com.unciv.models.ruleset.Event
import com.unciv.models.ruleset.tile.TerrainType
import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats
import com.unciv.models.stats.SubStat
import com.unciv.models.translations.fillPlaceholders
import com.unciv.models.translations.hasPlaceholderParameters
import com.unciv.models.translations.tr
@ -521,11 +523,11 @@ object UniqueTriggerActivation {
UniqueType.OneTimeProvideResources -> {
val resourceName = unique.params[1]
val resource = ruleset.tileResources[resourceName] ?: return null
if (!resource.isStockpiled()) return null
if (!resource.isStockpiled) return null
return {
val amount = unique.params[0].toInt()
civInfo.resourceStockpiles.add(resourceName, amount)
civInfo.gainStockpiledResource(resourceName, amount)
val notificationText = getNotificationText(
notification, triggerNotificationText,
@ -540,11 +542,11 @@ object UniqueTriggerActivation {
UniqueType.OneTimeConsumeResources -> {
val resourceName = unique.params[1]
val resource = ruleset.tileResources[resourceName] ?: return null
if (!resource.isStockpiled()) return null
if (!resource.isStockpiled) return null
return {
val amount = unique.params[0].toInt()
civInfo.resourceStockpiles.add(resourceName, -amount)
civInfo.gainStockpiledResource(resourceName, -amount)
val notificationText = getNotificationText(
notification, triggerNotificationText,
@ -556,6 +558,25 @@ object UniqueTriggerActivation {
}
}
UniqueType.OneTimeGainResource -> {
val resourceName = unique.params[1]
val resource = Stat.safeValueOf(resourceName) ?:
SubStat.safeValueOf(resourceName) ?:
ruleset.tileResources[resourceName] ?: return null
if (resource is TileResource && !resource.isStockpiled) return null
return {
var amount = unique.params[0].toInt()
if (unique.isModifiedByGameSpeed()) {
if (resource is Stat) amount = (amount * civInfo.gameInfo.speed.statCostModifiers[resource]!!).roundToInt()
else amount = (amount * civInfo.gameInfo.speed.modifier).roundToInt()
}
city?.addGameResource(resource, amount) ?: civInfo.addGameResource(resource, amount)
true
}
}
UniqueType.UnitsGainPromotion -> {
val filter = unique.params[0]
val promotionName = unique.params[1]

View File

@ -809,6 +809,7 @@ enum class UniqueType(
OneTimeConsumeResources("Instantly consumes [positiveAmount] [stockpiledResource]", UniqueTarget.Triggerable),
OneTimeProvideResources("Instantly provides [positiveAmount] [stockpiledResource]", UniqueTarget.Triggerable),
OneTimeGainResource("Instantly gain [amount] [stockpile]", UniqueTarget.Triggerable, flags = setOf(UniqueFlag.AcceptsSpeedModifier)),
OneTimeGainStat("Gain [amount] [stat]", UniqueTarget.Triggerable, flags = setOf(UniqueFlag.AcceptsSpeedModifier)),
OneTimeGainStatRange("Gain [amount]-[amount] [stat]", UniqueTarget.Triggerable),
OneTimeGainPantheon("Gain enough Faith for a Pantheon", UniqueTarget.Triggerable),

View File

@ -170,7 +170,7 @@ class UniqueValidator(val ruleset: Ruleset) {
)
if (unique.type in resourceUniques && conditional.type in resourceConditionals
&& ruleset.tileResources[conditional.params.last()]?.hasUnique(UniqueType.CityResource) == true)
&& ruleset.tileResources[conditional.params.last()]?.isCityWide == true)
rulesetErrors.add(
"$prefix contains the conditional \"${conditional.text}\"," +
" which references a citywide resource. This is not a valid conditional for a resource uniques, " +

View File

@ -0,0 +1,4 @@
package com.unciv.models.stats
interface GameResource {
}

View File

@ -11,7 +11,7 @@ enum class Stat(
val purchaseSound: UncivSound,
val character: Char,
val color: Color
) {
) : GameResource {
Production(NotificationIcon.Production, UncivSound.Click, Fonts.production, colorFromHex(0xc14d00)),
Food(NotificationIcon.Food, UncivSound.Click, Fonts.food, colorFromHex(0x24A348)),
Gold(NotificationIcon.Gold, UncivSound.Coin, Fonts.gold, colorFromHex(0xffeb7f)),
@ -22,7 +22,7 @@ enum class Stat(
companion object {
val statsUsableToBuy = setOf(Gold, Food, Science, Culture, Faith)
private val valuesAsMap = values().associateBy { it.name }
private val valuesAsMap = entries.associateBy { it.name }
fun safeValueOf(name: String) = valuesAsMap[name]
fun isStat(name: String) = name in valuesAsMap
fun names() = valuesAsMap.keys

View File

@ -0,0 +1,20 @@
package com.unciv.models.stats
enum class SubStat : GameResource {
GoldenAgePoints,
TotalCulture,
StoredFood,
;
companion object {
val useableToBuy = setOf(GoldenAgePoints, StoredFood)
val civWideSubStats = setOf(GoldenAgePoints, TotalCulture)
fun safeValueOf(name: String): SubStat? {
return when (name) {
GoldenAgePoints.name -> GoldenAgePoints
TotalCulture.name -> TotalCulture
StoredFood.name -> StoredFood
else -> null
}
}
}
}

View File

@ -44,7 +44,7 @@ object BaseUnitDescriptions {
for ((resourceName, amount) in baseUnit.getResourceRequirementsPerTurn(city.civ.state)) {
val available = availableResources[resourceName] ?: 0
val resource = baseUnit.ruleset.tileResources[resourceName] ?: continue
val consumesString = resourceName.getConsumesAmountString(amount, resource.isStockpiled())
val consumesString = resourceName.getConsumesAmountString(amount, resource.isStockpiled)
lines += "$consumesString ({[$available] available})".tr()
}
var strengthLine = ""
@ -112,7 +112,7 @@ object BaseUnitDescriptions {
textList += FormattedLine()
val resource = ruleset.tileResources[baseUnit.requiredResource]
textList += FormattedLine(
baseUnit.requiredResource!!.getConsumesAmountString(1, resource!!.isStockpiled()),
baseUnit.requiredResource!!.getConsumesAmountString(1, resource!!.isStockpiled),
link="Resources/${baseUnit.requiredResource}", color="#F42")
}

View File

@ -50,7 +50,7 @@ object BuildingDescriptions {
for ((resourceName, amount) in getResourceRequirementsPerTurn(city.state)) {
val available = city.getAvailableResourceAmount(resourceName)
val resource = city.getRuleset().tileResources[resourceName] ?: continue
val consumesString = resourceName.getConsumesAmountString(amount, resource.isStockpiled())
val consumesString = resourceName.getConsumesAmountString(amount, resource.isStockpiled)
translatedLines += if (showAdditionalInfo) "$consumesString ({[$available] available})".tr()
else consumesString.tr()
@ -187,7 +187,7 @@ object BuildingDescriptions {
textList += FormattedLine()
val resource = ruleset.tileResources[requiredResource]
textList += FormattedLine(
requiredResource!!.getConsumesAmountString(1, resource!!.isStockpiled()),
requiredResource!!.getConsumesAmountString(1, resource!!.isStockpiled),
link="Resources/$requiredResource", color="#F42" )
}

View File

@ -332,7 +332,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
else construction.getResourceRequirementsPerTurn(city.state)
for ((resourceName, amount) in constructionResource) {
val resource = cityConstructions.city.getRuleset().tileResources[resourceName] ?: continue
text += "\n" + resourceName.getConsumesAmountString(amount, resource.isStockpiled()).tr()
text += "\n" + resourceName.getConsumesAmountString(amount, resource.isStockpiled).tr()
}
table.defaults().pad(2f).minWidth(40f)

View File

@ -198,7 +198,7 @@ class CityStatsTable(private val cityScreen: CityScreen) : Table() {
for (resourceSupply in CityResources.getCityResourcesAvailableToCity(city))
resourceCounter.add(resourceSupply.resource, resourceSupply.amount)
for ((resource, amount) in resourceCounter)
if (resource.hasUnique(UniqueType.CityResource)) {
if (resource.isCityWide) {
resourceTable.add(amount.toLabel())
resourceTable.add(ImageGetter.getResourcePortrait(resource.name, 20f))
.padRight(5f)

View File

@ -81,7 +81,7 @@ class ResourcesOverviewTab(
return tile.countAsUnimproved()
}
val amount = get(resource, origin)?.amount ?: return null
val label = if (resource.isStockpiled() && amount > 0) "+$amount".toLabel()
val label = if (resource.isStockpiled && amount > 0) "+$amount".toLabel()
else amount.toLabel()
if (origin == ExtraInfoOrigin.Unimproved.name)
label.onClick { overviewScreen.showOneTimeNotification(
@ -92,7 +92,7 @@ class ResourcesOverviewTab(
private fun ResourceSupplyList.getTotalLabel(resource: TileResource): Label {
val total = filter { it.resource == resource }.sumOf { it.amount }
return if (resource.isStockpiled() && total > 0) "+$total".toLabel()
return if (resource.isStockpiled && total > 0) "+$total".toLabel()
else total.toLabel()
}

View File

@ -62,7 +62,7 @@ internal class WorldScreenTopBarResources(topbar: WorldScreenTopBar) : ScalingTa
}
val strategicResources = worldScreen.gameInfo.ruleset.tileResources.values
.filter { it.resourceType == ResourceType.Strategic && !it.hasUnique(UniqueType.CityResource) }
.filter { it.resourceType == ResourceType.Strategic && !it.isCityWide }
for (resource in strategicResources) {
val resourceImage = ImageGetter.getResourcePortrait(resource.name, iconSize)
val resourceLabel = "0".toLabel()
@ -101,7 +101,7 @@ internal class WorldScreenTopBarResources(topbar: WorldScreenTopBar) : ScalingTa
resourcesWrapper.add(icon).padLeft(if (index == 0) 0f else extraPadBetweenResources)
if (!resource.isStockpiled())
if (!resource.isStockpiled)
label.setText(amount.tr())
else {
val perTurn = civResourceSupply.firstOrNull { it.resource == resource }?.amount ?: 0

View File

@ -116,7 +116,7 @@ object UnitActionModifiers {
val amount = conditional.params[0].toInt()
val resourceName = conditional.params[1]
if(unit.civ.getCivResourcesByName()[resourceName] != null)
unit.civ.resourceStockpiles.add(resourceName, -amount)
unit.civ.gainStockpiledResource(resourceName, -amount)
}
UniqueType.UnitActionRemovingPromotion -> {
val promotionName = conditional.params[0]