From 4b7edca7a8f0f5daf0b511c3172e7b4657345bab Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Sun, 8 May 2022 20:22:01 +0200 Subject: [PATCH] Rework of the "Creates improvement on a specific tile" Unique (#6687) * Make Citadel tile takeover a unique * CreatesOneImprovement unique overhaul step 1 * CreatesOneImprovement unique overhaul - increase highlights alpha * Fix missing translatables, again * CreatesOneImprovement unique overhaul - review suggestions Co-authored-by: Yair Morgenstern --- .../TileImprovements.json | 8 +- .../Civ V - Vanilla/TileImprovements.json | 8 +- .../jsons/translations/template.properties | 1 + core/src/com/unciv/Constants.kt | 1 - .../com/unciv/logic/automation/Automation.kt | 42 +++- .../automation/ConstructionAutomation.kt | 34 ++- core/src/com/unciv/logic/battle/Battle.kt | 16 +- .../com/unciv/logic/city/CityConstructions.kt | 235 ++++++++++++------ .../unciv/logic/city/CityExpansionManager.kt | 2 + core/src/com/unciv/logic/city/CityInfo.kt | 4 +- core/src/com/unciv/logic/map/MapUnit.kt | 9 +- core/src/com/unciv/logic/map/TileInfo.kt | 48 +++- core/src/com/unciv/models/ruleset/Building.kt | 29 ++- core/src/com/unciv/models/ruleset/Ruleset.kt | 3 - .../ruleset/unique/UniqueParameterType.kt | 6 +- .../unciv/models/ruleset/unique/UniqueType.kt | 11 +- .../ui/cityscreen/CityConstructionsTable.kt | 98 +++++--- .../src/com/unciv/ui/cityscreen/CityScreen.kt | 181 ++++++++++---- .../ui/cityscreen/CityScreenTileTable.kt | 2 +- .../pickerscreens/ImprovementPickerScreen.kt | 18 +- .../unciv/ui/worldscreen/unit/UnitActions.kt | 31 +-- 21 files changed, 535 insertions(+), 252 deletions(-) diff --git a/android/assets/jsons/Civ V - Gods & Kings/TileImprovements.json b/android/assets/jsons/Civ V - Gods & Kings/TileImprovements.json index 846241979e..f03e63ea19 100644 --- a/android/assets/jsons/Civ V - Gods & Kings/TileImprovements.json +++ b/android/assets/jsons/Civ V - Gods & Kings/TileImprovements.json @@ -198,8 +198,12 @@ }, { "name": "Citadel", - "uniques": ["Great Improvement", "Gives a defensive bonus of [100]%", "Adjacent enemy units ending their turn take [30] damage", "Can be built just outside your borders"], - "civilopediaText": [{"text":"Constructing it will take over the tiles around it and assign them to your closest city"}] + "uniques": [ + "Great Improvement", + "Gives a defensive bonus of [100]%", + "Adjacent enemy units ending their turn take [30] damage", + "Can be built just outside your borders", + "Constructing it will take over the tiles around it and assign them to your closest city"] }, //Civilization unique improvements diff --git a/android/assets/jsons/Civ V - Vanilla/TileImprovements.json b/android/assets/jsons/Civ V - Vanilla/TileImprovements.json index b8db1dcf6b..35f69d1e1f 100644 --- a/android/assets/jsons/Civ V - Vanilla/TileImprovements.json +++ b/android/assets/jsons/Civ V - Vanilla/TileImprovements.json @@ -198,8 +198,12 @@ }, { "name": "Citadel", - "uniques": ["Great Improvement", "Gives a defensive bonus of [100]%", "Adjacent enemy units ending their turn take [30] damage", "Can be built just outside your borders"], - "civilopediaText": [{"text":"Constructing it will take over the tiles around it and assign them to your closest city"}] + "uniques": [ + "Great Improvement", + "Gives a defensive bonus of [100]%", + "Adjacent enemy units ending their turn take [30] damage", + "Can be built just outside your borders", + "Constructing it will take over the tiles around it and assign them to your closest city"] }, //Civilization unique improvements diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 1f902ac423..6b7b0852ea 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -953,6 +953,7 @@ Lock = Unlock = Move to city = Please enter a new name for your city = +Please select a tile for this building's [improvement] = # Ask for text or numbers popup UI diff --git a/core/src/com/unciv/Constants.kt b/core/src/com/unciv/Constants.kt index 1024f8b24b..251db39827 100644 --- a/core/src/com/unciv/Constants.kt +++ b/core/src/com/unciv/Constants.kt @@ -44,7 +44,6 @@ object Constants { const val unknownNationName = "???" const val fort = "Fort" - const val citadel = "Citadel" const val futureTech = "Future Tech" // Easter egg name. Is to avoid conflicts when players name their own religions. diff --git a/core/src/com/unciv/logic/automation/Automation.kt b/core/src/com/unciv/logic/automation/Automation.kt index 9a450337c2..d197747c6f 100644 --- a/core/src/com/unciv/logic/automation/Automation.kt +++ b/core/src/com/unciv/logic/automation/Automation.kt @@ -12,6 +12,7 @@ import com.unciv.models.ruleset.MilestoneType import com.unciv.models.ruleset.Victory import com.unciv.models.ruleset.Victory.Focus import com.unciv.models.ruleset.tile.ResourceType +import com.unciv.models.ruleset.tile.TileImprovement import com.unciv.models.ruleset.unique.LocalUniqueCache import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.BaseUnit @@ -169,9 +170,35 @@ object Automation { return true } + /** Checks both feasibility of Buildings with a CreatesOneImprovement unique + * and resource scarcity making a construction undesirable. + */ + fun allowAutomatedConstruction( + civInfo: CivilizationInfo, + cityInfo: CityInfo, + construction: INonPerpetualConstruction + ): Boolean { + return allowCreateImprovementBuildings(civInfo, cityInfo, construction) + && allowSpendingResource(civInfo, construction) + } + + /** Checks both feasibility of Buildings with a [UniqueType.CreatesOneImprovement] unique (appropriate tile available). + * Constructions without pass uncontested. */ + fun allowCreateImprovementBuildings( + civInfo: CivilizationInfo, + cityInfo: CityInfo, + construction: INonPerpetualConstruction + ): Boolean { + if (construction !is Building) return true + if (!construction.hasCreateOneImprovementUnique()) return true // redundant but faster??? + val improvement = construction.getImprovementToCreate(cityInfo.getRuleset()) ?: return true + return cityInfo.getTiles().any { + it.canBuildImprovement(improvement, civInfo) + } + } /** Determines whether the AI should be willing to spend strategic resources to build - * [construction] in [city], assumes that we are actually able to do so. */ + * [construction] for [civInfo], assumes that we are actually able to do so. */ fun allowSpendingResource(civInfo: CivilizationInfo, construction: INonPerpetualConstruction): Boolean { // City states do whatever they want if (civInfo.isCityState()) @@ -255,6 +282,15 @@ object Automation { } } + /** Support [UniqueType.CreatesOneImprovement] unique - find best tile for placement automation */ + fun getTileForConstructionImprovement(cityInfo: CityInfo, improvement: TileImprovement): TileInfo? { + return cityInfo.getTiles().filter { + it.canBuildImprovement(improvement, cityInfo.civInfo) + }.maxByOrNull { + rankTileForCityWork(it, cityInfo) + } + } + // Ranks a tile for any purpose except the expansion algorithm of cities internal fun rankTile(tile: TileInfo?, civInfo: CivilizationInfo): Float { if (tile == null) return 0f @@ -270,7 +306,7 @@ object Automation { } return rank } - + // Ranks a tile for the expansion algorithm of cities internal fun rankTileForExpansion(tile: TileInfo, cityInfo: CityInfo, localUniqueCache: LocalUniqueCache = LocalUniqueCache(false)): Int { @@ -326,7 +362,7 @@ object Automation { // Tiles not adjacent to owned land are very hard to acquire if (tile.neighbors.none { it.getCity() != null && it.getCity()!!.id == cityInfo.id }) score += 1000 - + return score } diff --git a/core/src/com/unciv/logic/automation/ConstructionAutomation.kt b/core/src/com/unciv/logic/automation/ConstructionAutomation.kt index 9834885503..6a5b1c10a5 100644 --- a/core/src/com/unciv/logic/automation/ConstructionAutomation.kt +++ b/core/src/com/unciv/logic/automation/ConstructionAutomation.kt @@ -55,17 +55,15 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ data class ConstructionChoice(val choice: String, var choiceModifier: Float, val remainingWork: Int) - private fun addChoice(choices: ArrayList, choice: String, choiceModifier: Float){ - choices.add(ConstructionChoice(choice,choiceModifier,cityConstructions.getRemainingWork(choice))) + private fun addChoice(choices: ArrayList, choice: String, choiceModifier: Float) { + choices.add(ConstructionChoice(choice, choiceModifier, cityConstructions.getRemainingWork(choice))) } fun chooseNextConstruction() { if (!UncivGame.Current.settings.autoAssignCityProduction - && civInfo.playerType == PlayerType.Human && !cityInfo.isPuppet - ) { + && civInfo.playerType == PlayerType.Human && !cityInfo.isPuppet) return - } if (cityConstructions.getCurrentConstruction() !is PerpetualConstruction) return // don't want to be stuck on these forever addFoodBuildingChoice() @@ -119,9 +117,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ .filter { it.getStatBuyCost(cityInfo, stat = Stat.Faith)!! <= civInfo.religionManager.storedFaith } .firstOrNull() ?: return - cityConstructions.purchaseConstruction(chosenItem.name, -1, false, stat=Stat.Faith) - } private fun addMilitaryUnitChoice() { @@ -152,7 +148,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ val buildableWorkboatUnits = buildableUnits .filter { it.hasUnique(UniqueType.CreateWaterImprovements) - && Automation.allowSpendingResource(civInfo, it) + && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } val alreadyHasWorkBoat = buildableWorkboatUnits.any() && !cityInfo.getTiles().any { @@ -182,7 +178,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ val workerEquivalents = buildableUnits .filter { it.hasUnique(UniqueType.BuildImprovements) - && Automation.allowSpendingResource(civInfo, it) + && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } if (workerEquivalents.none()) return // for mods with no worker units @@ -196,7 +192,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ private fun addCultureBuildingChoice() { val cultureBuilding = buildableNotWonders .filter { it.isStatRelated(Stat.Culture) - && Automation.allowSpendingResource(civInfo, it) }.minByOrNull { it.cost } + && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }.minByOrNull { it.cost } if (cultureBuilding != null) { var modifier = 0.5f if (cityInfo.cityStats.currentCityStats.culture == 0f) // It won't grow if we don't help it @@ -216,7 +212,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ private fun addOtherBuildingChoice() { val otherBuilding = buildableNotWonders - .filter { Automation.allowSpendingResource(civInfo, it) }.minByOrNull { it.cost } + .filter { Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }.minByOrNull { it.cost } if (otherBuilding != null) { val modifier = 0.6f addChoice(relativeCostEffectiveness, otherBuilding.name, modifier) @@ -256,7 +252,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ if (!buildableWonders.any()) return val highestPriorityWonder = buildableWonders - .filter { Automation.allowSpendingResource(civInfo, it) } + .filter { Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } .maxByOrNull { getWonderPriority(it) }!! val citiesBuildingWonders = civInfo.cities .count { it.cityConstructions.isBuildingWonder() } @@ -269,7 +265,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ private fun addUnitTrainingBuildingChoice() { val unitTrainingBuilding = buildableNotWonders.asSequence() .filter { it.hasUnique(UniqueType.UnitStartingExperience) - && Automation.allowSpendingResource(civInfo, it)}.minByOrNull { it.cost } + && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }.minByOrNull { it.cost } if (unitTrainingBuilding != null && (!civInfo.wantsToFocusOn(Focus.Culture) || isAtWar)) { var modifier = if (cityIsOverAverageProduction) 0.5f else 0.1f // You shouldn't be cranking out units anytime soon if (isAtWar) modifier *= 2 @@ -282,7 +278,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ private fun addDefenceBuildingChoice() { val defensiveBuilding = buildableNotWonders.asSequence() .filter { it.cityStrength > 0 - && Automation.allowSpendingResource(civInfo, it)}.minByOrNull { it.cost } + && Automation.allowAutomatedConstruction(civInfo, cityInfo, it)}.minByOrNull { it.cost } if (defensiveBuilding != null && (isAtWar || !civInfo.wantsToFocusOn(Victory.Focus.Culture))) { var modifier = 0.2f if (isAtWar) modifier = 0.5f @@ -300,7 +296,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ val happinessBuilding = buildableNotWonders.asSequence() .filter { (it.isStatRelated(Stat.Happiness) || it.uniques.contains("Remove extra unhappiness from annexed cities")) - && Automation.allowSpendingResource(civInfo, it)} + && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } .minByOrNull { it.cost } if (happinessBuilding != null) { var modifier = 1f @@ -315,7 +311,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ if (allTechsAreResearched) return val scienceBuilding = buildableNotWonders.asSequence() .filter { it.isStatRelated(Stat.Science) - && Automation.allowSpendingResource(civInfo, it)} + && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } .minByOrNull { it.cost } if (scienceBuilding != null) { var modifier = 1.1f @@ -327,7 +323,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ private fun addGoldBuildingChoice() { val goldBuilding = buildableNotWonders.asSequence().filter { it.isStatRelated(Stat.Gold) - && Automation.allowSpendingResource(civInfo, it)} + && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } .minByOrNull { it.cost } if (goldBuilding != null) { val modifier = if (civInfo.statsForNextTurn.gold < 0) 3f else 1.2f @@ -337,7 +333,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ private fun addProductionBuildingChoice() { val productionBuilding = buildableNotWonders.asSequence() - .filter { it.isStatRelated(Stat.Production) && Automation.allowSpendingResource(civInfo, it) } + .filter { it.isStatRelated(Stat.Production) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } .minByOrNull { it.cost } if (productionBuilding != null) { addChoice(relativeCostEffectiveness, productionBuilding.name, 1.5f) @@ -350,7 +346,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ .filter { (it.isStatRelated(Stat.Food) || it.hasUnique(UniqueType.CarryOverFood, conditionalState) - ) && Automation.allowSpendingResource(civInfo, it) + ) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }.minByOrNull { it.cost } if (foodBuilding != null) { var modifier = 1f diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index 560c491419..8ec8da15db 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -690,12 +690,12 @@ object Battle { for (civ in attackingCiv.getKnownCivs()) { civ.getDiplomacyManager(attackingCiv).setModifier(DiplomaticModifiers.UsedNuclearWeapons, -50f) } - + if (!attacker.isDefeated()) { attacker.unit.attacksThisTurn += 1 } } - + private fun doNukeExplosionForTile(attacker: MapUnitCombatant, tile: TileInfo, nukeStrength: Int) { // https://forums.civfanatics.com/resources/unit-guide-modern-future-units-g-k.25628/ // https://www.carlsguides.com/strategy/civilization5/units/aircraft-nukes.ph @@ -708,7 +708,7 @@ object Battle { if (civResources[resource]!! < 0 && !attacker.getCivInfo().isBarbarian()) damageModifierFromMissingResource *= 0.5f // I could not find a source for this number, but this felt about right } - + // Damage city and reduce its population val city = tile.getCity() if (city != null && tile.position == city.location) { @@ -716,7 +716,7 @@ object Battle { postBattleNotifications(attacker, CityCombatant(city), city.getCenterTile()) destroyIfDefeated(city.civInfo, attacker.getCivInfo()) } - + // Damage and/or destroy units on the tile for (unit in tile.getUnits().toList()) { // toList so if it's destroyed there's no concurrent modification val defender = MapUnitCombatant(unit) @@ -736,9 +736,7 @@ object Battle { if (tile.getTileImprovement()!!.hasUnique(UniqueType.Unpillagable)) { tile.improvement = null } else { - tile.turnsToImprovement = 2 - tile.improvementInProgress = tile.improvement - tile.improvement = null + tile.setPillaged() } } tile.roadStatus = RoadStatus.None @@ -756,7 +754,7 @@ object Battle { tile.addTerrainFeature("Fallout") } if (!tile.hasUnique(UniqueType.DestroyableByNukes)) return - + // Deprecated as of 3.19.19 -- If removed, the two successive `if`s above should be merged val destructionChance = if (tile.hasUnique(UniqueType.ResistsNukes)) 0.25f else 0.5f @@ -770,7 +768,7 @@ object Battle { // } } - + private fun doNukeExplosionDamageToCity(targetedCity: CityInfo, nukeStrength: Int, damageModifierFromMissingResource: Float) { if (nukeStrength > 1 && targetedCity.population.population < 5 && targetedCity.canBeDestroyed(true)) { targetedCity.destroyCity() diff --git a/core/src/com/unciv/logic/city/CityConstructions.kt b/core/src/com/unciv/logic/city/CityConstructions.kt index 34c4dc0fae..6b935f7b0c 100644 --- a/core/src/com/unciv/logic/city/CityConstructions.kt +++ b/core/src/com/unciv/logic/city/CityConstructions.kt @@ -1,9 +1,13 @@ package com.unciv.logic.city +import com.unciv.UncivGame +import com.unciv.logic.automation.Automation import com.unciv.logic.automation.ConstructionAutomation import com.unciv.logic.civilization.AlertType import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.civilization.PopupAlert +import com.unciv.logic.map.MapUnit // for Kdoc only +import com.unciv.logic.map.TileInfo import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.unique.LocalUniqueCache @@ -19,11 +23,13 @@ import com.unciv.ui.civilopedia.FormattedLine import com.unciv.ui.utils.Fonts import com.unciv.ui.utils.withItem import com.unciv.ui.utils.withoutItem +import com.unciv.ui.worldscreen.unit.UnitActions // for Kdoc only import java.util.* import kotlin.collections.ArrayList import kotlin.math.ceil import kotlin.math.roundToInt + /** * City constructions manager. * @@ -33,6 +39,7 @@ import kotlin.math.roundToInt * @property constructionQueue a list of constructions names enqueued */ class CityConstructions { + //region Non-Serialized Properties @Transient lateinit var cityInfo: CityInfo @@ -42,25 +49,32 @@ class CityConstructions { @Transient val builtBuildingUniqueMap = UniqueMap() - var builtBuildings = HashSet() - val inProgressConstructions = HashMap() + // No backing field, not serialized var currentConstructionFromQueue: String get() { - return if (constructionQueue.isEmpty()) "" - else constructionQueue.first() + return if (constructionQueue.isEmpty()) "" + else constructionQueue.first() } set(value) { if (constructionQueue.isEmpty()) constructionQueue.add(value) else constructionQueue[0] = value } + + //endregion + //region Serialized Fields + + var builtBuildings = HashSet() + val inProgressConstructions = HashMap() var currentConstructionIsUserSet = false var constructionQueue = mutableListOf() var productionOverflow = 0 - val queueMaxSize = 10 + private val queueMaxSize = 10 // Maps cities to the buildings they received val freeBuildingsProvidedFromThisCity: HashMap> = hashMapOf() - + + //endregion //region pure functions + fun clone(): CityConstructions { val toReturn = CityConstructions() toReturn.builtBuildings.addAll(builtBuildings) @@ -111,48 +125,13 @@ class CityConstructions { fun getCityProductionTextForCityButton(): String { val currentConstructionSnapshot = currentConstructionFromQueue // See below var result = currentConstructionSnapshot.tr() - if (currentConstructionSnapshot != "") { + if (currentConstructionSnapshot.isNotEmpty()) { val construction = PerpetualConstruction.perpetualConstructionsMap[currentConstructionSnapshot] - if (construction == null) result += getTurnsToConstructionString(currentConstructionSnapshot) - else result += construction.getProductionTooltip(cityInfo) + result += construction?.getProductionTooltip(cityInfo) + ?: getTurnsToConstructionString(currentConstructionSnapshot) } return result } - - fun addFreeBuildings() { - // "Gain a free [buildingName] [cityFilter]" - val freeBuildingUniques = cityInfo.getLocalMatchingUniques(UniqueType.GainFreeBuildings, StateForConditionals(cityInfo.civInfo, cityInfo)) - - for (unique in freeBuildingUniques) { - val freeBuildingName = cityInfo.civInfo.getEquivalentBuilding(unique.params[0]).name - val citiesThatApply = when (unique.params[1]) { - "in this city" -> listOf(cityInfo) - "in other cities" -> cityInfo.civInfo.cities.filter { it !== cityInfo } - else -> cityInfo.civInfo.cities.filter { it.matchesFilter(unique.params[1]) } - } - - for (city in citiesThatApply) { - if (city.cityConstructions.containsBuildingOrEquivalent(freeBuildingName)) continue - city.cityConstructions.addBuilding(freeBuildingName) - if (city.id !in freeBuildingsProvidedFromThisCity) - freeBuildingsProvidedFromThisCity[city.id] = hashSetOf() - - freeBuildingsProvidedFromThisCity[city.id]!!.add(freeBuildingName) - } - } - - // Civ-level uniques - for these only add free buildings from each city to itself to avoid weirdness on city conquest - for (unique in cityInfo.civInfo.getMatchingUniques(UniqueType.GainFreeBuildings, stateForConditionals = StateForConditionals(cityInfo.civInfo, cityInfo))) { - val freeBuildingName = cityInfo.civInfo.getEquivalentBuilding(unique.params[0]).name - if (cityInfo.matchesFilter(unique.params[1])) { - if (cityInfo.id !in freeBuildingsProvidedFromThisCity) - freeBuildingsProvidedFromThisCity[cityInfo.id] = hashSetOf() - freeBuildingsProvidedFromThisCity[cityInfo.id]!!.add(freeBuildingName) - if (!isBuilt(freeBuildingName)) - addBuilding(freeBuildingName) - } - } - } /** @constructionName needs to be a non-perpetual construction, else an empty string is returned */ internal fun getTurnsToConstructionString(constructionName: String, useStoredProduction:Boolean = true): String { @@ -214,7 +193,7 @@ class CityConstructions { // if the construction name is the same as the current construction, it isn't the first return constructionQueueIndex == constructionQueue.indexOfFirst { it == name } } - + internal fun getConstruction(constructionName: String): IConstruction { val gameBasics = cityInfo.getRuleset() @@ -284,9 +263,17 @@ class CityConstructions { return ceil((workLeft-productionOverflow) / production.toDouble()).toInt() } - //endregion + fun hasBuildableStatBuildings(stat: Stat): Boolean { + return getBasicStatBuildings(stat) + .map { cityInfo.civInfo.getEquivalentBuilding(it.name) } + .filter { it.isBuildable(this) || isBeingConstructedOrEnqueued(it.name) } + .any() + } + + //endregion //region state changing functions + fun setTransients() { builtBuildingObjects = ArrayList(builtBuildings.map { cityInfo.getRuleset().buildings[it] @@ -362,8 +349,8 @@ class CityConstructions { val construction = getConstruction(constructionName) // Perpetual constructions should always still be valid (I hope) if (construction is PerpetualConstruction) continue - - val rejectionReasons = + + val rejectionReasons = (construction as INonPerpetualConstruction).getRejectionReasons(this) if (rejectionReasons.hasAReasonToBeRemovedFromQueue()) { @@ -432,7 +419,7 @@ class CityConstructions { cityInfo.civInfo.addNotification("[${construction.name}] has been built in [" + cityInfo.name + "]", cityInfo.location, NotificationIcon.Construction, icon) } - + if (construction is Building && construction.hasUnique(UniqueType.TriggersAlertOnCompletion, StateForConditionals(cityInfo.civInfo, cityInfo) )) { @@ -469,6 +456,41 @@ class CityConstructions { builtBuildingUniqueMap.addUnique(unique) } + fun addFreeBuildings() { + // "Gain a free [buildingName] [cityFilter]" + val freeBuildingUniques = cityInfo.getLocalMatchingUniques(UniqueType.GainFreeBuildings, StateForConditionals(cityInfo.civInfo, cityInfo)) + + for (unique in freeBuildingUniques) { + val freeBuildingName = cityInfo.civInfo.getEquivalentBuilding(unique.params[0]).name + val citiesThatApply = when (unique.params[1]) { + "in this city" -> listOf(cityInfo) + "in other cities" -> cityInfo.civInfo.cities.filter { it !== cityInfo } + else -> cityInfo.civInfo.cities.filter { it.matchesFilter(unique.params[1]) } + } + + for (city in citiesThatApply) { + if (city.cityConstructions.containsBuildingOrEquivalent(freeBuildingName)) continue + city.cityConstructions.addBuilding(freeBuildingName) + if (city.id !in freeBuildingsProvidedFromThisCity) + freeBuildingsProvidedFromThisCity[city.id] = hashSetOf() + + freeBuildingsProvidedFromThisCity[city.id]!!.add(freeBuildingName) + } + } + + // Civ-level uniques - for these only add free buildings from each city to itself to avoid weirdness on city conquest + for (unique in cityInfo.civInfo.getMatchingUniques(UniqueType.GainFreeBuildings, stateForConditionals = StateForConditionals(cityInfo.civInfo, cityInfo))) { + val freeBuildingName = cityInfo.civInfo.getEquivalentBuilding(unique.params[0]).name + if (cityInfo.matchesFilter(unique.params[1])) { + if (cityInfo.id !in freeBuildingsProvidedFromThisCity) + freeBuildingsProvidedFromThisCity[cityInfo.id] = hashSetOf() + freeBuildingsProvidedFromThisCity[cityInfo.id]!!.add(freeBuildingName) + if (!isBuilt(freeBuildingName)) + addBuilding(freeBuildingName) + } + } + } + /** * Purchase a construction for gold * called from NextTurnAutomation and the City UI @@ -480,26 +502,40 @@ class CityConstructions { * @param automatic Flag whether automation should try to choose what next to build (not coming from UI) * Note: settings.autoAssignCityProduction is handled later * @param stat Stat object of the stat with which was paid for the construction - * @return Success (false e.g. unit cannot be placed + * @param tile Supports [UniqueType.CreatesOneImprovement] the tile to place the improvement from that unique on. + * Ignored when the [constructionName] does not have that unique. If null and the building has the unique, a tile is chosen automatically. + * @return Success (false e.g. unit cannot be placed) */ fun purchaseConstruction( - constructionName: String, - queuePosition: Int, - automatic: Boolean, - stat: Stat = Stat.Gold + constructionName: String, + queuePosition: Int, + automatic: Boolean, + stat: Stat = Stat.Gold, + tile: TileInfo? = null ): Boolean { - if (!(getConstruction(constructionName) as INonPerpetualConstruction).postBuildEvent(this, stat)) + val construction = getConstruction(constructionName) as? INonPerpetualConstruction ?: return false + + // Support UniqueType.CreatesOneImprovement: it is active when getImprovementToCreate returns an improvement + val improvementToPlace = (construction as? Building)?.getImprovementToCreate(cityInfo.getRuleset()) + if (improvementToPlace != null) { + // If active without a predetermined tile to place the improvement on, automate a tile + val finalTile = tile + ?: Automation.getTileForConstructionImprovement(cityInfo, improvementToPlace) + ?: return false // This was never reached in testing + finalTile.markForCreatesOneImprovement(improvementToPlace.name) + // postBuildEvent does the rest by calling cityConstructions.applyCreateOneImprovement + } + + if (!construction.postBuildEvent(this, stat)) return false // nothing built - no pay if (!cityInfo.civInfo.gameInfo.gameParameters.godMode) { - val construction = getConstruction(constructionName) - if (construction is PerpetualConstruction) return false - val constructionCost = (construction as INonPerpetualConstruction).getStatBuyCost(cityInfo, stat) - if (constructionCost == null) return false // We should never end up here anyway, so things have already gone _way_ wrong + val constructionCost = construction.getStatBuyCost(cityInfo, stat) + ?: return false // We should never end up here anyway, so things have already gone _way_ wrong cityInfo.addStat(stat, -1 * constructionCost) val conditionalState = StateForConditionals(civInfo = cityInfo.civInfo, cityInfo = cityInfo) - + if (( cityInfo.civInfo.getMatchingUniques(UniqueType.BuyUnitsIncreasingCost, conditionalState) + cityInfo.civInfo.getMatchingUniques(UniqueType.BuyBuildingsIncreasingCost, conditionalState) @@ -522,22 +558,13 @@ class CityConstructions { return true } - - fun hasBuildableStatBuildings(stat: Stat): Boolean { - return getBasicStatBuildings(stat) - .map { cityInfo.civInfo.getEquivalentBuilding(it.name) } - .filter { it.isBuildable(this) || isBeingConstructedOrEnqueued(it.name) } - .any() - } fun addCheapestBuildableStatBuilding(stat: Stat): String? { val cheapestBuildableStatBuilding = getBasicStatBuildings(stat) .map { cityInfo.civInfo.getEquivalentBuilding(it.name) } .filter { it.isBuildable(this) || isBeingConstructedOrEnqueued(it.name) } .minByOrNull { it.cost }?.name - - if (cheapestBuildableStatBuilding == null) - return null + ?: return null constructionComplete(getConstruction(cheapestBuildableStatBuilding) as INonPerpetualConstruction) @@ -555,6 +582,13 @@ class CityConstructions { } ConstructionAutomation(this).chooseNextConstruction() + + /** Support for [UniqueType.CreatesOneImprovement] - if an Improvement-creating Building was auto-queued, auto-choose a tile: */ + val building = getCurrentConstruction() as? Building ?: return + val improvement = building.getImprovementToCreate(cityInfo.getRuleset()) ?: return + if (getTileForImprovement(improvement.name) != null) return + val newTile = Automation.getTileForConstructionImprovement(cityInfo, improvement) ?: return + newTile.markForCreatesOneImprovement(improvement.name) } fun addToQueue(constructionName: String) { @@ -578,13 +612,13 @@ class CityConstructions { /** If this was done automatically, we should automatically try to choose a new construction and treat it as such */ fun removeFromQueue(constructionQueueIndex: Int, automatic: Boolean) { val constructionName = constructionQueue.removeAt(constructionQueueIndex) + + // UniqueType.CreatesOneImprovement support val construction = getConstruction(constructionName) if (construction is Building) { - val improvement = construction.getImprovement(cityInfo.getRuleset()) + val improvement = construction.getImprovementToCreate(cityInfo.getRuleset()) if (improvement != null) { - val tileWithImprovement = cityInfo.getTiles().firstOrNull { it.improvementInProgress == improvement.name } - tileWithImprovement?.improvementInProgress = null - tileWithImprovement?.turnsToImprovement = 0 + getTileForImprovement(improvement.name)?.stopWorkingOnImprovement() } } @@ -604,10 +638,61 @@ class CityConstructions { raisePriority(constructionQueueIndex + 1) } - //endregion private fun MutableList.swap(idx1: Int, idx2: Int) { val tmp = this[idx1] this[idx1] = this[idx2] this[idx2] = tmp } + + /** Support for [UniqueType.CreatesOneImprovement]: + * + * If [building] is an improvement-creating one, find a marked tile matching the improvement to be created + * (skip if none found), then un-mark the tile and place the improvement unless [removeOnly] is set. + */ + fun applyCreateOneImprovement(building: Building, removeOnly: Boolean = false) { + val improvement = building.getImprovementToCreate(cityInfo.getRuleset()) + ?: return + val tileForImprovement = getTileForImprovement(improvement.name) ?: return + tileForImprovement.stopWorkingOnImprovement() // clears mark + if (removeOnly) return + /**todo unify with [UnitActions.getImprovementConstructionActions] and [MapUnit.workOnImprovement] - this won't allow e.g. a building to place a road */ + tileForImprovement.improvement = improvement.name + cityInfo.civInfo.lastSeenImprovement[tileForImprovement.position] = improvement.name + cityInfo.cityStats.update() + cityInfo.civInfo.updateDetailedCivResources() + // If bought the worldscreen will not have been marked to update, and the new improvement won't show until later... + if (UncivGame.isCurrentInitialized()) + UncivGame.Current.worldScreen.shouldUpdate = true + } + + /** Support for [UniqueType.CreatesOneImprovement]: + * + * To be called after circumstances forced clearing a marker from a tile (pillaging, nuking). + * Should remove one matching building from the queue without looking for a marked tile. + */ + fun removeCreateOneImprovementConstruction(improvement: String) { + val ruleset = cityInfo.getRuleset() + val indexToRemove = constructionQueue.withIndex().mapNotNull { + val construction = getConstruction(it.value) + val buildingImprovement = (construction as? Building)?.getImprovementToCreate(ruleset)?.name + it.index.takeIf { buildingImprovement == improvement } + }.firstOrNull() ?: return + + constructionQueue.removeAt(indexToRemove) + + if (constructionQueue.isEmpty()) { + constructionQueue.add("Nothing") + currentConstructionIsUserSet = false + } else currentConstructionIsUserSet = true + } + + /** Support for [UniqueType.CreatesOneImprovement]: + * + * Find the selected tile for a specific improvement being constructed via a building, if any. + */ + fun getTileForImprovement(improvementName: String) = cityInfo.getTiles() + .firstOrNull { + it.isMarkedForCreatesOneImprovement(improvementName) + } + //endregion } diff --git a/core/src/com/unciv/logic/city/CityExpansionManager.kt b/core/src/com/unciv/logic/city/CityExpansionManager.kt index 34b196b27d..bdf80a11b0 100644 --- a/core/src/com/unciv/logic/city/CityExpansionManager.kt +++ b/core/src/com/unciv/logic/city/CityExpansionManager.kt @@ -129,6 +129,8 @@ class CityExpansionManager { city.lockedTiles.remove(tileInfo.position) } + tileInfo.removeCreatesOneImprovementMarker() + tileInfo.setOwningCity(null) cityInfo.civInfo.updateDetailedCivResources() diff --git a/core/src/com/unciv/logic/city/CityInfo.kt b/core/src/com/unciv/logic/city/CityInfo.kt index 1f471f0e21..760fec3026 100644 --- a/core/src/com/unciv/logic/city/CityInfo.kt +++ b/core/src/com/unciv/logic/city/CityInfo.kt @@ -696,7 +696,9 @@ class CityInfo { // The relinquish ownership MUST come before removing the city, // because it updates the city stats which assumes there is a capital, so if you remove the capital it crashes - getTiles().forEach { expansion.relinquishOwnership(it) } + for (tile in getTiles()) { + expansion.relinquishOwnership(tile) + } civInfo.cities = civInfo.cities.toMutableList().apply { remove(this@CityInfo) } getCenterTile().improvement = "City ruins" diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index 4f082314ad..f0686d2b26 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -449,9 +449,11 @@ class MapUnit { fun isIdle(): Boolean { if (currentMovement == 0f) return false - if (getTile().improvementInProgress != null - && canBuildImprovement(getTile().getTileImprovementInProgress()!!)) - return false + val tile = getTile() + if (tile.improvementInProgress != null && + canBuildImprovement(tile.getTileImprovementInProgress()!!) && + !tile.isMarkedForCreatesOneImprovement() + ) return false return !(isFortified() || isExploring() || isSleeping() || isAutomated() || isMoving()) } @@ -625,6 +627,7 @@ class MapUnit { private fun workOnImprovement() { val tile = getTile() + if (tile.isMarkedForCreatesOneImprovement()) return tile.turnsToImprovement -= 1 if (tile.turnsToImprovement != 0) return diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index 250796b14c..42241b9ee1 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -120,6 +120,8 @@ open class TileInfo { return toReturn } + //region pure functions + fun containsGreatImprovement(): Boolean { return getTileImprovement()?.isGreatImprovement() == true } @@ -128,7 +130,6 @@ open class TileInfo { if (improvementInProgress == null) return false return ruleset.tileImprovements[improvementInProgress!!]!!.isGreatImprovement() } - //region pure functions /** Returns military, civilian and air units in tile */ fun getUnits() = sequence { @@ -592,7 +593,7 @@ open class TileInfo { } } - fun hasImprovementInProgress() = improvementInProgress != null + fun hasImprovementInProgress() = improvementInProgress != null && turnsToImprovement > 0 @delegate:Transient private val _isCoastalTile: Boolean by lazy { neighbors.any { it.baseTerrain == Constants.coast } } @@ -714,6 +715,7 @@ open class TileInfo { return true } + /** Get info on a selected tile, used on WorldScreen (right side above minimap), CityScreen or MapEditorViewTab. */ fun toMarkup(viewingCiv: CivilizationInfo?): ArrayList { val lineList = ArrayList() val isViewableToPlayer = viewingCiv == null || UncivGame.Current.viewEntireMapForDebug @@ -727,6 +729,7 @@ open class TileInfo { if (UncivGame.Current.viewEntireMapForDebug || city.civInfo == viewingCiv) lineList += city.cityConstructions.getProductionMarkup(ruleset) } + lineList += FormattedLine(baseTerrain, link="Terrain/$baseTerrain") for (terrainFeature in terrainFeatures) lineList += FormattedLine(terrainFeature, link="Terrain/$terrainFeature") @@ -753,11 +756,14 @@ open class TileInfo { val shownImprovement = getShownImprovement(viewingCiv) if (shownImprovement != null) lineList += FormattedLine(shownImprovement, link="Improvement/$shownImprovement") + if (improvementInProgress != null && isViewableToPlayer) { + // Negative turnsToImprovement is used for UniqueType.CreatesOneImprovement val line = "{$improvementInProgress}" + if (turnsToImprovement > 0) " - $turnsToImprovement${Fonts.turn}" else " ({Under construction})" lineList += FormattedLine(line, link="Improvement/$improvementInProgress") } + if (civilianUnit != null && isViewableToPlayer) lineList += FormattedLine(civilianUnit!!.name.tr() + " - " + civilianUnit!!.civInfo.civName.tr(), link="Unit/${civilianUnit!!.name}") @@ -767,6 +773,7 @@ open class TileInfo { " - " + militaryUnit!!.civInfo.civName.tr() lineList += FormattedLine(milUnitString, link="Unit/${militaryUnit!!.name}") } + val defenceBonus = getDefensiveBonus() if (defenceBonus != 0f) { var defencePercentString = (defenceBonus * 100).toInt().toString() + "%" @@ -815,9 +822,16 @@ open class TileInfo { fun getContinent() = continent - //endregion + /** Checks if this tile is marked as target tile for a building with a [UniqueType.CreatesOneImprovement] unique */ + fun isMarkedForCreatesOneImprovement() = + turnsToImprovement < 0 && improvementInProgress != null + /** Checks if this tile is marked as target tile for a building with a [UniqueType.CreatesOneImprovement] unique creating a specific [improvement] */ + fun isMarkedForCreatesOneImprovement(improvement: String) = + turnsToImprovement < 0 && improvementInProgress == improvement + //endregion //region state-changing functions + fun setTransients() { setTerrainTransients() setUnitTransients(true) @@ -914,11 +928,39 @@ open class TileInfo { else improvement.getTurnsToBuild(civInfo, unit) } + /** Clears [improvementInProgress] and [turnsToImprovement] */ fun stopWorkingOnImprovement() { improvementInProgress = null turnsToImprovement = 0 } + /** Sets tile improvement to pillaged (without prior checks for validity) + * and ensures that matching [UniqueType.CreatesOneImprovement] queued buildings are removed. */ + fun setPillaged() { + // http://well-of-souls.com/civ/civ5_improvements.html says that naval improvements are destroyed upon pillage + // and I can't find any other sources so I'll go with that + if (isLand) { + // Setting turnsToImprovement might interfere with UniqueType.CreatesOneImprovement + removeCreatesOneImprovementMarker() + improvementInProgress = improvement + turnsToImprovement = 2 + } + improvement = null + } + + /** Marks tile as target tile for a building with a [UniqueType.CreatesOneImprovement] unique */ + fun markForCreatesOneImprovement(improvement: String) { + improvementInProgress = improvement + turnsToImprovement = -1 + } + /** Un-Marks a tile as target tile for a building with a [UniqueType.CreatesOneImprovement] unique, + * and ensures that matching queued buildings are removed. */ + fun removeCreatesOneImprovementMarker() { + if (!isMarkedForCreatesOneImprovement()) return + owningCity?.cityConstructions?.removeCreateOneImprovementConstruction(improvementInProgress!!) + stopWorkingOnImprovement() + } + fun normalizeToRuleset(ruleset: Ruleset) { if (naturalWonder != null && !ruleset.terrains.containsKey(naturalWonder)) naturalWonder = null diff --git a/core/src/com/unciv/models/ruleset/Building.kt b/core/src/com/unciv/models/ruleset/Building.kt index 83c5e6455a..669397c9e8 100644 --- a/core/src/com/unciv/models/ruleset/Building.kt +++ b/core/src/com/unciv/models/ruleset/Building.kt @@ -683,15 +683,8 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction { cityConstructions.addBuilding(name) - val improvement = getImprovement(civInfo.gameInfo.ruleSet) - if (improvement != null) { - val tileWithImprovement = cityConstructions.cityInfo.getTiles().firstOrNull { it.improvementInProgress == improvement.name } - if (tileWithImprovement != null) { - tileWithImprovement.turnsToImprovement = 0 - tileWithImprovement.improvementInProgress = null - tileWithImprovement.improvement = improvement.name - } - } + /** Support for [UniqueType.CreatesOneImprovement] */ + cityConstructions.applyCreateOneImprovement(this) // "Provides a free [buildingName] [cityFilter]" cityConstructions.addFreeBuildings() @@ -741,10 +734,20 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction { return false } - fun getImprovement(ruleset: Ruleset): TileImprovement? { - val improvementUnique = getMatchingUniques("Creates a [] improvement on a specific tile") - .firstOrNull() ?: return null - return ruleset.tileImprovements[improvementUnique.params[0]] + private val _hasCreatesOneImprovementUnique by lazy { + hasUnique(UniqueType.CreatesOneImprovement) + } + fun hasCreateOneImprovementUnique() = _hasCreatesOneImprovementUnique + + private var _getImprovementToCreate: TileImprovement? = null + fun getImprovementToCreate(ruleset: Ruleset): TileImprovement? { + if (!hasCreateOneImprovementUnique()) return null + if (_getImprovementToCreate == null) { + val improvementUnique = getMatchingUniques(UniqueType.CreatesOneImprovement) + .firstOrNull() ?: return null + _getImprovementToCreate = ruleset.tileImprovements[improvementUnique.params[0]] + } + return _getImprovementToCreate } fun isSellable() = !isAnyWonder() && !hasUnique(UniqueType.Unsellable) diff --git a/core/src/com/unciv/models/ruleset/Ruleset.kt b/core/src/com/unciv/models/ruleset/Ruleset.kt index 75cc151be8..6680478bbb 100644 --- a/core/src/com/unciv/models/ruleset/Ruleset.kt +++ b/core/src/com/unciv/models/ruleset/Ruleset.kt @@ -657,9 +657,6 @@ class Ruleset { if (building.requiredBuildingInAllCities != null) lines.add("${building.name} contains 'requiredBuildingInAllCities' - please convert to a \"" + UniqueType.RequiresBuildingInAllCities.text.fillPlaceholders(building.requiredBuildingInAllCities!!)+"\" unique", RulesetErrorSeverity.Warning) - for (unique in building.getMatchingUniques("Creates a [] improvement on a specific tile")) - if (!tileImprovements.containsKey(unique.params[0])) - lines += "${building.name} creates a ${unique.params[0]} improvement which does not exist!" checkUniques(building, lines, rulesetSpecific, forOptionsPopup) } diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt index 4570bdbff5..98ae999fa8 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt @@ -364,10 +364,12 @@ enum class UniqueParameterType( } }, - /** [UniqueType.ConstructImprovementConsumingUnit] */ + /** For [UniqueType.ConstructImprovementConsumingUnit], [UniqueType.CreatesOneImprovement] */ ImprovementName("improvementName", "Trading Post", "The name of any improvement"){ - override fun getErrorSeverity(parameterText: String,ruleset: Ruleset): + override fun getErrorSeverity(parameterText: String, ruleset: Ruleset): UniqueType.UniqueComplianceErrorSeverity? { + if (parameterText == Constants.cancelImprovementOrder) + return UniqueType.UniqueComplianceErrorSeverity.RulesetInvariant if (ruleset.tileImprovements.containsKey(parameterText)) return null return UniqueType.UniqueComplianceErrorSeverity.RulesetSpecific } diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index 7b760ae02f..fe22b5a7d4 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -366,12 +366,12 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags: CanOnlyBeBuiltInCertainCities("Can only be built [cityFilter]", UniqueTarget.Building), @Deprecated("as of 3.19.16", ReplaceWith("Can only be built [in annexed cities]")) CanOnlyBeBuiltInAnnexedCities("Can only be built in annexed cities", UniqueTarget.Building), - + MustHaveOwnedWithinTiles("Must have an owned [tileFilter] within [amount] tiles", UniqueTarget.Building), @Deprecated("as of 3.19.7", ReplaceWith("[stats] ")) StatsWithResource("[stats] with [resource]", UniqueTarget.Building), - + // Todo nuclear weapon and spaceship enabling requires a rethink. // This doesn't actually directly affect anything, the "Only available " of the nuclear weapons does that. EnablesNuclearWeapons("Enables nuclear weapon", UniqueTarget.Building), @@ -390,10 +390,10 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags: NotDestroyedWhenCityCaptured("Never destroyed when the city is captured", UniqueTarget.Building), DoublesGoldFromCapturingCity("Doubles Gold given to enemy if city is captured", UniqueTarget.Building), - RemoveAnnexUnhappiness("Remove extra unhappiness from annexed cities", UniqueTarget.Building), ConnectTradeRoutes("Connects trade routes over water", UniqueTarget.Building), + CreatesOneImprovement("Creates a [improvementName] improvement on a specific tile", UniqueTarget.Building), //endregion @@ -592,7 +592,7 @@ 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), - + ResourceWeighting("Generated with weight [amount]", UniqueTarget.Resource, flags = UniqueFlag.setOfHiddenToUsers), MinorDepositWeighting("Minor deposits generated with weight [amount]", UniqueTarget.Resource, flags = UniqueFlag.setOfHiddenToUsers), LuxuryWeightingForCityStates("Generated near City States with weight [amount]", UniqueTarget.Resource, flags = UniqueFlag.setOfHiddenToUsers), @@ -614,7 +614,8 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags: DefensiveBonus("Gives a defensive bonus of [relativeAmount]%", UniqueTarget.Improvement), ImprovementMaintenance("Costs [amount] gold per turn when in your territory", UniqueTarget.Improvement), // Unused DamagesAdjacentEnemyUnits("Adjacent enemy units ending their turn take [amount] damage", UniqueTarget.Improvement), - + TakeOverTilesAroundWhenBuilt("Constructing it will take over the tiles around it and assign them to your closest city", UniqueTarget.Improvement), + GreatImprovement("Great Improvement", UniqueTarget.Improvement), IsAncientRuinsEquivalent("Provides a random bonus when entered", UniqueTarget.Improvement), diff --git a/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt b/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt index cc15832730..525e2f5d8e 100644 --- a/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt +++ b/core/src/com/unciv/ui/cityscreen/CityConstructionsTable.kt @@ -9,8 +9,10 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextButton import com.badlogic.gdx.utils.Align import com.unciv.Constants import com.unciv.logic.city.* +import com.unciv.logic.map.TileInfo import com.unciv.models.UncivSound import com.unciv.models.ruleset.Building +import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.stats.Stat import com.unciv.models.translations.tr @@ -37,7 +39,6 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { /* -1 = Nothing, >= 0 queue entry (0 = current construction) */ private var selectedQueueEntry = -1 // None private var preferredBuyStat = Stat.Gold // Used for keyboard buy - var improvementBuildingToConstruct: Building? = null private val upperTable = Table(BaseScreen.skin) private val showCityInfoTableButton = "Show stats drilldown".toTextButton() @@ -91,7 +92,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { /** Forces layout calculation and returns the lower Table's (available constructions) width * - or - the upper Table's width, whichever is greater (in case the former only contains "Loading...") */ - fun getLowerWidth() = max(lowerTable.packIfNeeded().width, getUpperWidth()) // + fun getLowerWidth() = max(lowerTable.packIfNeeded().width, getUpperWidth()) fun addActorsToStage() { cityScreen.stage.addActor(upperTable) @@ -105,7 +106,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { upperTable.pack() // This should work when set once only in addActorsToStage, but it doesn't (table invisible - why?) upperTable.setPosition(posFromEdge, stageHeight - posFromEdge, Align.topLeft) - + updateAvailableConstructions() lowerTableScrollCell.maxHeight(stageHeight - upperTable.height - 2 * posFromEdge) } @@ -181,7 +182,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { constructionButtonDTOList.add( ConstructionButtonDTO( - entry, + entry, buttonText, entry.getRejectionReasons(cityConstructions).getMostImportantRejectionReason() ) @@ -236,7 +237,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { maxButtonWidth = max(maxButtonWidth, constructionButton.packIfNeeded().width) } - availableConstructionsTable.apply { + availableConstructionsTable.apply { clear() defaults().left().bottom() addCategory("Units", units, maxButtonWidth) @@ -247,7 +248,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { pack() } - availableConstructionsScrollPane.apply { + availableConstructionsScrollPane.apply { setSize(maxButtonWidth, min(availableConstructionsTable.prefHeight, lowerTableScrollCell.maxHeight)) layout() scrollY = constructionsScrollY @@ -296,8 +297,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { table.touchable = Touchable.enabled table.onClick { - cityScreen.selectedConstruction = cityConstructions.getConstruction(constructionName) - cityScreen.selectedTile = null + cityScreen.selectConstruction(constructionName) selectedQueueEntry = constructionQueueIndex cityScreen.update() } @@ -326,7 +326,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { pickConstructionButton.background = ImageGetter.getBackground(Color.BLACK) pickConstructionButton.touchable = Touchable.enabled - if (!isSelectedQueueEntry() && cityScreen.selectedConstruction != null && cityScreen.selectedConstruction == construction) { + if (!isSelectedQueueEntry() && cityScreen.selectedConstruction == construction) { pickConstructionButton.background = ImageGetter.getBackground(Color.GREEN.darken(0.5f)) } @@ -336,7 +336,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { if (!cannotAddConstructionToQueue(construction, cityScreen.city, cityScreen.city.cityConstructions)) { val addToQueueButton = ImageGetter.getImage("OtherIcons/New").apply { color = Color.BLACK }.surroundWithCircle(40f) - addToQueueButton.onClick(getConstructionSound(construction)) { + addToQueueButton.onClick(UncivSound.Silent) { addConstructionToQueue(construction, cityScreen.city.cityConstructions) } pickConstructionButton.add(addToQueueButton) @@ -350,8 +350,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { .colspan(pickConstructionButton.columns).fillX().left().padTop(2f) } pickConstructionButton.onClick { - cityScreen.selectedConstruction = construction - cityScreen.selectedTile = null + cityScreen.selectConstruction(construction) selectedQueueEntry = -1 cityScreen.update() } @@ -381,7 +380,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { else { button.onClick { cityConstructions.removeFromQueue(selectedQueueEntry, false) - cityScreen.selectedConstruction = null + cityScreen.clearSelection() selectedQueueEntry = -1 cityScreen.update() } @@ -392,7 +391,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { || cannotAddConstructionToQueue(construction, city, cityConstructions)) { button.disable() } else { - button.onClick(getConstructionSound(construction)) { + button.onClick(UncivSound.Silent) { addConstructionToQueue(construction, cityConstructions) } } @@ -404,17 +403,21 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { private fun addConstructionToQueue(construction: IConstruction, cityConstructions: CityConstructions) { // Some evil person decided to double tap real fast - #4977 - if (cannotAddConstructionToQueue(construction, cityScreen.city, cityScreen.city.cityConstructions)) + if (cannotAddConstructionToQueue(construction, cityScreen.city, cityConstructions)) return - if (construction is Building && construction.uniqueObjects.any { it.placeholderText == "Creates a [] improvement on a specific tile" }) { - cityScreen.selectedTile - improvementBuildingToConstruct = construction + + // UniqueType.CreatesOneImprovement support - don't add yet, postpone until target tile for the improvement is selected + if (construction is Building && construction.hasCreateOneImprovementUnique()) { + cityScreen.startPickTileForCreatesOneImprovement(construction, Stat.Gold, false) return } + cityScreen.stopPickTileForCreatesOneImprovement() + + Sounds.play(getConstructionSound(construction)) cityConstructions.addToQueue(construction.name) if (!construction.shouldBeDisplayed(cityConstructions)) // For buildings - unlike units which can be queued multiple times - cityScreen.selectedConstruction = null + cityScreen.clearSelection() cityScreen.update() cityScreen.game.settings.addCompletedTutorialTask("Pick construction") } @@ -454,7 +457,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { button.onClick { button.disable() - askToBuyConstruction(construction, stat) + buyButtonOnClick(construction, stat) } button.isEnabled = isConstructionPurchaseAllowed(construction, stat, constructionBuyCost) button.addTooltip('B') // The key binding is done in CityScreen constructor @@ -465,13 +468,29 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { return button } - + + private fun buyButtonOnClick(construction: INonPerpetualConstruction, stat: Stat = preferredBuyStat) { + if (construction !is Building || !construction.hasCreateOneImprovementUnique()) + return askToBuyConstruction(construction, stat) + if (selectedQueueEntry < 0) + return cityScreen.startPickTileForCreatesOneImprovement(construction, stat, true) + // Buying a UniqueType.CreatesOneImprovement building from queue must pass down + // the already selected tile, otherwise a new one is chosen from Automation code. + val improvement = construction.getImprovementToCreate(cityScreen.city.getRuleset())!! + val tileForImprovement = cityScreen.city.cityConstructions.getTileForImprovement(improvement.name) + askToBuyConstruction(construction, stat, tileForImprovement) + } + /** Ask whether user wants to buy [construction] for [stat]. * * Used from onClick and keyboard dispatch, thus only minimal parameters are passed, * and it needs to do all checks and the sound as appropriate. */ - fun askToBuyConstruction(construction: INonPerpetualConstruction, stat: Stat = preferredBuyStat) { + fun askToBuyConstruction( + construction: INonPerpetualConstruction, + stat: Stat = preferredBuyStat, + tile: TileInfo? = null + ) { if (!isConstructionPurchaseShown(construction, stat)) return val city = cityScreen.city val constructionBuyCost = construction.getStatBuyCost(city, stat)!! @@ -483,7 +502,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { "Would you like to purchase [${construction.name}] for [$constructionBuyCost] [${stat.character}]?".tr() YesNoPopup( purchasePrompt, - action = { purchaseConstruction(construction, stat) }, + action = { purchaseConstruction(construction, stat, tile) }, screen = cityScreen, restoreDefault = { cityScreen.update() } ).open() @@ -510,11 +529,17 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { } } - // called only by askToBuyConstruction's Yes answer - private fun purchaseConstruction(construction: INonPerpetualConstruction, stat: Stat = Stat.Gold) { + /** Called only by askToBuyConstruction's Yes answer - not to be confused with [CityConstructions.purchaseConstruction] + * @param tile supports [UniqueType.CreatesOneImprovement] + */ + private fun purchaseConstruction( + construction: INonPerpetualConstruction, + stat: Stat = Stat.Gold, + tile: TileInfo? = null + ) { Sounds.play(stat.purchaseSound) val city = cityScreen.city - if (!city.cityConstructions.purchaseConstruction(construction.name, selectedQueueEntry, false, stat)) { + if (!city.cityConstructions.purchaseConstruction(construction.name, selectedQueueEntry, false, stat, tile)) { Popup(cityScreen).apply { add("No space available to place [${construction.name}] near [${city.name}]".tr()).row() addCloseButton() @@ -524,16 +549,19 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { } if (isSelectedQueueEntry() || cityScreen.selectedConstruction?.isBuildable(city.cityConstructions) != true) { selectedQueueEntry = -1 - cityScreen.selectedConstruction = null - + cityScreen.clearSelection() + // Allow buying next queued or auto-assigned construction right away city.cityConstructions.chooseNextConstruction() - if (city.cityConstructions.currentConstructionFromQueue.isNotEmpty()) - cityScreen.selectedConstruction = city.cityConstructions.getCurrentConstruction().takeIf { it is INonPerpetualConstruction } + if (city.cityConstructions.currentConstructionFromQueue.isNotEmpty()) { + val newConstruction = city.cityConstructions.getCurrentConstruction() + if (newConstruction is INonPerpetualConstruction) + cityScreen.selectConstruction(newConstruction) + } } cityScreen.update() } - + private fun getRaisePriorityButton(constructionQueueIndex: Int, name: String, city: CityInfo): Table { val tab = Table() tab.add(ImageGetter.getArrowImage(Align.top).apply { color = Color.BLACK }.surroundWithCircle(40f)) @@ -542,8 +570,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { tab.onClick { tab.touchable = Touchable.disabled city.cityConstructions.raisePriority(constructionQueueIndex) - cityScreen.selectedConstruction = cityScreen.city.cityConstructions.getConstruction(name) - cityScreen.selectedTile = null + cityScreen.selectConstruction(name) selectedQueueEntry = constructionQueueIndex - 1 cityScreen.update() } @@ -559,8 +586,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { tab.onClick { tab.touchable = Touchable.disabled city.cityConstructions.lowerPriority(constructionQueueIndex) - cityScreen.selectedConstruction = cityScreen.city.cityConstructions.getConstruction(name) - cityScreen.selectedTile = null + cityScreen.selectConstruction(name) selectedQueueEntry = constructionQueueIndex + 1 cityScreen.update() } @@ -576,7 +602,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) { tab.onClick { tab.touchable = Touchable.disabled city.cityConstructions.removeFromQueue(constructionQueueIndex, false) - cityScreen.selectedConstruction = null + cityScreen.clearSelection() cityScreen.update() } } diff --git a/core/src/com/unciv/ui/cityscreen/CityScreen.kt b/core/src/com/unciv/ui/cityscreen/CityScreen.kt index f4839c1e32..1ebf2592f3 100644 --- a/core/src/com/unciv/ui/cityscreen/CityScreen.kt +++ b/core/src/com/unciv/ui/cityscreen/CityScreen.kt @@ -6,20 +6,27 @@ import com.badlogic.gdx.scenes.scene2d.ui.Image import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.utils.Align import com.unciv.UncivGame +import com.unciv.logic.automation.Automation import com.unciv.logic.city.CityInfo import com.unciv.logic.city.IConstruction import com.unciv.logic.city.INonPerpetualConstruction import com.unciv.logic.map.TileInfo +import com.unciv.models.ruleset.Building +import com.unciv.models.ruleset.tile.TileImprovement +import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.models.stats.Stat import com.unciv.ui.images.ImageGetter import com.unciv.ui.map.TileGroupMap +import com.unciv.ui.popup.ToastPopup +import com.unciv.ui.tilegroups.TileGroup import com.unciv.ui.tilegroups.TileSetStrings import com.unciv.ui.utils.* import java.util.* class CityScreen( internal val city: CityInfo, - var selectedConstruction: IConstruction? = null, - var selectedTile: TileInfo? = null + initSelectedConstruction: IConstruction? = null, + initSelectedTile: TileInfo? = null ): BaseScreen() { companion object { /** Distance from stage edges to floating widgets */ @@ -73,6 +80,27 @@ class CityScreen( /** The ScrollPane for the background map view of the city surroundings */ private val mapScrollPane = ZoomableScrollPane() + /** Support for [UniqueType.CreatesOneImprovement] - need user to pick a tile */ + class PickTileForImprovementData ( + val building: Building, + val improvement: TileImprovement, + val isBuying: Boolean, + val buyStat: Stat + ) + + // The following fields control what the user selects + var selectedConstruction: IConstruction? = initSelectedConstruction + private set + var selectedTile: TileInfo? = initSelectedTile + private set + /** If set, we are waiting for the user to pick a tile for [UniqueType.CreatesOneImprovement] */ + var pickTileData: PickTileForImprovementData? = null + /** A [Building] with [UniqueType.CreatesOneImprovement] has been selected _in the queue_: show the tile it will place the improvement on */ + private var selectedQueueEntryTargetTile: TileInfo? = null + /** Cached city.expansion.chooseNewTileToOwn() */ + // val should be OK as buying tiles is what changes this, and that would re-create the whole CityScreen + private val nextTileToOwn = city.expansion.chooseNewTileToOwn() + init { onBackButtonClicked { game.setWorldScreen() } UncivGame.Current.settings.addCompletedTutorialTask("Enter city screen") @@ -160,20 +188,37 @@ class CityScreen( } private fun updateTileGroups() { - val nextTile = city.expansion.chooseNewTileToOwn() + fun isExistingImprovementValuable(tileInfo: TileInfo, improvementToPlace: TileImprovement): Boolean { + if (tileInfo.improvement == null) return false + val civInfo = city.civInfo + val existingStats = tileInfo.getImprovementStats(tileInfo.getTileImprovement()!!, civInfo, city) + val replacingStats = tileInfo.getImprovementStats(improvementToPlace, civInfo, city) + return Automation.rankStatsValue(existingStats, civInfo) > Automation.rankStatsValue(replacingStats, civInfo) + } + fun getPickImprovementColor(tileInfo: TileInfo): Pair { + val improvementToPlace = pickTileData!!.improvement + return when { + tileInfo.isMarkedForCreatesOneImprovement() -> Color.BROWN to 0.7f + !tileInfo.canBuildImprovement(improvementToPlace, city.civInfo) -> Color.RED to 0.4f + isExistingImprovementValuable(tileInfo, improvementToPlace) -> Color.ORANGE to 0.5f + tileInfo.improvement != null -> Color.YELLOW to 0.6f + tileInfo.turnsToImprovement > 0 -> Color.YELLOW to 0.6f + else -> Color.GREEN to 0.5f + } + } for (tileGroup in tileGroups) { tileGroup.update() tileGroup.hideHighlight() - if (city.tiles.contains(tileGroup.tileInfo.position) - && constructionsTable.improvementBuildingToConstruct != null) { - val improvement = constructionsTable.improvementBuildingToConstruct!!.getImprovement(city.getRuleset())!! - if (tileGroup.tileInfo.canBuildImprovement(improvement, city.civInfo)) - tileGroup.showHighlight(Color.GREEN) - else tileGroup.showHighlight(Color.RED) - } - if (tileGroup.tileInfo == nextTile) { - tileGroup.showHighlight(Color.PURPLE) - tileGroup.setColor(0f, 0f, 0f, 0.7f) + when { + tileGroup.tileInfo == nextTileToOwn -> { + tileGroup.showHighlight(Color.PURPLE) + tileGroup.setColor(0f, 0f, 0f, 0.7f) + } + /** Support for [UniqueType.CreatesOneImprovement] */ + tileGroup.tileInfo == selectedQueueEntryTargetTile -> + tileGroup.showHighlight(Color.BROWN, 0.7f) + pickTileData != null && city.tiles.contains(tileGroup.tileInfo.position) -> + getPickImprovementColor(tileGroup.tileInfo).run { tileGroup.showHighlight(first, second) } } } } @@ -235,50 +280,20 @@ class CityScreen( val tileSetStrings = TileSetStrings() val cityTileGroups = cityInfo.getCenterTile().getTilesInDistance(5) - .filter { city.civInfo.exploredTiles.contains(it.position) } + .filter { cityInfo.civInfo.exploredTiles.contains(it.position) } .map { CityTileGroup(cityInfo, it, tileSetStrings) } for (tileGroup in cityTileGroups) { - val tileInfo = tileGroup.tileInfo - tileGroup.onClick { - if (city.isPuppet) return@onClick - - if (constructionsTable.improvementBuildingToConstruct != null) { - val improvement = constructionsTable.improvementBuildingToConstruct!!.getImprovement(city.getRuleset())!! - if (tileInfo.canBuildImprovement(improvement, cityInfo.civInfo)) { - tileInfo.improvementInProgress = improvement.name - tileInfo.turnsToImprovement = -1 - constructionsTable.improvementBuildingToConstruct = null - cityInfo.cityConstructions.addToQueue(improvement.name) - update() - } else { - constructionsTable.improvementBuildingToConstruct = null - update() - } - return@onClick - } - - selectedTile = tileInfo - selectedConstruction = null - if (tileGroup.isWorkable && canChangeState) { - if (!tileInfo.providesYield() && city.population.getFreePopulation() > 0) { - city.workedTiles.add(tileInfo.position) - game.settings.addCompletedTutorialTask("Reassign worked tiles") - } else if (tileInfo.isWorked() && !tileInfo.isLocked()) - city.workedTiles.remove(tileInfo.position) - city.cityStats.update() - } - update() + tileGroupOnClick(tileGroup, cityInfo) } - tileGroups.add(tileGroup) } val tilesToUnwrap = ArrayList() for (tileGroup in tileGroups) { - val xDifference = city.getCenterTile().position.x - tileGroup.tileInfo.position.x - val yDifference = city.getCenterTile().position.y - tileGroup.tileInfo.position.y + val xDifference = cityInfo.getCenterTile().position.x - tileGroup.tileInfo.position.x + val yDifference = cityInfo.getCenterTile().position.y - tileGroup.tileInfo.position.y //if difference is bigger than 5 the tileGroup we are looking for is on the other side of the map if (xDifference > 5 || xDifference < -5 || yDifference > 5 || yDifference < -5) { //so we want to unwrap its position @@ -299,6 +314,78 @@ class CityScreen( mapScrollPane.updateVisualScroll() } + private fun tileGroupOnClick(tileGroup: CityTileGroup, cityInfo: CityInfo) { + if (cityInfo.isPuppet) return + val tileInfo = tileGroup.tileInfo + + /** [UniqueType.CreatesOneImprovement] support - select tile for improvement */ + if (pickTileData != null) { + val pickTileData = this.pickTileData!! + this.pickTileData = null + val improvement = pickTileData.improvement + if (tileInfo.canBuildImprovement(improvement, cityInfo.civInfo)) { + if (pickTileData.isBuying) { + constructionsTable.askToBuyConstruction(pickTileData.building, pickTileData.buyStat, tileInfo) + } else { + // This way to store where the improvement a CreatesOneImprovement Building will create goes + // might get a bit fragile if several buildings constructing the same improvement type + // were to be allowed in the queue - or a little nontransparent to the user why they + // won't reorder - maybe one day redesign to have the target tiles attached to queue entries. + tileInfo.markForCreatesOneImprovement(improvement.name) + cityInfo.cityConstructions.addToQueue(pickTileData.building.name) + } + } + update() + return + } + + selectTile(tileInfo) + if (tileGroup.isWorkable && canChangeState) { + if (!tileInfo.providesYield() && cityInfo.population.getFreePopulation() > 0) { + cityInfo.workedTiles.add(tileInfo.position) + game.settings.addCompletedTutorialTask("Reassign worked tiles") + } else if (tileInfo.isWorked() && !tileInfo.isLocked()) + cityInfo.workedTiles.remove(tileInfo.position) + cityInfo.cityStats.update() + } + update() + } + + fun selectConstruction(name: String) { + selectConstruction(city.cityConstructions.getConstruction(name)) + } + fun selectConstruction(newConstruction: IConstruction) { + selectedConstruction = newConstruction + if (newConstruction is Building && newConstruction.hasCreateOneImprovementUnique()) { + val improvement = newConstruction.getImprovementToCreate(city.getRuleset()) + selectedQueueEntryTargetTile = if (improvement == null) null + else city.cityConstructions.getTileForImprovement(improvement.name) + } else { + selectedQueueEntryTargetTile = null + pickTileData = null + } + selectedTile = null + } + private fun selectTile(newTile: TileInfo?) { + selectedConstruction = null + selectedQueueEntryTargetTile = null + pickTileData = null + selectedTile = newTile + } + fun clearSelection() = selectTile(null) + + fun startPickTileForCreatesOneImprovement(construction: Building, stat: Stat, isBuying: Boolean) { + val improvement = construction.getImprovementToCreate(city.getRuleset()) ?: return + pickTileData = PickTileForImprovementData(construction, improvement, isBuying, stat) + updateTileGroups() + ToastPopup("Please select a tile for this building's [${improvement.name}]", this) + } + fun stopPickTileForCreatesOneImprovement() { + if (pickTileData == null) return + pickTileData = null + updateTileGroups() + } + fun exit() { game.setWorldScreen() game.worldScreen.mapHolder.setCenterPosition(city.location) diff --git a/core/src/com/unciv/ui/cityscreen/CityScreenTileTable.kt b/core/src/com/unciv/ui/cityscreen/CityScreenTileTable.kt index 09ca18ef1e..afd686a526 100644 --- a/core/src/com/unciv/ui/cityscreen/CityScreenTileTable.kt +++ b/core/src/com/unciv/ui/cityscreen/CityScreenTileTable.kt @@ -111,7 +111,7 @@ class CityScreenTileTable(private val cityScreen: CityScreen): Table() { Sounds.play(UncivSound.Coin) city.expansion.buyTile(selectedTile) // preselect the next tile on city screen rebuild so bulk buying can go faster - UncivGame.Current.setScreen(CityScreen(city, selectedTile = city.expansion.chooseNewTileToOwn())) + UncivGame.Current.setScreen(CityScreen(city, initSelectedTile = city.expansion.chooseNewTileToOwn())) }, screen = cityScreen, restoreDefault = { cityScreen.update() } diff --git a/core/src/com/unciv/ui/pickerscreens/ImprovementPickerScreen.kt b/core/src/com/unciv/ui/pickerscreens/ImprovementPickerScreen.kt index 366037a47a..21cc4600f9 100644 --- a/core/src/com/unciv/ui/pickerscreens/ImprovementPickerScreen.kt +++ b/core/src/com/unciv/ui/pickerscreens/ImprovementPickerScreen.kt @@ -26,12 +26,14 @@ class ImprovementPickerScreen( private val gameInfo = tileInfo.tileMap.gameInfo private val ruleSet = gameInfo.ruleSet private val currentPlayerCiv = gameInfo.getCurrentPlayerCivilization() + // Support for UniqueType.CreatesOneImprovement + private val tileMarkedForCreatesOneImprovement = tileInfo.isMarkedForCreatesOneImprovement() private fun getRequiredTechColumn(improvement: TileImprovement) = ruleSet.technologies[improvement.techRequired]?.column?.columnNumber ?: -1 fun accept(improvement: TileImprovement?) { - if (improvement == null) return + if (improvement == null || tileMarkedForCreatesOneImprovement) return if (improvement.name == Constants.cancelImprovementOrder) { tileInfo.stopWorkingOnImprovement() // no onAccept() - Worker can stay selected @@ -108,8 +110,9 @@ class ImprovementPickerScreen( val pickNow = when { suggestRemoval -> "${Constants.remove}[${tileInfo.getLastTerrain().name}] first".toLabel() - tileInfo.improvementInProgress != improvement.name -> "Pick now!".toLabel().onClick { accept(improvement) } - else -> "Current construction".toLabel() + tileInfo.improvementInProgress == improvement.name -> "Current construction".toLabel() + tileMarkedForCreatesOneImprovement -> null + else -> "Pick now!".toLabel().onClick { accept(improvement) } } val statIcons = getStatIconsTable(provideResource, removeImprovement) @@ -126,7 +129,6 @@ class ImprovementPickerScreen( val statsTable = getStatsTable(stats) statIcons.add(statsTable).padLeft(13f) - regularImprovements.add(statIcons).align(Align.right) val improvementButton = getPickerOptionButton(image, labelText) @@ -137,16 +139,14 @@ class ImprovementPickerScreen( } if (improvement.name == tileInfo.improvementInProgress) improvementButton.color = Color.GREEN - if (suggestRemoval){ + if (suggestRemoval || tileMarkedForCreatesOneImprovement) { improvementButton.disable() - } - regularImprovements.add(improvementButton) - - if (shortcutKey != null) { + } else if (shortcutKey != null) { keyPressDispatcher[shortcutKey] = { accept(improvement) } improvementButton.addTooltip(shortcutKey) } + regularImprovements.add(improvementButton) regularImprovements.add(pickNow).padLeft(10f).fillY() regularImprovements.row() } diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt index a23d632169..0db6697a2b 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt @@ -280,13 +280,7 @@ object UnitActions { return UnitAction(UnitActionType.Pillage, action = { - // http://well-of-souls.com/civ/civ5_improvements.html says that naval improvements are destroyed upon pillage - // and I can't find any other sources so I'll go with that - if (tile.isLand) { - tile.improvementInProgress = tile.improvement - tile.turnsToImprovement = 2 - } - tile.improvement = null + tile.setPillaged() unit.civInfo.lastSeenImprovement.remove(tile.position) if (tile.resource != null) tile.getOwner()?.updateDetailedCivResources() // this might take away a resource @@ -407,7 +401,6 @@ object UnitActions { tile.canBuildImprovement(it, unit.civInfo) && unit.canBuildImprovement(it) } - actionList += UnitAction(UnitActionType.ConstructImprovement, isCurrentAction = unit.currentTile.hasImprovementInProgress(), @@ -429,7 +422,7 @@ object UnitActions { }.takeIf { unit.currentMovement > 0 } ) } - + fun getAddInCapitalAction(unit: MapUnit, tile: TileInfo): UnitAction { return UnitAction(UnitActionType.AddInCapital, title = "Add to [${unit.getMatchingUniques(UniqueType.AddInCapital).first().params[0]}]", @@ -652,11 +645,10 @@ object UnitActions { val improvement = tile.ruleset.tileImprovements[improvementName] ?: continue - var resourcesAvailable = true - if (improvement.uniqueObjects.any { - it.isOfType(UniqueType.ConsumesResources) && civResources[unique.params[1]] ?: 0 < unique.params[0].toInt() - }) - resourcesAvailable = false + val resourcesAvailable = improvement.uniqueObjects.none { + it.isOfType(UniqueType.ConsumesResources) && + civResources[unique.params[1]] ?: 0 < unique.params[0].toInt() + } finalActions += UnitAction(UnitActionType.Create, title = "Create [$improvementName]", @@ -669,11 +661,11 @@ object UnitActions { "Remove $it" !in unitTile.ruleset.tileImprovements || it in improvement.terrainsCanBeBuiltOn } - ) + ) + unitTile.removeCreatesOneImprovementMarker() unitTile.improvement = improvementName - unitTile.improvementInProgress = null - unitTile.turnsToImprovement = 0 - if (improvementName == Constants.citadel) + unitTile.stopWorkingOnImprovement() + if (improvement.hasUnique(UniqueType.TakeOverTilesAroundWhenBuilt)) takeOverTilesAround(unit) val city = unitTile.getCity() if (city != null) { @@ -687,6 +679,9 @@ object UnitActions { resourcesAvailable && unit.currentMovement > 0f && tile.canBuildImprovement(improvement, unit.civInfo) + // Next test is to prevent interfering with UniqueType.CreatesOneImprovement - + // not pretty, but users *can* remove the building from the city queue an thus clear this: + && !tile.isMarkedForCreatesOneImprovement() && !tile.isImpassible() // Not 100% sure that this check is necessary... }) }