Fix custom map sizes - saves match, size obeyed, limit UI (#3965)

* Fix custom map sizes - Revive Enum MapSize and fix tech modifier for custom maps

* Fix custom map sizes - Fix saved size not matching tileset, obey custom size

* Fix custom map sizes - limiting custom size and UI

* Fix custom map sizes - linting and reduce warnings

* Fix custom map sizes - less verbose

* Popup gets a KeyPressDispatcher - templates
This commit is contained in:
SomeTroglodyte
2021-05-20 21:17:07 +02:00
committed by GitHub
parent f07c63c07f
commit 7c7d4181cc
13 changed files with 163 additions and 82 deletions

View File

@ -243,6 +243,10 @@ Small =
Medium =
Large =
Huge =
World wrap requires a minimum width of 32 tiles =
The provided map dimensions were too small =
The provided map dimensions were too big =
The provided map dimensions had an unacceptable aspect ratio =
Difficulty =

View File

@ -76,11 +76,5 @@ object Constants {
const val futureEra = "Future era"
const val barbarians = "Barbarians"
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,6 +10,7 @@ import com.unciv.logic.GameSaver
import com.unciv.logic.GameStarter
import com.unciv.logic.map.mapgenerator.MapGenerator
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.models.ruleset.RulesetCache
@ -51,7 +52,7 @@ class MainMenuScreen: CameraStageBaseScreen() {
thread(name = "ShowMapBackground") {
val newMap = MapGenerator(RulesetCache.getBaseRuleset())
.generateMap(MapParameters().apply { mapSize = MapSizeNew(Constants.small); type = MapType.default })
.generateMap(MapParameters().apply { mapSize = MapSizeNew(MapSize.Small); type = MapType.default })
Gdx.app.postRunnable { // for GL context
ImageGetter.setNewRuleset(RulesetCache.getBaseRuleset())
val mapHolder = EditorMapHolder(MapEditorScreen(), newMap)

View File

@ -1,7 +1,7 @@
package com.unciv.logic.civilization
import com.unciv.Constants
import com.unciv.logic.city.CityInfo
import com.unciv.logic.map.MapSize
import com.unciv.logic.map.RoadStatus
import com.unciv.logic.map.TileInfo
import com.unciv.models.ruleset.Unique
@ -24,7 +24,7 @@ class TechManager {
@Transient
private var researchedTechUniques = ArrayList<Unique>()
// MapUnit.canPassThrough is the most called function in the game, and having these extremey specific booleans is or way of improving the time cost
// MapUnit.canPassThrough is the most called function in the game, and having these extremely specific booleans is or way of improving the time cost
@Transient
var wayfinding = false
@Transient
@ -83,13 +83,15 @@ class TechManager {
.count { it.isMajorCiv() && !it.isDefeated() }
// https://forums.civfanatics.com/threads/the-mechanics-of-overflow-inflation.517970/
techCost /= 1 + techsResearchedKnownCivs / undefeatedCivs.toFloat() * 0.3f
// http://www.civclub.net/bbs/forum.php?mod=viewthread&tid=123976
val worldSizeModifier = when (civInfo.gameInfo.tileMap.mapParameters.mapSize.name) {
Constants.medium -> floatArrayOf(1.1f, 0.05f)
Constants.large -> floatArrayOf(1.2f, 0.03f)
Constants.huge -> floatArrayOf(1.3f, 0.02f)
// http://web.archive.org/web/20201204043641/http://www.civclub.net/bbs/forum.php?mod=viewthread&tid=123976
val worldSizeModifier = with (civInfo.gameInfo.tileMap.mapParameters.mapSize) {
when {
radius >= MapSize.Medium.radius -> floatArrayOf(1.1f, 0.05f)
radius >= MapSize.Large.radius -> floatArrayOf(1.2f, 0.03f)
radius >= MapSize.Huge.radius -> floatArrayOf(1.3f, 0.02f)
else -> floatArrayOf(1f, 0.05f)
}
}
techCost *= worldSizeModifier[0]
techCost *= 1 + (civInfo.cities.size - 1) * worldSizeModifier[1]
return techCost.toInt()

View File

@ -5,12 +5,12 @@ import com.unciv.logic.HexMath.getEquivalentHexagonalRadius
import com.unciv.logic.HexMath.getEquivalentRectangularSize
enum class MapSize(val radius: Int) {
Tiny(10),
Small(15),
Medium(20),
Large(30),
Huge(40)
enum class MapSize(val radius: Int, val width: Int, val height: Int) {
Tiny(10, 23, 15),
Small(15, 33, 21),
Medium(20, 44, 29),
Large(30, 66, 43),
Huge(40, 87, 57)
}
class MapSizeNew {
@ -20,17 +20,25 @@ class MapSizeNew {
var name = ""
/** Needed for Json parsing */
@Suppress("unused")
constructor()
private fun fromPredefined(predefined: MapSize) {
name = predefined.name
radius = predefined.radius
width = predefined.width
height = predefined.height
}
constructor(size: MapSize) {
fromPredefined(size)
}
constructor(name: String) {
this.name = name
/** 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 }
try {
fromPredefined(MapSize.valueOf(name))
} catch (_: Exception) {
fromPredefined(MapSize.Tiny)
}
}
@ -47,7 +55,42 @@ class MapSizeNew {
this.width = width
this.height = height
this.radius = getEquivalentHexagonalRadius(width, height)
}
/** Check custom dimensions, fix if too extreme
* @param worldWrap whether world wrap is on
* @return null if size was acceptable, otherwise untranslated reason message
*/
fun fixUndesiredSizes(worldWrap: Boolean): String? {
if (name != Constants.custom) return null // predefined sizes are OK
// world-wrap mas must always have an even width, so round down silently
if (worldWrap && width % 2 != 0 ) width--
// check for any bad condition and bail if none of them
val message = when {
worldWrap && width < 32 -> // otherwise horizontal scrolling will show edges, empirical
"World wrap requires a minimum width of 32 tiles"
width < 3 || height < 3 || radius < 2 ->
"The provided map dimensions were too small"
radius > 500 ->
"The provided map dimensions were too big"
height * 16 < width || width * 16 < height -> // aspect ratio > 16:1
"The provided map dimensions had an unacceptable aspect ratio"
else -> null
} ?: return null
// fix the size - not knowing whether hexagonal or rectangular is used
radius = when {
radius < 2 -> 2
radius > 500 -> 500
worldWrap && radius < 15 -> 15 // minimum for hexagonal but more than required for rectangular
else -> radius
}
val size = getEquivalentRectangularSize(radius)
width = size.x.toInt()
height = size.y.toInt()
// tell the caller that map dimensions have changed and why
return message
}
}
@ -78,7 +121,7 @@ class MapParameters {
var shape = MapShape.hexagonal
@Deprecated("replaced by mapSize since 3.14.7")
var size = MapSize.Medium
var mapSize = MapSizeNew(Constants.medium)
var mapSize = MapSizeNew(MapSize.Medium)
var noRuins = false
var noNaturalWonders = false
var worldWrap = false

View File

@ -25,10 +25,10 @@ class TileMap {
var bottomY = 0
@delegate:Transient
val maxLatitude: Float by lazy { if (values.isEmpty()) 0f else values.map { abs(it.latitude) }.max()!! }
val maxLatitude: Float by lazy { if (values.isEmpty()) 0f else values.map { abs(it.latitude) }.maxOrNull()!! }
@delegate:Transient
val maxLongitude: Float by lazy { if (values.isEmpty()) 0f else values.map { abs(it.longitude) }.max()!! }
val maxLongitude: Float by lazy { if (values.isEmpty()) 0f else values.map { abs(it.longitude) }.maxOrNull()!! }
@delegate:Transient
val naturalWonders: List<String> by lazy { tileList.asSequence().filter { it.isNaturalWonder() }.map { it.naturalWonder!! }.distinct().toList() }
@ -52,13 +52,18 @@ class TileMap {
/** generates a rectangular map of given width and height*/
constructor(width: Int, height: Int, ruleset: Ruleset, worldWrap: Boolean = false) {
val halfway = if (worldWrap) width / 2 - 1 else width / 2
for (x in -width / 2..halfway)
for (y in -height / 2..height / 2)
// world-wrap maps must always have an even width, so round down
val wrapAdjustedWidth = if (worldWrap && width % 2 != 0 ) width -1 else width
// Even widths will have coordinates ranging -x..(x-1), not -x..x, which is always an odd-sized range
// e.g. w=4 -> -2..1, w=5 -> -2..2, w=6 -> -3..2, w=7 -> -3..3
for (x in -wrapAdjustedWidth / 2 .. (wrapAdjustedWidth-1) / 2)
for (y in -height / 2 .. (height-1) / 2)
tileList.add(TileInfo().apply {
position = HexMath.evenQ2HexCoords(Vector2(x.toFloat(), y.toFloat()))
baseTerrain = Constants.grassland
})
setTransients(ruleset)
}

View File

@ -21,13 +21,11 @@ class MapGenerator(val ruleset: Ruleset) {
fun generateMap(mapParameters: MapParameters, seed: Long = System.currentTimeMillis()): TileMap {
val mapSize = mapParameters.mapSize
val mapType = mapParameters.type
val map: TileMap
if (mapParameters.shape == MapShape.rectangular) {
val size = HexMath.getEquivalentRectangularSize(mapSize.radius)
map = TileMap(size.x.toInt(), size.y.toInt(), ruleset, mapParameters.worldWrap)
}
else map = TileMap(mapSize.radius, ruleset, mapParameters.worldWrap)
val map: TileMap = if (mapParameters.shape == MapShape.rectangular)
TileMap(mapSize.width, mapSize.height, ruleset, mapParameters.worldWrap)
else
TileMap(mapSize.radius, ruleset, mapParameters.worldWrap)
map.mapParameters = mapParameters
map.mapParameters.seed = seed
@ -232,8 +230,8 @@ class MapGenerator(val ruleset: Ruleset) {
continue
}
val matchingTerrain = ruleset.terrains.values.firstOrNull {
it.uniqueObjects.any {
val matchingTerrain = ruleset.terrains.values.firstOrNull { terrain ->
terrain.uniqueObjects.any {
it.placeholderText == "Occurs at temperature between [] and [] and humidity between [] and []"
&& it.params[0].toFloat() < temperature && temperature <= it.params[1].toFloat()
&& it.params[2].toFloat() < humidity && humidity <= it.params[3].toFloat()

View File

@ -3,6 +3,7 @@ package com.unciv.ui.mapeditor
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.MainMenuScreen
import com.unciv.UncivGame
import com.unciv.logic.map.MapParameters
import com.unciv.logic.map.TileMap
import com.unciv.logic.map.mapgenerator.MapGenerator
@ -20,14 +21,16 @@ class NewMapScreen(val mapParameters: MapParameters = MapParameters()) : PickerS
private val ruleset = RulesetCache.getBaseRuleset()
private var generatedMap: TileMap? = null
private val mapParametersTable: MapParametersTable
init {
setDefaultCloseAction(MainMenuScreen())
mapParametersTable = MapParametersTable(mapParameters, isEmptyMapAllowed = true)
val newMapScreenOptionsTable = Table(skin).apply {
pad(10f)
add("Map Options".toLabel(fontSize = 24)).row()
add(MapParametersTable(mapParameters, isEmptyMapAllowed = true)).row()
add(mapParametersTable).row()
add(ModCheckboxTable(mapParameters.mods, this@NewMapScreen) {
ruleset.clear()
val newRuleset = RulesetCache.getComplexRuleset(mapParameters.mods)
@ -49,6 +52,18 @@ class NewMapScreen(val mapParameters: MapParameters = MapParameters()) : PickerS
rightButtonSetEnabled(true)
rightSideButton.onClick {
val message = mapParameters.mapSize.fixUndesiredSizes(mapParameters.worldWrap)
if (message != null) {
Gdx.app.postRunnable {
ToastPopup( message, UncivGame.Current.screen as CameraStageBaseScreen, 4000 )
with (mapParameters.mapSize) {
mapParametersTable.customMapSizeRadius.text = radius.toString()
mapParametersTable.customMapWidth.text = width.toString()
mapParametersTable.customMapHeight.text = height.toString()
}
}
return@onClick
}
Gdx.input.inputProcessor = null // remove input processing - nothing will be clicked!
rightButtonSetEnabled(false)

View File

@ -17,7 +17,7 @@ class MapOptionsTable(val newGameScreen: NewGameScreen): Table() {
val mapParameters = newGameScreen.gameSetupInfo.mapParameters
private var mapTypeSpecificTable = Table()
private val generatedMapOptionsTable = MapParametersTable(mapParameters)
val generatedMapOptionsTable = MapParametersTable(mapParameters)
private val savedMapOptionsTable = Table()
lateinit var mapTypeSelectBox: TranslatedSelectBox
@ -115,6 +115,6 @@ class MapOptionsTable(val newGameScreen: NewGameScreen): Table() {
// The SelectBox auto displays the text a object.toString(), which on the FileHandle itself includes the folder path.
// So we wrap it in another object with a custom toString()
class FileHandleWrapper(val fileHandle: FileHandle) {
override fun toString() = fileHandle.name()
override fun toString(): String = fileHandle.name()
}
}

View File

@ -7,10 +7,7 @@ import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextField
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.map.MapParameters
import com.unciv.logic.map.MapShape
import com.unciv.logic.map.MapSizeNew
import com.unciv.logic.map.MapType
import com.unciv.logic.map.*
import com.unciv.models.translations.tr
import com.unciv.ui.utils.*
@ -30,6 +27,9 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed
lateinit var noRuinsCheckbox: CheckBox
lateinit var noNaturalWondersCheckbox: CheckBox
lateinit var worldWrapCheckbox: CheckBox
lateinit var customMapSizeRadius: TextField
lateinit var customMapWidth: TextField
lateinit var customMapHeight: TextField
init {
@ -88,15 +88,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed
}
private fun addWorldSizeTable() {
val mapSizes = listOfNotNull(
Constants.tiny,
Constants.small,
Constants.medium,
Constants.large,
Constants.huge,
Constants.custom
)
val mapSizes = MapSize.values().map { it.name } + listOf(Constants.custom)
worldSizeSelectBox = TranslatedSelectBox(mapSizes, mapParameters.mapSize.name, skin)
worldSizeSelectBox.onChange { updateWorldSizeTable() }
@ -112,7 +104,7 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed
private fun addHexagonalSizeTable() {
val defaultRadius = mapParameters.mapSize.radius.toString()
val customMapSizeRadius = TextField(defaultRadius, skin).apply {
customMapSizeRadius = TextField(defaultRadius, skin).apply {
textFieldFilter = TextField.TextFieldFilter.DigitsOnlyFilter()
}
customMapSizeRadius.onChange {
@ -126,12 +118,12 @@ class MapParametersTable(val mapParameters: MapParameters, val isEmptyMapAllowed
private fun addRectangularSizeTable() {
val defaultWidth = mapParameters.mapSize.width.toString()
val customMapWidth = TextField(defaultWidth, skin).apply {
customMapWidth = TextField(defaultWidth, skin).apply {
textFieldFilter = TextField.TextFieldFilter.DigitsOnlyFilter()
}
val defaultHeight = mapParameters.mapSize.height.toString()
val customMapHeight = TextField(defaultHeight, skin).apply {
customMapHeight = TextField(defaultHeight, skin).apply {
textFieldFilter = TextField.TextFieldFilter.DigitsOnlyFilter()
}

View File

@ -5,6 +5,7 @@ import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.scenes.scene2d.ui.SelectBox
import com.badlogic.gdx.scenes.scene2d.ui.Skin
import com.badlogic.gdx.utils.Array
import com.unciv.UncivGame
import com.unciv.logic.*
import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.map.MapParameters
@ -38,12 +39,12 @@ class GameSetupInfo(var gameId:String, var gameParameters: GameParameters, var m
class NewGameScreen(private val previousScreen: CameraStageBaseScreen, _gameSetupInfo: GameSetupInfo?=null): IPreviousScreen, PickerScreen(disableScroll = true) {
override val gameSetupInfo = _gameSetupInfo ?: GameSetupInfo()
override var ruleset = RulesetCache.getComplexRuleset(gameSetupInfo.gameParameters.mods) // needs to be set because the gameoptionstable etc. depend on this
override var ruleset = RulesetCache.getComplexRuleset(gameSetupInfo.gameParameters.mods) // needs to be set because the GameOptionsTable etc. depend on this
var newGameOptionsTable = GameOptionsTable(this) { desiredCiv: String -> playerPickerTable.update(desiredCiv) }
// Has to be defined before the mapOptionsTable, since the mapOptionsTable refers to it on init
private var playerPickerTable = PlayerPickerTable(this, gameSetupInfo.gameParameters)
var mapOptionsTable = MapOptionsTable(this)
private var mapOptionsTable = MapOptionsTable(this)
init {
@ -100,13 +101,29 @@ class NewGameScreen(private val previousScreen: CameraStageBaseScreen, _gameSetu
if (rulesetIncompatibilities.isNotEmpty()) {
val incompatibleMap = Popup(this)
incompatibleMap.addGoodSizedLabel("Map is incompatible with the chosen ruleset!".tr()).row()
for(incompat in rulesetIncompatibilities)
incompatibleMap.addGoodSizedLabel(incompat).row()
for(incompatibility in rulesetIncompatibilities)
incompatibleMap.addGoodSizedLabel(incompatibility).row()
incompatibleMap.addCloseButton()
incompatibleMap.open()
game.setScreen(this) // to get the input back
return@onClick
}
} else {
// Generated map - check for sensible dimensions and if exceeded correct them and notify user
val mapSize = gameSetupInfo.mapParameters.mapSize
val message = mapSize.fixUndesiredSizes(gameSetupInfo.mapParameters.worldWrap)
if (message != null) {
Gdx.app.postRunnable {
ToastPopup( message, UncivGame.Current.screen as CameraStageBaseScreen, 4000 )
with (mapOptionsTable.generatedMapOptionsTable) {
customMapSizeRadius.text = mapSize.radius.toString()
customMapWidth.text = mapSize.width.toString()
customMapHeight.text = mapSize.height.toString()
}
}
game.setScreen(this) // to get the input back
return@onClick
}
}
rightSideButton.disable()

View File

@ -14,6 +14,8 @@ import com.badlogic.gdx.utils.Align
import com.unciv.UncivGame
import com.unciv.logic.HexMath
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.MapShape
import com.unciv.logic.map.MapSize
import com.unciv.logic.map.TileInfo
import com.unciv.ui.utils.IconCircleGroup
import com.unciv.ui.utils.ImageGetter
@ -35,11 +37,19 @@ class Minimap(val mapHolder: WorldMapHolder, minimapSize: Int) : Table(){
var bottomX = 0f
var bottomY = 0f
fun hexRow(vector2: Vector2) = vector2.x + vector2.y
val maxHexRow = mapHolder.tileMap.values.asSequence().map { hexRow(it.position) }.maxOrNull()!!
val minHexRow = mapHolder.tileMap.values.asSequence().map { hexRow(it.position) }.minOrNull()!!
val totalHexRows = maxHexRow - minHexRow
val groupSize = (minimapSize + 1) * 200f / totalHexRows
// fun hexRow(vector2: Vector2) = vector2.x + vector2.y
// val maxHexRow = mapHolder.tileMap.values.asSequence().map { hexRow(it.position) }.maxOrNull()!!
// val minHexRow = mapHolder.tileMap.values.asSequence().map { hexRow(it.position) }.minOrNull()!!
// val totalHexRows = maxHexRow - minHexRow
// val groupSize = (minimapSize + 1) * 200f / totalHexRows
// On hexagonal maps totalHexRows as calculated above is always 2 * radius.
// Support rectangular maps with extreme aspect ratios by scaling to the larger coordinate with a slight weighting to make the bounding box 4:3
val effectiveRadius = with(mapHolder.tileMap.mapParameters) {
if (shape != MapShape.rectangular) mapSize.radius
else max (mapSize.height, mapSize.width * 3 / 4) * MapSize.Huge.radius / MapSize.Huge.height
}
val groupSize = (minimapSize + 1) * 100f / effectiveRadius
for (tileInfo in mapHolder.tileMap.values) {
val hex = ImageGetter.getImage("OtherIcons/Hexagon")
@ -78,7 +88,7 @@ class Minimap(val mapHolder: WorldMapHolder, minimapSize: Int) : Table(){
/**### Transform and set coordinates for the scrollPositionIndicator.
*
* Relies on the [MiniMap]'s copy of the main [WorldMapHolder] as input.
* Relies on the [MiniMap][MinimapHolder.minimap]'s copy of the main [WorldMapHolder] as input.
*
* Requires [scrollPositionIndicator] to be a [ClippingImage] to keep the displayed portion of the indicator within the bounds of the minimap.
*/

View File

@ -1,11 +1,11 @@
package com.unciv.app.desktop
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.UncivGameParameters
import com.unciv.logic.GameStarter
import com.unciv.logic.civilization.PlayerType
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.GameSettings
@ -45,7 +45,7 @@ internal object ConsoleLauncher {
val newGame = GameStarter.startNewGame(gameSetupInfo)
UncivGame.Current.gameInfo = newGame
var simulation = Simulation(newGame,10,4)
val simulation = Simulation(newGame,10,4)
simulation.start()
@ -55,7 +55,7 @@ internal object ConsoleLauncher {
private fun getMapParameters(): MapParameters {
return MapParameters().apply {
mapSize = MapSizeNew(Constants.tiny)
mapSize = MapSizeNew(MapSize.Tiny)
noRuins = true
noNaturalWonders = true
}