Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

City-level resources #9774

Merged
merged 6 commits into from
Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -362,16 +362,17 @@ object NextTurnAutomation {
val highlyDesirableTilesInCity = city.tilesInRange.filter {
val hasNaturalWonder = it.naturalWonder != null
val hasLuxuryCivDoesntOwn =
it.hasViewableResource(civInfo) &&
it.tileResource.resourceType == ResourceType.Luxury &&
!civInfo.hasResource(it.resource!!)
it.hasViewableResource(civInfo)
&& it.tileResource.resourceType == ResourceType.Luxury
&& !civInfo.hasResource(it.resource!!)
val hasResourceCivHasNoneOrLittle =
(it.hasViewableResource(civInfo)
&& it.tileResource.resourceType == ResourceType.Strategic &&
(civInfo.getCivResourcesByName()[it.resource!!] ?: 0) <= 3)
it.hasViewableResource(civInfo)
&& it.tileResource.resourceType == ResourceType.Strategic
&& civInfo.getResourceAmount(it.resource!!) <= 3

it.isVisible(civInfo) && it.getOwner() == null
&& it.neighbors.any { neighbor -> neighbor.getCity() == city }
(hasNaturalWonder || hasLuxuryCivDoesntOwn || hasResourceCivHasNoneOrLittle)
&& it.neighbors.any { neighbor -> neighbor.getCity() == city }
(hasNaturalWonder || hasLuxuryCivDoesntOwn || hasResourceCivHasNoneOrLittle)
}
for (highlyDesirableTileInCity in highlyDesirableTilesInCity) {
highlyDesirableTiles.getOrPut(highlyDesirableTileInCity) { mutableSetOf() }
Expand Down Expand Up @@ -602,8 +603,7 @@ object NextTurnAutomation {

for (resource in civInfo.gameInfo.spaceResources) {
// Have enough resources already
val resourceCount = civInfo.getCivResourcesByName()[resource] ?: 0
if (resourceCount >= Automation.getReservedSpaceResourceAmount(civInfo))
if (civInfo.getResourceAmount(resource) >= Automation.getReservedSpaceResourceAmount(civInfo))
continue

val unitToDisband = civInfo.units.getCivUnits()
Expand Down
10 changes: 10 additions & 0 deletions core/src/com/unciv/logic/city/City.kt
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,16 @@ class City : IsPartOfGameInfoSerialization {
return cityResources
}

/** Gets the number of resources available to this city
* Accommodates both city-wide and civ-wide resources */
fun getResourceAmount(resourceName: String): Int {
val resource = getRuleset().tileResources[resourceName] ?: return 0

if (resource.hasUnique(UniqueType.CityResource))
return getCityResources().asSequence().filter { it.resource == resource }.sumOf { it.amount }
return civ.getResourceAmount(resourceName)
}

private fun getTileResourceAmount(tile: Tile): Int {
if (tile.resource == null) return 0
if (!tile.providesResources(civ)) return 0
Expand Down
4 changes: 2 additions & 2 deletions core/src/com/unciv/logic/city/managers/CityTurnManager.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.unciv.logic.city.managers

import com.unciv.logic.city.City
import com.unciv.logic.city.CityFlags
import com.unciv.logic.city.CityFocus
import com.unciv.logic.city.City
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.models.ruleset.tile.ResourceType
Expand Down Expand Up @@ -46,7 +46,7 @@ class CityTurnManager(val city: City) {

private fun tryWeLoveTheKing() {
if (city.demandedResource == "") return
if (city.civ.getCivResourcesByName()[city.demandedResource]!! > 0) {
if (city.getResourceAmount(city.demandedResource) > 0) {
city.setFlag(CityFlags.WeLoveTheKing, 20 + 1) // +1 because it will be decremented by 1 in the same startTurn()
city.civ.addNotification(
"Because they have [${city.demandedResource}], the citizens of [${city.name}] are celebrating We Love The King Day!",
Expand Down
9 changes: 8 additions & 1 deletion core/src/com/unciv/logic/civilization/Civilization.kt
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,13 @@ class Civilization : IsPartOfGameInfoSerialization {
return hashMap
}

/** Gets the number of resources available to this city
* Does not include city-wide resources
* Returns 0 for undefined resources */
fun getResourceAmount(resourceName:String): Int {
return getCivResourcesByName()[resourceName] ?: 0
}

fun getResourceModifier(resource: TileResource): Float {
var resourceModifier = 1f
for (unique in getMatchingUniques(UniqueType.DoubleResourceProduced))
Expand All @@ -437,7 +444,7 @@ class Civilization : IsPartOfGameInfoSerialization {
return resourceModifier
}

fun hasResource(resourceName: String): Boolean = getCivResourcesByName()[resourceName]!! > 0
fun hasResource(resourceName: String): Boolean = getResourceAmount(resourceName) > 0

fun hasUnique(uniqueType: UniqueType, stateForConditionals: StateForConditionals =
StateForConditionals(this)) = getMatchingUniques(uniqueType, stateForConditionals).any()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@ class CivInfoTransientCache(val civInfo: Civilization) {
newDetailedCivResources.subtractResourceRequirements(
unit.baseUnit.getResourceRequirementsPerTurn(), civInfo.gameInfo.ruleset, "Units")

newDetailedCivResources.removeAll { it.resource.hasUnique(UniqueType.CityResource) }

// 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ class TileInfoImprovementFunctions(val tile: Tile) {
yield(ImprovementBuildingProblem.Obsolete)

if (improvement.getMatchingUniques(UniqueType.ConsumesResources, stateForConditionals)
.any { civInfo.getCivResourcesByName()[it.params[1]]!! < it.params[0].toInt() })
.any { civInfo.getResourceAmount(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() })
.any { civInfo.getResourceAmount(it.params[1]) < it.params[0].toInt() })
yield(ImprovementBuildingProblem.MissingResources)

val knownFeatureRemovals = tile.ruleset.tileImprovements.values
Expand Down
8 changes: 3 additions & 5 deletions core/src/com/unciv/logic/trade/TradeEvaluation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,7 @@ class TradeEvaluation {
}

TradeType.Strategic_Resource -> {
val resources = civInfo.getCivResourcesByName()
val amountWillingToBuy = 2 - resources[offer.name]!!
val amountWillingToBuy = 2 - civInfo.getResourceAmount(offer.name)
if (amountWillingToBuy <= 0) return 0 // we already have enough.
val amountToBuyInOffer = min(amountWillingToBuy, offer.amount)

Expand Down Expand Up @@ -200,7 +199,7 @@ class TradeEvaluation {
}
TradeType.Luxury_Resource -> {
return when {
civInfo.getCivResourcesByName()[offer.name]!! > 1 -> 250 // fair price
civInfo.getResourceAmount(offer.name) > 1 -> 250 // fair price
civInfo.hasUnique(UniqueType.RetainHappinessFromLuxury) -> // If we retain 50% happiness, value at 375
750 - (civInfo.getMatchingUniques(UniqueType.RetainHappinessFromLuxury)
.first().params[0].toPercent() * 250).toInt()
Expand All @@ -221,8 +220,7 @@ class TradeEvaluation {
&& it.isBuildable(civInfo) }
if (!canUseForUnits) return 50 * offer.amount

val amountLeft = civInfo.getCivResourcesByName()[offer.name]
?: throw Exception("Got a strategic resource offer but the the resource doesn't exist in this ruleset!")
val amountLeft = civInfo.getResourceAmount(offer.name)

// Each strategic resource starts costing 100 more when we ass the 5 resources baseline
// That is to say, if I have 4 and you take one away, that's 200
Expand Down
15 changes: 5 additions & 10 deletions core/src/com/unciv/models/ruleset/Building.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueFlag
import com.unciv.models.ruleset.unique.UniqueParameterType
import com.unciv.models.ruleset.unique.UniqueTarget
import com.unciv.models.ruleset.unique.UniqueTriggerActivation
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats
Expand All @@ -23,8 +22,6 @@ import com.unciv.ui.components.extensions.getConsumesAmountString
import com.unciv.ui.components.extensions.getNeedMoreAmountString
import com.unciv.ui.components.extensions.toPercent
import com.unciv.ui.screens.civilopediascreen.FormattedLine
import com.unciv.utils.Concurrency
import kotlin.math.pow


class Building : RulesetStatsObject(), INonPerpetualConstruction {
Expand Down Expand Up @@ -119,10 +116,8 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
if (isWonder) translatedLines += "Wonder".tr()
if (isNationalWonder) translatedLines += "National Wonder".tr()
if (!isFree) {
val availableResources = if (!showAdditionalInfo) emptyMap()
else city.civ.getCivResourcesByName()
for ((resourceName, amount) in getResourceRequirementsPerTurn()) {
val available = availableResources[resourceName] ?: 0
val available = city.getResourceAmount(resourceName)
val resource = city.getRuleset().tileResources[resourceName] ?: continue
val consumesString = resourceName.getConsumesAmountString(amount, resource.isStockpiled())

Expand Down Expand Up @@ -617,10 +612,10 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
yield(RejectionReasonType.RequiresBuildingInThisCity.toInstance("Requires a [${civ.getEquivalentBuilding(requiredBuilding!!)}] in this city"))
}

for ((resource, requiredAmount) in getResourceRequirementsPerTurn()) {
val availableAmount = civ.getCivResourcesByName()[resource]!!
for ((resourceName, requiredAmount) in getResourceRequirementsPerTurn()) {
val availableAmount = cityConstructions.city.getResourceAmount(resourceName)
if (availableAmount < requiredAmount) {
yield(RejectionReasonType.ConsumesResources.toInstance(resource.getNeedMoreAmountString(requiredAmount - availableAmount)))
yield(RejectionReasonType.ConsumesResources.toInstance(resourceName.getNeedMoreAmountString(requiredAmount - availableAmount)))
}
}

Expand All @@ -644,7 +639,7 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {

override fun postBuildEvent(cityConstructions: CityConstructions, boughtWith: Stat?): Boolean {
val civInfo = cityConstructions.city.civ

if (civInfo.gameInfo.spaceResources.contains(name)) {
civInfo.victoryManager.currentsSpaceshipParts.add(name, 1)
return true
Expand Down
20 changes: 12 additions & 8 deletions core/src/com/unciv/models/ruleset/unique/Unique.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s
fun hasTriggerConditional(): Boolean {
if(conditionals.none()) return false
return conditionals.any{ conditional -> conditional.type?.targetTypes
?.any{ it.canAcceptUniqueTarget(UniqueTarget.TriggerCondition) || it.canAcceptUniqueTarget(UniqueTarget.UnitActionModifier) }
?.any{ it.canAcceptUniqueTarget(UniqueTarget.TriggerCondition) || it.canAcceptUniqueTarget(UniqueTarget.UnitActionModifier) }
?: false
}
}
Expand Down Expand Up @@ -153,24 +153,28 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s

val stateBasedRandom by lazy { Random(state.hashCode()) }

fun getResourceAmount(resourceName: String): Int {
if (state.city != null) return state.city.getResourceAmount(resourceName)
if (state.civInfo != null) return state.civInfo.getResourceAmount(resourceName)
return 0
}

return when (condition.type) {
// These are 'what to do' and not 'when to do' conditionals
UniqueType.ConditionalTimedUnique -> true

UniqueType.ConditionalChance -> stateBasedRandom.nextFloat() < condition.params[0].toFloat() / 100f
UniqueType.ConditionalBeforeTurns -> state.civInfo != null && state.civInfo.gameInfo.turns < condition.params[0].toInt()
UniqueType.ConditionalAfterTurns -> state.civInfo != null && state.civInfo.gameInfo.turns >= condition.params[0].toInt()

UniqueType.ConditionalChance -> stateBasedRandom.nextFloat() < condition.params[0].toFloat() / 100f

UniqueType.ConditionalNationFilter -> state.civInfo?.nation?.matchesFilter(condition.params[0]) == true
UniqueType.ConditionalWar -> state.civInfo?.isAtWar() == true
UniqueType.ConditionalNotWar -> state.civInfo?.isAtWar() == false
UniqueType.ConditionalWithResource -> state.civInfo?.hasResource(condition.params[0]) == true
UniqueType.ConditionalWithoutResource -> state.civInfo?.hasResource(condition.params[0]) == false
UniqueType.ConditionalWhenAboveAmountResource -> state.civInfo != null
&& state.civInfo.getCivResourcesByName()[condition.params[1]]!! > condition.params[0].toInt()
UniqueType.ConditionalWhenBelowAmountResource -> state.civInfo != null
&& state.civInfo.getCivResourcesByName()[condition.params[1]]!! < condition.params[0].toInt()
UniqueType.ConditionalWithResource -> getResourceAmount(condition.params[0]) > 0
UniqueType.ConditionalWithoutResource -> getResourceAmount(condition.params[0]) <= 0
UniqueType.ConditionalWhenAboveAmountResource -> getResourceAmount(condition.params[1]) > condition.params[0].toInt()
UniqueType.ConditionalWhenBelowAmountResource -> getResourceAmount(condition.params[1]) < condition.params[0].toInt()
UniqueType.ConditionalHappy ->
state.civInfo != null && state.civInfo.stats.happiness >= 0
UniqueType.ConditionalBetweenHappiness ->
Expand Down
1 change: 1 addition & 0 deletions core/src/com/unciv/models/ruleset/unique/UniqueType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
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),
CityResource("City-level resource", UniqueTarget.Resource),
CannotBeTraded("Cannot be traded", UniqueTarget.Resource),
NotShownOnWorldScreen("Not shown on world screen", UniqueTarget.Resource, flags = UniqueFlag.setOfHiddenToUsers),

Expand Down
15 changes: 15 additions & 0 deletions core/src/com/unciv/ui/screens/cityscreen/CityStatsTable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import com.unciv.UncivGame
import com.unciv.logic.city.City
import com.unciv.logic.city.CityFlags
import com.unciv.logic.city.CityFocus
import com.unciv.models.Counter
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat
import com.unciv.models.translations.tr
Expand Down Expand Up @@ -178,6 +180,19 @@ class CityStatsTable(private val cityScreen: CityScreen): Table() {
tableWithIcons.add("In resistance for another [${city.getFlag(CityFlags.Resistance)}] turns".toLabel()).row()
}

val resourceTable = Table()

val resourceCounter = Counter<TileResource>()
for (resourceSupply in city.getCityResources()) resourceCounter.add(resourceSupply.resource, resourceSupply.amount)
for ((resource, amount) in resourceCounter)
if (resource.hasUnique(UniqueType.CityResource)){
resourceTable.add(amount.toLabel())
resourceTable.add(ImageGetter.getResourcePortrait(resource.name, 20f))
.padRight(5f)
}
if (resourceTable.cells.notEmpty())
tableWithIcons.add(resourceTable)

val (wltkIcon: Actor?, wltkLabel: Label?) = when {
city.isWeLoveTheKingDayActive() ->
ImageGetter.getStatIcon("Food") to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import com.unciv.models.translations.tr
import com.unciv.ui.components.Fonts
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
import com.unciv.ui.components.extensions.disable
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.input.keyShortcuts
import com.unciv.ui.components.input.onClick
import com.unciv.ui.components.input.onDoubleClick
import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.images.ImageGetter
import kotlin.math.roundToInt

Expand Down Expand Up @@ -143,7 +143,7 @@ class ImprovementPickerScreen(
proposedSolutions.add("Have this tile inside your empire")
if (ImprovementBuildingProblem.MissingResources in unbuildableBecause) {
proposedSolutions.addAll(improvement.getMatchingUniques(UniqueType.ConsumesResources).filter {
currentPlayerCiv.getCivResourcesByName()[it.params[1]]!! < it.params[0].toInt()
currentPlayerCiv.getResourceAmount(it.params[1]) < it.params[0].toInt()
}.map { "Acquire more [$it]" })
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ class WorldScreenTopBar(val worldScreen: WorldScreen) : Table() {
}

val strategicResources = worldScreen.gameInfo.ruleset.tileResources.values
.filter { it.resourceType == ResourceType.Strategic }
.filter { it.resourceType == ResourceType.Strategic && !it.hasUnique(UniqueType.CityResource) }
for (resource in strategicResources) {
val resourceImage = ImageGetter.getResourcePortrait(resource.name, 20f)
val resourceLabel = "0".toLabel()
Expand Down
3 changes: 3 additions & 0 deletions docs/Modders/uniques.md
Original file line number Diff line number Diff line change
Expand Up @@ -1656,6 +1656,9 @@ Simple unique parameters are explained by mouseover. Complex parameters are expl
??? example "Stockpiled"
Applicable to: Resource

??? example "City-level resource"
Applicable to: Resource

??? example "Cannot be traded"
Applicable to: Resource

Expand Down
3 changes: 1 addition & 2 deletions tests/src/com/unciv/uniques/UnitUniquesTests.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.unciv.uniques

import com.badlogic.gdx.math.Vector2
import com.sun.source.tree.AssertTree
import com.unciv.logic.map.mapunit.UnitTurnManager
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.fillPlaceholders
Expand Down Expand Up @@ -83,7 +82,7 @@ class UnitUniquesTests {
civ.tech.addTechnology("Iron Working")
// capital already owns tile, but this relinquishes first - shouldn't require manual setTerrainTransients, updateCivResources called automatically
capital.expansion.takeOwnership(ironTile)
val ironAvailable = civ.getCivResourcesByName()["Iron"] ?: 0
val ironAvailable = civ.getResourceAmount("Iron")
Assert.assertTrue("Test preparation failed to add Iron to Civ resources", ironAvailable >= 3)

// See if that same Engineer could create a Manufactory NOW
Expand Down