StartingLocation-Improvements-be-gone phase 1 (#4951)

This commit is contained in:
SomeTroglodyte
2021-08-23 10:58:42 +02:00
committed by GitHub
parent 55f2bca9c7
commit b4ad34988c
15 changed files with 454 additions and 267 deletions

View File

@ -354,8 +354,7 @@ class GameInfo {
tile.terrainFeatures.remove(terrainFeature)
if (tile.resource != null && !ruleSet.tileResources.containsKey(tile.resource!!))
tile.resource = null
if (tile.improvement != null && !ruleSet.tileImprovements.containsKey(tile.improvement!!)
&& !tile.improvement!!.startsWith("StartingLocation ")) // To not remove the starting locations in GameStarter.startNewGame()
if (tile.improvement != null && !ruleSet.tileImprovements.containsKey(tile.improvement!!))
tile.improvement = null
for (unit in tile.getUnits()) {

View File

@ -20,62 +20,87 @@ import kotlin.collections.HashMap
import kotlin.math.max
object GameStarter {
// temporary instrumentation while tuning/debugging
private const val consoleOutput = true
private const val consoleTimings = true
fun startNewGame(gameSetupInfo: GameSetupInfo): GameInfo {
if (consoleOutput || consoleTimings)
println("\nGameStarter run with parameters ${gameSetupInfo.gameParameters}, map ${gameSetupInfo.mapParameters}")
val gameInfo = GameInfo()
lateinit var tileMap: TileMap
gameInfo.gameParameters = gameSetupInfo.gameParameters
val ruleset = RulesetCache.getComplexRuleset(gameInfo.gameParameters.mods)
if (gameSetupInfo.mapParameters.name != "") {
gameInfo.tileMap = MapSaver.loadMap(gameSetupInfo.mapFile!!)
if (gameSetupInfo.mapParameters.name != "") runAndMeasure("loadMap") {
tileMap = MapSaver.loadMap(gameSetupInfo.mapFile!!)
// Don't override the map parameters - this can include if we world wrap or not!
} else {
gameInfo.tileMap = MapGenerator(ruleset).generateMap(gameSetupInfo.mapParameters)
gameInfo.tileMap.mapParameters = gameSetupInfo.mapParameters
} else runAndMeasure("generateMap") {
tileMap = MapGenerator(ruleset).generateMap(gameSetupInfo.mapParameters)
tileMap.mapParameters = gameSetupInfo.mapParameters
}
runAndMeasure("addCivilizations") {
gameInfo.tileMap = tileMap
tileMap.gameInfo = gameInfo // need to set this transient before placing units in the map
addCivilizations(gameSetupInfo.gameParameters, gameInfo, ruleset) // this is before gameInfo.setTransients, so gameInfo doesn't yet have the gameBasics
}
gameInfo.tileMap.gameInfo = gameInfo // need to set this transient before placing units in the map
addCivilizations(gameSetupInfo.gameParameters, gameInfo, ruleset) // this is before gameInfo.setTransients, so gameInfo doesn't yet have the gameBasics
runAndMeasure("Remove units") {
// Remove units for civs that aren't in this game
for (tile in tileMap.values)
for (unit in tile.getUnits())
if (gameInfo.civilizations.none { it.civName == unit.owner }) {
unit.currentTile = tile
unit.setTransients(ruleset)
unit.removeFromTile()
}
}
// Remove units for civs that aren't in this game
for (tile in gameInfo.tileMap.values)
for (unit in tile.getUnits())
if (gameInfo.civilizations.none { it.civName == unit.owner }) {
unit.currentTile = tile
unit.setTransients(ruleset)
unit.removeFromTile()
}
runAndMeasure("setTransients") {
tileMap.setTransients(ruleset) // if we're starting from a map with pre-placed units, they need the civs to exist first
tileMap.setStartingLocationsTransients()
gameInfo.tileMap.setTransients(ruleset) // if we're starting from a map with preplaced units, they need the civs to exist first
gameInfo.difficulty = gameSetupInfo.gameParameters.difficulty
gameInfo.difficulty = gameSetupInfo.gameParameters.difficulty
gameInfo.setTransients() // needs to be before placeBarbarianUnit because it depends on the tilemap having its gameInfo set
}
runAndMeasure("Techs and Stats") {
addCivTechs(gameInfo, ruleset, gameSetupInfo)
gameInfo.setTransients() // needs to be before placeBarbarianUnit because it depends on the tilemap having its gameinfo set
addCivStats(gameInfo)
}
addCivTechs(gameInfo, ruleset, gameSetupInfo)
addCivStats(gameInfo)
// and only now do we add units for everyone, because otherwise both the gameInfo.setTransients() and the placeUnit will both add the unit to the civ's unit list!
addCivStartingUnits(gameInfo)
runAndMeasure("addCivStartingUnits") {
// and only now do we add units for everyone, because otherwise both the gameInfo.setTransients() and the placeUnit will both add the unit to the civ's unit list!
addCivStartingUnits(gameInfo)
}
// remove starting locations once we're done
for (tile in gameInfo.tileMap.values) {
if (tile.improvement != null && tile.improvement!!.startsWith("StartingLocation "))
tile.improvement = null
// set max starting movement for units loaded from map
tileMap.clearStartingLocations()
// set max starting movement for units loaded from map
for (tile in tileMap.values) {
for (unit in tile.getUnits()) unit.currentMovement = unit.getMaxMovement().toFloat()
}
// This triggers the one-time greeting from Nation.startIntroPart1/2
addPlayerIntros(gameInfo)
return gameInfo
}
private fun runAndMeasure(text: String, action: ()->Unit) {
if (!consoleTimings) return action()
val startNanos = System.nanoTime()
action()
val delta = System.nanoTime() - startNanos
println("GameStarter.$text took ${delta/1000000L}.${(delta/10000L).rem(100)}ms")
}
private fun addPlayerIntros(gameInfo: GameInfo) {
gameInfo.civilizations.filter {
// isNotEmpty should also exclude a spectator
@ -138,6 +163,8 @@ object GameStarter {
availableCivNames.addAll(ruleset.nations.filter { it.value.isMajorCiv() }.keys.shuffled())
availableCivNames.removeAll(newGameParameters.players.map { it.chosenCiv })
availableCivNames.remove(Constants.barbarians)
val startingTechs = ruleset.technologies.values.filter { it.uniques.contains("Starting tech") }
if (!newGameParameters.noBarbarians && ruleset.nations.containsKey(Constants.barbarians)) {
val barbarianCivilization = CivilizationInfo(Constants.barbarians)
@ -149,44 +176,36 @@ object GameStarter {
else availableCivNames.pop()
val playerCiv = CivilizationInfo(nationName)
for (tech in ruleset.technologies.values.filter { it.uniques.contains("Starting tech") })
for (tech in startingTechs)
playerCiv.tech.techsResearched.add(tech.name) // can't be .addTechnology because the civInfo isn't assigned yet
playerCiv.playerType = player.playerType
playerCiv.playerId = player.playerId
gameInfo.civilizations.add(playerCiv)
}
val cityStatesWithStartingLocations =
gameInfo.tileMap.values
.filter { it.improvement != null && it.improvement!!.startsWith("StartingLocation ") }
.map { it.improvement!!.replace("StartingLocation ", "") }
val civNamesWithStartingLocations = gameInfo.tileMap.startingLocationsByNation.keys
val availableCityStatesNames = Stack<String>()
// since we shuffle and then order by, we end up with all the City-States with starting tiles first in a random order,
// and then all the other City-States in a random order! Because the sortedBy function is stable!
availableCityStatesNames.addAll(ruleset.nations.filter { it.value.isCityState() }.keys
.shuffled().sortedByDescending { it in cityStatesWithStartingLocations })
.shuffled().sortedByDescending { it in civNamesWithStartingLocations })
val unusedMercantileResources = ruleset.tileResources.values.filter { it.unique == "Can only be created by Mercantile City-States" }.toMutableList()
val allMercantileResources = ruleset.tileResources.values.filter { it.unique == "Can only be created by Mercantile City-States" }.map { it.name }
val unusedMercantileResources = Stack<String>()
unusedMercantileResources.addAll(allMercantileResources.shuffled())
for (cityStateName in availableCityStatesNames.take(newGameParameters.numberOfCityStates)) {
val civ = CivilizationInfo(cityStateName)
civ.cityStatePersonality = CityStatePersonality.values().random()
if (ruleset.nations[cityStateName]?.cityStateType == CityStateType.Mercantile) {
if (!ruleset.tileResources.values.any { it.unique == "Can only be created by Mercantile City-States" }) {
civ.cityStateResource = null
} else if (unusedMercantileResources.isNotEmpty()) {
// First pick an unused luxury if possible
val unusedResource = unusedMercantileResources.random()
civ.cityStateResource = unusedResource.name
unusedMercantileResources.remove(unusedResource)
} else {
// Then random
civ.cityStateResource = ruleset.tileResources.values.filter { it.unique == "Can only be created by Mercantile City-States" }.random().name
}
civ.cityStateResource = when {
ruleset.nations[cityStateName]?.cityStateType != CityStateType.Mercantile -> null
allMercantileResources.isEmpty() -> null
unusedMercantileResources.empty() -> allMercantileResources.random() // When unused luxuries exhausted, random
else -> unusedMercantileResources.pop() // First pick an unused luxury if possible
}
gameInfo.civilizations.add(civ)
for (tech in ruleset.technologies.values.filter { it.uniques.contains("Starting tech") })
for (tech in startingTechs)
civ.tech.techsResearched.add(tech.name) // can't be .addTechnology because the civInfo isn't assigned yet
}
}
@ -194,38 +213,35 @@ object GameStarter {
private fun addCivStartingUnits(gameInfo: GameInfo) {
val ruleSet = gameInfo.ruleSet
val tileMap = gameInfo.tileMap
val startingEra = gameInfo.gameParameters.startingEra
var startingUnits: MutableList<String>
var eraUnitReplacement: String
val startScores = HashMap<TileInfo, Float>()
for (tile in gameInfo.tileMap.values) {
val startScores = HashMap<TileInfo, Float>(tileMap.values.size)
for (tile in tileMap.values) {
startScores[tile] = tile.getTileStartScore()
}
// First we get start locations for the major civs, on the second pass the city states (without predetermined starts) can squeeze in wherever
// I hear copying code is good
val cityStatesWithStartingLocations =
gameInfo.tileMap.values
.filter { it.improvement != null && it.improvement!!.startsWith("StartingLocation ") }
.map { it.improvement!!.replace("StartingLocation ", "") }
val bestCivs = gameInfo.civilizations.filter { !it.isBarbarian() && (!it.isCityState() || it.civName in cityStatesWithStartingLocations) }
val bestLocations = getStartingLocations(bestCivs, gameInfo.tileMap, startScores)
for (civ in bestCivs)
{
if (civ.isCityState()) // Already have explicit starting locations
val civNamesWithStartingLocations = tileMap.startingLocationsByNation.keys
val bestCivs = gameInfo.civilizations.filter { !it.isBarbarian() && (!it.isCityState() || it.civName in civNamesWithStartingLocations) }
val bestLocations = getStartingLocations(bestCivs, tileMap, startScores)
for ((civ, tile) in bestLocations) {
if (civ.civName in civNamesWithStartingLocations) // Already have explicit starting locations
continue
// Mark the best start locations so we remember them for the second pass
bestLocations[civ]!!.improvement = "StartingLocation " + civ.civName
tileMap.addStartingLocation(civ.civName, tile)
}
val startingLocations = getStartingLocations(
gameInfo.civilizations.filter { !it.isBarbarian() },
gameInfo.tileMap, startScores)
tileMap, startScores)
val settlerLikeUnits = ruleSet.units.filter {
it.value.uniqueObjects.any { it.placeholderText == Constants.settlerUnique }
it.value.uniqueObjects.any { unique -> unique.placeholderText == Constants.settlerUnique }
}
// no starting units for Barbarians and Spectators
@ -241,8 +257,7 @@ object GameStarter {
addCityStateLuxury(gameInfo, startingLocation)
for (tile in startingLocation.getTilesInDistance(3)) {
if (tile.improvement != null
&& !tile.improvement!!.startsWith("StartingLocation")
if (tile.improvement != null
&& tile.getTileImprovement()!!.isAncientRuinsEquivalent()
) {
tile.improvement = null // Remove ancient ruins in immediate vicinity
@ -305,7 +320,7 @@ object GameStarter {
}
if (unit == "Worker" && "Worker" !in ruleSet.units) {
val buildableWorkerLikeUnits = ruleSet.units.filter {
it.value.uniqueObjects.any { it.placeholderText == Constants.canBuildImprovements }
it.value.uniqueObjects.any { unique -> unique.placeholderText == Constants.canBuildImprovements }
&& it.value.isBuildable(civ)
&& it.value.isCivilian()
}
@ -353,18 +368,14 @@ object GameStarter {
landTilesInBigEnoughGroup.addAll(tilesInGroup)
}
val tilesWithStartingLocations = tileMap.values
.filter { it.improvement != null && it.improvement!!.startsWith("StartingLocation ") }
val civsOrderedByAvailableLocations = civs.shuffled() // Order should be random since it determines who gets best start
.sortedBy { civ ->
when {
tilesWithStartingLocations.any { it.improvement == "StartingLocation " + civ.civName } -> 1 // harshest requirements
civ.civName in tileMap.startingLocationsByNation -> 1 // harshest requirements
civ.nation.startBias.contains("Tundra") -> 2 // Tundra starts are hard to find, so let's do them first
civ.nation.startBias.isNotEmpty() -> 3 // less harsh
else -> 4
} // no requirements
else -> 4 // no requirements
}
}
for (minimumDistanceBetweenStartingLocations in tileMap.tileMatrix.size / 4 downTo 0) {
@ -375,7 +386,7 @@ object GameStarter {
val startingLocations = HashMap<CivilizationInfo, TileInfo>()
for (civ in civsOrderedByAvailableLocations) {
var startingLocation: TileInfo
val presetStartingLocation = tilesWithStartingLocations.firstOrNull { it.improvement == "StartingLocation " + civ.civName }
val presetStartingLocation = tileMap.startingLocationsByNation[civ.civName]?.randomOrNull() // in case map editor is extended to allow alternate starting locations for a nation
var distanceToNext = minimumDistanceBetweenStartingLocations
if (presetStartingLocation != null) startingLocation = presetStartingLocation
@ -389,11 +400,14 @@ object GameStarter {
var preferredTiles = freeTiles.toList()
for (startBias in civ.nation.startBias) {
if (startBias.startsWith("Avoid ")) {
val tileToAvoid = startBias.removePrefix("Avoid [").removeSuffix("]")
preferredTiles = preferredTiles.filter { !it.matchesTerrainFilter(tileToAvoid) }
} else if (startBias == Constants.coast) preferredTiles = preferredTiles.filter { it.isCoastalTile() }
else preferredTiles = preferredTiles.filter { it.matchesTerrainFilter(startBias) }
preferredTiles = when {
startBias.startsWith("Avoid [") -> {
val tileToAvoid = startBias.removePrefix("Avoid [").removeSuffix("]")
preferredTiles.filter { !it.matchesTerrainFilter(tileToAvoid) }
}
startBias == Constants.coast -> preferredTiles.filter { it.isCoastalTile() }
else -> preferredTiles.filter { it.matchesTerrainFilter(startBias) }
}
}
startingLocation = if (preferredTiles.isNotEmpty()) preferredTiles.last() else freeTiles.last()

View File

@ -10,20 +10,32 @@ object MapSaver {
fun json() = GameSaver.json()
private const val mapsFolder = "maps"
private const val saveZipped = false
private fun getMap(mapName:String) = Gdx.files.local("$mapsFolder/$mapName")
fun mapFromSavedString(mapString: String): TileMap {
val unzippedJson = try {
Gzip.unzip(mapString)
} catch (ex: Exception) {
mapString
}
return mapFromJson(unzippedJson)
}
fun mapToSavedString(tileMap: TileMap): String {
val mapJson = json().toJson(tileMap)
return if (saveZipped) Gzip.zip(mapJson) else mapJson
}
fun saveMap(mapName: String,tileMap: TileMap) {
getMap(mapName).writeString(Gzip.zip(json().toJson(tileMap)), false)
getMap(mapName).writeString(mapToSavedString(tileMap), false)
}
fun loadMap(mapFile:FileHandle):TileMap {
val gzippedString = mapFile.readString()
val unzippedJson = Gzip.unzip(gzippedString)
return json().fromJson(TileMap::class.java, unzippedJson)
return mapFromSavedString(mapFile.readString())
}
fun getMaps() = Gdx.files.local(mapsFolder).list()
fun getMaps(): Array<FileHandle> = Gdx.files.local(mapsFolder).list()
fun mapFromJson(json:String): TileMap = json().fromJson(TileMap::class.java, json)
private fun mapFromJson(json:String): TileMap = json().fromJson(TileMap::class.java, json)
}

View File

@ -158,5 +158,6 @@ class MapParameters {
}
// For debugging and MapGenerator console output
override fun toString() = "($mapSize ${if (worldWrap)"wrapped " else ""}$shape $type, Seed $seed, $elevationExponent/$temperatureExtremeness/$resourceRichness/$vegetationRichness/$rareFeaturesRichness/$maxCoastExtension/$tilesPerBiomeArea/$waterThreshold)"
override fun toString() = if (name.isNotEmpty()) "\"$name\""
else "($mapSize ${if (worldWrap)"wrapped " else ""}$shape $type, Seed $seed, $elevationExponent/$temperatureExtremeness/$resourceRichness/$vegetationRichness/$rareFeaturesRichness/$maxCoastExtension/$tilesPerBiomeArea/$waterThreshold)"
}

View File

@ -733,7 +733,6 @@ class MapUnit {
if (civInfo.isMajorCiv()
&& tile.improvement != null
&& !tile.improvement!!.startsWith("StartingLocation ")
&& tile.getTileImprovement()!!.isAncientRuinsEquivalent()
)
getAncientRuinBonus(tile)

View File

@ -659,8 +659,7 @@ open class TileInfo {
out.add("Terrain feature [$terrainFeature] does not exist in ruleset!")
if (resource != null && !ruleset.tileResources.containsKey(resource))
out.add("Resource [$resource] does not exist in ruleset!")
if (improvement != null && !improvement!!.startsWith("StartingLocation")
&& !ruleset.tileImprovements.containsKey(improvement))
if (improvement != null && !ruleset.tileImprovements.containsKey(improvement))
out.add("Improvement [$improvement] does not exist in ruleset!")
return out
}
@ -756,9 +755,9 @@ open class TileInfo {
roadStatus = RoadStatus.None
}
private fun normalizeTileImprovement(ruleset: Ruleset) {
if (improvement!!.startsWith("StartingLocation")) {
// This runs from map editor too, so the Pseudo-improvements for starting locations need to stay.
if (improvement!!.startsWith(TileMap.startingLocationPrefix)) {
if (!isLand || getLastTerrain().impassable) improvement = null
return
}

View File

@ -10,11 +10,47 @@ import com.unciv.models.ruleset.Nation
import com.unciv.models.ruleset.Ruleset
import kotlin.math.abs
/** An Unciv map with all properties as produced by the [map editor][com.unciv.ui.mapeditor.MapEditorScreen]
* or [MapGenerator][com.unciv.logic.map.mapgenerator.MapGenerator]; or as part of a running [game][GameInfo].
*
* Note: Will be Serialized -> Take special care with lateinit and lazy!
*/
class TileMap {
companion object {
const val startingLocationPrefix = "StartingLocation "
/**
* To be backwards compatible, a json without a startingLocations element will be recognized by an entry with this marker
* New saved maps will never have this marker and will always have a serialized startingLocations list even if empty.
* New saved maps will also never have "StartingLocation" improvements, these _must_ be converted before use anywhere outside map editor.
*/
private const val legacyMarker = " Legacy "
}
//region Fields, Serialized
var mapParameters = MapParameters()
private var tileList = ArrayList<TileInfo>()
/** Structure geared for simple serialization by Gdx.Json (which is a little blind to kotlin collections, especially HashSet)
* @param position [Vector2] of the location
* @param nation Name of the nation
*/
private data class StartingLocation(val position: Vector2 = Vector2.Zero, val nation: String = "")
private val startingLocations = arrayListOf(StartingLocation(Vector2.Zero, legacyMarker))
//endregion
//region Fields, Transient
/** Attention: lateinit will _stay uninitialized_ while in MapEditorScreen! */
@Transient
lateinit var gameInfo: GameInfo
/** Keep a copy of the [Ruleset] object passer to setTransients, for now only to allow subsequent setTransients without. Copied on [clone]. */
@Transient
var ruleset: Ruleset? = null
@Transient
var tileMatrix = ArrayList<ArrayList<TileInfo?>>() // this works several times faster than a hashmap, the performance difference is really astounding
@ -33,25 +69,31 @@ class TileMap {
@delegate:Transient
val naturalWonders: List<String> by lazy { tileList.asSequence().filter { it.isNaturalWonder() }.map { it.naturalWonder!! }.distinct().toList() }
var mapParameters = MapParameters()
private var tileList = ArrayList<TileInfo>()
// Excluded from Serialization by having no own backing field
val values: Collection<TileInfo>
get() = tileList
@Transient
val startingLocationsByNation = HashMap<String,HashSet<TileInfo>>()
//endregion
//region Constructors
/** for json parsing, we need to have a default constructor */
constructor()
/** generates an hexagonal map of given radius */
/** creates a hexagonal map of given radius (filled with grassland) */
constructor(radius: Int, ruleset: Ruleset, worldWrap: Boolean = false) {
startingLocations.clear()
for (vector in HexMath.getVectorsInDistance(Vector2.Zero, radius, worldWrap))
tileList.add(TileInfo().apply { position = vector; baseTerrain = Constants.grassland })
setTransients(ruleset)
}
/** generates a rectangular map of given width and height*/
/** creates a rectangular map of given width and height (filled with grassland) */
constructor(width: Int, height: Int, ruleset: Ruleset, worldWrap: Boolean = false) {
startingLocations.clear()
// world-wrap maps must always have an even width, so round down
val wrapAdjustedWidth = if (worldWrap && width % 2 != 0 ) width -1 else width
@ -67,39 +109,57 @@ class TileMap {
setTransients(ruleset)
}
//endregion
//region Operators and Standards
/** @return a deep-copy clone of the serializable fields, no transients initialized */
fun clone(): TileMap {
val toReturn = TileMap()
toReturn.tileList.addAll(tileList.map { it.clone() })
toReturn.mapParameters = mapParameters
toReturn.ruleset = ruleset
toReturn.startingLocations.clear()
toReturn.startingLocations.ensureCapacity(startingLocations.size)
toReturn.startingLocations.addAll(startingLocations)
return toReturn
}
operator fun contains(vector: Vector2) = contains(vector.x.toInt(), vector.y.toInt())
operator fun contains(vector: Vector2) =
contains(vector.x.toInt(), vector.y.toInt())
fun contains(x: Int, y: Int): Boolean {
operator fun get(vector: Vector2) =
get(vector.x.toInt(), vector.y.toInt())
fun contains(x: Int, y: Int) =
getOrNull(x, y) != null
operator fun get(x: Int, y: Int) =
tileMatrix[x - leftX][y - bottomY]!!
/** @return tile at hex coordinates ([x],[y]) or null if they are outside the map. Does *not* respect world wrap, use [getIfTileExistsOrNull] for that. */
private fun getOrNull (x: Int, y: Int): TileInfo? {
val arrayXIndex = x - leftX
if (arrayXIndex < 0 || arrayXIndex >= tileMatrix.size) return false
if (arrayXIndex < 0 || arrayXIndex >= tileMatrix.size) return null
val arrayYIndex = y - bottomY
if (arrayYIndex < 0 || arrayYIndex >= tileMatrix[arrayXIndex].size) return false
return tileMatrix[arrayXIndex][arrayYIndex] != null
if (arrayYIndex < 0 || arrayYIndex >= tileMatrix[arrayXIndex].size) return null
return tileMatrix[arrayXIndex][arrayYIndex]
}
operator fun get(x: Int, y: Int): TileInfo {
val arrayXIndex = x - leftX
val arrayYIndex = y - bottomY
return tileMatrix[arrayXIndex][arrayYIndex]!!
}
operator fun get(vector: Vector2): TileInfo {
return get(vector.x.toInt(), vector.y.toInt())
}
//endregion
//region Pure Functions
/** @return All tiles in a hexagon of radius [distance], including the tile at [origin] and all up to [distance] steps away.
* Respects map edges and world wrap. */
fun getTilesInDistance(origin: Vector2, distance: Int): Sequence<TileInfo> =
getTilesInDistanceRange(origin, 0..distance)
/** @return All tiles in a hexagonal ring around [origin] with the distances in [range]. Excludes the [origin] tile unless [range] starts at 0.
* Respects map edges and world wrap. */
fun getTilesInDistanceRange(origin: Vector2, range: IntRange): Sequence<TileInfo> =
range.asSequence().flatMap { getTilesAtDistance(origin, it) }
/** @return All tiles in a hexagonal ring 1 tile wide around [origin] with the [distance]. Contains the [origin] if and only if [distance] is <= 0.
* Respects map edges and world wrap. */
fun getTilesAtDistance(origin: Vector2, distance: Int): Sequence<TileInfo> =
if (distance <= 0) // silently take negatives.
sequenceOf(get(origin))
@ -133,6 +193,7 @@ class TileMap {
}
}.filterNotNull()
/** @return tile at hex coordinates ([x],[y]) or null if they are outside the map. Respects map edges and world wrap. */
private fun getIfTileExistsOrNull(x: Int, y: Int): TileInfo? {
if (contains(x, y))
return get(x, y)
@ -156,6 +217,166 @@ class TileMap {
return null
}
/**
* Returns the clockPosition of [otherTile] seen from [tile]'s position
* Returns -1 if not neighbors
*/
fun getNeighborTileClockPosition(tile: TileInfo, otherTile: TileInfo): Int {
val radius = if (mapParameters.shape == MapShape.rectangular)
mapParameters.mapSize.width / 2
else mapParameters.mapSize.radius
val xDifference = tile.position.x - otherTile.position.x
val yDifference = tile.position.y - otherTile.position.y
val xWrapDifferenceBottom = tile.position.x - (otherTile.position.x - radius)
val yWrapDifferenceBottom = tile.position.y - (otherTile.position.y - radius)
val xWrapDifferenceTop = tile.position.x - (otherTile.position.x + radius)
val yWrapDifferenceTop = tile.position.y - (otherTile.position.y + radius)
return when {
xDifference == 1f && yDifference == 1f -> 6 // otherTile is below
xDifference == -1f && yDifference == -1f -> 12 // otherTile is above
xDifference == 1f || xWrapDifferenceBottom == 1f -> 4 // otherTile is bottom-right
yDifference == 1f || yWrapDifferenceBottom == 1f -> 8 // otherTile is bottom-left
xDifference == -1f || xWrapDifferenceTop == -1f -> 10 // otherTile is top-left
yDifference == -1f || yWrapDifferenceTop == -1f -> 2 // otherTile is top-right
else -> -1
}
}
/** Convert relative direction of [otherTile] seen from [tile]'s position into a vector
* in world coordinates of length sqrt(3), so that it can be used to go from tile center to
* the edge of the hex in that direction (meaning the center of the border between the hexes)
*/
fun getNeighborTilePositionAsWorldCoords(tile: TileInfo, otherTile: TileInfo): Vector2 =
HexMath.getClockDirectionToWorldVector(getNeighborTileClockPosition(tile, otherTile))
/**
* Returns the closest position to (0, 0) outside the map which can be wrapped
* to the position of the given vector
*/
fun getUnWrappedPosition(position: Vector2): Vector2 {
if (!contains(position))
return position //The position is outside the map so its unwrapped already
val radius = if (mapParameters.shape == MapShape.rectangular)
mapParameters.mapSize.width / 2
else mapParameters.mapSize.radius
val vectorUnwrappedLeft = Vector2(position.x + radius, position.y - radius)
val vectorUnwrappedRight = Vector2(position.x - radius, position.y + radius)
return if (vectorUnwrappedRight.len() < vectorUnwrappedLeft.len())
vectorUnwrappedRight
else
vectorUnwrappedLeft
}
/** @return List of tiles visible from location [position] for a unit with sight range [sightDistance] */
fun getViewableTiles(position: Vector2, sightDistance: Int): List<TileInfo> {
val viewableTiles = getTilesInDistance(position, 1).toMutableList()
val currentTileHeight = get(position).getHeight()
for (i in 1..sightDistance) { // in each layer,
// This is so we don't use tiles in the same distance to "see over",
// that is to say, the "viewableTiles.contains(it) check will return false for neighbors from the same distance
val tilesToAddInDistanceI = ArrayList<TileInfo>()
for (cTile in getTilesAtDistance(position, i)) { // for each tile in that layer,
val cTileHeight = cTile.getHeight()
/*
Okay so, if we're looking at a tile from a to c with b in the middle,
we have several scenarios:
1. a>b - - I can see everything, b does not hide c
2. a==b
2.1 c>b - c is tall enough I can see it over b!
2.2 b blocks view from same-elevation tiles - hides c
2.3 none of the above - I can see c
3. a<b
3.1 b>=c - b hides c
3.2 b<c - c is tall enough I can see it over b!
This can all be summed up as "I can see c if a>b || c>b || (a==b && b !blocks same-elevation view)"
*/
val containsViewableNeighborThatCanSeeOver = cTile.neighbors.any {
bNeighbor: TileInfo ->
val bNeighborHeight = bNeighbor.getHeight()
viewableTiles.contains(bNeighbor) && (
currentTileHeight > bNeighborHeight // a>b
|| cTileHeight > bNeighborHeight // c>b
|| currentTileHeight == bNeighborHeight // a==b
&& !bNeighbor.hasUnique("Blocks line-of-sight from tiles at same elevation"))
}
if (containsViewableNeighborThatCanSeeOver) tilesToAddInDistanceI.add(cTile)
}
viewableTiles.addAll(tilesToAddInDistanceI)
}
return viewableTiles
}
/** Strips all units from [TileMap]
* @return stripped [clone] of [TileMap]
*/
fun stripAllUnits(): TileMap {
return clone().apply { tileList.forEach { it.stripUnits() } }
}
/** Build a list of incompatibilities of a map with a ruleset for the new game loader
*
* Is run before setTransients, so make do without startingLocationsByNation
*/
fun getRulesetIncompatibility(ruleset: Ruleset): HashSet<String> {
val rulesetIncompatibilities = HashSet<String>()
for (set in values.map { it.getRulesetIncompatibility(ruleset) })
rulesetIncompatibilities.addAll(set)
for ((_, nationName) in startingLocations) {
if (nationName !in ruleset.nations)
rulesetIncompatibilities.add("Nation [$nationName] does not exist in ruleset!")
}
rulesetIncompatibilities.remove("")
return rulesetIncompatibilities
}
//endregion
//region State-Changing Methods
/** Initialize transients - without, most operations, like [get] from coordinates, will fail.
* @param ruleset Required unless this is a clone of an initialized TileMap including one
* @param setUnitCivTransients when false Civ-specific parts of unit initialization are skipped, for the map editor.
*/
fun setTransients(ruleset: Ruleset? = null, setUnitCivTransients: Boolean = true) {
if (ruleset != null) this.ruleset = ruleset
if (this.ruleset == null) throw(IllegalStateException("TileMap.setTransients called without ruleset"))
if (tileMatrix.isEmpty()) {
val topY = tileList.asSequence().map { it.position.y.toInt() }.maxOrNull()!!
bottomY = tileList.asSequence().map { it.position.y.toInt() }.minOrNull()!!
val rightX = tileList.asSequence().map { it.position.x.toInt() }.maxOrNull()!!
leftX = tileList.asSequence().map { it.position.x.toInt() }.minOrNull()!!
for (x in leftX..rightX) {
val row = ArrayList<TileInfo?>()
for (y in bottomY..topY) row.add(null)
tileMatrix.add(row)
}
} else {
// Yes the map generator calls this repeatedly, and we don't want to end up with an oversized tileMatrix
// rightX is -leftX or -leftX + 1
if (tileMatrix.size != 1 - 2 * leftX && tileMatrix.size != 2 - 2 * leftX)
throw(IllegalStateException("TileMap.setTransients called on existing tileMatrix of different size"))
}
for (tileInfo in values) {
tileMatrix[tileInfo.position.x.toInt() - leftX][tileInfo.position.y.toInt() - bottomY] = tileInfo
tileInfo.tileMap = this
tileInfo.ruleset = this.ruleset!!
tileInfo.setTerrainTransients()
tileInfo.setUnitTransients(setUnitCivTransients)
}
}
/** Tries to place the [unitName] into the [TileInfo] closest to the given [position]
* @param position where to try to place the unit (or close - max 10 tiles distance)
@ -224,64 +445,13 @@ class TileMap {
}
fun getViewableTiles(position: Vector2, sightDistance: Int): List<TileInfo> {
val viewableTiles = getTilesInDistance(position, 1).toMutableList()
val currentTileHeight = get(position).getHeight()
for (i in 1..sightDistance) { // in each layer,
// This is so we don't use tiles in the same distance to "see over",
// that is to say, the "viewableTiles.contains(it) check will return false for neighbors from the same distance
val tilesToAddInDistanceI = ArrayList<TileInfo>()
for (cTile in getTilesAtDistance(position, i)) { // for each tile in that layer,
val cTileHeight = cTile.getHeight()
/*
Okay so, if we're looking at a tile from a to c with b in the middle,
we have several scenarios:
1. a>b - - I can see everything, b does not hide c
2. a==b
2.1 c>b - c is tall enough I can see it over b!
2.2 b blocks view from same-elevation tiles - hides c
2.3 none of the above - I can see c
3. a<b
3.1 b>=c - b hides c
3.2 b<c - c is tall enough I can see it over b!
This can all be summed up as "I can see c if a>b || c>b || (a==b && b !blocks same-elevation view)"
*/
val containsViewableNeighborThatCanSeeOver = cTile.neighbors.any {
bNeighbor: TileInfo ->
val bNeighborHeight = bNeighbor.getHeight()
viewableTiles.contains(bNeighbor) && (
currentTileHeight > bNeighborHeight // a>b
|| cTileHeight > bNeighborHeight // c>b
|| currentTileHeight == bNeighborHeight // a==b
&& !bNeighbor.hasUnique("Blocks line-of-sight from tiles at same elevation"))
}
if (containsViewableNeighborThatCanSeeOver) tilesToAddInDistanceI.add(cTile)
}
viewableTiles.addAll(tilesToAddInDistanceI)
}
return viewableTiles
}
/** Strips all units from [TileMap]
* @return stripped clone of [TileMap]
*/
fun stripAllUnits(): TileMap {
return clone().apply { tileList.forEach { it.stripUnits() } }
}
/** Strips all units and starting location from [TileMap] for specified [Player]
/** Strips all units and starting locations from [TileMap] for specified [Player]
* Operation in place
* @param player units of player to be stripped off
* @param player units of this player will be removed
*/
fun stripPlayer(player: Player) {
tileList.forEach {
if (it.improvement == "StartingLocation " + player.chosenCiv) {
if (it.improvement == startingLocationPrefix + player.chosenCiv) {
it.improvement = null
}
for (unit in it.getUnits()) if (unit.owner == player.chosenCiv) unit.removeFromTile()
@ -295,8 +465,8 @@ class TileMap {
*/
fun switchPlayersNation(player: Player, newNation: Nation) {
tileList.forEach {
if (it.improvement == "StartingLocation " + player.chosenCiv) {
it.improvement = "StartingLocation " + newNation.name
if (it.improvement == startingLocationPrefix + player.chosenCiv) {
it.improvement = startingLocationPrefix + newNation.name
}
for (unit in it.getUnits()) if (unit.owner == player.chosenCiv) {
unit.owner = newNation.name
@ -305,80 +475,61 @@ class TileMap {
}
}
fun setTransients(ruleset: Ruleset, setUnitCivTransients: Boolean = true) { // In the map editor, no Civs or Game exist, so we won't set the unit transients
val topY = tileList.asSequence().map { it.position.y.toInt() }.maxOrNull()!!
bottomY = tileList.asSequence().map { it.position.y.toInt() }.minOrNull()!!
val rightX = tileList.asSequence().map { it.position.x.toInt() }.maxOrNull()!!
leftX = tileList.asSequence().map { it.position.x.toInt() }.minOrNull()!!
for (x in leftX..rightX) {
val row = ArrayList<TileInfo?>()
for (y in bottomY..topY) row.add(null)
tileMatrix.add(row)
}
for (tileInfo in values) {
tileMatrix[tileInfo.position.x.toInt() - leftX][tileInfo.position.y.toInt() - bottomY] = tileInfo
tileInfo.tileMap = this
tileInfo.ruleset = ruleset
tileInfo.setTerrainTransients()
tileInfo.setUnitTransients(setUnitCivTransients)
/**
* Initialize startingLocations transients, including legacy support (maps saved with placeholder improvements)
*/
fun setStartingLocationsTransients() {
if (startingLocations.size == 1 && startingLocations[0].nation == legacyMarker)
return translateStartingLocationsFromMap()
startingLocationsByNation.clear()
for ((position, nationName) in startingLocations) {
val nationSet = startingLocationsByNation[nationName] ?: hashSetOf<TileInfo>().also { startingLocationsByNation[nationName] = it }
nationSet.add(get(position))
}
}
/**
* Returns the clockPosition of otherTile seen from tile's position
* Returns -1 if not neighbors
* Scan and remove placeholder improvements from map and build startingLocations from them
*/
fun getNeighborTileClockPosition(tile: TileInfo, otherTile: TileInfo): Int {
val radius = if (mapParameters.shape == MapShape.rectangular)
mapParameters.mapSize.width / 2
else mapParameters.mapSize.radius
fun translateStartingLocationsFromMap() {
startingLocations.clear()
tileList.asSequence()
.filter { it.improvement?.startsWith(startingLocationPrefix) == true }
.map { it to StartingLocation(it.position, it.improvement!!.removePrefix(startingLocationPrefix)) }
.sortedBy { it.second.nation } // vanity, or to make diffs between un-gzipped map files easier
.forEach { (tile, startingLocation) ->
tile.improvement = null
startingLocations.add(startingLocation)
}
setStartingLocationsTransients()
}
val xDifference = tile.position.x - otherTile.position.x
val yDifference = tile.position.y - otherTile.position.y
val xWrapDifferenceBottom = tile.position.x - (otherTile.position.x - radius)
val yWrapDifferenceBottom = tile.position.y - (otherTile.position.y - radius)
val xWrapDifferenceTop = tile.position.x - (otherTile.position.x + radius)
val yWrapDifferenceTop = tile.position.y - (otherTile.position.y + radius)
return when {
xDifference == 1f && yDifference == 1f -> 6 // otherTile is below
xDifference == -1f && yDifference == -1f -> 12 // otherTile is above
xDifference == 1f || xWrapDifferenceBottom == 1f -> 4 // otherTile is bottom-right
yDifference == 1f || yWrapDifferenceBottom == 1f -> 8 // otherTile is bottom-left
xDifference == -1f || xWrapDifferenceTop == -1f -> 10 // otherTile is top-left
yDifference == -1f || yWrapDifferenceTop == -1f -> 2 // otherTile is top-right
else -> -1
/**
* Place placeholder improvements on the map for the startingLocations entries.
*
* **For use by the map editor only**
*
* This is a copy, the startingLocations array and transients are untouched.
* Any actual improvements on the tiles will be overwritten.
*/
fun translateStartingLocationsToMap() {
for ((position, nationName) in startingLocations) {
get(position).improvement = startingLocationPrefix + nationName
}
}
/** Convert relative direction of otherTile seen from tile's position into a vector
* in world coordinates of length sqrt(3), so that it can be used to go from tile center to
* the edge of the hex in that direction (meaning the center of the border between the hexes)
*/
fun getNeighborTilePositionAsWorldCoords(tile: TileInfo, otherTile: TileInfo): Vector2 =
HexMath.getClockDirectionToWorldVector(getNeighborTileClockPosition(tile, otherTile))
/**
* Returns the closest position to (0, 0) outside the map which can be wrapped
* to the position of the given vector
*/
fun getUnWrappedPosition(position: Vector2): Vector2 {
if (!contains(position))
return position //The position is outside the map so its unwrapped already
var radius = mapParameters.mapSize.radius
if (mapParameters.shape == MapShape.rectangular)
radius = mapParameters.mapSize.width / 2
val vectorUnwrappedLeft = Vector2(position.x + radius, position.y - radius)
val vectorUnwrappedRight = Vector2(position.x - radius, position.y + radius)
return if (vectorUnwrappedRight.len() < vectorUnwrappedLeft.len())
vectorUnwrappedRight
else
vectorUnwrappedLeft
/** Adds a starting position, maintaining the transients */
fun addStartingLocation(nationName: String, tile: TileInfo) {
startingLocations.add(StartingLocation(tile.position, nationName))
val nationSet = startingLocationsByNation[nationName] ?: hashSetOf<TileInfo>().also { startingLocationsByNation[nationName] = it }
nationSet.add(tile)
}
/** Clears starting positions, e.g. after GameStarter is done with them. Does not clear the pseudo-improvements. */
fun clearStartingLocations() {
startingLocations.clear()
startingLocationsByNation.clear()
}
//endregion
}

View File

@ -40,6 +40,7 @@ class GameParameters { // Default values are the default new game
parameters.noBarbarians = noBarbarians
parameters.oneCityChallenge = oneCityChallenge
parameters.nuclearWeaponsEnabled = nuclearWeaponsEnabled
parameters.religionEnabled = religionEnabled
parameters.victoryTypes = ArrayList(victoryTypes)
parameters.startingEra = startingEra
parameters.isOnlineMultiplayer = isOnlineMultiplayer
@ -47,4 +48,24 @@ class GameParameters { // Default values are the default new game
parameters.mods = LinkedHashSet(mods)
return parameters
}
// For debugging and MapGenerator console output
override fun toString() = "($difficulty $gameSpeed $startingEra, " +
"${players.count { it.playerType == PlayerType.Human }} ${PlayerType.Human} " +
"${players.count { it.playerType == PlayerType.AI }} ${PlayerType.AI} " +
"$numberOfCityStates CS, " +
sequence<String> {
if (isOnlineMultiplayer) yield("Online Multiplayer")
if (noBarbarians) yield("No barbs")
if (oneCityChallenge) yield("OCC")
if (!nuclearWeaponsEnabled) yield("No nukes")
if (religionEnabled) yield("Religion")
if (godMode) yield("God mode")
if (VictoryType.Cultural !in victoryTypes) yield("No ${VictoryType.Cultural} Victory")
if (VictoryType.Diplomatic in victoryTypes) yield("${VictoryType.Diplomatic} Victory")
if (VictoryType.Domination !in victoryTypes) yield("No ${VictoryType.Domination} Victory")
if (VictoryType.Scientific !in victoryTypes) yield("No ${VictoryType.Scientific} Victory")
}.joinToString() +
(if (mods.isEmpty()) ", no mods" else mods.joinToString(",", ", mods=(", ")", 6) ) +
")"
}

View File

@ -162,7 +162,7 @@ class MapEditorOptionsTable(val mapEditorScreen: MapEditorScreen): Table(CameraS
val nationImage = getHex(ImageGetter.getNationIndicator(nation, 40f))
nationImage.onClick {
val improvementName = "StartingLocation " + nation.name
val improvementName = TileMap.startingLocationPrefix + nation.name
tileAction = {
it.improvement = improvementName
for ((tileInfo, tileGroups) in mapEditorScreen.mapHolder.tileGroups) {
@ -267,17 +267,6 @@ class MapEditorOptionsTable(val mapEditorScreen: MapEditorScreen): Table(CameraS
editorPickTable.add(AutoScrollPane(unitsTable)).height(scrollPanelHeight)
}
private fun nationsFromMap(tileMap: TileMap): ArrayList<Nation> {
val tilesWithStartingLocations = tileMap.values
.filter { it.improvement != null && it.improvement!!.startsWith("StartingLocation ") }
var nations = ArrayList<Nation>()
for (tile in tilesWithStartingLocations) {
var civName = tile.improvement!!.removePrefix("StartingLocation ")
nations.add(ruleset.nations[civName]!!)
}
return nations
}
private fun getPlayerIndexString(player: Player): String {
val index = gameParameters.players.indexOf(player) + 1
return "Player [$index]".tr()

View File

@ -35,6 +35,8 @@ class MapEditorScreen(): CameraStageBaseScreen() {
private fun initialize() {
ImageGetter.setNewRuleset(ruleset)
tileMap.setTransients(ruleset,false)
tileMap.setStartingLocationsTransients()
tileMap.translateStartingLocationsToMap()
UncivGame.Current.translations.translationActiveMods = ruleset.mods
mapHolder = EditorMapHolder(this, tileMap)

View File

@ -6,14 +6,12 @@ import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.badlogic.gdx.utils.Json
import com.unciv.logic.MapSaver
import com.unciv.logic.map.MapType
import com.unciv.logic.map.TileMap
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.translations.tr
import com.unciv.ui.pickerscreens.PickerScreen
import com.unciv.ui.saves.Gzip
import com.unciv.ui.utils.*
import kotlin.concurrent.thread
import com.unciv.ui.utils.AutoScrollPane as ScrollPane
@ -35,7 +33,7 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc
mapToSave.mapParameters.type = MapType.custom
thread(name = "SaveMap") {
try {
MapSaver.saveMap(mapNameTextField.text, mapToSave)
MapSaver.saveMap(mapNameTextField.text, getMapCloneForSave(mapToSave))
Gdx.app.postRunnable {
Gdx.input.inputProcessor = null // This is to stop ANRs happening here, until the map editor screen sets up.
game.setScreen(MapEditorScreen(mapToSave))
@ -119,9 +117,7 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc
if (save) {
val copyMapAsTextButton = "Copy to clipboard".toTextButton()
val copyMapAsTextAction = {
val json = Json().toJson(mapToSave)
val base64Gzip = Gzip.zip(json)
Gdx.app.clipboard.contents = base64Gzip
Gdx.app.clipboard.contents = MapSaver.mapToSavedString(getMapCloneForSave(mapToSave!!))
}
copyMapAsTextButton.onClick (copyMapAsTextAction)
keyPressDispatcher[KeyCharAndCode.ctrl('C')] = copyMapAsTextAction
@ -132,8 +128,7 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc
val loadFromClipboardAction = {
try {
val clipboardContentsString = Gdx.app.clipboard.contents.trim()
val decoded = Gzip.unzip(clipboardContentsString)
val loadedMap = MapSaver.mapFromJson(decoded)
val loadedMap = MapSaver.mapFromSavedString(clipboardContentsString)
game.setScreen(MapEditorScreen(loadedMap))
} catch (ex: Exception) {
couldNotLoadMapLabel.isVisible = true
@ -187,4 +182,8 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc
}
}
fun getMapCloneForSave(mapToSave: TileMap) = mapToSave!!.clone().also {
it.setTransients(setUnitCivTransients = false)
it.translateStartingLocationsFromMap()
}
}

View File

@ -91,10 +91,7 @@ class NewGameScreen(
if (mapOptionsTable.mapTypeSelectBox.selected.value == MapType.custom){
val map = MapSaver.loadMap(gameSetupInfo.mapFile!!)
val rulesetIncompatibilities = HashSet<String>()
for (set in map.values.map { it.getRulesetIncompatibility(ruleset) })
rulesetIncompatibilities.addAll(set)
rulesetIncompatibilities.remove("")
val rulesetIncompatibilities = map.getRulesetIncompatibility(ruleset)
if (rulesetIncompatibilities.isNotEmpty()) {
val incompatibleMap = Popup(this)

View File

@ -11,6 +11,7 @@ import com.unciv.UncivGame
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.RoadStatus
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
import com.unciv.ui.cityscreen.YieldGroup
import com.unciv.ui.utils.ImageGetter
import com.unciv.ui.utils.center
@ -331,9 +332,11 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
}
private fun removeMissingModReferences() {
// This runs from map editor too, so the Pseudo-improvements for starting locations need to stay.
// The nations can be checked.
val improvementName = tileInfo.improvement
if(improvementName != null && improvementName.startsWith("StartingLocation ")){
val nationName = improvementName.removePrefix("StartingLocation ")
if (improvementName != null && improvementName.startsWith(TileMap.startingLocationPrefix)) {
val nationName = improvementName.removePrefix(TileMap.startingLocationPrefix)
if (!tileInfo.ruleset.nations.containsKey(nationName))
tileInfo.improvement = null
}

View File

@ -16,6 +16,7 @@ import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable
import com.badlogic.gdx.utils.Align
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.map.TileMap
import com.unciv.models.ruleset.Era
import com.unciv.models.ruleset.Nation
import com.unciv.models.ruleset.Ruleset
@ -253,8 +254,8 @@ object ImageGetter {
fun getImprovementIcon(improvementName: String, size: Float = 20f): Actor {
if (improvementName.startsWith("Remove") || improvementName == Constants.cancelImprovementOrder)
return Table().apply { add(getImage("OtherIcons/Stop")).size(size) }
if (improvementName.startsWith("StartingLocation ")) {
val nationName = improvementName.removePrefix("StartingLocation ")
if (improvementName.startsWith(TileMap.startingLocationPrefix)) {
val nationName = improvementName.removePrefix(TileMap.startingLocationPrefix)
val nation = ruleset.nations[nationName]!!
return getNationIndicator(nation, size)
}

View File

@ -24,9 +24,9 @@ class TileInfoTable(private val viewingCiv :CivilizationInfo) : Table(CameraStag
add(getStatsTable(tile))
add( MarkupRenderer.render(tile.toMarkup(viewingCiv), padding = 0f, noLinkImages = true) {
UncivGame.Current.setScreen(CivilopediaScreen(viewingCiv.gameInfo.ruleSet, link = it))
} ).pad(5f)
// For debug only!
// add(tile.position.toString().toLabel()).colspan(2).pad(10f)
} ).pad(5f).row()
if (UncivGame.Current.viewEntireMapForDebug)
add(tile.position.run { "(${x.toInt()},${y.toInt()})" }.toLabel()).colspan(2).pad(5f)
}
pack()