Some improvements to Map Editor - step 1 (#6622)

This commit is contained in:
SomeTroglodyte 2022-04-27 07:19:04 +02:00 committed by GitHub
parent 4ad9d69424
commit 996c58e1fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 299 additions and 142 deletions

View File

@ -271,7 +271,6 @@ Military near City-State =
Sum: =
# Trades
Trade =
@ -488,6 +487,7 @@ Continent: [param] ([amount] tiles) =
Change map to fit selected ruleset? =
Area: [amount] tiles, [amount2] continents/islands =
Do you want to leave without saving the recent changes? =
Do you want to load another map without saving the recent changes? =
Invalid map: Area ([area]) does not match saved dimensions ([dimensions]). =
The dimensions have now been fixed for you. =
River generation failed! =
@ -503,6 +503,7 @@ Sprout vegetation =
Spawn rare features =
Distribute ice =
Assign continent IDs =
Place Natural Wonders =
Let the rivers flow =
Spread Resources =
Create ancient ruins =

View File

@ -75,14 +75,10 @@ class MainMenuScreen: BaseScreen() {
.generateMap(MapParameters().apply { mapSize = MapSizeNew(MapSize.Small); type = MapType.default })
postCrashHandlingRunnable { // for GL context
ImageGetter.setNewRuleset(RulesetCache.getVanillaRuleset())
val mapHolder = EditorMapHolder(MapEditorScreen(), newMap) {}
val mapHolder = EditorMapHolder(this, newMap) {}
backgroundTable.addAction(Actions.sequence(
Actions.fadeOut(0f),
Actions.run {
mapHolder.apply {
addTiles(this@MainMenuScreen.stage)
touchable = Touchable.disabled
}
backgroundTable.addActor(mapHolder)
mapHolder.center(backgroundTable)
},

View File

@ -9,7 +9,7 @@ object MapSaver {
fun json() = GameSaver.json()
private const val mapsFolder = "maps"
const val mapsFolder = "maps"
var saveZipped = true
private fun getMap(mapName:String) = Gdx.files.local("$mapsFolder/$mapName")

View File

@ -10,9 +10,13 @@ import kotlin.math.min
import kotlin.math.pow
class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRandomness) {
//region _Fields
private val firstLandTerrain = ruleset.terrains.values.first { it.type==TerrainType.Land }
private val landTerrainName = firstLandTerrain.name
private val firstWaterTerrain = ruleset.terrains.values.firstOrNull { it.type==TerrainType.Water }
private val waterTerrainName = firstWaterTerrain?.name ?: ""
private var waterThreshold = 0.0
//endregion
fun generateLand(tileMap: TileMap) {
// This is to accommodate land-only mods
@ -22,6 +26,8 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
return
}
waterThreshold = tileMap.mapParameters.waterThreshold.toDouble()
when (tileMap.mapParameters.type) {
MapType.pangaea -> createPangaea(tileMap)
MapType.innerSea -> createInnerSea(tileMap)
@ -33,11 +39,8 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
}
}
private fun spawnLandOrWater(tile: TileInfo, elevation: Double, threshold: Double) {
when {
elevation < threshold -> tile.baseTerrain = firstWaterTerrain!!.name
else -> tile.baseTerrain = firstLandTerrain.name
}
private fun spawnLandOrWater(tile: TileInfo, elevation: Double) {
tile.baseTerrain = if (elevation < waterThreshold) waterTerrainName else landTerrainName
}
/**
@ -62,15 +65,16 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
val elevationSeed = randomness.RNG.nextInt().toDouble()
for (tile in tileMap.values) {
val elevation = randomness.getPerlinNoise(tile, elevationSeed)
spawnLandOrWater(tile, elevation, tileMap.mapParameters.waterThreshold.toDouble())
spawnLandOrWater(tile, elevation)
}
}
private fun createArchipelago(tileMap: TileMap) {
val elevationSeed = randomness.RNG.nextInt().toDouble()
waterThreshold += 0.25
for (tile in tileMap.values) {
val elevation = getRidgedPerlinNoise(tile, elevationSeed)
spawnLandOrWater(tile, elevation, 0.25 + tileMap.mapParameters.waterThreshold.toDouble())
spawnLandOrWater(tile, elevation)
}
}
@ -79,7 +83,7 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
for (tile in tileMap.values) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed)
elevation = (elevation + getEllipticContinent(tile, tileMap)) / 2.0
spawnLandOrWater(tile, elevation, tileMap.mapParameters.waterThreshold.toDouble())
spawnLandOrWater(tile, elevation)
}
}
@ -88,7 +92,7 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
for (tile in tileMap.values) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed)
elevation -= getEllipticContinent(tile, tileMap, 0.6) * 0.3
spawnLandOrWater(tile, elevation, tileMap.mapParameters.waterThreshold.toDouble())
spawnLandOrWater(tile, elevation)
}
}
@ -97,7 +101,7 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
for (tile in tileMap.values) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed)
elevation = (elevation + getTwoContinentsTransform(tile, tileMap)) / 2.0
spawnLandOrWater(tile, elevation, tileMap.mapParameters.waterThreshold.toDouble())
spawnLandOrWater(tile, elevation)
}
}
@ -106,7 +110,7 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
for (tile in tileMap.values) {
var elevation = randomness.getPerlinNoise(tile, elevationSeed)
elevation = (elevation + getFourCornersTransform(tile, tileMap)) / 2.0
spawnLandOrWater(tile, elevation, tileMap.mapParameters.waterThreshold.toDouble())
spawnLandOrWater(tile, elevation)
}
}
@ -184,7 +188,6 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
return Perlin.ridgedNoise3d(worldCoords.x.toDouble(), worldCoords.y.toDouble(), seed, nOctaves, persistence, lacunarity, scale)
}
// region Cellular automata
private fun generateLandCellularAutomata(tileMap: TileMap) {
for (tile in tileMap.values) {
@ -196,5 +199,4 @@ class MapLandmassGenerator(val ruleset: Ruleset, val randomness: MapGenerationRa
smoothen(tileMap)
}
// endregion
}

View File

@ -316,16 +316,7 @@ class FormattedLine (
val image = category.getImage?.invoke(parts[1], iconSize) ?: return 0
if (iconCrossed) {
val cross = ImageGetter.getRedCross(iconSize * 0.7f, 0.7f)
val group = Group().apply {
isTransform = false
setSize(iconSize, iconSize)
image.center(this)
addActor(image)
cross.center(this)
addActor(cross)
}
table.add(group).size(iconSize).padRight(iconPad)
table.add(ImageGetter.getCrossedImage(image, iconSize)).size(iconSize).padRight(iconPad)
} else {
table.add(image).size(iconSize).padRight(iconPad)
}

View File

@ -344,6 +344,16 @@ object ImageGetter {
return redCross
}
fun getCrossedImage(image: Actor, iconSize: Float) = Group().apply {
isTransform = false
setSize(iconSize, iconSize)
image.center(this)
addActor(image)
val cross = getRedCross(iconSize * 0.7f, 0.7f)
cross.center(this)
addActor(cross)
}
fun getArrowImage(align:Int = Align.right): Image {
val image = getImage("OtherIcons/ArrowRight")
image.setOrigin(Align.center)

View File

@ -1,7 +1,9 @@
package com.unciv.ui.mapeditor
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.math.Vector2
import com.badlogic.gdx.scenes.scene2d.Stage
import com.badlogic.gdx.scenes.scene2d.*
import com.badlogic.gdx.scenes.scene2d.actions.Actions
import com.unciv.UncivGame
import com.unciv.logic.HexMath
import com.unciv.logic.map.TileInfo
@ -11,11 +13,18 @@ import com.unciv.ui.tilegroups.TileGroup
import com.unciv.ui.tilegroups.TileSetStrings
import com.unciv.ui.utils.*
/**
* This MapHolder is used both for the Map Editor and the Main Menu background!
* @param parentScreen a MapEditorScreen or a MainMenuScreen
*/
class EditorMapHolder(
parentScreen: BaseScreen,
internal val tileMap: TileMap,
private val onTileClick: (TileInfo) -> Unit
): ZoomableScrollPane() {
val editorScreen = parentScreen as? MapEditorScreen
val tileGroups = HashMap<TileInfo, List<TileGroup>>()
private lateinit var tileGroupMap: TileGroupMap<TileGroup>
private val allTileGroups = ArrayList<TileGroup>()
@ -23,9 +32,16 @@ class EditorMapHolder(
private val maxWorldZoomOut = UncivGame.Current.settings.maxWorldZoomOut
private val minZoomScale = 1f / maxWorldZoomOut
private var blinkAction: Action? = null
private var savedCaptureListeners = emptyList<EventListener>()
private var savedListeners = emptyList<EventListener>()
init {
if (editorScreen == null) touchable = Touchable.disabled
continuousScrollingX = tileMap.mapParameters.worldWrap
addTiles(parentScreen.stage)
if (editorScreen != null) addCaptureListener(getDragPaintListener())
}
internal fun addTiles(stage: Stage) {
@ -72,7 +88,8 @@ class EditorMapHolder(
*/
tileGroup.showEntireMap = true
tileGroup.update()
tileGroup.onClick { onTileClick(tileGroup.tileInfo) }
if (touchable != Touchable.disabled)
tileGroup.onClick { onTileClick(tileGroup.tileInfo) }
}
setSize(stage.width * maxWorldZoomOut, stage.height * maxWorldZoomOut)
@ -105,22 +122,100 @@ class EditorMapHolder(
return null
}
// Currently unused, drag painting will need it
fun getClosestTileTo(stageCoords: Vector2): TileInfo? {
val positionalCoords = tileGroupMap.getPositionalVector(stageCoords)
val hexPosition = HexMath.world2HexCoords(positionalCoords)
val rounded = HexMath.roundHexCoords(hexPosition)
return tileMap.getOrNull(rounded)
}
fun setCenterPosition(vector: Vector2) {
fun setCenterPosition(vector: Vector2, blink: Boolean = false) {
val tileGroup = allTileGroups.firstOrNull { it.tileInfo.position == vector } ?: return
scrollX = tileGroup.x + tileGroup.width / 2 - width / 2
scrollY = maxY - (tileGroup.y + tileGroup.width / 2 - height / 2)
if (!blink) return
removeAction(blinkAction) // so we don't have multiple blinks at once
blinkAction = Actions.repeat(3, Actions.sequence(
Actions.run { tileGroup.highlightImage.isVisible = false },
Actions.delay(.3f),
Actions.run { tileGroup.highlightImage.isVisible = true },
Actions.delay(.3f)
))
addAction(blinkAction) // Don't set it on the group because it's an actionless group
}
override fun zoom(zoomScale: Float) {
if (zoomScale < minZoomScale || zoomScale > 2f) return
setScale(zoomScale)
}
/*
The ScrollPane interferes with the dragging listener of MapEditorToolsDrawer.
Once the ZoomableScrollPane super is initialized, there are 3 listeners + 1 capture listener:
listeners[0] = ZoomableScrollPane.getFlickScrollListener()
listeners[1] = ZoomableScrollPane.addZoomListeners: override fun scrolled (MouseWheel)
listeners[2] = ZoomableScrollPane.addZoomListeners: override fun zoom (Android pinch)
captureListeners[0] = ScrollPane.addCaptureListener: touchDown, touchUp, touchDragged, mouseMoved
Clearing and putting back the captureListener _should_ suffice, but in practice it doesn't.
Therefore, save all listeners when they're hurting us, and put them back when needed.
*/
internal fun killListeners() {
savedCaptureListeners = captureListeners.toList()
savedListeners = listeners.toList()
clearListeners()
}
internal fun resurrectListeners() {
val captureListenersToAdd = savedCaptureListeners
savedCaptureListeners = emptyList()
val listenersToAdd = savedListeners
savedListeners = emptyList()
for (listener in listenersToAdd) addListener(listener)
for (listener in captureListenersToAdd) addCaptureListener(listener)
}
/** Factory to create the listener that does "paint by dragging"
* Should only be called if this MapHolder is used from MapEditorScreen
*/
private fun getDragPaintListener(): InputListener {
return object : InputListener() {
var isDragging = false
var isPainting = false
var touchDownTime = System.currentTimeMillis()
override fun touchDown(event: InputEvent?, x: Float, y: Float, pointer: Int, button: Int): Boolean {
touchDownTime = System.currentTimeMillis()
return true
}
override fun touchDragged(event: InputEvent?, x: Float, y: Float, pointer: Int) {
if (!isDragging) {
isDragging = true
val deltaTime = System.currentTimeMillis() - touchDownTime
if (deltaTime > 400) {
isPainting = true
stage.cancelTouchFocusExcept(this, this@EditorMapHolder)
}
}
if (!isPainting) return
editorScreen!!.hideSelection()
val stageCoords = actor.stageToLocalCoordinates(Vector2(event!!.stageX, event.stageY))
val centerTileInfo = getClosestTileTo(stageCoords)
?: return
editorScreen.tabs.edit.paintTilesWithBrush(centerTileInfo)
}
override fun touchUp(event: InputEvent?, x: Float, y: Float, pointer: Int, button: Int) {
// Reset the whole map
if (isPainting) {
updateTileGroups()
setTransients()
}
isDragging = false
isPainting = false
}
}
}
fun getClosestTileTo(stageCoords: Vector2): TileInfo? {
val positionalCoords = tileGroupMap.getPositionalVector(stageCoords)
val hexPosition = HexMath.world2HexCoords(positionalCoords)
val rounded = HexMath.roundHexCoords(hexPosition)
return tileMap.getOrNull(rounded)
}
}

View File

@ -68,7 +68,7 @@ class MapEditorEditFeaturesTab(
val eraserIcon = "Terrain/${firstFeature.name}"
val eraser = FormattedLine("Remove features", icon = eraserIcon, size = 32, iconCrossed = true)
add(eraser.render(0f).apply { onClick {
editTab.setBrush("Remove feature", eraserIcon) { tile ->
editTab.setBrush("Remove features", eraserIcon, true) { tile ->
tile.removeTerrainFeatures()
}
} }).padBottom(0f).row()
@ -139,7 +139,7 @@ class MapEditorEditResourcesTab(
val eraserIcon = "Resource/${firstResource.name}"
val eraser = FormattedLine("Remove resource", icon = eraserIcon, size = 32, iconCrossed = true)
add(eraser.render(0f).apply { onClick {
editTab.setBrush("Remove resource", eraserIcon) { tile ->
editTab.setBrush("Remove resource", eraserIcon, true) { tile ->
tile.resource = null
}
} }).padBottom(0f).row()
@ -186,7 +186,7 @@ class MapEditorEditImprovementsTab(
val eraserIcon = "Improvement/${firstImprovement.name}"
val eraser = FormattedLine("Remove improvement", icon = eraserIcon, size = 32, iconCrossed = true)
add(eraser.render(0f).apply { onClick {
editTab.setBrush("Remove improvement", eraserIcon) { tile ->
editTab.setBrush("Remove improvement", eraserIcon, true) { tile ->
tile.improvement = null
tile.roadStatus = RoadStatus.None
}
@ -259,7 +259,7 @@ class MapEditorEditStartsTab(
val eraserIcon = "Nation/${firstNation.name}"
val eraser = FormattedLine("Remove starting locations", icon = eraserIcon, size = 24, iconCrossed = true)
add(eraser.render(0f).apply { onClick {
editTab.setBrush(BrushHandlerType.Direct, "Remove starting locations", eraserIcon) { tile ->
editTab.setBrush(BrushHandlerType.Direct, "Remove starting locations", eraserIcon, true) { tile ->
tile.tileMap.removeStartingLocations(tile.position)
}
} }).padBottom(0f).row()
@ -406,16 +406,8 @@ class MapEditorEditRiversTab(
}
}
}.makeTileGroup()
private fun getRemoveRiverIcon() = Group().apply {
isTransform = false
setSize(iconSize, iconSize)
val tileGroup = getTileGroupWithRivers(RiverEdge.All)
tileGroup.center(this)
addActor(tileGroup)
val cross = ImageGetter.getRedCross(iconSize * 0.7f, 1f)
cross.center(this)
addActor(cross)
}
private fun getRemoveRiverIcon() =
ImageGetter.getCrossedImage(getTileGroupWithRivers(RiverEdge.All), iconSize)
private fun getRiverIcon(edge: RiverEdge) = Group().apply {
// wrap same as getRemoveRiverIcon so the icons align the same (using getTileGroupWithRivers directly works but looks ugly - reason unknown to me)
isTransform = false

View File

@ -107,21 +107,12 @@ class MapEditorEditTab(
private fun selectPage(index: Int) = subTabs.selectPage(index)
fun setBrush(
name: String,
icon: String,
isRemove: Boolean = false,
applyAction: (TileInfo)->Unit
) {
fun setBrush(name: String, icon: String, isRemove: Boolean = false, applyAction: (TileInfo)->Unit) {
brushHandlerType = BrushHandlerType.Tile
brushCell.setActor(FormattedLine(name, icon = icon, iconCrossed = isRemove).render(0f))
brushAction = applyAction
}
private fun setBrush(
name: String,
icon: Actor,
applyAction: (TileInfo)->Unit
) {
private fun setBrush(name: String, icon: Actor, applyAction: (TileInfo)->Unit) {
brushHandlerType = BrushHandlerType.Tile
val line = Table().apply {
add(icon).padRight(10f)
@ -130,22 +121,12 @@ class MapEditorEditTab(
brushCell.setActor(line)
brushAction = applyAction
}
fun setBrush(
handlerType: BrushHandlerType,
name: String,
icon: String,
isRemove: Boolean = false,
applyAction: (TileInfo)->Unit
) {
fun setBrush(handlerType: BrushHandlerType, name: String, icon: String,
isRemove: Boolean = false, applyAction: (TileInfo)->Unit) {
setBrush(name, icon, isRemove, applyAction)
brushHandlerType = handlerType
}
fun setBrush(
handlerType: BrushHandlerType,
name: String,
icon: Actor,
applyAction: (TileInfo)->Unit
) {
fun setBrush(handlerType: BrushHandlerType, name: String, icon: Actor, applyAction: (TileInfo)->Unit) {
setBrush(name, icon, applyAction)
brushHandlerType = handlerType
}
@ -236,7 +217,7 @@ class MapEditorEditTab(
resultingTiles.forEach { editorScreen.updateAndHighlight(it, Color.SKY) }
}
private fun paintTilesWithBrush(tile: TileInfo) {
internal fun paintTilesWithBrush(tile: TileInfo) {
val tiles =
if (brushSize == -1) {
val bfs = BFS(tile) { it.isSimilarEnough(tile) }

View File

@ -62,7 +62,7 @@ class MapEditorFilesTable(
)
if (includeMods) {
for (modFolder in RulesetCache.values.mapNotNull { it.folderLocation }) {
val mapsFolder = modFolder.child("maps")
val mapsFolder = modFolder.child(MapSaver.mapsFolder)
if (mapsFolder.exists())
sortedFiles.addAll(
mapsFolder.list()
@ -92,4 +92,14 @@ class MapEditorFilesTable(
}
layout()
}
fun noMapsAvailable(includeMods: Boolean = false): Boolean {
if (MapSaver.getMaps().any()) return true
if (!includeMods) return false
for (modFolder in RulesetCache.values.mapNotNull { it.folderLocation }) {
val mapsFolder = modFolder.child(MapSaver.mapsFolder)
if (mapsFolder.exists() && mapsFolder.list().any()) return true
}
return false
}
}

View File

@ -4,8 +4,11 @@ import com.badlogic.gdx.Gdx
import com.badlogic.gdx.scenes.scene2d.ui.ButtonGroup
import com.badlogic.gdx.scenes.scene2d.ui.CheckBox
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.logic.map.MapParameters
import com.unciv.logic.map.MapType
import com.unciv.logic.map.TileMap
import com.unciv.logic.map.mapgenerator.MapGenerator
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.translations.tr
import com.unciv.ui.images.ImageGetter
@ -48,6 +51,11 @@ class MapEditorGenerateTab(
}
private fun generate(step: MapGeneratorSteps) {
if (step <= MapGeneratorSteps.Landmass && step in seedUsedForStep) {
// reseed visibly when starting from scratch (new seed shows in advanced settings widget)
newTab.mapParametersTable.reseed()
seedUsedForStep -= step
}
val mapParameters = editorScreen.newMapParameters.clone() // this clone is very important here
val message = mapParameters.mapSize.fixUndesiredSizes(mapParameters.worldWrap)
if (message != null) {
@ -76,29 +84,53 @@ class MapEditorGenerateTab(
Gdx.input.inputProcessor = null // remove input processing - nothing will be clicked!
setButtonsEnabled(false)
thread(name = "MapGenerator") {
fun freshMapCompleted(generatedMap: TileMap, mapParameters: MapParameters, newRuleset: Ruleset, selectPage: Int) {
MapEditorScreen.saveDefaultParameters(mapParameters)
editorScreen.loadMap(generatedMap, newRuleset, selectPage) // also reactivates inputProcessor
editorScreen.isDirty = true
setButtonsEnabled(true)
}
fun stepCompleted(step: MapGeneratorSteps) {
if (step == MapGeneratorSteps.NaturalWonders) editorScreen.naturalWondersNeedRefresh = true
editorScreen.mapHolder.updateTileGroups()
editorScreen.isDirty = true
setButtonsEnabled(true)
Gdx.input.inputProcessor = editorScreen.stage
}
// Map generation can take a while and we don't want ANRs
thread(name = "MapGenerator", isDaemon = true) {
try {
// Map generation can take a while and we don't want ANRs
if (step == MapGeneratorSteps.All) {
val newRuleset = RulesetCache.getComplexRuleset(mapParameters.mods, mapParameters.baseRuleset)
val generatedMap = MapGenerator(newRuleset).generateMap(mapParameters)
Gdx.app.postRunnable {
MapEditorScreen.saveDefaultParameters(mapParameters)
editorScreen.loadMap(generatedMap, newRuleset)
editorScreen.isDirty = true
setButtonsEnabled(true)
Gdx.input.inputProcessor = editorScreen.stage
val (newRuleset, generator) = if (step > MapGeneratorSteps.Landmass) null to null
else {
val newRuleset = RulesetCache.getComplexRuleset(mapParameters.mods, mapParameters.baseRuleset)
newRuleset to MapGenerator(newRuleset)
}
} else {
MapGenerator(editorScreen.ruleset).generateSingleStep(editorScreen.tileMap, step)
Gdx.app.postRunnable {
if (step == MapGeneratorSteps.NaturalWonders) editorScreen.naturalWondersNeedRefresh = true
editorScreen.mapHolder.updateTileGroups()
editorScreen.isDirty = true
setButtonsEnabled(true)
Gdx.input.inputProcessor = editorScreen.stage
when (step) {
MapGeneratorSteps.All -> {
val generatedMap = generator!!.generateMap(mapParameters)
Gdx.app.postRunnable {
freshMapCompleted(generatedMap, mapParameters, newRuleset!!, selectPage = 0)
}
}
MapGeneratorSteps.Landmass -> {
// This step _could_ run on an existing tileMap, but that opens a loophole where you get hills on water - fixing that is more expensive than always recreating
mapParameters.type = MapType.empty
val generatedMap = generator!!.generateMap(mapParameters)
mapParameters.type = editorScreen.newMapParameters.type
generator.generateSingleStep(generatedMap, step)
val savedScale = editorScreen.mapHolder.scaleX
Gdx.app.postRunnable {
freshMapCompleted(generatedMap, mapParameters, newRuleset!!, selectPage = 1)
editorScreen.mapHolder.zoom(savedScale)
}
}
else -> {
editorScreen.tileMap.mapParameters.seed = mapParameters.seed
MapGenerator(editorScreen.ruleset).generateSingleStep(editorScreen.tileMap, step)
Gdx.app.postRunnable {
stepCompleted(step)
}
}
}
} catch (exception: Exception) {

View File

@ -47,7 +47,9 @@ class MapEditorLoadTab(
private fun loadHandler() {
if (chosenMap == null) return
thread(name = "MapLoader", block = this::loaderThread)
editorScreen.askIfDirty("Do you want to load another map without saving the recent changes?") {
thread(name = "MapLoader", isDaemon = true, block = this::loaderThread)
}
}
private fun deleteHandler() {
@ -119,7 +121,6 @@ class MapEditorLoadTab(
editorScreen.loadMap(map)
needPopup = false
popup?.close()
Gdx.input.inputProcessor = stage
} catch (ex: Throwable) {
needPopup = false
popup?.close()
@ -138,4 +139,6 @@ class MapEditorLoadTab(
}
}
}
fun noMapsAvailable() = mapFiles.noMapsAvailable()
}

View File

@ -37,7 +37,7 @@ class MapEditorMainTabs(
addPage("Load", load,
ImageGetter.getImage("OtherIcons/Load"), 25f,
shortcutKey = KeyCharAndCode.ctrl('l'),
disabled = MapSaver.getMaps().isEmpty())
disabled = load.noMapsAvailable())
addPage("Save", save,
ImageGetter.getImage("OtherIcons/Checkmark"), 25f,
shortcutKey = KeyCharAndCode.ctrl('s'))

View File

@ -27,6 +27,8 @@ class MapEditorSaveTab(
private val saveButton = "Save map".toTextButton()
private val deleteButton = "Delete map".toTextButton()
private val quitButton = "Exit map editor".toTextButton()
private val mapNameTextField = TextField("", skin)
private var chosenMap: FileHandle? = null
@ -48,6 +50,9 @@ class MapEditorSaveTab(
deleteButton.onClick(this::deleteHandler)
buttonTable.add(deleteButton)
quitButton.onClick(editorScreen::closeEditor)
buttonTable.add(quitButton)
buttonTable.pack()
val fileTableHeight = editorScreen.stage.height - headerHeight - mapNameTextField.prefHeight - buttonTable.height - 22f

View File

@ -2,6 +2,7 @@ package com.unciv.ui.mapeditor
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
import com.unciv.MainMenuScreen
import com.unciv.UncivGame
import com.unciv.logic.HexMath
@ -17,26 +18,28 @@ import com.unciv.ui.popup.YesNoPopup
import com.unciv.ui.tilegroups.TileGroup
import com.unciv.ui.utils.*
//todo normalize properly
//todo drag painting - migrate from old editor
//todo Nat Wonder step generator: *New* wonders?
//todo functional Tab for Units
//todo copy/paste tile areas? (As tool tab, brush sized, floodfill forbidden, tab displays copied area)
//todo Synergy with Civilopedia for drawing loose tiles / terrain icons
//todo left-align everything so a half-open drawer is more useful
//todo combined brush
//todo Load should check isDirty before discarding and replacing the current map
//todo New function `convertTerrains` is auto-run after rivers the right decision for step-wise generation? Will paintRiverFromTo need the same? Will painting manually need the conversion?
//todo work in Simon's changes to continent/landmass
//todo work in Simon's regions - check whether generate and store or discard is the way
//todo Regions: If relevant, view and possibly work in Simon's colored visualization
//todo Strategic Resource abundance control
//todo Tooltips for Edit items with info on placeability? Place this info as Brush description? In Expander?
//todo Civilopedia links from edit items by right-click/long-tap?
//todo Mod tab change base ruleset - disableAllCheckboxes - instead some intelligence to leave those mods on that stay compatible?
//todo The setSkin call in newMapHolder belongs in ImageGetter.setNewRuleset and should be intelligent as resetFont is expensive and the probability a mod touched a few EmojiIcons is low
//todo new brush: remove natural wonder
//todo "random nation" starting location (maybe no new internal representation = all major nations)
//todo Nat Wonder step generator: Needs tweaks to avoid placing duplicates or wonders too close together
//todo Music? Different suffix? Off? 20% Volume?
//todo Unciv Europe Map Example - does not load due to "Gold / Gold ore": Solve problem that multiple errors are not shown nicely, and re-enable fixing the map and displaying it
//todo See #6610 - re-layout after the map size dropdown changes to custom and new widgets are inserted - can reach "Create" only by dragging the _header_ of the sub-TabbedPager
class MapEditorScreen(map: TileMap? = null): BaseScreen() {
/** The map being edited, with mod list for that map */
@ -82,7 +85,7 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen() {
isDirty = false
tabs = MapEditorMainTabs(this)
MapEditorToolsDrawer(tabs, stage)
MapEditorToolsDrawer(tabs, stage, mapHolder)
// The top level pager assigns its own key bindings, but making nested TabbedPagers bind keys
// so all levels select to show the tab in question is too complex. Sub-Tabs need to maintain
@ -141,7 +144,7 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen() {
return result
}
fun loadMap(map: TileMap, newRuleset: Ruleset? = null) {
fun loadMap(map: TileMap, newRuleset: Ruleset? = null, selectPage: Int = 0) {
mapHolder.remove()
tileMap = map
checkAndFixMapSize()
@ -149,10 +152,8 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen() {
RulesetCache.getComplexRuleset(map.mapParameters.mods, map.mapParameters.baseRuleset)
mapHolder = newMapHolder()
isDirty = false
Gdx.app.postRunnable {
// Doing this directly freezes the game, despite loadMap already running under postRunnable
tabs.selectPage(0)
}
Gdx.input.inputProcessor = stage
tabs.selectPage(selectPage) // must be done _after_ resetting inputProcessor!
}
fun getMapCloneForSave() =
@ -171,10 +172,14 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen() {
}
internal fun closeEditor() {
if (!isDirty) return game.setScreen(MainMenuScreen())
YesNoPopup("Do you want to leave without saving the recent changes?", action = {
askIfDirty("Do you want to leave without saving the recent changes?") {
game.setScreen(MainMenuScreen())
}, screen = this, restoreDefault = {
}
}
fun askIfDirty(question: String, action: ()->Unit) {
if (!isDirty) return action()
YesNoPopup(question, action, screen = this, restoreDefault = {
keyPressDispatcher[KeyCharAndCode.BACK] = this::closeEditor
}).open()
}

View File

@ -1,24 +1,32 @@
package com.unciv.ui.mapeditor
import com.badlogic.gdx.Application
import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.InputEvent
import com.badlogic.gdx.scenes.scene2d.InputListener
import com.badlogic.gdx.scenes.scene2d.Stage
import com.badlogic.gdx.scenes.scene2d.Touchable
import com.badlogic.gdx.scenes.scene2d.actions.FloatAction
import com.badlogic.gdx.scenes.scene2d.ui.Container
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.utils.Align
import com.unciv.ui.images.ImageGetter
import com.unciv.ui.utils.BaseScreen
import com.unciv.ui.utils.addSeparatorVertical
import kotlin.math.abs
class MapEditorToolsDrawer(
tabs: MapEditorMainTabs,
initStage: Stage
initStage: Stage,
private val mapHolder: EditorMapHolder
): Table(BaseScreen.skin) {
companion object {
const val handleWidth = 10f
private companion object {
const val arrowImage = "OtherIcons/BackArrow"
const val animationDuration = 0.333f
const val clickEpsilon = 0.001f
}
private val handleWidth = if (Gdx.app.type == Application.ApplicationType.Desktop) 10f else 25f
private val arrowSize = if (Gdx.app.type == Application.ApplicationType.Desktop) 10f else 20f //todo tweak on actual phone
var splitAmount = 1f
set(value) {
@ -26,22 +34,35 @@ class MapEditorToolsDrawer(
reposition()
}
private val arrowIcon = ImageGetter.getImage(arrowImage)
init {
touchable = Touchable.childrenOnly
addSeparatorVertical(Color.CLEAR, handleWidth) // the "handle"
arrowIcon.setSize(arrowSize, arrowSize)
arrowIcon.setOrigin(Align.center)
arrowIcon.rotation = 180f
val arrowWrapper = Container(arrowIcon)
arrowWrapper.align(Align.center)
arrowWrapper.setSize(arrowSize, arrowSize)
arrowWrapper.setOrigin(Align.center)
add(arrowWrapper).align(Align.center).width(handleWidth).fillY().apply { // the "handle"
background = ImageGetter.getBackground(BaseScreen.skin.get("color", Color::class.java))
}
add(tabs)
.height(initStage.height)
.fill().top()
pack()
setPosition(initStage.width, 0f, Align.bottomRight)
initStage.addActor(this)
initStage.addListener(getListener(this))
initStage.addCaptureListener(getListener(this))
}
private class SplitAmountAction(
private val drawer: MapEditorToolsDrawer,
endAmount: Float
): FloatAction(drawer.splitAmount, endAmount, 0.333f) {
): FloatAction(drawer.splitAmount, endAmount, animationDuration) {
override fun act(delta: Float): Boolean {
val result = super.act(delta)
drawer.splitAmount = value
@ -59,6 +80,7 @@ class MapEditorToolsDrawer(
if (draggingPointer != -1) return false
if (pointer == 0 && button != 0) return false
if (x !in drawer.x..(drawer.x + handleWidth)) return false
mapHolder.killListeners()
draggingPointer = pointer
lastX = x
handleX = drawer.x
@ -67,6 +89,7 @@ class MapEditorToolsDrawer(
}
override fun touchUp(event: InputEvent, x: Float, y: Float, pointer: Int, button: Int) {
if (pointer != draggingPointer) return
mapHolder.resurrectListeners()
draggingPointer = -1
if (oldSplitAmount < 0f) return
addAction(SplitAmountAction(drawer, if (splitAmount > 0.5f) 0f else 1f))
@ -74,17 +97,32 @@ class MapEditorToolsDrawer(
override fun touchDragged(event: InputEvent, x: Float, y: Float, pointer: Int) {
if (pointer != draggingPointer) return
val delta = x - lastX
val availWidth = stage.width - handleWidth
handleX += delta
lastX = x
splitAmount = ((availWidth - handleX) / drawer.width).coerceIn(0f, 1f)
if (oldSplitAmount >= 0f && abs(oldSplitAmount - splitAmount) >= 0.0001f) oldSplitAmount = -1f
handleX += delta
splitAmount = drawer.xToSplitAmount(handleX)
if (oldSplitAmount >= 0f && abs(oldSplitAmount - splitAmount) >= clickEpsilon ) oldSplitAmount = -1f
}
}
// single-use helpers placed together for readability. One should be the exact inverse of the other except for the clamping.
private fun splitAmountToX() =
stage.width - width + (1f - splitAmount) * (width - handleWidth)
private fun xToSplitAmount(x: Float) =
(1f - (x + width - stage.width) / (width - handleWidth)).coerceIn(0f, 1f)
fun reposition() {
if (stage == null) return
val dx = stage.width + (1f - splitAmount) * (width - handleWidth)
setPosition(dx, 0f, Align.bottomRight)
when (splitAmount) {
0f -> {
arrowIcon.rotation = 0f
arrowIcon.isVisible = true
}
1f -> {
arrowIcon.rotation = 180f
arrowIcon.isVisible = true
}
else -> arrowIcon.isVisible = false
}
setPosition(splitAmountToX(), 0f, Align.bottomLeft)
}
}

View File

@ -214,7 +214,7 @@ class MapEditorViewTab(
if (tiles.isEmpty()) return
if (roundRobinIndex >= tiles.size) roundRobinIndex = 0
val tile = tiles[roundRobinIndex++]
editorScreen.mapHolder.setCenterPosition(tile.position)
editorScreen.mapHolder.setCenterPosition(tile.position, blink = true)
tileClickHandler(tile)
}

View File

@ -42,7 +42,7 @@ enum class MapGeneratorSteps(
RareFeatures("Spawn rare features", MapGeneratorStepsHelpers.applyRareFeatures),
Ice("Distribute ice"),
Continents("Assign continent IDs"),
NaturalWonders("Natural Wonders"),
NaturalWonders("Place Natural Wonders"),
Rivers("Let the rivers flow"),
Resources("Spread Resources", MapGeneratorStepsHelpers.applyResources),
AncientRuins("Create ancient ruins"),

View File

@ -27,7 +27,7 @@ class MapOptionsTable(private val newGameScreen: NewGameScreen): Table() {
private val mapFilesSequence = sequence<FileHandle> {
yieldAll(MapSaver.getMaps().asSequence())
for (modFolder in RulesetCache.values.mapNotNull { it.folderLocation }) {
val mapsFolder = modFolder.child("maps")
val mapsFolder = modFolder.child(MapSaver.mapsFolder)
if (mapsFolder.exists())
yieldAll(mapsFolder.list().asSequence())
}

View File

@ -163,12 +163,8 @@ class ImprovementPickerScreen(
// icon for removing the resource by replacing improvement
if (removeImprovement && tileInfo.hasViewableResource(currentPlayerCiv) && tileInfo.tileResource.improvement == tileInfo.improvement) {
val crossedResource = Group()
val cross = ImageGetter.getRedCross(30f, 0.8f)
val resourceIcon = ImageGetter.getResourceImage(tileInfo.resource.toString(), 30f)
crossedResource.addActor(resourceIcon)
crossedResource.addActor(cross)
statIcons.add(crossedResource).padTop(30f).padRight(33f)
val resourceIcon = ImageGetter.getResourceImage(tileInfo.resource!!, 30f)
statIcons.add(ImageGetter.getCrossedImage(resourceIcon, 30f))
}
return statIcons
}