Add Replay feature in VictoryScreen (#8844)

* Add Replay feature in VictoryScreen.

* Add Replay feature in VictoryScreen.

* Support for i18n

* Extract year to text conversion into common util to be used from VictoryScreen.kt and WorldScreenTopBar.kt

* Remove ReplayMapTile and modify MinimapTile so that it can support both use cases.

* Reuse code for spreading out tiles onto tile layer between Minimap and ReplayMap by factoring it out into a new MinimapTileUtil

* Revert "Reuse code for spreading out tiles onto tile layer between Minimap and ReplayMap by factoring it out into a new MinimapTileUtil"

This reverts commit d4cddb4312.

* Add Replay feature in VictoryScreen.

* Add Replay feature in VictoryScreen.

* Support for i18n

* Extract year to text conversion into common util to be used from VictoryScreen.kt and WorldScreenTopBar.kt

* Remove ReplayMapTile and modify MinimapTile so that it can support both use cases.

* Revert some unintentional indentation changes

* Refactor some common logic of Minimap and ReplayMap into MinimapTileUtil

* Slightly increase ReplayMap size and simplify logic to calculate tile size since input is static.

* Indentation again... :|

* Unify isCityCenter & isCapital into an enum in TileHistory and shorten identifiers

* Use city.getTiles() instead of city.tiles in CityInfoConquestFunctions.kt

* Improve tileSize calculation in ReplayMap.kt

* Remove extra padding in VictoryScreen -> Replay to prevent WorldScreenTopBar from acting up on the next turn.

* Make return value of MinimapTileUtil.spreadOutMinimapTiles more useful to callers

* Cancel Replay timer when VictoryScreen is disposed or when Replay is opened again.

* Cancel replay map timer task whenever tab is switched in VictoryScreen

* Improve serialization for TileHistory by using a custom serializer. This removes the need for holding two copies of the same thing and to use String based keys.

* Add backwards compatibility for replay. The replay will start at the turn where it came into play.

* Remove debugging code :|

* Use gameInfo field rather than going throug the global UncivGame...
This commit is contained in:
WhoIsJohannes
2023-03-12 18:59:48 +01:00
committed by GitHub
parent 680da3232f
commit f4dca2281e
16 changed files with 374 additions and 46 deletions

View File

@ -1314,6 +1314,7 @@ Vote for [civilizationName] =
Continue = Continue =
Abstained = Abstained =
Vote for World Leader = Vote for World Leader =
Replay =
# Capturing a city # Capturing a city

View File

@ -4,6 +4,7 @@ import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.utils.Json import com.badlogic.gdx.utils.Json
import com.badlogic.gdx.utils.SerializationException import com.badlogic.gdx.utils.SerializationException
import com.unciv.logic.map.tile.TileHistory
import com.unciv.ui.components.KeyCharAndCode import com.unciv.ui.components.KeyCharAndCode
import com.unciv.ui.components.KeyboardBindings import com.unciv.ui.components.KeyboardBindings
import java.time.Duration import java.time.Duration
@ -25,6 +26,7 @@ fun json() = Json().apply {
setSerializer(Duration::class.java, DurationSerializer()) setSerializer(Duration::class.java, DurationSerializer())
setSerializer(KeyCharAndCode::class.java, KeyCharAndCode.Serializer()) setSerializer(KeyCharAndCode::class.java, KeyCharAndCode.Serializer())
setSerializer(KeyboardBindings::class.java, KeyboardBindings.Serializer()) setSerializer(KeyboardBindings::class.java, KeyboardBindings.Serializer())
setSerializer(TileHistory::class.java, TileHistory.Serializer())
} }
/** /**

View File

@ -207,4 +207,12 @@ object BackwardCompatibility {
gameParameters.gameSpeed = "" gameParameters.gameSpeed = ""
} }
} }
fun GameInfo.migrateToTileHistory() {
if (historyStartTurn >= 0) return
for (tile in getCities().flatMap { it.getTiles() }) {
tile.history.recordTakeOwnership(tile)
}
historyStartTurn = turns
}
} }

View File

@ -7,6 +7,7 @@ import com.unciv.logic.BackwardCompatibility.convertFortify
import com.unciv.logic.BackwardCompatibility.convertOldGameSpeed import com.unciv.logic.BackwardCompatibility.convertOldGameSpeed
import com.unciv.logic.BackwardCompatibility.guaranteeUnitPromotions import com.unciv.logic.BackwardCompatibility.guaranteeUnitPromotions
import com.unciv.logic.BackwardCompatibility.migrateBarbarianCamps import com.unciv.logic.BackwardCompatibility.migrateBarbarianCamps
import com.unciv.logic.BackwardCompatibility.migrateToTileHistory
import com.unciv.logic.BackwardCompatibility.removeMissingModReferences import com.unciv.logic.BackwardCompatibility.removeMissingModReferences
import com.unciv.logic.GameInfo.Companion.CURRENT_COMPATIBILITY_NUMBER import com.unciv.logic.GameInfo.Companion.CURRENT_COMPATIBILITY_NUMBER
import com.unciv.logic.GameInfo.Companion.FIRST_WITHOUT import com.unciv.logic.GameInfo.Companion.FIRST_WITHOUT
@ -103,6 +104,17 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
// Set to false whenever the results still need te be processed // Set to false whenever the results still need te be processed
var diplomaticVictoryVotesProcessed = false var diplomaticVictoryVotesProcessed = false
/** The turn the replay history started recording.
*
* * `-1` means the game was serialized with an older version without replay
* * `0` would be the normal value in any newer game
* (remember gameParameters.startingEra is not implemented through turns starting > 0)
* * `>0` would be set by compatibility migration, handled in [BackwardCompatibility.migrateToTileHistory]
*
* @see [com.unciv.logic.map.tile.TileHistory]
*/
var historyStartTurn = -1
/** /**
* Keep track of a custom location this game was saved to _or_ loaded from, using it as the default custom location for any further save/load attempts. * Keep track of a custom location this game was saved to _or_ loaded from, using it as the default custom location for any further save/load attempts.
*/ */
@ -164,6 +176,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
toReturn.oneMoreTurnMode = oneMoreTurnMode toReturn.oneMoreTurnMode = oneMoreTurnMode
toReturn.customSaveLocation = customSaveLocation toReturn.customSaveLocation = customSaveLocation
toReturn.victoryData = victoryData toReturn.victoryData = victoryData
toReturn.historyStartTurn = historyStartTurn
return toReturn return toReturn
} }
@ -591,6 +604,8 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
cityDistances.game = this cityDistances.game = this
guaranteeUnitPromotions() guaranteeUnitPromotions()
migrateToTileHistory()
} }
//endregion //endregion

View File

@ -98,6 +98,8 @@ object GameStarter {
} }
runAndMeasure("setTransients") { runAndMeasure("setTransients") {
// mark as no migrateToTileHistory necessary
gameInfo.historyStartTurn = 0
tileMap.setTransients(ruleset) // if we're starting from a map with pre-placed units, they need the civs to exist first tileMap.setTransients(ruleset) // if we're starting from a map with pre-placed units, they need the civs to exist first
tileMap.setStartingLocationsTransients() tileMap.setStartingLocationsTransients()

View File

@ -149,6 +149,8 @@ class CityExpansionManager : IsPartOfGameInfoSerialization {
city.civ.cache.updateCivResources() city.civ.cache.updateCivResources()
city.cityStats.update() city.cityStats.update()
tile.history.recordRelinquishOwnership(tile)
} }
/** /**
@ -175,6 +177,8 @@ class CityExpansionManager : IsPartOfGameInfoSerialization {
unit.movement.teleportToClosestMoveableTile() unit.movement.teleportToClosestMoveableTile()
city.civ.cache.updateViewableTiles() city.civ.cache.updateViewableTiles()
tile.history.recordTakeOwnership(tile)
} }
fun nextTurn(culture: Float) { fun nextTurn(culture: Float) {

View File

@ -2,7 +2,6 @@
import com.unciv.Constants import com.unciv.Constants
import com.unciv.GUI import com.unciv.GUI
import com.unciv.UncivGame
import com.unciv.logic.battle.Battle import com.unciv.logic.battle.Battle
import com.unciv.logic.city.City import com.unciv.logic.city.City
import com.unciv.logic.city.CityFlags import com.unciv.logic.city.CityFlags
@ -324,6 +323,11 @@ class CityInfoConquestFunctions(val city: City){
// Update proximity rankings // Update proximity rankings
civ.updateProximity(oldCiv, civ.updateProximity(oldCiv,
oldCiv.updateProximity(civ)) oldCiv.updateProximity(civ))
// Update history
city.getTiles().forEach { tile ->
tile.history.recordTakeOwnership(tile)
}
} }
} }

View File

@ -130,6 +130,8 @@ open class Tile : IsPartOfGameInfoSerialization {
var hasBottomRiver = false var hasBottomRiver = false
var hasBottomLeftRiver = false var hasBottomLeftRiver = false
var history: TileHistory = TileHistory()
private var continent = -1 private var continent = -1
val latitude: Float val latitude: Float
@ -169,6 +171,7 @@ open class Tile : IsPartOfGameInfoSerialization {
toReturn.hasBottomRiver = hasBottomRiver toReturn.hasBottomRiver = hasBottomRiver
toReturn.continent = continent toReturn.continent = continent
toReturn.exploredBy.addAll(exploredBy) toReturn.exploredBy.addAll(exploredBy)
toReturn.history = history.clone()
return toReturn return toReturn
} }

View File

@ -0,0 +1,93 @@
package com.unciv.logic.map.tile
import com.badlogic.gdx.utils.Json
import com.badlogic.gdx.utils.JsonValue
import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.logic.map.tile.TileHistory.TileHistoryState.CityCenterType
import java.util.*
/**
* Records events throughout the game related to a tile.
*
* Used for end of game replay.
*
* @see com.unciv.ui.screens.victoryscreen.ReplayMap
*/
open class TileHistory : IsPartOfGameInfoSerialization {
class TileHistoryState(
/** The name of the civilization owning this tile or `null` if there is no owner. */
var owningCivName: String? = null,
/** `null` if this tile does not have a city center. Otherwise this field denotes of which type this city center is. */
var cityCenterType: CityCenterType = CityCenterType.None
) : IsPartOfGameInfoSerialization {
enum class CityCenterType(val serializedRepresentation: String) {
None("N"),
Regular("R"),
Capital("C");
companion object {
fun deserialize(s: String): CityCenterType =
values().firstOrNull { it.serializedRepresentation == s } ?: None
}
}
constructor(tile: Tile) : this(
tile.getOwner()?.civName,
when {
!tile.isCityCenter() -> CityCenterType.None
tile.getCity()?.isCapital() == true -> CityCenterType.Capital
else -> CityCenterType.Regular
}
)
}
/** History records by turn. */
private var history: TreeMap<Int, TileHistoryState> = TreeMap()
fun recordTakeOwnership(tile: Tile) {
history[tile.tileMap.gameInfo.turns] =
TileHistoryState(tile)
}
fun recordRelinquishOwnership(tile: Tile) {
history[tile.tileMap.gameInfo.turns] =
TileHistoryState()
}
fun getState(turn: Int): TileHistoryState {
return history.floorEntry(turn)?.value ?: TileHistoryState()
}
fun clone(): TileHistory {
val toReturn = TileHistory()
toReturn.history = TreeMap(history)
return toReturn
}
/** Custom Json formatter for a [TileHistory].
* Output looks like this: `history:{0:[Spain,C],12:[China,R]}`
*/
class Serializer : Json.Serializer<TileHistory> {
override fun write(json: Json, `object`: TileHistory, knownType: Class<*>?) {
json.writeObjectStart()
for ((key, entry) in `object`.history) {
json.writeArrayStart(key.toString())
json.writeValue(entry.owningCivName)
json.writeValue(entry.cityCenterType.serializedRepresentation)
json.writeArrayEnd()
}
json.writeObjectEnd()
}
override fun read(json: Json, jsonData: JsonValue, type: Class<*>?) = TileHistory().apply {
for (entry in jsonData) {
val turn = entry.name.toInt()
val owningCivName =
(if (entry[0].isString) entry.getString(0) else "").takeUnless { it.isEmpty() }
val cityCenterType = CityCenterType.deserialize(entry.getString(1))
history[turn] = TileHistoryState(owningCivName, cityCenterType)
}
}
}
}

View File

@ -0,0 +1,14 @@
package com.unciv.ui.components
import com.unciv.models.translations.tr
import kotlin.math.abs
object YearTextUtil {
/** Converts a year to a human-readable year (e.g. "1800 AD" or "3000 BC") while respecting the Maya calendar. */
fun toYearText(year: Int, usesMayaCalendar: Boolean): String {
val yearText = if (usesMayaCalendar) MayaCalendar.yearToMayaDate(year)
else "[" + abs(year) + "] " + (if (year < 0) "BC" else "AD")
return yearText.tr()
}
}

View File

@ -0,0 +1,72 @@
package com.unciv.ui.screens.victoryscreen
import com.badlogic.gdx.graphics.g2d.Batch
import com.badlogic.gdx.scenes.scene2d.Group
import com.unciv.UncivGame
import com.unciv.logic.map.TileMap
import com.unciv.ui.screens.worldscreen.minimap.MinimapTile
import com.unciv.ui.screens.worldscreen.minimap.MinimapTileUtil
import kotlin.math.min
// Mostly copied from MiniMap
class ReplayMap(val tileMap: TileMap) : Group() {
private val tileLayer = Group()
private val minimapTiles: List<MinimapTile>
init {
// don't try to resize rotate etc - this table has a LOT of children so that's valuable
// render time!
isTransform = false
val tileSize = calcTileSize()
minimapTiles = createReplayMap(tileSize)
val tileExtension = MinimapTileUtil.spreadOutMinimapTiles(tileLayer, minimapTiles, tileSize)
for (group in tileLayer.children) {
group.moveBy(-tileExtension.x, -tileExtension.y)
}
// there are tiles "below the zero",
// so we zero out the starting position of the whole board so they will be displayed as well
tileLayer.setSize(tileExtension.width, tileExtension.height)
setSize(tileLayer.width, tileLayer.height)
addActor(tileLayer)
}
private fun calcTileSize(): Float {
val mapIsNotRectangular =
tileMap.mapParameters.shape != com.unciv.logic.map.MapShape.rectangular
val tileRows = with(tileMap.mapParameters.mapSize) {
if (mapIsNotRectangular) radius * 2 + 1 else height
}
val tileColumns = with(tileMap.mapParameters.mapSize) {
if (mapIsNotRectangular) radius * 2 + 1 else width
}
// 200 is about how much space we need for the top navigation and close button at the
// bottom.
val tileSizeToFitHeight = (UncivGame.Current.worldScreen!!.stage.height - 200) / tileRows
val tileSizeToFitWidth = UncivGame.Current.worldScreen!!.stage.width / tileColumns
return min(tileSizeToFitHeight, tileSizeToFitWidth)
}
private fun createReplayMap(tileSize: Float): List<MinimapTile> {
val tiles = ArrayList<MinimapTile>()
for (tile in tileMap.values) {
val minimapTile = MinimapTile(tile, tileSize) {}
tiles.add(minimapTile)
}
return tiles
}
fun update(turn: Int) {
for (minimapTile in minimapTiles) {
minimapTile.updateColor(false, turn)
minimapTile.updateBorders(turn).updateActorsIn(this)
minimapTile.updateCityCircle(turn).updateActorsIn(this)
}
}
// For debugging purposes
override fun draw(batch: Batch?, parentAlpha: Float) = super.draw(batch, parentAlpha)
}

View File

@ -1,22 +1,25 @@
package com.unciv.ui.screens.victoryscreen package com.unciv.ui.screens.victoryscreen
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align import com.badlogic.gdx.utils.Align
import com.badlogic.gdx.utils.Timer
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.models.metadata.GameSetupInfo import com.unciv.models.metadata.GameSetupInfo
import com.unciv.models.ruleset.Victory import com.unciv.models.ruleset.Victory
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.images.ImageGetter import com.unciv.ui.components.YearTextUtil
import com.unciv.ui.screens.newgamescreen.NewGameScreen
import com.unciv.ui.screens.pickerscreens.PickerScreen
import com.unciv.ui.components.extensions.addSeparator import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.enable import com.unciv.ui.components.extensions.enable
import com.unciv.ui.components.extensions.onClick import com.unciv.ui.components.extensions.onClick
import com.unciv.ui.components.extensions.toLabel import com.unciv.ui.components.extensions.toLabel
import com.unciv.ui.components.extensions.toTextButton import com.unciv.ui.components.extensions.toTextButton
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.newgamescreen.NewGameScreen
import com.unciv.ui.screens.pickerscreens.PickerScreen
import com.unciv.ui.screens.worldscreen.WorldScreen import com.unciv.ui.screens.worldscreen.WorldScreen
class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() { class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
@ -27,6 +30,8 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
private val contentsTable = Table() private val contentsTable = Table()
private var replayTimer : Timer.Task? = null
init { init {
val difficultyLabel = ("{Difficulty}: {${gameInfo.difficulty}}").toLabel() val difficultyLabel = ("{Difficulty}: {${gameInfo.difficulty}}").toLabel()
difficultyLabel.setPosition(10f, stage.height - 10, Align.topLeft) difficultyLabel.setPosition(10f, stage.height - 10, Align.topLeft)
@ -41,9 +46,6 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
val rankingLabel = if (UncivGame.Current.settings.useDemographics) "Demographics" else "Rankings" val rankingLabel = if (UncivGame.Current.settings.useDemographics) "Demographics" else "Rankings"
val setCivRankingsButton = rankingLabel.toTextButton().onClick { setCivRankingsTable() } val setCivRankingsButton = rankingLabel.toTextButton().onClick { setCivRankingsTable() }
tabsTable.add(setCivRankingsButton) tabsTable.add(setCivRankingsButton)
topTable.add(tabsTable)
topTable.addSeparator()
topTable.add(contentsTable)
if (playerCivInfo.isSpectator()) if (playerCivInfo.isSpectator())
setGlobalVictoryTable() setGlobalVictoryTable()
@ -72,6 +74,16 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
} else if (!someoneHasWon) { } else if (!someoneHasWon) {
setDefaultCloseAction() setDefaultCloseAction()
} }
if (playerCivInfo.isSpectator() || someoneHasWon || playerCivInfo.isDefeated()) {
val replayLabel = "Replay"
val replayButton = replayLabel.toTextButton().onClick { setReplayTable() }
tabsTable.add(replayButton)
}
topTable.add(tabsTable)
topTable.addSeparator()
topTable.add(contentsTable)
} }
@ -121,7 +133,7 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
ourVictoryStatusTable.add(victory.value.victoryScreenHeader.toLabel()) ourVictoryStatusTable.add(victory.value.victoryScreenHeader.toLabel())
} }
contentsTable.clear() resetContent()
contentsTable.add(ourVictoryStatusTable) contentsTable.add(ourVictoryStatusTable)
} }
@ -156,7 +168,7 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
globalVictoryTable.add(getGlobalVictoryColumn(majorCivs, victory.key)) globalVictoryTable.add(getGlobalVictoryColumn(majorCivs, victory.key))
} }
contentsTable.clear() resetContent()
contentsTable.add(globalVictoryTable) contentsTable.add(globalVictoryTable)
} }
@ -181,12 +193,47 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
private fun setCivRankingsTable() { private fun setCivRankingsTable() {
val majorCivs = gameInfo.civilizations.filter { it.isMajorCiv() } val majorCivs = gameInfo.civilizations.filter { it.isMajorCiv() }
contentsTable.clear() resetContent()
if (UncivGame.Current.settings.useDemographics) contentsTable.add(buildDemographicsTable(majorCivs)) if (UncivGame.Current.settings.useDemographics) contentsTable.add(buildDemographicsTable(majorCivs))
else contentsTable.add(buildRankingsTable(majorCivs)) else contentsTable.add(buildRankingsTable(majorCivs))
} }
private fun setReplayTable() {
val replayTable = Table().apply { defaults().pad(10f) }
val yearLabel = "".toLabel()
replayTable.add(yearLabel).row()
val replayMap = ReplayMap(gameInfo.tileMap)
replayTable.add(replayMap).row()
var nextTurn = gameInfo.historyStartTurn
val finalTurn = gameInfo.turns
resetContent()
replayTimer = Timer.schedule(
object : Timer.Task() {
override fun run() {
updateReplayTable(yearLabel, replayMap, nextTurn++)
}
}, 0.0f,
// A game of 600 rounds will take one minute.
0.1f,
// End at the last turn.
finalTurn - nextTurn
)
contentsTable.add(replayTable)
}
private fun updateReplayTable(yearLabel: Label, replayMap: ReplayMap, turn: Int) {
val finalTurn = gameInfo.turns
val year = gameInfo.getYear(turn - finalTurn)
yearLabel.setText(
YearTextUtil.toYearText(
year, gameInfo.currentPlayerCiv.isLongCountDisplay()
)
)
replayMap.update(turn)
}
enum class RankLabels { Rank, Value, Best, Average, Worst} enum class RankLabels { Rank, Value, Best, Average, Worst}
private fun buildDemographicsTable(majorCivs: List<Civilization>): Table { private fun buildDemographicsTable(majorCivs: List<Civilization>): Table {
val demographicsTable = Table().apply { defaults().pad(5f) } val demographicsTable = Table().apply { defaults().pad(5f) }
@ -291,4 +338,14 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
civGroup.pack() civGroup.pack()
return civGroup return civGroup
} }
private fun resetContent() {
replayTimer?.cancel()
contentsTable.clear()
}
override fun dispose() {
super.dispose()
replayTimer?.cancel()
}
} }

View File

@ -17,6 +17,7 @@ import com.unciv.models.translations.tr
import com.unciv.ui.components.Fonts import com.unciv.ui.components.Fonts
import com.unciv.ui.components.MayaCalendar import com.unciv.ui.components.MayaCalendar
import com.unciv.ui.components.UncivTooltip.Companion.addTooltip import com.unciv.ui.components.UncivTooltip.Companion.addTooltip
import com.unciv.ui.components.YearTextUtil
import com.unciv.ui.components.extensions.colorFromRGB import com.unciv.ui.components.extensions.colorFromRGB
import com.unciv.ui.components.extensions.darken import com.unciv.ui.components.extensions.darken
import com.unciv.ui.components.extensions.onClick import com.unciv.ui.components.extensions.onClick
@ -34,7 +35,6 @@ import com.unciv.ui.screens.pickerscreens.PolicyPickerScreen
import com.unciv.ui.screens.pickerscreens.TechPickerScreen import com.unciv.ui.screens.pickerscreens.TechPickerScreen
import com.unciv.ui.screens.victoryscreen.VictoryScreen import com.unciv.ui.screens.victoryscreen.VictoryScreen
import com.unciv.ui.screens.worldscreen.mainmenu.WorldScreenMenuPopup import com.unciv.ui.screens.worldscreen.mainmenu.WorldScreenMenuPopup
import kotlin.math.abs
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -340,11 +340,10 @@ class WorldScreenTopBar(val worldScreen: WorldScreen) : Table() {
} }
private fun updateResourcesTable(civInfo: Civilization) { private fun updateResourcesTable(civInfo: Civilization) {
val year = civInfo.gameInfo.getYear() val yearText = YearTextUtil.toYearText(
val yearText = if (civInfo.isLongCountDisplay()) MayaCalendar.yearToMayaDate(year) civInfo.gameInfo.getYear(), civInfo.isLongCountDisplay()
else "[" + abs(year) + "] " + (if (year < 0) "BC" else "AD") )
turnsLabel.setText(Fonts.turn + "" + civInfo.gameInfo.turns + " | " + yearText.tr()) turnsLabel.setText(Fonts.turn + "" + civInfo.gameInfo.turns + " | " + yearText)
resourcesWrapper.clearChildren() resourcesWrapper.clearChildren()
var firstPadLeft = 20f // We want a distance from the turns entry to the first resource, but only if any resource is displayed var firstPadLeft = 20f // We want a distance from the turns entry to the first resource, but only if any resource is displayed
val civResources = civInfo.getCivResources() val civResources = civInfo.getCivResources()

View File

@ -32,11 +32,6 @@ class Minimap(val mapHolder: WorldMapHolder, minimapSize: Int, private val civIn
// don't try to resize rotate etc - this table has a LOT of children so that's valuable render time! // don't try to resize rotate etc - this table has a LOT of children so that's valuable render time!
isTransform = false isTransform = false
var topX = -Float.MAX_VALUE
var topY = -Float.MAX_VALUE
var bottomX = Float.MAX_VALUE
var bottomY = Float.MAX_VALUE
// Set fixed minimap size // Set fixed minimap size
val stageMinimapSize = calcMinimapSize(minimapSize) val stageMinimapSize = calcMinimapSize(minimapSize)
setSize(stageMinimapSize.x, stageMinimapSize.y) setSize(stageMinimapSize.x, stageMinimapSize.y)
@ -44,25 +39,19 @@ class Minimap(val mapHolder: WorldMapHolder, minimapSize: Int, private val civIn
// Calculate max tileSize to fit in mimimap // Calculate max tileSize to fit in mimimap
tileSize = calcTileSize(stageMinimapSize) tileSize = calcTileSize(stageMinimapSize)
minimapTiles = createMinimapTiles(tileSize) minimapTiles = createMinimapTiles(tileSize)
for (image in minimapTiles.map { it.image }) {
tileLayer.addActor(image)
// keeps track of the current top/bottom/left/rightmost tiles to size and position the minimap correctly
topX = max(topX, image.x + tileSize)
topY = max(topY, image.y + tileSize)
bottomX = min(bottomX, image.x)
bottomY = min(bottomY, image.y)
}
val tileExtension = MinimapTileUtil.spreadOutMinimapTiles(tileLayer, minimapTiles, tileSize)
// there are tiles "below the zero", // there are tiles "below the zero",
// so we zero out the starting position of the whole board so they will be displayed as well // so we zero out the starting position of the whole board so they will be displayed as well
tileLayer.setSize(width, height) tileLayer.setSize(width, height)
// Center tiles in minimap holder // Center tiles in minimap holder
tileMapWidth = topX - bottomX tileMapWidth = tileExtension.width
tileMapHeight = topY - bottomY tileMapHeight = tileExtension.height
val padX = (stageMinimapSize.x - tileMapWidth) * 0.5f - bottomX val padX =
val padY = (stageMinimapSize.y - tileMapHeight) * 0.5f - bottomY (stageMinimapSize.x - tileMapWidth) * 0.5f - (tileExtension.x)
val padY =
(stageMinimapSize.y - tileMapHeight) * 0.5f - (tileExtension.y)
for (group in tileLayer.children) { for (group in tileLayer.children) {
group.moveBy(padX, padY) group.moveBy(padX, padY)
} }

View File

@ -8,6 +8,7 @@ import com.badlogic.gdx.utils.Align
import com.unciv.logic.map.HexMath import com.unciv.logic.map.HexMath
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.tile.Tile import com.unciv.logic.map.tile.Tile
import com.unciv.logic.map.tile.TileHistory.TileHistoryState.CityCenterType
import com.unciv.ui.images.IconCircleGroup import com.unciv.ui.images.IconCircleGroup
import com.unciv.ui.images.ImageGetter import com.unciv.ui.images.ImageGetter
import com.unciv.ui.components.extensions.onClick import com.unciv.ui.components.extensions.onClick
@ -16,7 +17,7 @@ import com.unciv.utils.DebugUtils
import kotlin.math.PI import kotlin.math.PI
import kotlin.math.atan import kotlin.math.atan
internal class MinimapTile(val tile: Tile, tileSize: Float, val onClick: () -> Unit) { class MinimapTile(val tile: Tile, tileSize: Float, val onClick: () -> Unit) {
val image: Image = ImageGetter.getImage("OtherIcons/Hexagon") val image: Image = ImageGetter.getImage("OtherIcons/Hexagon")
private var cityCircleImage: IconCircleGroup? = null private var cityCircleImage: IconCircleGroup? = null
var owningCiv: Civilization? = null var owningCiv: Civilization? = null
@ -35,12 +36,15 @@ internal class MinimapTile(val tile: Tile, tileSize: Float, val onClick: () -> U
image.onClick(onClick) image.onClick(onClick)
} }
fun updateColor(isTileUnrevealed: Boolean) { fun updateColor(isTileUnrevealed: Boolean, turn: Int? = null) {
image.isVisible = DebugUtils.VISIBLE_MAP || !isTileUnrevealed image.isVisible = DebugUtils.VISIBLE_MAP || !isTileUnrevealed
if (!image.isVisible) return if (!image.isVisible) return
val isCityCenter =
if (turn == null) tile.isCityCenter() else tile.history.getState(turn).cityCenterType != CityCenterType.None
val owningCiv = if (turn == null) tile.getOwner() else getOwningCivFromHistory(tile, turn)
image.color = when { image.color = when {
tile.isCityCenter() && !tile.isWater -> tile.getOwner()!!.nation.getInnerColor() isCityCenter && !tile.isWater -> owningCiv!!.nation.getInnerColor()
tile.getCity() != null && !tile.isWater -> tile.getOwner()!!.nation.getOuterColor() owningCiv != null && !tile.isWater -> owningCiv.nation.getOuterColor()
else -> tile.getBaseTerrain().getColor().lerp(Color.GRAY, 0.5f) else -> tile.getBaseTerrain().getColor().lerp(Color.GRAY, 0.5f)
} }
} }
@ -52,16 +56,25 @@ internal class MinimapTile(val tile: Tile, tileSize: Float, val onClick: () -> U
} }
} }
fun updateBorders(): ActorChange { fun updateBorders(turn: Int? = null): ActorChange {
val owningCiv = if (turn == null) tile.getOwner() else getOwningCivFromHistory(tile, turn)
val imagesBefore = neighborToBorderImage.values.toSet() val imagesBefore = neighborToBorderImage.values.toSet()
for (neighbor in tile.neighbors) { for (neighbor in tile.neighbors) {
val shouldHaveBorderDisplayed = tile.getOwner() != null val neighborOwningCiv =
&& neighbor.getOwner() != tile.getOwner() if (turn == null) neighbor.getOwner() else getOwningCivFromHistory(
neighbor,
turn
)
val shouldHaveBorderDisplayed = owningCiv != null
&& neighborOwningCiv != owningCiv
if (!shouldHaveBorderDisplayed) { if (!shouldHaveBorderDisplayed) {
neighborToBorderImage.remove(neighbor) neighborToBorderImage.remove(neighbor)
continue continue
} }
if (neighbor in neighborToBorderImage) continue if (neighbor in neighborToBorderImage) {
neighborToBorderImage[neighbor]!!.color = owningCiv!!.nation.getInnerColor()
continue
}
val borderImage = ImageGetter.getWhiteDot() val borderImage = ImageGetter.getWhiteDot()
@ -86,18 +99,34 @@ internal class MinimapTile(val tile: Tile, tileSize: Float, val onClick: () -> U
-relativeWorldPosition.y * hexagonEdgeLength / 2 -relativeWorldPosition.y * hexagonEdgeLength / 2
) )
borderImage.rotateBy(angle) borderImage.rotateBy(angle)
borderImage.color = tile.getOwner()!!.nation.getInnerColor() borderImage.color = owningCiv!!.nation.getInnerColor()
neighborToBorderImage[neighbor] = borderImage neighborToBorderImage[neighbor] = borderImage
} }
val imagesAfter = neighborToBorderImage.values.toSet() val imagesAfter = neighborToBorderImage.values.toSet()
return ActorChange(imagesBefore - imagesAfter, imagesAfter - imagesBefore) return ActorChange(imagesBefore - imagesAfter, imagesAfter - imagesBefore)
} }
fun updateCityCircle(): ActorChange { fun updateCityCircle(turn: Int? = null): ActorChange {
val prevCircle = cityCircleImage val prevCircle = cityCircleImage
val owningCiv = if (turn == null) tile.getOwner() else getOwningCivFromHistory(tile, turn)
val isCityCenter =
if (turn == null) tile.isCityCenter() else tile.history.getState(turn).cityCenterType != CityCenterType.None
val nation = tile.getOwner()!!.nation if (owningCiv == null || !isCityCenter) {
val nationIconSize = (if (tile.getCity()!!.isCapital() && tile.getOwner()!!.isMajorCiv()) 1.667f else 1.25f) * image.width return ActorChange(
if (prevCircle != null) setOf(prevCircle) else emptySet(),
emptySet()
)
}
val nation = owningCiv.nation
val isCapital =
if (turn == null)
tile.getCity()!!.isCapital()
else
tile.history.getState(turn).cityCenterType ==
CityCenterType.Capital
val nationIconSize = (if (isCapital && owningCiv.isMajorCiv()) 1.667f else 1.25f) * image.width
val cityCircle = ImageGetter.getCircle().apply { color = nation.getInnerColor() } val cityCircle = ImageGetter.getCircle().apply { color = nation.getInnerColor() }
.surroundWithCircle(nationIconSize, color = nation.getOuterColor()) .surroundWithCircle(nationIconSize, color = nation.getOuterColor())
val hexCenterXPosition = image.x + image.width / 2 val hexCenterXPosition = image.x + image.width / 2
@ -109,4 +138,12 @@ internal class MinimapTile(val tile: Tile, tileSize: Float, val onClick: () -> U
return ActorChange(if (prevCircle != null) setOf(prevCircle) else emptySet(), setOf(cityCircle)) return ActorChange(if (prevCircle != null) setOf(prevCircle) else emptySet(), setOf(cityCircle))
} }
fun getOwningCivFromHistory(tile: Tile, turn: Int) : Civilization? {
val owningCivName = tile.history.getState(turn).owningCivName
return if (owningCivName == null) null else tile.tileMap.gameInfo.getCivilization(
owningCivName
)
}
} }

View File

@ -0,0 +1,28 @@
package com.unciv.ui.screens.worldscreen.minimap
import com.badlogic.gdx.scenes.scene2d.Group
import java.awt.geom.Rectangle2D
import kotlin.math.max
import kotlin.math.min
object MinimapTileUtil {
fun spreadOutMinimapTiles(tileLayer: Group, tiles: List<MinimapTile>, tileSize: Float) : Rectangle2D.Float {
var topX = -Float.MAX_VALUE
var topY = -Float.MAX_VALUE
var bottomX = Float.MAX_VALUE
var bottomY = Float.MAX_VALUE
for (image in tiles.map { it.image }) {
tileLayer.addActor(image)
// keeps track of the current top/bottom/left/rightmost tiles to size and position the minimap correctly
topX = max(topX, image.x + tileSize)
topY = max(topY, image.y + tileSize)
bottomX = min(bottomX, image.x)
bottomY = min(bottomY, image.y)
}
return Rectangle2D.Float(bottomX, bottomY, topX-bottomX, topY-bottomY)
}
}