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",
"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

View File

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

View File

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

View File

@ -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.

View File

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

View File

@ -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<ConstructionChoice>, choice: String, choiceModifier: Float){
choices.add(ConstructionChoice(choice,choiceModifier,cityConstructions.getRemainingWork(choice)))
private fun addChoice(choices: ArrayList<ConstructionChoice>, 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

View File

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

View File

@ -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,8 +49,7 @@ class CityConstructions {
@Transient
val builtBuildingUniqueMap = UniqueMap()
var builtBuildings = HashSet<String>()
val inProgressConstructions = HashMap<String, Int>()
// No backing field, not serialized
var currentConstructionFromQueue: String
get() {
return if (constructionQueue.isEmpty()) ""
@ -52,15 +58,23 @@ class CityConstructions {
set(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 constructionQueue = mutableListOf<String>()
var productionOverflow = 0
val queueMaxSize = 10
private val queueMaxSize = 10
// Maps cities to the buildings they received
val freeBuildingsProvidedFromThisCity: HashMap<String, HashSet<String>> = hashMapOf()
//endregion
//region pure functions
fun clone(): CityConstructions {
val toReturn = CityConstructions()
toReturn.builtBuildings.addAll(builtBuildings)
@ -111,49 +125,14 @@ 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 {
val construction = getConstruction(constructionName)
@ -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]
@ -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,22 +502,36 @@ 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
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)
@ -523,21 +559,12 @@ 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<String>.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
}

View File

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

View File

@ -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"

View File

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

View File

@ -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<FormattedLine> {
val lineList = ArrayList<FormattedLine>()
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

View File

@ -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")
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
return ruleset.tileImprovements[improvementUnique.params[0]]
_getImprovementToCreate = ruleset.tileImprovements[improvementUnique.params[0]]
}
return _getImprovementToCreate
}
fun isSellable() = !isAnyWonder() && !hasUnique(UniqueType.Unsellable)

View File

@ -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)
}

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"){
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
}

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),
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
@ -614,6 +614,7 @@ 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),

View File

@ -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)
@ -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
@ -466,12 +469,28 @@ 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,12 +549,15 @@ 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()
}
@ -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()
}
}

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.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,21 +188,38 @@ 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<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) {
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) {
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()
tileGroupOnClick(tileGroup, cityInfo)
}
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)
}
val tilesToUnwrap = ArrayList<CityTileGroup>()
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)

View File

@ -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() }

View File

@ -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()
}

View File

@ -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
@ -408,7 +402,6 @@ object UnitActions {
&& unit.canBuildImprovement(it)
}
actionList += UnitAction(UnitActionType.ConstructImprovement,
isCurrentAction = unit.currentTile.hasImprovementInProgress(),
action = {
@ -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]",
@ -670,10 +662,10 @@ object UnitActions {
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...
})
}