Prevent selling free buildings (#10094)

* A few yield extensions - use in existing code to do later

* Refactor getFreeBuildings to allow hasFreeBuilding not enumerating all

* Prevent selling free buildings - with a little easter egg

* Test translatability

* Shift "Free Building" methods towards preferring object parameters

* Remove easter egg

* Linting and improving Kdoc precision

* Linting and improving Kdoc precision: CityConstructions
This commit is contained in:
SomeTroglodyte 2023-09-13 09:27:32 +02:00 committed by GitHub
parent 6016754a18
commit 8aeae30050
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 111 additions and 59 deletions

View File

@ -622,7 +622,7 @@ object NextTurnAutomation {
city.cityConstructions.isBuilt(it.name)
&& it.requiresResource(resource)
&& it.isSellable()
&& it.name !in civInfo.civConstructions.getFreeBuildings(city.id) }
&& !civInfo.civConstructions.hasFreeBuilding(city, it) }
.randomOrNull()
if (buildingToSell != null) {
city.sellBuilding(buildingToSell)

View File

@ -255,7 +255,7 @@ class City : IsPartOfGameInfoSerialization {
}
private fun manageCityResourcesRequiredByBuildings(cityResources: ResourceSupplyList) {
val freeBuildings = civ.civConstructions.getFreeBuildings(id)
val freeBuildings = civ.civConstructions.getFreeBuildingNames(this)
for (building in cityConstructions.getBuiltBuildings()) {
// Free buildings cost no resources
if (building.name in freeBuildings) continue

View File

@ -79,7 +79,9 @@ class CityConstructions : IsPartOfGameInfoSerialization {
var productionOverflow = 0
private val queueMaxSize = 10
// Maps cities to the buildings they received
/** Maps cities by id to a set of the buildings they received (by nation equivalent name)
* Source: [UniqueType.GainFreeBuildings]
*/
val freeBuildingsProvidedFromThisCity: HashMap<String, HashSet<String>> = hashMapOf()
//endregion
@ -122,7 +124,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
*/
fun getMaintenanceCosts(): Int {
var maintenanceCost = 0
val freeBuildings = city.civ.civConstructions.getFreeBuildings(city.id)
val freeBuildings = city.civ.civConstructions.getFreeBuildingNames(city)
for (building in getBuiltBuildings())
if (building.name !in freeBuildings)
@ -602,10 +604,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
for (city in citiesThatApply) {
if (city.cityConstructions.containsBuildingOrEquivalent(freeBuilding.name)) continue
city.cityConstructions.addBuilding(freeBuilding)
if (city.id !in freeBuildingsProvidedFromThisCity)
freeBuildingsProvidedFromThisCity[city.id] = hashSetOf()
freeBuildingsProvidedFromThisCity[city.id]!!.add(freeBuilding.name)
freeBuildingsProvidedFromThisCity.getOrPut(city.id) { hashSetOf() }.add(freeBuilding.name)
}
}
@ -613,9 +612,7 @@ class CityConstructions : IsPartOfGameInfoSerialization {
for (unique in city.civ.getMatchingUniques(UniqueType.GainFreeBuildings, stateForConditionals = StateForConditionals(city.civ, city))) {
val freeBuilding = city.civ.getEquivalentBuilding(unique.params[0])
if (city.matchesFilter(unique.params[1])) {
if (city.id !in freeBuildingsProvidedFromThisCity)
freeBuildingsProvidedFromThisCity[city.id] = hashSetOf()
freeBuildingsProvidedFromThisCity[city.id]!!.add(freeBuilding.name)
freeBuildingsProvidedFromThisCity.getOrPut(city.id) { hashSetOf() }.add(freeBuilding.name)
if (!isBuilt(freeBuilding.name))
addBuilding(freeBuilding)
}

View File

@ -50,7 +50,7 @@ class CityConquestFunctions(val city: City){
private fun removeBuildingsOnMoveToCiv(oldCiv: Civilization) {
// Remove all buildings provided for free to this city
for (building in city.civ.civConstructions.getFreeBuildings(city.id)) {
for (building in city.civ.civConstructions.getFreeBuildingNames(city)) {
city.cityConstructions.removeBuilding(building)
}

View File

@ -1,46 +1,53 @@
package com.unciv.logic.civilization
import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.logic.city.City
import com.unciv.models.Counter
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.INonPerpetualConstruction
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.stats.Stat
import com.unciv.ui.components.extensions.yieldAllNotNull
class CivConstructions : IsPartOfGameInfoSerialization {
@Transient
lateinit var civInfo: Civilization
// Maps objects to the amount of times bought
/** Maps construction names to the amount of times bought */
val boughtItemsWithIncreasingPrice: Counter<String> = Counter()
// Maps to cities to all free buildings they contain
/** Maps cities by id to a set of all free buildings by name they contain.
* The building name is the Nation-specific equivalent if available.
* Sources: [UniqueType.FreeStatBuildings] **and** [UniqueType.FreeSpecificBuildings]
* This is persisted and _never_ cleared or elements removed (per civ and game).
*/
private val freeBuildings: HashMap<String, HashSet<String>> = hashMapOf()
// Maps stats to the cities that have received a building of that stat
// We can't use an enum instead of a string, due to the inability of the JSON-parser
/** Maps stat names to a set of cities by id that have received a building of that stat.
* Source: [UniqueType.FreeStatBuildings]
* This is persisted and _never_ cleared or elements removed (per civ and game).
*/
// We can't use the Stat enum instead of a string, due to the inability of the JSON-parser
// to function properly and forcing this to be an `HashMap<String, HashSet<String>>`
// when loading, even if this wasn't the original type, leading to run-time errors.
private val freeStatBuildingsProvided: HashMap<String, HashSet<String>> = hashMapOf()
// Maps buildings to the cities that have received that building
/** Maps buildings by name to a set of cities by id that have received that building.
* The building name is the Nation-specific equivalent if available.
* Source: [UniqueType.FreeSpecificBuildings]
* This is persisted and _never_ cleared or elements removed (per civ and game).
*/
private val freeSpecificBuildingsProvided: HashMap<String, HashSet<String>> = hashMapOf()
init {
for (stat in Stat.values()) {
freeStatBuildingsProvided[stat.name] = hashSetOf()
}
}
fun clone(): CivConstructions {
val toReturn = CivConstructions()
toReturn.civInfo = civInfo
toReturn.freeBuildings.putAll(freeBuildings)
toReturn.freeStatBuildingsProvided.putAll(freeStatBuildingsProvided)
toReturn.freeSpecificBuildingsProvided.putAll(freeSpecificBuildingsProvided)
toReturn.boughtItemsWithIncreasingPrice.add(boughtItemsWithIncreasingPrice.clone())
toReturn.boughtItemsWithIncreasingPrice.add(boughtItemsWithIncreasingPrice) // add copies
return toReturn
}
@ -57,21 +64,32 @@ class CivConstructions : IsPartOfGameInfoSerialization {
addFreeSpecificBuildings()
}
fun getFreeBuildings(cityId: String): HashSet<String> {
val toReturn = freeBuildings[cityId] ?: hashSetOf()
/** Common to [hasFreeBuildingByName] and [getFreeBuildingNames] - 'has' doesn't need the whole set, one enumeration is enough.
* Note: Operates on String city.id and String building name, close to the serialized and stored form.
* When/if we do a transient cache for these using our objects, please rewrite this.
*/
private fun getFreeBuildingNamesSequence(cityId: String) = sequence {
yieldAllNotNull(freeBuildings[cityId])
for (city in civInfo.cities) {
val freeBuildingsProvided =
city.cityConstructions.freeBuildingsProvidedFromThisCity[cityId]
if (freeBuildingsProvided != null)
toReturn.addAll(freeBuildingsProvided)
yieldAllNotNull(city.cityConstructions.freeBuildingsProvidedFromThisCity[cityId])
}
return toReturn
}
/** Gets a Set of all building names the [city] has for free, from nationwide sources or buildings in other cities */
fun getFreeBuildingNames(city: City) =
getFreeBuildingNamesSequence(city.id).toSet()
/** Tests whether the [city] has [building] for free, from nationwide sources or buildings in other cities */
fun hasFreeBuilding(city: City, building: Building) =
hasFreeBuildingByName(city.id, building.name)
/** Tests whether a city by [cityId] has a building named [buildingName] for free, from nationwide sources or buildings in other cities */
private fun hasFreeBuildingByName(cityId: String, buildingName: String) =
getFreeBuildingNamesSequence(cityId).contains(buildingName)
private fun addFreeBuilding(cityId: String, building: String) {
if (!freeBuildings.containsKey(cityId))
freeBuildings[cityId] = hashSetOf()
freeBuildings[cityId]!!.add(civInfo.getEquivalentBuilding(building).name)
freeBuildings.getOrPut(cityId) { hashSetOf() }
.add(civInfo.getEquivalentBuilding(building).name)
}
private fun addFreeStatsBuildings() {
@ -79,7 +97,6 @@ class CivConstructions : IsPartOfGameInfoSerialization {
.groupBy { it.params[0] }
.mapKeys { Stat.valueOf(it.key) }
.mapValues { unique -> unique.value.sumOf { it.params[1].toInt() } }
.toMutableMap()
for ((stat, amount) in statUniquesData) {
addFreeStatBuildings(stat, amount)
@ -88,11 +105,12 @@ class CivConstructions : IsPartOfGameInfoSerialization {
private fun addFreeStatBuildings(stat: Stat, amount: Int) {
for (city in civInfo.cities.take(amount)) {
if (freeStatBuildingsProvided[stat.name]!!.contains(city.id) || !city.cityConstructions.hasBuildableStatBuildings(stat)) continue
if (freeStatBuildingsProvided[stat.name]?.contains(city.id) == true) continue
if (!city.cityConstructions.hasBuildableStatBuildings(stat)) continue
val builtBuilding = city.cityConstructions.addCheapestBuildableStatBuilding(stat)
if (builtBuilding != null) {
freeStatBuildingsProvided[stat.name]!!.add(city.id)
freeStatBuildingsProvided.getOrPut(stat.name) { hashSetOf() }.add(city.id)
addFreeBuilding(city.id, builtBuilding)
}
}
@ -117,14 +135,18 @@ class CivConstructions : IsPartOfGameInfoSerialization {
building.postBuildEvent(city.cityConstructions)
if (!freeSpecificBuildingsProvided.containsKey(building.name))
freeSpecificBuildingsProvided[building.name] = hashSetOf()
freeSpecificBuildingsProvided[building.name]!!.add(city.id)
freeSpecificBuildingsProvided.getOrPut(building.name) { hashSetOf() }.add(city.id)
addFreeBuilding(city.id, building.name)
}
}
/** Calculates a civ-wide total for [objectToCount].
*
* It counts:
* * "Spaceship part" units added to "spaceship" in capital
* * Built buildings or those in a construction queue
* * Units on the map or being constructed
*/
fun countConstructedObjects(objectToCount: INonPerpetualConstruction): Int {
val amountInSpaceShip = civInfo.victoryManager.currentsSpaceshipParts[objectToCount.name]

View File

@ -109,7 +109,7 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
fun getDescription(city: City, showAdditionalInfo: Boolean): String {
val stats = getStats(city)
val translatedLines = ArrayList<String>() // Some translations require special handling
val isFree = name in city.civ.civConstructions.getFreeBuildings(city.id)
val isFree = city.civ.civConstructions.hasFreeBuilding(city, this)
if (uniqueTo != null) translatedLines += if (replaces == null) "Unique to [$uniqueTo]".tr()
else "Unique to [$uniqueTo], replaces [$replaces]".tr()
val missingUnique = getMatchingUniques(UniqueType.RequiresBuildingInAllCities).firstOrNull()
@ -467,7 +467,7 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
for (unique in uniqueObjects) {
if (unique.type != UniqueType.OnlyAvailableWhen &&
!unique.conditionalsApply(StateForConditionals(civ, cityConstructions.city))) continue
@Suppress("NON_EXHAUSTIVE_WHEN")
when (unique.type) {
// for buildings that are created as side effects of other things, and not directly built,

View File

@ -68,3 +68,18 @@ fun <T> Iterable<T>.toGdxArray(): Array<T> {
for (it in this) arr.add(it)
return arr
}
/** [yield][SequenceScope.yield]s [element] if it's not null */
suspend fun <T> SequenceScope<T>.yieldIfNotNull(element: T?) {
if (element != null) yield(element)
}
/** [yield][SequenceScope.yield]s all elements of [elements] if it's not null */
suspend fun <T> SequenceScope<T>.yieldAllNotNull(elements: Iterable<T>?) {
if (elements != null) yieldAll(elements)
}
@JvmName("yieldAllNotNullNotNull")
/** [yield][SequenceScope.yield]s all non-null elements of [elements] if it's not null */
suspend fun <T> SequenceScope<T>.yieldAllNotNull(elements: Iterable<T?>?) {
if (elements == null) return
for (element in elements) yieldIfNotNull(element)
}

View File

@ -440,6 +440,10 @@ class CityScreen(
update()
}
/** Convenience shortcut to [CivConstructions.hasFreeBuilding][com.unciv.logic.civilization.CivConstructions.hasFreeBuilding], nothing more */
internal fun hasFreeBuilding(building: Building) =
city.civ.civConstructions.hasFreeBuilding(city, building)
fun selectConstruction(name: String) {
selectConstruction(city.cityConstructions.getConstruction(name))
}

View File

@ -285,7 +285,7 @@ class CityStatsTable(private val cityScreen: CityScreen): Table() {
val statsAndSpecialists = Table()
val icon = ImageGetter.getConstructionPortrait(building.name, 50f)
val isFree = building.name in cityScreen.city.civ.civConstructions.getFreeBuildings(cityScreen.city.id)
val isFree = cityScreen.hasFreeBuilding(building)
val displayName = if (isFree) "{${building.name}} ({Free})" else building.name
info.add(displayName.toLabel(fontSize = Constants.defaultFontSize, hideIcons = true)).padBottom(5f).right().row()

View File

@ -16,6 +16,7 @@ import com.unciv.models.translations.tr
import com.unciv.ui.components.Fonts
import com.unciv.ui.components.extensions.darken
import com.unciv.ui.components.extensions.disable
import com.unciv.ui.components.extensions.isEnabled
import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.components.input.onClick
import com.unciv.ui.images.ImageGetter
@ -99,22 +100,15 @@ class ConstructionInfoTable(val cityScreen: CityScreen): Table() {
row()
add(sellBuildingButton).padTop(5f).colspan(2).center()
sellBuildingButton.onClick(UncivSound.Coin) {
val isFree = cityScreen.hasFreeBuilding(construction)
val enableSell = !isFree &&
!cityScreen.city.isPuppet &&
cityScreen.canChangeState &&
(!cityScreen.city.hasSoldBuildingThisTurn || cityScreen.city.civ.gameInfo.gameParameters.godMode)
sellBuildingButton.isEnabled = enableSell
if (sellBuildingButton.isEnabled) sellBuildingButton.onClick(UncivSound.Coin) {
sellBuildingButton.disable()
cityScreen.closeAllPopups()
ConfirmPopup(
cityScreen,
"Are you sure you want to sell this [${construction.name}]?",
sellText,
restoreDefault = {
cityScreen.update()
}
) {
cityScreen.city.sellBuilding(construction)
cityScreen.clearSelection()
cityScreen.update()
}.open()
sellBuildingClicked(construction, isFree, sellText)
}
if (cityScreen.city.hasSoldBuildingThisTurn && !cityScreen.city.civ.gameInfo.gameParameters.godMode
@ -125,4 +119,24 @@ class ConstructionInfoTable(val cityScreen: CityScreen): Table() {
}
}
private fun sellBuildingClicked(construction: Building, isFree: Boolean, sellText: String) {
cityScreen.closeAllPopups()
ConfirmPopup(
cityScreen,
"Are you sure you want to sell this [${construction.name}]?",
sellText,
restoreDefault = {
cityScreen.update()
}
) {
sellBuildingConfirmed(construction, isFree)
}.open()
}
private fun sellBuildingConfirmed(construction: Building, isFree: Boolean) {
cityScreen.city.sellBuilding(construction)
cityScreen.clearSelection()
cityScreen.update()
}
}