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 may choose [amount] free Policies =
You gain the [policy] Policy = You gain the [policy] Policy =
You enter a Golden Age = You enter a Golden Age =
You have gained [amount] [resourceName] =
You have lost [amount] [resourceName] =
## Trigger causes ## Trigger causes

View File

@ -591,7 +591,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
spaceResources.clear() spaceResources.clear()
spaceResources.addAll(ruleset.buildings.values.filter { it.hasUnique(UniqueType.SpaceshipPart) } 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 }) spaceResources.addAll(ruleset.victories.values.flatMap { it.requiredSpaceshipParts })
barbarians.setTransients(this) barbarians.setTransients(this)

View File

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

View File

@ -121,7 +121,7 @@ object UnitAutomation {
val upgradedUnit = unit.upgrade.getUnitToUpgradeTo() val upgradedUnit = unit.upgrade.getUnitToUpgradeTo()
if (!upgradedUnit.isBuildable(unit.civ)) return false // for resource reasons, usually 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 // The upgrade requires new resource types, so check if we are willing to invest them
if (!Automation.allowSpendingResource(unit.civ, upgradedUnit)) return false if (!Automation.allowSpendingResource(unit.civ, upgradedUnit)) return false
} }

View File

@ -818,7 +818,7 @@ object Battle {
var damageModifierFromMissingResource = 1f var damageModifierFromMissingResource = 1f
val civResources = attacker.getCivInfo().getCivResourcesByName() 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()) if (civResources[resource]!! < 0 && !attacker.getCivInfo().isBarbarian())
damageModifierFromMissingResource *= 0.5f // I could not find a source for this number, but this felt about right 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() 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()) if (civResources[resource]!! < 0 && !civInfo.isBarbarian())
modifiers["Missing resource"] = -25 //todo ModConstants modifiers["Missing resource"] = -25 //todo ModConstants

View File

@ -222,7 +222,7 @@ class City : IsPartOfGameInfoSerialization {
for (building in cityConstructions.getBuiltBuildings()) { for (building in cityConstructions.getBuiltBuildings()) {
// Free buildings cost no resources // Free buildings cost no resources
if (building.name in freeBuildings) continue 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]" 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() constructionQueue.clear()
for (constructionName in queueSnapshot) { 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) constructionQueue.add(constructionName)
} }
} }
@ -392,6 +404,14 @@ class CityConstructions : IsPartOfGameInfoSerialization {
} }
private fun constructionBegun(construction: IConstruction) { 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 !is Building) return
if (!construction.hasUnique(UniqueType.TriggersAlertOnStart)) return if (!construction.hasUnique(UniqueType.TriggersAlertOnStart)) return
val buildingIcon = "BuildingIcons/${construction.name}" 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.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile import com.unciv.logic.map.tile.Tile
import com.unciv.logic.trade.TradeRequest import com.unciv.logic.trade.TradeRequest
import com.unciv.models.Counter
import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.Policy import com.unciv.models.ruleset.Policy
import com.unciv.models.ruleset.Victory import com.unciv.models.ruleset.Victory
@ -111,7 +112,7 @@ class Civilization : IsPartOfGameInfoSerialization {
var detailedCivResources = ResourceSupplyList() var detailedCivResources = ResourceSupplyList()
@Transient @Transient
var summarizedCivResources = ResourceSupplyList() var summarizedCivResourceSupply = ResourceSupplyList()
@Transient @Transient
val cityStateFunctions = CityStateFunctions(this) val cityStateFunctions = CityStateFunctions(this)
@ -172,6 +173,8 @@ class Civilization : IsPartOfGameInfoSerialization {
/** See DiplomacyManager.flagsCountdown for why this does not map Enums to ints */ /** See DiplomacyManager.flagsCountdown for why this does not map Enums to ints */
var flagsCountdown = HashMap<String, Int>() var flagsCountdown = HashMap<String, Int>()
var resourceStockpiles = Counter<String>()
/** Arraylist instead of HashMap as the same unique might appear multiple times /** 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 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 * 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.attacksSinceTurnStart = attacksSinceTurnStart.copy()
toReturn.hasMovedAutomatedUnits = hasMovedAutomatedUnits toReturn.hasMovedAutomatedUnits = hasMovedAutomatedUnits
toReturn.statsHistory = statsHistory.clone() toReturn.statsHistory = statsHistory.clone()
toReturn.resourceStockpiles = resourceStockpiles.clone()
return toReturn return toReturn
} }
@ -376,12 +380,18 @@ class Civilization : IsPartOfGameInfoSerialization {
fun getHappiness() = stats.happiness 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 { fun getCivResourcesWithOriginsForTrade(): ResourceSupplyList {
val newResourceSupplyList = ResourceSupplyList(keepZeroAmounts = true) val newResourceSupplyList = ResourceSupplyList(keepZeroAmounts = true)
for (resourceSupply in detailedCivResources) { 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 we got it from another trade or from a CS, preserve the origin
if (resourceSupply.isCityStateOrTradeOrigin()) { if (resourceSupply.isCityStateOrTradeOrigin()) {
newResourceSupplyList.add(resourceSupply.copy()) 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 * 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> { fun getCivResourcesByName(): HashMap<String, Int> {
val hashMap = HashMap<String, Int>(gameInfo.ruleset.tileResources.size) val hashMap = HashMap<String, Int>(gameInfo.ruleset.tileResources.size)
for (resource in gameInfo.ruleset.tileResources.keys) hashMap[resource] = 0 for (resource in gameInfo.ruleset.tileResources.keys) hashMap[resource] = 0
for (entry in getCivResources()) for (entry in getCivResourceSupply())
hashMap[entry.resource.name] = entry.amount if (!entry.resource.isStockpiled())
hashMap[entry.resource.name] = entry.amount
for ((key, value) in resourceStockpiles)
hashMap[key] = value
return hashMap return hashMap
} }
@ -442,7 +456,7 @@ class Civilization : IsPartOfGameInfoSerialization {
yieldAll(religionManager.religion!!.getFounderUniques() yieldAll(religionManager.religion!!.getFounderUniques()
.filter { it.isOfType(uniqueType) && it.conditionalsApply(stateForConditionals) }) .filter { it.isOfType(uniqueType) && it.conditionalsApply(stateForConditionals) })
yieldAll(getCivResources().asSequence() yieldAll(getCivResourceSupply().asSequence()
.filter { it.amount > 0 } .filter { it.amount > 0 }
.flatMap { it.resource.getMatchingUniques(uniqueType, stateForConditionals) } .flatMap { it.resource.getMatchingUniques(uniqueType, stateForConditionals) }
) )

View File

@ -404,6 +404,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
val isResourceFilter: (TradeOffer) -> Boolean = { val isResourceFilter: (TradeOffer) -> Boolean = {
(it.type == TradeType.Strategic_Resource || it.type == TradeType.Luxury_Resource) (it.type == TradeType.Strategic_Resource || it.type == TradeType.Luxury_Resource)
&& resourcesMap.containsKey(it.name) && resourcesMap.containsKey(it.name)
&& !resourcesMap[it.name]!!.isStockpiled()
} }
for (trade in trades) { for (trade in trades) {
for (offer in trade.ourOffers.filter(isResourceFilter)) 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, // 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) // 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() val negativeCivResources = civInfo.getCivResourceSupply()
.filter { it.amount < 0 }.map { it.resource.name } .filter { it.amount < 0 && !it.resource.isStockpiled() }.map { it.resource.name }
for (offer in trade.ourOffers) { for (offer in trade.ourOffers) {
if (offer.type in listOf(TradeType.Luxury_Resource, TradeType.Strategic_Resource) 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()) if (civInfo.cities.isNotEmpty() && civInfo.gameInfo.ruleset.technologies.isNotEmpty())
civInfo.tech.updateResearchProgress() 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.civConstructions.startTurn()
civInfo.attacksSinceTurnStart.clear() civInfo.attacksSinceTurnStart.clear()
civInfo.updateStatsForNextTurn() // for things that change when turn passes e.g. golden age, city state influence 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() unit.doAction()
} else civInfo.hasMovedAutomatedUnits = false } 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 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) val offeringCiv = civInfo.gameInfo.getCivilization(tradeRequest.requestingCiv)
if (offeringCiv.isDefeated() || !TradeEvaluation().isTradeValid(tradeRequest.trade, civInfo, offeringCiv)) { 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, // 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 // and in any case it'll be updated once civ info transients are
civInfo.updateStatsForNextTurn() // unit upkeep civInfo.updateStatsForNextTurn() // unit upkeep
if (mapUnit.baseUnit.getResourceRequirements().isNotEmpty()) if (mapUnit.baseUnit.getResourceRequirementsPerTurn().isNotEmpty())
civInfo.cache.updateCivResources() civInfo.cache.updateCivResources()
} }
} }
@ -111,7 +111,7 @@ class UnitManager(val civInfo:Civilization) {
nextPotentiallyDueAt = 0 nextPotentiallyDueAt = 0
civInfo.updateStatsForNextTurn() // unit upkeep civInfo.updateStatsForNextTurn() // unit upkeep
if (mapUnit.baseUnit.getResourceRequirements().isNotEmpty()) if (mapUnit.baseUnit.getResourceRequirementsPerTurn().isNotEmpty())
civInfo.cache.updateCivResources() civInfo.cache.updateCivResources()
} }

View File

@ -230,10 +230,10 @@ class CivInfoStatsForNextTurn(val civInfo: Civilization) {
for (unique in civInfo.getMatchingUniques(UniqueType.BonusHappinessFromLuxury)) for (unique in civInfo.getMatchingUniques(UniqueType.BonusHappinessFromLuxury))
happinessPerUniqueLuxury += unique.params[0].toInt() happinessPerUniqueLuxury += unique.params[0].toInt()
val ownedLuxuries = civInfo.getCivResources().map { it.resource } val ownedLuxuries = civInfo.getCivResourceSupply().map { it.resource }
.filter { it.resourceType == ResourceType.Luxury } .filter { it.resourceType == ResourceType.Luxury }
val relevantLuxuries = civInfo.getCivResources().asSequence() val relevantLuxuries = civInfo.getCivResourceSupply().asSequence()
.map { it.resource } .map { it.resource }
.count { it.resourceType == ResourceType.Luxury .count { it.resourceType == ResourceType.Luxury
&& it.getMatchingUniques(UniqueType.ObsoleteWith) && it.getMatchingUniques(UniqueType.ObsoleteWith)
@ -245,7 +245,7 @@ class CivInfoStatsForNextTurn(val civInfo: Civilization) {
val luxuriesProvidedByCityStates = civInfo.getKnownCivs().asSequence() val luxuriesProvidedByCityStates = civInfo.getKnownCivs().asSequence()
.filter { it.isCityState() && it.getAllyCiv() == civInfo.civName } .filter { it.isCityState() && it.getAllyCiv() == civInfo.civName }
.flatMap { it.getCivResources().map { res -> res.resource } } .flatMap { it.getCivResourceSupply().map { res -> res.resource } }
.distinct() .distinct()
.count { it.resourceType === ResourceType.Luxury && ownedLuxuries.contains(it) } .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()) for (unit in civInfo.units.getCivUnits())
newDetailedCivResources.subtractResourceRequirements( 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 // 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 if (civInfo.detailedCivResources == newDetailedCivResources) return
civInfo.detailedCivResources = newDetailedCivResources 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 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 // And update civ stats, since the new unit changes both unit upkeep and resource consumption
civInfo.updateStatsForNextTurn() civInfo.updateStatsForNextTurn()
if (unit.baseUnit.getResourceRequirements().isNotEmpty()) if (unit.baseUnit.getResourceRequirementsPerTurn().isNotEmpty())
civInfo.cache.updateCivResources() civInfo.cache.updateCivResources()
return unit return unit

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ package com.unciv.logic.city
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.models.ruleset.unique.IHasUniques import com.unciv.models.ruleset.unique.IHasUniques
import com.unciv.models.ruleset.unique.StateForConditionals 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.unique.UniqueType
import com.unciv.models.stats.INamed import com.unciv.models.stats.INamed
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
@ -14,8 +15,11 @@ import kotlin.math.roundToInt
interface IConstruction : INamed { interface IConstruction : INamed {
fun isBuildable(cityConstructions: CityConstructions): Boolean fun isBuildable(cityConstructions: CityConstructions): Boolean
fun shouldBeDisplayed(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 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 { interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
@ -82,6 +86,9 @@ interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
fun getCostForConstructionsIncreasingInPrice(baseCost: Int, increaseCost: Int, previouslyBought: Int): Int { fun getCostForConstructionsIncreasingInPrice(baseCost: Int, increaseCost: Int, previouslyBought: Int): Int {
return (baseCost + increaseCost / 2f * ( previouslyBought * previouslyBought + previouslyBought )).toInt() 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 = override fun isBuildable(cityConstructions: CityConstructions): Boolean =
throw Exception("Impossible!") throw Exception("Impossible!")
override fun getResourceRequirements(): HashMap<String, Int> = hashMapOf() override fun getResourceRequirementsPerTurn(): HashMap<String, Int> = hashMapOf()
override fun requiresResource(resource: String) = false 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)) if (!ruleset.tileResources.containsKey(resource))
lines += "${unit.name} requires resource $resource which does not exist!" lines += "${unit.name} requires resource $resource which does not exist!"
if (unit.replaces != null && !ruleset.units.containsKey(unit.replaces!!)) if (unit.replaces != null && !ruleset.units.containsKey(unit.replaces!!))
@ -189,7 +189,7 @@ class RulesetValidator(val ruleset: Ruleset) {
for (specialistName in building.specialistSlots.keys) for (specialistName in building.specialistSlots.keys)
if (!ruleset.specialists.containsKey(specialistName)) if (!ruleset.specialists.containsKey(specialistName))
lines += "${building.name} provides specialist $specialistName which does not exist!" 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)) if (!ruleset.tileResources.containsKey(resource))
lines += "${building.name} requires resource $resource which does not exist!" lines += "${building.name} requires resource $resource which does not exist!"
if (building.replaces != null && !ruleset.buildings.containsKey(building.replaces!!)) 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)) yield(FormattedLine("${Fonts.range} " + "[${unit.range}] vs [${originalUnit.range}]".tr(), indent=1))
if (unit.movement != originalUnit.movement) if (unit.movement != originalUnit.movement)
yield(FormattedLine("${Fonts.movement} " + "[${unit.movement}] vs [${originalUnit.movement}]".tr(), indent=1)) yield(FormattedLine("${Fonts.movement} " + "[${unit.movement}] vs [${originalUnit.movement}]".tr(), indent=1))
for (resource in originalUnit.getResourceRequirements().keys) for (resource in originalUnit.getResourceRequirementsPerTurn().keys)
if (!unit.getResourceRequirements().containsKey(resource)) { if (!unit.getResourceRequirementsPerTurn().containsKey(resource)) {
yield(FormattedLine("[$resource] not required", link="Resource/$resource", indent=1)) yield(FormattedLine("[$resource] not required", link="Resource/$resource", indent=1))
} }
// This does not use the auto-linking FormattedLine(Unique) for two reasons: // This does not use the auto-linking FormattedLine(Unique) for two reasons:

View File

@ -62,7 +62,7 @@ class ResourceSupplyList(
add(resourceSupply) 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) { fun subtractResourceRequirements(resourceRequirements: HashMap<String, Int>, ruleset: Ruleset, origin: String) {
for ((resourceName, amount) in resourceRequirements) { for ((resourceName, amount) in resourceRequirements) {
val resource = ruleset.tileResources[resourceName] ?: continue val resource = ruleset.tileResources[resourceName] ?: continue

View File

@ -15,6 +15,7 @@ class TileResource : RulesetStatsObject() {
var resourceType: ResourceType = ResourceType.Bonus var resourceType: ResourceType = ResourceType.Bonus
var terrainsCanBeFoundOn: List<String> = listOf() var terrainsCanBeFoundOn: List<String> = listOf()
var improvement: String? = null var improvement: String? = null
/** stats that this resource adds to a tile */
var improvementStats: Stats? = null var improvementStats: Stats? = null
var revealedBy: String? = null var revealedBy: String? = null
var improvedBy: List<String> = listOf() 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()) { if (buildingsThatConsumeThis.isNotEmpty()) {
textList += FormattedLine() textList += FormattedLine()
textList += FormattedLine("{Buildings that consume this resource}:") 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()) { if (unitsThatConsumeThis.isNotEmpty()) {
textList += FormattedLine() textList += FormattedLine()
textList += FormattedLine("{Units that consume this resource}: ") textList += FormattedLine("{Units that consume this resource}: ")
@ -135,6 +136,8 @@ class TileResource : RulesetStatsObject() {
} }
} }
fun isStockpiled() = hasUnique(UniqueType.Stockpiled)
class DepositAmount { class DepositAmount {
var sparse: Int = 1 var sparse: Int = 1
var default: Int = 2 var default: Int = 2

View File

@ -330,6 +330,38 @@ object UniqueTriggerActivation {
return true 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 -> { UniqueType.OneTimeRevealEntireMap -> {
if (notification != null) { if (notification != null) {
civInfo.addNotification(notification, LocationAction(tile?.position), NotificationCategory.General, NotificationIcon.Scout) civInfo.addNotification(notification, LocationAction(tile?.position), NotificationCategory.General, NotificationIcon.Scout)
@ -366,6 +398,21 @@ object UniqueTriggerActivation {
return promotedUnitLocations.isNotEmpty() 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 -> { UniqueType.CityStateCanGiftGreatPeople -> {
civInfo.addFlag( civInfo.addFlag(
CivFlags.CityStateGreatPersonGift.name, CivFlags.CityStateGreatPersonGift.name,
@ -376,21 +423,6 @@ object UniqueTriggerActivation {
} }
return true 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 -> { UniqueType.OneTimeGainStat -> {
val stat = Stat.safeValueOf(unique.params[1]) ?: return false 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) civInfo.addNotification(notificationText, LocationAction(tile?.position), NotificationCategory.General, stat.notificationIcon)
return true return true
} }
UniqueType.OneTimeGainStatRange -> { UniqueType.OneTimeGainStatRange -> {
val stat = Stat.safeValueOf(unique.params[2]) ?: return false 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), ConsumesResources("Consumes [amount] [resource]", UniqueTarget.Improvement, UniqueTarget.Building, UniqueTarget.Unit),
ProvidesResources("Provides [amount] [resource]", UniqueTarget.Improvement, UniqueTarget.Global), 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), GrowthPercentBonus("[relativeAmount]% growth [cityFilter]", UniqueTarget.Global, UniqueTarget.FollowerBelief),
CarryOverFood("[relativeAmount]% Food is carried over after population increases [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 /////// Resource uniques
ResourceAmountOnTiles("Deposits in [tileFilter] tiles always provide [amount] resources", UniqueTarget.Resource), ResourceAmountOnTiles("Deposits in [tileFilter] tiles always provide [amount] resources", UniqueTarget.Resource),
CityStateOnlyResource("Can only be created by Mercantile City-States", 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), ResourceWeighting("Generated with weight [amount]", UniqueTarget.Resource, flags = UniqueFlag.setOfHiddenToUsers),
MinorDepositWeighting("Minor deposits 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), OneTimeFreeBelief("Gain a free [beliefType] belief", UniqueTarget.Triggerable),
OneTimeTriggerVoting("Triggers voting for the Diplomatic Victory", UniqueTarget.Triggerable), // used in Building 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), OneTimeGainStatRange("Gain [amount]-[amount] [stat]", UniqueTarget.Triggerable),
OneTimeGainPantheon("Gain enough Faith for a Pantheon", UniqueTarget.Triggerable), OneTimeGainPantheon("Gain enough Faith for a Pantheon", UniqueTarget.Triggerable),
OneTimeGainProphet("Gain enough Faith for [amount]% of a Great Prophet", 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 if (!civ.isBarbarian()) { // Barbarians don't need resources
for ((resource, requiredAmount) in getResourceRequirements()) { for ((resource, requiredAmount) in getResourceRequirementsPerTurn()) {
val availableAmount = civ.getCivResourcesByName()[resource]!! val availableAmount = civ.getCivResourcesByName()[resource]!!
if (availableAmount < requiredAmount) { if (availableAmount < requiredAmount) {
result.add( result.add(
@ -317,7 +317,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
fun movesLikeAirUnits() = type.getMovementType() == UnitMovementType.Air fun movesLikeAirUnits() = type.getMovementType() == UnitMovementType.Air
/** Returns resource requirements from both uniques and requiredResource field */ /** 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 { private val resourceRequirementsInternal: HashMap<String, Int> by lazy {
val resourceRequirements = HashMap<String, Int>() val resourceRequirements = HashMap<String, Int>()
@ -327,7 +327,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
resourceRequirements resourceRequirements
} }
override fun requiresResource(resource: String) = getResourceRequirements().containsKey(resource) override fun requiresResource(resource: String) = getResourceRequirementsPerTurn().containsKey(resource)
fun isRanged() = rangedStrength > 0 fun isRanged() = rangedStrength > 0
fun isMelee() = !isRanged() && strength > 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.badlogic.gdx.math.Vector2
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.components.Fonts
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.time.Duration import java.time.Duration
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
@ -17,7 +18,11 @@ fun Int.toPercent() = toFloat().toPercent()
fun Float.toPercent() = 1 + this/100 fun Float.toPercent() = 1 + this/100
/** Convert a [resource name][this] into "Consumes [amount] $resource" string (untranslated) */ /** 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) */ /** Convert a [resource name][this] into "Need [amount] more $resource" string (untranslated) */
fun String.getNeedMoreAmountString(amount: Int) = "Need [$amount] more [$this]" fun String.getNeedMoreAmountString(amount: Int) = "Need [$amount] more [$this]"
@ -34,7 +39,7 @@ fun Duration.format(): String {
if (firstPartAlreadyAdded) { if (firstPartAlreadyAdded) {
sb.append(", ") sb.append(", ")
} }
sb.append("[${part}] $unit") sb.append("[$part] $unit")
firstPartAlreadyAdded = true firstPartAlreadyAdded = true
} }
return sb.toString() 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.ruleset.unit.UnitType
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.screens.civilopediascreen.FormattedLine
import com.unciv.ui.components.Fonts import com.unciv.ui.components.Fonts
import com.unciv.ui.components.extensions.getConsumesAmountString import com.unciv.ui.components.extensions.getConsumesAmountString
import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.components.extensions.toPercent
import com.unciv.ui.screens.civilopediascreen.FormattedLine
import kotlin.math.pow import kotlin.math.pow
object BaseUnitDescriptions { object BaseUnitDescriptions {
@ -35,10 +35,12 @@ object BaseUnitDescriptions {
* @param city Supplies civInfo to show available resources after resource requirements */ * @param city Supplies civInfo to show available resources after resource requirements */
fun getDescription(baseUnit: BaseUnit, city: City): String { fun getDescription(baseUnit: BaseUnit, city: City): String {
val lines = mutableListOf<String>() val lines = mutableListOf<String>()
val availableResources = city.civ.getCivResources().associate { it.resource.name to it.amount } val availableResources = city.civ.getCivResourcesByName()
for ((resource, amount) in baseUnit.getResourceRequirements()) { for ((resourceName, amount) in baseUnit.getResourceRequirementsPerTurn()) {
val available = availableResources[resource] ?: 0 val available = availableResources[resourceName] ?: 0
lines += "{${resource.getConsumesAmountString(amount)}} ({[$available] available})".tr() val resource = baseUnit.ruleset.tileResources[resourceName] ?: continue
val consumesString = resourceName.getConsumesAmountString(amount, resource.isStockpiled())
lines += "$consumesString ({[$available] available})".tr()
} }
var strengthLine = "" var strengthLine = ""
if (baseUnit.strength != 0) { 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 val buyCost = (30.0 * baseUnit.cost.toFloat().pow(0.75f) * baseUnit.hurryCostModifier.toPercent()).toInt() / 10 * 10
stats += "$buyCost${Fonts.gold}" stats += "$buyCost${Fonts.gold}"
} }
textList += FormattedLine(stats.joinToString(", ", "{Cost}: ")) textList += FormattedLine(stats.joinToString("/", "{Cost}: "))
} }
if (baseUnit.replacementTextForUniques.isNotEmpty()) { if (baseUnit.replacementTextForUniques.isNotEmpty()) {
@ -105,12 +107,13 @@ object BaseUnitDescriptions {
} }
} }
val resourceRequirements = baseUnit.getResourceRequirements() val resourceRequirements = baseUnit.getResourceRequirementsPerTurn()
if (resourceRequirements.isNotEmpty()) { if (resourceRequirements.isNotEmpty()) {
textList += FormattedLine() textList += FormattedLine()
for ((resource, amount) in resourceRequirements) { for ((resourceName, amount) in resourceRequirements) {
val resource = ruleset.tileResources[resourceName] ?: continue
textList += FormattedLine( textList += FormattedLine(
resource.getConsumesAmountString(amount), resourceName.getConsumesAmountString(amount, resource.isStockpiled()),
link = "Resource/$resource", color = "#F42" 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 useStoredProduction = entry is Building || !cityConstructions.isBeingConstructedOrEnqueued(entry.name)
val buttonText = cityConstructions.getTurnsToConstructionString(entry.name, useStoredProduction).trim() val buttonText = cityConstructions.getTurnsToConstructionString(entry.name, useStoredProduction).trim()
val resourcesRequired = entry.getResourceRequirements() val resourcesRequired = entry.getResourceRequirementsPerTurn()
val mostImportantRejection = val mostImportantRejection =
entry.getRejectionReasons(cityConstructions) entry.getRejectionReasons(cityConstructions)
.filter { it.isImportantRejection() } .filter { it.isImportantRejection() }
@ -317,9 +317,11 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
if (constructionName in PerpetualConstruction.perpetualConstructionsMap) "\n" if (constructionName in PerpetualConstruction.perpetualConstructionsMap) "\n"
else cityConstructions.getTurnsToConstructionString(constructionName, isFirstConstructionOfItsKind) else cityConstructions.getTurnsToConstructionString(constructionName, isFirstConstructionOfItsKind)
val constructionResource = cityConstructions.getConstruction(constructionName).getResourceRequirements() val constructionResource = cityConstructions.getConstruction(constructionName).getResourceRequirementsPerTurn()
for ((resource, amount) in constructionResource) for ((resourceName, amount) in constructionResource) {
text += "\n" + resource.getConsumesAmountString(amount).tr() val resource = cityConstructions.city.getRuleset().tileResources[resourceName] ?: continue
text += "\n" + resourceName.getConsumesAmountString(amount, resource.isStockpiled()).tr()
}
table.defaults().pad(2f).minWidth(40f) table.defaults().pad(2f).minWidth(40f)
if (isFirstConstructionOfItsKind) table.add(getProgressBar(constructionName)).minWidth(5f) 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) 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() constructionTable.add(resourceTable).expandX().left()
pickConstructionButton.add(constructionTable).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.ResourceType
import com.unciv.models.ruleset.tile.TileResource import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.translations.tr 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.UncivTooltip.Companion.addTooltip
import com.unciv.ui.components.extensions.addSeparator import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.addSeparatorVertical 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.pad
import com.unciv.ui.components.extensions.surroundWithCircle import com.unciv.ui.components.extensions.surroundWithCircle
import com.unciv.ui.components.extensions.toLabel 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( class ResourcesOverviewTab(
@ -73,10 +73,16 @@ class ResourcesOverviewTab(
private val extraOrigins: List<ExtraInfoOrigin> = extraDrilldown.asSequence() private val extraOrigins: List<ExtraInfoOrigin> = extraDrilldown.asSequence()
.mapNotNull { ExtraInfoOrigin.safeValueOf(it.origin) }.distinct().toList() .mapNotNull { ExtraInfoOrigin.safeValueOf(it.origin) }.distinct().toList()
private fun ResourceSupplyList.getLabel(resource: TileResource, origin: String): Label? = private fun ResourceSupplyList.getLabel(resource: TileResource, origin: String): Label? {
get(resource, origin)?.amount?.toLabel() val amount = get(resource, origin)?.amount ?: return null
private fun ResourceSupplyList.getTotalLabel(resource: TileResource): Label = return if (resource.isStockpiled() && amount > 0) "+$amount".toLabel()
filter { it.resource == resource }.sumOf { it.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) = private fun getResourceImage(name: String) =
ImageGetter.getResourcePortrait(name, iconSize).apply { ImageGetter.getResourcePortrait(name, iconSize).apply {
onClick { onClick {

View File

@ -12,6 +12,7 @@ import com.badlogic.gdx.utils.Align
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.TileResource import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stats import com.unciv.models.stats.Stats
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.components.Fonts 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.setFontColor
import com.unciv.ui.components.extensions.setFontSize import com.unciv.ui.components.extensions.setFontSize
import com.unciv.ui.components.extensions.toLabel 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.components.extensions.toTextButton
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popups.popups import com.unciv.ui.popups.popups
@ -347,14 +349,23 @@ class WorldScreenTopBar(val worldScreen: WorldScreen) : Table() {
turnsLabel.setText(Fonts.turn + "" + civInfo.gameInfo.turns + " | " + yearText) turnsLabel.setText(Fonts.turn + "" + civInfo.gameInfo.turns + " | " + yearText)
resourcesWrapper.clearChildren() resourcesWrapper.clearChildren()
var firstPadLeft = 20f // We want a distance from the turns entry to the first resource, but only if any resource is displayed 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) { for ((resource, label, icon) in resourceActors) {
if (resource.revealedBy != null && !civInfo.tech.isResearched(resource.revealedBy!!)) if (resource.revealedBy != null && !civInfo.tech.isResearched(resource.revealedBy!!))
continue continue
if (resource.hasUnique(UniqueType.NotShownOnWorldScreen)) continue
resourcesWrapper.add(icon).padLeft(firstPadLeft).padRight(0f) resourcesWrapper.add(icon).padLeft(firstPadLeft).padRight(0f)
firstPadLeft = 5f firstPadLeft = 5f
val amount = civResources.get(resource, "All")?.amount ?: 0 val amount = civResources[resource.name] ?: 0
label.setText(amount) 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 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 // Check _new_ resource requirements
// Using Counter to aggregate is a bit exaggerated, but - respect the mad modder. // Using Counter to aggregate is a bit exaggerated, but - respect the mad modder.
val resourceRequirementsDelta = Counter<String>() val resourceRequirementsDelta = Counter<String>()
for ((resource, amount) in unit.baseUnit().getResourceRequirements()) for ((resource, amount) in unit.baseUnit().getResourceRequirementsPerTurn())
resourceRequirementsDelta.add(resource, -amount) resourceRequirementsDelta.add(resource, -amount)
for ((resource, amount) in upgradedUnit.getResourceRequirements()) for ((resource, amount) in upgradedUnit.getResourceRequirementsPerTurn())
resourceRequirementsDelta.add(resource, amount) resourceRequirementsDelta.add(resource, amount)
val newResourceRequirementsString = resourceRequirementsDelta.entries val newResourceRequirementsString = resourceRequirementsDelta.entries
.filter { it.value > 0 } .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) // 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. // Using Counter to aggregate is a bit exaggerated, but - respect the mad modder.
val resourceRequirementsDelta = Counter<String>() val resourceRequirementsDelta = Counter<String>()
for ((resource, amount) in unit.baseUnit().getResourceRequirements()) for ((resource, amount) in unit.baseUnit().getResourceRequirementsPerTurn())
resourceRequirementsDelta.add(resource, -amount) resourceRequirementsDelta.add(resource, -amount)
for ((resource, amount) in upgradedUnit.getResourceRequirements()) for ((resource, amount) in upgradedUnit.getResourceRequirementsPerTurn())
resourceRequirementsDelta.add(resource, amount) resourceRequirementsDelta.add(resource, amount)
val newResourceRequirementsString = resourceRequirementsDelta.entries val newResourceRequirementsString = resourceRequirementsDelta.entries
.filter { it.value > 0 } .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" ??? example "Triggers voting for the Diplomatic Victory"
Applicable to: Triggerable 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]" Example: "Gain [3] [Culture]"
Applicable to: Triggerable Applicable to: Triggerable
@ -914,6 +924,11 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
Applicable to: Building, Unit, Improvement Applicable to: Building, Unit, Improvement
??? example "Costs [amount] [resource]"
Example: "Costs [3] [Iron]"
Applicable to: Building, Unit, Improvement
??? example "Unbuildable" ??? example "Unbuildable"
Applicable to: Building, Unit, Improvement 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" ??? example "Can only be created by Mercantile City-States"
Applicable to: Resource 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 [amount]"
Example: "Generated with weight [3]" Example: "Generated with weight [3]"