From adb51d92645cc6228b72ac43c30afcef6307f69a Mon Sep 17 00:00:00 2001 From: Yair Morgenstern Date: Sun, 9 Apr 2023 18:01:26 +0300 Subject: [PATCH] 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 --- .../jsons/translations/template.properties | 2 + core/src/com/unciv/logic/GameInfo.kt | 2 +- .../com/unciv/logic/automation/Automation.kt | 6 +- .../logic/automation/unit/UnitAutomation.kt | 2 +- core/src/com/unciv/logic/battle/Battle.kt | 2 +- .../com/unciv/logic/battle/BattleDamage.kt | 2 +- core/src/com/unciv/logic/city/City.kt | 2 +- .../com/unciv/logic/city/CityConstructions.kt | 22 ++++++- .../unciv/logic/civilization/Civilization.kt | 26 ++++++-- .../diplomacy/DiplomacyManager.kt | 5 +- .../civilization/managers/TurnManager.kt | 7 ++- .../civilization/managers/UnitManager.kt | 4 +- .../transients/CivInfoStatsForNextTurn.kt | 6 +- .../transients/CivInfoTransientCache.kt | 4 +- core/src/com/unciv/logic/map/TileMap.kt | 2 +- .../map/tile/TileInfoImprovementFunctions.kt | 19 +++--- .../com/unciv/logic/trade/TradeEvaluation.kt | 6 +- core/src/com/unciv/models/ruleset/Building.kt | 31 +++++---- .../com/unciv/models/ruleset/IConstruction.kt | 11 +++- .../unciv/models/ruleset/RulesetValidator.kt | 4 +- .../com/unciv/models/ruleset/nation/Nation.kt | 4 +- .../models/ruleset/tile/ResourceSupplyList.kt | 2 +- .../unciv/models/ruleset/tile/TileResource.kt | 7 ++- .../ruleset/unique/UniqueTriggerActivation.kt | 63 ++++++++++++++----- .../unciv/models/ruleset/unique/UniqueType.kt | 13 +++- .../com/unciv/models/ruleset/unit/BaseUnit.kt | 6 +- .../extensions/FormattingExtensions.kt | 9 ++- .../BaseUnitDescriptions.kt | 21 ++++--- .../cityscreen/CityConstructionsTable.kt | 16 +++-- .../overviewscreen/ResourcesOverviewTable.kt | 22 ++++--- .../screens/worldscreen/WorldScreenTopBar.kt | 17 ++++- .../worldscreen/unit/actions/UnitActions.kt | 4 +- .../unit/actions/UnitActionsUpgrade.kt | 4 +- docs/Modders/uniques.md | 23 ++++++- 34 files changed, 265 insertions(+), 111 deletions(-) diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 09d7e3ac35..4efe17edba 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -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 diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index c6b672e129..ff618228fe 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -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) diff --git a/core/src/com/unciv/logic/automation/Automation.kt b/core/src/com/unciv/logic/automation/Automation.kt index eafe4b5802..fce19bc48a 100644 --- a/core/src/com/unciv/logic/automation/Automation.kt +++ b/core/src/com/unciv/logic/automation/Automation.kt @@ -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 diff --git a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt index d77900048e..3d436d3bc4 100644 --- a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt @@ -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 } diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index 853a6b8fb0..e17e70ab49 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -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 } diff --git a/core/src/com/unciv/logic/battle/BattleDamage.kt b/core/src/com/unciv/logic/battle/BattleDamage.kt index fca4595a34..7c2bf5a72c 100644 --- a/core/src/com/unciv/logic/battle/BattleDamage.kt +++ b/core/src/com/unciv/logic/battle/BattleDamage.kt @@ -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 diff --git a/core/src/com/unciv/logic/city/City.kt b/core/src/com/unciv/logic/city/City.kt index 99dc99b4ed..8becbcd7ed 100644 --- a/core/src/com/unciv/logic/city/City.kt +++ b/core/src/com/unciv/logic/city/City.kt @@ -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]" diff --git a/core/src/com/unciv/logic/city/CityConstructions.kt b/core/src/com/unciv/logic/city/CityConstructions.kt index 37da95db85..c549a236aa 100644 --- a/core/src/com/unciv/logic/city/CityConstructions.kt +++ b/core/src/com/unciv/logic/city/CityConstructions.kt @@ -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}" diff --git a/core/src/com/unciv/logic/civilization/Civilization.kt b/core/src/com/unciv/logic/civilization/Civilization.kt index 2eb4b51734..a252108fba 100644 --- a/core/src/com/unciv/logic/civilization/Civilization.kt +++ b/core/src/com/unciv/logic/civilization/Civilization.kt @@ -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() + var resourceStockpiles = Counter() + /** 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() 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 { val hashMap = HashMap(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) } ) diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt index eeac6bac4a..7c64cae49a 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt @@ -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) diff --git a/core/src/com/unciv/logic/civilization/managers/TurnManager.kt b/core/src/com/unciv/logic/civilization/managers/TurnManager.kt index f1005b63d7..8c1b8d9656 100644 --- a/core/src/com/unciv/logic/civilization/managers/TurnManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/TurnManager.kt @@ -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)) { diff --git a/core/src/com/unciv/logic/civilization/managers/UnitManager.kt b/core/src/com/unciv/logic/civilization/managers/UnitManager.kt index fde177dc1f..68e4156e14 100644 --- a/core/src/com/unciv/logic/civilization/managers/UnitManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/UnitManager.kt @@ -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() } diff --git a/core/src/com/unciv/logic/civilization/transients/CivInfoStatsForNextTurn.kt b/core/src/com/unciv/logic/civilization/transients/CivInfoStatsForNextTurn.kt index a9fd5d0c6d..aa774988d8 100644 --- a/core/src/com/unciv/logic/civilization/transients/CivInfoStatsForNextTurn.kt +++ b/core/src/com/unciv/logic/civilization/transients/CivInfoStatsForNextTurn.kt @@ -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) } diff --git a/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt b/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt index 4f249126d8..fc5ce73f36 100644 --- a/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt +++ b/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt @@ -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 } diff --git a/core/src/com/unciv/logic/map/TileMap.kt b/core/src/com/unciv/logic/map/TileMap.kt index 05bb8942ba..e0842616b2 100644 --- a/core/src/com/unciv/logic/map/TileMap.kt +++ b/core/src/com/unciv/logic/map/TileMap.kt @@ -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 diff --git a/core/src/com/unciv/logic/map/tile/TileInfoImprovementFunctions.kt b/core/src/com/unciv/logic/map/tile/TileInfoImprovementFunctions.kt index 8c80833766..adb973667c 100644 --- a/core/src/com/unciv/logic/map/tile/TileInfoImprovementFunctions.kt +++ b/core/src/com/unciv/logic/map/tile/TileInfoImprovementFunctions.kt @@ -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 diff --git a/core/src/com/unciv/logic/trade/TradeEvaluation.kt b/core/src/com/unciv/logic/trade/TradeEvaluation.kt index 3f8f0cad24..8ca5b6bb61 100644 --- a/core/src/com/unciv/logic/trade/TradeEvaluation.kt +++ b/core/src/com/unciv/logic/trade/TradeEvaluation.kt @@ -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 diff --git a/core/src/com/unciv/models/ruleset/Building.kt b/core/src/com/unciv/models/ruleset/Building.kt index 5dad119cdf..b1ae21203e 100644 --- a/core/src/com/unciv/models/ruleset/Building.kt +++ b/core/src/com/unciv/models/ruleset/Building.kt @@ -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 = resourceRequirementsInternal + override fun getResourceRequirementsPerTurn(): HashMap = resourceRequirementsInternal private val resourceRequirementsInternal: HashMap by lazy { val resourceRequirements = HashMap() @@ -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 } } diff --git a/core/src/com/unciv/models/ruleset/IConstruction.kt b/core/src/com/unciv/models/ruleset/IConstruction.kt index 35cb9519c7..120146655f 100644 --- a/core/src/com/unciv/models/ruleset/IConstruction.kt +++ b/core/src/com/unciv/models/ruleset/IConstruction.kt @@ -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 + /** Gets *per turn* resource requirements - does not include immediate costs for stockpiled resources */ + fun getResourceRequirementsPerTurn(): HashMap fun requiresResource(resource: String): Boolean + /** We can't call this getMatchingUniques because then it would conflict with IHasUniques */ + fun getMatchingUniquesNotConflicting(uniqueType: UniqueType) = sequenceOf() } 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 = + 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 = hashMapOf() + override fun getResourceRequirementsPerTurn(): HashMap = hashMapOf() override fun requiresResource(resource: String) = false diff --git a/core/src/com/unciv/models/ruleset/RulesetValidator.kt b/core/src/com/unciv/models/ruleset/RulesetValidator.kt index e2fcbd2386..a0851d0359 100644 --- a/core/src/com/unciv/models/ruleset/RulesetValidator.kt +++ b/core/src/com/unciv/models/ruleset/RulesetValidator.kt @@ -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!!)) diff --git a/core/src/com/unciv/models/ruleset/nation/Nation.kt b/core/src/com/unciv/models/ruleset/nation/Nation.kt index 952be09658..13113501bb 100644 --- a/core/src/com/unciv/models/ruleset/nation/Nation.kt +++ b/core/src/com/unciv/models/ruleset/nation/Nation.kt @@ -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: diff --git a/core/src/com/unciv/models/ruleset/tile/ResourceSupplyList.kt b/core/src/com/unciv/models/ruleset/tile/ResourceSupplyList.kt index 05515f344d..0e22b30762 100644 --- a/core/src/com/unciv/models/ruleset/tile/ResourceSupplyList.kt +++ b/core/src/com/unciv/models/ruleset/tile/ResourceSupplyList.kt @@ -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, ruleset: Ruleset, origin: String) { for ((resourceName, amount) in resourceRequirements) { val resource = ruleset.tileResources[resourceName] ?: continue diff --git a/core/src/com/unciv/models/ruleset/tile/TileResource.kt b/core/src/com/unciv/models/ruleset/tile/TileResource.kt index de38f5dbda..891d19f433 100644 --- a/core/src/com/unciv/models/ruleset/tile/TileResource.kt +++ b/core/src/com/unciv/models/ruleset/tile/TileResource.kt @@ -15,6 +15,7 @@ class TileResource : RulesetStatsObject() { var resourceType: ResourceType = ResourceType.Bonus var terrainsCanBeFoundOn: List = listOf() var improvement: String? = null + /** stats that this resource adds to a tile */ var improvementStats: Stats? = null var revealedBy: String? = null var improvedBy: List = 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 diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt index 5465031de1..203bc05387 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueTriggerActivation.kt @@ -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 diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index a88376bf20..58afb1ba29 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -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), diff --git a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt index 03e6f3d374..d39ff38096 100644 --- a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt +++ b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt @@ -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 = resourceRequirementsInternal + override fun getResourceRequirementsPerTurn(): HashMap = resourceRequirementsInternal private val resourceRequirementsInternal: HashMap by lazy { val resourceRequirements = HashMap() @@ -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 diff --git a/core/src/com/unciv/ui/components/extensions/FormattingExtensions.kt b/core/src/com/unciv/ui/components/extensions/FormattingExtensions.kt index e1e86b985d..76660fb206 100644 --- a/core/src/com/unciv/ui/components/extensions/FormattingExtensions.kt +++ b/core/src/com/unciv/ui/components/extensions/FormattingExtensions.kt @@ -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() diff --git a/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt b/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt index 33572e7c28..7f35a06dda 100644 --- a/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt +++ b/core/src/com/unciv/ui/objectdescriptions/BaseUnitDescriptions.kt @@ -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() - 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" ) } diff --git a/core/src/com/unciv/ui/screens/cityscreen/CityConstructionsTable.kt b/core/src/com/unciv/ui/screens/cityscreen/CityConstructionsTable.kt index b4ed626b67..6a06d7af5a 100644 --- a/core/src/com/unciv/ui/screens/cityscreen/CityConstructionsTable.kt +++ b/core/src/com/unciv/ui/screens/cityscreen/CityConstructionsTable.kt @@ -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() diff --git a/core/src/com/unciv/ui/screens/overviewscreen/ResourcesOverviewTable.kt b/core/src/com/unciv/ui/screens/overviewscreen/ResourcesOverviewTable.kt index cefe655103..e8979c3091 100644 --- a/core/src/com/unciv/ui/screens/overviewscreen/ResourcesOverviewTable.kt +++ b/core/src/com/unciv/ui/screens/overviewscreen/ResourcesOverviewTable.kt @@ -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 = 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 { diff --git a/core/src/com/unciv/ui/screens/worldscreen/WorldScreenTopBar.kt b/core/src/com/unciv/ui/screens/worldscreen/WorldScreenTopBar.kt index dae39d6c84..4c32c123f4 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/WorldScreenTopBar.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/WorldScreenTopBar.kt @@ -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 } diff --git a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt index 4d9e77b417..a592b3e3f7 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActions.kt @@ -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() - 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 } diff --git a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsUpgrade.kt b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsUpgrade.kt index 06a38a8409..19913752da 100644 --- a/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsUpgrade.kt +++ b/core/src/com/unciv/ui/screens/worldscreen/unit/actions/UnitActionsUpgrade.kt @@ -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() - 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 } diff --git a/docs/Modders/uniques.md b/docs/Modders/uniques.md index 95ef9d0326..c2def78505 100644 --- a/docs/Modders/uniques.md +++ b/docs/Modders/uniques.md @@ -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]"