Improvement queue (#11677)

* Improvement queue: Framework

* Improvement queue: Minimal UI

* Remove debug code

* Fix merge error

* Address tuvus's input

* Implement tuvus's UI wishes

* Fix merge errors

* Fix more merge errors
This commit is contained in:
SomeTroglodyte
2024-06-14 16:39:46 +02:00
committed by GitHub
parent 728713dc3e
commit e74897469c
9 changed files with 193 additions and 106 deletions

View File

@ -1,6 +1,5 @@
package com.unciv.logic.map.mapunit package com.unciv.logic.map.mapunit
import com.unciv.UncivGame
import com.unciv.logic.civilization.LocationAction import com.unciv.logic.civilization.LocationAction
import com.unciv.logic.civilization.MapUnitAction import com.unciv.logic.civilization.MapUnitAction
import com.unciv.logic.civilization.NotificationCategory import com.unciv.logic.civilization.NotificationCategory
@ -19,7 +18,12 @@ class UnitTurnManager(val unit: MapUnit) {
if (unit.currentMovement > 0 if (unit.currentMovement > 0
&& unit.getTile().improvementInProgress != null && unit.getTile().improvementInProgress != null
&& unit.canBuildImprovement(unit.getTile().getTileImprovementInProgress()!!) && unit.canBuildImprovement(unit.getTile().getTileImprovementInProgress()!!)
) workOnImprovement() ) {
val tile = unit.getTile()
if (tile.doWorkerTurn(unit))
tile.getCity()?.updateCitizens = true
}
if (!unit.hasUnitMovedThisTurn() && unit.isFortified() && unit.turnsFortified < 2) { if (!unit.hasUnitMovedThisTurn() && unit.isFortified() && unit.turnsFortified < 2) {
unit.turnsFortified++ unit.turnsFortified++
} }
@ -162,23 +166,4 @@ class UnitTurnManager(val unit: MapUnit) {
unit.addMovementMemory() unit.addMovementMemory()
unit.attacksSinceTurnStart.clear() unit.attacksSinceTurnStart.clear()
} }
private fun workOnImprovement() {
val tile = unit.getTile()
if (tile.isMarkedForCreatesOneImprovement()) return
tile.turnsToImprovement -= 1
if (tile.turnsToImprovement != 0) return
if (unit.civ.isCurrentPlayer())
UncivGame.Current.settings.addCompletedTutorialTask("Construct an improvement")
val improvementInProgress = tile.improvementInProgress ?: return
tile.setImprovement(improvementInProgress, unit.civ, unit)
tile.improvementInProgress = null
tile.getCity()?.updateCitizens = true
}
} }

View File

@ -1,8 +1,11 @@
package com.unciv.logic.map.tile package com.unciv.logic.map.tile
import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.utils.Json
import com.badlogic.gdx.utils.JsonValue
import com.unciv.Constants import com.unciv.Constants
import com.unciv.GUI import com.unciv.GUI
import com.unciv.UncivGame
import com.unciv.logic.IsPartOfGameInfoSerialization import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.logic.MultiFilter import com.unciv.logic.MultiFilter
import com.unciv.logic.city.City import com.unciv.logic.city.City
@ -14,6 +17,7 @@ import com.unciv.logic.map.TileMap
import com.unciv.logic.map.mapgenerator.MapGenerator import com.unciv.logic.map.mapgenerator.MapGenerator
import com.unciv.logic.map.mapgenerator.MapResourceSetting import com.unciv.logic.map.mapgenerator.MapResourceSetting
import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.mapunit.UnitTurnManager
import com.unciv.logic.map.mapunit.movement.UnitMovement import com.unciv.logic.map.mapunit.movement.UnitMovement
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.tile.ResourceType
@ -27,13 +31,14 @@ import com.unciv.models.ruleset.unique.UniqueMap
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.components.extensions.withItem import com.unciv.ui.components.extensions.withItem
import com.unciv.ui.components.extensions.withoutItem import com.unciv.ui.components.extensions.withoutItem
import com.unciv.ui.components.fonts.Fonts
import com.unciv.utils.DebugUtils import com.unciv.utils.DebugUtils
import com.unciv.utils.Log import com.unciv.utils.Log
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min import kotlin.math.min
import kotlin.random.Random import kotlin.random.Random
class Tile : IsPartOfGameInfoSerialization { class Tile : IsPartOfGameInfoSerialization, Json.Serializable {
//region Serialized fields //region Serialized fields
var militaryUnit: MapUnit? = null var militaryUnit: MapUnit? = null
var civilianUnit: MapUnit? = null var civilianUnit: MapUnit? = null
@ -56,13 +61,27 @@ class Tile : IsPartOfGameInfoSerialization {
var resourceAmount: Int = 0 var resourceAmount: Int = 0
var improvement: String? = null var improvement: String? = null
var improvementInProgress: String? = null
var improvementIsPillaged = false var improvementIsPillaged = false
internal class ImprovementQueueEntry(
val improvement: String, turnsToImprovement: Int
) : IsPartOfGameInfoSerialization {
@Suppress("unused") // Gdx Json will find this constructor and use it
private constructor() : this("", 0)
var turnsToImprovement: Int = turnsToImprovement
private set
override fun toString() = "$improvement: $turnsToImprovement${Fonts.turn}"
/** @return `true` if it's still counting and not finished */
fun countDown(): Boolean {
turnsToImprovement = (turnsToImprovement - 1).coerceAtLeast(0)
return turnsToImprovement > 0
}
}
private val improvementQueue = ArrayList<ImprovementQueueEntry>(1)
var roadStatus = RoadStatus.None var roadStatus = RoadStatus.None
var roadIsPillaged = false var roadIsPillaged = false
private var roadOwner: String = "" // either who last built the road or last owner of tile private var roadOwner: String = "" // either who last built the road or last owner of tile
var turnsToImprovement: Int = 0
var hasBottomRightRiver = false var hasBottomRightRiver = false
var hasBottomRiver = false var hasBottomRiver = false
@ -168,6 +187,10 @@ class Tile : IsPartOfGameInfoSerialization {
private var isAdjacentToRiver = false private var isAdjacentToRiver = false
@Transient @Transient
private var isAdjacentToRiverKnown = false private var isAdjacentToRiverKnown = false
val improvementInProgress get() = improvementQueue.firstOrNull()?.improvement
val turnsToImprovement get() = improvementQueue.firstOrNull()?.turnsToImprovement ?: 0
//endregion //endregion
fun clone(): Tile { fun clone(): Tile {
@ -191,12 +214,11 @@ class Tile : IsPartOfGameInfoSerialization {
toReturn.resource = resource toReturn.resource = resource
toReturn.resourceAmount = resourceAmount toReturn.resourceAmount = resourceAmount
toReturn.improvement = improvement toReturn.improvement = improvement
toReturn.improvementInProgress = improvementInProgress toReturn.improvementQueue.addAll(improvementQueue)
toReturn.improvementIsPillaged = improvementIsPillaged toReturn.improvementIsPillaged = improvementIsPillaged
toReturn.roadStatus = roadStatus toReturn.roadStatus = roadStatus
toReturn.roadIsPillaged = roadIsPillaged toReturn.roadIsPillaged = roadIsPillaged
toReturn.roadOwner = roadOwner toReturn.roadOwner = roadOwner
toReturn.turnsToImprovement = turnsToImprovement
toReturn.hasBottomLeftRiver = hasBottomLeftRiver toReturn.hasBottomLeftRiver = hasBottomLeftRiver
toReturn.hasBottomRightRiver = hasBottomRightRiver toReturn.hasBottomRightRiver = hasBottomRightRiver
toReturn.hasBottomRiver = hasBottomRiver toReturn.hasBottomRiver = hasBottomRiver
@ -249,11 +271,12 @@ class Tile : IsPartOfGameInfoSerialization {
fun isNaturalWonder(): Boolean = naturalWonder != null fun isNaturalWonder(): Boolean = naturalWonder != null
fun isImpassible() = lastTerrain.impassable fun isImpassible() = lastTerrain.impassable
fun hasImprovementInProgress() = improvementQueue.isNotEmpty()
fun getTileImprovement(): TileImprovement? = if (improvement == null) null else ruleset.tileImprovements[improvement!!] fun getTileImprovement(): TileImprovement? = if (improvement == null) null else ruleset.tileImprovements[improvement!!]
fun isPillaged(): Boolean = improvementIsPillaged || roadIsPillaged fun isPillaged(): Boolean = improvementIsPillaged || roadIsPillaged
fun getUnpillagedTileImprovement(): TileImprovement? = if (getUnpillagedImprovement() == null) null else ruleset.tileImprovements[improvement!!] fun getUnpillagedTileImprovement(): TileImprovement? = if (getUnpillagedImprovement() == null) null else ruleset.tileImprovements[improvement!!]
fun getTileImprovementInProgress(): TileImprovement? = if (improvementInProgress == null) null else ruleset.tileImprovements[improvementInProgress!!] fun getTileImprovementInProgress(): TileImprovement? = improvementQueue.firstOrNull()?.let { ruleset.tileImprovements[it.improvement] }
fun containsGreatImprovement() = getTileImprovement()?.isGreatImprovement() == true fun containsGreatImprovement() = getTileImprovement()?.isGreatImprovement() == true
fun getImprovementToPillage(): TileImprovement? { fun getImprovementToPillage(): TileImprovement? {
@ -494,8 +517,6 @@ class Tile : IsPartOfGameInfoSerialization {
} }
} }
fun hasImprovementInProgress() = improvementInProgress != null && turnsToImprovement > 0
fun isCoastalTile() = _isCoastalTile fun isCoastalTile() = _isCoastalTile
fun hasViewableResource(civInfo: Civilization): Boolean = fun hasViewableResource(civInfo: Civilization): Boolean =
@ -822,13 +843,12 @@ class Tile : IsPartOfGameInfoSerialization {
} }
} }
/** Does not remove roads */ /** Does not remove roads */
fun removeImprovement() = fun removeImprovement() =
improvementFunctions.changeImprovement(null) improvementFunctions.setImprovement(null)
fun setImprovement(improvementStr: String, civToHandleCompletion: Civilization? = null, unit: MapUnit? = null) = fun setImprovement(improvementStr: String, civToHandleCompletion: Civilization? = null, unit: MapUnit? = null) =
improvementFunctions.changeImprovement(improvementStr, civToHandleCompletion, unit) improvementFunctions.setImprovement(improvementStr, civToHandleCompletion, unit)
// function handling when adding a road to the tile // function handling when adding a road to the tile
fun addRoad(roadType: RoadStatus, creatingCivInfo: Civilization?) { fun addRoad(roadType: RoadStatus, creatingCivInfo: Civilization?) {
@ -852,15 +872,41 @@ class Tile : IsPartOfGameInfoSerialization {
} }
fun startWorkingOnImprovement(improvement: TileImprovement, civInfo: Civilization, unit: MapUnit) { fun startWorkingOnImprovement(improvement: TileImprovement, civInfo: Civilization, unit: MapUnit) {
improvementInProgress = improvement.name improvementQueue.clear()
turnsToImprovement = if (civInfo.gameInfo.gameParameters.godMode) 1 queueImprovement(improvement, civInfo, unit)
else improvement.getTurnsToBuild(civInfo, unit)
} }
/** Clears [improvementInProgress] and [turnsToImprovement] */ /** Clears [improvementQueue] */
fun stopWorkingOnImprovement() { fun stopWorkingOnImprovement() {
improvementInProgress = null improvementQueue.clear()
turnsToImprovement = 0 }
/** Adds an entry to the [improvementQueue], by looking up the time it takes using [civInfo] and [unit] */
fun queueImprovement(improvement: TileImprovement, civInfo: Civilization, unit: MapUnit) {
val turns = if (civInfo.gameInfo.gameParameters.godMode) 1
else improvement.getTurnsToBuild(civInfo, unit)
queueImprovement(improvement.name, turns)
}
/** Adds an entry to the [improvementQueue] with explicit [turnsToImprovement] */
fun queueImprovement(improvementName: String, turnsToImprovement: Int) {
improvementQueue.add(ImprovementQueueEntry(improvementName, turnsToImprovement))
}
/** Called from [UnitTurnManager.endTurn] when a Worker "spends time" here
* @return `true` if any work got finished and upstream moght want to update things */
fun doWorkerTurn(worker: MapUnit): Boolean {
if (isMarkedForCreatesOneImprovement()) return false
if (improvementQueue.isEmpty()) return false
if (improvementQueue.first().countDown()) return false
val queueEntry = improvementQueue.removeAt(0)
if (worker.civ.isCurrentPlayer())
UncivGame.Current.settings.addCompletedTutorialTask("Construct an improvement")
setImprovement(queueEntry.improvement, worker.civ, worker)
return true
} }
/** Sets tile improvement to pillaged (without prior checks for validity) /** Sets tile improvement to pillaged (without prior checks for validity)
@ -878,8 +924,7 @@ class Tile : IsPartOfGameInfoSerialization {
// Setting turnsToImprovement might interfere with UniqueType.CreatesOneImprovement // Setting turnsToImprovement might interfere with UniqueType.CreatesOneImprovement
improvementFunctions.removeCreatesOneImprovementMarker() improvementFunctions.removeCreatesOneImprovementMarker()
improvementInProgress = null // remove any in progress work as well improvementQueue.clear() // remove any in progress work as well
turnsToImprovement = 0
// if no Repair action, destroy improvements instead // if no Repair action, destroy improvements instead
if (ruleset.tileImprovements[Constants.repair] == null) { if (ruleset.tileImprovements[Constants.repair] == null) {
if (canPillageTileImprovement()) if (canPillageTileImprovement())
@ -902,8 +947,7 @@ class Tile : IsPartOfGameInfoSerialization {
} }
fun setRepaired() { fun setRepaired() {
improvementInProgress = null improvementQueue.clear()
turnsToImprovement = 0
if (improvementIsPillaged) if (improvementIsPillaged)
improvementIsPillaged = false improvementIsPillaged = false
else else
@ -1016,5 +1060,23 @@ class Tile : IsPartOfGameInfoSerialization {
return lineList.joinToString() return lineList.joinToString()
} }
override fun write(json: Json) {
json.writeFields(this)
// Compatibility code for the case an improvementQueue-using game is loaded by an older version: Write fake fields
json.writeValue("improvementInProgress", improvementInProgress, String::class.java)
json.writeValue("turnsToImprovement", turnsToImprovement, Int::class.java)
}
override fun read(json: Json, jsonData: JsonValue) {
json.readFields(this, jsonData)
// Compatibility code for the case an pre-improvementQueue game is loaded by this version: Read legacy fields
if (improvementQueue.isEmpty() && jsonData.get("improvementQueue") == null) {
val improvementInProgress = jsonData.getString("improvementInProgress", "")
val turnsToImprovement = jsonData.getInt("turnsToImprovement", 0)
if (improvementInProgress.isNotEmpty() && turnsToImprovement != 0)
improvementQueue.add(ImprovementQueueEntry(improvementInProgress, turnsToImprovement))
}
}
//endregion //endregion
} }

View File

@ -195,7 +195,7 @@ class TileImprovementFunctions(val tile: Tile) {
} }
fun changeImprovement(improvementName: String?, fun setImprovement(improvementName: String?,
/** For road assignment and taking over tiles - DO NOT pass when simulating improvement effects! */ /** For road assignment and taking over tiles - DO NOT pass when simulating improvement effects! */
civToActivateBroaderEffects: Civilization? = null, unit: MapUnit? = null) { civToActivateBroaderEffects: Civilization? = null, unit: MapUnit? = null) {
val improvementObject = tile.ruleset.tileImprovements[improvementName] val improvementObject = tile.ruleset.tileImprovements[improvementName]
@ -307,8 +307,7 @@ class TileImprovementFunctions(val tile: Tile) {
private fun tryProvideProductionToClosestCity(removedTerrainFeature: String, civ: Civilization) { private fun tryProvideProductionToClosestCity(removedTerrainFeature: String, civ: Civilization) {
val closestCity = civ.cities.minByOrNull { it.getCenterTile().aerialDistanceTo(tile) } val closestCity = civ.cities.minByOrNull { it.getCenterTile().aerialDistanceTo(tile) }
@Suppress("FoldInitializerAndIfToElvis") ?: return
if (closestCity == null) return
val distance = closestCity.getCenterTile().aerialDistanceTo(tile) val distance = closestCity.getCenterTile().aerialDistanceTo(tile)
var productionPointsToAdd = if (distance == 1) 20 else 20 - (distance - 2) * 5 var productionPointsToAdd = if (distance == 1) 20 else 20 - (distance - 2) * 5
if (tile.owningCity == null || tile.owningCity!!.civ != civ) productionPointsToAdd = if (tile.owningCity == null || tile.owningCity!!.civ != civ) productionPointsToAdd =
@ -372,8 +371,8 @@ class TileImprovementFunctions(val tile: Tile) {
/** Marks tile as target tile for a building with a [UniqueType.CreatesOneImprovement] unique */ /** Marks tile as target tile for a building with a [UniqueType.CreatesOneImprovement] unique */
fun markForCreatesOneImprovement(improvement: String) { fun markForCreatesOneImprovement(improvement: String) {
tile.improvementInProgress = improvement tile.stopWorkingOnImprovement()
tile.turnsToImprovement = -1 tile.queueImprovement(improvement, -1)
} }
/** Un-Marks a tile as target tile for a building with a [UniqueType.CreatesOneImprovement] unique, /** Un-Marks a tile as target tile for a building with a [UniqueType.CreatesOneImprovement] unique,
@ -383,6 +382,4 @@ class TileImprovementFunctions(val tile: Tile) {
tile.owningCity?.cityConstructions?.removeCreateOneImprovementConstruction(tile.improvementInProgress!!) tile.owningCity?.cityConstructions?.removeCreateOneImprovementConstruction(tile.improvementInProgress!!)
tile.stopWorkingOnImprovement() tile.stopWorkingOnImprovement()
} }
} }

View File

@ -55,7 +55,6 @@ object TileNormalizer {
private fun Tile.clearImprovement() { private fun Tile.clearImprovement() {
// This runs from mapgen, so don't go through the side-effect-triggering TileImprovementFunctions // This runs from mapgen, so don't go through the side-effect-triggering TileImprovementFunctions
improvement = null improvement = null
improvementInProgress = null stopWorkingOnImprovement()
turnsToImprovement = 0
} }
} }

View File

@ -48,6 +48,7 @@ private class RestorableTextButtonStyle(
val restoreStyle: ButtonStyle val restoreStyle: ButtonStyle
) : TextButtonStyle(baseStyle) ) : TextButtonStyle(baseStyle)
//todo ButtonStyle *does* have a `disabled` Drawable, and Button ignores touches in disabled state anyway - all this is a wrong approach
/** Disable a [Button] by setting its [touchable][Button.touchable] and [style][Button.style] properties. */ /** Disable a [Button] by setting its [touchable][Button.touchable] and [style][Button.style] properties. */
fun Button.disable() { fun Button.disable() {
touchable = Touchable.disabled touchable = Touchable.disabled

View File

@ -28,14 +28,14 @@ internal class ConsoleTileCommands: ConsoleCommandNode {
val selectedTile = console.getSelectedTile() val selectedTile = console.getSelectedTile()
val improvement = params[0].find(console.gameInfo.ruleset.tileImprovements.values) val improvement = params[0].find(console.gameInfo.ruleset.tileImprovements.values)
val civ = params.getOrNull(1)?.let { console.getCivByName(it) } val civ = params.getOrNull(1)?.let { console.getCivByName(it) }
selectedTile.improvementFunctions.changeImprovement(improvement.name, civ) selectedTile.improvementFunctions.setImprovement(improvement.name, civ)
selectedTile.getCity()?.reassignPopulation() selectedTile.getCity()?.reassignPopulation()
DevConsoleResponse.OK DevConsoleResponse.OK
}, },
"removeimprovement" to ConsoleAction("tile removeimprovement") { console, _ -> "removeimprovement" to ConsoleAction("tile removeimprovement") { console, _ ->
val selectedTile = console.getSelectedTile() val selectedTile = console.getSelectedTile()
selectedTile.improvementFunctions.changeImprovement(null) selectedTile.improvementFunctions.setImprovement(null)
selectedTile.getCity()?.reassignPopulation() selectedTile.getCity()?.reassignPopulation()
DevConsoleResponse.OK DevConsoleResponse.OK
}, },

View File

@ -1,6 +1,7 @@
package com.unciv.ui.screens.pickerscreens package com.unciv.ui.screens.pickerscreens
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Actor
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.Constants import com.unciv.Constants
@ -12,6 +13,7 @@ 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.stats.Stats import com.unciv.models.stats.Stats
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.components.SmallButtonStyle
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
import com.unciv.ui.components.extensions.disable import com.unciv.ui.components.extensions.disable
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
@ -39,22 +41,26 @@ class ImprovementPickerScreen(
private var selectedImprovement: TileImprovement? = null private var selectedImprovement: TileImprovement? = null
private val gameInfo = tile.tileMap.gameInfo private val gameInfo = tile.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 // Support for UniqueType.CreatesOneImprovement
private val tileMarkedForCreatesOneImprovement = tile.isMarkedForCreatesOneImprovement() private val tileMarkedForCreatesOneImprovement = tile.isMarkedForCreatesOneImprovement()
private val tileWithoutLastTerrain: Tile
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?, secondImprovement: TileImprovement? = null) {
if (improvement == null || tileMarkedForCreatesOneImprovement) return if (improvement == null || tileMarkedForCreatesOneImprovement) return
if (improvement.name == Constants.cancelImprovementOrder) { if (improvement.name == Constants.cancelImprovementOrder) {
tile.stopWorkingOnImprovement() tile.stopWorkingOnImprovement()
// no onAccept() - Worker can stay selected // no onAccept() - Worker can stay selected
} else { } else {
if (improvement.name != tile.improvementInProgress) if (improvement.name != tile.improvementInProgress) {
tile.startWorkingOnImprovement(improvement, currentPlayerCiv, unit) tile.startWorkingOnImprovement(improvement, currentPlayerCiv, unit)
if (secondImprovement != null)
tile.queueImprovement(secondImprovement, currentPlayerCiv, unit)
}
unit.action = null // this is to "wake up" the worker if it's sleeping unit.action = null // this is to "wake up" the worker if it's sleeping
onAccept() onAccept()
} }
@ -74,28 +80,20 @@ class ImprovementPickerScreen(
// clone tileInfo without "top" feature if it could be removed // clone tileInfo without "top" feature if it could be removed
// Keep this copy around for speed // Keep this copy around for speed
val tileWithoutLastTerrain: Tile = tile.clone() tileWithoutLastTerrain = tile.clone()
tileWithoutLastTerrain.setTerrainTransients() tileWithoutLastTerrain.setTerrainTransients()
if (Constants.remove + tileWithoutLastTerrain.lastTerrain.name in ruleSet.tileImprovements) { if (Constants.remove + tileWithoutLastTerrain.lastTerrain.name in ruleset.tileImprovements) {
tileWithoutLastTerrain.removeTerrainFeature(tileWithoutLastTerrain.lastTerrain.name) tileWithoutLastTerrain.removeTerrainFeature(tileWithoutLastTerrain.lastTerrain.name)
} }
val cityUniqueCache = LocalUniqueCache() val cityUniqueCache = LocalUniqueCache()
for (improvement in ruleSet.tileImprovements.values) { for (improvement in ruleset.tileImprovements.values) {
var suggestRemoval = false
// canBuildImprovement() would allow e.g. great improvements thus we need to exclude them - except cancel // canBuildImprovement() would allow e.g. great improvements thus we need to exclude them - except cancel
if (improvement.turnsToBuild == -1 && improvement.name != Constants.cancelImprovementOrder) continue if (improvement.turnsToBuild == -1 && improvement.name != Constants.cancelImprovementOrder) continue
if (improvement.name == tile.improvement) continue // also checked by canImprovementBeBuiltHere, but after more expensive tests if (improvement.name == tile.improvement) continue // also checked by canImprovementBeBuiltHere, but after more expensive tests
if (!unit.canBuildImprovement(improvement)) continue if (!unit.canBuildImprovement(improvement)) continue
val problemReport = getProblemReport(improvement) ?: continue
var unbuildableBecause = tile.improvementFunctions.getImprovementBuildingProblems(improvement, currentPlayerCiv).toSet()
if (!canReport(unbuildableBecause)) {
// Try after pretending to have removed the top terrain layer.
unbuildableBecause = tileWithoutLastTerrain.improvementFunctions.getImprovementBuildingProblems(improvement, currentPlayerCiv).toSet()
if (!canReport(unbuildableBecause)) continue
else suggestRemoval = true
}
val image = ImageGetter.getImprovementPortrait(improvement.name, 30f) val image = ImageGetter.getImprovementPortrait(improvement.name, 30f)
@ -103,7 +101,7 @@ class ImprovementPickerScreen(
var shortcutKey = improvement.shortcutKey var shortcutKey = improvement.shortcutKey
if (shortcutKey != null) { if (shortcutKey != null) {
val techLevel = getRequiredTechColumn(improvement) val techLevel = getRequiredTechColumn(improvement)
val isSuperseded = ruleSet.tileImprovements.values.asSequence() val isSuperseded = ruleset.tileImprovements.values.asSequence()
// *other* improvements with same shortcutKey // *other* improvements with same shortcutKey
.filter { it.shortcutKey == improvement.shortcutKey && it != improvement } .filter { it.shortcutKey == improvement.shortcutKey && it != improvement }
// civ can build it (checks tech researched) // civ can build it (checks tech researched)
@ -127,29 +125,6 @@ class ImprovementPickerScreen(
&& improvement.name != Constants.cancelImprovementOrder) && improvement.name != Constants.cancelImprovementOrder)
if (tile.improvement != null && removeImprovement) labelText += "\n" + "Replaces [${tile.improvement}]".tr() if (tile.improvement != null && removeImprovement) labelText += "\n" + "Replaces [${tile.improvement}]".tr()
val proposedSolutions = mutableListOf<String>()
if (suggestRemoval)
proposedSolutions.add("${Constants.remove}[${tile.lastTerrain.name}] first")
if (ImprovementBuildingProblem.MissingTech in unbuildableBecause)
proposedSolutions.add("Research [${improvement.techRequired}] first")
if (ImprovementBuildingProblem.NotJustOutsideBorders in unbuildableBecause)
proposedSolutions.add("Have this tile close to your borders")
if (ImprovementBuildingProblem.OutsideBorders in unbuildableBecause)
proposedSolutions.add("Have this tile inside your empire")
if (ImprovementBuildingProblem.MissingResources in unbuildableBecause) {
proposedSolutions.addAll(improvement.getMatchingUniques(UniqueType.ConsumesResources).filter {
currentPlayerCiv.getResourceAmount(it.params[1]) < it.params[0].toInt()
}.map { "Acquire more [$it]" })
}
val explanationText = when {
proposedSolutions.any() -> proposedSolutions.joinToString("}\n{", "{", "}").toLabel()
tile.improvementInProgress == improvement.name -> "Current construction".toLabel()
tileMarkedForCreatesOneImprovement -> null
else -> "Pick now!".toLabel().onClick { accept(improvement) }
}
val statIcons = getStatIconsTable(provideResource, removeImprovement) val statIcons = getStatIconsTable(provideResource, removeImprovement)
// get benefits of the new improvement // get benefits of the new improvement
@ -182,13 +157,20 @@ class ImprovementPickerScreen(
improvementButton.onActivation(type = ActivationTypes.Tap, noEquivalence = true) { improvementButton.onActivation(type = ActivationTypes.Tap, noEquivalence = true) {
selectedImprovement = improvement selectedImprovement = improvement
pick(improvement.name.tr()) pick(improvement.name.tr())
descriptionLabel.setText(improvement.getDescription(ruleSet)) descriptionLabel.setText(improvement.getDescription(ruleset))
} }
improvementButton.onDoubleClick { accept(improvement) } improvementButton.onDoubleClick { accept(improvement) }
if (improvement.name == tile.improvementInProgress) improvementButton.color = Color.GREEN when {
if (proposedSolutions.isNotEmpty() || tileMarkedForCreatesOneImprovement) { improvement.name == tile.improvementInProgress ->
improvementButton.color = Color.GREEN
problemReport.isQueueable() ->
// TODO should be a skin ButtonStyle, this mixes with the style override from disable() below - which is a very wrong approach anyway
improvementButton.setColor(0.625f, 1f, 0.625f, 1f) // #a0ffa0 - brightened GREEN
}
if (!problemReport.isEmpty() || tileMarkedForCreatesOneImprovement) {
improvementButton.disable() improvementButton.disable()
} else if (shortcutKey != null) { } else if (shortcutKey != null) {
// Shortcut keys trigger what onDoubleClick does, not equivalent to single Click: // Shortcut keys trigger what onDoubleClick does, not equivalent to single Click:
@ -197,7 +179,7 @@ class ImprovementPickerScreen(
} }
regularImprovements.add(improvementButton) regularImprovements.add(improvementButton)
regularImprovements.add(explanationText).padLeft(10f).fillY() regularImprovements.add(getExplanationActor(improvement, problemReport)).padLeft(10f)
regularImprovements.row() regularImprovements.row()
} }
@ -251,4 +233,66 @@ class ImprovementPickerScreen(
} }
return statsTable return statsTable
} }
private class ProblemReport {
var suggestRemoval = false
var removalImprovement: TileImprovement? = null
val proposedSolutions = mutableListOf<String>()
fun isEmpty() = proposedSolutions.isEmpty()
fun isQueueable() = removalImprovement != null && proposedSolutions.size == 1
fun toLabel() = proposedSolutions.joinToString("}\n{", "{", "}").toLabel()
}
private fun getProblemReport(improvement: TileImprovement): ProblemReport? {
val report = ProblemReport()
var unbuildableBecause = tile.improvementFunctions.getImprovementBuildingProblems(improvement, currentPlayerCiv).toSet()
if (!canReport(unbuildableBecause)) {
// Try after pretending to have removed the top terrain layer.
unbuildableBecause = tileWithoutLastTerrain.improvementFunctions.getImprovementBuildingProblems(improvement, currentPlayerCiv).toSet()
if (!canReport(unbuildableBecause)) return null
report.suggestRemoval = true
}
with(report) {
if (suggestRemoval) {
val removalName = Constants.remove + tile.lastTerrain.name
removalImprovement = ruleset.tileImprovements[removalName]
if (removalImprovement != null)
proposedSolutions.add("${Constants.remove}[${tile.lastTerrain.name}] first")
}
if (ImprovementBuildingProblem.MissingTech in unbuildableBecause)
proposedSolutions.add("Research [${improvement.techRequired}] first")
if (ImprovementBuildingProblem.NotJustOutsideBorders in unbuildableBecause)
proposedSolutions.add("Have this tile close to your borders")
if (ImprovementBuildingProblem.OutsideBorders in unbuildableBecause)
proposedSolutions.add("Have this tile inside your empire")
if (ImprovementBuildingProblem.MissingResources in unbuildableBecause) {
proposedSolutions.addAll(improvement.getMatchingUniques(UniqueType.ConsumesResources).filter {
currentPlayerCiv.getResourceAmount(it.params[1]) < it.params[0].toInt()
}.map { "Acquire more [$it]" })
}
}
return report
}
private fun getExplanationActor(improvement: TileImprovement, report: ProblemReport): Actor? {
fun getPickNowButton(action: ()->Unit) = "Pick now!".toTextButton(SmallButtonStyle()).onClick(action)
// Formerly: "Pick now!".toLabel().onClick(action), with later fillY() to make it easier to hit (#3989)
if (report.isEmpty()) return when {
tile.improvementInProgress == improvement.name -> "Current construction".toLabel()
tileMarkedForCreatesOneImprovement -> null
else -> getPickNowButton { accept(improvement) }
}
val label = report.toLabel()
if (!report.isQueueable()) return label
return Table().apply {
defaults().center()
add(label).padBottom(5f).row()
add(getPickNowButton { accept(report.removalImprovement, improvement) })
}
}
} }

View File

@ -456,8 +456,7 @@ object UnitActionsFromUniques {
return UnitAction(UnitActionType.Repair, 90f, return UnitAction(UnitActionType.Repair, 90f,
title = "${UnitActionType.Repair} [${unit.currentTile.getImprovementToRepair()!!.name}] - [${turnsToBuild}${Fonts.turn}]", title = "${UnitActionType.Repair} [${unit.currentTile.getImprovementToRepair()!!.name}] - [${turnsToBuild}${Fonts.turn}]",
action = { action = {
tile.turnsToImprovement = getRepairTurns(unit) tile.queueImprovement(Constants.repair, turnsToBuild)
tile.improvementInProgress = Constants.repair
}.takeIf { couldConstruct } }.takeIf { couldConstruct }
) )
} }

View File

@ -177,7 +177,7 @@ class TileImprovementConstructionTests {
@Test @Test
fun buildingRoadBuildsARoad() { fun buildingRoadBuildsARoad() {
val tile = tileMap[1,1] val tile = tileMap[1,1]
tile.improvementFunctions.changeImprovement("Road") tile.improvementFunctions.setImprovement("Road")
assert(tile.roadStatus == RoadStatus.Road) assert(tile.roadStatus == RoadStatus.Road)
} }
@ -185,7 +185,7 @@ class TileImprovementConstructionTests {
fun removingRoadRemovesRoad() { fun removingRoadRemovesRoad() {
val tile = tileMap[1,1] val tile = tileMap[1,1]
tile.roadStatus = RoadStatus.Road tile.roadStatus = RoadStatus.Road
tile.improvementFunctions.changeImprovement("Remove Road") tile.improvementFunctions.setImprovement("Remove Road")
assert(tile.roadStatus == RoadStatus.None) assert(tile.roadStatus == RoadStatus.None)
} }
@ -193,9 +193,9 @@ class TileImprovementConstructionTests {
fun removingForestRemovesForestAndLumbermill() { fun removingForestRemovesForestAndLumbermill() {
val tile = tileMap[1,1] val tile = tileMap[1,1]
tile.addTerrainFeature("Forest") tile.addTerrainFeature("Forest")
tile.improvementFunctions.changeImprovement("Lumber mill") tile.improvementFunctions.setImprovement("Lumber mill")
assert(tile.getTileImprovement()!!.name == "Lumber mill") assert(tile.getTileImprovement()!!.name == "Lumber mill")
tile.improvementFunctions.changeImprovement("Remove Forest") tile.improvementFunctions.setImprovement("Remove Forest")
assert(tile.terrainFeatures.isEmpty()) assert(tile.terrainFeatures.isEmpty())
assert(tile.improvement == null) // Lumber mill can ONLY be on Forest, and is therefore removed assert(tile.improvement == null) // Lumber mill can ONLY be on Forest, and is therefore removed
} }
@ -206,9 +206,9 @@ class TileImprovementConstructionTests {
tile.addTerrainFeature("Forest") tile.addTerrainFeature("Forest")
tile.resource = "Deer" tile.resource = "Deer"
tile.baseTerrain = "Plains" tile.baseTerrain = "Plains"
tile.improvementFunctions.changeImprovement("Camp") tile.improvementFunctions.setImprovement("Camp")
assert(tile.getTileImprovement()!!.name == "Camp") assert(tile.getTileImprovement()!!.name == "Camp")
tile.improvementFunctions.changeImprovement("Remove Forest") tile.improvementFunctions.setImprovement("Remove Forest")
assert(tile.terrainFeatures.isEmpty()) assert(tile.terrainFeatures.isEmpty())
assert(tile.improvement == "Camp") // Camp can be both on Forest AND on Plains, so not removed assert(tile.improvement == "Camp") // Camp can be both on Forest AND on Plains, so not removed
} }
@ -253,7 +253,7 @@ class TileImprovementConstructionTests {
tile.addTerrainFeature("Forest") tile.addTerrainFeature("Forest")
val lumberMill = testGame.ruleset.tileImprovements["Lumber mill"]!! val lumberMill = testGame.ruleset.tileImprovements["Lumber mill"]!!
tile.improvementFunctions.changeImprovement(lumberMill.name) tile.improvementFunctions.setImprovement(lumberMill.name)
assert(tile.getTileImprovement() == lumberMill) assert(tile.getTileImprovement() == lumberMill)
// 1f 1p from forest, 2p from lumber mill since all techs are researched // 1f 1p from forest, 2p from lumber mill since all techs are researched