Custom map size (#2876)

* Adding custom map sizes. Initial commit

* Custom map sizes UI update

* Custom map size with rectangular shape

* Added compatibility with older Maps and Game saves

* Fixed build errors and added warning messages

Co-authored-by: Yair Morgenstern <yairm210@hotmail.com>
This commit is contained in:
Alexander Korolyov
2021-05-06 17:39:28 +02:00
committed by GitHub
parent b8f9b99bd1
commit 453f5588ac
13 changed files with 170 additions and 29 deletions

View File

@ -270,6 +270,8 @@ Extension mods: =
World Wrap = World Wrap =
World wrap maps are very memory intensive - creating large world wrap maps on Android can lead to crashes! = World wrap maps are very memory intensive - creating large world wrap maps on Android can lead to crashes! =
Anything above 80 by 50 may work very slowly on Android! =
Anything above 40 may work very slowly on Android! =
# Multiplayer # Multiplayer

View File

@ -76,4 +76,11 @@ object Constants {
const val futureEra = "Future era" const val futureEra = "Future era"
const val barbarians = "Barbarians" const val barbarians = "Barbarians"
const val spectator = "Spectator" const val spectator = "Spectator"
const val tiny = "Tiny"
const val small = "Small"
const val medium = "Medium"
const val large = "Large"
const val huge = "Huge"
const val custom = "Custom"
} }

View File

@ -10,7 +10,7 @@ import com.unciv.logic.GameSaver
import com.unciv.logic.GameStarter import com.unciv.logic.GameStarter
import com.unciv.logic.map.mapgenerator.MapGenerator import com.unciv.logic.map.mapgenerator.MapGenerator
import com.unciv.logic.map.MapParameters import com.unciv.logic.map.MapParameters
import com.unciv.logic.map.MapSize import com.unciv.logic.map.MapSizeNew
import com.unciv.logic.map.MapType import com.unciv.logic.map.MapType
import com.unciv.models.ruleset.RulesetCache import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
@ -50,7 +50,7 @@ class MainMenuScreen: CameraStageBaseScreen() {
thread(name = "ShowMapBackground") { thread(name = "ShowMapBackground") {
val newMap = MapGenerator(RulesetCache.getBaseRuleset()) val newMap = MapGenerator(RulesetCache.getBaseRuleset())
.generateMap(MapParameters().apply { size = MapSize.Small; type = MapType.default }) .generateMap(MapParameters().apply { mapSize = MapSizeNew(Constants.small); type = MapType.default })
Gdx.app.postRunnable { // for GL context Gdx.app.postRunnable { // for GL context
ImageGetter.setNewRuleset(RulesetCache.getBaseRuleset()) ImageGetter.setNewRuleset(RulesetCache.getBaseRuleset())
val mapHolder = EditorMapHolder(MapEditorScreen(), newMap) val mapHolder = EditorMapHolder(MapEditorScreen(), newMap)

View File

@ -7,6 +7,7 @@ import com.unciv.logic.automation.NextTurnAutomation
import com.unciv.logic.city.CityConstructions import com.unciv.logic.city.CityConstructions
import com.unciv.logic.city.PerpetualConstruction import com.unciv.logic.city.PerpetualConstruction
import com.unciv.logic.civilization.* import com.unciv.logic.civilization.*
import com.unciv.logic.map.MapSizeNew
import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap import com.unciv.logic.map.TileMap
import com.unciv.models.metadata.GameParameters import com.unciv.models.metadata.GameParameters
@ -272,6 +273,12 @@ class GameInfo {
if (currentPlayer == "") currentPlayer = civilizations.first { it.isPlayerCivilization() }.civName if (currentPlayer == "") currentPlayer = civilizations.first { it.isPlayerCivilization() }.civName
currentPlayerCiv = getCivilization(currentPlayer) currentPlayerCiv = getCivilization(currentPlayer)
// as of version 3.9.18, added new custom map size
// empty mapSize name and non-custom map type means - it is old style map size,
// therefore we need to create new mapSize property
if (tileMap.mapParameters.mapSize.name == "" && tileMap.mapParameters.type != Constants.custom)
tileMap.mapParameters.mapSize = MapSizeNew(tileMap.mapParameters.size.name)
// this is separated into 2 loops because when we activate updateVisibleTiles in civ.setTransients, // this is separated into 2 loops because when we activate updateVisibleTiles in civ.setTransients,
// we try to find new civs, and we check if civ is barbarian, which we can't know unless the gameInfo is already set. // we try to find new civs, and we check if civ is barbarian, which we can't know unless the gameInfo is already set.
for (civInfo in civilizations) civInfo.gameInfo = this for (civInfo in civilizations) civInfo.gameInfo = this

View File

@ -41,6 +41,16 @@ object HexMath {
return Vector2(width, height) return Vector2(width, height)
} }
/** Returns a radius of a hexagonal map that have approximately the same number of
* tiles as a rectangular map of a given width/height
*/
fun getEquivalentHexagonalRadius(width: Int, height: Int): Int {
val nTiles = width * height.toFloat()
if (nTiles < 1) return 0
val radius = ((sqrt(12*nTiles - 3) - 3) / 6).roundToInt()
return radius
}
fun getAdjacentVectors(origin: Vector2): ArrayList<Vector2> { fun getAdjacentVectors(origin: Vector2): ArrayList<Vector2> {
val vectors = arrayListOf( val vectors = arrayListOf(
Vector2(1f, 0f), Vector2(1f, 0f),

View File

@ -1,7 +1,7 @@
package com.unciv.logic.civilization package com.unciv.logic.civilization
import com.unciv.Constants
import com.unciv.logic.city.CityInfo import com.unciv.logic.city.CityInfo
import com.unciv.logic.map.MapSize
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.models.ruleset.Unique import com.unciv.models.ruleset.Unique
@ -84,10 +84,10 @@ class TechManager {
// https://forums.civfanatics.com/threads/the-mechanics-of-overflow-inflation.517970/ // https://forums.civfanatics.com/threads/the-mechanics-of-overflow-inflation.517970/
techCost /= 1 + techsResearchedKnownCivs / undefeatedCivs.toFloat() * 0.3f techCost /= 1 + techsResearchedKnownCivs / undefeatedCivs.toFloat() * 0.3f
// http://www.civclub.net/bbs/forum.php?mod=viewthread&tid=123976 // http://www.civclub.net/bbs/forum.php?mod=viewthread&tid=123976
val worldSizeModifier = when (civInfo.gameInfo.tileMap.mapParameters.size) { val worldSizeModifier = when (civInfo.gameInfo.tileMap.mapParameters.mapSize.name) {
MapSize.Medium -> floatArrayOf(1.1f, 0.05f) Constants.medium -> floatArrayOf(1.1f, 0.05f)
MapSize.Large -> floatArrayOf(1.2f, 0.03f) Constants.large -> floatArrayOf(1.2f, 0.03f)
MapSize.Huge -> floatArrayOf(1.3f, 0.02f) Constants.huge -> floatArrayOf(1.3f, 0.02f)
else -> floatArrayOf(1f, 0.05f) else -> floatArrayOf(1f, 0.05f)
} }
techCost *= worldSizeModifier[0] techCost *= worldSizeModifier[0]

View File

@ -1,5 +1,10 @@
package com.unciv.logic.map package com.unciv.logic.map
import com.unciv.Constants
import com.unciv.logic.HexMath.getEquivalentHexagonalRadius
import com.unciv.logic.HexMath.getEquivalentRectangularSize
enum class MapSize(val radius: Int) { enum class MapSize(val radius: Int) {
Tiny(10), Tiny(10),
Small(15), Small(15),
@ -8,6 +13,44 @@ enum class MapSize(val radius: Int) {
Huge(40) Huge(40)
} }
class MapSizeNew {
var radius = 0
var width = 0
var height = 0
var name = ""
/** Needed for Json parsing */
constructor()
constructor(name: String) {
/** Hard coded values from getEquivalentRectangularSize() */
when (name) {
Constants.tiny -> { radius = 10; width = 23; height = 15 }
Constants.small -> { radius = 15; width = 33; height = 21 }
Constants.medium -> { radius = 20; width = 44; height = 29 }
Constants.large -> { radius = 30; width = 66; height = 43 }
Constants.huge -> { radius = 40; width = 87; height = 57 }
}
}
constructor(radius: Int) {
name = Constants.custom
this.radius = radius
val size = getEquivalentRectangularSize(radius)
this.width = size.x.toInt()
this.height = size.y.toInt()
}
constructor(width: Int, height: Int) {
name = Constants.custom
this.width = width
this.height = height
this.radius = getEquivalentHexagonalRadius(width, height)
}
}
object MapShape { object MapShape {
const val hexagonal = "Hexagonal" const val hexagonal = "Hexagonal"
const val rectangular = "Rectangular" const val rectangular = "Rectangular"
@ -33,7 +76,9 @@ class MapParameters {
var name = "" var name = ""
var type = MapType.pangaea var type = MapType.pangaea
var shape = MapShape.hexagonal var shape = MapShape.hexagonal
var size: MapSize = MapSize.Medium @Deprecated("replaced by mapSize since 3.19.18")
var size = MapSize.Medium
var mapSize = MapSizeNew(Constants.medium)
var noRuins = false var noRuins = false
var noNaturalWonders = false var noNaturalWonders = false
var worldWrap = false var worldWrap = false

View File

@ -21,16 +21,15 @@ class MapGenerator(val ruleset: Ruleset) {
private var randomness = MapGenerationRandomness() private var randomness = MapGenerationRandomness()
fun generateMap(mapParameters: MapParameters, seed: Long = System.currentTimeMillis()): TileMap { fun generateMap(mapParameters: MapParameters, seed: Long = System.currentTimeMillis()): TileMap {
val mapRadius = mapParameters.size.radius val mapSize = mapParameters.mapSize
val mapType = mapParameters.type val mapType = mapParameters.type
val map: TileMap val map: TileMap
if (mapParameters.shape == MapShape.rectangular) { if (mapParameters.shape == MapShape.rectangular) {
val size = HexMath.getEquivalentRectangularSize(mapRadius) val size = HexMath.getEquivalentRectangularSize(mapSize.radius)
map = TileMap(size.x.toInt(), size.y.toInt(), ruleset, mapParameters.worldWrap) map = TileMap(size.x.toInt(), size.y.toInt(), ruleset, mapParameters.worldWrap)
} }
else else map = TileMap(mapSize.radius, ruleset, mapParameters.worldWrap)
map = TileMap(mapRadius, ruleset, mapParameters.worldWrap)
map.mapParameters = mapParameters map.mapParameters = mapParameters
map.mapParameters.seed = seed map.mapParameters.seed = seed
@ -120,7 +119,7 @@ class MapGenerator(val ruleset: Ruleset) {
} }
private fun spreadResources(tileMap: TileMap) { private fun spreadResources(tileMap: TileMap) {
val distance = tileMap.mapParameters.size.radius val distance = tileMap.mapParameters.mapSize.radius
for (tile in tileMap.values) for (tile in tileMap.values)
tile.resource = null tile.resource = null

View File

@ -108,7 +108,7 @@ class MapLandmassGenerator(val randomness: MapGenerationRandomness) {
} }
private fun percentualDistanceToCenter(tileInfo: TileInfo, tileMap: TileMap): Double { private fun percentualDistanceToCenter(tileInfo: TileInfo, tileMap: TileMap): Double {
val mapRadius = tileMap.mapParameters.size.radius val mapRadius = tileMap.mapParameters.mapSize.radius
if (tileMap.mapParameters.shape == MapShape.hexagonal) if (tileMap.mapParameters.shape == MapShape.hexagonal)
return HexMath.getDistance(Vector2.Zero, tileInfo.position).toDouble() / mapRadius return HexMath.getDistance(Vector2.Zero, tileInfo.position).toDouble() / mapRadius
else { else {
@ -132,6 +132,8 @@ class MapLandmassGenerator(val randomness: MapGenerationRandomness) {
// region Cellular automata // region Cellular automata
private fun generateLandCellularAutomata(tileMap: TileMap) { private fun generateLandCellularAutomata(tileMap: TileMap) {
val mapRadius = tileMap.mapParameters.mapSize.radius
val mapType = tileMap.mapParameters.type
val numSmooth = 4 val numSmooth = 4
// init // init
@ -161,7 +163,7 @@ class MapLandmassGenerator(val randomness: MapGenerationRandomness) {
} }
private fun getInitialTerrainCellularAutomata(tileInfo: TileInfo, mapParameters: MapParameters): TerrainType { private fun getInitialTerrainCellularAutomata(tileInfo: TileInfo, mapParameters: MapParameters): TerrainType {
val mapRadius = mapParameters.size.radius val mapRadius = mapParameters.mapSize.radius
// default // default
if (HexMath.getDistance(Vector2.Zero, tileInfo.position) > 0.9f * mapRadius) { if (HexMath.getDistance(Vector2.Zero, tileInfo.position) > 0.9f * mapRadius) {

View File

@ -18,7 +18,7 @@ class NaturalWonderGenerator(val ruleset: Ruleset) {
fun spawnNaturalWonders(tileMap: TileMap, randomness: MapGenerationRandomness) { fun spawnNaturalWonders(tileMap: TileMap, randomness: MapGenerationRandomness) {
if (tileMap.mapParameters.noNaturalWonders) if (tileMap.mapParameters.noNaturalWonders)
return return
val mapRadius = tileMap.mapParameters.size.radius val mapRadius = tileMap.mapParameters.mapSize.radius
// number of Natural Wonders scales linearly with mapRadius as #wonders = mapRadius * 0.13133208 - 0.56128831 // number of Natural Wonders scales linearly with mapRadius as #wonders = mapRadius * 0.13133208 - 0.56128831
val numberToSpawn = round(mapRadius * 0.13133208f - 0.56128831f).toInt() val numberToSpawn = round(mapRadius * 0.13133208f - 0.56128831f).toInt()

View File

@ -1,8 +1,12 @@
package com.unciv.ui.newgamescreen package com.unciv.ui.newgamescreen
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.CheckBox import com.badlogic.gdx.scenes.scene2d.ui.CheckBox
import com.badlogic.gdx.scenes.scene2d.ui.Slider import com.badlogic.gdx.scenes.scene2d.ui.Slider
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.unciv.Constants
import com.unciv.logic.map.*
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.map.MapParameters import com.unciv.logic.map.MapParameters
import com.unciv.logic.map.MapShape import com.unciv.logic.map.MapShape
@ -20,16 +24,21 @@ import com.unciv.ui.utils.*
class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed: Boolean = false): class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed: Boolean = false):
Table() { Table() {
lateinit var mapTypeSelectBox: TranslatedSelectBox lateinit var mapTypeSelectBox: TranslatedSelectBox
lateinit var worldSizeSelectBox: TranslatedSelectBox
private var customWorldSizeTable = Table ()
private var hexagonalSizeTable = Table()
private var rectangularSizeTable = Table()
lateinit var noRuinsCheckbox: CheckBox lateinit var noRuinsCheckbox: CheckBox
lateinit var noNaturalWondersCheckbox: CheckBox lateinit var noNaturalWondersCheckbox: CheckBox
lateinit var worldWrapCheckbox: CheckBox lateinit var worldWrapCheckbox: CheckBox
init { init {
skin = CameraStageBaseScreen.skin skin = CameraStageBaseScreen.skin
defaults().pad(5f) defaults().pad(5f)
addMapShapeSelectBox() addMapShapeSelectBox()
addMapTypeSelectBox() addMapTypeSelectBox()
addWorldSizeSelectBox() addWorldSizeTable()
addNoRuinsCheckbox() addNoRuinsCheckbox()
addNoNaturalWondersCheckbox() addNoNaturalWondersCheckbox()
if (UncivGame.Current.settings.showExperimentalWorldWrap) { if (UncivGame.Current.settings.showExperimentalWorldWrap) {
@ -47,6 +56,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed
TranslatedSelectBox(mapShapes, mapParameters.shape, skin) TranslatedSelectBox(mapShapes, mapParameters.shape, skin)
mapShapeSelectBox.onChange { mapShapeSelectBox.onChange {
mapParameters.shape = mapShapeSelectBox.selected.value mapParameters.shape = mapShapeSelectBox.selected.value
updateWorldSizeTable()
} }
add ("{Map Shape}:".toLabel()).left() add ("{Map Shape}:".toLabel()).left()
@ -78,20 +88,79 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed
add(mapTypeSelectBox).fillX().row() add(mapTypeSelectBox).fillX().row()
} }
private fun addWorldSizeTable() {
private fun addWorldSizeSelectBox() { val mapSizes = listOfNotNull(
val worldSizeSelectBox = TranslatedSelectBox( Constants.tiny,
MapSize.values().map { it.name }, Constants.small,
mapParameters.size.name, Constants.medium,
skin Constants.large,
Constants.huge,
Constants.custom
) )
worldSizeSelectBox.onChange { worldSizeSelectBox = TranslatedSelectBox(mapSizes, mapParameters.mapSize.name, skin)
mapParameters.size = MapSize.valueOf(worldSizeSelectBox.selected.value) worldSizeSelectBox.onChange { updateWorldSizeTable() }
}
addHexagonalSizeTable()
addRectangularSizeTable()
add("{World Size}:".toLabel()).left() add("{World Size}:".toLabel()).left()
add(worldSizeSelectBox).fillX().row() add(worldSizeSelectBox).fillX().row()
add(customWorldSizeTable).colspan(2).grow().row()
updateWorldSizeTable()
}
private fun addHexagonalSizeTable() {
val defaultRadius = mapParameters.mapSize.radius.toString()
val customMapSizeRadius = TextField(defaultRadius, skin).apply {
textFieldFilter = TextField.TextFieldFilter.DigitsOnlyFilter()
}
customMapSizeRadius.onChange {
mapParameters.mapSize = MapSizeNew(customMapSizeRadius.text.toIntOrNull() ?: 0 )
}
hexagonalSizeTable.add("{Radius}:".toLabel()).grow().left()
hexagonalSizeTable.add(customMapSizeRadius).right().row()
hexagonalSizeTable.add("Anything above 40 may work very slowly on Android!".toLabel(Color.RED)
.apply { wrap=true }).width(prefWidth).colspan(hexagonalSizeTable.columns)
}
private fun addRectangularSizeTable() {
val defaultWidth = mapParameters.mapSize.width.toString()
val customMapWidth = TextField(defaultWidth, skin).apply {
textFieldFilter = TextField.TextFieldFilter.DigitsOnlyFilter()
}
val defaultHeight = mapParameters.mapSize.height.toString()
val customMapHeight = TextField(defaultHeight, skin).apply {
textFieldFilter = TextField.TextFieldFilter.DigitsOnlyFilter()
}
customMapWidth.onChange {
mapParameters.mapSize = MapSizeNew(customMapWidth.text.toIntOrNull() ?: 0, customMapHeight.text.toIntOrNull() ?: 0)
}
customMapHeight.onChange {
mapParameters.mapSize = MapSizeNew(customMapWidth.text.toIntOrNull() ?: 0, customMapHeight.text.toIntOrNull() ?: 0)
}
rectangularSizeTable.defaults().pad(5f)
rectangularSizeTable.add("{Width}:".toLabel()).grow().left()
rectangularSizeTable.add(customMapWidth).right().row()
rectangularSizeTable.add("{Height}:".toLabel()).grow().left()
rectangularSizeTable.add(customMapHeight).right().row()
rectangularSizeTable.add("Anything above 80 by 50 may work very slowly on Android!".toLabel(Color.RED)
.apply { wrap=true }).width(prefWidth).colspan(hexagonalSizeTable.columns)
}
private fun updateWorldSizeTable() {
customWorldSizeTable.clear()
if (mapParameters.shape == MapShape.hexagonal && worldSizeSelectBox.selected.value == Constants.custom)
customWorldSizeTable.add(hexagonalSizeTable).grow().row()
else if (mapParameters.shape == MapShape.rectangular && worldSizeSelectBox.selected.value == Constants.custom)
customWorldSizeTable.add(rectangularSizeTable).grow().row()
else
mapParameters.mapSize = MapSizeNew(worldSizeSelectBox.selected.value)
} }
private fun addNoRuinsCheckbox() { private fun addNoRuinsCheckbox() {

View File

@ -132,7 +132,6 @@ class MinimapHolder(mapHolder: WorldMapHolder): Table() {
pack() pack()
} }
private fun getWrappedMinimap(): Table { private fun getWrappedMinimap(): Table {
val internalMinimapWrapper = Table() val internalMinimapWrapper = Table()
internalMinimapWrapper.add(minimap) internalMinimapWrapper.add(minimap)

View File

@ -1,11 +1,12 @@
package com.unciv.app.desktop package com.unciv.app.desktop
import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.UncivGameParameters import com.unciv.UncivGameParameters
import com.unciv.logic.GameStarter import com.unciv.logic.GameStarter
import com.unciv.logic.civilization.PlayerType import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.map.MapParameters import com.unciv.logic.map.MapParameters
import com.unciv.logic.map.MapSize import com.unciv.logic.map.MapSizeNew
import com.unciv.models.metadata.GameParameters import com.unciv.models.metadata.GameParameters
import com.unciv.models.metadata.GameSettings import com.unciv.models.metadata.GameSettings
import com.unciv.models.metadata.GameSpeed import com.unciv.models.metadata.GameSpeed
@ -54,7 +55,7 @@ internal object ConsoleLauncher {
private fun getMapParameters(): MapParameters { private fun getMapParameters(): MapParameters {
return MapParameters().apply { return MapParameters().apply {
size = MapSize.Tiny mapSize = MapSizeNew(Constants.tiny)
noRuins = true noRuins = true
noNaturalWonders = true noNaturalWonders = true
} }