City construction queue (#1479)

* Construction Queue

* Added constructionQueue in addition to currentConstruction: if currentConstruction is done, next construction from Queue is started; if Queue is empty invoke ConstructionAutomation
* Queue utility methods: add, remove, higher prio, lower prio
* Icons to move constructions in queue
* Top left city stats moved to top right panel
* Added current construction and queue construction to top left
* Extended selected construction (containing description) moved to bottom right, it is now displayed alternatively to selected tile

Rework

* Max queue size, cannot change queue in puppet city or in other player turn

* Queue and current construction reset on puppeting city

Co-authored-by: Yair Morgenstern <yairm210@hotmail.com>
This commit is contained in:
r3versi 2020-01-09 19:58:15 +01:00 committed by Yair Morgenstern
parent 977fcfb97e
commit 12a98aa4bb
13 changed files with 584 additions and 273 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 B

View file

@ -292,6 +292,13 @@ Requires a [buildingName] in this city =
Requires [resource] =
Required tech: [requiredTech] =
Current construction =
Construction queue =
Pick a construction =
Queue empty =
Add to queue =
Remove from queue =
Diplomacy =
War =
Peace =

View file

@ -11,17 +11,25 @@ import com.unciv.models.translations.tr
import com.unciv.ui.utils.withItem
import com.unciv.ui.utils.withoutItem
import java.util.*
import kotlin.collections.ArrayList
/**
* City constructions manager.
*
* @property cityInfo the city it refers to
* @property currentConstruction the name of the construction is currently worked, default = "Monument"
* @property currentConstructionIsUserSet a flag indicating if the [currentConstruction] has been set by the user or by the AI
* @property constructionQueue a list of constructions names enqueued
*/
class CityConstructions {
@Transient lateinit var cityInfo: CityInfo
@Transient private var builtBuildingObjects=ArrayList<Building>()
@Transient private var builtBuildingObjects = ArrayList<Building>()
var builtBuildings = HashSet<String>()
val inProgressConstructions = HashMap<String, Int>()
var currentConstruction: String = "Monument" // default starting building!
var currentConstruction: String = "Monument"
var currentConstructionIsUserSet = false
var constructionQueue = mutableListOf<String>()
val queueMaxSize = 10
//region pure functions
fun clone(): CityConstructions {
@ -30,6 +38,7 @@ class CityConstructions {
toReturn.inProgressConstructions.putAll(inProgressConstructions)
toReturn.currentConstruction=currentConstruction
toReturn.currentConstructionIsUserSet=currentConstructionIsUserSet
toReturn.constructionQueue.addAll(constructionQueue)
return toReturn
}
@ -39,6 +48,9 @@ class CityConstructions {
fun getConstructableUnits() = cityInfo.getRuleset().units.values
.asSequence().filter { it.isBuildable(this) }
/**
* @return [Stats] provided by all built buildings in city plus the bonus from Library
*/
fun getStats(): Stats {
val stats = Stats()
for (building in getBuiltBuildings())
@ -47,8 +59,14 @@ class CityConstructions {
return stats
}
/**
* @return Maintenance cost of all built buildings
*/
fun getMaintenanceCosts(): Int = getBuiltBuildings().sumBy { it.maintenance }
/**
* @return Bonus (%) [Stats] provided by all built buildings in city
*/
fun getStatPercentBonuses(): Stats {
val stats = Stats()
for (building in getBuiltBuildings())
@ -69,7 +87,9 @@ class CityConstructions {
}
fun getProductionForTileInfo(): String {
val currentConstructionSnapshot = currentConstruction // this is because there were rare errors tht I assume were caused because currentContruction changed on another thread
/* this is because there were rare errors tht I assume were caused because
currentContruction changed on another thread */
val currentConstructionSnapshot = currentConstruction
var result = currentConstructionSnapshot.tr()
if (currentConstructionSnapshot!=""
&& SpecialConstruction.getSpecialConstructions().none { it.name==currentConstructionSnapshot }) {
@ -80,17 +100,17 @@ class CityConstructions {
}
fun getCurrentConstruction(): IConstruction = getConstruction(currentConstruction)
fun getIConstructionQueue(): List<IConstruction> = constructionQueue.map{ getConstruction(it) }
fun isBuilt(buildingName: String): Boolean = builtBuildings.contains(buildingName)
fun isBeingConstructed(constructionName: String): Boolean = currentConstruction == constructionName
fun isEnqueued(constructionName: String): Boolean = constructionQueue.contains(constructionName)
fun isQueueFull(): Boolean = constructionQueue.size == queueMaxSize
fun isBuildingWonder(): Boolean {
val currentConstruction = getCurrentConstruction()
if (currentConstruction is Building) {
return currentConstruction.isWonder
}
return false
return currentConstruction is Building && currentConstruction.isWonder
}
internal fun getConstruction(constructionName: String): IConstruction {
@ -179,6 +199,7 @@ class CityConstructions {
fun endTurn(cityStats: Stats) {
stopUnbuildableConstruction()
validateConstructionQueue()
if(getConstruction(currentConstruction) !is SpecialConstruction)
addProductionPoints(Math.round(cityStats.production))
@ -198,7 +219,17 @@ class CityConstructions {
currentConstruction = saveCurrentConstruction
}
fun constructionComplete(construction: IConstruction) {
private fun validateConstructionQueue() {
val queueSnapshot = mutableListOf<String>().apply { addAll(constructionQueue) }
constructionQueue.clear()
for (construction in queueSnapshot) {
if (getConstruction(construction).isBuildable(this))
constructionQueue.add(construction)
}
}
private fun constructionComplete(construction: IConstruction) {
construction.postBuildEvent(this)
inProgressConstructions.remove(currentConstruction)
@ -252,16 +283,66 @@ class CityConstructions {
cancelCurrentConstruction()
}
fun cancelCurrentConstruction(){
currentConstructionIsUserSet=false
currentConstruction=""
private fun cancelCurrentConstruction() {
currentConstructionIsUserSet = false
currentConstruction = ""
chooseNextConstruction()
}
fun chooseNextConstruction() {
if(currentConstructionIsUserSet) return
if (constructionQueue.isNotEmpty()) {
currentConstructionIsUserSet = true
currentConstruction = constructionQueue.removeAt(0)
stopUnbuildableConstruction()
if (currentConstruction != "") return
}
ConstructionAutomation(this).chooseNextConstruction()
}
//endregion
fun isEnqueuable(constructionName: String): Boolean {
return true
}
fun addToQueue(constructionName: String) {
if (!isQueueFull())
constructionQueue.add(constructionName)
}
fun removeFromQueue(idx: Int) {
// idx -1 is the current construction
if (idx < 0) {
// To prevent Construction Automation
if (constructionQueue.isEmpty()) constructionQueue.add("Nothing")
cancelCurrentConstruction()
} else
constructionQueue.removeAt(idx)
}
fun higherPrio(idx: Int) {
// change current construction
if(idx == 0) {
// Add current construction to queue after the first element
constructionQueue.add(1, currentConstruction)
cancelCurrentConstruction()
}
else
constructionQueue.swap(idx-1, idx)
}
// Lowering == Highering next element in queue
fun lowerPrio(idx: Int) {
higherPrio(idx+1)
}
//endregion
private fun MutableList<String>.swap(idx1: Int, idx2: Int) {
val tmp = this[idx1]
this[idx1] = this[idx2]
this[idx2] = tmp
}
} // for json parsing, we need to have a default constructor

View file

@ -392,7 +392,10 @@ class CityInfo {
isPuppet = true
health = getMaxHealth() / 2 // I think that cities recover to half health when conquered?
cityStats.update()
cityConstructions.chooseNextConstruction() // The city could be producing something that puppets shouldn't, like units
// The city could be producing something that puppets shouldn't, like units
cityConstructions.currentConstructionIsUserSet = false
cityConstructions.constructionQueue.clear()
cityConstructions.chooseNextConstruction()
}
private fun diplomaticRepercussionsForConqueringCity(oldCiv: CivilizationInfo, conqueringCiv: CivilizationInfo) {

View file

@ -223,6 +223,8 @@ class Building : NamedStats(), IConstruction{
fun getRejectionReason(construction: CityConstructions):String{
if (construction.isBuilt(name)) return "Already built"
if (construction.isBeingConstructed(name)) return "Is being built"
if (construction.isEnqueued(name)) return "Already enqueued"
if ("Must be next to desert" in uniques
&& !construction.cityInfo.getCenterTile().getTilesInDistance(1).any { it.baseTerrain == Constants.desert })

View file

@ -6,17 +6,22 @@ import com.badlogic.gdx.scenes.scene2d.ui.Image
import com.badlogic.gdx.scenes.scene2d.ui.Skin
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.utils.Align
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.GreatPersonManager
import com.unciv.models.ruleset.Building
import com.unciv.models.translations.tr
import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats
import com.unciv.ui.utils.*
import com.unciv.ui.worldscreen.optionstable.YesNoPopupTable
import java.text.DecimalFormat
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.round
class CityInfoTable(private val cityScreen: CityScreen) : Table(CameraStageBaseScreen.skin) {
val pad = 5f
@ -29,10 +34,9 @@ class CityInfoTable(private val cityScreen: CityScreen) : Table(CameraStageBaseS
clear()
val cityInfo = cityScreen.city
addCityStats(cityInfo)
addBuildingsInfo(cityInfo)
addStatInfo()
addGreatPersonPointInfo(cityInfo)
pack()
@ -92,6 +96,65 @@ class CityInfoTable(private val cityScreen: CityScreen) : Table(CameraStageBaseS
}
}
private fun addCityStats(cityInfo: CityInfo) {
val statsTable = Table().align(Align.center)
addCategory("Stats", statsTable)
val columns = Stats().toHashMap().size
statsTable.add(Label("{Unassigned population}:".tr()
+" "+cityInfo.population.getFreePopulation().toString() + "/" + cityInfo.population.population, skin))
.pad(5f).row()
val turnsToExpansionString : String
if (cityInfo.cityStats.currentCityStats.culture > 0) {
var turnsToExpansion = ceil((cityInfo.expansion.getCultureToNextTile() - cityInfo.expansion.cultureStored)
/ cityInfo.cityStats.currentCityStats.culture).toInt()
if (turnsToExpansion < 1) turnsToExpansion = 1
turnsToExpansionString = "[$turnsToExpansion] turns to expansion".tr()
} else {
turnsToExpansionString = "Stopped expansion".tr()
}
statsTable.add(Label(turnsToExpansionString + " (" + cityInfo.expansion.cultureStored + "/" + cityInfo.expansion.getCultureToNextTile() + ")",
skin)).pad(5f).row()
val turnsToPopString : String
if (cityInfo.cityStats.currentCityStats.food > 0) {
if (cityInfo.cityConstructions.currentConstruction == Constants.settler) {
turnsToPopString = "Food converts to production".tr()
} else {
var turnsToPopulation = ceil((cityInfo.population.getFoodToNextPopulation()-cityInfo.population.foodStored)
/ cityInfo.cityStats.currentCityStats.food).toInt()
if (turnsToPopulation < 1) turnsToPopulation = 1
turnsToPopString = "[$turnsToPopulation] turns to new population".tr()
}
} else if (cityInfo.cityStats.currentCityStats.food < 0) {
val turnsToStarvation = floor(cityInfo.population.foodStored / -cityInfo.cityStats.currentCityStats.food).toInt() + 1
turnsToPopString = "[$turnsToStarvation] turns to lose population".tr()
} else {
turnsToPopString = "Stopped population growth".tr()
}
statsTable.add(Label(turnsToPopString + " (" + cityInfo.population.foodStored + "/" + cityInfo.population.getFoodToNextPopulation() + ")"
,skin)).pad(5f).row()
if (cityInfo.resistanceCounter > 0) {
statsTable.add(Label("In resistance for another [${cityInfo.resistanceCounter}] turns".tr(),skin)).pad(5f).row()
}
statsTable.addSeparator()
val ministatsTable = Table().pad(5f)
ministatsTable.defaults()
statsTable.add(ministatsTable)
for(stat in cityInfo.cityStats.currentCityStats.toHashMap()) {
if(stat.key == Stat.Happiness) continue
ministatsTable.add(ImageGetter.getStatIcon(stat.key.name)).size(20f).padRight(3f)
ministatsTable.add(round(stat.value).toInt().toString().toLabel()).padRight(13f)
}
}
private fun addBuildingsInfo(cityInfo: CityInfo) {
val wonders = mutableListOf<Building>()
val specialistBuildings = mutableListOf<Building>()

View file

@ -5,14 +5,12 @@ import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.utils.Align
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.HexMath
import com.unciv.logic.city.CityInfo
import com.unciv.logic.city.IConstruction
import com.unciv.logic.map.TileInfo
import com.unciv.models.Tutorial
import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats
import com.unciv.models.translations.tr
import com.unciv.ui.map.TileGroupMap
import com.unciv.ui.tilegroups.TileSetStrings
@ -21,51 +19,52 @@ import java.util.*
import kotlin.math.ceil
import kotlin.math.round
class CityScreen(internal val city: CityInfo) : CameraStageBaseScreen() {
private var selectedTile: TileInfo? = null
class CityScreen(internal val city: CityInfo): CameraStageBaseScreen() {
var selectedTile: TileInfo? = null
var selectedConstruction: IConstruction? = null
// Clockwise from the top-left
/** Displays city stats - sits on the top left side */
var topCityStatsTable=Table()
private var razeCityButtonHolder = Table() // sits on the top
/** Displays buildings, specialists and stats drilldown - sits on the top right of the city screen */
private var cityInfoTable = CityInfoTable(this)
/** Displays tile info, sits on the bottom right */
private var tileTable = CityScreenTileTable(city)
/** Displays city name, allows switching between cities - sits on the bottom */
private var cityPickerTable = CityScreenCityPickerTable(this)
/** Holds production list and current production - sits on the bottom left */
/** Displays current production, production queue and available productions list - sits on LEFT */
private var constructionsTable = ConstructionsTable(this)
/** Displays raze city button - sits on TOP CENTER */
private var razeCityButtonHolder = Table()
/** Displays stats, buildings, specialists and stats drilldown - sits on TOP RIGHT */
private var cityInfoTable = CityInfoTable(this)
/** Displays tile info, alternate with selectedConstructionTable - sits on BOTTOM RIGHT */
private var tileTable = CityScreenTileTable(city)
/** Displays selected construction info, alternate with tileTable - sits on BOTTOM RIGHT */
private var selectedConstructionTable = ConstructionInfoTable(this.city)
/** Displays city name, allows switching between cities - sits on BOTTOM CENTER */
private var cityPickerTable = CityScreenCityPickerTable(this)
/** Holds City tiles group*/
private var tileGroups = ArrayList<CityTileGroup>()
init {
onBackButtonClicked { game.setWorldScreen() }
addTiles()
UncivGame.Current.settings.addCompletedTutorialTask("Enter city screen")
val tableBackgroundColor = ImageGetter.getBlue().lerp(Color.BLACK,0.5f)
addTiles()
var buildingsTableContainer = Table()
buildingsTableContainer.pad(3f)
buildingsTableContainer.background = ImageGetter.getBackground(tableBackgroundColor)
buildingsTableContainer.background = ImageGetter.getBackground(ImageGetter.getBlue().lerp(Color.BLACK,0.5f))
cityInfoTable.update()
val buildingsScroll = ScrollPane(cityInfoTable)
buildingsTableContainer.add(buildingsScroll)
.size(stage.width/4,stage.height / 2)
buildingsTableContainer.add(buildingsScroll).size(stage.width/4,stage.height / 2)
buildingsTableContainer = buildingsTableContainer.addBorder(2f, Color.WHITE)
buildingsTableContainer.setPosition(stage.width - buildingsTableContainer.width-5,
stage.height - buildingsTableContainer.height-5)
buildingsTableContainer.setPosition( stage.width - 5f, stage.height - 5f, Align.topRight)
//stage.setDebugTableUnderMouse(true)
stage.addActor(constructionsTable)
stage.addActor(tileTable)
stage.addActor(selectedConstructionTable)
stage.addActor(cityPickerTable)
stage.addActor(buildingsTableContainer)
@ -73,87 +72,38 @@ class CityScreen(internal val city: CityInfo) : CameraStageBaseScreen() {
}
internal fun update() {
cityInfoTable.update()
city.cityStats.update()
constructionsTable.update(selectedConstruction)
constructionsTable.setPosition(5f, stage.height - 5f, Align.topLeft)
cityPickerTable.update()
cityPickerTable.centerX(stage)
constructionsTable.update()
updateAnnexAndRazeCityButton()
tileTable.update(selectedTile)
tileTable.setPosition(stage.width-5, 5f,Align.bottomRight)
tileTable.setPosition(stage.width - 5f, 5f, Align.bottomRight)
selectedConstructionTable.update(selectedConstruction)
selectedConstructionTable.setPosition(stage.width - 5f, 5f, Align.bottomRight)
cityInfoTable.update()
updateAnnexAndRazeCityButton()
updateTileGroups()
topCityStatsTable.remove()
topCityStatsTable = getCityStatsTable()
topCityStatsTable.setPosition(5f, stage.height-5, Align.topLeft)
stage.addActor(topCityStatsTable)
constructionsTable.height=stage.height-topCityStatsTable.height
constructionsTable.setPosition(5f, stage.height-5-topCityStatsTable.height, Align.topLeft)
if (city.getCenterTile().getTilesAtDistance(4).isNotEmpty()){
if (city.getCenterTile().getTilesAtDistance(4).isNotEmpty())
displayTutorial(Tutorial.CityRange)
}
}
private fun updateTileGroups() {
val nextTile = city.expansion.chooseNewTileToOwn()
for (tileGroup in tileGroups) {
tileGroup.update()
if(tileGroup.tileInfo == nextTile){
tileGroup.showCircle(Color.PURPLE)
tileGroup.setColor(0f,0f,0f,0.7f)
}
}
}
fun getCityStatsTable(): Table {
val table=Table().pad(10f)
table.defaults().pad(5f)
table.background=ImageGetter.getBackground(Color.BLACK.cpy().apply { a=0.8f })
val columns = Stats().toHashMap().size
val unassignedPopText = "{Unassigned population}:".tr()+
city.population.getFreePopulation().toString() + "/" + city.population.population
table.add(unassignedPopText.toLabel()).colspan(columns).row()
var turnsToExpansionString = when {
city.cityStats.currentCityStats.culture > 0 -> {
var turnsToExpansion = ceil((city.expansion.getCultureToNextTile() - city.expansion.cultureStored)
/ city.cityStats.currentCityStats.culture).toInt()
if (turnsToExpansion < 1) turnsToExpansion = 1
"[$turnsToExpansion] turns to expansion".tr()
}
else -> "Stopped expansion".tr()
}
turnsToExpansionString += " (" + city.expansion.cultureStored + "/" + city.expansion.getCultureToNextTile() + ")"
table.add(turnsToExpansionString.toLabel()).colspan(columns).row()
var turnsToPopString = when {
city.isGrowing() -> "[${city.getNumTurnsToNewPopulation()}] turns to new population"
city.isStarving() -> "[${city.getNumTurnsToStarvation()}] turns to lose population"
city.cityConstructions.currentConstruction == Constants.settler -> "Food converts to production"
else -> "Stopped population growth"
}.tr()
turnsToPopString += " (" + city.population.foodStored + "/" + city.population.getFoodToNextPopulation() + ")"
table.add(turnsToPopString.toLabel()).colspan(columns).row()
if (city.isInResistance()) {
table.add("In resistance for another [${city.resistanceCounter}] turns".toLabel()).colspan(columns).row()
}
table.addSeparator()
val beige = colorFromRGB(194,180,131)
for(stat in city.cityStats.currentCityStats.toHashMap()) {
if(stat.key==Stat.Happiness) continue
val minitable=Table().padRight(5f).padLeft(5f)
minitable.add(ImageGetter.getStatIcon(stat.key.name)).size(20f).padRight(3f)
minitable.add(round(stat.value).toInt().toString().toLabel())
table.add(minitable)
}
return table.addBorder(2f, beige)
}
private fun updateAnnexAndRazeCityButton() {
@ -167,15 +117,13 @@ class CityScreen(internal val city: CityInfo) : CameraStageBaseScreen() {
update()
}
razeCityButtonHolder.add(annexCityButton).colspan(cityPickerTable.columns)
}
else if(!city.isBeingRazed) {
} else if(!city.isBeingRazed) {
val razeCityButton = TextButton("Raze city".tr(), skin)
razeCityButton.labelCell.pad(10f)
razeCityButton.onClick { city.isBeingRazed=true; update() }
if(!UncivGame.Current.worldScreen.isPlayersTurn) razeCityButton.disable()
razeCityButtonHolder.add(razeCityButton).colspan(cityPickerTable.columns)
}
else {
} else {
val stopRazingCityButton = TextButton("Stop razing city".tr(), skin)
stopRazingCityButton.labelCell.pad(10f)
stopRazingCityButton.onClick { city.isBeingRazed=false; update() }
@ -203,6 +151,7 @@ class CityScreen(internal val city: CityInfo) : CameraStageBaseScreen() {
tileGroup.onClick {
if (!city.isPuppet) {
selectedTile = tileInfo
selectedConstruction = null
if (tileGroup.isWorkable && UncivGame.Current.worldScreen.isPlayersTurn) {
if (!tileInfo.isWorked() && city.population.getFreePopulation() > 0) {
city.workedTiles.add(tileInfo.position)
@ -235,5 +184,4 @@ class CityScreen(internal val city: CityInfo) : CameraStageBaseScreen() {
scrollPane.scrollPercentY=0.5f
scrollPane.updateVisualScroll()
}
}

View file

@ -0,0 +1,78 @@
package com.unciv.ui.cityscreen
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
import com.unciv.logic.city.CityInfo
import com.unciv.logic.city.IConstruction
import com.unciv.logic.city.SpecialConstruction
import com.unciv.ui.utils.ImageGetter
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.translations.tr
import com.unciv.ui.utils.*
class ConstructionInfoTable(val city: CityInfo): Table() {
val selectedConstructionTable = Table()
init{
selectedConstructionTable.background = ImageGetter.getBackground(ImageGetter.getBlue().lerp(Color.BLACK, 0.5f))
add(selectedConstructionTable).pad(2f).fill()
background = ImageGetter.getBackground(Color.WHITE)
}
fun update(selectedConstruction: IConstruction?) {
selectedConstructionTable.clear()
selectedConstructionTable.pad(20f)
if (selectedConstruction == null) {
isVisible = false
return
}
isVisible = true
addSelectedConstructionTable(selectedConstruction)
pack()
}
private fun addSelectedConstructionTable(construction: IConstruction) {
val cityConstructions = city.cityConstructions
//val selectedConstructionTable = Table()
selectedConstructionTable.background = ImageGetter.getBackground(ImageGetter.getBlue().lerp(Color.BLACK,0.5f))
selectedConstructionTable.pad(10f)
selectedConstructionTable.add(
ImageGetter.getConstructionImage(construction.name).surroundWithCircle(50f))
.pad(5f)
var buildingText = construction.name.tr()
if (SpecialConstruction.getSpecialConstructions().none { it.name == construction.name }) {
val turnsToComplete = cityConstructions.turnsToConstruction(construction.name)
buildingText += ("\r\n" + "Cost".tr() + " " + construction.getProductionCost(city.civInfo).toString()).tr()
buildingText += "\r\n" + turnsToComplete + turnOrTurns(turnsToComplete)
}
selectedConstructionTable.add(buildingText.toLabel()).row()
val description: String
if (construction is BaseUnit)
description = construction.getDescription(true)
else if (construction is Building)
description = construction.getDescription(true, city.civInfo, city.civInfo.gameInfo.ruleSet)
else if(construction is SpecialConstruction)
description = construction.description.tr()
else description="" // Should never happen
val descriptionLabel = description.toLabel()
descriptionLabel.setWrap(true)
descriptionLabel.width = stage.width / 4
val descriptionScroll = ScrollPane(descriptionLabel)
selectedConstructionTable.add(descriptionScroll).colspan(2).width(stage.width / 4).height(stage.height / 8)
}
private fun turnOrTurns(number: Int): String = if(number > 1) " {turns}".tr() else " {turn}".tr()
}

View file

@ -2,217 +2,344 @@ package com.unciv.ui.cityscreen
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.scenes.scene2d.ui.*
import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame
import com.unciv.logic.city.CityInfo
import com.unciv.logic.city.IConstruction
import com.unciv.logic.city.SpecialConstruction
import com.unciv.models.UncivSound
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.translations.tr
import com.unciv.models.stats.Stat
import com.unciv.ui.utils.*
import com.unciv.ui.worldscreen.optionstable.YesNoPopupTable
class ConstructionsTable(val cityScreen: CityScreen) : Table(CameraStageBaseScreen.skin){
class ConstructionsTable(val cityScreen: CityScreen) : Table(CameraStageBaseScreen.skin) {
/* -2 = Nothing, -1 = current construction, >= 0 queue entry */
private var selectedQueueEntry = -2 // None
var constructionScrollPane:ScrollPane?=null
var lastConstruction = ""
private val constructionsQueueScrollPane: ScrollPane
private val availableConstructionsScrollPane: ScrollPane
private val constructionsQueueTable = Table()
private val availableConstructionsTable = Table()
private val buttons = Table()
fun update() {
val city = cityScreen.city
pad(10f)
columnDefaults(0).padRight(10f)
clear()
private val pad = 10f
addConstructionPickerScrollpane(city)
addCurrentConstructionTable(city)
init {
constructionsQueueScrollPane = ScrollPane(constructionsQueueTable.addBorder(2f, Color.WHITE))
availableConstructionsScrollPane = ScrollPane(availableConstructionsTable.addBorder(2f, Color.WHITE))
constructionsQueueTable.background = ImageGetter.getBackground(Color.BLACK)
availableConstructionsTable.background = ImageGetter.getBackground(Color.BLACK)
add(constructionsQueueScrollPane).left().padBottom(pad).row()
add(buttons).center().bottom().padBottom(pad).row()
add(availableConstructionsScrollPane).left().bottom().row()
}
fun update(selectedConstruction: IConstruction?) {
val queueScrollY = constructionsQueueScrollPane.scrollY
val constrScrollY = availableConstructionsScrollPane.scrollY
clearContent()
updateButtons(selectedConstruction)
updateConstructionQueue()
constructionsQueueScrollPane.layout()
constructionsQueueScrollPane.scrollY = queueScrollY
constructionsQueueScrollPane.updateVisualScroll()
getCell(constructionsQueueScrollPane).maxHeight(stage.height / 3 - 10f)
// Need to pack before computing space left for bottom panel
pack()
val usedHeight = constructionsQueueScrollPane.height + buttons.height + 2f * pad + 10f
updateAvailableConstructions()
availableConstructionsScrollPane.layout()
availableConstructionsScrollPane.scrollY = constrScrollY
availableConstructionsScrollPane.updateVisualScroll()
getCell(availableConstructionsScrollPane).maxHeight(stage.height - usedHeight)
pack()
}
private fun getProductionButton(construction: String, buttonText: String, rejectionReason: String=""): Table {
val pickProductionButton = Table()
pickProductionButton.touchable = Touchable.enabled
pickProductionButton.align(Align.left)
pickProductionButton.pad(5f)
private fun clearContent() {
constructionsQueueTable.clear()
buttons.clear()
availableConstructionsTable.clear()
}
if(cityScreen.city.cityConstructions.currentConstruction==construction)
pickProductionButton.background = ImageGetter.getBackground(Color.GREEN.cpy().lerp(Color.BLACK,0.5f))
else
pickProductionButton.background = ImageGetter.getBackground(Color.BLACK)
private fun updateButtons(construction: IConstruction?) {
buttons.add(getQueueButton(construction)).padRight(5f)
buttons.add(getBuyButton(construction))
}
pickProductionButton.add(ImageGetter.getConstructionImage(construction).surroundWithCircle(40f)).padRight(10f)
pickProductionButton.add(buttonText.toLabel())
private fun updateConstructionQueue() {
val city = cityScreen.city
val cityConstructions = city.cityConstructions
val currentConstruction = cityConstructions.currentConstruction
val queue = cityConstructions.constructionQueue
if(rejectionReason=="" && UncivGame.Current.worldScreen.isPlayersTurn) { // no rejection reason means we can build it!
pickProductionButton.onClick {
if (!cityScreen.city.isPuppet) {
lastConstruction = cityScreen.city.cityConstructions.currentConstruction
cityScreen.city.cityConstructions.currentConstruction = construction
cityScreen.city.cityConstructions.currentConstructionIsUserSet = true
cityScreen.city.cityStats.update()
cityScreen.update()
cityScreen.game.settings.addCompletedTutorialTask("Pick construction")
}
constructionsQueueTable.defaults().pad(0f)
constructionsQueueTable.add(getHeader("Current construction".tr())).fillX()
constructionsQueueTable.addSeparator()
if (currentConstruction != "")
constructionsQueueTable.add(getQueueEntry(-1, currentConstruction, queue.isEmpty(), selectedQueueEntry == -1))
.expandX().fillX().row()
else
constructionsQueueTable.add("Pick a construction".toLabel()).pad(2f).row()
constructionsQueueTable.addSeparator()
constructionsQueueTable.add(getHeader("Construction queue".tr())).fillX()
constructionsQueueTable.addSeparator()
if (queue.isNotEmpty()) {
queue.forEachIndexed { i, constructionName ->
constructionsQueueTable.add(getQueueEntry(i, constructionName, i == queue.size - 1, i == selectedQueueEntry))
.expandX().fillX().row()
constructionsQueueTable.addSeparator()
}
}
else {
pickProductionButton.color = Color.GRAY
pickProductionButton.row()
pickProductionButton.add(rejectionReason.toLabel(Color.RED)).colspan(pickProductionButton.columns)
}
if(construction==cityScreen.city.cityConstructions.currentConstruction)
pickProductionButton.color= Color.GREEN
return pickProductionButton
} else
constructionsQueueTable.add("Queue empty".toLabel()).pad(2f).row()
}
private fun Table.addCategory(title:String,list:ArrayList<Table>){
if(list.isEmpty()) return
val titleTable = Table()
titleTable.background = ImageGetter.getBackground(ImageGetter.getBlue())
titleTable.add(title.toLabel(fontSize = 24))
addSeparator()
add(titleTable).fill().row()
addSeparator()
for(table in list) {
add(table).fill().row()
if(table != list.last()) addSeparator()
}
}
private fun addConstructionPickerScrollpane(city: CityInfo) {
private fun updateAvailableConstructions() {
val city = cityScreen.city
val cityConstructions = city.cityConstructions
val constructionPickerTable = Table()
constructionPickerTable.background = ImageGetter.getBackground(Color.BLACK)
val units = ArrayList<Table>()
for (unit in city.getRuleset().units.values.filter { it.shouldBeDisplayed(cityConstructions) }) {
val turnsToUnit = cityConstructions.turnsToConstruction(unit.name)
units += getProductionButton(unit.name,
unit.name.tr() + "\r\n" + turnsToUnit + (if(turnsToUnit>1) " {turns}".tr() else " {turn}".tr()),
unit.getRejectionReason(cityConstructions))
}
constructionPickerTable.addCategory("Units",units)
val buildableWonders = ArrayList<Table>()
val buildableNationalWonders = ArrayList<Table>()
val buildableBuildings = ArrayList<Table>()
val specialConstructions = ArrayList<Table>()
for (building in city.getRuleset().buildings.values) {
if (!building.shouldBeDisplayed(cityConstructions) && building.name != cityConstructions.currentConstruction) continue
val turnsToBuilding = cityConstructions.turnsToConstruction(building.name)
val productionTextButton = getProductionButton(building.name,
building.name.tr() + "\r\n" + turnsToBuilding + (if(turnsToBuilding>1) " {turns}".tr() else " {turn}".tr()),
building.getRejectionReason(cityConstructions)
)
if (building.isWonder)
buildableWonders += productionTextButton
else if(building.isNationalWonder)
buildableNationalWonders += productionTextButton
else
buildableBuildings += productionTextButton
for (unit in city.getRuleset().units.values.filter { it.shouldBeDisplayed(cityConstructions) }) {
val turnsToUnit = cityConstructions.turnsToConstruction(unit.name)
val productionButton = getProductionButton(unit.name,
unit.name.tr() + "\r\n" + turnsToUnit + turnOrTurns(turnsToUnit),
unit.getRejectionReason(cityConstructions))
units.add(productionButton)
}
constructionPickerTable.addCategory("Wonders",buildableWonders)
constructionPickerTable.addCategory("National Wonders",buildableNationalWonders)
constructionPickerTable.addCategory("Buildings",buildableBuildings)
for (building in city.getRuleset().buildings.values.filter { it.shouldBeDisplayed(cityConstructions)}) {
val turnsToBuilding = cityConstructions.turnsToConstruction(building.name)
val productionTextButton = getProductionButton(building.name,
building.name.tr() + "\r\n" + turnsToBuilding + turnOrTurns(turnsToBuilding),
building.getRejectionReason(cityConstructions)
)
if (building.isWonder) buildableWonders += productionTextButton
else if (building.isNationalWonder) buildableNationalWonders += productionTextButton
else buildableBuildings += productionTextButton
}
val specialConstructions = ArrayList<Table>()
for (specialConstruction in SpecialConstruction.getSpecialConstructions().filter { it.shouldBeDisplayed(cityConstructions) }) {
specialConstructions += getProductionButton(specialConstruction.name,
"Produce [${specialConstruction.name}]".tr())
}
constructionPickerTable.addCategory("Other",specialConstructions)
val scrollPane = ScrollPane(constructionPickerTable.addBorder(2f, Color.WHITE))
// This is to keep the same amount of scrolling on the construction picker scroll between refresh()es
if(constructionScrollPane!=null){
scrollPane.layout()
scrollPane.scrollY = constructionScrollPane!!.scrollY
scrollPane.updateVisualScroll()
}
constructionScrollPane = scrollPane
add(scrollPane).maxHeight(stage.height / 3 * (stage.height/600)).row()
availableConstructionsTable.addCategory("Units", units)
availableConstructionsTable.addCategory("Wonders", buildableWonders)
availableConstructionsTable.addCategory("National Wonders", buildableNationalWonders)
availableConstructionsTable.addCategory("Buildings", buildableBuildings)
availableConstructionsTable.addCategory("Other", specialConstructions)
}
private fun addCurrentConstructionTable(city: CityInfo) {
val cityConstructions = city.cityConstructions
val construction = cityConstructions.getCurrentConstruction()
private fun getQueueEntry(idx: Int, name: String, isLast: Boolean, isSelected: Boolean): Table {
val city = cityScreen.city
val table = Table()
table.align(Align.left).pad(5f)
table.background = ImageGetter.getBackground(Color.BLACK)
row()
val purchaseConstructionButton: TextButton
if (!city.isPuppet && !city.isInResistance() && construction.canBePurchased()) {
if (isSelected)
table.background = ImageGetter.getBackground(Color.GREEN.cpy().lerp(Color.BLACK, 0.5f))
val turnsToComplete = cityScreen.city.cityConstructions.turnsToConstruction(name)
val text = name.tr() + "\r\n" + turnsToComplete + turnOrTurns(turnsToComplete)
table.defaults().pad(2f).minWidth(40f)
table.add(ImageGetter.getConstructionImage(name).surroundWithCircle(40f)).padRight(10f)
table.add(text.toLabel()).expandX().fillX().left()
if (idx >= 0) table.add(getHigherPrioButton(idx, name, city)).right()
else table.add().right()
if (!isLast) table.add(getLowerPrioButton(idx, name, city)).right()
else table.add().right()
table.touchable = Touchable.enabled
table.onClick {
cityScreen.selectedConstruction = cityScreen.city.cityConstructions.getConstruction(name)
cityScreen.selectedTile = null
selectedQueueEntry = idx
cityScreen.update()
}
return table
}
private fun getProductionButton(construction: String, buttonText: String, rejectionReason: String = "", isSelectable: Boolean = true): Table {
val pickProductionButton = Table()
pickProductionButton.align(Align.left).pad(5f)
pickProductionButton.background = ImageGetter.getBackground(Color.BLACK)
pickProductionButton.touchable = Touchable.enabled
if (!isSelectedQueueEntry() && cityScreen.selectedConstruction != null && cityScreen.selectedConstruction!!.name == construction) {
pickProductionButton.background = ImageGetter.getBackground(Color.GREEN.cpy().lerp(Color.BLACK, 0.5f))
}
pickProductionButton.add(ImageGetter.getConstructionImage(construction).surroundWithCircle(40f)).padRight(10f)
pickProductionButton.add(buttonText.toLabel()).expandX().fillX().left()
// no rejection reason means we can build it!
if(rejectionReason == "") {
pickProductionButton.onClick {
cityScreen.selectedConstruction = cityScreen.city.cityConstructions.getConstruction(construction)
cityScreen.selectedTile = null
selectedQueueEntry = -2
cityScreen.update()
}
} else {
pickProductionButton.color = Color.GRAY
pickProductionButton.row()
pickProductionButton.add(rejectionReason.toLabel(Color.RED)).colspan(pickProductionButton.columns).fillX().left().padTop(2f)
}
return pickProductionButton
}
private fun isSelectedQueueEntry(): Boolean = selectedQueueEntry > -2
private fun getQueueButton(construction: IConstruction?): TextButton {
val city = cityScreen.city
val cityConstructions = city.cityConstructions
val button: TextButton
if (isSelectedQueueEntry()) {
button = TextButton("Remove from queue".tr(), CameraStageBaseScreen.skin)
if (UncivGame.Current.worldScreen.isPlayersTurn && !city.isPuppet) {
button.onClick {
cityConstructions.removeFromQueue(selectedQueueEntry)
cityScreen.selectedConstruction = null
selectedQueueEntry = -2
cityScreen.update()
}
} else {
button.disable()
}
} else {
button = TextButton("Add to queue".tr(), CameraStageBaseScreen.skin)
if (construction != null
&& !cityConstructions.isQueueFull()
&& cityConstructions.getConstruction(construction.name).isBuildable(cityConstructions)
&& UncivGame.Current.worldScreen.isPlayersTurn
&& !city.isPuppet) {
button.onClick {
cityConstructions.addToQueue(construction.name)
cityScreen.update()
}
} else {
button.disable()
}
}
button.labelCell.pad(5f)
return button
}
private fun getBuyButton(construction: IConstruction?): TextButton {
val city = cityScreen.city
val cityConstructions = city.cityConstructions
val button = TextButton("", CameraStageBaseScreen.skin)
if (construction != null
&& construction.canBePurchased()
&& UncivGame.Current.worldScreen.isPlayersTurn
&& !city.isPuppet) {
val constructionGoldCost = construction.getGoldCost(city.civInfo)
purchaseConstructionButton = TextButton("Buy for [$constructionGoldCost] gold".tr(), CameraStageBaseScreen.skin)
purchaseConstructionButton.onClick(UncivSound.Coin) {
YesNoPopupTable("Would you like to purchase [${construction.name}] for [$constructionGoldCost] gold?".tr(), {
cityConstructions.purchaseConstruction(construction.name)
if(lastConstruction!="" && cityConstructions.getConstruction(lastConstruction).isBuildable(cityConstructions))
city.cityConstructions.currentConstruction = lastConstruction
cityScreen.update() // since the list of available buildings needs to be updated too, so we can "see" that the building we bought now exists in the city
if (isSelectedQueueEntry()) {
cityConstructions.removeFromQueue(selectedQueueEntry)
selectedQueueEntry = -2
cityScreen.selectedConstruction = null
}
cityScreen.update()
}, cityScreen)
}
if (constructionGoldCost > city.civInfo.gold) {
purchaseConstructionButton.disable()
}
if (constructionGoldCost > city.civInfo.gold)
button.disable()
} else {
purchaseConstructionButton = TextButton("Buy".tr(), CameraStageBaseScreen.skin)
purchaseConstructionButton.disable()
}
purchaseConstructionButton.labelCell.pad(10f)
add(purchaseConstructionButton).pad(10f).row()
val currentConstructionTable = Table()
currentConstructionTable.background = ImageGetter.getBackground(ImageGetter.getBlue().lerp(Color.BLACK,0.5f))
currentConstructionTable.pad(10f)
val userNeedsToSetProduction = city.cityConstructions.currentConstruction==""
if(!userNeedsToSetProduction) {
currentConstructionTable.add(
ImageGetter.getConstructionImage(city.cityConstructions.currentConstruction).surroundWithCircle(50f))
.pad(5f)
val buildingText = city.cityConstructions.getCityProductionTextForCityButton()
currentConstructionTable.add(buildingText.toLabel()).row()
}
else{
currentConstructionTable.add() // no icon
currentConstructionTable.add("Pick construction".toLabel()).row()
button.setText("Buy".tr())
button.disable()
}
val description: String
if(userNeedsToSetProduction)
description=""
else if (construction is BaseUnit)
description = construction.getDescription(true)
else if (construction is Building)
description = construction.getDescription(true, city.civInfo,
city.civInfo.gameInfo.ruleSet)
else if(construction is SpecialConstruction)
description = construction.description.tr()
else description="" // Should never happen
button.labelCell.pad(5f)
val descriptionLabel = description.toLabel()
descriptionLabel.setWrap(true)
descriptionLabel.width = stage.width / 4
val descriptionScroll = ScrollPane(descriptionLabel)
currentConstructionTable.add(descriptionScroll).colspan(2)
.width(stage.width / 4).height(stage.height / 8)
add(currentConstructionTable.addBorder(2f, Color.WHITE))
return button
}
private fun getHigherPrioButton(idx: Int, name: String, city: CityInfo): Table {
val tab = Table()
tab.add(ImageGetter.getImage("OtherIcons/Up").surroundWithCircle(40f))
if (UncivGame.Current.worldScreen.isPlayersTurn && !city.isPuppet) {
tab.touchable = Touchable.enabled
tab.onClick {
tab.touchable = Touchable.disabled
city.cityConstructions.higherPrio(idx)
cityScreen.selectedConstruction = cityScreen.city.cityConstructions.getConstruction(name)
cityScreen.selectedTile = null
selectedQueueEntry = idx - 1
cityScreen.update()
}
}
return tab
}
private fun getLowerPrioButton(idx: Int, name: String, city: CityInfo): Table {
val tab = Table()
tab.add(ImageGetter.getImage("OtherIcons/Down").surroundWithCircle(40f))
if (UncivGame.Current.worldScreen.isPlayersTurn && !city.isPuppet) {
tab.touchable = Touchable.enabled
tab.onClick {
tab.touchable = Touchable.disabled
city.cityConstructions.lowerPrio(idx)
cityScreen.selectedConstruction = cityScreen.city.cityConstructions.getConstruction(name)
cityScreen.selectedTile = null
selectedQueueEntry = idx + 1
cityScreen.update()
}
}
return tab
}
private fun turnOrTurns(number: Int): String = if (number > 1) " {turns}".tr() else " {turn}".tr()
private fun getHeader(title: String): Table {
val headerTable = Table()
headerTable.background = ImageGetter.getBackground(ImageGetter.getBlue())
headerTable.add(title.toLabel(fontSize = 24)).fillX()
return headerTable
}
private fun Table.addCategory(title: String, list: ArrayList<Table>) {
if (list.isEmpty()) return
addSeparator()
add(getHeader(title)).fill().row()
addSeparator()
for (table in list) {
add(table).fill().left().row()
if (table != list.last()) addSeparator()
}
}
}

View file

@ -168,11 +168,13 @@ fun Actor.surroundWithCircle(size:Float,resizeActor:Boolean=true): IconCircleGro
return IconCircleGroup(size,this,resizeActor)
}
fun Actor.addBorder(size:Float,color:Color):Table{
fun Actor.addBorder(size:Float,color:Color,expandCell:Boolean=false):Table{
val table = Table()
table.pad(size)
table.background = ImageGetter.getBackground(color)
table.add(this).fill()
val cell = table.add(this)
if (expandCell) cell.expand()
cell.fill()
table.pack()
return table
}