City Screen improvements (#8598)

* City Screen improvements

* Buy icon reverted to Gold icon

---------

Co-authored-by: tunerzinc@gmail.com <vfylfhby>
This commit is contained in:
vegeta1k95 2023-02-06 14:22:36 +01:00 committed by GitHub
parent 4b2d4a3877
commit 4e15a1581b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 643 additions and 485 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 KiB

After

Width:  |  Height:  |  Size: 469 KiB

View File

@ -55,6 +55,15 @@ class CityExpansionManager : IsPartOfGameInfoSerialization {
return cultureToNextTile.roundToInt()
}
fun canBuyTile(tile: Tile): Boolean {
return when {
city.isPuppet -> false
tile.getOwner() != null -> false
tile !in city.tilesInRange -> false
else -> tile.neighbors.any { it.getCity() == city }
}
}
fun buyTile(tile: Tile) {
val goldCost = getGoldCostOfTile(tile)

View File

@ -697,6 +697,14 @@ class Civilization : IsPartOfGameInfoSerialization {
}
}
fun hasGoldToBuy(price: Int): Boolean {
return when {
gameInfo.gameParameters.godMode -> true
price == 0 -> true
else -> gold >= price
}
}
fun addStats(stats: Stats){
for ((stat, amount) in stats) addStat(stat, amount.toInt())
}

View File

@ -0,0 +1,29 @@
package com.unciv.ui.cityscreen
import com.unciv.ui.UncivStage
import com.unciv.ui.tilegroups.TileGroupMap
import com.unciv.ui.utils.ZoomableScrollPane
class CityMapHolder : ZoomableScrollPane(20f, 20f) {
init {
setupZoomPanListeners()
}
private fun setupZoomPanListeners() {
fun setActHit() {
val isEnabled = !isZooming() && !isPanning
(stage as UncivStage).performPointerEnterExitEvents = isEnabled
val tileGroupMap = actor as TileGroupMap<*>
tileGroupMap.shouldAct = isEnabled
tileGroupMap.shouldHit = isEnabled
}
onPanStartListener = { setActHit() }
onPanStopListener = { setActHit() }
onZoomStartListener = { setActHit() }
onZoomStopListener = { setActHit() }
}
}

View File

@ -16,9 +16,11 @@ import com.unciv.models.ruleset.tile.TileImprovement
import com.unciv.models.ruleset.unique.LocalUniqueCache
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.stats.Stat
import com.unciv.models.translations.tr
import com.unciv.ui.audio.CityAmbiencePlayer
import com.unciv.ui.audio.SoundPlayer
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.popup.ConfirmPopup
import com.unciv.ui.tilegroups.TileGroupMap
import com.unciv.ui.popup.ToastPopup
import com.unciv.ui.tilegroups.CityTileGroup
@ -26,7 +28,6 @@ import com.unciv.ui.tilegroups.TileSetStrings
import com.unciv.ui.utils.BaseScreen
import com.unciv.ui.utils.KeyCharAndCode
import com.unciv.ui.utils.RecreateOnResize
import com.unciv.ui.utils.ZoomableScrollPane
import com.unciv.ui.utils.extensions.disable
import com.unciv.ui.utils.extensions.keyShortcuts
import com.unciv.ui.utils.extensions.onActivation
@ -88,7 +89,7 @@ class CityScreen(
private var tileGroups = ArrayList<CityTileGroup>()
/** The ScrollPane for the background map view of the city surroundings */
private val mapScrollPane = ZoomableScrollPane()
private val mapScrollPane = CityMapHolder()
/** Support for [UniqueType.CreatesOneImprovement] - need user to pick a tile */
class PickTileForImprovementData (
@ -302,9 +303,8 @@ class CityScreen(
.map { CityTileGroup(cityInfo, it, tileSetStrings) }
for (tileGroup in cityTileGroups) {
tileGroup.onClick {
tileGroupOnClick(tileGroup, cityInfo)
}
tileGroup.onClick { tileGroupOnClick(tileGroup, cityInfo) }
tileGroup.layerMisc.onClick { tileWorkedIconOnClick(tileGroup, cityInfo) }
tileGroups.add(tileGroup)
}
@ -330,6 +330,45 @@ class CityScreen(
mapScrollPane.updateVisualScroll()
}
private fun tileWorkedIconOnClick(tileGroup: CityTileGroup, city: City) {
if (!canChangeState || city.isPuppet) return
val tile = tileGroup.tile
// Cycling as: Not-worked -> Worked -> Locked -> Not-worked
if (tileGroup.isWorkable) {
if (!tile.providesYield() && city.population.getFreePopulation() > 0) {
city.workedTiles.add(tile.position)
game.settings.addCompletedTutorialTask("Reassign worked tiles")
} else if (tile.isWorked() && !tile.isLocked()) {
city.lockedTiles.add(tile.position)
} else if (tile.isLocked()) {
city.workedTiles.remove(tile.position)
city.lockedTiles.remove(tile.position)
}
city.cityStats.update()
update()
} else if (tileGroup.isPurchasable) {
val price = city.expansion.getGoldCostOfTile(tile)
val purchasePrompt = "Currently you have [${city.civ.gold}] [Gold].".tr() + "\n\n" +
"Would you like to purchase [Tile] for [$price] [${Stat.Gold.character}]?".tr()
ConfirmPopup(
this,
purchasePrompt,
"Purchase",
true,
restoreDefault = { update() }
) {
SoundPlayer.play(UncivSound.Coin)
city.expansion.buyTile(tile)
// preselect the next tile on city screen rebuild so bulk buying can go faster
UncivGame.Current.replaceCurrentScreen(CityScreen(city, initSelectedTile = city.expansion.chooseNewTileToOwn()))
}.open()
}
}
private fun tileGroupOnClick(tileGroup: CityTileGroup, city: City) {
if (city.isPuppet) return
val tileInfo = tileGroup.tile
@ -356,17 +395,6 @@ class CityScreen(
}
selectTile(tileInfo)
if (tileGroup.isWorkable && canChangeState) {
if (!tileInfo.providesYield() && city.population.getFreePopulation() > 0) {
city.workedTiles.add(tileInfo.position)
city.lockedTiles.add(tileInfo.position)
game.settings.addCompletedTutorialTask("Reassign worked tiles")
} else if (tileInfo.isWorked()) {
city.workedTiles.remove(tileInfo.position)
city.lockedTiles.remove(tileInfo.position)
}
city.cityStats.update()
}
update()
}

View File

@ -59,7 +59,7 @@ class CityScreenTileTable(private val cityScreen: CityScreen): Table() {
innerTable.row()
innerTable.add(getTileStatsTable(stats)).row()
if (isTilePurchaseShown(selectedTile)) {
if (city.expansion.canBuyTile(selectedTile)) {
val goldCostOfTile = city.expansion.getGoldCostOfTile(selectedTile)
val buyTileButton = "Buy for [$goldCostOfTile] gold".toTextButton()
buyTileButton.onActivation {
@ -67,7 +67,7 @@ class CityScreenTileTable(private val cityScreen: CityScreen): Table() {
askToBuyTile(selectedTile)
}
buyTileButton.keyShortcuts.add('T')
buyTileButton.isEnabled = isTilePurchaseAllowed(goldCostOfTile)
buyTileButton.isEnabled = cityScreen.canChangeState && city.civ.hasGoldToBuy(goldCostOfTile)
buyTileButton.addTooltip('T') // The key binding is done in CityScreen constructor
innerTable.add(buyTileButton).padTop(5f).row()
}
@ -113,9 +113,9 @@ class CityScreenTileTable(private val cityScreen: CityScreen): Table() {
*/
private fun askToBuyTile(selectedTile: Tile) {
// These checks are redundant for the onClick action, but not for the keyboard binding
if (!isTilePurchaseShown(selectedTile)) return
if (!city.expansion.canBuyTile(selectedTile)) return
val goldCostOfTile = city.expansion.getGoldCostOfTile(selectedTile)
if (!isTilePurchaseAllowed(goldCostOfTile)) return
if (!city.civ.hasGoldToBuy(goldCostOfTile)) return
cityScreen.closeAllPopups()
@ -135,21 +135,6 @@ class CityScreenTileTable(private val cityScreen: CityScreen): Table() {
}.open()
}
/** This tests whether the buy button should be _shown_ */
private fun isTilePurchaseShown(selectedTile: Tile) = when {
selectedTile.getOwner() != null -> false
selectedTile !in city.tilesInRange -> false
else -> selectedTile.neighbors.any { it.getCity() == city }
}
/** This tests whether the buy button should be _enabled_ */
private fun isTilePurchaseAllowed(goldCostOfTile: Int) = when {
city.isPuppet -> false
!cityScreen.canChangeState -> false
city.civ.gameInfo.gameParameters.godMode -> true
goldCostOfTile == 0 -> true
else -> city.civ.gold >= goldCostOfTile
}
private fun getTileStatsTable(stats: Stats): Table {
val statsTable = Table()
statsTable.defaults().pad(2f)

View File

@ -1,23 +1,38 @@
package com.unciv.ui.tilegroups
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame
import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.tile.Tile
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.utils.extensions.addToCenter
import com.unciv.ui.utils.extensions.darken
import com.unciv.ui.utils.extensions.setFontColor
import com.unciv.ui.utils.extensions.toGroup
import com.unciv.ui.utils.extensions.toLabel
class CityTileGroup(private val city: City, tile: Tile, tileSetStrings: TileSetStrings) : TileGroup(tile,tileSetStrings) {
var isWorkable = false
var isPurchasable = false
init {
if (city.location == tile.position)
layerMisc.setNewPopulationIcon(ImageGetter.getImage("OtherIcons/Star"))
layerMisc.touchable = Touchable.childrenOnly
}
override fun update(viewingCiv: Civilization?) {
super.update(city.civ)
isWorkable = false
isPurchasable = false
layerMisc.removeWorkedIcon()
var icon: Actor? = null
when {
// Does not belong to us
@ -25,6 +40,25 @@ class CityTileGroup(private val city: City, tile: Tile, tileSetStrings: TileSetS
layerTerrain.dim(0.3f)
layerMisc.setYieldVisible(UncivGame.Current.settings.showTileYields)
layerMisc.dimYields(true)
// Can be purchased in principle? Add icon.
if (city.expansion.canBuyTile(tile)) {
val price = city.expansion.getGoldCostOfTile(tile)
val label = price.toString().toLabel(fontSize = 9, alignment = Align.center)
val image = ImageGetter.getImage("TileIcons/Buy")
icon = image.toGroup(26f).apply { isTransform = false }
icon.addToCenter(label)
label.y -= 15f
// Can be purchased now?
if (!city.civ.hasGoldToBuy(price)) {
image.color = Color.WHITE.darken(0.5f)
label.setFontColor(Color.RED)
} else {
isPurchasable = true
}
}
}
// Out of city range
@ -40,34 +74,56 @@ class CityTileGroup(private val city: City, tile: Tile, tileSetStrings: TileSetS
layerMisc.dimYields(true)
}
// Locked
tile.isLocked() -> {
layerMisc.setNewPopulationIcon(ImageGetter.getImage("OtherIcons/Lock"))
isWorkable = true
// City Center
tile.isCityCenter() -> {
icon = ImageGetter.getImage("TileIcons/CityCenter")
layerMisc.dimYields(false)
}
// Workable
tile.isWorked() || !tile.providesYield() -> {
layerMisc.setNewPopulationIcon()
// Does not provide yields
tile.stats.getTileStats(viewingCiv).isEmpty() -> {
// Do nothing
}
// Locked
tile.isLocked() -> {
icon = ImageGetter.getImage("TileIcons/Locked")
isWorkable = true
layerMisc.dimYields(false)
}
// Worked
tile.isWorked() -> {
icon = ImageGetter.getImage("TileIcons/Worked")
isWorkable = true
layerMisc.dimYields(false)
}
// Not-worked
else -> {
icon = ImageGetter.getImage("TileIcons/NotWorked")
isWorkable = true
layerMisc.dimYields(true)
}
}
// No unit flags inside CityScreen
layerUnitFlag.isVisible = false
if (icon != null) {
icon.setSize(26f, 26f)
icon.setPosition(width/2 - icon.width/2,
height*0.85f - icon.height/2)
layerMisc.addWorkedIcon(icon)
}
// Pixel art, road, improvements are dimmed inside CityScreen
// No unit flags and city-buttons inside CityScreen
layerUnitFlag.isVisible = false
layerCityButton.isVisible = false
// Pixel art, roads, improvements are dimmed inside CityScreen
layerUnitArt.dim()
layerFeatures.dim()
layerMisc.dimImprovement(true)
// Dim yield icons if tile is not worked
if (!tile.providesYield())
layerMisc.dimYields(true)
// Update citizen icon and put whole layer (yield, pop, improvement, res) to front
layerMisc.updatePopulationIcon()
// Put whole layer (yield, pop, improvement, res) to front
layerMisc.toFront()
}
}

View File

@ -1,30 +1,54 @@
package com.unciv.ui.tilegroups
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.unciv.UncivGame
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.tile.Tile
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.utils.extensions.center
import com.unciv.ui.utils.extensions.darken
import com.unciv.ui.worldscreen.WorldScreen
class WorldTileGroup(internal val worldScreen: WorldScreen, tile: Tile, tileSetStrings: TileSetStrings)
: TileGroup(tile,tileSetStrings) {
override fun update(viewingCiv: Civilization?) {
layerMisc.removePopulationIcon()
val city = tile.getCity()
val tileIsViewable = isViewable(viewingCiv!!)
// Show population icon overlay (if option is enabled)
if (tileIsViewable && tile.isWorked() && UncivGame.Current.settings.showWorkedTiles
&& city!!.civ == viewingCiv) {
layerMisc.setNewPopulationIcon()
}
super.update(viewingCiv)
init {
layerMisc.touchable = Touchable.disabled
}
override fun update(viewingCiv: Civilization?) {
super.update(viewingCiv)
updateWorkedIcon(viewingCiv!!)
}
private fun updateWorkedIcon(viewingCiv: Civilization) {
layerMisc.removeWorkedIcon()
val shouldShowWorkedIcon = UncivGame.Current.settings.showWorkedTiles // Overlay enabled;
&& isViewable(viewingCiv) // We see tile;
&& tile.getCity()?.civ == viewingCiv // Tile belongs to us;
&& tile.isWorked() // Tile is worked;
if (!shouldShowWorkedIcon)
return
val icon = when {
tile.isLocked() -> ImageGetter.getImage("TileIcons/Locked").apply { color = Color.WHITE.darken(0.5f) }
tile.isWorked() && tile.providesYield() -> ImageGetter.getImage("TileIcons/Worked").apply { color = Color.WHITE.darken(0.5f) }
else -> null
}
if (icon != null) {
icon.setSize(20f, 20f)
icon.center(this)
icon.x += 20f
layerMisc.addWorkedIcon(icon)
}
}
override fun clone(): WorldTileGroup = WorldTileGroup(worldScreen, tile , tileSetStrings)
}

View File

@ -1,6 +1,7 @@
package com.unciv.ui.tilegroups.layers
import com.badlogic.gdx.scenes.scene2d.Group
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.ui.Image
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.tile.Tile
@ -11,6 +12,7 @@ import com.unciv.ui.tilegroups.TileSetStrings
abstract class TileLayer(val tileGroup: TileGroup, size: Float) : Group() {
init {
touchable = Touchable.disabled
isTransform = false
@Suppress("LeakingThis")
setSize(size, size)

View File

@ -13,14 +13,13 @@ import com.unciv.models.helpers.MapArrowType
import com.unciv.models.helpers.MiscArrowTypes
import com.unciv.models.helpers.TintedMapArrow
import com.unciv.models.helpers.UnitMovementMemoryType
import com.unciv.ui.tilegroups.YieldGroup
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.tilegroups.TileGroup
import com.unciv.ui.tilegroups.TileSetStrings
import com.unciv.ui.tilegroups.WorldTileGroup
import com.unciv.ui.tilegroups.YieldGroup
import com.unciv.ui.utils.extensions.center
import com.unciv.ui.utils.extensions.centerX
import com.unciv.ui.utils.extensions.darken
import com.unciv.ui.utils.extensions.toLabel
import kotlin.math.atan2
import kotlin.math.min
@ -44,7 +43,15 @@ private class MapArrow(val targetTile: Tile, val arrowType: MapArrowType, val st
class TileLayerMisc(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, size) {
override fun act(delta: Float) {}
override fun hit(x: Float, y: Float, touchable: Boolean): Actor? = null
override fun hit(x: Float, y: Float, touchable: Boolean): Actor? {
return if (workedIcon == null) {
null
} else {
val coords = Vector2(x, y)
workedIcon!!.parentToLocalCoordinates(coords)
workedIcon!!.hit(coords.x, coords.y, touchable)
}
}
private var yieldsInitialized = false
private var yields = YieldGroup().apply { isVisible = false }
@ -53,10 +60,10 @@ class TileLayerMisc(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, si
private val arrowsToDraw = ArrayList<MapArrow>()
private val arrows = HashMap<Tile, ArrayList<Actor>>()
private var resourceName: String? = null
private var resourceIcon: Actor? = null
private var workedIcon: Actor? = null
var improvementIcon: Actor? = null
var populationIcon: Image? = null
var resourceIcon: Actor? = null
var resourceName: String? = null
private val startingLocationIcons = mutableListOf<Actor>()
@ -233,36 +240,16 @@ class TileLayerMisc(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, si
}
}
fun updatePopulationIcon() {
val icon = populationIcon
if (icon != null) {
icon.setSize(25f, 25f)
icon.setPosition(width / 2 - icon.width / 2,
height * 0.85f - icon.height / 2)
icon.color = when {
tile().isCityCenter() -> Color.GOLD.cpy()
tile().providesYield() -> Color.WHITE.cpy()
else -> Color.GRAY.cpy()
}
icon.toFront()
}
fun removeWorkedIcon() {
workedIcon?.remove()
workedIcon = null
determineVisibility()
}
fun setNewPopulationIcon(icon: Image = ImageGetter.getStatIcon("Population")
.apply { color = Color.GREEN.darken(0.5f) }) {
populationIcon?.remove()
populationIcon = icon
populationIcon!!.run {
setSize(20f, 20f)
center(tileGroup)
x += 20 // right
}
addActor(populationIcon)
}
fun removePopulationIcon() {
populationIcon?.remove()
populationIcon = null
fun addWorkedIcon(icon: Actor) {
workedIcon = icon
addActor(workedIcon)
determineVisibility()
}
@ -278,7 +265,7 @@ class TileLayerMisc(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, si
fun dimImprovement(dim: Boolean) { improvementIcon?.color?.a = if (dim) 0.5f else 1f }
fun dimResource(dim: Boolean) { resourceIcon?.color?.a = if (dim) 0.5f else 1f }
fun dimYields(dim: Boolean) { yields.color.a = if (dim) 0.5f else 1f }
fun dimPopulation(dim: Boolean) { populationIcon?.color?.a = if (dim) 0.4f else 1f }
fun dimPopulation(dim: Boolean) { workedIcon?.color?.a = if (dim) 0.4f else 1f }
fun setYieldVisible(isVisible: Boolean) {
yields.isVisible = isVisible
@ -305,7 +292,7 @@ class TileLayerMisc(tileGroup: TileGroup, size: Float) : TileLayer(tileGroup, si
isVisible = yields.isVisible
|| resourceIcon?.isVisible == true
|| improvementIcon != null
|| populationIcon != null
|| workedIcon != null
|| arrows.isNotEmpty()
|| startingLocationIcons.isNotEmpty()
}

View File

@ -294,6 +294,8 @@ open class ZoomableScrollPane(
}
override fun panStop(event: InputEvent?, x: Float, y: Float, pointer: Int, button: Int) {
if (zoomListener.isZooming)
zoomListener.isZooming = false
isPanning = false
onPanStopListener?.invoke()
}