mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-22 22:00:24 +07:00
StartingLocation-Improvements-be-gone phase 1 (#4951)
This commit is contained in:
@ -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()) {
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
@ -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)"
|
||||
}
|
||||
|
@ -733,7 +733,6 @@ class MapUnit {
|
||||
|
||||
if (civInfo.isMajorCiv()
|
||||
&& tile.improvement != null
|
||||
&& !tile.improvement!!.startsWith("StartingLocation ")
|
||||
&& tile.getTileImprovement()!!.isAncientRuinsEquivalent()
|
||||
)
|
||||
getAncientRuinBonus(tile)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) ) +
|
||||
")"
|
||||
}
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
|
Reference in New Issue
Block a user