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 <yairm210@hotmail.com>
This commit is contained in:
SomeTroglodyte
2022-05-08 20:22:01 +02:00
committed by GitHub
parent 7c24b0b6af
commit 4b7edca7a8
21 changed files with 535 additions and 252 deletions

View File

@ -198,8 +198,12 @@
}, },
{ {
"name": "Citadel", "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"], "uniques": [
"civilopediaText": [{"text":"Constructing it will take over the tiles around it and assign them to your closest city"}] "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 //Civilization unique improvements

View File

@ -198,8 +198,12 @@
}, },
{ {
"name": "Citadel", "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"], "uniques": [
"civilopediaText": [{"text":"Constructing it will take over the tiles around it and assign them to your closest city"}] "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 //Civilization unique improvements

View File

@ -953,6 +953,7 @@ Lock =
Unlock = Unlock =
Move to city = Move to city =
Please enter a new name for your 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 # Ask for text or numbers popup UI

View File

@ -44,7 +44,6 @@ object Constants {
const val unknownNationName = "???" const val unknownNationName = "???"
const val fort = "Fort" const val fort = "Fort"
const val citadel = "Citadel"
const val futureTech = "Future Tech" const val futureTech = "Future Tech"
// Easter egg name. Is to avoid conflicts when players name their own religions. // Easter egg name. Is to avoid conflicts when players name their own religions.

View File

@ -12,6 +12,7 @@ import com.unciv.models.ruleset.MilestoneType
import com.unciv.models.ruleset.Victory import com.unciv.models.ruleset.Victory
import com.unciv.models.ruleset.Victory.Focus import com.unciv.models.ruleset.Victory.Focus
import com.unciv.models.ruleset.tile.ResourceType 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.LocalUniqueCache
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.BaseUnit
@ -169,9 +170,35 @@ object Automation {
return true 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 /** 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 { fun allowSpendingResource(civInfo: CivilizationInfo, construction: INonPerpetualConstruction): Boolean {
// City states do whatever they want // City states do whatever they want
if (civInfo.isCityState()) 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 // Ranks a tile for any purpose except the expansion algorithm of cities
internal fun rankTile(tile: TileInfo?, civInfo: CivilizationInfo): Float { internal fun rankTile(tile: TileInfo?, civInfo: CivilizationInfo): Float {
if (tile == null) return 0f if (tile == null) return 0f

View File

@ -55,17 +55,15 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
data class ConstructionChoice(val choice: String, var choiceModifier: Float, val remainingWork: Int) data class ConstructionChoice(val choice: String, var choiceModifier: Float, val remainingWork: Int)
private fun addChoice(choices: ArrayList<ConstructionChoice>, choice: String, choiceModifier: Float){ private fun addChoice(choices: ArrayList<ConstructionChoice>, choice: String, choiceModifier: Float) {
choices.add(ConstructionChoice(choice,choiceModifier,cityConstructions.getRemainingWork(choice))) choices.add(ConstructionChoice(choice, choiceModifier, cityConstructions.getRemainingWork(choice)))
} }
fun chooseNextConstruction() { fun chooseNextConstruction() {
if (!UncivGame.Current.settings.autoAssignCityProduction if (!UncivGame.Current.settings.autoAssignCityProduction
&& civInfo.playerType == PlayerType.Human && !cityInfo.isPuppet && civInfo.playerType == PlayerType.Human && !cityInfo.isPuppet)
) {
return return
}
if (cityConstructions.getCurrentConstruction() !is PerpetualConstruction) return // don't want to be stuck on these forever if (cityConstructions.getCurrentConstruction() !is PerpetualConstruction) return // don't want to be stuck on these forever
addFoodBuildingChoice() addFoodBuildingChoice()
@ -119,9 +117,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
.filter { it.getStatBuyCost(cityInfo, stat = Stat.Faith)!! <= civInfo.religionManager.storedFaith } .filter { it.getStatBuyCost(cityInfo, stat = Stat.Faith)!! <= civInfo.religionManager.storedFaith }
.firstOrNull() ?: return .firstOrNull() ?: return
cityConstructions.purchaseConstruction(chosenItem.name, -1, false, stat=Stat.Faith) cityConstructions.purchaseConstruction(chosenItem.name, -1, false, stat=Stat.Faith)
} }
private fun addMilitaryUnitChoice() { private fun addMilitaryUnitChoice() {
@ -152,7 +148,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
val buildableWorkboatUnits = buildableUnits val buildableWorkboatUnits = buildableUnits
.filter { .filter {
it.hasUnique(UniqueType.CreateWaterImprovements) it.hasUnique(UniqueType.CreateWaterImprovements)
&& Automation.allowSpendingResource(civInfo, it) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it)
} }
val alreadyHasWorkBoat = buildableWorkboatUnits.any() val alreadyHasWorkBoat = buildableWorkboatUnits.any()
&& !cityInfo.getTiles().any { && !cityInfo.getTiles().any {
@ -182,7 +178,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
val workerEquivalents = buildableUnits val workerEquivalents = buildableUnits
.filter { .filter {
it.hasUnique(UniqueType.BuildImprovements) it.hasUnique(UniqueType.BuildImprovements)
&& Automation.allowSpendingResource(civInfo, it) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it)
} }
if (workerEquivalents.none()) return // for mods with no worker units if (workerEquivalents.none()) return // for mods with no worker units
@ -196,7 +192,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private fun addCultureBuildingChoice() { private fun addCultureBuildingChoice() {
val cultureBuilding = buildableNotWonders val cultureBuilding = buildableNotWonders
.filter { it.isStatRelated(Stat.Culture) .filter { it.isStatRelated(Stat.Culture)
&& Automation.allowSpendingResource(civInfo, it) }.minByOrNull { it.cost } && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }.minByOrNull { it.cost }
if (cultureBuilding != null) { if (cultureBuilding != null) {
var modifier = 0.5f var modifier = 0.5f
if (cityInfo.cityStats.currentCityStats.culture == 0f) // It won't grow if we don't help it 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() { private fun addOtherBuildingChoice() {
val otherBuilding = buildableNotWonders val otherBuilding = buildableNotWonders
.filter { Automation.allowSpendingResource(civInfo, it) }.minByOrNull { it.cost } .filter { Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }.minByOrNull { it.cost }
if (otherBuilding != null) { if (otherBuilding != null) {
val modifier = 0.6f val modifier = 0.6f
addChoice(relativeCostEffectiveness, otherBuilding.name, modifier) addChoice(relativeCostEffectiveness, otherBuilding.name, modifier)
@ -256,7 +252,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
if (!buildableWonders.any()) return if (!buildableWonders.any()) return
val highestPriorityWonder = buildableWonders val highestPriorityWonder = buildableWonders
.filter { Automation.allowSpendingResource(civInfo, it) } .filter { Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.maxByOrNull { getWonderPriority(it) }!! .maxByOrNull { getWonderPriority(it) }!!
val citiesBuildingWonders = civInfo.cities val citiesBuildingWonders = civInfo.cities
.count { it.cityConstructions.isBuildingWonder() } .count { it.cityConstructions.isBuildingWonder() }
@ -269,7 +265,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private fun addUnitTrainingBuildingChoice() { private fun addUnitTrainingBuildingChoice() {
val unitTrainingBuilding = buildableNotWonders.asSequence() val unitTrainingBuilding = buildableNotWonders.asSequence()
.filter { it.hasUnique(UniqueType.UnitStartingExperience) .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)) { 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 var modifier = if (cityIsOverAverageProduction) 0.5f else 0.1f // You shouldn't be cranking out units anytime soon
if (isAtWar) modifier *= 2 if (isAtWar) modifier *= 2
@ -282,7 +278,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private fun addDefenceBuildingChoice() { private fun addDefenceBuildingChoice() {
val defensiveBuilding = buildableNotWonders.asSequence() val defensiveBuilding = buildableNotWonders.asSequence()
.filter { it.cityStrength > 0 .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))) { if (defensiveBuilding != null && (isAtWar || !civInfo.wantsToFocusOn(Victory.Focus.Culture))) {
var modifier = 0.2f var modifier = 0.2f
if (isAtWar) modifier = 0.5f if (isAtWar) modifier = 0.5f
@ -300,7 +296,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
val happinessBuilding = buildableNotWonders.asSequence() val happinessBuilding = buildableNotWonders.asSequence()
.filter { (it.isStatRelated(Stat.Happiness) .filter { (it.isStatRelated(Stat.Happiness)
|| it.uniques.contains("Remove extra unhappiness from annexed cities")) || it.uniques.contains("Remove extra unhappiness from annexed cities"))
&& Automation.allowSpendingResource(civInfo, it)} && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.minByOrNull { it.cost } .minByOrNull { it.cost }
if (happinessBuilding != null) { if (happinessBuilding != null) {
var modifier = 1f var modifier = 1f
@ -315,7 +311,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
if (allTechsAreResearched) return if (allTechsAreResearched) return
val scienceBuilding = buildableNotWonders.asSequence() val scienceBuilding = buildableNotWonders.asSequence()
.filter { it.isStatRelated(Stat.Science) .filter { it.isStatRelated(Stat.Science)
&& Automation.allowSpendingResource(civInfo, it)} && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.minByOrNull { it.cost } .minByOrNull { it.cost }
if (scienceBuilding != null) { if (scienceBuilding != null) {
var modifier = 1.1f var modifier = 1.1f
@ -327,7 +323,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private fun addGoldBuildingChoice() { private fun addGoldBuildingChoice() {
val goldBuilding = buildableNotWonders.asSequence().filter { it.isStatRelated(Stat.Gold) val goldBuilding = buildableNotWonders.asSequence().filter { it.isStatRelated(Stat.Gold)
&& Automation.allowSpendingResource(civInfo, it)} && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.minByOrNull { it.cost } .minByOrNull { it.cost }
if (goldBuilding != null) { if (goldBuilding != null) {
val modifier = if (civInfo.statsForNextTurn.gold < 0) 3f else 1.2f val modifier = if (civInfo.statsForNextTurn.gold < 0) 3f else 1.2f
@ -337,7 +333,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private fun addProductionBuildingChoice() { private fun addProductionBuildingChoice() {
val productionBuilding = buildableNotWonders.asSequence() 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 } .minByOrNull { it.cost }
if (productionBuilding != null) { if (productionBuilding != null) {
addChoice(relativeCostEffectiveness, productionBuilding.name, 1.5f) addChoice(relativeCostEffectiveness, productionBuilding.name, 1.5f)
@ -350,7 +346,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
.filter { .filter {
(it.isStatRelated(Stat.Food) (it.isStatRelated(Stat.Food)
|| it.hasUnique(UniqueType.CarryOverFood, conditionalState) || it.hasUnique(UniqueType.CarryOverFood, conditionalState)
) && Automation.allowSpendingResource(civInfo, it) ) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it)
}.minByOrNull { it.cost } }.minByOrNull { it.cost }
if (foodBuilding != null) { if (foodBuilding != null) {
var modifier = 1f var modifier = 1f

View File

@ -736,9 +736,7 @@ object Battle {
if (tile.getTileImprovement()!!.hasUnique(UniqueType.Unpillagable)) { if (tile.getTileImprovement()!!.hasUnique(UniqueType.Unpillagable)) {
tile.improvement = null tile.improvement = null
} else { } else {
tile.turnsToImprovement = 2 tile.setPillaged()
tile.improvementInProgress = tile.improvement
tile.improvement = null
} }
} }
tile.roadStatus = RoadStatus.None tile.roadStatus = RoadStatus.None

View File

@ -1,9 +1,13 @@
package com.unciv.logic.city 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.automation.ConstructionAutomation
import com.unciv.logic.civilization.AlertType import com.unciv.logic.civilization.AlertType
import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PopupAlert 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.Building
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.unique.LocalUniqueCache 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.Fonts
import com.unciv.ui.utils.withItem import com.unciv.ui.utils.withItem
import com.unciv.ui.utils.withoutItem import com.unciv.ui.utils.withoutItem
import com.unciv.ui.worldscreen.unit.UnitActions // for Kdoc only
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.roundToInt import kotlin.math.roundToInt
/** /**
* City constructions manager. * City constructions manager.
* *
@ -33,6 +39,7 @@ import kotlin.math.roundToInt
* @property constructionQueue a list of constructions names enqueued * @property constructionQueue a list of constructions names enqueued
*/ */
class CityConstructions { class CityConstructions {
//region Non-Serialized Properties
@Transient @Transient
lateinit var cityInfo: CityInfo lateinit var cityInfo: CityInfo
@ -42,8 +49,7 @@ class CityConstructions {
@Transient @Transient
val builtBuildingUniqueMap = UniqueMap() val builtBuildingUniqueMap = UniqueMap()
var builtBuildings = HashSet<String>() // No backing field, not serialized
val inProgressConstructions = HashMap<String, Int>()
var currentConstructionFromQueue: String var currentConstructionFromQueue: String
get() { get() {
return if (constructionQueue.isEmpty()) "" return if (constructionQueue.isEmpty()) ""
@ -52,15 +58,23 @@ class CityConstructions {
set(value) { set(value) {
if (constructionQueue.isEmpty()) constructionQueue.add(value) else constructionQueue[0] = value if (constructionQueue.isEmpty()) constructionQueue.add(value) else constructionQueue[0] = value
} }
//endregion
//region Serialized Fields
var builtBuildings = HashSet<String>()
val inProgressConstructions = HashMap<String, Int>()
var currentConstructionIsUserSet = false var currentConstructionIsUserSet = false
var constructionQueue = mutableListOf<String>() var constructionQueue = mutableListOf<String>()
var productionOverflow = 0 var productionOverflow = 0
val queueMaxSize = 10 private val queueMaxSize = 10
// Maps cities to the buildings they received // Maps cities to the buildings they received
val freeBuildingsProvidedFromThisCity: HashMap<String, HashSet<String>> = hashMapOf() val freeBuildingsProvidedFromThisCity: HashMap<String, HashSet<String>> = hashMapOf()
//endregion
//region pure functions //region pure functions
fun clone(): CityConstructions { fun clone(): CityConstructions {
val toReturn = CityConstructions() val toReturn = CityConstructions()
toReturn.builtBuildings.addAll(builtBuildings) toReturn.builtBuildings.addAll(builtBuildings)
@ -111,49 +125,14 @@ class CityConstructions {
fun getCityProductionTextForCityButton(): String { fun getCityProductionTextForCityButton(): String {
val currentConstructionSnapshot = currentConstructionFromQueue // See below val currentConstructionSnapshot = currentConstructionFromQueue // See below
var result = currentConstructionSnapshot.tr() var result = currentConstructionSnapshot.tr()
if (currentConstructionSnapshot != "") { if (currentConstructionSnapshot.isNotEmpty()) {
val construction = PerpetualConstruction.perpetualConstructionsMap[currentConstructionSnapshot] val construction = PerpetualConstruction.perpetualConstructionsMap[currentConstructionSnapshot]
if (construction == null) result += getTurnsToConstructionString(currentConstructionSnapshot) result += construction?.getProductionTooltip(cityInfo)
else result += construction.getProductionTooltip(cityInfo) ?: getTurnsToConstructionString(currentConstructionSnapshot)
} }
return result 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 */ /** @constructionName needs to be a non-perpetual construction, else an empty string is returned */
internal fun getTurnsToConstructionString(constructionName: String, useStoredProduction:Boolean = true): String { internal fun getTurnsToConstructionString(constructionName: String, useStoredProduction:Boolean = true): String {
val construction = getConstruction(constructionName) val construction = getConstruction(constructionName)
@ -284,9 +263,17 @@ class CityConstructions {
return ceil((workLeft-productionOverflow) / production.toDouble()).toInt() 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 //region state changing functions
fun setTransients() { fun setTransients() {
builtBuildingObjects = ArrayList(builtBuildings.map { builtBuildingObjects = ArrayList(builtBuildings.map {
cityInfo.getRuleset().buildings[it] cityInfo.getRuleset().buildings[it]
@ -469,6 +456,41 @@ class CityConstructions {
builtBuildingUniqueMap.addUnique(unique) 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 * Purchase a construction for gold
* called from NextTurnAutomation and the City UI * called from NextTurnAutomation and the City UI
@ -480,22 +502,36 @@ class CityConstructions {
* @param automatic Flag whether automation should try to choose what next to build (not coming from UI) * @param automatic Flag whether automation should try to choose what next to build (not coming from UI)
* Note: settings.autoAssignCityProduction is handled later * Note: settings.autoAssignCityProduction is handled later
* @param stat Stat object of the stat with which was paid for the construction * @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( fun purchaseConstruction(
constructionName: String, constructionName: String,
queuePosition: Int, queuePosition: Int,
automatic: Boolean, automatic: Boolean,
stat: Stat = Stat.Gold stat: Stat = Stat.Gold,
tile: TileInfo? = null
): Boolean { ): 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 return false // nothing built - no pay
if (!cityInfo.civInfo.gameInfo.gameParameters.godMode) { if (!cityInfo.civInfo.gameInfo.gameParameters.godMode) {
val construction = getConstruction(constructionName) val constructionCost = construction.getStatBuyCost(cityInfo, stat)
if (construction is PerpetualConstruction) return false ?: return false // We should never end up here anyway, so things have already gone _way_ wrong
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
cityInfo.addStat(stat, -1 * constructionCost) cityInfo.addStat(stat, -1 * constructionCost)
val conditionalState = StateForConditionals(civInfo = cityInfo.civInfo, cityInfo = cityInfo) val conditionalState = StateForConditionals(civInfo = cityInfo.civInfo, cityInfo = cityInfo)
@ -523,21 +559,12 @@ class CityConstructions {
return true 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? { fun addCheapestBuildableStatBuilding(stat: Stat): String? {
val cheapestBuildableStatBuilding = getBasicStatBuildings(stat) val cheapestBuildableStatBuilding = getBasicStatBuildings(stat)
.map { cityInfo.civInfo.getEquivalentBuilding(it.name) } .map { cityInfo.civInfo.getEquivalentBuilding(it.name) }
.filter { it.isBuildable(this) || isBeingConstructedOrEnqueued(it.name) } .filter { it.isBuildable(this) || isBeingConstructedOrEnqueued(it.name) }
.minByOrNull { it.cost }?.name .minByOrNull { it.cost }?.name
?: return null
if (cheapestBuildableStatBuilding == null)
return null
constructionComplete(getConstruction(cheapestBuildableStatBuilding) as INonPerpetualConstruction) constructionComplete(getConstruction(cheapestBuildableStatBuilding) as INonPerpetualConstruction)
@ -555,6 +582,13 @@ class CityConstructions {
} }
ConstructionAutomation(this).chooseNextConstruction() 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) { 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 */ /** 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) { fun removeFromQueue(constructionQueueIndex: Int, automatic: Boolean) {
val constructionName = constructionQueue.removeAt(constructionQueueIndex) val constructionName = constructionQueue.removeAt(constructionQueueIndex)
// UniqueType.CreatesOneImprovement support
val construction = getConstruction(constructionName) val construction = getConstruction(constructionName)
if (construction is Building) { if (construction is Building) {
val improvement = construction.getImprovement(cityInfo.getRuleset()) val improvement = construction.getImprovementToCreate(cityInfo.getRuleset())
if (improvement != null) { if (improvement != null) {
val tileWithImprovement = cityInfo.getTiles().firstOrNull { it.improvementInProgress == improvement.name } getTileForImprovement(improvement.name)?.stopWorkingOnImprovement()
tileWithImprovement?.improvementInProgress = null
tileWithImprovement?.turnsToImprovement = 0
} }
} }
@ -604,10 +638,61 @@ class CityConstructions {
raisePriority(constructionQueueIndex + 1) raisePriority(constructionQueueIndex + 1)
} }
//endregion
private fun MutableList<String>.swap(idx1: Int, idx2: Int) { private fun MutableList<String>.swap(idx1: Int, idx2: Int) {
val tmp = this[idx1] val tmp = this[idx1]
this[idx1] = this[idx2] this[idx1] = this[idx2]
this[idx2] = tmp 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
} }

View File

@ -129,6 +129,8 @@ class CityExpansionManager {
city.lockedTiles.remove(tileInfo.position) city.lockedTiles.remove(tileInfo.position)
} }
tileInfo.removeCreatesOneImprovementMarker()
tileInfo.setOwningCity(null) tileInfo.setOwningCity(null)
cityInfo.civInfo.updateDetailedCivResources() cityInfo.civInfo.updateDetailedCivResources()

View File

@ -696,7 +696,9 @@ class CityInfo {
// The relinquish ownership MUST come before removing the city, // 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 // 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) } civInfo.cities = civInfo.cities.toMutableList().apply { remove(this@CityInfo) }
getCenterTile().improvement = "City ruins" getCenterTile().improvement = "City ruins"

View File

@ -449,9 +449,11 @@ class MapUnit {
fun isIdle(): Boolean { fun isIdle(): Boolean {
if (currentMovement == 0f) return false if (currentMovement == 0f) return false
if (getTile().improvementInProgress != null val tile = getTile()
&& canBuildImprovement(getTile().getTileImprovementInProgress()!!)) if (tile.improvementInProgress != null &&
return false canBuildImprovement(tile.getTileImprovementInProgress()!!) &&
!tile.isMarkedForCreatesOneImprovement()
) return false
return !(isFortified() || isExploring() || isSleeping() || isAutomated() || isMoving()) return !(isFortified() || isExploring() || isSleeping() || isAutomated() || isMoving())
} }
@ -625,6 +627,7 @@ class MapUnit {
private fun workOnImprovement() { private fun workOnImprovement() {
val tile = getTile() val tile = getTile()
if (tile.isMarkedForCreatesOneImprovement()) return
tile.turnsToImprovement -= 1 tile.turnsToImprovement -= 1
if (tile.turnsToImprovement != 0) return if (tile.turnsToImprovement != 0) return

View File

@ -120,6 +120,8 @@ open class TileInfo {
return toReturn return toReturn
} }
//region pure functions
fun containsGreatImprovement(): Boolean { fun containsGreatImprovement(): Boolean {
return getTileImprovement()?.isGreatImprovement() == true return getTileImprovement()?.isGreatImprovement() == true
} }
@ -128,7 +130,6 @@ open class TileInfo {
if (improvementInProgress == null) return false if (improvementInProgress == null) return false
return ruleset.tileImprovements[improvementInProgress!!]!!.isGreatImprovement() return ruleset.tileImprovements[improvementInProgress!!]!!.isGreatImprovement()
} }
//region pure functions
/** Returns military, civilian and air units in tile */ /** Returns military, civilian and air units in tile */
fun getUnits() = sequence { fun getUnits() = sequence {
@ -592,7 +593,7 @@ open class TileInfo {
} }
} }
fun hasImprovementInProgress() = improvementInProgress != null fun hasImprovementInProgress() = improvementInProgress != null && turnsToImprovement > 0
@delegate:Transient @delegate:Transient
private val _isCoastalTile: Boolean by lazy { neighbors.any { it.baseTerrain == Constants.coast } } private val _isCoastalTile: Boolean by lazy { neighbors.any { it.baseTerrain == Constants.coast } }
@ -714,6 +715,7 @@ open class TileInfo {
return true return true
} }
/** Get info on a selected tile, used on WorldScreen (right side above minimap), CityScreen or MapEditorViewTab. */
fun toMarkup(viewingCiv: CivilizationInfo?): ArrayList<FormattedLine> { fun toMarkup(viewingCiv: CivilizationInfo?): ArrayList<FormattedLine> {
val lineList = ArrayList<FormattedLine>() val lineList = ArrayList<FormattedLine>()
val isViewableToPlayer = viewingCiv == null || UncivGame.Current.viewEntireMapForDebug val isViewableToPlayer = viewingCiv == null || UncivGame.Current.viewEntireMapForDebug
@ -727,6 +729,7 @@ open class TileInfo {
if (UncivGame.Current.viewEntireMapForDebug || city.civInfo == viewingCiv) if (UncivGame.Current.viewEntireMapForDebug || city.civInfo == viewingCiv)
lineList += city.cityConstructions.getProductionMarkup(ruleset) lineList += city.cityConstructions.getProductionMarkup(ruleset)
} }
lineList += FormattedLine(baseTerrain, link="Terrain/$baseTerrain") lineList += FormattedLine(baseTerrain, link="Terrain/$baseTerrain")
for (terrainFeature in terrainFeatures) for (terrainFeature in terrainFeatures)
lineList += FormattedLine(terrainFeature, link="Terrain/$terrainFeature") lineList += FormattedLine(terrainFeature, link="Terrain/$terrainFeature")
@ -753,11 +756,14 @@ open class TileInfo {
val shownImprovement = getShownImprovement(viewingCiv) val shownImprovement = getShownImprovement(viewingCiv)
if (shownImprovement != null) if (shownImprovement != null)
lineList += FormattedLine(shownImprovement, link="Improvement/$shownImprovement") lineList += FormattedLine(shownImprovement, link="Improvement/$shownImprovement")
if (improvementInProgress != null && isViewableToPlayer) { if (improvementInProgress != null && isViewableToPlayer) {
// Negative turnsToImprovement is used for UniqueType.CreatesOneImprovement
val line = "{$improvementInProgress}" + val line = "{$improvementInProgress}" +
if (turnsToImprovement > 0) " - $turnsToImprovement${Fonts.turn}" else " ({Under construction})" if (turnsToImprovement > 0) " - $turnsToImprovement${Fonts.turn}" else " ({Under construction})"
lineList += FormattedLine(line, link="Improvement/$improvementInProgress") lineList += FormattedLine(line, link="Improvement/$improvementInProgress")
} }
if (civilianUnit != null && isViewableToPlayer) if (civilianUnit != null && isViewableToPlayer)
lineList += FormattedLine(civilianUnit!!.name.tr() + " - " + civilianUnit!!.civInfo.civName.tr(), lineList += FormattedLine(civilianUnit!!.name.tr() + " - " + civilianUnit!!.civInfo.civName.tr(),
link="Unit/${civilianUnit!!.name}") link="Unit/${civilianUnit!!.name}")
@ -767,6 +773,7 @@ open class TileInfo {
" - " + militaryUnit!!.civInfo.civName.tr() " - " + militaryUnit!!.civInfo.civName.tr()
lineList += FormattedLine(milUnitString, link="Unit/${militaryUnit!!.name}") lineList += FormattedLine(milUnitString, link="Unit/${militaryUnit!!.name}")
} }
val defenceBonus = getDefensiveBonus() val defenceBonus = getDefensiveBonus()
if (defenceBonus != 0f) { if (defenceBonus != 0f) {
var defencePercentString = (defenceBonus * 100).toInt().toString() + "%" var defencePercentString = (defenceBonus * 100).toInt().toString() + "%"
@ -815,9 +822,16 @@ open class TileInfo {
fun getContinent() = continent 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 //region state-changing functions
fun setTransients() { fun setTransients() {
setTerrainTransients() setTerrainTransients()
setUnitTransients(true) setUnitTransients(true)
@ -914,11 +928,39 @@ open class TileInfo {
else improvement.getTurnsToBuild(civInfo, unit) else improvement.getTurnsToBuild(civInfo, unit)
} }
/** Clears [improvementInProgress] and [turnsToImprovement] */
fun stopWorkingOnImprovement() { fun stopWorkingOnImprovement() {
improvementInProgress = null improvementInProgress = null
turnsToImprovement = 0 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) { fun normalizeToRuleset(ruleset: Ruleset) {
if (naturalWonder != null && !ruleset.terrains.containsKey(naturalWonder)) if (naturalWonder != null && !ruleset.terrains.containsKey(naturalWonder))
naturalWonder = null naturalWonder = null

View File

@ -683,15 +683,8 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
cityConstructions.addBuilding(name) cityConstructions.addBuilding(name)
val improvement = getImprovement(civInfo.gameInfo.ruleSet) /** Support for [UniqueType.CreatesOneImprovement] */
if (improvement != null) { cityConstructions.applyCreateOneImprovement(this)
val tileWithImprovement = cityConstructions.cityInfo.getTiles().firstOrNull { it.improvementInProgress == improvement.name }
if (tileWithImprovement != null) {
tileWithImprovement.turnsToImprovement = 0
tileWithImprovement.improvementInProgress = null
tileWithImprovement.improvement = improvement.name
}
}
// "Provides a free [buildingName] [cityFilter]" // "Provides a free [buildingName] [cityFilter]"
cityConstructions.addFreeBuildings() cityConstructions.addFreeBuildings()
@ -741,10 +734,20 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
return false return false
} }
fun getImprovement(ruleset: Ruleset): TileImprovement? { private val _hasCreatesOneImprovementUnique by lazy {
val improvementUnique = getMatchingUniques("Creates a [] improvement on a specific tile") 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 .firstOrNull() ?: return null
return ruleset.tileImprovements[improvementUnique.params[0]] _getImprovementToCreate = ruleset.tileImprovements[improvementUnique.params[0]]
}
return _getImprovementToCreate
} }
fun isSellable() = !isAnyWonder() && !hasUnique(UniqueType.Unsellable) fun isSellable() = !isAnyWonder() && !hasUnique(UniqueType.Unsellable)

View File

@ -657,9 +657,6 @@ class Ruleset {
if (building.requiredBuildingInAllCities != null) if (building.requiredBuildingInAllCities != null)
lines.add("${building.name} contains 'requiredBuildingInAllCities' - please convert to a \"" + lines.add("${building.name} contains 'requiredBuildingInAllCities' - please convert to a \"" +
UniqueType.RequiresBuildingInAllCities.text.fillPlaceholders(building.requiredBuildingInAllCities!!)+"\" unique", RulesetErrorSeverity.Warning) 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) checkUniques(building, lines, rulesetSpecific, forOptionsPopup)
} }

View File

@ -364,10 +364,12 @@ enum class UniqueParameterType(
} }
}, },
/** [UniqueType.ConstructImprovementConsumingUnit] */ /** For [UniqueType.ConstructImprovementConsumingUnit], [UniqueType.CreatesOneImprovement] */
ImprovementName("improvementName", "Trading Post", "The name of any improvement"){ 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? { UniqueType.UniqueComplianceErrorSeverity? {
if (parameterText == Constants.cancelImprovementOrder)
return UniqueType.UniqueComplianceErrorSeverity.RulesetInvariant
if (ruleset.tileImprovements.containsKey(parameterText)) return null if (ruleset.tileImprovements.containsKey(parameterText)) return null
return UniqueType.UniqueComplianceErrorSeverity.RulesetSpecific return UniqueType.UniqueComplianceErrorSeverity.RulesetSpecific
} }

View File

@ -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), NotDestroyedWhenCityCaptured("Never destroyed when the city is captured", UniqueTarget.Building),
DoublesGoldFromCapturingCity("Doubles Gold given to enemy if 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), RemoveAnnexUnhappiness("Remove extra unhappiness from annexed cities", UniqueTarget.Building),
ConnectTradeRoutes("Connects trade routes over water", UniqueTarget.Building), ConnectTradeRoutes("Connects trade routes over water", UniqueTarget.Building),
CreatesOneImprovement("Creates a [improvementName] improvement on a specific tile", UniqueTarget.Building),
//endregion //endregion
@ -614,6 +614,7 @@ enum class UniqueType(val text: String, vararg targets: UniqueTarget, val flags:
DefensiveBonus("Gives a defensive bonus of [relativeAmount]%", UniqueTarget.Improvement), DefensiveBonus("Gives a defensive bonus of [relativeAmount]%", UniqueTarget.Improvement),
ImprovementMaintenance("Costs [amount] gold per turn when in your territory", UniqueTarget.Improvement), // Unused 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), 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), GreatImprovement("Great Improvement", UniqueTarget.Improvement),
IsAncientRuinsEquivalent("Provides a random bonus when entered", UniqueTarget.Improvement), IsAncientRuinsEquivalent("Provides a random bonus when entered", UniqueTarget.Improvement),

View File

@ -9,8 +9,10 @@ import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.Constants import com.unciv.Constants
import com.unciv.logic.city.* import com.unciv.logic.city.*
import com.unciv.logic.map.TileInfo
import com.unciv.models.UncivSound import com.unciv.models.UncivSound
import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
@ -37,7 +39,6 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
/* -1 = Nothing, >= 0 queue entry (0 = current construction) */ /* -1 = Nothing, >= 0 queue entry (0 = current construction) */
private var selectedQueueEntry = -1 // None private var selectedQueueEntry = -1 // None
private var preferredBuyStat = Stat.Gold // Used for keyboard buy private var preferredBuyStat = Stat.Gold // Used for keyboard buy
var improvementBuildingToConstruct: Building? = null
private val upperTable = Table(BaseScreen.skin) private val upperTable = Table(BaseScreen.skin)
private val showCityInfoTableButton = "Show stats drilldown".toTextButton() 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 /** 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...") * - 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() { fun addActorsToStage() {
cityScreen.stage.addActor(upperTable) cityScreen.stage.addActor(upperTable)
@ -296,8 +297,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
table.touchable = Touchable.enabled table.touchable = Touchable.enabled
table.onClick { table.onClick {
cityScreen.selectedConstruction = cityConstructions.getConstruction(constructionName) cityScreen.selectConstruction(constructionName)
cityScreen.selectedTile = null
selectedQueueEntry = constructionQueueIndex selectedQueueEntry = constructionQueueIndex
cityScreen.update() cityScreen.update()
} }
@ -326,7 +326,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
pickConstructionButton.background = ImageGetter.getBackground(Color.BLACK) pickConstructionButton.background = ImageGetter.getBackground(Color.BLACK)
pickConstructionButton.touchable = Touchable.enabled 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)) 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)) { if (!cannotAddConstructionToQueue(construction, cityScreen.city, cityScreen.city.cityConstructions)) {
val addToQueueButton = ImageGetter.getImage("OtherIcons/New").apply { color = Color.BLACK }.surroundWithCircle(40f) 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) addConstructionToQueue(construction, cityScreen.city.cityConstructions)
} }
pickConstructionButton.add(addToQueueButton) pickConstructionButton.add(addToQueueButton)
@ -350,8 +350,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
.colspan(pickConstructionButton.columns).fillX().left().padTop(2f) .colspan(pickConstructionButton.columns).fillX().left().padTop(2f)
} }
pickConstructionButton.onClick { pickConstructionButton.onClick {
cityScreen.selectedConstruction = construction cityScreen.selectConstruction(construction)
cityScreen.selectedTile = null
selectedQueueEntry = -1 selectedQueueEntry = -1
cityScreen.update() cityScreen.update()
} }
@ -381,7 +380,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
else { else {
button.onClick { button.onClick {
cityConstructions.removeFromQueue(selectedQueueEntry, false) cityConstructions.removeFromQueue(selectedQueueEntry, false)
cityScreen.selectedConstruction = null cityScreen.clearSelection()
selectedQueueEntry = -1 selectedQueueEntry = -1
cityScreen.update() cityScreen.update()
} }
@ -392,7 +391,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
|| cannotAddConstructionToQueue(construction, city, cityConstructions)) { || cannotAddConstructionToQueue(construction, city, cityConstructions)) {
button.disable() button.disable()
} else { } else {
button.onClick(getConstructionSound(construction)) { button.onClick(UncivSound.Silent) {
addConstructionToQueue(construction, cityConstructions) addConstructionToQueue(construction, cityConstructions)
} }
} }
@ -404,17 +403,21 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
private fun addConstructionToQueue(construction: IConstruction, cityConstructions: CityConstructions) { private fun addConstructionToQueue(construction: IConstruction, cityConstructions: CityConstructions) {
// Some evil person decided to double tap real fast - #4977 // 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 return
if (construction is Building && construction.uniqueObjects.any { it.placeholderText == "Creates a [] improvement on a specific tile" }) {
cityScreen.selectedTile // UniqueType.CreatesOneImprovement support - don't add yet, postpone until target tile for the improvement is selected
improvementBuildingToConstruct = construction if (construction is Building && construction.hasCreateOneImprovementUnique()) {
cityScreen.startPickTileForCreatesOneImprovement(construction, Stat.Gold, false)
return return
} }
cityScreen.stopPickTileForCreatesOneImprovement()
Sounds.play(getConstructionSound(construction))
cityConstructions.addToQueue(construction.name) cityConstructions.addToQueue(construction.name)
if (!construction.shouldBeDisplayed(cityConstructions)) // For buildings - unlike units which can be queued multiple times if (!construction.shouldBeDisplayed(cityConstructions)) // For buildings - unlike units which can be queued multiple times
cityScreen.selectedConstruction = null cityScreen.clearSelection()
cityScreen.update() cityScreen.update()
cityScreen.game.settings.addCompletedTutorialTask("Pick construction") cityScreen.game.settings.addCompletedTutorialTask("Pick construction")
} }
@ -454,7 +457,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
button.onClick { button.onClick {
button.disable() button.disable()
askToBuyConstruction(construction, stat) buyButtonOnClick(construction, stat)
} }
button.isEnabled = isConstructionPurchaseAllowed(construction, stat, constructionBuyCost) button.isEnabled = isConstructionPurchaseAllowed(construction, stat, constructionBuyCost)
button.addTooltip('B') // The key binding is done in CityScreen constructor button.addTooltip('B') // The key binding is done in CityScreen constructor
@ -466,12 +469,28 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
return button 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]. /** Ask whether user wants to buy [construction] for [stat].
* *
* Used from onClick and keyboard dispatch, thus only minimal parameters are passed, * Used from onClick and keyboard dispatch, thus only minimal parameters are passed,
* and it needs to do all checks and the sound as appropriate. * 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 if (!isConstructionPurchaseShown(construction, stat)) return
val city = cityScreen.city val city = cityScreen.city
val constructionBuyCost = construction.getStatBuyCost(city, stat)!! 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() "Would you like to purchase [${construction.name}] for [$constructionBuyCost] [${stat.character}]?".tr()
YesNoPopup( YesNoPopup(
purchasePrompt, purchasePrompt,
action = { purchaseConstruction(construction, stat) }, action = { purchaseConstruction(construction, stat, tile) },
screen = cityScreen, screen = cityScreen,
restoreDefault = { cityScreen.update() } restoreDefault = { cityScreen.update() }
).open() ).open()
@ -510,11 +529,17 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
} }
} }
// called only by askToBuyConstruction's Yes answer /** Called only by askToBuyConstruction's Yes answer - not to be confused with [CityConstructions.purchaseConstruction]
private fun purchaseConstruction(construction: INonPerpetualConstruction, stat: Stat = Stat.Gold) { * @param tile supports [UniqueType.CreatesOneImprovement]
*/
private fun purchaseConstruction(
construction: INonPerpetualConstruction,
stat: Stat = Stat.Gold,
tile: TileInfo? = null
) {
Sounds.play(stat.purchaseSound) Sounds.play(stat.purchaseSound)
val city = cityScreen.city 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 { Popup(cityScreen).apply {
add("No space available to place [${construction.name}] near [${city.name}]".tr()).row() add("No space available to place [${construction.name}] near [${city.name}]".tr()).row()
addCloseButton() addCloseButton()
@ -524,12 +549,15 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
} }
if (isSelectedQueueEntry() || cityScreen.selectedConstruction?.isBuildable(city.cityConstructions) != true) { if (isSelectedQueueEntry() || cityScreen.selectedConstruction?.isBuildable(city.cityConstructions) != true) {
selectedQueueEntry = -1 selectedQueueEntry = -1
cityScreen.selectedConstruction = null cityScreen.clearSelection()
// Allow buying next queued or auto-assigned construction right away // Allow buying next queued or auto-assigned construction right away
city.cityConstructions.chooseNextConstruction() city.cityConstructions.chooseNextConstruction()
if (city.cityConstructions.currentConstructionFromQueue.isNotEmpty()) if (city.cityConstructions.currentConstructionFromQueue.isNotEmpty()) {
cityScreen.selectedConstruction = city.cityConstructions.getCurrentConstruction().takeIf { it is INonPerpetualConstruction } val newConstruction = city.cityConstructions.getCurrentConstruction()
if (newConstruction is INonPerpetualConstruction)
cityScreen.selectConstruction(newConstruction)
}
} }
cityScreen.update() cityScreen.update()
} }
@ -542,8 +570,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
tab.onClick { tab.onClick {
tab.touchable = Touchable.disabled tab.touchable = Touchable.disabled
city.cityConstructions.raisePriority(constructionQueueIndex) city.cityConstructions.raisePriority(constructionQueueIndex)
cityScreen.selectedConstruction = cityScreen.city.cityConstructions.getConstruction(name) cityScreen.selectConstruction(name)
cityScreen.selectedTile = null
selectedQueueEntry = constructionQueueIndex - 1 selectedQueueEntry = constructionQueueIndex - 1
cityScreen.update() cityScreen.update()
} }
@ -559,8 +586,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
tab.onClick { tab.onClick {
tab.touchable = Touchable.disabled tab.touchable = Touchable.disabled
city.cityConstructions.lowerPriority(constructionQueueIndex) city.cityConstructions.lowerPriority(constructionQueueIndex)
cityScreen.selectedConstruction = cityScreen.city.cityConstructions.getConstruction(name) cityScreen.selectConstruction(name)
cityScreen.selectedTile = null
selectedQueueEntry = constructionQueueIndex + 1 selectedQueueEntry = constructionQueueIndex + 1
cityScreen.update() cityScreen.update()
} }
@ -576,7 +602,7 @@ class CityConstructionsTable(private val cityScreen: CityScreen) {
tab.onClick { tab.onClick {
tab.touchable = Touchable.disabled tab.touchable = Touchable.disabled
city.cityConstructions.removeFromQueue(constructionQueueIndex, false) city.cityConstructions.removeFromQueue(constructionQueueIndex, false)
cityScreen.selectedConstruction = null cityScreen.clearSelection()
cityScreen.update() cityScreen.update()
} }
} }

View File

@ -6,20 +6,27 @@ import com.badlogic.gdx.scenes.scene2d.ui.Image
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.automation.Automation
import com.unciv.logic.city.CityInfo import com.unciv.logic.city.CityInfo
import com.unciv.logic.city.IConstruction import com.unciv.logic.city.IConstruction
import com.unciv.logic.city.INonPerpetualConstruction import com.unciv.logic.city.INonPerpetualConstruction
import com.unciv.logic.map.TileInfo 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.images.ImageGetter
import com.unciv.ui.map.TileGroupMap 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.tilegroups.TileSetStrings
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
import java.util.* import java.util.*
class CityScreen( class CityScreen(
internal val city: CityInfo, internal val city: CityInfo,
var selectedConstruction: IConstruction? = null, initSelectedConstruction: IConstruction? = null,
var selectedTile: TileInfo? = null initSelectedTile: TileInfo? = null
): BaseScreen() { ): BaseScreen() {
companion object { companion object {
/** Distance from stage edges to floating widgets */ /** Distance from stage edges to floating widgets */
@ -73,6 +80,27 @@ class CityScreen(
/** The ScrollPane for the background map view of the city surroundings */ /** The ScrollPane for the background map view of the city surroundings */
private val mapScrollPane = ZoomableScrollPane() 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 { init {
onBackButtonClicked { game.setWorldScreen() } onBackButtonClicked { game.setWorldScreen() }
UncivGame.Current.settings.addCompletedTutorialTask("Enter city screen") UncivGame.Current.settings.addCompletedTutorialTask("Enter city screen")
@ -160,21 +188,38 @@ class CityScreen(
} }
private fun updateTileGroups() { 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<Color, Float> {
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) { for (tileGroup in tileGroups) {
tileGroup.update() tileGroup.update()
tileGroup.hideHighlight() tileGroup.hideHighlight()
if (city.tiles.contains(tileGroup.tileInfo.position) when {
&& constructionsTable.improvementBuildingToConstruct != null) { tileGroup.tileInfo == nextTileToOwn -> {
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.showHighlight(Color.PURPLE)
tileGroup.setColor(0f, 0f, 0f, 0.7f) 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 tileSetStrings = TileSetStrings()
val cityTileGroups = cityInfo.getCenterTile().getTilesInDistance(5) 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) } .map { CityTileGroup(cityInfo, it, tileSetStrings) }
for (tileGroup in cityTileGroups) { for (tileGroup in cityTileGroups) {
val tileInfo = tileGroup.tileInfo
tileGroup.onClick { tileGroup.onClick {
if (city.isPuppet) return@onClick tileGroupOnClick(tileGroup, cityInfo)
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()
}
tileGroups.add(tileGroup) tileGroups.add(tileGroup)
} }
val tilesToUnwrap = ArrayList<CityTileGroup>() val tilesToUnwrap = ArrayList<CityTileGroup>()
for (tileGroup in tileGroups) { for (tileGroup in tileGroups) {
val xDifference = city.getCenterTile().position.x - tileGroup.tileInfo.position.x val xDifference = cityInfo.getCenterTile().position.x - tileGroup.tileInfo.position.x
val yDifference = city.getCenterTile().position.y - tileGroup.tileInfo.position.y 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 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) { if (xDifference > 5 || xDifference < -5 || yDifference > 5 || yDifference < -5) {
//so we want to unwrap its position //so we want to unwrap its position
@ -299,6 +314,78 @@ class CityScreen(
mapScrollPane.updateVisualScroll() 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() { fun exit() {
game.setWorldScreen() game.setWorldScreen()
game.worldScreen.mapHolder.setCenterPosition(city.location) game.worldScreen.mapHolder.setCenterPosition(city.location)

View File

@ -111,7 +111,7 @@ class CityScreenTileTable(private val cityScreen: CityScreen): Table() {
Sounds.play(UncivSound.Coin) Sounds.play(UncivSound.Coin)
city.expansion.buyTile(selectedTile) city.expansion.buyTile(selectedTile)
// preselect the next tile on city screen rebuild so bulk buying can go faster // 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, screen = cityScreen,
restoreDefault = { cityScreen.update() } restoreDefault = { cityScreen.update() }

View File

@ -26,12 +26,14 @@ class ImprovementPickerScreen(
private val gameInfo = tileInfo.tileMap.gameInfo private val gameInfo = tileInfo.tileMap.gameInfo
private val ruleSet = gameInfo.ruleSet private val ruleSet = gameInfo.ruleSet
private val currentPlayerCiv = gameInfo.getCurrentPlayerCivilization() private val currentPlayerCiv = gameInfo.getCurrentPlayerCivilization()
// Support for UniqueType.CreatesOneImprovement
private val tileMarkedForCreatesOneImprovement = tileInfo.isMarkedForCreatesOneImprovement()
private fun getRequiredTechColumn(improvement: TileImprovement) = private fun getRequiredTechColumn(improvement: TileImprovement) =
ruleSet.technologies[improvement.techRequired]?.column?.columnNumber ?: -1 ruleSet.technologies[improvement.techRequired]?.column?.columnNumber ?: -1
fun accept(improvement: TileImprovement?) { fun accept(improvement: TileImprovement?) {
if (improvement == null) return if (improvement == null || tileMarkedForCreatesOneImprovement) return
if (improvement.name == Constants.cancelImprovementOrder) { if (improvement.name == Constants.cancelImprovementOrder) {
tileInfo.stopWorkingOnImprovement() tileInfo.stopWorkingOnImprovement()
// no onAccept() - Worker can stay selected // no onAccept() - Worker can stay selected
@ -108,8 +110,9 @@ class ImprovementPickerScreen(
val pickNow = when { val pickNow = when {
suggestRemoval -> "${Constants.remove}[${tileInfo.getLastTerrain().name}] first".toLabel() suggestRemoval -> "${Constants.remove}[${tileInfo.getLastTerrain().name}] first".toLabel()
tileInfo.improvementInProgress != improvement.name -> "Pick now!".toLabel().onClick { accept(improvement) } tileInfo.improvementInProgress == improvement.name -> "Current construction".toLabel()
else -> "Current construction".toLabel() tileMarkedForCreatesOneImprovement -> null
else -> "Pick now!".toLabel().onClick { accept(improvement) }
} }
val statIcons = getStatIconsTable(provideResource, removeImprovement) val statIcons = getStatIconsTable(provideResource, removeImprovement)
@ -126,7 +129,6 @@ class ImprovementPickerScreen(
val statsTable = getStatsTable(stats) val statsTable = getStatsTable(stats)
statIcons.add(statsTable).padLeft(13f) statIcons.add(statsTable).padLeft(13f)
regularImprovements.add(statIcons).align(Align.right) regularImprovements.add(statIcons).align(Align.right)
val improvementButton = getPickerOptionButton(image, labelText) val improvementButton = getPickerOptionButton(image, labelText)
@ -137,16 +139,14 @@ class ImprovementPickerScreen(
} }
if (improvement.name == tileInfo.improvementInProgress) improvementButton.color = Color.GREEN if (improvement.name == tileInfo.improvementInProgress) improvementButton.color = Color.GREEN
if (suggestRemoval){ if (suggestRemoval || tileMarkedForCreatesOneImprovement) {
improvementButton.disable() improvementButton.disable()
} } else if (shortcutKey != null) {
regularImprovements.add(improvementButton)
if (shortcutKey != null) {
keyPressDispatcher[shortcutKey] = { accept(improvement) } keyPressDispatcher[shortcutKey] = { accept(improvement) }
improvementButton.addTooltip(shortcutKey) improvementButton.addTooltip(shortcutKey)
} }
regularImprovements.add(improvementButton)
regularImprovements.add(pickNow).padLeft(10f).fillY() regularImprovements.add(pickNow).padLeft(10f).fillY()
regularImprovements.row() regularImprovements.row()
} }

View File

@ -280,13 +280,7 @@ object UnitActions {
return UnitAction(UnitActionType.Pillage, return UnitAction(UnitActionType.Pillage,
action = { action = {
// http://well-of-souls.com/civ/civ5_improvements.html says that naval improvements are destroyed upon pillage tile.setPillaged()
// 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
unit.civInfo.lastSeenImprovement.remove(tile.position) unit.civInfo.lastSeenImprovement.remove(tile.position)
if (tile.resource != null) tile.getOwner()?.updateDetailedCivResources() // this might take away a resource if (tile.resource != null) tile.getOwner()?.updateDetailedCivResources() // this might take away a resource
@ -408,7 +402,6 @@ object UnitActions {
&& unit.canBuildImprovement(it) && unit.canBuildImprovement(it)
} }
actionList += UnitAction(UnitActionType.ConstructImprovement, actionList += UnitAction(UnitActionType.ConstructImprovement,
isCurrentAction = unit.currentTile.hasImprovementInProgress(), isCurrentAction = unit.currentTile.hasImprovementInProgress(),
action = { action = {
@ -652,11 +645,10 @@ object UnitActions {
val improvement = tile.ruleset.tileImprovements[improvementName] val improvement = tile.ruleset.tileImprovements[improvementName]
?: continue ?: continue
var resourcesAvailable = true val resourcesAvailable = improvement.uniqueObjects.none {
if (improvement.uniqueObjects.any { it.isOfType(UniqueType.ConsumesResources) &&
it.isOfType(UniqueType.ConsumesResources) && civResources[unique.params[1]] ?: 0 < unique.params[0].toInt() civResources[unique.params[1]] ?: 0 < unique.params[0].toInt()
}) }
resourcesAvailable = false
finalActions += UnitAction(UnitActionType.Create, finalActions += UnitAction(UnitActionType.Create,
title = "Create [$improvementName]", title = "Create [$improvementName]",
@ -670,10 +662,10 @@ object UnitActions {
it in improvement.terrainsCanBeBuiltOn it in improvement.terrainsCanBeBuiltOn
} }
) )
unitTile.removeCreatesOneImprovementMarker()
unitTile.improvement = improvementName unitTile.improvement = improvementName
unitTile.improvementInProgress = null unitTile.stopWorkingOnImprovement()
unitTile.turnsToImprovement = 0 if (improvement.hasUnique(UniqueType.TakeOverTilesAroundWhenBuilt))
if (improvementName == Constants.citadel)
takeOverTilesAround(unit) takeOverTilesAround(unit)
val city = unitTile.getCity() val city = unitTile.getCity()
if (city != null) { if (city != null) {
@ -687,6 +679,9 @@ object UnitActions {
resourcesAvailable resourcesAvailable
&& unit.currentMovement > 0f && unit.currentMovement > 0f
&& tile.canBuildImprovement(improvement, unit.civInfo) && 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... && !tile.isImpassible() // Not 100% sure that this check is necessary...
}) })
} }