From 996c58e1fb2c68015f4a945450b1706065342e20 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Wed, 27 Apr 2022 07:19:04 +0200 Subject: [PATCH] Some improvements to Map Editor - step 1 (#6622) --- .../jsons/translations/template.properties | 3 +- core/src/com/unciv/MainMenuScreen.kt | 6 +- core/src/com/unciv/logic/MapSaver.kt | 2 +- .../map/mapgenerator/MapLandmassGenerator.kt | 30 ++--- .../unciv/ui/civilopedia/CivilopediaText.kt | 11 +- core/src/com/unciv/ui/images/ImageGetter.kt | 10 ++ .../com/unciv/ui/mapeditor/EditorMapHolder.kt | 117 ++++++++++++++++-- .../ui/mapeditor/MapEditorEditSubTabs.kt | 20 +-- .../unciv/ui/mapeditor/MapEditorEditTab.kt | 31 +---- .../unciv/ui/mapeditor/MapEditorFilesTable.kt | 12 +- .../ui/mapeditor/MapEditorGenerateTab.kt | 74 +++++++---- .../unciv/ui/mapeditor/MapEditorLoadTab.kt | 7 +- .../unciv/ui/mapeditor/MapEditorMainTabs.kt | 2 +- .../unciv/ui/mapeditor/MapEditorSaveTab.kt | 5 + .../com/unciv/ui/mapeditor/MapEditorScreen.kt | 33 ++--- .../ui/mapeditor/MapEditorToolsDrawer.kt | 64 ++++++++-- .../unciv/ui/mapeditor/MapEditorViewTab.kt | 2 +- .../unciv/ui/mapeditor/MapGeneratorSteps.kt | 2 +- .../unciv/ui/newgamescreen/MapOptionsTable.kt | 2 +- .../pickerscreens/ImprovementPickerScreen.kt | 8 +- 20 files changed, 299 insertions(+), 142 deletions(-) diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index c85086f3e4..03ea1ed1a4 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -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 = diff --git a/core/src/com/unciv/MainMenuScreen.kt b/core/src/com/unciv/MainMenuScreen.kt index 80ef666f34..0ccb439be0 100644 --- a/core/src/com/unciv/MainMenuScreen.kt +++ b/core/src/com/unciv/MainMenuScreen.kt @@ -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) }, diff --git a/core/src/com/unciv/logic/MapSaver.kt b/core/src/com/unciv/logic/MapSaver.kt index fffccc1e1e..2b7de2ad88 100644 --- a/core/src/com/unciv/logic/MapSaver.kt +++ b/core/src/com/unciv/logic/MapSaver.kt @@ -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") diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapLandmassGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapLandmassGenerator.kt index f9aca44ce2..872730a7ba 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/MapLandmassGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/MapLandmassGenerator.kt @@ -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 } \ No newline at end of file diff --git a/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt b/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt index 03b7cadb96..e9256d2fd9 100644 --- a/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt +++ b/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt @@ -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) } diff --git a/core/src/com/unciv/ui/images/ImageGetter.kt b/core/src/com/unciv/ui/images/ImageGetter.kt index f00a7725c3..28e6bc6583 100644 --- a/core/src/com/unciv/ui/images/ImageGetter.kt +++ b/core/src/com/unciv/ui/images/ImageGetter.kt @@ -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) diff --git a/core/src/com/unciv/ui/mapeditor/EditorMapHolder.kt b/core/src/com/unciv/ui/mapeditor/EditorMapHolder.kt index 7e3c4dbd36..93dab7dfa6 100644 --- a/core/src/com/unciv/ui/mapeditor/EditorMapHolder.kt +++ b/core/src/com/unciv/ui/mapeditor/EditorMapHolder.kt @@ -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>() private lateinit var tileGroupMap: TileGroupMap private val allTileGroups = ArrayList() @@ -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() + private var savedListeners = emptyList() + 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) + } } diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorEditSubTabs.kt b/core/src/com/unciv/ui/mapeditor/MapEditorEditSubTabs.kt index e3a8f795ba..8531930f71 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorEditSubTabs.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorEditSubTabs.kt @@ -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 diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorEditTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorEditTab.kt index 2a83e91668..23f208abfb 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorEditTab.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorEditTab.kt @@ -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) } diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorFilesTable.kt b/core/src/com/unciv/ui/mapeditor/MapEditorFilesTable.kt index 75def9f39b..16a4c63af3 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorFilesTable.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorFilesTable.kt @@ -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 + } } diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorGenerateTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorGenerateTab.kt index 36a9197e5f..558edecd31 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorGenerateTab.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorGenerateTab.kt @@ -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) { diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorLoadTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorLoadTab.kt index e59bea0f22..dc282619d6 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorLoadTab.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorLoadTab.kt @@ -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() } \ No newline at end of file diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorMainTabs.kt b/core/src/com/unciv/ui/mapeditor/MapEditorMainTabs.kt index 980a498172..45bf36572e 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorMainTabs.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorMainTabs.kt @@ -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')) diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorSaveTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorSaveTab.kt index 6c65b9b751..7d03a56a63 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorSaveTab.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorSaveTab.kt @@ -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 diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt b/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt index ec74dd80a7..10951a61e5 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt @@ -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() } diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorToolsDrawer.kt b/core/src/com/unciv/ui/mapeditor/MapEditorToolsDrawer.kt index 3a5fcb42c9..6f56d0c53b 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorToolsDrawer.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorToolsDrawer.kt @@ -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) } } diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorViewTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorViewTab.kt index fd3206b0cf..050003d7e8 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorViewTab.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorViewTab.kt @@ -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) } diff --git a/core/src/com/unciv/ui/mapeditor/MapGeneratorSteps.kt b/core/src/com/unciv/ui/mapeditor/MapGeneratorSteps.kt index 24d8a81a41..86d01af49b 100644 --- a/core/src/com/unciv/ui/mapeditor/MapGeneratorSteps.kt +++ b/core/src/com/unciv/ui/mapeditor/MapGeneratorSteps.kt @@ -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"), diff --git a/core/src/com/unciv/ui/newgamescreen/MapOptionsTable.kt b/core/src/com/unciv/ui/newgamescreen/MapOptionsTable.kt index 52fd6ae5fb..a5f256c48f 100644 --- a/core/src/com/unciv/ui/newgamescreen/MapOptionsTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/MapOptionsTable.kt @@ -27,7 +27,7 @@ class MapOptionsTable(private val newGameScreen: NewGameScreen): Table() { private val mapFilesSequence = sequence { 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()) } diff --git a/core/src/com/unciv/ui/pickerscreens/ImprovementPickerScreen.kt b/core/src/com/unciv/ui/pickerscreens/ImprovementPickerScreen.kt index 2e5c647669..366037a47a 100644 --- a/core/src/com/unciv/ui/pickerscreens/ImprovementPickerScreen.kt +++ b/core/src/com/unciv/ui/pickerscreens/ImprovementPickerScreen.kt @@ -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 }