Resource stockpiles! (#9147)

* Resource stockpiles!

* toString extension including sign (+/-)

* Trigger uniques to provide/consume stockpiled resources

* Fixed build

* Display 'per turn' for stockpiled resources that are consumed per turn

* "Costs [amount] [resource]" works!

* Stockpile unique costs are displayed in construction button

* Added unique to prevert certain resources from being traded
This commit is contained in:
Yair Morgenstern 2023-04-09 18:01:26 +03:00 committed by GitHub
parent 0c60f87b27
commit adb51d9264
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 265 additions and 111 deletions

View File

@ -963,6 +963,8 @@ You may choose a free Policy =
You may choose [amount] free Policies =
You gain the [policy] Policy =
You enter a Golden Age =
You have gained [amount] [resourceName] =
You have lost [amount] [resourceName] =
## Trigger causes

View File

@ -591,7 +591,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
spaceResources.clear()
spaceResources.addAll(ruleset.buildings.values.filter { it.hasUnique(UniqueType.SpaceshipPart) }
.flatMap { it.getResourceRequirements().keys })
.flatMap { it.getResourceRequirementsPerTurn().keys })
spaceResources.addAll(ruleset.victories.values.flatMap { it.requiredSpaceshipParts })
barbarians.setTransients(this)

View File

@ -263,7 +263,7 @@ object Automation {
if (construction.name in civInfo.gameInfo.spaceResources)
return true
val requiredResources = construction.getResourceRequirements()
val requiredResources = construction.getResourceRequirementsPerTurn()
// Does it even require any resources?
if (requiredResources.isEmpty())
return true
@ -281,9 +281,9 @@ object Automation {
for (city in civInfo.cities) {
val otherConstruction = city.cityConstructions.getCurrentConstruction()
if (otherConstruction is Building)
futureForBuildings += otherConstruction.getResourceRequirements()[resource] ?: 0
futureForBuildings += otherConstruction.getResourceRequirementsPerTurn()[resource] ?: 0
else
futureForUnits += otherConstruction.getResourceRequirements()[resource] ?: 0
futureForUnits += otherConstruction.getResourceRequirementsPerTurn()[resource] ?: 0
}
// Make sure we have some for space

View File

@ -121,7 +121,7 @@ object UnitAutomation {
val upgradedUnit = unit.upgrade.getUnitToUpgradeTo()
if (!upgradedUnit.isBuildable(unit.civ)) return false // for resource reasons, usually
if (upgradedUnit.getResourceRequirements().keys.any { !unit.baseUnit.requiresResource(it) }) {
if (upgradedUnit.getResourceRequirementsPerTurn().keys.any { !unit.baseUnit.requiresResource(it) }) {
// The upgrade requires new resource types, so check if we are willing to invest them
if (!Automation.allowSpendingResource(unit.civ, upgradedUnit)) return false
}

View File

@ -818,7 +818,7 @@ object Battle {
var damageModifierFromMissingResource = 1f
val civResources = attacker.getCivInfo().getCivResourcesByName()
for (resource in attacker.unit.baseUnit.getResourceRequirements().keys) {
for (resource in attacker.unit.baseUnit.getResourceRequirementsPerTurn().keys) {
if (civResources[resource]!! < 0 && !attacker.getCivInfo().isBarbarian())
damageModifierFromMissingResource *= 0.5f // I could not find a source for this number, but this felt about right
}

View File

@ -72,7 +72,7 @@ object BattleDamage {
}
val civResources = civInfo.getCivResourcesByName()
for (resource in combatant.unit.baseUnit.getResourceRequirements().keys)
for (resource in combatant.unit.baseUnit.getResourceRequirementsPerTurn().keys)
if (civResources[resource]!! < 0 && !civInfo.isBarbarian())
modifiers["Missing resource"] = -25 //todo ModConstants

View File

@ -222,7 +222,7 @@ class City : IsPartOfGameInfoSerialization {
for (building in cityConstructions.getBuiltBuildings()) {
// Free buildings cost no resources
if (building.name in freeBuildings) continue
cityResources.subtractResourceRequirements(building.getResourceRequirements(), getRuleset(), "Buildings")
cityResources.subtractResourceRequirements(building.getResourceRequirementsPerTurn(), getRuleset(), "Buildings")
}
for (unique in getLocalMatchingUniques(UniqueType.ProvidesResources, StateForConditionals(civ, this))) { // E.G "Provides [1] [Iron]"

View File

@ -349,7 +349,19 @@ class CityConstructions : IsPartOfGameInfoSerialization {
constructionQueue.clear()
for (constructionName in queueSnapshot) {
if (getConstruction(constructionName).isBuildable(this))
val construction = getConstruction(constructionName)
// First construction will be built next turn, we need to make sure it has the correct resources
if (constructionQueue.isEmpty() && getWorkDone(constructionName) == 0) {
val costUniques = construction.getMatchingUniquesNotConflicting(UniqueType.CostsResources)
val civResources = city.civ.getCivResourcesByName()
if (costUniques.any {
val resourceName = it.params[1]
civResources[resourceName] == null
|| it.params[0].toInt() > civResources[resourceName]!! })
continue // Removes this construction from the queue
}
if (construction.isBuildable(this))
constructionQueue.add(constructionName)
}
}
@ -392,6 +404,14 @@ class CityConstructions : IsPartOfGameInfoSerialization {
}
private fun constructionBegun(construction: IConstruction) {
val costUniques = construction.getMatchingUniquesNotConflicting(UniqueType.CostsResources)
for (unique in costUniques){
val amount = unique.params[0].toInt()
val resourceName = unique.params[1]
city.civ.resourceStockpiles.add(resourceName, -amount)
}
if (construction !is Building) return
if (!construction.hasUnique(UniqueType.TriggersAlertOnStart)) return
val buildingIcon = "BuildingIcons/${construction.name}"

View File

@ -31,6 +31,7 @@ import com.unciv.logic.civilization.transients.CivInfoTransientCache
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
import com.unciv.logic.trade.TradeRequest
import com.unciv.models.Counter
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.Policy
import com.unciv.models.ruleset.Victory
@ -111,7 +112,7 @@ class Civilization : IsPartOfGameInfoSerialization {
var detailedCivResources = ResourceSupplyList()
@Transient
var summarizedCivResources = ResourceSupplyList()
var summarizedCivResourceSupply = ResourceSupplyList()
@Transient
val cityStateFunctions = CityStateFunctions(this)
@ -172,6 +173,8 @@ class Civilization : IsPartOfGameInfoSerialization {
/** See DiplomacyManager.flagsCountdown for why this does not map Enums to ints */
var flagsCountdown = HashMap<String, Int>()
var resourceStockpiles = Counter<String>()
/** Arraylist instead of HashMap as the same unique might appear multiple times
* We don't use pairs, as these cannot be serialized due to having no no-arg constructor
* We ALSO can't use a class inheriting from ArrayList<TemporaryUnique>() because ANNOYINGLY that doesn't pass deserialization
@ -288,6 +291,7 @@ class Civilization : IsPartOfGameInfoSerialization {
toReturn.attacksSinceTurnStart = attacksSinceTurnStart.copy()
toReturn.hasMovedAutomatedUnits = hasMovedAutomatedUnits
toReturn.statsHistory = statsHistory.clone()
toReturn.resourceStockpiles = resourceStockpiles.clone()
return toReturn
}
@ -376,12 +380,18 @@ class Civilization : IsPartOfGameInfoSerialization {
fun getHappiness() = stats.happiness
fun getCivResources(): ResourceSupplyList = summarizedCivResources
/** Note that for stockpiled resources, this gives by how much it grows per turn, not current amount */
fun getCivResourceSupply(): ResourceSupplyList = summarizedCivResourceSupply
// Preserves some origins for resources so we can separate them for trades
/** Preserves some origins for resources so we can separate them for trades
* Stockpiled uniques cannot be traded currently
*/
fun getCivResourcesWithOriginsForTrade(): ResourceSupplyList {
val newResourceSupplyList = ResourceSupplyList(keepZeroAmounts = true)
for (resourceSupply in detailedCivResources) {
if (resourceSupply.resource.isStockpiled()) continue
if (resourceSupply.resource.hasUnique(UniqueType.CannotBeTraded)) continue
// If we got it from another trade or from a CS, preserve the origin
if (resourceSupply.isCityStateOrTradeOrigin()) {
newResourceSupplyList.add(resourceSupply.copy())
@ -398,12 +408,16 @@ class Civilization : IsPartOfGameInfoSerialization {
/**
* Returns a dictionary of ALL resource names, and the amount that the civ has of each
* Stockpiled resources return the stockpiled amount
*/
fun getCivResourcesByName(): HashMap<String, Int> {
val hashMap = HashMap<String, Int>(gameInfo.ruleset.tileResources.size)
for (resource in gameInfo.ruleset.tileResources.keys) hashMap[resource] = 0
for (entry in getCivResources())
hashMap[entry.resource.name] = entry.amount
for (entry in getCivResourceSupply())
if (!entry.resource.isStockpiled())
hashMap[entry.resource.name] = entry.amount
for ((key, value) in resourceStockpiles)
hashMap[key] = value
return hashMap
}
@ -442,7 +456,7 @@ class Civilization : IsPartOfGameInfoSerialization {
yieldAll(religionManager.religion!!.getFounderUniques()
.filter { it.isOfType(uniqueType) && it.conditionalsApply(stateForConditionals) })
yieldAll(getCivResources().asSequence()
yieldAll(getCivResourceSupply().asSequence()
.filter { it.amount > 0 }
.flatMap { it.resource.getMatchingUniques(uniqueType, stateForConditionals) }
)

View File

@ -404,6 +404,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
val isResourceFilter: (TradeOffer) -> Boolean = {
(it.type == TradeType.Strategic_Resource || it.type == TradeType.Luxury_Resource)
&& resourcesMap.containsKey(it.name)
&& !resourcesMap[it.name]!!.isStockpiled()
}
for (trade in trades) {
for (offer in trade.ourOffers.filter(isResourceFilter))
@ -436,8 +437,8 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
// 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.getCivResources()
.filter { it.amount < 0 }.map { it.resource.name }
val negativeCivResources = civInfo.getCivResourceSupply()
.filter { it.amount < 0 && !it.resource.isStockpiled() }.map { it.resource.name }
for (offer in trade.ourOffers) {
if (offer.type in listOf(TradeType.Luxury_Resource, TradeType.Strategic_Resource)

View File

@ -36,6 +36,11 @@ class TurnManager(val civInfo: Civilization) {
if (civInfo.cities.isNotEmpty() && civInfo.gameInfo.ruleset.technologies.isNotEmpty())
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)
civInfo.civConstructions.startTurn()
civInfo.attacksSinceTurnStart.clear()
civInfo.updateStatsForNextTurn() // for things that change when turn passes e.g. golden age, city state influence
@ -70,8 +75,6 @@ class TurnManager(val civInfo: Civilization) {
unit.doAction()
} else civInfo.hasMovedAutomatedUnits = false
civInfo.cache.updateCivResources() // If you offered a trade last turn, this turn it will have been accepted/declined
for (tradeRequest in civInfo.tradeRequests.toList()) { // remove trade requests where one of the sides can no longer supply
val offeringCiv = civInfo.gameInfo.getCivilization(tradeRequest.requestingCiv)
if (offeringCiv.isDefeated() || !TradeEvaluation().isTradeValid(tradeRequest.trade, civInfo, offeringCiv)) {

View File

@ -98,7 +98,7 @@ class UnitManager(val civInfo:Civilization) {
// Not relevant when updating Tile transients, since some info of the civ itself isn't yet available,
// and in any case it'll be updated once civ info transients are
civInfo.updateStatsForNextTurn() // unit upkeep
if (mapUnit.baseUnit.getResourceRequirements().isNotEmpty())
if (mapUnit.baseUnit.getResourceRequirementsPerTurn().isNotEmpty())
civInfo.cache.updateCivResources()
}
}
@ -111,7 +111,7 @@ class UnitManager(val civInfo:Civilization) {
nextPotentiallyDueAt = 0
civInfo.updateStatsForNextTurn() // unit upkeep
if (mapUnit.baseUnit.getResourceRequirements().isNotEmpty())
if (mapUnit.baseUnit.getResourceRequirementsPerTurn().isNotEmpty())
civInfo.cache.updateCivResources()
}

View File

@ -230,10 +230,10 @@ class CivInfoStatsForNextTurn(val civInfo: Civilization) {
for (unique in civInfo.getMatchingUniques(UniqueType.BonusHappinessFromLuxury))
happinessPerUniqueLuxury += unique.params[0].toInt()
val ownedLuxuries = civInfo.getCivResources().map { it.resource }
val ownedLuxuries = civInfo.getCivResourceSupply().map { it.resource }
.filter { it.resourceType == ResourceType.Luxury }
val relevantLuxuries = civInfo.getCivResources().asSequence()
val relevantLuxuries = civInfo.getCivResourceSupply().asSequence()
.map { it.resource }
.count { it.resourceType == ResourceType.Luxury
&& it.getMatchingUniques(UniqueType.ObsoleteWith)
@ -245,7 +245,7 @@ class CivInfoStatsForNextTurn(val civInfo: Civilization) {
val luxuriesProvidedByCityStates = civInfo.getKnownCivs().asSequence()
.filter { it.isCityState() && it.getAllyCiv() == civInfo.civName }
.flatMap { it.getCivResources().map { res -> res.resource } }
.flatMap { it.getCivResourceSupply().map { res -> res.resource } }
.distinct()
.count { it.resourceType === ResourceType.Luxury && ownedLuxuries.contains(it) }

View File

@ -305,13 +305,13 @@ class CivInfoTransientCache(val civInfo: Civilization) {
for (unit in civInfo.units.getCivUnits())
newDetailedCivResources.subtractResourceRequirements(
unit.baseUnit.getResourceRequirements(), civInfo.gameInfo.ruleset, "Units")
unit.baseUnit.getResourceRequirementsPerTurn(), civInfo.gameInfo.ruleset, "Units")
// Check if anything has actually changed so we don't update stats for no reason - this uses List equality which means it checks the elements
if (civInfo.detailedCivResources == newDetailedCivResources) return
civInfo.detailedCivResources = newDetailedCivResources
civInfo.summarizedCivResources = newDetailedCivResources.sumByResource("All")
civInfo.summarizedCivResourceSupply = newDetailedCivResources.sumByResource("All")
civInfo.updateStatsForNextTurn() // More or less resources = more or less happiness, with potential domino effects
}

View File

@ -538,7 +538,7 @@ class TileMap : IsPartOfGameInfoSerialization {
// And update civ stats, since the new unit changes both unit upkeep and resource consumption
civInfo.updateStatsForNextTurn()
if (unit.baseUnit.getResourceRequirements().isNotEmpty())
if (unit.baseUnit.getResourceRequirementsPerTurn().isNotEmpty())
civInfo.cache.updateCivResources()
return unit

View File

@ -36,19 +36,20 @@ class TileInfoImprovementFunctions(val tile: Tile) {
yield(ImprovementBuildingProblem.NotJustOutsideBorders)
}
if (improvement.getMatchingUniques(UniqueType.OnlyAvailableWhen, StateForConditionals.IgnoreConditionals).any {
!it.conditionalsApply(stateForConditionals)
})
if (improvement.getMatchingUniques(UniqueType.OnlyAvailableWhen, StateForConditionals.IgnoreConditionals)
.any { !it.conditionalsApply(stateForConditionals) })
yield(ImprovementBuildingProblem.UnmetConditional)
if (improvement.getMatchingUniques(UniqueType.ObsoleteWith, stateForConditionals).any {
civInfo.tech.isResearched(it.params[0])
})
if (improvement.getMatchingUniques(UniqueType.ObsoleteWith, stateForConditionals)
.any { civInfo.tech.isResearched(it.params[0]) })
yield(ImprovementBuildingProblem.Obsolete)
if (improvement.getMatchingUniques(UniqueType.ConsumesResources, stateForConditionals).any {
civInfo.getCivResourcesByName()[it.params[1]]!! < it.params[0].toInt()
})
if (improvement.getMatchingUniques(UniqueType.ConsumesResources, stateForConditionals)
.any { civInfo.getCivResourcesByName()[it.params[1]]!! < it.params[0].toInt() })
yield(ImprovementBuildingProblem.MissingResources)
if (improvement.getMatchingUniques(UniqueType.CostsResources)
.any { civInfo.getCivResourcesByName()[it.params[1]]!! < it.params[0].toInt() })
yield(ImprovementBuildingProblem.MissingResources)
val knownFeatureRemovals = tile.ruleset.tileImprovements.values

View File

@ -118,9 +118,9 @@ class TradeEvaluation {
val amountToBuyInOffer = min(amountWillingToBuy, offer.amount)
val canUseForBuildings = civInfo.cities
.any { city -> city.cityConstructions.getBuildableBuildings().any { it.getResourceRequirements().containsKey(offer.name) } }
.any { city -> city.cityConstructions.getBuildableBuildings().any { it.getResourceRequirementsPerTurn().containsKey(offer.name) } }
val canUseForUnits = civInfo.cities
.any { city -> city.cityConstructions.getConstructableUnits().any { it.getResourceRequirements().containsKey(offer.name) } }
.any { city -> city.cityConstructions.getConstructableUnits().any { it.getResourceRequirementsPerTurn().containsKey(offer.name) } }
if (!canUseForBuildings && !canUseForUnits) return 0
return 50 * amountToBuyInOffer
@ -217,7 +217,7 @@ class TradeEvaluation {
if (!civInfo.isAtWar()) return 50 * offer.amount
val canUseForUnits = civInfo.gameInfo.ruleset.units.values
.any { it.getResourceRequirements().containsKey(offer.name)
.any { it.getResourceRequirementsPerTurn().containsKey(offer.name)
&& it.isBuildable(civInfo) }
if (!canUseForUnits) return 50 * offer.amount

View File

@ -122,12 +122,14 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
if (isNationalWonder) lines += "National Wonder"
if (!isFree) {
val availableResources = if (!showAdditionalInfo) emptyMap()
else city.civ.getCivResources().associate { it.resource.name to it.amount }
for ((resource, amount) in getResourceRequirements()) {
val available = availableResources[resource] ?: 0
lines += if (showAdditionalInfo)
"{${resource.getConsumesAmountString(amount)}} ({[$available] available})"
else resource.getConsumesAmountString(amount)
else city.civ.getCivResourcesByName()
for ((resourceName, amount) in getResourceRequirementsPerTurn()) {
val available = availableResources[resourceName] ?: 0
val resource = city.getRuleset().tileResources[resourceName] ?: continue
val consumesString = resourceName.getConsumesAmountString(amount, resource.isStockpiled())
lines += if (showAdditionalInfo) "$consumesString ({[$available] available})"
else consumesString
}
}
@ -254,14 +256,14 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
textList += FormattedLine("Requires [$requiredBuilding] to be built in the city",
link="Building/$requiredBuilding")
val resourceRequirements = getResourceRequirements()
val resourceRequirements = getResourceRequirementsPerTurn()
if (resourceRequirements.isNotEmpty()) {
textList += FormattedLine()
for ((resource, amount) in resourceRequirements) {
for ((resourceName, amount) in resourceRequirements) {
val resource = ruleset.tileResources[resourceName] ?: continue
textList += FormattedLine(
// the 1 variant should deprecate some time
resource.getConsumesAmountString(amount),
link="Resources/$resource", color="#F42" )
resourceName.getConsumesAmountString(amount, resource.isStockpiled()),
link="Resources/$resourceName", color="#F42" )
}
}
@ -615,7 +617,7 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
yield(RejectionReasonType.RequiresBuildingInThisCity.toInstance("Requires a [${civ.getEquivalentBuilding(requiredBuilding!!)}] in this city"))
}
for ((resource, requiredAmount) in getResourceRequirements()) {
for ((resource, requiredAmount) in getResourceRequirementsPerTurn()) {
val availableAmount = civ.getCivResourcesByName()[resource]!!
if (availableAmount < requiredAmount) {
yield(RejectionReasonType.ConsumesResources.toInstance(resource.getNeedMoreAmountString(requiredAmount - availableAmount)))
@ -740,7 +742,7 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
fun isSellable() = !isAnyWonder() && !hasUnique(UniqueType.Unsellable)
override fun getResourceRequirements(): HashMap<String, Int> = resourceRequirementsInternal
override fun getResourceRequirementsPerTurn(): HashMap<String, Int> = resourceRequirementsInternal
private val resourceRequirementsInternal: HashMap<String, Int> by lazy {
val resourceRequirements = HashMap<String, Int>()
@ -756,6 +758,9 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
for (unique in getMatchingUniques(UniqueType.ConsumesResources)) {
if (unique.params[1] == resource) return true
}
for (unique in getMatchingUniques(UniqueType.CostsResources)) {
if (unique.params[1] == resource) return true
}
return false
}
}

View File

@ -3,6 +3,7 @@ package com.unciv.logic.city
import com.unciv.logic.civilization.Civilization
import com.unciv.models.ruleset.unique.IHasUniques
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.stats.INamed
import com.unciv.models.stats.Stat
@ -14,8 +15,11 @@ import kotlin.math.roundToInt
interface IConstruction : INamed {
fun isBuildable(cityConstructions: CityConstructions): Boolean
fun shouldBeDisplayed(cityConstructions: CityConstructions): Boolean
fun getResourceRequirements(): HashMap<String,Int>
/** Gets *per turn* resource requirements - does not include immediate costs for stockpiled resources */
fun getResourceRequirementsPerTurn(): HashMap<String,Int>
fun requiresResource(resource: String): Boolean
/** We can't call this getMatchingUniques because then it would conflict with IHasUniques */
fun getMatchingUniquesNotConflicting(uniqueType: UniqueType) = sequenceOf<Unique>()
}
interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
@ -82,6 +86,9 @@ interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
fun getCostForConstructionsIncreasingInPrice(baseCost: Int, increaseCost: Int, previouslyBought: Int): Int {
return (baseCost + increaseCost / 2f * ( previouslyBought * previouslyBought + previouslyBought )).toInt()
}
override fun getMatchingUniquesNotConflicting(uniqueType: UniqueType): Sequence<Unique> =
getMatchingUniques(uniqueType)
}
@ -210,7 +217,7 @@ open class PerpetualConstruction(override var name: String, val description: Str
override fun isBuildable(cityConstructions: CityConstructions): Boolean =
throw Exception("Impossible!")
override fun getResourceRequirements(): HashMap<String, Int> = hashMapOf()
override fun getResourceRequirementsPerTurn(): HashMap<String, Int> = hashMapOf()
override fun requiresResource(resource: String) = false

View File

@ -157,7 +157,7 @@ class RulesetValidator(val ruleset: Ruleset) {
)
}
for (resource in unit.getResourceRequirements().keys)
for (resource in unit.getResourceRequirementsPerTurn().keys)
if (!ruleset.tileResources.containsKey(resource))
lines += "${unit.name} requires resource $resource which does not exist!"
if (unit.replaces != null && !ruleset.units.containsKey(unit.replaces!!))
@ -189,7 +189,7 @@ class RulesetValidator(val ruleset: Ruleset) {
for (specialistName in building.specialistSlots.keys)
if (!ruleset.specialists.containsKey(specialistName))
lines += "${building.name} provides specialist $specialistName which does not exist!"
for (resource in building.getResourceRequirements().keys)
for (resource in building.getResourceRequirementsPerTurn().keys)
if (!ruleset.tileResources.containsKey(resource))
lines += "${building.name} requires resource $resource which does not exist!"
if (building.replaces != null && !ruleset.buildings.containsKey(building.replaces!!))

View File

@ -236,8 +236,8 @@ class Nation : RulesetObject() {
yield(FormattedLine("${Fonts.range} " + "[${unit.range}] vs [${originalUnit.range}]".tr(), indent=1))
if (unit.movement != originalUnit.movement)
yield(FormattedLine("${Fonts.movement} " + "[${unit.movement}] vs [${originalUnit.movement}]".tr(), indent=1))
for (resource in originalUnit.getResourceRequirements().keys)
if (!unit.getResourceRequirements().containsKey(resource)) {
for (resource in originalUnit.getResourceRequirementsPerTurn().keys)
if (!unit.getResourceRequirementsPerTurn().containsKey(resource)) {
yield(FormattedLine("[$resource] not required", link="Resource/$resource", indent=1))
}
// This does not use the auto-linking FormattedLine(Unique) for two reasons:

View File

@ -62,7 +62,7 @@ class ResourceSupplyList(
add(resourceSupply)
}
/** Add entries from a requirements list (as produced by [IConstruction.getResourceRequirements]), expressing requirement as negative supply. */
/** Add entries from a requirements list (as produced by [IConstruction.getResourceRequirementsPerTurn]), expressing requirement as negative supply. */
fun subtractResourceRequirements(resourceRequirements: HashMap<String, Int>, ruleset: Ruleset, origin: String) {
for ((resourceName, amount) in resourceRequirements) {
val resource = ruleset.tileResources[resourceName] ?: continue

View File

@ -15,6 +15,7 @@ class TileResource : RulesetStatsObject() {
var resourceType: ResourceType = ResourceType.Bonus
var terrainsCanBeFoundOn: List<String> = listOf()
var improvement: String? = null
/** stats that this resource adds to a tile */
var improvementStats: Stats? = null
var revealedBy: String? = null
var improvedBy: List<String> = listOf()
@ -91,7 +92,7 @@ class TileResource : RulesetStatsObject() {
}
}
val buildingsThatConsumeThis = ruleset.buildings.values.filter { it.getResourceRequirements().containsKey(name) }
val buildingsThatConsumeThis = ruleset.buildings.values.filter { it.getResourceRequirementsPerTurn().containsKey(name) }
if (buildingsThatConsumeThis.isNotEmpty()) {
textList += FormattedLine()
textList += FormattedLine("{Buildings that consume this resource}:")
@ -100,7 +101,7 @@ class TileResource : RulesetStatsObject() {
}
}
val unitsThatConsumeThis = ruleset.units.values.filter { it.getResourceRequirements().containsKey(name) }
val unitsThatConsumeThis = ruleset.units.values.filter { it.getResourceRequirementsPerTurn().containsKey(name) }
if (unitsThatConsumeThis.isNotEmpty()) {
textList += FormattedLine()
textList += FormattedLine("{Units that consume this resource}: ")
@ -135,6 +136,8 @@ class TileResource : RulesetStatsObject() {
}
}
fun isStockpiled() = hasUnique(UniqueType.Stockpiled)
class DepositAmount {
var sparse: Int = 1
var default: Int = 2

View File

@ -330,6 +330,38 @@ object UniqueTriggerActivation {
return true
}
UniqueType.OneTimeProvideResources -> {
val amount = unique.params[0].toInt()
val resourceName = unique.params[1]
val resource = ruleSet.tileResources[resourceName] ?: return false
if (!resource.isStockpiled()) return false
civInfo.resourceStockpiles.add(resourceName, amount)
val notificationText = getNotificationText(notification, triggerNotificationText,
"You have gained [$amount] [$resourceName]")
?: return true
civInfo.addNotification(notificationText, NotificationCategory.General, NotificationIcon.Science, "ResourceIcons/$resourceName")
return true
}
UniqueType.OneTimeConsumeResources -> {
val amount = unique.params[0].toInt()
val resourceName = unique.params[1]
val resource = ruleSet.tileResources[resourceName] ?: return false
if (!resource.isStockpiled()) return false
civInfo.resourceStockpiles.add(resourceName, amount)
val notificationText = getNotificationText(notification, triggerNotificationText,
"You have lost [$amount] [$resourceName]")
?: return true
civInfo.addNotification(notificationText, NotificationCategory.General, NotificationIcon.Science, "ResourceIcons/$resourceName")
return true
}
UniqueType.OneTimeRevealEntireMap -> {
if (notification != null) {
civInfo.addNotification(notification, LocationAction(tile?.position), NotificationCategory.General, NotificationIcon.Scout)
@ -366,6 +398,21 @@ object UniqueTriggerActivation {
return promotedUnitLocations.isNotEmpty()
}
/**
* The mechanics for granting great people are wonky, but basically the following happens:
* Based on the game speed, a timer with some amount of turns is set, 40 on regular speed
* Every turn, 1 is subtracted from this timer, as long as you have at least 1 city state ally
* So no, the number of city-state allies does not matter for this. You have a global timer for all of them combined.
* If the timer reaches the amount of city-state allies you have (or 10, whichever is lower), it is reset.
* You will then receive a random great person from a random city-state you are allied to
* The very first time after acquiring this policy, the timer is set to half of its normal value
* This is the basics, and apart from this, there is some randomness in the exact turn count, but I don't know how much
* There is surprisingly little information findable online about this policy, and the civ 5 source files are
* also quite though to search through, so this might all be incorrect.
* For now this mechanic seems decent enough that this is fine.
* Note that the way this is implemented now, this unique does NOT stack
* I could parametrize the [Allied], but eh.
*/
UniqueType.CityStateCanGiftGreatPeople -> {
civInfo.addFlag(
CivFlags.CityStateGreatPersonGift.name,
@ -376,21 +423,6 @@ object UniqueTriggerActivation {
}
return true
}
// The mechanics for granting great people are wonky, but basically the following happens:
// Based on the game speed, a timer with some amount of turns is set, 40 on regular speed
// Every turn, 1 is subtracted from this timer, as long as you have at least 1 city state ally
// So no, the number of city-state allies does not matter for this. You have a global timer for all of them combined.
// If the timer reaches the amount of city-state allies you have (or 10, whichever is lower), it is reset.
// You will then receive a random great person from a random city-state you are allied to
// The very first time after acquiring this policy, the timer is set to half of its normal value
// This is the basics, and apart from this, there is some randomness in the exact turn count, but I don't know how much
// There is surprisingly little information findable online about this policy, and the civ 5 source files are
// also quite though to search through, so this might all be incorrect.
// For now this mechanic seems decent enough that this is fine.
// Note that the way this is implemented now, this unique does NOT stack
// I could parametrize the [Allied], but eh.
UniqueType.OneTimeGainStat -> {
val stat = Stat.safeValueOf(unique.params[1]) ?: return false
@ -414,6 +446,7 @@ object UniqueTriggerActivation {
civInfo.addNotification(notificationText, LocationAction(tile?.position), NotificationCategory.General, stat.notificationIcon)
return true
}
UniqueType.OneTimeGainStatRange -> {
val stat = Stat.safeValueOf(unique.params[2]) ?: return false

View File

@ -155,6 +155,9 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
ConsumesResources("Consumes [amount] [resource]", UniqueTarget.Improvement, UniqueTarget.Building, UniqueTarget.Unit),
ProvidesResources("Provides [amount] [resource]", UniqueTarget.Improvement, UniqueTarget.Global),
/** For stockpiled resources */
CostsResources("Costs [amount] [resource]", UniqueTarget.Improvement, UniqueTarget.Building, UniqueTarget.Unit),
GrowthPercentBonus("[relativeAmount]% growth [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
CarryOverFood("[relativeAmount]% Food is carried over after population increases [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
@ -579,6 +582,9 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
/////// Resource uniques
ResourceAmountOnTiles("Deposits in [tileFilter] tiles always provide [amount] resources", UniqueTarget.Resource),
CityStateOnlyResource("Can only be created by Mercantile City-States", UniqueTarget.Resource),
Stockpiled("Stockpiled", UniqueTarget.Resource),
CannotBeTraded("Cannot be traded", UniqueTarget.Resource),
NotShownOnWorldScreen("Not shown on world screen", UniqueTarget.Resource, flags = UniqueFlag.setOfHiddenToUsers),
ResourceWeighting("Generated with weight [amount]", UniqueTarget.Resource, flags = UniqueFlag.setOfHiddenToUsers),
MinorDepositWeighting("Minor deposits generated with weight [amount]", UniqueTarget.Resource, flags = UniqueFlag.setOfHiddenToUsers),
@ -724,7 +730,12 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
OneTimeFreeBelief("Gain a free [beliefType] belief", UniqueTarget.Triggerable),
OneTimeTriggerVoting("Triggers voting for the Diplomatic Victory", UniqueTarget.Triggerable), // used in Building
OneTimeGainStat("Gain [amount] [stat]", UniqueTarget.Triggerable),
/** For stockpiled resources */
OneTimeConsumeResources("Instantly consumes [amount] [resource]", UniqueTarget.Triggerable),
/** For stockpiled resources */
OneTimeProvideResources("Instantly provides [amount] [resource]", UniqueTarget.Triggerable),
OneTimeGainStat("Gain [amount] [stat/resource]", UniqueTarget.Triggerable),
OneTimeGainStatRange("Gain [amount]-[amount] [stat]", UniqueTarget.Triggerable),
OneTimeGainPantheon("Gain enough Faith for a Pantheon", UniqueTarget.Triggerable),
OneTimeGainProphet("Gain enough Faith for [amount]% of a Great Prophet", UniqueTarget.Triggerable),

View File

@ -175,7 +175,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
}
if (!civ.isBarbarian()) { // Barbarians don't need resources
for ((resource, requiredAmount) in getResourceRequirements()) {
for ((resource, requiredAmount) in getResourceRequirementsPerTurn()) {
val availableAmount = civ.getCivResourcesByName()[resource]!!
if (availableAmount < requiredAmount) {
result.add(
@ -317,7 +317,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
fun movesLikeAirUnits() = type.getMovementType() == UnitMovementType.Air
/** Returns resource requirements from both uniques and requiredResource field */
override fun getResourceRequirements(): HashMap<String, Int> = resourceRequirementsInternal
override fun getResourceRequirementsPerTurn(): HashMap<String, Int> = resourceRequirementsInternal
private val resourceRequirementsInternal: HashMap<String, Int> by lazy {
val resourceRequirements = HashMap<String, Int>()
@ -327,7 +327,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
resourceRequirements
}
override fun requiresResource(resource: String) = getResourceRequirements().containsKey(resource)
override fun requiresResource(resource: String) = getResourceRequirementsPerTurn().containsKey(resource)
fun isRanged() = rangedStrength > 0
fun isMelee() = !isRanged() && strength > 0

View File

@ -2,6 +2,7 @@ package com.unciv.ui.components.extensions
import com.badlogic.gdx.math.Vector2
import com.unciv.models.translations.tr
import com.unciv.ui.components.Fonts
import java.text.SimpleDateFormat
import java.time.Duration
import java.time.temporal.ChronoUnit
@ -17,7 +18,11 @@ fun Int.toPercent() = toFloat().toPercent()
fun Float.toPercent() = 1 + this/100
/** Convert a [resource name][this] into "Consumes [amount] $resource" string (untranslated) */
fun String.getConsumesAmountString(amount: Int) = "Consumes [$amount] [$this]"
fun String.getConsumesAmountString(amount: Int, isStockpiled:Boolean): String {
val uniqueString = "{Consumes [$amount] [$this]}"
if (!isStockpiled) return uniqueString
else return "$uniqueString /${Fonts.turn}"
}
/** Convert a [resource name][this] into "Need [amount] more $resource" string (untranslated) */
fun String.getNeedMoreAmountString(amount: Int) = "Need [$amount] more [$this]"
@ -34,7 +39,7 @@ fun Duration.format(): String {
if (firstPartAlreadyAdded) {
sb.append(", ")
}
sb.append("[${part}] $unit")
sb.append("[$part] $unit")
firstPartAlreadyAdded = true
}
return sb.toString()

View File

@ -9,10 +9,10 @@ import com.unciv.models.ruleset.unit.UnitMovementType
import com.unciv.models.ruleset.unit.UnitType
import com.unciv.models.stats.Stat
import com.unciv.models.translations.tr
import com.unciv.ui.screens.civilopediascreen.FormattedLine
import com.unciv.ui.components.Fonts
import com.unciv.ui.components.extensions.getConsumesAmountString
import com.unciv.ui.components.extensions.toPercent
import com.unciv.ui.screens.civilopediascreen.FormattedLine
import kotlin.math.pow
object BaseUnitDescriptions {
@ -35,10 +35,12 @@ object BaseUnitDescriptions {
* @param city Supplies civInfo to show available resources after resource requirements */
fun getDescription(baseUnit: BaseUnit, city: City): String {
val lines = mutableListOf<String>()
val availableResources = city.civ.getCivResources().associate { it.resource.name to it.amount }
for ((resource, amount) in baseUnit.getResourceRequirements()) {
val available = availableResources[resource] ?: 0
lines += "{${resource.getConsumesAmountString(amount)}} ({[$available] available})".tr()
val availableResources = city.civ.getCivResourcesByName()
for ((resourceName, amount) in baseUnit.getResourceRequirementsPerTurn()) {
val available = availableResources[resourceName] ?: 0
val resource = baseUnit.ruleset.tileResources[resourceName] ?: continue
val consumesString = resourceName.getConsumesAmountString(amount, resource.isStockpiled())
lines += "$consumesString ({[$available] available})".tr()
}
var strengthLine = ""
if (baseUnit.strength != 0) {
@ -90,7 +92,7 @@ object BaseUnitDescriptions {
val buyCost = (30.0 * baseUnit.cost.toFloat().pow(0.75f) * baseUnit.hurryCostModifier.toPercent()).toInt() / 10 * 10
stats += "$buyCost${Fonts.gold}"
}
textList += FormattedLine(stats.joinToString(", ", "{Cost}: "))
textList += FormattedLine(stats.joinToString("/", "{Cost}: "))
}
if (baseUnit.replacementTextForUniques.isNotEmpty()) {
@ -105,12 +107,13 @@ object BaseUnitDescriptions {
}
}
val resourceRequirements = baseUnit.getResourceRequirements()
val resourceRequirements = baseUnit.getResourceRequirementsPerTurn()
if (resourceRequirements.isNotEmpty()) {
textList += FormattedLine()
for ((resource, amount) in resourceRequirements) {
for ((resourceName, amount) in resourceRequirements) {
val resource = ruleset.tileResources[resourceName] ?: continue
textList += FormattedLine(
resource.getConsumesAmountString(amount),
resourceName.getConsumesAmountString(amount, resource.isStockpiled()),
link = "Resource/$resource", color = "#F42"
)
}

View File

@ -202,7 +202,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
val useStoredProduction = entry is Building || !cityConstructions.isBeingConstructedOrEnqueued(entry.name)
val buttonText = cityConstructions.getTurnsToConstructionString(entry.name, useStoredProduction).trim()
val resourcesRequired = entry.getResourceRequirements()
val resourcesRequired = entry.getResourceRequirementsPerTurn()
val mostImportantRejection =
entry.getRejectionReasons(cityConstructions)
.filter { it.isImportantRejection() }
@ -317,9 +317,11 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
if (constructionName in PerpetualConstruction.perpetualConstructionsMap) "\n"
else cityConstructions.getTurnsToConstructionString(constructionName, isFirstConstructionOfItsKind)
val constructionResource = cityConstructions.getConstruction(constructionName).getResourceRequirements()
for ((resource, amount) in constructionResource)
text += "\n" + resource.getConsumesAmountString(amount).tr()
val constructionResource = cityConstructions.getConstruction(constructionName).getResourceRequirementsPerTurn()
for ((resourceName, amount) in constructionResource) {
val resource = cityConstructions.city.getRuleset().tileResources[resourceName] ?: continue
text += "\n" + resourceName.getConsumesAmountString(amount, resource.isStockpiled()).tr()
}
table.defaults().pad(2f).minWidth(40f)
if (isFirstConstructionOfItsKind) table.add(getProgressBar(constructionName)).minWidth(5f)
@ -396,6 +398,12 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
resourceTable.add(ImageGetter.getResourcePortrait(resource, 15f)).padBottom(1f)
}
}
for (unique in constructionButtonDTO.construction.getMatchingUniquesNotConflicting(UniqueType.CostsResources)){
val color = if (constructionButtonDTO.rejectionReason?.type == RejectionReasonType.ConsumesResources)
Color.RED else Color.WHITE
resourceTable.add(unique.params[0].toLabel(fontColor = color)).expandX().left().padLeft(5f)
resourceTable.add(ImageGetter.getResourcePortrait(unique.params[1], 15f)).padBottom(1f)
}
constructionTable.add(resourceTable).expandX().left()
pickConstructionButton.add(constructionTable).expandX().left()

View File

@ -11,10 +11,6 @@ import com.unciv.models.ruleset.tile.ResourceSupplyList
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.translations.tr
import com.unciv.ui.screens.civilopediascreen.CivilopediaCategories
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.addSeparatorVertical
@ -22,6 +18,10 @@ import com.unciv.ui.components.extensions.onClick
import com.unciv.ui.components.extensions.pad
import com.unciv.ui.components.extensions.surroundWithCircle
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.basescreen.BaseScreen
import com.unciv.ui.screens.civilopediascreen.CivilopediaCategories
import com.unciv.ui.screens.civilopediascreen.CivilopediaScreen
class ResourcesOverviewTab(
@ -73,10 +73,16 @@ class ResourcesOverviewTab(
private val extraOrigins: List<ExtraInfoOrigin> = extraDrilldown.asSequence()
.mapNotNull { ExtraInfoOrigin.safeValueOf(it.origin) }.distinct().toList()
private fun ResourceSupplyList.getLabel(resource: TileResource, origin: String): Label? =
get(resource, origin)?.amount?.toLabel()
private fun ResourceSupplyList.getTotalLabel(resource: TileResource): Label =
filter { it.resource == resource }.sumOf { it.amount }.toLabel()
private fun ResourceSupplyList.getLabel(resource: TileResource, origin: String): Label? {
val amount = get(resource, origin)?.amount ?: return null
return if (resource.isStockpiled() && amount > 0) "+$amount".toLabel()
else amount.toLabel()
}
private fun ResourceSupplyList.getTotalLabel(resource: TileResource): Label {
val total = filter { it.resource == resource }.sumOf { it.amount }
return if (resource.isStockpiled() && total > 0) "+$total".toLabel()
else total.toLabel()
}
private fun getResourceImage(name: String) =
ImageGetter.getResourcePortrait(name, iconSize).apply {
onClick {

View File

@ -12,6 +12,7 @@ import com.badlogic.gdx.utils.Align
import com.unciv.logic.civilization.Civilization
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stats
import com.unciv.models.translations.tr
import com.unciv.ui.components.Fonts
@ -24,6 +25,7 @@ import com.unciv.ui.components.extensions.onClick
import com.unciv.ui.components.extensions.setFontColor
import com.unciv.ui.components.extensions.setFontSize
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toStringSigned
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.popups
@ -347,14 +349,23 @@ class WorldScreenTopBar(val worldScreen: WorldScreen) : Table() {
turnsLabel.setText(Fonts.turn + "" + civInfo.gameInfo.turns + " | " + yearText)
resourcesWrapper.clearChildren()
var firstPadLeft = 20f // We want a distance from the turns entry to the first resource, but only if any resource is displayed
val civResources = civInfo.getCivResources()
val civResources = civInfo.getCivResourcesByName()
val civResourceSupply = civInfo.getCivResourceSupply()
for ((resource, label, icon) in resourceActors) {
if (resource.revealedBy != null && !civInfo.tech.isResearched(resource.revealedBy!!))
continue
if (resource.hasUnique(UniqueType.NotShownOnWorldScreen)) continue
resourcesWrapper.add(icon).padLeft(firstPadLeft).padRight(0f)
firstPadLeft = 5f
val amount = civResources.get(resource, "All")?.amount ?: 0
label.setText(amount)
val amount = civResources[resource.name] ?: 0
if (!resource.isStockpiled())
label.setText(amount)
else {
val perTurn = civResourceSupply.firstOrNull { it.resource == resource }?.amount ?: 0
if (perTurn == 0) label.setText(amount)
else label.setText("$amount (${perTurn.toStringSigned()})")
}
resourcesWrapper.add(label).padTop(8f) // digits don't have descenders, so push them down a little
}

View File

@ -336,9 +336,9 @@ object UnitActions {
// Check _new_ resource requirements
// Using Counter to aggregate is a bit exaggerated, but - respect the mad modder.
val resourceRequirementsDelta = Counter<String>()
for ((resource, amount) in unit.baseUnit().getResourceRequirements())
for ((resource, amount) in unit.baseUnit().getResourceRequirementsPerTurn())
resourceRequirementsDelta.add(resource, -amount)
for ((resource, amount) in upgradedUnit.getResourceRequirements())
for ((resource, amount) in upgradedUnit.getResourceRequirementsPerTurn())
resourceRequirementsDelta.add(resource, amount)
val newResourceRequirementsString = resourceRequirementsDelta.entries
.filter { it.value > 0 }

View File

@ -42,9 +42,9 @@ object UnitActionsUpgrade{
// Check _new_ resource requirements (display only - yes even for free or special upgrades)
// Using Counter to aggregate is a bit exaggerated, but - respect the mad modder.
val resourceRequirementsDelta = Counter<String>()
for ((resource, amount) in unit.baseUnit().getResourceRequirements())
for ((resource, amount) in unit.baseUnit().getResourceRequirementsPerTurn())
resourceRequirementsDelta.add(resource, -amount)
for ((resource, amount) in upgradedUnit.getResourceRequirements())
for ((resource, amount) in upgradedUnit.getResourceRequirementsPerTurn())
resourceRequirementsDelta.add(resource, amount)
val newResourceRequirementsString = resourceRequirementsDelta.entries
.filter { it.value > 0 }

View File

@ -74,7 +74,17 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
??? example "Triggers voting for the Diplomatic Victory"
Applicable to: Triggerable
??? example "Gain [amount] [stat]"
??? example "Instantly consumes [amount] [resource]"
Example: "Instantly consumes [3] [Iron]"
Applicable to: Triggerable
??? example "Instantly provides [amount] [resource]"
Example: "Instantly provides [3] [Iron]"
Applicable to: Triggerable
??? example "Gain [amount] [stat/resource]"
Example: "Gain [3] [Culture]"
Applicable to: Triggerable
@ -914,6 +924,11 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
Applicable to: Building, Unit, Improvement
??? example "Costs [amount] [resource]"
Example: "Costs [3] [Iron]"
Applicable to: Building, Unit, Improvement
??? example "Unbuildable"
Applicable to: Building, Unit, Improvement
@ -1601,6 +1616,12 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
??? example "Can only be created by Mercantile City-States"
Applicable to: Resource
??? example "Stockpiled"
Applicable to: Resource
??? example "Not shown on world screen"
Applicable to: Resource
??? example "Generated with weight [amount]"
Example: "Generated with weight [3]"