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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 374 additions and 46 deletions

View File

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

View File

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

View File

@ -207,4 +207,12 @@ object BackwardCompatibility {
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.guaranteeUnitPromotions
import com.unciv.logic.BackwardCompatibility.migrateBarbarianCamps
import com.unciv.logic.BackwardCompatibility.migrateToTileHistory
import com.unciv.logic.BackwardCompatibility.removeMissingModReferences
import com.unciv.logic.GameInfo.Companion.CURRENT_COMPATIBILITY_NUMBER
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
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.
*/
@ -164,6 +176,7 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
toReturn.oneMoreTurnMode = oneMoreTurnMode
toReturn.customSaveLocation = customSaveLocation
toReturn.victoryData = victoryData
toReturn.historyStartTurn = historyStartTurn
return toReturn
}
@ -591,6 +604,8 @@ class GameInfo : IsPartOfGameInfoSerialization, HasGameInfoSerializationVersion
cityDistances.game = this
guaranteeUnitPromotions()
migrateToTileHistory()
}
//endregion

View File

@ -98,6 +98,8 @@ object GameStarter {
}
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.setStartingLocationsTransients()

View File

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

View File

@ -2,7 +2,6 @@
import com.unciv.Constants
import com.unciv.GUI
import com.unciv.UncivGame
import com.unciv.logic.battle.Battle
import com.unciv.logic.city.City
import com.unciv.logic.city.CityFlags
@ -324,6 +323,11 @@ class CityInfoConquestFunctions(val city: City){
// Update proximity rankings
civ.updateProximity(oldCiv,
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 hasBottomLeftRiver = false
var history: TileHistory = TileHistory()
private var continent = -1
val latitude: Float
@ -169,6 +171,7 @@ open class Tile : IsPartOfGameInfoSerialization {
toReturn.hasBottomRiver = hasBottomRiver
toReturn.continent = continent
toReturn.exploredBy.addAll(exploredBy)
toReturn.history = history.clone()
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
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.utils.Align
import com.badlogic.gdx.utils.Timer
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.civilization.Civilization
import com.unciv.models.metadata.GameSetupInfo
import com.unciv.models.ruleset.Victory
import com.unciv.models.translations.tr
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.screens.newgamescreen.NewGameScreen
import com.unciv.ui.screens.pickerscreens.PickerScreen
import com.unciv.ui.components.YearTextUtil
import com.unciv.ui.components.extensions.addSeparator
import com.unciv.ui.components.extensions.enable
import com.unciv.ui.components.extensions.onClick
import com.unciv.ui.components.extensions.toLabel
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
class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
@ -27,6 +30,8 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
private val contentsTable = Table()
private var replayTimer : Timer.Task? = null
init {
val difficultyLabel = ("{Difficulty}: {${gameInfo.difficulty}}").toLabel()
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 setCivRankingsButton = rankingLabel.toTextButton().onClick { setCivRankingsTable() }
tabsTable.add(setCivRankingsButton)
topTable.add(tabsTable)
topTable.addSeparator()
topTable.add(contentsTable)
if (playerCivInfo.isSpectator())
setGlobalVictoryTable()
@ -72,6 +74,16 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
} else if (!someoneHasWon) {
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())
}
contentsTable.clear()
resetContent()
contentsTable.add(ourVictoryStatusTable)
}
@ -156,7 +168,7 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
globalVictoryTable.add(getGlobalVictoryColumn(majorCivs, victory.key))
}
contentsTable.clear()
resetContent()
contentsTable.add(globalVictoryTable)
}
@ -181,12 +193,47 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
private fun setCivRankingsTable() {
val majorCivs = gameInfo.civilizations.filter { it.isMajorCiv() }
contentsTable.clear()
resetContent()
if (UncivGame.Current.settings.useDemographics) contentsTable.add(buildDemographicsTable(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}
private fun buildDemographicsTable(majorCivs: List<Civilization>): Table {
val demographicsTable = Table().apply { defaults().pad(5f) }
@ -291,4 +338,14 @@ class VictoryScreen(val worldScreen: WorldScreen) : PickerScreen() {
civGroup.pack()
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.MayaCalendar
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.darken
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.victoryscreen.VictoryScreen
import com.unciv.ui.screens.worldscreen.mainmenu.WorldScreenMenuPopup
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.roundToInt
@ -340,11 +340,10 @@ class WorldScreenTopBar(val worldScreen: WorldScreen) : Table() {
}
private fun updateResourcesTable(civInfo: Civilization) {
val year = civInfo.gameInfo.getYear()
val yearText = if (civInfo.isLongCountDisplay()) MayaCalendar.yearToMayaDate(year)
else "[" + abs(year) + "] " + (if (year < 0) "BC" else "AD")
turnsLabel.setText(Fonts.turn + "" + civInfo.gameInfo.turns + " | " + yearText.tr())
val yearText = YearTextUtil.toYearText(
civInfo.gameInfo.getYear(), civInfo.isLongCountDisplay()
)
turnsLabel.setText(Fonts.turn + "" + civInfo.gameInfo.turns + " | " + yearText)
resourcesWrapper.clearChildren()
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()

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!
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
val stageMinimapSize = calcMinimapSize(minimapSize)
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
tileSize = calcTileSize(stageMinimapSize)
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",
// so we zero out the starting position of the whole board so they will be displayed as well
tileLayer.setSize(width, height)
// Center tiles in minimap holder
tileMapWidth = topX - bottomX
tileMapHeight = topY - bottomY
val padX = (stageMinimapSize.x - tileMapWidth) * 0.5f - bottomX
val padY = (stageMinimapSize.y - tileMapHeight) * 0.5f - bottomY
tileMapWidth = tileExtension.width
tileMapHeight = tileExtension.height
val padX =
(stageMinimapSize.x - tileMapWidth) * 0.5f - (tileExtension.x)
val padY =
(stageMinimapSize.y - tileMapHeight) * 0.5f - (tileExtension.y)
for (group in tileLayer.children) {
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.civilization.Civilization
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.ImageGetter
import com.unciv.ui.components.extensions.onClick
@ -16,7 +17,7 @@ import com.unciv.utils.DebugUtils
import kotlin.math.PI
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")
private var cityCircleImage: IconCircleGroup? = null
var owningCiv: Civilization? = null
@ -35,12 +36,15 @@ internal class MinimapTile(val tile: Tile, tileSize: Float, val onClick: () -> U
image.onClick(onClick)
}
fun updateColor(isTileUnrevealed: Boolean) {
fun updateColor(isTileUnrevealed: Boolean, turn: Int? = null) {
image.isVisible = DebugUtils.VISIBLE_MAP || !isTileUnrevealed
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 {
tile.isCityCenter() && !tile.isWater -> tile.getOwner()!!.nation.getInnerColor()
tile.getCity() != null && !tile.isWater -> tile.getOwner()!!.nation.getOuterColor()
isCityCenter && !tile.isWater -> owningCiv!!.nation.getInnerColor()
owningCiv != null && !tile.isWater -> owningCiv.nation.getOuterColor()
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()
for (neighbor in tile.neighbors) {
val shouldHaveBorderDisplayed = tile.getOwner() != null
&& neighbor.getOwner() != tile.getOwner()
val neighborOwningCiv =
if (turn == null) neighbor.getOwner() else getOwningCivFromHistory(
neighbor,
turn
)
val shouldHaveBorderDisplayed = owningCiv != null
&& neighborOwningCiv != owningCiv
if (!shouldHaveBorderDisplayed) {
neighborToBorderImage.remove(neighbor)
continue
}
if (neighbor in neighborToBorderImage) continue
if (neighbor in neighborToBorderImage) {
neighborToBorderImage[neighbor]!!.color = owningCiv!!.nation.getInnerColor()
continue
}
val borderImage = ImageGetter.getWhiteDot()
@ -86,18 +99,34 @@ internal class MinimapTile(val tile: Tile, tileSize: Float, val onClick: () -> U
-relativeWorldPosition.y * hexagonEdgeLength / 2
)
borderImage.rotateBy(angle)
borderImage.color = tile.getOwner()!!.nation.getInnerColor()
borderImage.color = owningCiv!!.nation.getInnerColor()
neighborToBorderImage[neighbor] = borderImage
}
val imagesAfter = neighborToBorderImage.values.toSet()
return ActorChange(imagesBefore - imagesAfter, imagesAfter - imagesBefore)
}
fun updateCityCircle(): ActorChange {
fun updateCityCircle(turn: Int? = null): ActorChange {
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
val nationIconSize = (if (tile.getCity()!!.isCapital() && tile.getOwner()!!.isMajorCiv()) 1.667f else 1.25f) * image.width
if (owningCiv == null || !isCityCenter) {
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() }
.surroundWithCircle(nationIconSize, color = nation.getOuterColor())
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))
}
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)
}
}