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

View File

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

View File

@ -20,53 +20,70 @@ import kotlin.collections.HashMap
import kotlin.math.max import kotlin.math.max
object GameStarter { object GameStarter {
// temporary instrumentation while tuning/debugging
private const val consoleOutput = true
private const val consoleTimings = true
fun startNewGame(gameSetupInfo: GameSetupInfo): GameInfo { fun startNewGame(gameSetupInfo: GameSetupInfo): GameInfo {
if (consoleOutput || consoleTimings)
println("\nGameStarter run with parameters ${gameSetupInfo.gameParameters}, map ${gameSetupInfo.mapParameters}")
val gameInfo = GameInfo() val gameInfo = GameInfo()
lateinit var tileMap: TileMap
gameInfo.gameParameters = gameSetupInfo.gameParameters gameInfo.gameParameters = gameSetupInfo.gameParameters
val ruleset = RulesetCache.getComplexRuleset(gameInfo.gameParameters.mods) val ruleset = RulesetCache.getComplexRuleset(gameInfo.gameParameters.mods)
if (gameSetupInfo.mapParameters.name != "") { if (gameSetupInfo.mapParameters.name != "") runAndMeasure("loadMap") {
gameInfo.tileMap = MapSaver.loadMap(gameSetupInfo.mapFile!!) tileMap = MapSaver.loadMap(gameSetupInfo.mapFile!!)
// Don't override the map parameters - this can include if we world wrap or not! // Don't override the map parameters - this can include if we world wrap or not!
} else { } else runAndMeasure("generateMap") {
gameInfo.tileMap = MapGenerator(ruleset).generateMap(gameSetupInfo.mapParameters) tileMap = MapGenerator(ruleset).generateMap(gameSetupInfo.mapParameters)
gameInfo.tileMap.mapParameters = 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 runAndMeasure("Remove units") {
addCivilizations(gameSetupInfo.gameParameters, gameInfo, ruleset) // this is before gameInfo.setTransients, so gameInfo doesn't yet have the gameBasics // 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 runAndMeasure("setTransients") {
for (tile in gameInfo.tileMap.values) tileMap.setTransients(ruleset) // if we're starting from a map with pre-placed units, they need the civs to exist first
for (unit in tile.getUnits()) tileMap.setStartingLocationsTransients()
if (gameInfo.civilizations.none { it.civName == unit.owner }) {
unit.currentTile = tile
unit.setTransients(ruleset)
unit.removeFromTile()
}
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) 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!
addCivStats(gameInfo) addCivStartingUnits(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)
// remove starting locations once we're done // remove starting locations once we're done
for (tile in gameInfo.tileMap.values) { tileMap.clearStartingLocations()
if (tile.improvement != null && tile.improvement!!.startsWith("StartingLocation "))
tile.improvement = null // set max starting movement for units loaded from map
// set max starting movement for units loaded from map for (tile in tileMap.values) {
for (unit in tile.getUnits()) unit.currentMovement = unit.getMaxMovement().toFloat() for (unit in tile.getUnits()) unit.currentMovement = unit.getMaxMovement().toFloat()
} }
@ -76,6 +93,14 @@ object GameStarter {
return 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) { private fun addPlayerIntros(gameInfo: GameInfo) {
gameInfo.civilizations.filter { gameInfo.civilizations.filter {
// isNotEmpty should also exclude a spectator // isNotEmpty should also exclude a spectator
@ -139,6 +164,8 @@ object GameStarter {
availableCivNames.removeAll(newGameParameters.players.map { it.chosenCiv }) availableCivNames.removeAll(newGameParameters.players.map { it.chosenCiv })
availableCivNames.remove(Constants.barbarians) availableCivNames.remove(Constants.barbarians)
val startingTechs = ruleset.technologies.values.filter { it.uniques.contains("Starting tech") }
if (!newGameParameters.noBarbarians && ruleset.nations.containsKey(Constants.barbarians)) { if (!newGameParameters.noBarbarians && ruleset.nations.containsKey(Constants.barbarians)) {
val barbarianCivilization = CivilizationInfo(Constants.barbarians) val barbarianCivilization = CivilizationInfo(Constants.barbarians)
gameInfo.civilizations.add(barbarianCivilization) gameInfo.civilizations.add(barbarianCivilization)
@ -149,44 +176,36 @@ object GameStarter {
else availableCivNames.pop() else availableCivNames.pop()
val playerCiv = CivilizationInfo(nationName) 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.tech.techsResearched.add(tech.name) // can't be .addTechnology because the civInfo isn't assigned yet
playerCiv.playerType = player.playerType playerCiv.playerType = player.playerType
playerCiv.playerId = player.playerId playerCiv.playerId = player.playerId
gameInfo.civilizations.add(playerCiv) gameInfo.civilizations.add(playerCiv)
} }
val cityStatesWithStartingLocations = val civNamesWithStartingLocations = gameInfo.tileMap.startingLocationsByNation.keys
gameInfo.tileMap.values
.filter { it.improvement != null && it.improvement!!.startsWith("StartingLocation ") }
.map { it.improvement!!.replace("StartingLocation ", "") }
val availableCityStatesNames = Stack<String>() 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, // 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! // 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 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)) { for (cityStateName in availableCityStatesNames.take(newGameParameters.numberOfCityStates)) {
val civ = CivilizationInfo(cityStateName) val civ = CivilizationInfo(cityStateName)
civ.cityStatePersonality = CityStatePersonality.values().random() civ.cityStatePersonality = CityStatePersonality.values().random()
if (ruleset.nations[cityStateName]?.cityStateType == CityStateType.Mercantile) { civ.cityStateResource = when {
if (!ruleset.tileResources.values.any { it.unique == "Can only be created by Mercantile City-States" }) { ruleset.nations[cityStateName]?.cityStateType != CityStateType.Mercantile -> null
civ.cityStateResource = null allMercantileResources.isEmpty() -> null
} else if (unusedMercantileResources.isNotEmpty()) { unusedMercantileResources.empty() -> allMercantileResources.random() // When unused luxuries exhausted, random
// First pick an unused luxury if possible else -> unusedMercantileResources.pop() // 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
}
} }
gameInfo.civilizations.add(civ) 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 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) { private fun addCivStartingUnits(gameInfo: GameInfo) {
val ruleSet = gameInfo.ruleSet val ruleSet = gameInfo.ruleSet
val tileMap = gameInfo.tileMap
val startingEra = gameInfo.gameParameters.startingEra val startingEra = gameInfo.gameParameters.startingEra
var startingUnits: MutableList<String> var startingUnits: MutableList<String>
var eraUnitReplacement: String var eraUnitReplacement: String
val startScores = HashMap<TileInfo, Float>() val startScores = HashMap<TileInfo, Float>(tileMap.values.size)
for (tile in gameInfo.tileMap.values) { for (tile in tileMap.values) {
startScores[tile] = tile.getTileStartScore() 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 // 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 // I hear copying code is good
val cityStatesWithStartingLocations = val civNamesWithStartingLocations = tileMap.startingLocationsByNation.keys
gameInfo.tileMap.values val bestCivs = gameInfo.civilizations.filter { !it.isBarbarian() && (!it.isCityState() || it.civName in civNamesWithStartingLocations) }
.filter { it.improvement != null && it.improvement!!.startsWith("StartingLocation ") } val bestLocations = getStartingLocations(bestCivs, tileMap, startScores)
.map { it.improvement!!.replace("StartingLocation ", "") } for ((civ, tile) in bestLocations) {
val bestCivs = gameInfo.civilizations.filter { !it.isBarbarian() && (!it.isCityState() || it.civName in cityStatesWithStartingLocations) } if (civ.civName in civNamesWithStartingLocations) // Already have explicit starting locations
val bestLocations = getStartingLocations(bestCivs, gameInfo.tileMap, startScores)
for (civ in bestCivs)
{
if (civ.isCityState()) // Already have explicit starting locations
continue continue
// Mark the best start locations so we remember them for the second pass // 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( val startingLocations = getStartingLocations(
gameInfo.civilizations.filter { !it.isBarbarian() }, gameInfo.civilizations.filter { !it.isBarbarian() },
gameInfo.tileMap, startScores) tileMap, startScores)
val settlerLikeUnits = ruleSet.units.filter { 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 // no starting units for Barbarians and Spectators
@ -242,7 +258,6 @@ object GameStarter {
for (tile in startingLocation.getTilesInDistance(3)) { for (tile in startingLocation.getTilesInDistance(3)) {
if (tile.improvement != null if (tile.improvement != null
&& !tile.improvement!!.startsWith("StartingLocation")
&& tile.getTileImprovement()!!.isAncientRuinsEquivalent() && tile.getTileImprovement()!!.isAncientRuinsEquivalent()
) { ) {
tile.improvement = null // Remove ancient ruins in immediate vicinity tile.improvement = null // Remove ancient ruins in immediate vicinity
@ -305,7 +320,7 @@ object GameStarter {
} }
if (unit == "Worker" && "Worker" !in ruleSet.units) { if (unit == "Worker" && "Worker" !in ruleSet.units) {
val buildableWorkerLikeUnits = ruleSet.units.filter { 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.isBuildable(civ)
&& it.value.isCivilian() && it.value.isCivilian()
} }
@ -353,18 +368,14 @@ object GameStarter {
landTilesInBigEnoughGroup.addAll(tilesInGroup) 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 val civsOrderedByAvailableLocations = civs.shuffled() // Order should be random since it determines who gets best start
.sortedBy { civ -> .sortedBy { civ ->
when { 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.contains("Tundra") -> 2 // Tundra starts are hard to find, so let's do them first
civ.nation.startBias.isNotEmpty() -> 3 // less harsh civ.nation.startBias.isNotEmpty() -> 3 // less harsh
else -> 4 else -> 4 // no requirements
} // no requirements }
} }
for (minimumDistanceBetweenStartingLocations in tileMap.tileMatrix.size / 4 downTo 0) { for (minimumDistanceBetweenStartingLocations in tileMap.tileMatrix.size / 4 downTo 0) {
@ -375,7 +386,7 @@ object GameStarter {
val startingLocations = HashMap<CivilizationInfo, TileInfo>() val startingLocations = HashMap<CivilizationInfo, TileInfo>()
for (civ in civsOrderedByAvailableLocations) { for (civ in civsOrderedByAvailableLocations) {
var startingLocation: TileInfo 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 var distanceToNext = minimumDistanceBetweenStartingLocations
if (presetStartingLocation != null) startingLocation = presetStartingLocation if (presetStartingLocation != null) startingLocation = presetStartingLocation
@ -389,11 +400,14 @@ object GameStarter {
var preferredTiles = freeTiles.toList() var preferredTiles = freeTiles.toList()
for (startBias in civ.nation.startBias) { for (startBias in civ.nation.startBias) {
if (startBias.startsWith("Avoid ")) { preferredTiles = when {
val tileToAvoid = startBias.removePrefix("Avoid [").removeSuffix("]") startBias.startsWith("Avoid [") -> {
preferredTiles = preferredTiles.filter { !it.matchesTerrainFilter(tileToAvoid) } val tileToAvoid = startBias.removePrefix("Avoid [").removeSuffix("]")
} else if (startBias == Constants.coast) preferredTiles = preferredTiles.filter { it.isCoastalTile() } preferredTiles.filter { !it.matchesTerrainFilter(tileToAvoid) }
else preferredTiles = preferredTiles.filter { it.matchesTerrainFilter(startBias) } }
startBias == Constants.coast -> preferredTiles.filter { it.isCoastalTile() }
else -> preferredTiles.filter { it.matchesTerrainFilter(startBias) }
}
} }
startingLocation = if (preferredTiles.isNotEmpty()) preferredTiles.last() else freeTiles.last() startingLocation = if (preferredTiles.isNotEmpty()) preferredTiles.last() else freeTiles.last()

View File

@ -10,20 +10,32 @@ object MapSaver {
fun json() = GameSaver.json() fun json() = GameSaver.json()
private const val mapsFolder = "maps" private const val mapsFolder = "maps"
private const val saveZipped = false
private fun getMap(mapName:String) = Gdx.files.local("$mapsFolder/$mapName") 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) { 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 { fun loadMap(mapFile:FileHandle):TileMap {
val gzippedString = mapFile.readString() return mapFromSavedString(mapFile.readString())
val unzippedJson = Gzip.unzip(gzippedString)
return json().fromJson(TileMap::class.java, unzippedJson)
} }
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 // 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() if (civInfo.isMajorCiv()
&& tile.improvement != null && tile.improvement != null
&& !tile.improvement!!.startsWith("StartingLocation ")
&& tile.getTileImprovement()!!.isAncientRuinsEquivalent() && tile.getTileImprovement()!!.isAncientRuinsEquivalent()
) )
getAncientRuinBonus(tile) getAncientRuinBonus(tile)

View File

@ -659,8 +659,7 @@ open class TileInfo {
out.add("Terrain feature [$terrainFeature] does not exist in ruleset!") out.add("Terrain feature [$terrainFeature] does not exist in ruleset!")
if (resource != null && !ruleset.tileResources.containsKey(resource)) if (resource != null && !ruleset.tileResources.containsKey(resource))
out.add("Resource [$resource] does not exist in ruleset!") out.add("Resource [$resource] does not exist in ruleset!")
if (improvement != null && !improvement!!.startsWith("StartingLocation") if (improvement != null && !ruleset.tileImprovements.containsKey(improvement))
&& !ruleset.tileImprovements.containsKey(improvement))
out.add("Improvement [$improvement] does not exist in ruleset!") out.add("Improvement [$improvement] does not exist in ruleset!")
return out return out
} }
@ -756,9 +755,9 @@ open class TileInfo {
roadStatus = RoadStatus.None roadStatus = RoadStatus.None
} }
private fun normalizeTileImprovement(ruleset: Ruleset) { 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 if (!isLand || getLastTerrain().impassable) improvement = null
return return
} }

View File

@ -10,11 +10,47 @@ import com.unciv.models.ruleset.Nation
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import kotlin.math.abs 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 { 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 @Transient
lateinit var gameInfo: GameInfo 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 @Transient
var tileMatrix = ArrayList<ArrayList<TileInfo?>>() // this works several times faster than a hashmap, the performance difference is really astounding 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 @delegate:Transient
val naturalWonders: List<String> by lazy { tileList.asSequence().filter { it.isNaturalWonder() }.map { it.naturalWonder!! }.distinct().toList() } val naturalWonders: List<String> by lazy { tileList.asSequence().filter { it.isNaturalWonder() }.map { it.naturalWonder!! }.distinct().toList() }
var mapParameters = MapParameters() // Excluded from Serialization by having no own backing field
private var tileList = ArrayList<TileInfo>()
val values: Collection<TileInfo> val values: Collection<TileInfo>
get() = tileList get() = tileList
@Transient
val startingLocationsByNation = HashMap<String,HashSet<TileInfo>>()
//endregion
//region Constructors
/** for json parsing, we need to have a default constructor */ /** for json parsing, we need to have a default constructor */
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) { constructor(radius: Int, ruleset: Ruleset, worldWrap: Boolean = false) {
startingLocations.clear()
for (vector in HexMath.getVectorsInDistance(Vector2.Zero, radius, worldWrap)) for (vector in HexMath.getVectorsInDistance(Vector2.Zero, radius, worldWrap))
tileList.add(TileInfo().apply { position = vector; baseTerrain = Constants.grassland }) tileList.add(TileInfo().apply { position = vector; baseTerrain = Constants.grassland })
setTransients(ruleset) 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) { constructor(width: Int, height: Int, ruleset: Ruleset, worldWrap: Boolean = false) {
startingLocations.clear()
// world-wrap maps must always have an even width, so round down // world-wrap maps must always have an even width, so round down
val wrapAdjustedWidth = if (worldWrap && width % 2 != 0 ) width -1 else width val wrapAdjustedWidth = if (worldWrap && width % 2 != 0 ) width -1 else width
@ -67,39 +109,57 @@ class TileMap {
setTransients(ruleset) setTransients(ruleset)
} }
//endregion
//region Operators and Standards
/** @return a deep-copy clone of the serializable fields, no transients initialized */
fun clone(): TileMap { fun clone(): TileMap {
val toReturn = TileMap() val toReturn = TileMap()
toReturn.tileList.addAll(tileList.map { it.clone() }) toReturn.tileList.addAll(tileList.map { it.clone() })
toReturn.mapParameters = mapParameters toReturn.mapParameters = mapParameters
toReturn.ruleset = ruleset
toReturn.startingLocations.clear()
toReturn.startingLocations.ensureCapacity(startingLocations.size)
toReturn.startingLocations.addAll(startingLocations)
return toReturn 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 val arrayXIndex = x - leftX
if (arrayXIndex < 0 || arrayXIndex >= tileMatrix.size) return false if (arrayXIndex < 0 || arrayXIndex >= tileMatrix.size) return null
val arrayYIndex = y - bottomY val arrayYIndex = y - bottomY
if (arrayYIndex < 0 || arrayYIndex >= tileMatrix[arrayXIndex].size) return false if (arrayYIndex < 0 || arrayYIndex >= tileMatrix[arrayXIndex].size) return null
return tileMatrix[arrayXIndex][arrayYIndex] != null return tileMatrix[arrayXIndex][arrayYIndex]
} }
operator fun get(x: Int, y: Int): TileInfo { //endregion
val arrayXIndex = x - leftX //region Pure Functions
val arrayYIndex = y - bottomY
return tileMatrix[arrayXIndex][arrayYIndex]!!
}
operator fun get(vector: Vector2): TileInfo {
return get(vector.x.toInt(), vector.y.toInt())
}
/** @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> = fun getTilesInDistance(origin: Vector2, distance: Int): Sequence<TileInfo> =
getTilesInDistanceRange(origin, 0..distance) 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> = fun getTilesInDistanceRange(origin: Vector2, range: IntRange): Sequence<TileInfo> =
range.asSequence().flatMap { getTilesAtDistance(origin, it) } 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> = fun getTilesAtDistance(origin: Vector2, distance: Int): Sequence<TileInfo> =
if (distance <= 0) // silently take negatives. if (distance <= 0) // silently take negatives.
sequenceOf(get(origin)) sequenceOf(get(origin))
@ -133,6 +193,7 @@ class TileMap {
} }
}.filterNotNull() }.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? { private fun getIfTileExistsOrNull(x: Int, y: Int): TileInfo? {
if (contains(x, y)) if (contains(x, y))
return get(x, y) return get(x, y)
@ -156,6 +217,166 @@ class TileMap {
return null 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] /** 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) * @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> { /** Strips all units and starting locations from [TileMap] for specified [Player]
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]
* Operation in place * 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) { fun stripPlayer(player: Player) {
tileList.forEach { tileList.forEach {
if (it.improvement == "StartingLocation " + player.chosenCiv) { if (it.improvement == startingLocationPrefix + player.chosenCiv) {
it.improvement = null it.improvement = null
} }
for (unit in it.getUnits()) if (unit.owner == player.chosenCiv) unit.removeFromTile() for (unit in it.getUnits()) if (unit.owner == player.chosenCiv) unit.removeFromTile()
@ -295,8 +465,8 @@ class TileMap {
*/ */
fun switchPlayersNation(player: Player, newNation: Nation) { fun switchPlayersNation(player: Player, newNation: Nation) {
tileList.forEach { tileList.forEach {
if (it.improvement == "StartingLocation " + player.chosenCiv) { if (it.improvement == startingLocationPrefix + player.chosenCiv) {
it.improvement = "StartingLocation " + newNation.name it.improvement = startingLocationPrefix + newNation.name
} }
for (unit in it.getUnits()) if (unit.owner == player.chosenCiv) { for (unit in it.getUnits()) if (unit.owner == player.chosenCiv) {
unit.owner = newNation.name 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()!! * Initialize startingLocations transients, including legacy support (maps saved with placeholder improvements)
bottomY = tileList.asSequence().map { it.position.y.toInt() }.minOrNull()!! */
val rightX = tileList.asSequence().map { it.position.x.toInt() }.maxOrNull()!! fun setStartingLocationsTransients() {
leftX = tileList.asSequence().map { it.position.x.toInt() }.minOrNull()!! if (startingLocations.size == 1 && startingLocations[0].nation == legacyMarker)
return translateStartingLocationsFromMap()
for (x in leftX..rightX) { startingLocationsByNation.clear()
val row = ArrayList<TileInfo?>() for ((position, nationName) in startingLocations) {
for (y in bottomY..topY) row.add(null) val nationSet = startingLocationsByNation[nationName] ?: hashSetOf<TileInfo>().also { startingLocationsByNation[nationName] = it }
tileMatrix.add(row) nationSet.add(get(position))
}
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)
} }
} }
/** /**
* Returns the clockPosition of otherTile seen from tile's position * Scan and remove placeholder improvements from map and build startingLocations from them
* Returns -1 if not neighbors
*/ */
fun getNeighborTileClockPosition(tile: TileInfo, otherTile: TileInfo): Int { fun translateStartingLocationsFromMap() {
val radius = if (mapParameters.shape == MapShape.rectangular) startingLocations.clear()
mapParameters.mapSize.width / 2 tileList.asSequence()
else mapParameters.mapSize.radius .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 * Place placeholder improvements on the map for the startingLocations entries.
val xWrapDifferenceBottom = tile.position.x - (otherTile.position.x - radius) *
val yWrapDifferenceBottom = tile.position.y - (otherTile.position.y - radius) * **For use by the map editor only**
val xWrapDifferenceTop = tile.position.x - (otherTile.position.x + radius) *
val yWrapDifferenceTop = tile.position.y - (otherTile.position.y + radius) * This is a copy, the startingLocations array and transients are untouched.
* Any actual improvements on the tiles will be overwritten.
return when { */
xDifference == 1f && yDifference == 1f -> 6 // otherTile is below fun translateStartingLocationsToMap() {
xDifference == -1f && yDifference == -1f -> 12 // otherTile is above for ((position, nationName) in startingLocations) {
xDifference == 1f || xWrapDifferenceBottom == 1f -> 4 // otherTile is bottom-right get(position).improvement = startingLocationPrefix + nationName
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 /** Adds a starting position, maintaining the transients */
* in world coordinates of length sqrt(3), so that it can be used to go from tile center to fun addStartingLocation(nationName: String, tile: TileInfo) {
* the edge of the hex in that direction (meaning the center of the border between the hexes) startingLocations.add(StartingLocation(tile.position, nationName))
*/ val nationSet = startingLocationsByNation[nationName] ?: hashSetOf<TileInfo>().also { startingLocationsByNation[nationName] = it }
fun getNeighborTilePositionAsWorldCoords(tile: TileInfo, otherTile: TileInfo): Vector2 = nationSet.add(tile)
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
} }
/** 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.noBarbarians = noBarbarians
parameters.oneCityChallenge = oneCityChallenge parameters.oneCityChallenge = oneCityChallenge
parameters.nuclearWeaponsEnabled = nuclearWeaponsEnabled parameters.nuclearWeaponsEnabled = nuclearWeaponsEnabled
parameters.religionEnabled = religionEnabled
parameters.victoryTypes = ArrayList(victoryTypes) parameters.victoryTypes = ArrayList(victoryTypes)
parameters.startingEra = startingEra parameters.startingEra = startingEra
parameters.isOnlineMultiplayer = isOnlineMultiplayer parameters.isOnlineMultiplayer = isOnlineMultiplayer
@ -47,4 +48,24 @@ class GameParameters { // Default values are the default new game
parameters.mods = LinkedHashSet(mods) parameters.mods = LinkedHashSet(mods)
return parameters 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)) val nationImage = getHex(ImageGetter.getNationIndicator(nation, 40f))
nationImage.onClick { nationImage.onClick {
val improvementName = "StartingLocation " + nation.name val improvementName = TileMap.startingLocationPrefix + nation.name
tileAction = { tileAction = {
it.improvement = improvementName it.improvement = improvementName
for ((tileInfo, tileGroups) in mapEditorScreen.mapHolder.tileGroups) { for ((tileInfo, tileGroups) in mapEditorScreen.mapHolder.tileGroups) {
@ -267,17 +267,6 @@ class MapEditorOptionsTable(val mapEditorScreen: MapEditorScreen): Table(CameraS
editorPickTable.add(AutoScrollPane(unitsTable)).height(scrollPanelHeight) 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 { private fun getPlayerIndexString(player: Player): String {
val index = gameParameters.players.indexOf(player) + 1 val index = gameParameters.players.indexOf(player) + 1
return "Player [$index]".tr() return "Player [$index]".tr()

View File

@ -35,6 +35,8 @@ class MapEditorScreen(): CameraStageBaseScreen() {
private fun initialize() { private fun initialize() {
ImageGetter.setNewRuleset(ruleset) ImageGetter.setNewRuleset(ruleset)
tileMap.setTransients(ruleset,false) tileMap.setTransients(ruleset,false)
tileMap.setStartingLocationsTransients()
tileMap.translateStartingLocationsToMap()
UncivGame.Current.translations.translationActiveMods = ruleset.mods UncivGame.Current.translations.translationActiveMods = ruleset.mods
mapHolder = EditorMapHolder(this, tileMap) 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.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.badlogic.gdx.scenes.scene2d.ui.TextField import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.badlogic.gdx.utils.Json
import com.unciv.logic.MapSaver import com.unciv.logic.MapSaver
import com.unciv.logic.map.MapType import com.unciv.logic.map.MapType
import com.unciv.logic.map.TileMap import com.unciv.logic.map.TileMap
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.ui.pickerscreens.PickerScreen import com.unciv.ui.pickerscreens.PickerScreen
import com.unciv.ui.saves.Gzip
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
import kotlin.concurrent.thread import kotlin.concurrent.thread
import com.unciv.ui.utils.AutoScrollPane as ScrollPane 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 mapToSave.mapParameters.type = MapType.custom
thread(name = "SaveMap") { thread(name = "SaveMap") {
try { try {
MapSaver.saveMap(mapNameTextField.text, mapToSave) MapSaver.saveMap(mapNameTextField.text, getMapCloneForSave(mapToSave))
Gdx.app.postRunnable { Gdx.app.postRunnable {
Gdx.input.inputProcessor = null // This is to stop ANRs happening here, until the map editor screen sets up. Gdx.input.inputProcessor = null // This is to stop ANRs happening here, until the map editor screen sets up.
game.setScreen(MapEditorScreen(mapToSave)) game.setScreen(MapEditorScreen(mapToSave))
@ -119,9 +117,7 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc
if (save) { if (save) {
val copyMapAsTextButton = "Copy to clipboard".toTextButton() val copyMapAsTextButton = "Copy to clipboard".toTextButton()
val copyMapAsTextAction = { val copyMapAsTextAction = {
val json = Json().toJson(mapToSave) Gdx.app.clipboard.contents = MapSaver.mapToSavedString(getMapCloneForSave(mapToSave!!))
val base64Gzip = Gzip.zip(json)
Gdx.app.clipboard.contents = base64Gzip
} }
copyMapAsTextButton.onClick (copyMapAsTextAction) copyMapAsTextButton.onClick (copyMapAsTextAction)
keyPressDispatcher[KeyCharAndCode.ctrl('C')] = copyMapAsTextAction keyPressDispatcher[KeyCharAndCode.ctrl('C')] = copyMapAsTextAction
@ -132,8 +128,7 @@ class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousSc
val loadFromClipboardAction = { val loadFromClipboardAction = {
try { try {
val clipboardContentsString = Gdx.app.clipboard.contents.trim() val clipboardContentsString = Gdx.app.clipboard.contents.trim()
val decoded = Gzip.unzip(clipboardContentsString) val loadedMap = MapSaver.mapFromSavedString(clipboardContentsString)
val loadedMap = MapSaver.mapFromJson(decoded)
game.setScreen(MapEditorScreen(loadedMap)) game.setScreen(MapEditorScreen(loadedMap))
} catch (ex: Exception) { } catch (ex: Exception) {
couldNotLoadMapLabel.isVisible = true 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){ if (mapOptionsTable.mapTypeSelectBox.selected.value == MapType.custom){
val map = MapSaver.loadMap(gameSetupInfo.mapFile!!) val map = MapSaver.loadMap(gameSetupInfo.mapFile!!)
val rulesetIncompatibilities = HashSet<String>() val rulesetIncompatibilities = map.getRulesetIncompatibility(ruleset)
for (set in map.values.map { it.getRulesetIncompatibility(ruleset) })
rulesetIncompatibilities.addAll(set)
rulesetIncompatibilities.remove("")
if (rulesetIncompatibilities.isNotEmpty()) { if (rulesetIncompatibilities.isNotEmpty()) {
val incompatibleMap = Popup(this) val incompatibleMap = Popup(this)

View File

@ -11,6 +11,7 @@ import com.unciv.UncivGame
import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.RoadStatus import com.unciv.logic.map.RoadStatus
import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
import com.unciv.ui.cityscreen.YieldGroup import com.unciv.ui.cityscreen.YieldGroup
import com.unciv.ui.utils.ImageGetter import com.unciv.ui.utils.ImageGetter
import com.unciv.ui.utils.center import com.unciv.ui.utils.center
@ -331,9 +332,11 @@ open class TileGroup(var tileInfo: TileInfo, var tileSetStrings:TileSetStrings,
} }
private fun removeMissingModReferences() { 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 val improvementName = tileInfo.improvement
if(improvementName != null && improvementName.startsWith("StartingLocation ")){ if (improvementName != null && improvementName.startsWith(TileMap.startingLocationPrefix)) {
val nationName = improvementName.removePrefix("StartingLocation ") val nationName = improvementName.removePrefix(TileMap.startingLocationPrefix)
if (!tileInfo.ruleset.nations.containsKey(nationName)) if (!tileInfo.ruleset.nations.containsKey(nationName))
tileInfo.improvement = null 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.badlogic.gdx.utils.Align
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.map.TileMap
import com.unciv.models.ruleset.Era import com.unciv.models.ruleset.Era
import com.unciv.models.ruleset.Nation import com.unciv.models.ruleset.Nation
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
@ -253,8 +254,8 @@ object ImageGetter {
fun getImprovementIcon(improvementName: String, size: Float = 20f): Actor { fun getImprovementIcon(improvementName: String, size: Float = 20f): Actor {
if (improvementName.startsWith("Remove") || improvementName == Constants.cancelImprovementOrder) if (improvementName.startsWith("Remove") || improvementName == Constants.cancelImprovementOrder)
return Table().apply { add(getImage("OtherIcons/Stop")).size(size) } return Table().apply { add(getImage("OtherIcons/Stop")).size(size) }
if (improvementName.startsWith("StartingLocation ")) { if (improvementName.startsWith(TileMap.startingLocationPrefix)) {
val nationName = improvementName.removePrefix("StartingLocation ") val nationName = improvementName.removePrefix(TileMap.startingLocationPrefix)
val nation = ruleset.nations[nationName]!! val nation = ruleset.nations[nationName]!!
return getNationIndicator(nation, size) return getNationIndicator(nation, size)
} }

View File

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