diff --git a/android/Images/OtherIcons/Down.png b/android/Images/OtherIcons/Down.png new file mode 100644 index 00000000..6cd06058 Binary files /dev/null and b/android/Images/OtherIcons/Down.png differ diff --git a/android/Images/OtherIcons/Money.png b/android/Images/OtherIcons/Money.png new file mode 100644 index 00000000..b8deecb3 Binary files /dev/null and b/android/Images/OtherIcons/Money.png differ diff --git a/android/Images/OtherIcons/Shock.png b/android/Images/OtherIcons/Shock.png new file mode 100644 index 00000000..480c3d14 Binary files /dev/null and b/android/Images/OtherIcons/Shock.png differ diff --git a/android/Images/OtherIcons/Up.png b/android/Images/OtherIcons/Up.png new file mode 100644 index 00000000..e813450d Binary files /dev/null and b/android/Images/OtherIcons/Up.png differ diff --git a/android/assets/jsons/translationsByLanguage/template.properties b/android/assets/jsons/translationsByLanguage/template.properties index 76e22d5f..2c7ca2fc 100644 --- a/android/assets/jsons/translationsByLanguage/template.properties +++ b/android/assets/jsons/translationsByLanguage/template.properties @@ -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 = diff --git a/core/src/com/unciv/logic/city/CityConstructions.kt b/core/src/com/unciv/logic/city/CityConstructions.kt index bb94d586..cac21585 100644 --- a/core/src/com/unciv/logic/city/CityConstructions.kt +++ b/core/src/com/unciv/logic/city/CityConstructions.kt @@ -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() + @Transient private var builtBuildingObjects = ArrayList() var builtBuildings = HashSet() val inProgressConstructions = HashMap() - var currentConstruction: String = "Monument" // default starting building! + var currentConstruction: String = "Monument" var currentConstructionIsUserSet = false + var constructionQueue = mutableListOf() + 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 = 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().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.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 \ No newline at end of file diff --git a/core/src/com/unciv/logic/city/CityInfo.kt b/core/src/com/unciv/logic/city/CityInfo.kt index 098a4c0a..ff24336f 100644 --- a/core/src/com/unciv/logic/city/CityInfo.kt +++ b/core/src/com/unciv/logic/city/CityInfo.kt @@ -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) { diff --git a/core/src/com/unciv/models/ruleset/Building.kt b/core/src/com/unciv/models/ruleset/Building.kt index f75f9abe..c1b94f2e 100644 --- a/core/src/com/unciv/models/ruleset/Building.kt +++ b/core/src/com/unciv/models/ruleset/Building.kt @@ -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 }) diff --git a/core/src/com/unciv/ui/cityscreen/CityInfoTable.kt b/core/src/com/unciv/ui/cityscreen/CityInfoTable.kt index af260f9f..18258bdd 100644 --- a/core/src/com/unciv/ui/cityscreen/CityInfoTable.kt +++ b/core/src/com/unciv/ui/cityscreen/CityInfoTable.kt @@ -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() val specialistBuildings = mutableListOf() diff --git a/core/src/com/unciv/ui/cityscreen/CityScreen.kt b/core/src/com/unciv/ui/cityscreen/CityScreen.kt index 4ab1704b..90603987 100644 --- a/core/src/com/unciv/ui/cityscreen/CityScreen.kt +++ b/core/src/com/unciv/ui/cityscreen/CityScreen.kt @@ -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() 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() } - } \ No newline at end of file diff --git a/core/src/com/unciv/ui/cityscreen/ConstructionInfoTable.kt b/core/src/com/unciv/ui/cityscreen/ConstructionInfoTable.kt new file mode 100644 index 00000000..41ec2e48 --- /dev/null +++ b/core/src/com/unciv/ui/cityscreen/ConstructionInfoTable.kt @@ -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() +} \ No newline at end of file diff --git a/core/src/com/unciv/ui/cityscreen/ConstructionsTable.kt b/core/src/com/unciv/ui/cityscreen/ConstructionsTable.kt index 54e18adf..cab82993 100644 --- a/core/src/com/unciv/ui/cityscreen/ConstructionsTable.kt +++ b/core/src/com/unciv/ui/cityscreen/ConstructionsTable.kt @@ -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){ - 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
() - 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
() val buildableNationalWonders = ArrayList
() val buildableBuildings = ArrayList
() + val specialConstructions = ArrayList
() - 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
() 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
) { + 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() + } + } } \ No newline at end of file diff --git a/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt b/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt index 83083f91..03d02cbc 100644 --- a/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt +++ b/core/src/com/unciv/ui/utils/CameraStageBaseScreen.kt @@ -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 }