diff --git a/android/assets/jsons/translations/German.properties b/android/assets/jsons/translations/German.properties index d029aa7a8c..7dc7c3535c 100644 --- a/android/assets/jsons/translations/German.properties +++ b/android/assets/jsons/translations/German.properties @@ -10,9 +10,13 @@ StartWithCapitalLetter = true -# Starting from here normal translations start, as written on +# Starting from here normal translations start, as described in # https://yairm210.github.io/Unciv/Other/Translating/ +# Base ruleset names +Civ V - Vanilla = Civ V - Vanilla +Civ V - Gods & Kings = Civ V - Götter & Könige + # Tutorial tasks Move a unit!\nClick on a unit > Click on a destination > Click the arrow popup = Eine Einheit bewegen!\nKlicke auf eine Einheit > Klicke auf ein Ziel > Klicke auf das Pfeil-Popup. @@ -87,7 +91,6 @@ Add to queue = Hinzufügen zur Warteschlange Remove from queue = Entferne aus Warteschlange Show stats drilldown = Zeige Statistiken Show construction queue = Zeige Produktionswarteschlange -Save = Speichern Cancel = Abbrechen Diplomacy = Diplomatie @@ -320,8 +323,6 @@ Map Type = Kartentyp Map file = Karten-Datei Max Turns = Maximale Runden Could not load map! = Diese Karte konnte nicht geladen werden! -Invalid map: Area ([area]) does not match saved dimensions ([dimensions]). = Üngültige Karte: Die Fläche ([area]) stimmt nicht mit den gespeicherten Abmessungen ([dimensions]) überein. -The dimensions have now been fixed for you. = Die Abmessungen wurden für dich korrigiert. Generated = Generiert Existing = Bestehende Custom = Benutzerdefiniert @@ -432,6 +433,80 @@ World wrap maps are very memory intensive - creating large world wrap maps on An Anything above 80 by 50 may work very slowly on Android! = Auf Android kann alles über 80 mal 50 sehr langsam sein. Anything above 40 may work very slowly on Android! = Auf Android kann alles über 40 sehr langsam sein. +# Map editor + +## Tabs/Buttons +Map editor = Karteneditor +View = Ansehen +Generate = Generieren +Partial = Partiell +Generator steps = Generator-Schritte +Edit = Bearbeiten +Rivers = Flüsse +Load = Laden +Save = Speichern +New map = Neue Karte +Empty = Leer +Save map = Karte speichern +Exit map editor = Karteneditor verlassen +Change map ruleset = Regelsatz ändern +Change the map to use the ruleset selected on this page = Diesen Regelsatz für die\naktuelle Karte speichern und anwenden +Revert to map ruleset = Regelsatz zurücksetzen +Reset the controls to reflect the current map ruleset = Den Regelsatz der Karte wieder\nin diesen Steuerelementen anzeigen +Features = Geländemerkmale +Starting locations = Startpositionen +Tile Matching Criteria = Genauigkeit Feldvergleich +Complete match = Exakte Übereinstimmung +Except improvements = Außer Verbesserungen +Base and terrain features = Gelände und -Merkmale +Base terrain only = Gelände +Land or water only = Nur Land/Wasser + +## Labels/messages +Brush ([size]): = Pinsel ([size]): +# The letter shown in the [size] parameter above for setting "Floodfill" +F = A +Error loading map! = Fehler beim Laden der Karte +Map saved successfully! = Karte erfolgreich gespeichert! +It looks like your map can't be saved! = Deine Karte kann aus irgendeinem Grund nicht gespeichert werden! +Current map RNG seed: [amount] = Seed der aktuellen Karte: [amount] +Map copy and paste = Karte kopieren/einfügen +Position: [param] = Position: [param] +Starting location(s): [param] = Startpositionen: [param] +Continent: [param] ([amount] tiles) = Kontinent: [param] ([amount] Felder) +Change map to fit selected ruleset? = Karte ändern, um sie dem neuen Regelsatz anzupassen? +Area: [amount] tiles, [amount2] continents/islands = Fläche: [amount] Felder, [amount2] Kontinente/Inseln +Do you want to leave without saving the recent changes? = Willst Du wirklich den Editor verlassen ohne die Änderungen zu speichern? +Invalid map: Area ([area]) does not match saved dimensions ([dimensions]). = Ungültige Karte: Die Fläche ([area]) stimmt nicht mit den gespeicherten Abmessungen ([dimensions]) überein. +The dimensions have now been fixed for you. = Die Abmessungen wurden für dich korrigiert. +River generation failed! = Flüsse generieren ist fehlgeschlagen! +Please don't use step 'Landmass' with map type 'Empty', create a new empty map instead. = Bitte statt Einzelschritt 'Landmasse generieren' mit Typ 'Leer' gleich eine neue leere Karte generieren! + +## Map/Tool names +My new map = Meine neue Karte +Generate landmass = Landmasse generieren +Raise mountains and hills = Geländeerhebungen +Humidity and temperature = Feuchtigkeit und Temperatur +Lakes and coastline = Seen und Küstenlinien +Sprout vegetation = Pflanzen sprießen lassen +Spawn rare features = Außergewöhnliches Gelände +Distribute ice = Eis verteilen +Assign continent IDs = Kontinent-IDs zuweisen +Let the rivers flow = Flüsse fließen lassen +Spread Resources = Ressourcen verteilen +Create ancient ruins = Ruinen verteilen +Floodfill = Ausfüllen +[nation] starting location = Startposition von [nation] +Remove features = Geländemerkmale entfernen +Remove improvement = Verbesserungen entfernen +Remove resource = Ressource entfernen +Remove starting locations = Startpositionen entfernen +Remove rivers = Flüsse entfernen +Spawn river from/to = Fluß von/nach generieren +Bottom left river = Fluss unten links +Bottom right river = Fluss unten rechts +Bottom river = Fluss unten + # Multiplayer Help = Hilfe @@ -492,9 +567,6 @@ Saved at = Gespeichert um Load map = Karte laden Delete map = Karte löschen Are you sure you want to delete this map? = Bist du dir sicher, dass du diese Karte löschen möchtest? -Upload map = Karte hochladen -Could not upload map! = Karte konnte nicht hochgeladen werden! -Map uploaded successfully! = Karte wurde erfolgreich hochgeladen! Saving... = Speichere... Overwrite existing file? = Vorhandene Datei überschreiben? It looks like your saved game can't be loaded! = Dieser Spielstand konnte nicht geladen werden! @@ -1095,41 +1167,16 @@ for = für Missing translations: = Fehlende Übersetzungen: Resolution = Auflösung Tileset = Feldgrafik-Satz -Map editor = Karteneditor Create = Erstellen -New map = Neue Karte -Empty = Leer Language = Sprache -Terrains & Resources = Gelände & Ressourcen Improvements = Verbesserungen -Clear current map = Lösche aktuelle Karte -Save map = Karte speichern -Download map = Karte herunterladen Loading... = Lade... -Error loading map! = Fehler beim Laden der Karte Filter: = Filter: OK = OK -Exit map editor = Karteneditor verlassen -[nation] starting location = Startposition von [nation] -Clear terrain features = Lösche Geländemerkmale -Clear improvements = Lösche Verbesserungen -Clear resource = Lösche Ressource -Remove units = Entferne Einheiten -Player [index] = Spieler [index] -Player [playerIndex] starting location = Spieler [playerIndex] Startgebiet -Bottom left river = Fluss unten links -Bottom right river = Fluss unten rechts -Bottom river = Fluss unten -Requires = Benötigt -Menu = Menü -Brush Size = Pinselgröße -Map saved = Karte gespeichert -Change ruleset = Regelsatz ändern Base terrain [terrain] does not exist in ruleset! = Gelände [terrain] fehlt im Regelsatz! Terrain feature [feature] does not exist in ruleset! = Geländemerkmal [feature] fehlt im Regelsatz! Resource [resource] does not exist in ruleset! = Ressource [resource] fehlt im Regelsatz! Improvement [improvement] does not exist in ruleset! = Verbesserung [improvement] fehlt im Regelsatz! -Change map to fit selected ruleset? = Karte ändern, um sie dem neuen Regelsatz anzupassen? # Civilopedia difficulty levels Player settings = Spieler-Einstellungen diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index a177690871..8660ce66ea 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -11,9 +11,13 @@ StartWithCapitalLetter = -# Starting from here normal translations start, as written on +# Starting from here normal translations start, as described in # https://yairm210.github.io/Unciv/Other/Translating/ +# Base ruleset names +Civ V - Vanilla = +Civ V - Gods & Kings = + # Tutorial tasks Move a unit!\nClick on a unit > Click on a destination > Click the arrow popup = @@ -88,7 +92,6 @@ Add to queue = Remove from queue = Show stats drilldown = Show construction queue = -Save = Cancel = Diplomacy = @@ -324,8 +327,6 @@ Map Type = Map file = Max Turns = Could not load map! = -Invalid map: Area ([area]) does not match saved dimensions ([dimensions]). = -The dimensions have now been fixed for you. = Generated = Existing = Custom = @@ -437,6 +438,84 @@ World wrap maps are very memory intensive - creating large world wrap maps on An Anything above 80 by 50 may work very slowly on Android! = Anything above 40 may work very slowly on Android! = +# Map editor + +## Tabs/Buttons +Map editor = +View = +Generate = +Partial = +Generator steps = +Edit = +Rivers = +Load = +Save = +New map = +Empty = +Save map = +Load map = +Delete map = +Are you sure you want to delete this map? = +It looks like your map can't be saved! = +Exit map editor = +Change map ruleset = +Change the map to use the ruleset selected on this page = +Revert to map ruleset = +Reset the controls to reflect the current map ruleset = +Features = +Starting locations = +Tile Matching Criteria = +Complete match = +Except improvements = +Base and terrain features = +Base terrain only = +Land or water only = + +## Labels/messages +Brush ([size]): = +# The letter shown in the [size] parameter above for setting "Floodfill" +F = +Error loading map! = +Map saved successfully! = +It looks like your map can't be saved! = +Current map RNG seed: [amount] = +Map copy and paste = +Position: [param] = +Starting location(s): [param] = +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? = +Invalid map: Area ([area]) does not match saved dimensions ([dimensions]). = +The dimensions have now been fixed for you. = +River generation failed! = +Please don't use step 'Landmass' with map type 'Empty', create a new empty map instead. = + +## Map/Tool names +My new map = +Generate landmass = +Raise mountains and hills = +Humidity and temperature = +Lakes and coastline = +Sprout vegetation = +Spawn rare features = +Distribute ice = +Assign continent IDs = +Let the rivers flow = +Spread Resources = +Create ancient ruins = +Floodfill = +[nation] starting location = +Remove features = +Remove improvement = +Remove resource = +Remove starting locations = +Remove rivers = +Spawn river from/to = +Bottom left river = +Bottom right river = +Bottom river = + # Multiplayer Help = @@ -494,12 +573,6 @@ Could not load game = Load [saveFileName] = Delete save = Saved at = -Load map = -Delete map = -Are you sure you want to delete this map? = -Upload map = -Could not upload map! = -Map uploaded successfully! = Saving... = Overwrite existing file? = It looks like your saved game can't be loaded! = @@ -1104,41 +1177,16 @@ Missing translations: = Version = Resolution = Tileset = -Map editor = Create = -New map = -Empty = Language = -Terrains & Resources = Improvements = -Clear current map = -Save map = -Download map = Loading... = -Error loading map! = Filter: = OK = -Exit map editor = -[nation] starting location = -Clear terrain features = -Clear improvements = -Clear resource = -Remove units = -Player [index] = -Player [playerIndex] starting location = -Bottom left river = -Bottom right river = -Bottom river = -Requires = -Menu = -Brush Size = -Map saved = -Change ruleset = Base terrain [terrain] does not exist in ruleset! = Terrain feature [feature] does not exist in ruleset! = Resource [resource] does not exist in ruleset! = Improvement [improvement] does not exist in ruleset! = -Change map to fit selected ruleset? = # Civilopedia difficulty levels Player settings = diff --git a/core/src/com/unciv/MainMenuScreen.kt b/core/src/com/unciv/MainMenuScreen.kt index 3b0d48a6c0..80ef666f34 100644 --- a/core/src/com/unciv/MainMenuScreen.kt +++ b/core/src/com/unciv/MainMenuScreen.kt @@ -75,12 +75,12 @@ 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(MapEditorScreen(), newMap) {} backgroundTable.addAction(Actions.sequence( Actions.fadeOut(0f), Actions.run { mapHolder.apply { - addTiles(this@MainMenuScreen.stage.width, this@MainMenuScreen.stage.height) + addTiles(this@MainMenuScreen.stage) touchable = Touchable.disabled } backgroundTable.addActor(mapHolder) @@ -120,7 +120,7 @@ class MainMenuScreen: BaseScreen() { column2.add(multiplayerTable).row() val mapEditorScreenTable = getMenuButton("Map editor", "OtherIcons/MapEditor", 'e') - { if(stage.actors.none { it is MapEditorMainScreenPopup }) MapEditorMainScreenPopup(this) } + { game.setScreen(MapEditorScreen()) } column2.add(mapEditorScreenTable).row() val modsTable = getMenuButton("Mods", "OtherIcons/Mods", 'd') @@ -152,49 +152,6 @@ class MainMenuScreen: BaseScreen() { } - /** Shows the [Popup] with the map editor initialization options */ - class MapEditorMainScreenPopup(screen: MainMenuScreen): Popup(screen){ - init{ - // Using MainMenuScreen.getMenuButton - normally that would place key bindings into the - // screen's key dispatcher, but we need them in this Popup's dispatcher instead. - // Thus the crutch with keyVisualOnly, we assign the key binding here but want - // The button to install the tooltip handler anyway. - - defaults().pad(10f) - - val tableBackground = ImageGetter.getBackground(colorFromRGB(29, 102, 107)) - - val newMapAction = { - val newMapScreen = NewMapScreen() - newMapScreen.setDefaultCloseAction(MainMenuScreen()) - screen.game.setScreen(newMapScreen) - screen.dispose() - } - val newMapButton = screen.getMenuButton("New map", "OtherIcons/New", 'n', true, newMapAction) - newMapButton.background = tableBackground - add(newMapButton).row() - keyPressDispatcher['n'] = newMapAction - - val loadMapAction = { - val loadMapScreen = SaveAndLoadMapScreen(null, false, screen) - loadMapScreen.setDefaultCloseAction(MainMenuScreen()) - screen.game.setScreen(loadMapScreen) - screen.dispose() - } - val loadMapButton = screen.getMenuButton("Load map", "OtherIcons/Load", 'l', true, loadMapAction) - loadMapButton.background = tableBackground - add(loadMapButton).row() - keyPressDispatcher['l'] = loadMapAction - - add(screen.getMenuButton(Constants.close, "OtherIcons/Close") { close() } - .apply { background=tableBackground }) - keyPressDispatcher[KeyCharAndCode.BACK] = { close() } - - open(force = true) - } - } - - private fun autoLoadGame() { val loadingPopup = Popup(this) loadingPopup.addGoodSizedLabel("Loading...") diff --git a/core/src/com/unciv/logic/civilization/TechManager.kt b/core/src/com/unciv/logic/civilization/TechManager.kt index f8f05e9741..0997f7cb47 100644 --- a/core/src/com/unciv/logic/civilization/TechManager.kt +++ b/core/src/com/unciv/logic/civilization/TechManager.kt @@ -54,7 +54,7 @@ class TechManager { /** For calculating Great Scientist yields - see https://civilization.fandom.com/wiki/Great_Scientist_(Civ5) */ var scienceOfLast8Turns = IntArray(8) { 0 } var scienceFromResearchAgreements = 0 - /** This is the lit of strings, which is serialized */ + /** This is the list of strings, which is serialized */ var techsResearched = HashSet() /** When moving towards a certain tech, the user doesn't have to manually pick every one. */ diff --git a/core/src/com/unciv/logic/map/MapParameters.kt b/core/src/com/unciv/logic/map/MapParameters.kt index ef52d98a45..e3012318c5 100644 --- a/core/src/com/unciv/logic/map/MapParameters.kt +++ b/core/src/com/unciv/logic/map/MapParameters.kt @@ -228,21 +228,31 @@ class MapParameters { (if (worldWrap) "w" else "") } + // Human readable float representation akin to .net "0.###" - round to N digits but without redundant trailing zeroes + private fun Float.niceToString(maxPrecision: Int) = + "%.${maxPrecision}f".format(this).trimEnd('0').trimEnd('.') + // For debugging and MapGenerator console output override fun toString() = sequence { if (name.isNotEmpty()) yield("\"$name\" ") yield("(") if (mapSize.name != MapSize.custom) yield(mapSize.name + " ") - if (worldWrap) yield("wrapped ") + if (worldWrap) yield("{wrapped} ") yield(shape) - yield(" " + displayMapDimensions()) + yield(" " + displayMapDimensions() + ")") yield(mapResources) if (name.isEmpty()) return@sequence - yield(", $type, Seed $seed, ") - yield("$elevationExponent/$temperatureExtremeness/$resourceRichness/$vegetationRichness/") - yield("$rareFeaturesRichness/$maxCoastExtension/$tilesPerBiomeArea/$waterThreshold") - }.joinToString("", postfix = ")") - + yield("\n$type, {Seed} $seed") + yield(", {Map Height}=" + elevationExponent.niceToString(2)) + yield(", {Temperature extremeness}=" + temperatureExtremeness.niceToString(2)) + yield(", {Resource richness}=" + resourceRichness.niceToString(3)) + yield(", {Vegetation richness}=" + vegetationRichness.niceToString(2)) + yield(", {Rare features richness}=" + rareFeaturesRichness.niceToString(3)) + yield(", {Max Coast extension}=$maxCoastExtension") + yield(", {Biome areas extension}=$tilesPerBiomeArea") + yield(", {Water level}=" + waterThreshold.niceToString(2)) + }.joinToString("") + fun numberOfTiles() = if (shape == MapShape.hexagonal) { 1 + 3 * mapSize.radius * (mapSize.radius - 1) diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index fd6a9205a2..e9f7961337 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -771,6 +771,7 @@ open class TileInfo { lineList += FormattedLine("[$defencePercentString] to unit defence") } if (isImpassible()) lineList += FormattedLine(Constants.impassable) + if (isLand && isAdjacentTo(Constants.freshWater)) lineList += FormattedLine(Constants.freshWater) return lineList } @@ -802,9 +803,10 @@ open class TileInfo { out.add("Terrain feature [$terrainFeature] does not exist in ruleset!") if (resource != null && !ruleset.tileResources.containsKey(resource)) out.add("Resource [$resource] does not exist in ruleset!") - if (improvement != null && !improvement!!.startsWith(TileMap.startingLocationPrefix) - && !ruleset.tileImprovements.containsKey(improvement)) + if (improvement != null && !ruleset.tileImprovements.containsKey(improvement)) out.add("Improvement [$improvement] does not exist in ruleset!") + if (naturalWonder != null && !ruleset.terrains.containsKey(naturalWonder)) + out.add("Natural Wonder [$naturalWonder] does not exist in ruleset!") return out } @@ -877,7 +879,7 @@ open class TileInfo { } } - fun setTerrainFeatures(terrainFeatureList:List){ + fun setTerrainFeatures(terrainFeatureList:List) { terrainFeatures = terrainFeatureList terrainFeatureObjects = terrainFeatureList.mapNotNull { ruleset.terrains[it] } } @@ -888,6 +890,9 @@ open class TileInfo { fun removeTerrainFeature(terrainFeature: String) = setTerrainFeatures(ArrayList(terrainFeatures).apply { remove(terrainFeature) }) + fun removeTerrainFeatures() = + setTerrainFeatures(listOf()) + /** If the unit isn't in the ruleset we can't even know what type of unit this is! So check each place * This works with no transients so can be called from gameInfo.setTransients with no fear @@ -912,26 +917,28 @@ open class TileInfo { } fun normalizeToRuleset(ruleset: Ruleset) { - if (!ruleset.terrains.containsKey(naturalWonder)) naturalWonder = null + if (naturalWonder != null && !ruleset.terrains.containsKey(naturalWonder)) + naturalWonder = null if (naturalWonder != null) { - val naturalWonder = ruleset.terrains[naturalWonder]!! - baseTerrain = naturalWonder.turnsInto!! + baseTerrain = this.getNaturalWonder().turnsInto!! setTerrainFeatures(listOf()) resource = null improvement = null } - for (terrainFeature in terrainFeatures.toList()) { + if (!ruleset.terrains.containsKey(baseTerrain)) + baseTerrain = ruleset.terrains.values.first { it.type == TerrainType.Land && !it.impassable }.name + + val newFeatures = ArrayList() + for (terrainFeature in terrainFeatures) { val terrainFeatureObject = ruleset.terrains[terrainFeature] - if (terrainFeatureObject == null) { - removeTerrainFeature(terrainFeature) - continue - } - + ?: continue if (terrainFeatureObject.occursOn.isNotEmpty() && !terrainFeatureObject.occursOn.contains(baseTerrain)) - removeTerrainFeature(terrainFeature) + continue + newFeatures.add(terrainFeature) } - + if (newFeatures.size != terrainFeatures.size) + setTerrainFeatures(newFeatures) if (resource != null && !ruleset.tileResources.containsKey(resource)) resource = null if (resource != null) { diff --git a/core/src/com/unciv/logic/map/TileMap.kt b/core/src/com/unciv/logic/map/TileMap.kt index 8cf7a3e0f9..f8b79c3c96 100644 --- a/core/src/com/unciv/logic/map/TileMap.kt +++ b/core/src/com/unciv/logic/map/TileMap.kt @@ -628,6 +628,12 @@ class TileMap { startingLocationsByNation[nationName]!!.clear() } + /** Removes all starting positions for [position], rebuilding the transients */ + fun removeStartingLocations(position: Vector2) { + startingLocations.removeAll(startingLocations.filter { it.position == position }) + setStartingLocationsTransients() + } + /** Clears starting positions, e.g. after GameStarter is done with them. Does not clear the pseudo-improvements. */ fun clearStartingLocations() { startingLocations.clear() diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt index 41349c88c5..8674505187 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt @@ -13,6 +13,7 @@ import com.unciv.models.ruleset.tile.TerrainType import com.unciv.models.ruleset.unique.Unique import kotlin.math.* import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.ui.mapeditor.MapGeneratorSteps import kotlin.random.Random @@ -130,6 +131,33 @@ class MapGenerator(val ruleset: Ruleset) { return map } + fun generateSingleStep(map: TileMap, step: MapGeneratorSteps) { + if (map.mapParameters.seed == 0L) + map.mapParameters.seed = System.currentTimeMillis() + + randomness.seedRNG(map.mapParameters.seed) + + when(step) { + MapGeneratorSteps.None -> return + MapGeneratorSteps.All -> throw IllegalArgumentException("MapGeneratorSteps.All cannot be used in generateSingleStep") + MapGeneratorSteps.Landmass -> MapLandmassGenerator(ruleset, randomness).generateLand(map) + MapGeneratorSteps.Elevation -> raiseMountainsAndHills(map) + MapGeneratorSteps.HumidityAndTemperature -> applyHumidityAndTemperature(map) + MapGeneratorSteps.LakesAndCoast -> spawnLakesAndCoasts(map) + MapGeneratorSteps.Vegetation -> spawnVegetation(map) + MapGeneratorSteps.RareFeatures -> spawnRareFeatures(map) + MapGeneratorSteps.Ice -> spawnIce(map) + MapGeneratorSteps.Continents -> map.assignContinents(TileMap.AssignContinentsMode.Reassign) + MapGeneratorSteps.NaturalWonders -> NaturalWonderGenerator(ruleset, randomness).spawnNaturalWonders(map) + MapGeneratorSteps.Rivers -> { + RiverGenerator(map, randomness, ruleset).spawnRivers() + convertTerrains(map, ruleset) + } + MapGeneratorSteps.Resources -> spreadResources(map) + MapGeneratorSteps.AncientRuins -> spreadAncientRuins(map) + } + } + private fun runAndMeasure(text: String, action: ()->Unit) { if (!consoleTimings) return action() diff --git a/core/src/com/unciv/logic/map/mapgenerator/RiverGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/RiverGenerator.kt index d0cfb7d7ee..9ea1569fc8 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/RiverGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/RiverGenerator.kt @@ -42,23 +42,30 @@ class RiverGenerator( return true } - private fun getClosestWaterTile(tile: TileInfo): TileInfo { + fun getClosestWaterTile(tile: TileInfo): TileInfo? { for (distance in 1..maxRiverLength) { val waterTiles = tile.getTilesAtDistance(distance).filter { it.isWater } if (waterTiles.any()) return waterTiles.toList().random(randomness.RNG) } - throw IllegalStateException() + return null } private fun spawnRiver(initialPosition: TileInfo) { - // Recommendation: Draw a bunch of hexagons on paper before trying to understand this, it's super helpful! val endPosition = getClosestWaterTile(initialPosition) + ?: throw IllegalStateException("No water found for river destination") + spawnRiver(initialPosition, endPosition) + } + + fun spawnRiver(initialPosition: TileInfo, endPosition: TileInfo, resultingTiles: MutableSet? = null) { + // Recommendation: Draw a bunch of hexagons on paper before trying to understand this, it's super helpful! var riverCoordinate = RiverCoordinate(initialPosition.position, RiverCoordinate.BottomRightOrLeft.values().random(randomness.RNG)) for (step in 1..maxRiverLength) { // Arbitrary max on river length, otherwise this will go in circles - rarely + val riverCoordinateTile = tileMap[riverCoordinate.position] + resultingTiles?.add(riverCoordinateTile) if (riverCoordinate.getAdjacentTiles(tileMap).any { it.isWater }) return val possibleCoordinates = riverCoordinate.getAdjacentPositions(tileMap) if (possibleCoordinates.none()) return // end of the line @@ -71,7 +78,6 @@ class RiverGenerator( .component2().random(randomness.RNG) // set new rivers in place - val riverCoordinateTile = tileMap[riverCoordinate.position] if (newCoordinate.position == riverCoordinate.position) // same tile, switched right-to-left riverCoordinateTile.hasBottomRiver = true else if (riverCoordinate.bottomRightOrLeft == RiverCoordinate.BottomRightOrLeft.BottomRight) { diff --git a/core/src/com/unciv/models/metadata/GameSetupInfo.kt b/core/src/com/unciv/models/metadata/GameSetupInfo.kt index cd765a1832..4864b65278 100644 --- a/core/src/com/unciv/models/metadata/GameSetupInfo.kt +++ b/core/src/com/unciv/models/metadata/GameSetupInfo.kt @@ -15,7 +15,7 @@ class GameSetupInfo( // This constructor is used for starting a new game from a running one, cloning the setup, including map seed constructor(gameInfo: GameInfo) : this(gameInfo.gameParameters.clone(), gameInfo.tileMap.mapParameters.clone()) - // Cloning constructor used for [fromSettings] and [GameParametersScreen], reseeds map + // Cloning constructor used for [fromSettings], reseeds map constructor(setup: GameSetupInfo): this(setup.gameParameters.clone(), setup.mapParameters.clone()) companion object { diff --git a/core/src/com/unciv/ui/civilopedia/CivilopediaScreen.kt b/core/src/com/unciv/ui/civilopedia/CivilopediaScreen.kt index 6c8a53c15b..8eadf582a1 100644 --- a/core/src/com/unciv/ui/civilopedia/CivilopediaScreen.kt +++ b/core/src/com/unciv/ui/civilopedia/CivilopediaScreen.kt @@ -174,13 +174,16 @@ class CivilopediaScreen( val imageSize = 50f onBackButtonClicked { game.setScreen(previousScreen) } - val religionEnabled = game.gameInfo.isReligionEnabled() + val religionEnabled = if (game.isGameInfoInitialized()) game.gameInfo.isReligionEnabled() + else ruleset.beliefs.isNotEmpty() + val victoryTypes = if (game.isGameInfoInitialized()) game.gameInfo.gameParameters.victoryTypes + else VictoryType.values().toList() fun shouldBeDisplayed(obj: IHasUniques): Boolean { return when { obj.hasUnique(UniqueType.HiddenFromCivilopedia) -> false (!religionEnabled && obj.hasUnique(UniqueType.HiddenWithoutReligion)) -> false - obj.getMatchingUniques(UniqueType.HiddenWithoutVictoryType).any { !game.gameInfo.gameParameters.victoryTypes.contains(VictoryType.valueOf(it.params[0] )) } -> false + obj.getMatchingUniques(UniqueType.HiddenWithoutVictoryType).any { !victoryTypes.contains(VictoryType.valueOf(it.params[0])) } -> false else -> true } } diff --git a/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt b/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt index cc13301e75..03b7cadb96 100644 --- a/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt +++ b/core/src/com/unciv/ui/civilopedia/CivilopediaText.kt @@ -110,7 +110,7 @@ class FormattedLine ( /** Returns true if this formatted line will not display anything */ fun isEmpty(): Boolean = text.isEmpty() && extraImage.isEmpty() && - !starred && icon.isEmpty() && link.isEmpty() + !starred && icon.isEmpty() && link.isEmpty() && !separator /** Self-check to potentially support the mod checker * @return `null` if no problems found, or multiline String naming problems. @@ -352,9 +352,9 @@ object MarkupRenderer { /** Default cell padding of non-empty lines */ private const val defaultPadding = 2.5f /** Padding above a [separator][FormattedLine.separator] line */ - private const val separatorTopPadding = 5f + private const val separatorTopPadding = 10f /** Padding below a [separator][FormattedLine.separator] line */ - private const val separatorBottomPadding = 15f + private const val separatorBottomPadding = 10f /** * Build a Gdx [Table] showing [formatted][FormattedLine] [content][lines]. diff --git a/core/src/com/unciv/ui/mapeditor/EditorMapHolder.kt b/core/src/com/unciv/ui/mapeditor/EditorMapHolder.kt index 145e4bd9cf..7e3c4dbd36 100644 --- a/core/src/com/unciv/ui/mapeditor/EditorMapHolder.kt +++ b/core/src/com/unciv/ui/mapeditor/EditorMapHolder.kt @@ -1,32 +1,43 @@ package com.unciv.ui.mapeditor import com.badlogic.gdx.math.Vector2 +import com.badlogic.gdx.scenes.scene2d.Stage +import com.unciv.UncivGame import com.unciv.logic.HexMath -import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileMap import com.unciv.ui.map.TileGroupMap import com.unciv.ui.tilegroups.TileGroup import com.unciv.ui.tilegroups.TileSetStrings -import com.unciv.ui.utils.ZoomableScrollPane -import com.unciv.ui.utils.center -import com.unciv.ui.utils.onClick +import com.unciv.ui.utils.* -class EditorMapHolder(private val mapEditorScreen: MapEditorScreen, internal val tileMap: TileMap): ZoomableScrollPane() { +class EditorMapHolder( + parentScreen: BaseScreen, + internal val tileMap: TileMap, + private val onTileClick: (TileInfo) -> Unit +): ZoomableScrollPane() { val tileGroups = HashMap>() - lateinit var tileGroupMap: TileGroupMap + private lateinit var tileGroupMap: TileGroupMap private val allTileGroups = ArrayList() + private val maxWorldZoomOut = UncivGame.Current.settings.maxWorldZoomOut + private val minZoomScale = 1f / maxWorldZoomOut + init { continuousScrollingX = tileMap.mapParameters.worldWrap + addTiles(parentScreen.stage) } - internal fun addTiles(leftAndRightPadding: Float, topAndBottomPadding: Float) { + internal fun addTiles(stage: Stage) { val tileSetStrings = TileSetStrings() val daTileGroups = tileMap.values.map { TileGroup(it, tileSetStrings) } - tileGroupMap = TileGroupMap(daTileGroups, leftAndRightPadding, topAndBottomPadding, continuousScrollingX) + tileGroupMap = TileGroupMap( + daTileGroups, + stage.width * maxWorldZoomOut / 2, + stage.height * maxWorldZoomOut / 2, + continuousScrollingX) actor = tileGroupMap val mirrorTileGroups = tileGroupMap.getMirrorTiles() @@ -48,29 +59,25 @@ class EditorMapHolder(private val mapEditorScreen: MapEditorScreen, internal val for (tileGroup in allTileGroups) { +/* revisit when Unit editing is re-implemented // This is a hack to make the unit icons render correctly on the game, even though the map isn't part of a game // and the units aren't assigned to any "real" CivInfo - tileGroup.tileInfo.getUnits().forEach { it.civInfo= CivilizationInfo() - .apply { nation=mapEditorScreen.ruleset.nations[it.owner]!! } } - - tileGroup.showEntireMap = true - tileGroup.update() - tileGroup.onClick { - - val distance = mapEditorScreen.mapEditorOptionsTable.brushSize - 1 - - for (tileInfo in mapEditorScreen.tileMap.getTilesInDistance(tileGroup.tileInfo.position, distance)) { - mapEditorScreen.mapEditorOptionsTable.updateTileWhenClicked(tileInfo) - - tileInfo.setTerrainTransients() - tileGroups[tileInfo]!!.forEach { it.update() } + //to do make safe the !! + //to do worse - don't create a whole Civ instance per unit + tileGroup.tileInfo.getUnits().forEach { + it.civInfo = CivilizationInfo().apply { + nation = ruleset.nations[it.owner]!! } } +*/ + tileGroup.showEntireMap = true + tileGroup.update() + tileGroup.onClick { onTileClick(tileGroup.tileInfo) } } - setSize(mapEditorScreen.stage.width * 2, mapEditorScreen.stage.height * 2) + setSize(stage.width * maxWorldZoomOut, stage.height * maxWorldZoomOut) setOrigin(width / 2,height / 2) - center(mapEditorScreen.stage) + center(stage) layout() @@ -89,11 +96,31 @@ class EditorMapHolder(private val mapEditorScreen: MapEditorScreen, internal val tileInfo.setTerrainTransients() } + // This emulates `private TileMap.getOrNull(Int,Int)` and should really move there + // still more efficient than `if (rounded in tileMap) tileMap[rounded] else null` + private fun TileMap.getOrNull(pos: Vector2): TileInfo? { + val x = pos.x.toInt() + val y = pos.y.toInt() + if (contains(x, y)) return get(x, y) + 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) + } - return if (rounded in tileMap) tileMap[rounded] else null + fun setCenterPosition(vector: Vector2) { + 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) + } + + override fun zoom(zoomScale: Float) { + if (zoomScale < minZoomScale || zoomScale > 2f) return + setScale(zoomScale) } } diff --git a/core/src/com/unciv/ui/mapeditor/GameParametersScreen.kt b/core/src/com/unciv/ui/mapeditor/GameParametersScreen.kt index 9862be7bca..52bd5576ed 100644 --- a/core/src/com/unciv/ui/mapeditor/GameParametersScreen.kt +++ b/core/src/com/unciv/ui/mapeditor/GameParametersScreen.kt @@ -12,43 +12,18 @@ import com.unciv.ui.pickerscreens.PickerScreen import com.unciv.ui.utils.* /** + * As of MapEditor V2, the editor no longer deals with GameParameters, **only** with MapParameters, + * and has no need of this. There are no instantiations. The class, stripped, is left in as skeleton + * so its references in PlayerPickerTable and NationPickerPopup can stay. They have been effectively dead even before. + * * This [Screen] is used for editing game parameters when scenario is edited/created in map editor. * Implements [IPreviousScreen] for compatibility with [PlayerPickerTable], [GameOptionsTable] * Uses [PlayerPickerTable] and [GameOptionsTable] to change local [gameSetupInfo]. Upon confirmation * updates [mapEditorScreen] and switches to it. * @param [mapEditorScreen] previous screen from map editor. */ +@Deprecated("As of 4.0.x") class GameParametersScreen(var mapEditorScreen: MapEditorScreen): IPreviousScreen, PickerScreen(disableScroll = true) { - override var gameSetupInfo = GameSetupInfo(mapEditorScreen.gameSetupInfo) + override var gameSetupInfo = GameSetupInfo(mapParameters = mapEditorScreen.newMapParameters) override var ruleset = RulesetCache.getComplexRuleset(gameSetupInfo.gameParameters.mods, gameSetupInfo.gameParameters.baseRuleset) - var playerPickerTable = PlayerPickerTable(this, gameSetupInfo.gameParameters) - var gameOptionsTable = GameOptionsTable(this) { desiredCiv: String -> playerPickerTable.update(desiredCiv) } - - - init { - setDefaultCloseAction(mapEditorScreen) - - topTable.add(AutoScrollPane(gameOptionsTable).apply { setScrollingDisabled(true, false) }) - .maxHeight(topTable.parent.height).width(stage.width / 2).padTop(20f).top() - topTable.addSeparatorVertical() - topTable.add(playerPickerTable).maxHeight(topTable.parent.height).width(stage.width / 2).padTop(20f).top() - rightSideButton.setText(Constants.OK.tr()) - rightSideButton.onClick { - mapEditorScreen.gameSetupInfo = gameSetupInfo - mapEditorScreen.ruleset.clear() - mapEditorScreen.ruleset.add(ruleset) - mapEditorScreen.mapEditorOptionsTable.update() - // Remove resources that are not applicable to this ruleset - for(tile in mapEditorScreen.tileMap.values) { - if (tile.resource != null && !ruleset.tileResources.containsKey(tile.resource!!)) - tile.resource = null - if (tile.improvement != null && !ruleset.tileImprovements.containsKey(tile.improvement!!)) - tile.improvement = null - } - - mapEditorScreen.mapHolder.updateTileGroups() - UncivGame.Current.setScreen(mapEditorScreen) - dispose() - } - } } diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorEditSubTabs.kt b/core/src/com/unciv/ui/mapeditor/MapEditorEditSubTabs.kt new file mode 100644 index 0000000000..e3a8f795ba --- /dev/null +++ b/core/src/com/unciv/ui/mapeditor/MapEditorEditSubTabs.kt @@ -0,0 +1,443 @@ +package com.unciv.ui.mapeditor + +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.scenes.scene2d.Group +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.unciv.Constants +import com.unciv.UncivGame +import com.unciv.logic.map.RoadStatus +import com.unciv.logic.map.TileInfo +import com.unciv.models.ruleset.Nation +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.tile.* +import com.unciv.models.translations.tr +import com.unciv.ui.civilopedia.FormattedLine +import com.unciv.ui.civilopedia.MarkupRenderer +import com.unciv.ui.images.ImageGetter +import com.unciv.ui.mapeditor.MapEditorEditTab.BrushHandlerType +import com.unciv.ui.tilegroups.TileGroup +import com.unciv.ui.tilegroups.TileSetStrings +import com.unciv.ui.utils.* + +internal interface IMapEditorEditSubTabs { + fun isDisabled(): Boolean +} + + +/** Implements the Map editor Edit-Terrains UI Tab */ +class MapEditorEditTerrainTab( + private val editTab: MapEditorEditTab, + private val ruleset: Ruleset +): Table(BaseScreen.skin), IMapEditorEditSubTabs { + init { + top() + defaults().pad(10f).fillX().left() + add(MarkupRenderer.render( + getTerrains(), + iconDisplay = FormattedLine.IconDisplay.NoLink + ) { + editTab.setBrush(it, "Terrain/$it") { tile -> + tile.baseTerrain = it + tile.naturalWonder = null + } + }).row() + } + + private fun allTerrains() = ruleset.terrains.values.asSequence() + .filter { it.type.isBaseTerrain } + private fun getTerrains() = allTerrains() + .map { FormattedLine(it.name, it.name, "Terrain/${it.name}", size = 32) } + .toList() + + override fun isDisabled() = false // allTerrains().none() // wanna see _that_ mod... +} + + +/** Implements the Map editor Edit-Features UI Tab */ +class MapEditorEditFeaturesTab( + private val editTab: MapEditorEditTab, + private val ruleset: Ruleset +): Table(BaseScreen.skin), IMapEditorEditSubTabs { + init { + top() + defaults().pad(10f).fillX().left() + allowedFeatures().firstOrNull()?.let { addFeatures(it) } + } + + private fun addFeatures(firstFeature: Terrain) { + 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 -> + tile.removeTerrainFeatures() + } + } }).padBottom(0f).row() + add(MarkupRenderer.render( + getFeatures(), + iconDisplay = FormattedLine.IconDisplay.NoLink + ) { + editTab.setBrush(it, "Terrain/$it") { tile -> + if (it !in tile.terrainFeatures) + tile.addTerrainFeature(it) + } + }).padTop(0f).row() + } + + private fun allowedFeatures() = ruleset.terrains.values.asSequence() + .filter { it.type == TerrainType.TerrainFeature } + private fun getFeatures() = allowedFeatures() + .map { FormattedLine(it.name, it.name, "Terrain/${it.name}", size = 32) } + .toList() + + override fun isDisabled() = allowedFeatures().none() +} + + +/** Implements the Map editor Edit-NaturalWonders UI Tab */ +class MapEditorEditWondersTab( + private val editTab: MapEditorEditTab, + private val ruleset: Ruleset +): Table(BaseScreen.skin), IMapEditorEditSubTabs { + init { + top() + defaults().pad(10f).fillX().left() + add(MarkupRenderer.render( + getWonders(), + iconDisplay = FormattedLine.IconDisplay.NoLink + ) { + editTab.setBrush(it, "Terrain/$it") { tile -> + // Normally the caller would ensure compliance, but here we make an exception - place it no matter what + tile.baseTerrain = ruleset.terrains[it]!!.turnsInto!! + tile.removeTerrainFeatures() + tile.naturalWonder = it + } + }).row() + } + + private fun allowedWonders() = ruleset.terrains.values.asSequence() + .filter { it.type == TerrainType.NaturalWonder } + private fun getWonders() = allowedWonders() + .map { FormattedLine(it.name, it.name, "Terrain/${it.name}", size = 32) } + .toList() + + override fun isDisabled() = allowedWonders().none() +} + + +/** Implements the Map editor Edit-Resources UI Tab */ +class MapEditorEditResourcesTab( + private val editTab: MapEditorEditTab, + private val ruleset: Ruleset +): Table(BaseScreen.skin), IMapEditorEditSubTabs { + init { + top() + defaults().pad(10f).fillX().left() + allowedResources().firstOrNull()?.let { addResources(it) } + } + + private fun addResources(firstResource: TileResource) { + 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 -> + tile.resource = null + } + } }).padBottom(0f).row() + add(MarkupRenderer.render( + getResources(), + iconDisplay = FormattedLine.IconDisplay.NoLink + ) { + editTab.setBrush(it, "Resource/$it") { tile -> + tile.resource = it + } + }).padTop(0f).row() + } + + private fun allowedResources() = ruleset.tileResources.values.asSequence() + .filter { !it.hasUnique("Can only be created by Mercantile City-States") } //todo type-i-fy + private fun getResources(): List = sequence { + var lastGroup = ResourceType.Bonus + for (resource in allowedResources()) { + val name = resource.name + if (resource.resourceType != lastGroup) { + lastGroup = resource.resourceType + yield(FormattedLine(separator = true, color = "#888")) + } + yield (FormattedLine(name, name, "Resource/$name", size = 32)) + } + }.toList() + + override fun isDisabled() = allowedResources().none() +} + + +/** Implements the Map editor Edit-Improvements UI Tab */ +class MapEditorEditImprovementsTab( + private val editTab: MapEditorEditTab, + private val ruleset: Ruleset +): Table(BaseScreen.skin), IMapEditorEditSubTabs { + init { + top() + defaults().pad(10f).fillX().left() + allowedImprovements().firstOrNull()?.let { addImprovements(it) } + } + + private fun addImprovements(firstImprovement: TileImprovement) { + 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 -> + tile.improvement = null + tile.roadStatus = RoadStatus.None + } + } }).padBottom(0f).row() + add(MarkupRenderer.render( + getImprovements(), + iconDisplay = FormattedLine.IconDisplay.NoLink + ) { + val road = RoadStatus.values().firstOrNull { r -> r.name == it } + if (road != null) + editTab.setBrush(BrushHandlerType.Road, it, "Improvement/$it") { tile -> + tile.roadStatus = if (tile.roadStatus == road) RoadStatus.None else road + } + else + editTab.setBrush(it, "Improvement/$it") { tile -> + tile.improvement = it + } + }).padTop(0f).row() + } + + private fun allowedImprovements() = ruleset.tileImprovements.values.asSequence() + .filter { improvement -> + disallowImprovements.none { improvement.name.startsWith(it) } + } + private fun getImprovements(): List = sequence { + var lastGroup = 0 + for (improvement in allowedImprovements()) { + val name = improvement.name + val group = improvement.group() + if (group != lastGroup) { + lastGroup = group + yield(FormattedLine(separator = true, color = "#888")) + } + yield (FormattedLine(name, name, "Improvement/$name", size = 32)) + } + }.toList() + + override fun isDisabled() = allowedImprovements().none() + + companion object { + //todo This should really be easier, the attributes should allow such a test in one go + private val disallowImprovements = listOf( + "Remove ", "Cancel improvement", "City center", Constants.barbarianEncampment + ) + private fun TileImprovement.group() = when { + RoadStatus.values().any { it.name == name } -> 2 + "Great Improvement" in uniques -> 3 + uniqueTo != null -> 4 + "Unpillagable" in uniques -> 5 + else -> 0 + } + } +} + + +/** Implements the Map editor Edit-StartingLocations UI Tab */ +class MapEditorEditStartsTab( + private val editTab: MapEditorEditTab, + private val ruleset: Ruleset +): Table(BaseScreen.skin), IMapEditorEditSubTabs { + private val collator = UncivGame.Current.settings.getCollatorFromLocale() + + init { + top() + defaults().pad(10f).fillX().left() + allowedNations().firstOrNull()?.let { addNations(it) } + } + + private fun addNations(firstNation: Nation) { + 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 -> + tile.tileMap.removeStartingLocations(tile.position) + } + } }).padBottom(0f).row() + add(MarkupRenderer.render( + getNations(), + iconDisplay = FormattedLine.IconDisplay.NoLink + ) { + editTab.setBrush(BrushHandlerType.Direct, it, "Nation/$it") { tile -> + // toggle the starting location here, note this allows + // both multiple locations per nation and multiple nations per tile + if (!tile.tileMap.addStartingLocation(it, tile)) + tile.tileMap.removeStartingLocation(it, tile) + } + }).padTop(0f).row() + } + + private fun allowedNations() = ruleset.nations.values.asSequence() + .filter { it.name !in disallowNations } + private fun getNations() = allowedNations() + .sortedWith(compareBy{ it.isCityState() }.thenBy(collator) { it.name.tr() }) + .map { FormattedLine("[${it.name}] starting location", it.name, "Nation/${it.name}", size = 24) } + .toList() + + override fun isDisabled() = allowedNations().none() + + companion object { + private val disallowNations = setOf(Constants.spectator, Constants.barbarians) + } +} + + +/** Implements the Map editor Edit-Rivers UI Tab */ +class MapEditorEditRiversTab( + private val editTab: MapEditorEditTab, + private val ruleset: Ruleset +): Table(BaseScreen.skin), IMapEditorEditSubTabs, TabbedPager.IPageExtensions { + private val iconSize = 50f + private val showOnTerrain = ruleset.terrains.values.asSequence() + .filter { it.type.isBaseTerrain && !it.isRough() } + .sortedByDescending { it.production * 2 + it.food } + .firstOrNull() + ?: ruleset.terrains[Constants.plains] + ?: ruleset.terrains.values.first() + + init { + top() + defaults().pad(10f).left() + val removeLine = Table().apply { + add(getRemoveRiverIcon()).padRight(10f) + add("Remove rivers".toLabel(fontSize = 32)) + onClick { + editTab.setBrush(BrushHandlerType.River,"Remove rivers", getRemoveRiverIcon()) { tile -> + tile.hasBottomLeftRiver = false + tile.hasBottomRightRiver = false + tile.hasBottomRiver = false + // User probably expects all six edges to be cleared + val x = tile.position.x.toInt() + val y = tile.position.y.toInt() + tile.tileMap.getIfTileExistsOrNull(x, y + 1)?.hasBottomLeftRiver = false + tile.tileMap.getIfTileExistsOrNull(x + 1, y)?.hasBottomRightRiver = false + tile.tileMap.getIfTileExistsOrNull(x + 1, y + 1)?.hasBottomRiver = false + } + } + } + add(removeLine).row() + + val leftRiverLine = Table().apply { + add(getRiverIcon(RiverEdge.Left)).padRight(10f) + add("Bottom left river".toLabel(fontSize = 32)) + onClick { + editTab.setBrush(BrushHandlerType.Direct,"Bottom left river", getTileGroupWithRivers(RiverEdge.Left)) { tile -> + tile.hasBottomLeftRiver = !tile.hasBottomLeftRiver + } + } + } + add(leftRiverLine).row() + + val bottomRiverLine = Table().apply { + add(getRiverIcon(RiverEdge.Bottom)).padRight(10f) + add("Bottom river".toLabel(fontSize = 32)) + onClick { + editTab.setBrush(BrushHandlerType.Direct,"Bottom river", getTileGroupWithRivers(RiverEdge.Bottom)) { tile -> + tile.hasBottomRiver = !tile.hasBottomRiver + } + } + } + add(bottomRiverLine).row() + + val rightRiverLine = Table().apply { + add(getRiverIcon(RiverEdge.Right)).padRight(10f) + add("Bottom right river".toLabel(fontSize = 32)) + onClick { + editTab.setBrush(BrushHandlerType.Direct,"Bottom right river", getTileGroupWithRivers(RiverEdge.Right)) { tile -> + tile.hasBottomRightRiver = !tile.hasBottomRightRiver + } + } + } + add(rightRiverLine).row() + + //todo this needs a better icon + val spawnRiverLine = Table().apply { + add(getRiverIcon(RiverEdge.All)).padRight(10f) + add("Spawn river from/to".toLabel(fontSize = 32)) + onClick { + editTab.setBrush( + BrushHandlerType.RiverFromTo, + name = "Spawn river from/to", + icon = getTileGroupWithRivers(RiverEdge.All), + applyAction = {} // Actual effect done via BrushHandlerType + ) + } + } + add(spawnRiverLine).row() + } + + override fun isDisabled() = false + + override fun activated(index: Int, caption: String, pager: TabbedPager) { + editTab.brushSize = 1 + } + + private fun TileInfo.makeTileGroup(): TileGroup { + ruleset = this@MapEditorEditRiversTab.ruleset + setTerrainTransients() + return TileGroup(this, TileSetStrings(), iconSize * 36f/54f).apply { + showEntireMap = true + forMapEditorIcon = true + update() + } + } + + private enum class RiverEdge { Left, Bottom, Right, All } + private fun getTileGroupWithRivers(edge: RiverEdge) = + TileInfo().apply { + baseTerrain = showOnTerrain.name + when (edge) { + RiverEdge.Left -> hasBottomLeftRiver = true + RiverEdge.Bottom -> hasBottomRiver = true + RiverEdge.Right -> hasBottomRightRiver = true + RiverEdge.All -> { + hasBottomLeftRiver = true + hasBottomRightRiver = true + hasBottomRiver = true + } + } + }.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 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 + setSize(iconSize, iconSize) + val tileGroup = getTileGroupWithRivers(edge) + tileGroup.center(this) + addActor(tileGroup) + } +} + + +/** Implements the Map editor Edit-Units UI Tab */ +@Suppress("unused") +class MapEditorEditUnitsTab( + private val editTab: MapEditorEditTab, + private val ruleset: Ruleset +): Table(BaseScreen.skin), IMapEditorEditSubTabs { + init { + top() + defaults().pad(10f).left() + add("Work in progress".toLabel(Color.FIREBRICK, 24)) + } + + override fun isDisabled() = true +} diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorEditTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorEditTab.kt new file mode 100644 index 0000000000..2a83e91668 --- /dev/null +++ b/core/src/com/unciv/ui/mapeditor/MapEditorEditTab.kt @@ -0,0 +1,344 @@ +package com.unciv.ui.mapeditor + +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.scenes.scene2d.Actor +import com.badlogic.gdx.scenes.scene2d.Group +import com.badlogic.gdx.scenes.scene2d.ui.Cell +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.unciv.logic.map.BFS +import com.unciv.logic.map.TileInfo +import com.unciv.logic.map.mapgenerator.MapGenerationRandomness +import com.unciv.logic.map.mapgenerator.RiverGenerator +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.translations.tr +import com.unciv.ui.civilopedia.FormattedLine +import com.unciv.ui.images.ImageGetter +import com.unciv.ui.mapeditor.MapEditorOptionsTab.TileMatchFuzziness +import com.unciv.ui.popup.ToastPopup +import com.unciv.ui.utils.* + +class MapEditorEditTab( + private val editorScreen: MapEditorScreen, + headerHeight: Float +): Table(BaseScreen.skin), TabbedPager.IPageExtensions { + private val subTabs: TabbedPager + private val brushTable = Table(skin) + private val brushSlider: UncivSlider + private val brushLabel = "Brush ([1]):".toLabel() + private val brushCell: Cell + + private var ruleset = editorScreen.ruleset + private val randomness = MapGenerationRandomness() // for auto river + + enum class BrushHandlerType { None, Direct, Tile, Road, River, RiverFromTo } + private var brushHandlerType = BrushHandlerType.None + + /** This applies the current brush to one tile **without** validation or transient updates */ + private var brushAction: (TileInfo)->Unit = {} + /** Brush size: 1..5 means hexagon of radius x, -1 means floodfill */ + internal var brushSize = 1 + set(value) { + field = value + brushSlider.value = if (value < 0) 6f else value.toFloat() + } + /** Copy of same field in [MapEditorOptionsTab] */ + private var tileMatchFuzziness = TileMatchFuzziness.CompleteMatch + + /** Tile to run a river _from_ (both ends are set with the same tool, so we need the memory) */ + private var riverStartTile: TileInfo? = null + /** Tile to run a river _to_ */ + private var riverEndTile: TileInfo? = null + + private enum class AllEditSubTabs( + val caption: String, + val key: Char, + val icon: String, + val instantiate: (MapEditorEditTab, Ruleset)->Table + ) { + Terrain("Terrain", 't', "OtherIcons/Terrains", { parent, ruleset -> MapEditorEditTerrainTab(parent, ruleset) }), + TerrainFeatures("Features", 'f', "OtherIcons/Star", { parent, ruleset -> MapEditorEditFeaturesTab(parent, ruleset) }), + NaturalWonders("Wonders", 'w', "OtherIcons/Star", { parent, ruleset -> MapEditorEditWondersTab(parent, ruleset) }), + Resources("Resources", 'r', "OtherIcons/Resources", { parent, ruleset -> MapEditorEditResourcesTab(parent, ruleset) }), + Improvements("Improvements", 'i', "OtherIcons/Improvements", { parent, ruleset -> MapEditorEditImprovementsTab(parent, ruleset) }), + Rivers("Rivers", 'v', "OtherIcons/Star", { parent, ruleset -> MapEditorEditRiversTab(parent, ruleset) }), + StartingLocations("Starting locations", 's', "OtherIcons/Nations", { parent, ruleset -> MapEditorEditStartsTab(parent, ruleset) }), + Units("Units", 'u', "OtherIcons/Shield", { parent, ruleset -> MapEditorEditUnitsTab(parent, ruleset) }), + } + + init { + top() + + brushTable.apply { + pad(5f) + defaults().pad(10f).left() + add(brushLabel) + brushCell = add().padLeft(0f) + brushSlider = UncivSlider(1f,6f,1f, getTipText = { getBrushTip(it).tr() }) { + brushSize = if (it > 5f) -1 else it.toInt() + brushLabel.setText("Brush ([${getBrushTip(it).take(1)}]):".tr()) + } + add(brushSlider).padLeft(0f) + } + + // TabbedPager parameters specify content page area. Assume subTabs will have the same headerHeight + // as the master tabs, the 2f is for the separator, and the 10f for reduced header padding: + val subTabsHeight = editorScreen.stage.height - 2 * headerHeight - brushTable.prefHeight - 2f + 10f + val subTabsWidth = editorScreen.getToolsWidth() + subTabs = TabbedPager( + minimumHeight = subTabsHeight, + maximumHeight = subTabsHeight, + minimumWidth = subTabsWidth, + maximumWidth = subTabsWidth, + headerPadding = 5f, + capacity = AllEditSubTabs.values().size + ) + + for (page in AllEditSubTabs.values()) { + // Empty tabs with placeholders, filled when activated() + subTabs.addPage(page.caption, Group(), ImageGetter.getImage(page.icon), 20f, + shortcutKey = KeyCharAndCode(page.key), disabled = true) + } + subTabs.selectPage(0) + + add(brushTable).fillX().row() + addSeparator(Color.GRAY) + add(subTabs).left().fillX().row() + } + + private fun selectPage(index: Int) = subTabs.selectPage(index) + + 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 + ) { + brushHandlerType = BrushHandlerType.Tile + val line = Table().apply { + add(icon).padRight(10f) + add(name.toLabel()) + } + brushCell.setActor(line) + brushAction = applyAction + } + 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 + ) { + setBrush(name, icon, applyAction) + brushHandlerType = handlerType + } + + override fun activated(index: Int, caption: String, pager: TabbedPager) { + if (editorScreen.editTabsNeedRefresh) { + // ruleset has changed + ruleset = editorScreen.ruleset + ImageGetter.setNewRuleset(ruleset) + for (page in AllEditSubTabs.values()) { + val tab = page.instantiate(this, ruleset) + subTabs.replacePage(page.caption, tab) + subTabs.setPageDisabled(page.caption, (tab as IMapEditorEditSubTabs).isDisabled()) + } + brushHandlerType = BrushHandlerType.None + editorScreen.editTabsNeedRefresh = false + } + + editorScreen.tileClickHandler = this::tileClickHandler + pager.setScrollDisabled(true) + tileMatchFuzziness = editorScreen.tileMatchFuzziness + + val keyPressDispatcher = editorScreen.keyPressDispatcher + keyPressDispatcher['t'] = { selectPage(0) } + keyPressDispatcher['f'] = { selectPage(1) } + keyPressDispatcher['w'] = { selectPage(2) } + keyPressDispatcher['r'] = { selectPage(3) } + keyPressDispatcher['i'] = { selectPage(4) } + keyPressDispatcher['v'] = { selectPage(5) } + keyPressDispatcher['s'] = { selectPage(6) } + keyPressDispatcher['u'] = { selectPage(7) } + keyPressDispatcher['1'] = { brushSize = 1 } + keyPressDispatcher['2'] = { brushSize = 2 } + keyPressDispatcher['3'] = { brushSize = 3 } + keyPressDispatcher['4'] = { brushSize = 4 } + keyPressDispatcher['5'] = { brushSize = 5 } + keyPressDispatcher[KeyCharAndCode.ctrl('f')] = { brushSize = -1 } + } + + override fun deactivated(index: Int, caption: String, pager: TabbedPager) { + pager.setScrollDisabled(true) + editorScreen.tileClickHandler = null + editorScreen.keyPressDispatcher.revertToCheckPoint() + } + + fun tileClickHandler(tile: TileInfo) { + if (brushSize < -1 || brushSize > 5 || brushHandlerType == BrushHandlerType.None) return + editorScreen.hideSelection() + + when (brushHandlerType) { + BrushHandlerType.None -> Unit + BrushHandlerType.RiverFromTo -> + selectRiverFromOrTo(tile) + else -> + paintTilesWithBrush(tile) + } + } + + private fun selectRiverFromOrTo(tile: TileInfo) { + val tilesToHighlight = mutableSetOf(tile) + if (tile.isLand) { + // Land means river from. Start the river if we have a 'to', choose a 'to' if not. + riverStartTile = tile + if (riverEndTile != null) return paintRiverFromTo() + val riverGenerator = RiverGenerator(editorScreen.tileMap, randomness, ruleset) + riverEndTile = riverGenerator.getClosestWaterTile(tile) + if (riverEndTile != null) tilesToHighlight += riverEndTile!! + } else { + // Water means river to. Start the river if we have a 'from' + riverEndTile = tile + if (riverStartTile != null) return paintRiverFromTo() + } + tilesToHighlight.forEach { editorScreen.highlightTile(it, Color.BLUE) } + } + private fun paintRiverFromTo() { + val resultingTiles = mutableSetOf() + randomness.seedRNG(editorScreen.newMapParameters.seed) + try { + val riverGenerator = RiverGenerator(editorScreen.tileMap, randomness, ruleset) + riverGenerator.spawnRiver(riverStartTile!!, riverEndTile!!, resultingTiles) + } catch (ex: Exception) { + println(ex.message) + ToastPopup("River generation failed!", editorScreen) + } + riverStartTile = null + riverEndTile = null + editorScreen.isDirty = true + resultingTiles.forEach { editorScreen.updateAndHighlight(it, Color.SKY) } + } + + private fun paintTilesWithBrush(tile: TileInfo) { + val tiles = + if (brushSize == -1) { + val bfs = BFS(tile) { it.isSimilarEnough(tile) } + bfs.stepToEnd() + bfs.getReachedTiles().asSequence() + } else { + tile.getTilesInDistance(brushSize - 1) + } + tiles.forEach { + @Suppress("NON_EXHAUSTIVE_WHEN") // other cases can't reach here + when (brushHandlerType) { + BrushHandlerType.Direct -> directPaintTile(it) + BrushHandlerType.Tile -> paintTile(it) + BrushHandlerType.Road -> roadPaintTile(it) + BrushHandlerType.River -> riverPaintTile(it) + } + } + } + + /** Used for starting locations - no temp tile as brushAction needs to access tile.tileMap */ + private fun directPaintTile(tile: TileInfo) { + brushAction(tile) + editorScreen.isDirty = true + editorScreen.updateAndHighlight(tile) + } + + /** Used for rivers - same as directPaintTile but may need to update 10,12 and 2 o'clock neighbor tiles too */ + private fun riverPaintTile(tile: TileInfo) { + directPaintTile(tile) + tile.neighbors.forEach { + if (it.position.x > tile.position.x || it.position.y > tile.position.y) + editorScreen.updateTile(it) + } + } + + // Used for roads - same as paintTile but all neighbors need TileGroup.update too + private fun roadPaintTile(tile: TileInfo) { + if (!paintTile(tile)) return + tile.neighbors.forEach { editorScreen.updateTile(it) } + } + + /** apply brush to a single tile */ + private fun paintTile(tile: TileInfo): Boolean { + // Approach is "Try - matches - leave or revert" because an off-map simulation would fail some tile filters + val savedTile = tile.clone() + val paintedTile = tile.clone() + brushAction(paintedTile) + paintedTile.ruleset = ruleset + try { + paintedTile.setTerrainTransients() + } catch (ex: Exception) { + val message = ex.message ?: throw ex + if (!message.endsWith("not exist in this ruleset!")) throw ex + ToastPopup(message, editorScreen) + } + + brushAction(tile) + tile.setTerrainTransients() + tile.normalizeToRuleset(ruleset) // todo: this does not do what we need + if (!paintedTile.isSimilarEnough(tile)) { + // revert tile to original state + tile.applyFrom(savedTile) + return false + } + + if (tile.naturalWonder != savedTile.naturalWonder) + editorScreen.naturalWondersNeedRefresh = true + editorScreen.isDirty = true + editorScreen.updateAndHighlight(tile) + return true + } + + private fun TileInfo.isSimilarEnough(other: TileInfo) = when { + tileMatchFuzziness <= TileMatchFuzziness.CompleteMatch && + improvement != other.improvement || + roadStatus != other.roadStatus -> false + tileMatchFuzziness <= TileMatchFuzziness.NoImprovement && + resource != other.resource -> false + tileMatchFuzziness <= TileMatchFuzziness.BaseAndFeatures && + terrainFeatures.toSet() != other.terrainFeatures.toSet() -> false + tileMatchFuzziness <= TileMatchFuzziness.BaseTerrain && + baseTerrain != other.baseTerrain -> false + tileMatchFuzziness <= TileMatchFuzziness.LandOrWater && + isLand != other.isLand -> false + else -> naturalWonder == other.naturalWonder + } + + private fun TileInfo.applyFrom(other: TileInfo) { + // 90% copy w/o position, improvement times or transients. Add units once Unit paint is in. + baseTerrain = other.baseTerrain + setTerrainFeatures(other.terrainFeatures) + resource = other.resource + improvement = other.improvement + naturalWonder = other.naturalWonder + roadStatus = other.roadStatus + hasBottomLeftRiver = other.hasBottomLeftRiver + hasBottomRightRiver = other.hasBottomRightRiver + hasBottomRiver = other.hasBottomRiver + setTerrainTransients() + } + + companion object { + private fun getBrushTip(value: Float) = if (value > 5f) "Floodfill" else value.toInt().toString() + } +} diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorFilesTable.kt b/core/src/com/unciv/ui/mapeditor/MapEditorFilesTable.kt new file mode 100644 index 0000000000..75def9f39b --- /dev/null +++ b/core/src/com/unciv/ui/mapeditor/MapEditorFilesTable.kt @@ -0,0 +1,95 @@ +package com.unciv.ui.mapeditor + +import com.badlogic.gdx.files.FileHandle +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.scenes.scene2d.ui.TextButton +import com.unciv.logic.MapSaver +import com.unciv.models.ruleset.RulesetCache +import com.unciv.ui.images.ImageGetter +import com.unciv.ui.utils.BaseScreen +import com.unciv.ui.utils.onClick +import com.unciv.ui.utils.pad +import com.unciv.ui.utils.toLabel + +class MapEditorFilesTable( + initWidth: Float, + private val includeMods: Boolean = false, + private val onSelect: (FileHandle) -> Unit +): Table(BaseScreen.skin) { + private var selectedIndex = -1 + + private data class ListEntry(val mod: String, val file: FileHandle) + private var sortedFiles = ArrayList() + + init { + defaults().pad(5f).maxWidth(initWidth) + } + + private fun markSelection(button: TextButton, row: Int) { + for (cell in cells) { + if (cell.actor != button && cell.actor is TextButton) + cell.actor.color = Color.WHITE + } + button.color = Color.BLUE + selectedIndex = row + onSelect(sortedFiles[row].file) + } + + fun moveSelection(delta: Int) { + selectedIndex = when { + selectedIndex + delta in sortedFiles.indices -> + selectedIndex + delta + selectedIndex + delta < 0 -> + sortedFiles.size - 1 + else -> 0 + } + val button = cells[selectedIndex].actor as TextButton + (parent as? ScrollPane)?.let { + it.scrollY = (height - button.y) - (it.height - button.height) / 2 + } + markSelection(button, selectedIndex) + } + + fun update() { + clear() + sortedFiles.clear() + sortedFiles.addAll( + MapSaver.getMaps() + .sortedByDescending { it.lastModified() } + .map { ListEntry("", it) } + ) + if (includeMods) { + for (modFolder in RulesetCache.values.mapNotNull { it.folderLocation }) { + val mapsFolder = modFolder.child("maps") + if (mapsFolder.exists()) + sortedFiles.addAll( + mapsFolder.list() + .sortedBy { it.name() } + .map { ListEntry(modFolder.name(), it) } + ) + } + } + + var lastMod = "" + for ((index, entry) in sortedFiles.withIndex()) { + val (mod, mapFile) = entry + if (mod != lastMod) { + // One header per Mod + add(Table().apply { + add(ImageGetter.getDot(Color.LIGHT_GRAY)).minHeight(2f).minWidth(15f) + add(mod.toLabel(Color.LIGHT_GRAY)).left().pad(0f,2f) + add(ImageGetter.getDot(Color.LIGHT_GRAY)).minHeight(2f).growX().row() + }).growX().row() + lastMod = mod + } + val mapButton = TextButton(mapFile.name(), BaseScreen.skin) + mapButton.onClick { + markSelection(mapButton, index) + } + add(mapButton).row() + } + layout() + } +} diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorGenerateTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorGenerateTab.kt new file mode 100644 index 0000000000..36a9197e5f --- /dev/null +++ b/core/src/com/unciv/ui/mapeditor/MapEditorGenerateTab.kt @@ -0,0 +1,167 @@ +package com.unciv.ui.mapeditor + +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.MapType +import com.unciv.logic.map.mapgenerator.MapGenerator +import com.unciv.models.ruleset.RulesetCache +import com.unciv.models.translations.tr +import com.unciv.ui.images.ImageGetter +import com.unciv.ui.newgamescreen.MapParametersTable +import com.unciv.ui.popup.Popup +import com.unciv.ui.popup.ToastPopup +import com.unciv.ui.utils.* +import kotlin.concurrent.thread + +class MapEditorGenerateTab( + private val editorScreen: MapEditorScreen +): TabbedPager(capacity = 2) { + private val newTab = MapEditorNewMapTab(this) + private val partialTab = MapEditorGenerateStepsTab(this) + + // Since we allow generation components to be run repeatedly, it might surprise the user that + // the outcome stays the same when repeated - due to them operating on the same seed. + // So we change the seed behind the scenes if already used for a certain step... + private val seedUsedForStep = mutableSetOf() + + init { + name = "Generate" + top() + addPage("New map", newTab, + ImageGetter.getImage("OtherIcons/New"), 20f, + shortcutKey = KeyCharAndCode.ctrl('n')) + addPage("Partial", partialTab, + ImageGetter.getImage("OtherIcons/Settings"), 20f, + shortcutKey = KeyCharAndCode.ctrl('g')) + selectPage(0) + setButtonsEnabled(true) + partialTab.generateButton.disable() // Starts with choice "None" + } + + private fun setButtonsEnabled(enable: Boolean) { + newTab.generateButton.isEnabled = enable + newTab.generateButton.setText( (if(enable) "Create" else "Working...").tr()) + partialTab.generateButton.isEnabled = enable + partialTab.generateButton.setText( (if(enable) "Generate" else "Working...").tr()) + } + + private fun generate(step: MapGeneratorSteps) { + val mapParameters = editorScreen.newMapParameters.clone() // this clone is very important here + val message = mapParameters.mapSize.fixUndesiredSizes(mapParameters.worldWrap) + if (message != null) { + Gdx.app.postRunnable { + ToastPopup( message, editorScreen, 4000 ) + newTab.mapParametersTable.run { mapParameters.mapSize.also { + customMapSizeRadius.text = it.radius.toString() + customMapWidth.text = it.width.toString() + customMapHeight.text = it.height.toString() + } } + } + return + } + + if (step == MapGeneratorSteps.Landmass && mapParameters.type == MapType.empty) { + ToastPopup("Please don't use step 'Landmass' with map type 'Empty', create a new empty map instead.", editorScreen) + return + } + + if (step in seedUsedForStep) { + mapParameters.reseed() + } else { + seedUsedForStep += step + } + + Gdx.input.inputProcessor = null // remove input processing - nothing will be clicked! + setButtonsEnabled(false) + + thread(name = "MapGenerator") { + 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 + } + } 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 + } + } + } catch (exception: Exception) { + println("Map generator exception: ${exception.message}") + Gdx.app.postRunnable { + setButtonsEnabled(true) + Gdx.input.inputProcessor = editorScreen.stage + Popup(editorScreen).apply { + addGoodSizedLabel("It looks like we can't make a map with the parameters you requested!".tr()) + row() + addCloseButton() + }.open() + } + } + } + } + + class MapEditorNewMapTab( + private val parent: MapEditorGenerateTab + ): Table(BaseScreen.skin) { + val generateButton = "".toTextButton() + val mapParametersTable = MapParametersTable(parent.editorScreen.newMapParameters, isEmptyMapAllowed = true) + + init { + top() + pad(10f) + add("Map Options".toLabel(fontSize = 24)).row() + add(mapParametersTable).row() + add(generateButton).padTop(15f).row() + generateButton.onClick { parent.generate(MapGeneratorSteps.All) } + } + } + + class MapEditorGenerateStepsTab( + private val parent: MapEditorGenerateTab + ): Table(BaseScreen.skin) { + private val optionGroup = ButtonGroup() + val generateButton = "".toTextButton() + private var choice = MapGeneratorSteps.None + private val newMapParameters = parent.editorScreen.newMapParameters + private val tileMap = parent.editorScreen.tileMap + private val actualMapParameters = tileMap.mapParameters + + init { + top() + pad(10f) + defaults().pad(2.5f) + add("Generator steps".toLabel(fontSize = 24)).row() + optionGroup.setMinCheckCount(0) + for (option in MapGeneratorSteps.values()) { + if (option <= MapGeneratorSteps.All) continue + val checkBox = option.label.toCheckBox { + choice = option + generateButton.enable() + } + add(checkBox).row() + optionGroup.add(checkBox) + } + add(generateButton).padTop(15f).row() + generateButton.onClick { + parent.generate(choice) + choice.copyParameters?.invoke(newMapParameters, actualMapParameters) + } + } + } +} diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorLoadTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorLoadTab.kt new file mode 100644 index 0000000000..e59bea0f22 --- /dev/null +++ b/core/src/com/unciv/ui/mapeditor/MapEditorLoadTab.kt @@ -0,0 +1,141 @@ +package com.unciv.ui.mapeditor + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.Input +import com.badlogic.gdx.files.FileHandle +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.unciv.logic.MapSaver +import com.unciv.logic.UncivShowableException +import com.unciv.models.ruleset.RulesetCache +import com.unciv.models.translations.tr +import com.unciv.ui.popup.Popup +import com.unciv.ui.popup.ToastPopup +import com.unciv.ui.popup.YesNoPopup +import com.unciv.ui.utils.* +import kotlin.concurrent.thread + +class MapEditorLoadTab( + private val editorScreen: MapEditorScreen, + headerHeight: Float +): Table(BaseScreen.skin), TabbedPager.IPageExtensions { + private val mapFiles = MapEditorFilesTable( + initWidth = editorScreen.getToolsWidth() - 20f, + includeMods = true, + this::selectFile) + + private val loadButton = "Load map".toTextButton() + private val deleteButton = "Delete map".toTextButton() + + private var chosenMap: FileHandle? = null + + init { + val buttonTable = Table(skin) + buttonTable.defaults().pad(10f).fillX() + loadButton.onClick(this::loadHandler) + buttonTable.add(loadButton) + deleteButton.onClick(this::deleteHandler) + buttonTable.add(deleteButton) + buttonTable.pack() + + val fileTableHeight = editorScreen.stage.height - headerHeight - buttonTable.height - 2f + val scrollPane = AutoScrollPane(mapFiles, skin) + scrollPane.setOverscroll(false, true) + add(scrollPane).height(fileTableHeight).width(editorScreen.getToolsWidth() - 20f).row() + add(buttonTable).row() + } + + private fun loadHandler() { + if (chosenMap == null) return + thread(name = "MapLoader", block = this::loaderThread) + } + + private fun deleteHandler() { + if (chosenMap == null) return + YesNoPopup("Are you sure you want to delete this map?", { + chosenMap!!.delete() + mapFiles.update() + }, editorScreen).open() + } + + override fun activated(index: Int, caption: String, pager: TabbedPager) { + pager.setScrollDisabled(true) + mapFiles.update() + editorScreen.keyPressDispatcher[KeyCharAndCode.RETURN] = this::loadHandler + editorScreen.keyPressDispatcher[KeyCharAndCode.DEL] = this::deleteHandler + editorScreen.keyPressDispatcher[Input.Keys.UP] = { mapFiles.moveSelection(-1) } + editorScreen.keyPressDispatcher[Input.Keys.DOWN] = { mapFiles.moveSelection(1) } + selectFile(null) + } + + override fun deactivated(index: Int, caption: String, pager: TabbedPager) { + editorScreen.keyPressDispatcher.revertToCheckPoint() + pager.setScrollDisabled(false) + } + + fun selectFile(file: FileHandle?) { + chosenMap = file + loadButton.isEnabled = (file != null) + deleteButton.isEnabled = (file != null) + deleteButton.color = if (file != null) Color.SCARLET else Color.BROWN + } + + fun loaderThread() { + var popup: Popup? = null + var needPopup = true // loadMap can fail faster than postRunnable runs + Gdx.app.postRunnable { + if (!needPopup) return@postRunnable + popup = Popup(editorScreen).apply { + addGoodSizedLabel("Loading...") + open() + } + } + try { + val map = MapSaver.loadMap(chosenMap!!, checkSizeErrors = false) + + val missingMods = map.mapParameters.mods.filter { it !in RulesetCache }.toMutableList() + // [TEMPORARY] conversion of old maps with a base ruleset contained in the mods + val newBaseRuleset = map.mapParameters.mods.filter { it !in missingMods }.firstOrNull { RulesetCache[it]!!.modOptions.isBaseRuleset } + if (newBaseRuleset != null) map.mapParameters.baseRuleset = newBaseRuleset + // + + if (map.mapParameters.baseRuleset !in RulesetCache) missingMods += map.mapParameters.baseRuleset + if (missingMods.isNotEmpty()) { + Gdx.app.postRunnable { + needPopup = false + popup?.close() + ToastPopup("Missing mods: [${missingMods.joinToString()}]", editorScreen) + } + } else Gdx.app.postRunnable { + Gdx.input.inputProcessor = null // This is to stop ANRs happening here, until the map editor screen sets up. + try { + // For deprecated maps, set the base ruleset field if it's still saved in the mods field + val modBaseRuleset = map.mapParameters.mods.firstOrNull { RulesetCache[it]!!.modOptions.isBaseRuleset } + if (modBaseRuleset != null) { + map.mapParameters.baseRuleset = modBaseRuleset + map.mapParameters.mods -= modBaseRuleset + } + + editorScreen.loadMap(map) + needPopup = false + popup?.close() + Gdx.input.inputProcessor = stage + } catch (ex: Throwable) { + needPopup = false + popup?.close() + println("Error displaying map \"$chosenMap\": ${ex.localizedMessage}") + Gdx.input.inputProcessor = editorScreen.stage + ToastPopup("Error loading map!", editorScreen) + } + } + } catch (ex: Throwable) { + needPopup = false + Gdx.app.postRunnable { + popup?.close() + println("Error loading map \"$chosenMap\": ${ex.localizedMessage}") + ToastPopup("Error loading map!".tr() + + (if (ex is UncivShowableException) "\n" + ex.message else ""), editorScreen) + } + } + } +} \ 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 new file mode 100644 index 0000000000..980a498172 --- /dev/null +++ b/core/src/com/unciv/ui/mapeditor/MapEditorMainTabs.kt @@ -0,0 +1,52 @@ +package com.unciv.ui.mapeditor + +import com.unciv.logic.MapSaver +import com.unciv.ui.images.ImageGetter +import com.unciv.ui.utils.KeyCharAndCode +import com.unciv.ui.utils.TabbedPager + +class MapEditorMainTabs( + editorScreen: MapEditorScreen +) : TabbedPager( + minimumHeight = editorScreen.stage.height, + maximumHeight = editorScreen.stage.height, + headerFontSize = 24, + keyPressDispatcher = editorScreen.keyPressDispatcher, + capacity = 7 +) { + val view = MapEditorViewTab(editorScreen) + val generate = MapEditorGenerateTab(editorScreen) + val edit = MapEditorEditTab(editorScreen, headerHeight) + val load = MapEditorLoadTab(editorScreen, headerHeight) + val save = MapEditorSaveTab(editorScreen, headerHeight) + val mods = MapEditorModsTab(editorScreen) + val options = MapEditorOptionsTab(editorScreen) + + init { + prefWidth = editorScreen.getToolsWidth() + + addPage("View", view, + ImageGetter.getImage("OtherIcons/Search"), 25f, + shortcutKey = KeyCharAndCode.ctrl('i')) + addPage("Generate", generate, + ImageGetter.getImage("OtherIcons/New"), 25f, + shortcutKey = KeyCharAndCode.ctrl('n')) + addPage("Edit", edit, + ImageGetter.getImage("OtherIcons/Terrains"), 25f, + shortcutKey = KeyCharAndCode.ctrl('e')) + addPage("Load", load, + ImageGetter.getImage("OtherIcons/Load"), 25f, + shortcutKey = KeyCharAndCode.ctrl('l'), + disabled = MapSaver.getMaps().isEmpty()) + addPage("Save", save, + ImageGetter.getImage("OtherIcons/Checkmark"), 25f, + shortcutKey = KeyCharAndCode.ctrl('s')) + addPage("Mods", mods, + ImageGetter.getImage("OtherIcons/Mods"), 25f, + shortcutKey = KeyCharAndCode.ctrl('d')) + addPage("Options", options, + ImageGetter.getImage("OtherIcons/Settings"), 25f, + shortcutKey = KeyCharAndCode.ctrl('o')) + selectPage(0) + } +} diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorMenuPopup.kt b/core/src/com/unciv/ui/mapeditor/MapEditorMenuPopup.kt deleted file mode 100644 index 5d2cf43d63..0000000000 --- a/core/src/com/unciv/ui/mapeditor/MapEditorMenuPopup.kt +++ /dev/null @@ -1,185 +0,0 @@ -package com.unciv.ui.mapeditor - -import com.badlogic.gdx.Gdx -import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane -import com.badlogic.gdx.scenes.scene2d.ui.Table -import com.unciv.Constants -import com.unciv.MainMenuScreen -import com.unciv.UncivGame -import com.unciv.models.ruleset.RulesetCache -import com.unciv.models.translations.tr -import com.unciv.ui.images.ImageGetter -import com.unciv.ui.newgamescreen.ModCheckboxTable -import com.unciv.ui.newgamescreen.TranslatedSelectBox -import com.unciv.ui.popup.Popup -import com.unciv.ui.popup.ToastPopup -import com.unciv.ui.utils.* -import kotlin.math.max - -class MapEditorMenuPopup(var mapEditorScreen: MapEditorScreen): Popup(mapEditorScreen) { - - init { - defaults().fillX() - add(("{RNG Seed} " + mapEditorScreen.tileMap.mapParameters.seed.toString()).toLabel()).row() - addButton("Copy to clipboard") { Gdx.app.clipboard.contents = mapEditorScreen.tileMap.mapParameters.seed.toString() } - addSeparator() - addButton("New map", 'n') { - mapEditorScreen.tileMap.mapParameters.reseed() - UncivGame.Current.setScreen(NewMapScreen(mapEditorScreen.tileMap.mapParameters)) - } - addButton("Save map", 's') { mapEditorScreen.game.setScreen(SaveAndLoadMapScreen(mapEditorScreen.tileMap, true, mapEditorScreen)); close() } - addButton("Load map", 'l') { mapEditorScreen.game.setScreen(SaveAndLoadMapScreen(mapEditorScreen.tileMap, false, mapEditorScreen)); close() } - addButton("Exit map editor", 'x') { mapEditorScreen.game.setScreen(MainMenuScreen()); mapEditorScreen.dispose() } - addButton("Change ruleset", 'c') { MapEditorRulesetPopup(mapEditorScreen).open(); close() } - addCloseButton() - } - - class MapEditorRulesetPopup(val mapEditorScreen: MapEditorScreen) : Popup(mapEditorScreen) { - var ruleset = mapEditorScreen.ruleset.clone() // don't take the actual one, so we can decide to not make changes - val checkboxTable: ModCheckboxTable - val mapParameters = mapEditorScreen.tileMap.mapParameters - - init { - val mods = mapParameters.mods - val baseRuleset = mapParameters.baseRuleset - - checkboxTable = ModCheckboxTable(mods, baseRuleset, mapEditorScreen) { - ruleset.clear() - val newRuleset = RulesetCache.getComplexRuleset(mods, baseRuleset) - ruleset.add(newRuleset) - ruleset.mods += mods - ruleset.modOptions = newRuleset.modOptions - - ImageGetter.setNewRuleset(ruleset) - } - - val combinedTable = Table(BaseScreen.skin) - - val baseRulesetSelectionBox = getBaseRulesetSelectBox() - if (baseRulesetSelectionBox != null) { - // TODO: For some reason I'm unable to get these two tables to be equally wide - // someone who knows what they're doing should fix this - val maxWidth = max(baseRulesetSelectionBox.minWidth, checkboxTable.minWidth) - baseRulesetSelectionBox.width = maxWidth - checkboxTable.width = maxWidth - combinedTable.add(baseRulesetSelectionBox).row() - } - - combinedTable.add(checkboxTable) - - add(ScrollPane(combinedTable)).maxHeight(mapEditorScreen.stage.height * 0.8f).colspan(2).row() - - addButtonInRow("Save", KeyCharAndCode.RETURN) { - val incompatibilities = HashSet() - for (set in mapEditorScreen.tileMap.values.map { it.getRulesetIncompatibility(ruleset) }) - incompatibilities.addAll(set) - incompatibilities.remove("") - - if (incompatibilities.isEmpty()) { - mapEditorScreen.tileMap.mapParameters.mods = mods - mapEditorScreen.game.setScreen(MapEditorScreen(mapEditorScreen.tileMap)) // reset all images etc. - return@addButtonInRow - } - - val incompatibilityTable = Table() - for (inc in incompatibilities) - incompatibilityTable.add(inc.toLabel()).row() - Popup(screen).apply { - add(ScrollPane(incompatibilityTable)).colspan(2) - .maxHeight(screen.stage.height * 0.8f).row() - add("Change map to fit selected ruleset?".toLabel()).colspan(2).row() - addButtonInRow(Constants.yes, 'y') { - for (tile in mapEditorScreen.tileMap.values) - tile.normalizeToRuleset(ruleset) - mapEditorScreen.tileMap.mapParameters.mods = mods - mapEditorScreen.game.setScreen(MapEditorScreen(mapEditorScreen.tileMap)) - } - addButtonInRow(Constants.no, 'n') { close() } - equalizeLastTwoButtonWidths() - }.open(true) - } - - // Reset - no changes - addCloseButton { ImageGetter.setNewRuleset(mapEditorScreen.ruleset) } - } - - private fun getBaseRulesetSelectBox(): Table? { - val rulesetSelectionBox = Table() - - val sortedBaseRulesets = RulesetCache.getSortedBaseRulesets() - if (sortedBaseRulesets.size < 2) return null - - rulesetSelectionBox.add("{Base Ruleset}:".toLabel()).left() - val selectBox = TranslatedSelectBox(sortedBaseRulesets, mapParameters.baseRuleset, BaseScreen.skin) - - val onChange = onChange@{ newBaseRuleset: String -> - val previousSelection = mapParameters.baseRuleset - if (newBaseRuleset == previousSelection) return@onChange null - - // Check if this mod is well-defined - val baseRulesetErrors = RulesetCache[newBaseRuleset]!!.checkModLinks() - if (baseRulesetErrors.isError()) { - val toastMessage = "The mod you selected is incorrectly defined!".tr() + "\n\n${baseRulesetErrors.getErrorText()}" - ToastPopup(toastMessage, mapEditorScreen, 5000L) - return@onChange previousSelection - } - - // If so, add it to the current ruleset - mapParameters.baseRuleset = newBaseRuleset - reloadRuleset() - - // Check if the ruleset in it's entirety is still well-defined - val modLinkErrors = ruleset.checkModLinks() - if (modLinkErrors.isError()) { - mapParameters.mods.clear() - reloadRuleset() - // TODO: These can't be shown as this screen is itself a popup, what should be done instead? - val toastMessage = - "This base ruleset is not compatible with the previously selected\nextension mods. They have been disabled.".tr() - ToastPopup(toastMessage, mapEditorScreen, 5000L) - - checkboxTable.disableAllCheckboxes() - } else if (modLinkErrors.isWarnUser()) { - val toastMessage = - "{The mod combination you selected has problems.}\n{You can play it, but don't expect everything to work!}".tr() + - "\n\n${modLinkErrors.getErrorText()}" - ToastPopup(toastMessage, mapEditorScreen, 5000L) - } - - - checkboxTable.setBaseRuleset(newBaseRuleset) - - null - } - - - selectBox.onChange { - val newValue = onChange(selectBox.selected.value) - if (newValue != null) selectBox.setSelected(newValue) - } - - onChange(mapParameters.baseRuleset) - - rulesetSelectionBox.add(selectBox).fillX().row() - return rulesetSelectionBox - } - - private fun reloadRuleset() { - ruleset.clear() - val newRuleset = RulesetCache.getComplexRuleset(mapParameters.mods, mapParameters.baseRuleset) - ruleset.add(newRuleset) - ruleset.mods += mapParameters.baseRuleset - ruleset.mods += mapParameters.mods - ruleset.modOptions = newRuleset.modOptions - - mapEditorScreen.tileMap.removeMissingTerrainModReferences(ruleset) - - ImageGetter.setNewRuleset(ruleset) - - // Recreate screen, since the improvementss, nations etc. could be outdated - mapEditorScreen.game.setScreen(MapEditorScreen(mapEditorScreen.tileMap)) - } - - } - -} diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorModsTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorModsTab.kt new file mode 100644 index 0000000000..704e6df100 --- /dev/null +++ b/core/src/com/unciv/ui/mapeditor/MapEditorModsTab.kt @@ -0,0 +1,144 @@ +package com.unciv.ui.mapeditor + +import com.badlogic.gdx.scenes.scene2d.ui.Cell +import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.utils.Align +import com.unciv.Constants +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.RulesetCache +import com.unciv.ui.newgamescreen.ModCheckboxTable +import com.unciv.ui.newgamescreen.TranslatedSelectBox +import com.unciv.ui.popup.Popup +import com.unciv.ui.utils.* +import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip + +class MapEditorModsTab( + private val editorScreen: MapEditorScreen +): Table(BaseScreen.skin), TabbedPager.IPageExtensions { + private val mods = editorScreen.newMapParameters.mods + private var modsTable: ModCheckboxTable + private val modsTableCell: Cell + private val applyButton = "Change map ruleset".toTextButton() + private val revertButton = "Revert to map ruleset".toTextButton() + private val baseRulesetSelectBox: TranslatedSelectBox + + init { + val rulesetName = editorScreen.newMapParameters.baseRuleset + // Out dirty flag `modsTabNeedsRefresh` will be true on first activation, + // so this will be replaced and can now be minimal + modsTable = ModCheckboxTable(linkedSetOf(), rulesetName, editorScreen, false) {} + + val baseRulesets = RulesetCache.getSortedBaseRulesets() + baseRulesetSelectBox = TranslatedSelectBox(baseRulesets, rulesetName, BaseScreen.skin) + baseRulesetSelectBox.onChange { + val newBaseRuleset = baseRulesetSelectBox.selected.value + editorScreen.newMapParameters.baseRuleset = newBaseRuleset + modsTable.setBaseRuleset(newBaseRuleset) + modsTable.disableAllCheckboxes() + enableApplyButton() + } + + top() + pad(5f) + + add(Table().apply { + add("{Base Ruleset}:".toLabel()) + add(baseRulesetSelectBox).fillX() + }).fillX().padBottom(10f).row() + + add(Table().apply { + add(applyButton).padRight(10f) + add(revertButton) + }).fillX().pad(10f).row() + + modsTableCell = add(modsTable) + row() + + applyButton.onClick(this::applyControls) + applyButton.addTooltip("Change the map to use the ruleset selected on this page", 21f, targetAlign = Align.bottom) + + revertButton.onClick(this::revertControls) + revertButton.addTooltip("Reset the controls to reflect the current map ruleset", 21f, targetAlign = Align.bottom) + } + + private fun enableApplyButton() { + val currentParameters = editorScreen.tileMap.mapParameters + val enabled = + currentParameters.mods != mods || + currentParameters.baseRuleset != baseRulesetSelectBox.selected.value + applyButton.isEnabled = enabled + revertButton.isEnabled = enabled + } + + private fun revertControls() { + val currentParameters = editorScreen.tileMap.mapParameters + baseRulesetSelectBox.setSelected(currentParameters.baseRuleset) + mods.clear() + mods.addAll(currentParameters.mods) // clone current "into" editorScreen.newMapParameters.mods + modsTable = ModCheckboxTable(mods, currentParameters.baseRuleset, editorScreen, false) { + enableApplyButton() + } + modsTableCell.setActor(modsTable) + enableApplyButton() + } + + private fun applyControls() { + val newRuleset = RulesetCache.getComplexRuleset(mods, editorScreen.newMapParameters.baseRuleset) + val incompatibilities = getIncompatibilities(newRuleset) + if (incompatibilities.isEmpty()) { + editorScreen.applyRuleset(newRuleset, editorScreen.newMapParameters.baseRuleset, mods) + enableApplyButton() + } else { + AskFitMapToRulesetPopup(editorScreen, incompatibilities) { + fitMapToRuleset(newRuleset) + editorScreen.applyRuleset(newRuleset, editorScreen.newMapParameters.baseRuleset, mods) + enableApplyButton() + } + } + } + + override fun activated(index: Int, caption: String, pager: TabbedPager) { + enableApplyButton() + if (!editorScreen.modsTabNeedsRefresh) return + editorScreen.modsTabNeedsRefresh = false + revertControls() + } + + private fun getIncompatibilities(newRuleset: Ruleset): List { + val incompatibilities = HashSet() + for (tile in editorScreen.tileMap.values) { + incompatibilities += tile.getRulesetIncompatibility(newRuleset) + } + incompatibilities.remove("") + return incompatibilities.sorted() + } + + private class AskFitMapToRulesetPopup( + editorScreen: BaseScreen, + incompatibilities: List, + onOK: () -> Unit + ): Popup(editorScreen) { + init { + val incompatibilityTable = Table().apply { + for (inc in incompatibilities) + add(inc.toLabel()).row() + } + add(ScrollPane(incompatibilityTable)).colspan(2) + .maxHeight(screen.stage.height * 0.8f).row() + addGoodSizedLabel("Change map to fit selected ruleset?", 24).colspan(2).row() + addButtonInRow(Constants.yes, 'y') { + onOK() + close() + } + addButtonInRow(Constants.no, 'n') { close() } + equalizeLastTwoButtonWidths() + open(true) + } + } + + private fun fitMapToRuleset(newRuleset: Ruleset) { + for (tile in editorScreen.tileMap.values) + tile.normalizeToRuleset(newRuleset) + } +} diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorOptionsTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorOptionsTab.kt new file mode 100644 index 0000000000..e29dabfdee --- /dev/null +++ b/core/src/com/unciv/ui/mapeditor/MapEditorOptionsTab.kt @@ -0,0 +1,85 @@ +package com.unciv.ui.mapeditor + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.graphics.Color +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.MapSaver +import com.unciv.models.translations.tr +import com.unciv.ui.popup.ToastPopup +import com.unciv.ui.utils.* + +class MapEditorOptionsTab( + private val editorScreen: MapEditorScreen +): Table(BaseScreen.skin), TabbedPager.IPageExtensions { + private val seedLabel = "".toLabel(Color.GOLD) + private val copySeedButton = "Copy to clipboard".toTextButton() + private val tileMatchGroup = ButtonGroup() + private val copyMapButton = "Copy to clipboard".toTextButton() + private val pasteMapButton = "Load copied data".toTextButton() + + private var seedToCopy = "" + private var tileMatchFuzziness = TileMatchFuzziness.CompleteMatch + + enum class TileMatchFuzziness(val label: String) { + CompleteMatch("Complete match"), + NoImprovement("Except improvements"), + BaseAndFeatures("Base and terrain features"), + BaseTerrain("Base terrain only"), + LandOrWater("Land or water only"), + } + init { + top() + defaults().pad(10f) + + add("Tile Matching Criteria".toLabel(Color.GOLD)).row() + for (option in TileMatchFuzziness.values()) { + val check = option.label.toCheckBox(option == tileMatchFuzziness) + { tileMatchFuzziness = option } + add(check).row() + tileMatchGroup.add(check) + } + addSeparator(Color.GRAY) + + add(seedLabel).row() + add(copySeedButton).row() + copySeedButton.onClick { + Gdx.app.clipboard.contents = seedToCopy + } + addSeparator(Color.GRAY) + + add("Map copy and paste".toLabel(Color.GOLD)).row() + copyMapButton.onClick(this::copyHandler) + add(copyMapButton).row() + pasteMapButton.onClick(this::pasteHandler) + add(pasteMapButton).row() + } + + private fun copyHandler() { + Gdx.app.clipboard.contents = MapSaver.mapToSavedString(editorScreen.getMapCloneForSave()) + } + + private fun pasteHandler() { + try { + val clipboardContentsString = Gdx.app.clipboard.contents.trim() + val loadedMap = MapSaver.mapFromSavedString(clipboardContentsString, checkSizeErrors = false) + editorScreen.loadMap(loadedMap) + } catch (ex: Exception) { + ToastPopup("Could not load map!", editorScreen) + } + } + + override fun activated(index: Int, caption: String, pager: TabbedPager) { + seedToCopy = editorScreen.tileMap.mapParameters.seed.toString() + seedLabel.setText("Current map RNG seed: [$seedToCopy]".tr()) + editorScreen.keyPressDispatcher[KeyCharAndCode.ctrl('c')] = this::copyHandler + editorScreen.keyPressDispatcher[KeyCharAndCode.ctrl('v')] = this::pasteHandler + pasteMapButton.isEnabled = Gdx.app.clipboard.hasContents() + } + + override fun deactivated(index: Int, caption: String, pager: TabbedPager) { + editorScreen.tileMatchFuzziness = tileMatchFuzziness + editorScreen.keyPressDispatcher.revertToCheckPoint() + } +} diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorOptionsTable.kt b/core/src/com/unciv/ui/mapeditor/MapEditorOptionsTable.kt deleted file mode 100644 index 49b2ee078e..0000000000 --- a/core/src/com/unciv/ui/mapeditor/MapEditorOptionsTable.kt +++ /dev/null @@ -1,458 +0,0 @@ -package com.unciv.ui.mapeditor - -import com.badlogic.gdx.graphics.Color -import com.badlogic.gdx.scenes.scene2d.Actor -import com.badlogic.gdx.scenes.scene2d.Group -import com.badlogic.gdx.scenes.scene2d.Touchable -import com.badlogic.gdx.scenes.scene2d.ui.Table -import com.unciv.Constants -import com.unciv.UncivGame -import com.unciv.logic.civilization.CivilizationInfo -import com.unciv.logic.map.MapUnit -import com.unciv.logic.map.RoadStatus -import com.unciv.logic.map.TileInfo -import com.unciv.models.metadata.Player -import com.unciv.models.ruleset.Nation -import com.unciv.models.ruleset.tile.TerrainType -import com.unciv.models.translations.tr -import com.unciv.ui.images.IconCircleGroup -import com.unciv.ui.images.ImageGetter -import com.unciv.ui.tilegroups.TileGroup -import com.unciv.ui.tilegroups.TileSetStrings -import com.unciv.ui.utils.* - -class MapEditorOptionsTable(val mapEditorScreen: MapEditorScreen): Table(BaseScreen.skin) { - private val tileSetLocation = "TileSets/" + UncivGame.Current.settings.tileSet + "/" - - var tileAction: (TileInfo) -> Unit = {} - - private val editorPickTable = Table() - - var brushSize = 1 - private var currentHex: Actor = Group() - - private val ruleset = mapEditorScreen.ruleset - private val gameParameters = mapEditorScreen.gameSetupInfo.gameParameters - - private val scrollPanelHeight = mapEditorScreen.stage.height * 0.7f - 100f // -100 reserved for currentHex table - - init { - update() - touchable = Touchable.enabled - } - - fun update() { - clear() - height = mapEditorScreen.stage.height - width = mapEditorScreen.stage.width / 3 - - setTerrainsAndResources() - - val tabPickerTable = Table().apply { defaults().pad(10f) } - val terrainsAndResourcesTabButton = "Terrains & Resources".toTextButton() - .onClick { setTerrainsAndResources() } - tabPickerTable.add(terrainsAndResourcesTabButton) - - val improvementsButton = "Improvements".toTextButton() - .onClick { setImprovements() } - tabPickerTable.add(improvementsButton) - - tabPickerTable.pack() - - val sliderTab = Table() - - val sliderLabel = "{Brush Size} $brushSize".toLabel() - val slider = UncivSlider(1f, 5f, 1f, initial = brushSize.toFloat()) { - brushSize = it.toInt() - sliderLabel.setText("{Brush Size} $brushSize".tr()) - } - - sliderTab.defaults().pad(5f) - sliderTab.add(sliderLabel) - sliderTab.add(slider) - - add(sliderTab).row() - add(AutoScrollPane(tabPickerTable).apply { this.width = mapEditorScreen.stage.width / 3 }).row() - - add(editorPickTable).row() - } - - private fun setTerrainsAndResources() { - - val baseTerrainTable = Table().apply { defaults().pad(20f) } - val terrainFeaturesTable = Table().apply { defaults().pad(20f) } - - terrainFeaturesTable.add(getHex(ImageGetter.getRedCross(50f, 0.6f)).apply { - onClick { - tileAction = { - it.setTerrainFeatures(listOf()) - it.naturalWonder = null - it.hasBottomRiver = false - it.hasBottomLeftRiver = false - it.hasBottomRightRiver = false - } - setCurrentHex(getHex(ImageGetter.getRedCross(40f, 0.6f)), "Clear terrain features") - } - }).row() - - - addTerrainOptions(terrainFeaturesTable, baseTerrainTable) - addRiverToggleOptions(baseTerrainTable) - - - val resources = getResourceActors() - - background = ImageGetter.getBackground(Color.GRAY.cpy().apply { a = 0.7f }) - - val terrainsAndResourcesTable = Table() - terrainsAndResourcesTable.add(AutoScrollPane(baseTerrainTable).apply { setScrollingDisabled(true, false) }).height(scrollPanelHeight) - - terrainsAndResourcesTable.add(AutoScrollPane(terrainFeaturesTable).apply { setScrollingDisabled(true, false) }).height(scrollPanelHeight) - - val resourcesTable = Table() - for (resource in resources) resourcesTable.add(resource).row() - resourcesTable.pack() - terrainsAndResourcesTable.add(AutoScrollPane(resourcesTable).apply { setScrollingDisabled(true, false) }).height(scrollPanelHeight).row() - - terrainsAndResourcesTable.pack() - - editorPickTable.clear() - editorPickTable.add(terrainsAndResourcesTable) - } - - private fun setImprovements() { - - editorPickTable.clear() - - val improvementsTable = Table() - improvementsTable.add(getHex(ImageGetter.getRedCross(40f, 0.6f)).apply { - onClick { - tileAction = { it.improvement = null; it.roadStatus = RoadStatus.None } - setCurrentHex(getHex(ImageGetter.getRedCross(40f, 0.6f)), "Clear improvements") - } - }).row() - - for (improvement in ruleset.tileImprovements.values) { - if (improvement.name.startsWith(Constants.remove)) continue - if (improvement.name == Constants.cancelImprovementOrder) continue - val improvementImage = getHex(ImageGetter.getImprovementIcon(improvement.name, 40f)) - improvementImage.onClick { - tileAction = { - when (improvement.name) { - RoadStatus.Road.name -> it.roadStatus = RoadStatus.Road - RoadStatus.Railroad.name -> it.roadStatus = RoadStatus.Railroad - else -> it.improvement = improvement.name - } - } - val improvementIcon = getHex(ImageGetter.getImprovementIcon(improvement.name, 40f)) - setCurrentHex(improvementIcon, improvement.name.tr() + "\n" + improvement.cloneStats().toString()) - } - improvementsTable.add(improvementImage).row() - } - editorPickTable.add(AutoScrollPane(improvementsTable).apply { setScrollingDisabled(true, false) }).height(scrollPanelHeight) - - // Menu for the Starting Locations - val nationTable = Table() - - for (nation in ruleset.nations.values) { - if (nation.isSpectator() || nation.isBarbarian()) continue // no improvements for spectator - - val nationImage = getHex(ImageGetter.getNationIndicator(nation, 40f)) - nationImage.onClick { - tileAction = { - mapEditorScreen.tileMap.apply { - // toggle the starting location here, note this allows - // both multiple locations per nation and multiple nations per tile - if (!addStartingLocation(nation.name, it)) - removeStartingLocation(nation.name, it) - } - } - - val nationIcon = getHex(ImageGetter.getNationIndicator(nation, 40f)) - setCurrentHex(nationIcon, "[${nation.name}] starting location") - } - nationTable.add(nationImage).row() - } - - editorPickTable.add(AutoScrollPane(nationTable).apply { setScrollingDisabled(true, false) }).height(scrollPanelHeight) - } - - /** currently unused */ - fun setUnits() { - editorPickTable.clear() - - val nationsTable = Table() - - // default player - first MajorCiv player - val defaultPlayer = gameParameters.players.first { - it.chosenCiv != Constants.spectator && it.chosenCiv != Constants.random - } - var currentPlayer = getPlayerIndexString(defaultPlayer) - var currentNation: Nation = ruleset.nations[defaultPlayer.chosenCiv]!! - var currentUnit = ruleset.units.values.first() - - fun setUnitTileAction() { - val unitImage = ImageGetter.getUnitIcon(currentUnit.name, currentNation.getInnerColor()) - .surroundWithCircle(40f * 0.9f).apply { circle.color = currentNation.getOuterColor() } - .surroundWithCircle(40f, false).apply { circle.color = currentNation.getInnerColor() } - - setCurrentHex(unitImage, currentUnit.name.tr() + " - $currentPlayer (" + currentNation.name.tr() + ")") - tileAction = { - val unit = MapUnit() - unit.baseUnit = currentUnit - unit.name = currentUnit.name - unit.owner = currentNation.name - unit.civInfo = CivilizationInfo(currentNation.name).apply { nation = currentNation } // needed for the unit icon to render correctly - unit.updateUniques(ruleset) - if (unit.movement.canMoveTo(it)) { - when { - unit.baseUnit.movesLikeAirUnits() -> { - it.airUnits.add(unit) - if (!it.isCityCenter()) unit.isTransported = true // if not city - air unit enters carrier - } - unit.isCivilian() -> it.civilianUnit = unit - else -> it.militaryUnit = unit - } - unit.currentTile = it // needed for unit icon - unit needs to know if it's embarked or not... - } - } - } - - // delete units icon - nationsTable.add(getCrossedIcon().onClick { - tileAction = { it.stripUnits() } - setCurrentHex(getCrossedIcon(), "Remove units") - }).row() - - // player icons - for (player in gameParameters.players) { - if (player.chosenCiv == Constants.random || player.chosenCiv == Constants.spectator) - continue - val nation = ruleset.nations[player.chosenCiv]!! - val nationImage = ImageGetter.getNationIndicator(nation, 40f) - nationsTable.add(nationImage).row() - nationImage.onClick { - currentNation = nation - currentPlayer = getPlayerIndexString(player) - setUnitTileAction() - } - } - - // barbarians icon - if (!gameParameters.noBarbarians) { - val barbarians = ruleset.nations.values.filter { it.isBarbarian() } - for (nation in barbarians) { - val nationImage = ImageGetter.getNationIndicator(nation, 40f) - nationsTable.add(nationImage).row() - nationImage.onClick { - currentNation = nation - currentPlayer = "" - setUnitTileAction() - } - } - } - - editorPickTable.add(AutoScrollPane(nationsTable)).height(scrollPanelHeight) - - val unitsTable = Table() - for (unit in ruleset.units.values) { - val unitImage = ImageGetter.getUnitIcon(unit.name).surroundWithCircle(40f) - unitsTable.add(unitImage).row() - unitImage.onClick { currentUnit = unit; setUnitTileAction() } - } - editorPickTable.add(AutoScrollPane(unitsTable)).height(scrollPanelHeight) - } - - private fun getPlayerIndexString(player: Player): String { - val index = gameParameters.players.indexOf(player) + 1 - return "Player [$index]".tr() - } - - private fun getCrossedIcon(): Actor { - return ImageGetter.getRedCross(20f, 0.6f) - .surroundWithCircle(40f, false) - .apply { circle.color = Color.WHITE } - } - - private fun getCrossedResource(): Actor { - val redCross = ImageGetter.getRedCross(45f, 0.5f) - val group = IconCircleGroup(40f, redCross, false) - group.circle.color = ImageGetter.foodCircleColor - return group - } - - private fun getResourceActors(): ArrayList { - val resources = ArrayList() - resources.add(getHex(getCrossedResource()).apply { - onClick { - tileAction = { it.resource = null } - setCurrentHex(getHex(getCrossedResource()), "Clear resource") - } - }) - - for (resource in ruleset.tileResources.values) { - if (resource.terrainsCanBeFoundOn.none { ruleset.terrains.containsKey(it) }) continue // This resource can't be placed - val resourceHex = getHex(ImageGetter.getResourceImage(resource.name, 40f)) - resourceHex.onClick { - tileAction = { it.setTileResource(resource) } - - // for the tile image - val tileInfo = TileInfo() - tileInfo.ruleset = mapEditorScreen.ruleset - val terrain = resource.terrainsCanBeFoundOn.first { ruleset.terrains.containsKey(it) } - val terrainObject = ruleset.terrains[terrain]!! - - if (terrainObject.type != TerrainType.TerrainFeature) tileInfo.baseTerrain = terrain - else { - tileInfo.baseTerrain = - if (terrainObject.occursOn.isNotEmpty()) terrainObject.occursOn.first() - else ruleset.terrains.values.first { it.type == TerrainType.Land }.name - tileInfo.addTerrainFeature(terrain) - } - - tileInfo.resource = resource.name - tileInfo.setTerrainTransients() - - setCurrentHex(tileInfo, resource.name.tr() + "\n" + resource.cloneStats().toString()) - } - resources.add(resourceHex) - } - return resources - } - - private fun addTerrainOptions(terrainFeaturesTable: Table, baseTerrainTable: Table) { - for (terrain in ruleset.terrains.values) { - val tileInfo = TileInfo() - tileInfo.ruleset = ruleset - if (terrain.type == TerrainType.TerrainFeature) { - tileInfo.baseTerrain = when { - terrain.occursOn.isNotEmpty() -> terrain.occursOn.first() - else -> Constants.grassland - } - tileInfo.addTerrainFeature(terrain.name) - } else tileInfo.baseTerrain = terrain.name - val group = makeTileGroup(tileInfo) - - group.onClick { - tileAction = { - it.naturalWonder = null // If we're setting a base terrain it should remove the nat wonder - when (terrain.type) { - TerrainType.TerrainFeature -> { - if (terrain.occursOn.contains(it.getLastTerrain().name)) - it.addTerrainFeature(terrain.name) - } - TerrainType.NaturalWonder -> it.naturalWonder = terrain.name - else -> it.baseTerrain = terrain.name - } - } - setCurrentHex(tileInfo, terrain.name.tr() + "\n" + terrain.cloneStats().toString()) - } - - if (terrain.type == TerrainType.TerrainFeature) - terrainFeaturesTable.add(group).row() - else baseTerrainTable.add(group).row() - } - - - baseTerrainTable.pack() - terrainFeaturesTable.pack() - } - - private fun addRiverToggleOptions(baseTerrainTable: Table) { - baseTerrainTable.addSeparator() - - val tileInfoBottomRightRiver = TileInfo() - tileInfoBottomRightRiver.baseTerrain = Constants.plains - tileInfoBottomRightRiver.hasBottomRightRiver = true - val tileGroupBottomRightRiver = makeTileGroup(tileInfoBottomRightRiver) - tileGroupBottomRightRiver.onClick { - tileAction = { it.hasBottomRightRiver = !it.hasBottomRightRiver } - - setCurrentHex(tileInfoBottomRightRiver, "Bottom right river") - } - baseTerrainTable.add(tileGroupBottomRightRiver).row() - - - val tileInfoBottomRiver = TileInfo() - tileInfoBottomRiver.baseTerrain = Constants.plains - tileInfoBottomRiver.hasBottomRiver = true - val tileGroupBottomRiver = makeTileGroup(tileInfoBottomRiver) - tileGroupBottomRiver.onClick { - tileAction = { it.hasBottomRiver = !it.hasBottomRiver } - setCurrentHex(tileInfoBottomRiver, "Bottom river") - } - baseTerrainTable.add(tileGroupBottomRiver).row() - - - val tileInfoBottomLeftRiver = TileInfo() - tileInfoBottomLeftRiver.hasBottomLeftRiver = true - tileInfoBottomLeftRiver.baseTerrain = Constants.plains - val tileGroupBottomLeftRiver = makeTileGroup(tileInfoBottomLeftRiver) - tileGroupBottomLeftRiver.onClick { - tileAction = { it.hasBottomLeftRiver = !it.hasBottomLeftRiver } - setCurrentHex(tileInfoBottomLeftRiver, "Bottom left river") - } - baseTerrainTable.add(tileGroupBottomLeftRiver).row() - - baseTerrainTable.pack() - } - - private fun makeTileGroup(tileInfo: TileInfo): TileGroup { - tileInfo.ruleset = mapEditorScreen.ruleset - tileInfo.setTerrainTransients() - val group = TileGroup(tileInfo, TileSetStrings()) - group.showEntireMap = true - group.forMapEditorIcon = true - group.update() - return group - } - - - private fun getHex(image: Actor? = null): Group { - val hex = ImageGetter.getImage(tileSetLocation + "Hexagon") - hex.color = Color.WHITE - hex.width *= 0.3f - hex.height *= 0.3f - val group = Group() - group.setSize(hex.width, hex.height) - hex.center(group) - group.addActor(hex) - - if (image != null) { - image.setSize(40f, 40f) - image.center(group) - group.addActor(image) - } - return group - } - - - fun updateTileWhenClicked(tileInfo: TileInfo) { - tileAction(tileInfo) - tileInfo.normalizeToRuleset(ruleset) - } - - - private fun setCurrentHex(tileInfo: TileInfo, text: String) { - val tileGroup = TileGroup(tileInfo, TileSetStrings()) - .apply { - showEntireMap = true - forMapEditorIcon = true - update() - } - tileGroup.baseLayerGroup.moveBy(-10f, 10f) - setCurrentHex(tileGroup, text) - } - - private fun setCurrentHex(actor: Actor, text: String) { - currentHex.remove() - val currentHexTable = Table() - currentHexTable.add(text.toLabel()).padRight(30f) - currentHexTable.add(actor) - currentHexTable.pack() - currentHex = currentHexTable - currentHex.setPosition(stage.width - currentHex.width - 10, 10f) - stage.addActor(currentHex) - } - -} \ No newline at end of file diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorSaveTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorSaveTab.kt new file mode 100644 index 0000000000..6c65b9b751 --- /dev/null +++ b/core/src/com/unciv/ui/mapeditor/MapEditorSaveTab.kt @@ -0,0 +1,121 @@ +package com.unciv.ui.mapeditor + +import com.badlogic.gdx.Gdx +import com.badlogic.gdx.Input +import com.badlogic.gdx.files.FileHandle +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.scenes.scene2d.ui.TextField +import com.unciv.logic.MapSaver +import com.unciv.logic.map.MapType +import com.unciv.logic.map.TileMap +import com.unciv.models.translations.tr +import com.unciv.ui.popup.Popup +import com.unciv.ui.popup.ToastPopup +import com.unciv.ui.popup.YesNoPopup +import com.unciv.ui.utils.* +import kotlin.concurrent.thread + +class MapEditorSaveTab( + private val editorScreen: MapEditorScreen, + headerHeight: Float +): Table(BaseScreen.skin), TabbedPager.IPageExtensions { + private val mapFiles = MapEditorFilesTable( + initWidth = editorScreen.getToolsWidth() - 40f, + includeMods = false, + this::selectFile) + + private val saveButton = "Save map".toTextButton() + private val deleteButton = "Delete map".toTextButton() + private val mapNameTextField = TextField("", skin) + + private var chosenMap: FileHandle? = null + + init { + mapNameTextField.maxLength = 100 + mapNameTextField.textFieldFilter = TextField.TextFieldFilter { _, char -> char != '\\' && char != '/' } + mapNameTextField.selectAll() + // do NOT take the keyboard focus here! We're not even visible. + add(mapNameTextField).pad(10f).fillX().row() + + val buttonTable = Table(skin) + buttonTable.defaults().pad(10f).fillX() + saveButton.onClick(this::saveHandler) + mapNameTextField.onChange { + saveButton.isEnabled = mapNameTextField.text.isNotBlank() + } + buttonTable.add(saveButton) + + deleteButton.onClick(this::deleteHandler) + buttonTable.add(deleteButton) + buttonTable.pack() + + val fileTableHeight = editorScreen.stage.height - headerHeight - mapNameTextField.prefHeight - buttonTable.height - 22f + val scrollPane = AutoScrollPane(mapFiles, skin) + scrollPane.setOverscroll(false, true) + add(scrollPane).height(fileTableHeight).fillX().row() + add(buttonTable).row() + } + + private fun saveHandler() { + if (mapNameTextField.text.isBlank()) return + editorScreen.tileMap.mapParameters.name = mapNameTextField.text + editorScreen.tileMap.mapParameters.type = MapType.custom + thread(name = "MapSaver", block = this::saverThread) + } + + private fun deleteHandler() { + if (chosenMap == null) return + YesNoPopup("Are you sure you want to delete this map?", { + chosenMap!!.delete() + mapFiles.update() + }, editorScreen).open() + } + + override fun activated(index: Int, caption: String, pager: TabbedPager) { + pager.setScrollDisabled(true) + mapFiles.update() + editorScreen.keyPressDispatcher[KeyCharAndCode.RETURN] = this::saveHandler + editorScreen.keyPressDispatcher[KeyCharAndCode.DEL] = this::deleteHandler + editorScreen.keyPressDispatcher[Input.Keys.UP] = { mapFiles.moveSelection(-1) } + editorScreen.keyPressDispatcher[Input.Keys.DOWN] = { mapFiles.moveSelection(1) } + selectFile(null) + } + + override fun deactivated(index: Int, caption: String, pager: TabbedPager) { + editorScreen.keyPressDispatcher.revertToCheckPoint() + pager.setScrollDisabled(false) + stage.keyboardFocus = null + } + + fun selectFile(file: FileHandle?) { + chosenMap = file + mapNameTextField.text = file?.name() ?: editorScreen.tileMap.mapParameters.name + if (mapNameTextField.text.isBlank()) mapNameTextField.text = "My new map".tr() + mapNameTextField.setSelection(Int.MAX_VALUE, Int.MAX_VALUE) // sets caret to end of text + stage.keyboardFocus = mapNameTextField + saveButton.isEnabled = true + deleteButton.isEnabled = (file != null) + deleteButton.color = if (file != null) Color.SCARLET else Color.BROWN + } + + private fun saverThread() { + try { + val mapToSave = editorScreen.getMapCloneForSave() + mapToSave.assignContinents(TileMap.AssignContinentsMode.Reassign) + MapSaver.saveMap(mapNameTextField.text, mapToSave) + Gdx.app.postRunnable { + ToastPopup("Map saved successfully!", editorScreen) + } + editorScreen.isDirty = false + } catch (ex: Exception) { + ex.printStackTrace() + Gdx.app.postRunnable { + val cantLoadGamePopup = Popup(editorScreen) + cantLoadGamePopup.addGoodSizedLabel("It looks like your map can't be saved!").row() + cantLoadGamePopup.addCloseButton() + cantLoadGamePopup.open(force = true) + } + } + } +} \ No newline at end of file diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt b/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt index 901d4f7b6c..ec74dd80a7 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt @@ -1,178 +1,234 @@ package com.unciv.ui.mapeditor +import com.badlogic.gdx.Gdx import com.badlogic.gdx.graphics.Color -import com.badlogic.gdx.math.Vector2 -import com.badlogic.gdx.scenes.scene2d.InputEvent -import com.badlogic.gdx.scenes.scene2d.InputListener -import com.badlogic.gdx.scenes.scene2d.actions.Actions -import com.unciv.Constants +import com.unciv.MainMenuScreen import com.unciv.UncivGame import com.unciv.logic.HexMath -import com.unciv.logic.map.MapShape -import com.unciv.logic.map.MapSizeNew -import com.unciv.logic.map.TileInfo -import com.unciv.logic.map.TileMap -import com.unciv.models.ruleset.RulesetCache +import com.unciv.logic.map.* +import com.unciv.models.metadata.BaseRuleset import com.unciv.models.metadata.GameSetupInfo +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.RulesetCache import com.unciv.models.translations.tr -import com.unciv.ui.crashhandling.postCrashHandlingRunnable import com.unciv.ui.images.ImageGetter import com.unciv.ui.popup.ToastPopup -import com.unciv.ui.popup.popups +import com.unciv.ui.popup.YesNoPopup +import com.unciv.ui.tilegroups.TileGroup import com.unciv.ui.utils.* -class MapEditorScreen(): BaseScreen() { - var mapName = "" - var tileMap = TileMap() - var ruleset = RulesetCache.getVanillaRuleset() // This will return a clone - var gameSetupInfo = GameSetupInfo() - lateinit var mapHolder: EditorMapHolder +//todo normalize properly - lateinit var mapEditorOptionsTable: MapEditorOptionsTable - - private val showHideEditorOptionsButton = ">".toTextButton() +//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 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 - constructor(map: TileMap) : this() { - tileMap = map - checkAndFixMapSize() - ruleset = RulesetCache.getComplexRuleset(map.mapParameters.mods, map.mapParameters.baseRuleset) - initialize() +class MapEditorScreen(map: TileMap? = null): BaseScreen() { + /** The map being edited, with mod list for that map */ + var tileMap: TileMap + /** Flag indicating the map should be saved */ + var isDirty = false + + /** The parameters to use for new maps, and the UI-shown mod list (which can be applied to the active map) */ + val newMapParameters = getDefaultParameters() + + /** RuleSet corresponding to [tileMap]'s mod list */ + var ruleset: Ruleset + + /** Set only by loading a map from file and used only by mods tab */ + var modsTabNeedsRefresh = false + /** Set by loading a map or changing ruleset and used only by the edit tabs */ + var editTabsNeedRefresh = false + /** Set on load, generate or paint natural wonder - used to read nat wonders for the view tab */ + var naturalWondersNeedRefresh = false + /** Copy of same field in [MapEditorOptionsTab] */ + var tileMatchFuzziness = MapEditorOptionsTab.TileMatchFuzziness.CompleteMatch + + // UI + var mapHolder: EditorMapHolder + val tabs: MapEditorMainTabs + var tileClickHandler: ((tile: TileInfo)->Unit)? = null + + private val highlightedTileGroups = mutableListOf() + + init { + if (map == null) { + ruleset = RulesetCache[BaseRuleset.Civ_V_GnK.fullName]!! + tileMap = TileMap(MapSize.Tiny.radius, ruleset, false).apply { + mapParameters.mapSize = MapSizeNew(MapSize.Tiny) + } + } else { + ruleset = map.ruleset ?: + RulesetCache.getComplexRuleset(map.mapParameters.mods, map.mapParameters.baseRuleset) + tileMap = map + } + + mapHolder = newMapHolder() // will set up ImageGetter and translations, and all dirty flags + isDirty = false + + tabs = MapEditorMainTabs(this) + MapEditorToolsDrawer(tabs, stage) + + // 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 + // the key binding here and the used key in their `addPage`s again for the tooltips. + fun selectGeneratePage(index: Int) { tabs.run { selectPage(1); generate.selectPage(index) } } + keyPressDispatcher[KeyCharAndCode.ctrl('n')] = { selectGeneratePage(0) } + keyPressDispatcher[KeyCharAndCode.ctrl('g')] = { selectGeneratePage(1) } + keyPressDispatcher[KeyCharAndCode.BACK] = this::closeEditor + keyPressDispatcher.setCheckpoint() } - private fun initialize() { + companion object { + private fun getDefaultParameters(): MapParameters { + val lastSetup = UncivGame.Current.settings.lastGameSetup + ?: return MapParameters() + return lastSetup.mapParameters.clone().apply { + reseed() + mods.removeAll(RulesetCache.getSortedBaseRulesets()) + } + } + fun saveDefaultParameters(parameters: MapParameters) { + val settings = UncivGame.Current.settings + val lastSetup = settings.lastGameSetup + ?: GameSetupInfo().also { settings.lastGameSetup = it } + lastSetup.mapParameters = parameters.clone() + settings.save() + } + } + + fun getToolsWidth() = stage.width * 0.4f + + private fun newMapHolder(): EditorMapHolder { ImageGetter.setNewRuleset(ruleset) + // setNewRuleset is missing some graphics - those "EmojiIcons"&co already rendered as font characters + // so to get the "Water" vs "Gold" icons when switching between Deciv and Vanilla to render properly, + // we will need to ditch the already rendered font glyphs. Fonts.resetFont is not sufficient, + // the skin seems to clone a separate copy of the Fonts singleton, proving that kotlin 'object' + // are not really guaranteed to exist in one instance only. + setSkin() + tileMap.setTransients(ruleset,false) tileMap.setStartingLocationsTransients() UncivGame.Current.translations.translationActiveMods = ruleset.mods - mapHolder = EditorMapHolder(this, tileMap) - mapHolder.addTiles(stage.width, stage.height) - stage.addActor(mapHolder) - stage.scrollFocus = mapHolder - - mapEditorOptionsTable = MapEditorOptionsTable(this) - stage.addActor(mapEditorOptionsTable) - mapEditorOptionsTable.setPosition(stage.width - mapEditorOptionsTable.width, 0f) - - showHideEditorOptionsButton.labelCell.pad(10f) - showHideEditorOptionsButton.pack() - showHideEditorOptionsButton.onClick { - if (showHideEditorOptionsButton.text.toString() == ">") { - mapEditorOptionsTable.addAction(Actions.moveTo(stage.width, 0f, 0.5f)) - showHideEditorOptionsButton.setText("<") - } else { - mapEditorOptionsTable.addAction(Actions.moveTo(stage.width - mapEditorOptionsTable.width, 0f, 0.5f)) - showHideEditorOptionsButton.setText(">") - } + val result = EditorMapHolder(this, tileMap) { + tileClickHandler?.invoke(it) } - showHideEditorOptionsButton.setPosition(stage.width - showHideEditorOptionsButton.width - 10f, - stage.height - showHideEditorOptionsButton.height - 10f) - stage.addActor(showHideEditorOptionsButton) - val openOptionsMenu = { - if (popups.none { it is MapEditorMenuPopup }) - MapEditorMenuPopup(this).open(force = true) + stage.root.addActorAt(0, result) + stage.scrollFocus = result + + isDirty = true + modsTabNeedsRefresh = true + editTabsNeedRefresh = true + naturalWondersNeedRefresh = true + return result + } + + fun loadMap(map: TileMap, newRuleset: Ruleset? = null) { + mapHolder.remove() + tileMap = map + checkAndFixMapSize() + ruleset = newRuleset ?: + 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) } - val optionsMenuButton = "Menu".toTextButton() - optionsMenuButton.onClick(openOptionsMenu) - keyPressDispatcher[KeyCharAndCode.BACK] = openOptionsMenu - optionsMenuButton.label.setFontSize(Constants.headingFontSize) - optionsMenuButton.labelCell.pad(20f) - optionsMenuButton.pack() - optionsMenuButton.x = 30f - optionsMenuButton.y = 30f - stage.addActor(optionsMenuButton) + } - mapHolder.addCaptureListener(object : InputListener() { - var isDragging = false - var isPainting = false - var touchDownTime = System.currentTimeMillis() - var lastDrawnTiles = HashSet() + fun getMapCloneForSave() = + tileMap.clone().apply { + setTransients(setUnitCivTransients = false) + } - override fun touchDown(event: InputEvent?, x: Float, y: Float, pointer: Int, button: Int): Boolean { - touchDownTime = System.currentTimeMillis() - return true - } + fun applyRuleset(newRuleset: Ruleset, newBaseRuleset: String, mods: LinkedHashSet) { + mapHolder.remove() + tileMap.mapParameters.baseRuleset = newBaseRuleset + tileMap.mapParameters.mods = mods + tileMap.ruleset = newRuleset + ruleset = newRuleset + mapHolder = newMapHolder() + modsTabNeedsRefresh = false + } - 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, mapHolder) - } - } + internal fun closeEditor() { + if (!isDirty) return game.setScreen(MainMenuScreen()) + YesNoPopup("Do you want to leave without saving the recent changes?", action = { + game.setScreen(MainMenuScreen()) + }, screen = this, restoreDefault = { + keyPressDispatcher[KeyCharAndCode.BACK] = this::closeEditor + }).open() + } - if (isPainting) { - - for (tileInfo in lastDrawnTiles) - mapHolder.tileGroups[tileInfo]!!.forEach { it.hideHighlight() } - lastDrawnTiles.clear() - - val stageCoords = mapHolder.actor.stageToLocalCoordinates(Vector2(event!!.stageX, event.stageY)) - val centerTileInfo = mapHolder.getClosestTileTo(stageCoords) - if (centerTileInfo != null) { - val distance = mapEditorOptionsTable.brushSize - 1 - - for (tileInfo in tileMap.getTilesInDistance(centerTileInfo.position, distance)) { - mapEditorOptionsTable.updateTileWhenClicked(tileInfo) - - tileInfo.setTerrainTransients() - mapHolder.tileGroups[tileInfo]!!.forEach { - it.update() - it.showHighlight(Color.WHITE) - } - - lastDrawnTiles.add(tileInfo) - } - } - } - } - - override fun touchUp(event: InputEvent?, x: Float, y: Float, pointer: Int, button: Int) { - // Reset the whole map - if (isPainting) { - mapHolder.updateTileGroups() - mapHolder.setTransients() - } - - isDragging = false - isPainting = false - } - }) + fun hideSelection() { + for (group in highlightedTileGroups) + group.hideHighlight() + highlightedTileGroups.clear() + } + fun highlightTile(tile: TileInfo, color: Color = Color.WHITE) { + for (group in mapHolder.tileGroups[tile] ?: return) { + group.showHighlight(color) + highlightedTileGroups.add(group) + } + } + fun updateTile(tile: TileInfo) { + mapHolder.tileGroups[tile]!!.forEach { + it.update() + } + } + fun updateAndHighlight(tile: TileInfo, color: Color = Color.WHITE) { + updateTile(tile) + highlightTile(tile, color) } private fun checkAndFixMapSize() { val areaFromTiles = tileMap.values.size - tileMap.mapParameters.run { - val areaFromSize = getArea() - if (areaFromSize == areaFromTiles) return - postCrashHandlingRunnable { - val message = ("Invalid map: Area ([$areaFromTiles]) does not match saved dimensions ([" + - displayMapDimensions() + "]).").tr() + - "\n" + "The dimensions have now been fixed for you.".tr() - ToastPopup(message, this@MapEditorScreen, 4000L ) - } - if (shape == MapShape.hexagonal) { - mapSize = MapSizeNew(HexMath.getHexagonalRadiusForArea(areaFromTiles).toInt()) - return - } + val params = tileMap.mapParameters + val areaFromSize = params.getArea() + if (areaFromSize == areaFromTiles) return - // These mimic tileMap.max* without the abs() - val minLatitude = (tileMap.values.map { it.latitude }.minOrNull() ?: 0f).toInt() - val minLongitude = (tileMap.values.map { it.longitude }.minOrNull() ?: 0f).toInt() - val maxLatitude = (tileMap.values.map { it.latitude }.maxOrNull() ?: 0f).toInt() - val maxLongitude = (tileMap.values.map { it.longitude }.maxOrNull() ?: 0f).toInt() - mapSize = MapSizeNew((maxLongitude - minLongitude + 1), (maxLatitude - minLatitude + 1) / 2) + Gdx.app.postRunnable { + val message = ("Invalid map: Area ([$areaFromTiles]) does not match saved dimensions ([" + + params.displayMapDimensions() + "]).").tr() + + "\n" + "The dimensions have now been fixed for you.".tr() + ToastPopup(message, this@MapEditorScreen, 4000L ) } + + if (params.shape == MapShape.hexagonal) { + params.mapSize = MapSizeNew(HexMath.getHexagonalRadiusForArea(areaFromTiles).toInt()) + return + } + + // These mimic tileMap.max* without the abs() + val minLatitude = (tileMap.values.map { it.latitude }.minOrNull() ?: 0f).toInt() + val minLongitude = (tileMap.values.map { it.longitude }.minOrNull() ?: 0f).toInt() + val maxLatitude = (tileMap.values.map { it.latitude }.maxOrNull() ?: 0f).toInt() + val maxLongitude = (tileMap.values.map { it.longitude }.maxOrNull() ?: 0f).toInt() + params.mapSize = MapSizeNew((maxLongitude - minLongitude + 1), (maxLatitude - minLatitude + 1) / 2) } override fun resize(width: Int, height: Int) { if (stage.viewport.screenWidth != width || stage.viewport.screenHeight != height) { - game.setScreen(MapEditorScreen(mapHolder.tileMap)) + game.setScreen(MapEditorScreen(tileMap)) } } } diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorToolsDrawer.kt b/core/src/com/unciv/ui/mapeditor/MapEditorToolsDrawer.kt new file mode 100644 index 0000000000..3a5fcb42c9 --- /dev/null +++ b/core/src/com/unciv/ui/mapeditor/MapEditorToolsDrawer.kt @@ -0,0 +1,90 @@ +package com.unciv.ui.mapeditor + +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.Table +import com.badlogic.gdx.utils.Align +import com.unciv.ui.utils.BaseScreen +import com.unciv.ui.utils.addSeparatorVertical +import kotlin.math.abs + +class MapEditorToolsDrawer( + tabs: MapEditorMainTabs, + initStage: Stage +): Table(BaseScreen.skin) { + companion object { + const val handleWidth = 10f + } + + var splitAmount = 1f + set(value) { + field = value + reposition() + } + + init { + touchable = Touchable.childrenOnly + addSeparatorVertical(Color.CLEAR, handleWidth) // the "handle" + add(tabs) + .height(initStage.height) + .fill().top() + pack() + setPosition(initStage.width, 0f, Align.bottomRight) + initStage.addActor(this) + initStage.addListener(getListener(this)) + } + + private class SplitAmountAction( + private val drawer: MapEditorToolsDrawer, + endAmount: Float + ): FloatAction(drawer.splitAmount, endAmount, 0.333f) { + override fun act(delta: Float): Boolean { + val result = super.act(delta) + drawer.splitAmount = value + return result + } + } + + private fun getListener(drawer: MapEditorToolsDrawer) = object : InputListener() { + private var draggingPointer = -1 + private var oldSplitAmount = -1f + private var lastX = 0f + private var handleX = 0f + + override fun touchDown(event: InputEvent, x: Float, y: Float, pointer: Int, button: Int ): Boolean { + if (draggingPointer != -1) return false + if (pointer == 0 && button != 0) return false + if (x !in drawer.x..(drawer.x + handleWidth)) return false + draggingPointer = pointer + lastX = x + handleX = drawer.x + oldSplitAmount = splitAmount + return true + } + override fun touchUp(event: InputEvent, x: Float, y: Float, pointer: Int, button: Int) { + if (pointer != draggingPointer) return + draggingPointer = -1 + if (oldSplitAmount < 0f) return + addAction(SplitAmountAction(drawer, if (splitAmount > 0.5f) 0f else 1f)) + } + 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 + } + } + + fun reposition() { + if (stage == null) return + val dx = stage.width + (1f - splitAmount) * (width - handleWidth) + setPosition(dx, 0f, Align.bottomRight) + } +} diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorViewTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorViewTab.kt new file mode 100644 index 0000000000..fd3206b0cf --- /dev/null +++ b/core/src/com/unciv/ui/mapeditor/MapEditorViewTab.kt @@ -0,0 +1,232 @@ +package com.unciv.ui.mapeditor + +import com.badlogic.gdx.graphics.Color +import com.badlogic.gdx.scenes.scene2d.ui.Cell +import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.unciv.UncivGame +import com.unciv.logic.GameInfo +import com.unciv.logic.civilization.CivilizationInfo +import com.unciv.logic.map.TileInfo +import com.unciv.logic.map.TileMap +import com.unciv.models.Counter +import com.unciv.models.ruleset.Nation +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.stats.Stats +import com.unciv.models.translations.tr +import com.unciv.ui.civilopedia.CivilopediaScreen +import com.unciv.ui.civilopedia.FormattedLine +import com.unciv.ui.civilopedia.MarkupRenderer +import com.unciv.ui.civilopedia.FormattedLine.IconDisplay +import com.unciv.ui.popup.ToastPopup +import com.unciv.ui.utils.* + +class MapEditorViewTab( + private val editorScreen: MapEditorScreen +): Table(BaseScreen.skin), TabbedPager.IPageExtensions { + private var tileDataCell: Cell? = null + private val mockCiv = createMockCiv(editorScreen.ruleset) + private val naturalWonders = Counter() + /** Click-locating items with several instances: round robin, for simplicity only a global one */ + private var roundRobinIndex = 0 + private val collator = UncivGame.Current.settings.getCollatorFromLocale() + private val labelWidth = editorScreen.getToolsWidth() - 40f + + init { + top() + defaults().pad(5f, 20f) + update() + } + + private fun createMockCiv(ruleset: Ruleset) = CivilizationInfo().apply { + // This crappy construct exists only to allow us to call TileInfo.getTileStats + nation = Nation() + nation.name = "Test" + gameInfo = GameInfo() + gameInfo.ruleSet = ruleset + // show yields of strategic resources too + tech.techsResearched.addAll(ruleset.technologies.keys) + } + + private fun CivilizationInfo.updateMockCiv(ruleset: Ruleset) { + if (gameInfo.ruleSet === ruleset) return + gameInfo.ruleSet = ruleset + tech.techsResearched.addAll(ruleset.technologies.keys) + } + + private fun update() { + clear() + mockCiv.updateMockCiv(editorScreen.ruleset) + + val tileMap = editorScreen.tileMap + + val headerText = tileMap.mapParameters.name.ifEmpty { "New map" } + add(ExpanderTab( + headerText, + startsOutOpened = false + ) { + val mapParameterText = tileMap.mapParameters.toString() + .replace("\"${tileMap.mapParameters.name}\" ", "") + val mapParameterLabel = WrappableLabel(mapParameterText, labelWidth) + it.add(mapParameterLabel.apply { wrap = true }).row() + }).row() + + try { + tileMap.assignContinents(TileMap.AssignContinentsMode.Ensure) + } catch (ex: Exception) { + ToastPopup("Error assigning continents: ${ex.message}", editorScreen) + } + + val statsText = "Area: [${tileMap.values.size}] tiles, [${tileMap.continentSizes.size}] continents/islands" + val statsLabel = WrappableLabel(statsText, labelWidth) + add(statsLabel.apply { wrap = true }).row() + + // Map editor must not touch tileMap.naturalWonders as it is a by lazy immutable list, + // and we wouldn't be able to fix it when the natural wonders change + if (editorScreen.naturalWondersNeedRefresh) { + naturalWonders.clear() + tileMap.values.asSequence() + .mapNotNull { it.naturalWonder } + .sortedWith(compareBy(collator) { it.tr() }) + .forEach { + naturalWonders.add(it, 1) + } + editorScreen.naturalWondersNeedRefresh = false + } + if (naturalWonders.isNotEmpty()) { + val lines = naturalWonders.map { + FormattedLine(if (it.value == 1) it.key else "{${it.key}} (${it.value})", it.key, "Terrain/${it.key}") + } + add(ExpanderTab( + "{Natural Wonders} (${naturalWonders.size})", + fontSize = 21, + startsOutOpened = false, + headerPad = 5f + ) { + it.add(MarkupRenderer.render(lines, iconDisplay = IconDisplay.NoLink) { name-> + scrollToWonder(name) + }) + }).row() + } + + // Starting locations not cached like natural wonders - storage is already compact + if (tileMap.startingLocationsByNation.isNotEmpty()) { + val lines = tileMap.getStartingLocationSummary() + .map { FormattedLine(if (it.second == 1) it.first else "{${it.first}} (${it.second})", it.first, "Nation/${it.first}") } + add(ExpanderTab( + "{Starting locations} (${tileMap.startingLocationsByNation.size})", + fontSize = 21, + startsOutOpened = false, + headerPad = 5f + ) { + it.add(MarkupRenderer.render(lines.toList(), iconDisplay = IconDisplay.NoLink) { name -> + scrollToStartOfNation(name) + }) + }).row() + } + + addSeparator() + + tileDataCell = add(Table()).fillX() + row() + + addSeparator() + add("Exit map editor".toTextButton().apply { onClick(editorScreen::closeEditor) }).row() + + invalidateHierarchy() //todo - unsure this helps + validate() + } + + override fun activated(index: Int, caption: String, pager: TabbedPager) { + editorScreen.tileClickHandler = this::tileClickHandler + update() + } + + override fun deactivated(index: Int, caption: String, pager: TabbedPager) { + editorScreen.hideSelection() + tileDataCell?.setActor(null) + editorScreen.tileClickHandler = null + } + + fun tileClickHandler(tile: TileInfo) { + if (tileDataCell == null) return + + val lines = ArrayList() + + lines += FormattedLine("Position: [${tile.position.toString().replace(".0","")}]") + lines += FormattedLine() + + lines.addAll(tile.toMarkup(null)) + + val stats = try { + tile.getTileStats(null, mockCiv) + } catch (ex: Exception) { + // Maps aren't always fixed to remove dead references... like resource "Gold" + if (ex.message != null) + ToastPopup(ex.message!!, editorScreen) + Stats() + } + if (!stats.isEmpty()) { + lines += FormattedLine() + lines += FormattedLine(stats.toString()) + } + + val nations = tile.tileMap.getTileStartingLocations(tile) + .joinToString { it.name.tr() } + if (nations.isNotEmpty()) { + lines += FormattedLine() + lines += FormattedLine("Starting location(s): [$nations]") + } + + val continent = tile.getContinent() + if (continent >= 0) { + lines += FormattedLine() + lines += FormattedLine("Continent: [$continent] ([${tile.tileMap.continentSizes[continent]}] tiles)", link = "continent") + } + + tileDataCell?.setActor(MarkupRenderer.render(lines, labelWidth) { + if (it == "continent") { + // Visualize the continent this tile is on + editorScreen.hideSelection() + val color = Color.BROWN.darken(0.5f) + for (markTile in tile.tileMap.values) { + if (markTile.getContinent() == continent) + editorScreen.highlightTile(markTile, color) + } + } else { + // This needs CivilopediaScreen to be able to work without a GameInfo! + UncivGame.Current.setScreen(CivilopediaScreen(tile.ruleset, editorScreen, link = it)) + } + }) + + editorScreen.hideSelection() + editorScreen.highlightTile(tile, Color.CORAL) + } + + private fun scrollToWonder(name: String) { + scrollToNextTileOf(editorScreen.tileMap.values.filter { it.naturalWonder == name }) + } + private fun scrollToStartOfNation(name: String) { + val tiles = editorScreen.tileMap.startingLocationsByNation[name] + ?: return + scrollToNextTileOf(tiles.toList()) + } + private fun scrollToNextTileOf(tiles: List) { + if (tiles.isEmpty()) return + if (roundRobinIndex >= tiles.size) roundRobinIndex = 0 + val tile = tiles[roundRobinIndex++] + editorScreen.mapHolder.setCenterPosition(tile.position) + tileClickHandler(tile) + } + + private fun TileMap.getTileStartingLocations(tile: TileInfo?) = + startingLocationsByNation.asSequence() + .filter { tile == null || tile in it.value } + .mapNotNull { ruleset!!.nations[it.key] } + .sortedWith(compareBy{ it.isCityState() }.thenBy(collator) { it.name.tr() }) + + private fun TileMap.getStartingLocationSummary() = + startingLocationsByNation.asSequence() + .mapNotNull { if (it.key in ruleset!!.nations) ruleset!!.nations[it.key]!! to it.value.size else null } + .sortedWith(compareBy>{ it.first.isCityState() }.thenBy(collator) { it.first.name.tr() }) + .map { it.first.name to it.second } +} diff --git a/core/src/com/unciv/ui/mapeditor/MapGeneratorSteps.kt b/core/src/com/unciv/ui/mapeditor/MapGeneratorSteps.kt new file mode 100644 index 0000000000..24d8a81a41 --- /dev/null +++ b/core/src/com/unciv/ui/mapeditor/MapGeneratorSteps.kt @@ -0,0 +1,49 @@ +package com.unciv.ui.mapeditor + +import com.unciv.logic.map.MapParameters + +private object MapGeneratorStepsHelpers { + val applyLandmass = fun(newParameters: MapParameters, actualParameters: MapParameters) { + actualParameters.type = newParameters.type + actualParameters.waterThreshold = newParameters.waterThreshold + actualParameters.seed = newParameters.seed + } + val applyElevation = fun(newParameters: MapParameters, actualParameters: MapParameters) { + actualParameters.elevationExponent = newParameters.elevationExponent + } + val applyHumidityAndTemperature = fun(newParameters: MapParameters, actualParameters: MapParameters) { + actualParameters.temperatureExtremeness = newParameters.temperatureExtremeness + } + val applyLakesAndCoast = fun(newParameters: MapParameters, actualParameters: MapParameters) { + actualParameters.maxCoastExtension = newParameters.maxCoastExtension + } + val applyVegetation = fun(newParameters: MapParameters, actualParameters: MapParameters) { + actualParameters.vegetationRichness = newParameters.vegetationRichness + } + val applyRareFeatures = fun(newParameters: MapParameters, actualParameters: MapParameters) { + actualParameters.rareFeaturesRichness = newParameters.rareFeaturesRichness + } + val applyResources = fun(newParameters: MapParameters, actualParameters: MapParameters) { + actualParameters.resourceRichness = newParameters.resourceRichness + } +} + +enum class MapGeneratorSteps( + val label: String, + val copyParameters: ((MapParameters, MapParameters)->Unit)? = null +) { + None(""), + All("All"), // Special case - applying params done elsewhere + Landmass("Generate landmass", MapGeneratorStepsHelpers.applyLandmass), + Elevation("Raise mountains and hills", MapGeneratorStepsHelpers.applyElevation), + HumidityAndTemperature("Humidity and temperature", MapGeneratorStepsHelpers.applyHumidityAndTemperature), + LakesAndCoast("Lakes and coastline", MapGeneratorStepsHelpers.applyLakesAndCoast), + Vegetation("Sprout vegetation", MapGeneratorStepsHelpers.applyVegetation), + RareFeatures("Spawn rare features", MapGeneratorStepsHelpers.applyRareFeatures), + Ice("Distribute ice"), + Continents("Assign continent IDs"), + NaturalWonders("Natural Wonders"), + Rivers("Let the rivers flow"), + Resources("Spread Resources", MapGeneratorStepsHelpers.applyResources), + AncientRuins("Create ancient ruins"), +} diff --git a/core/src/com/unciv/ui/mapeditor/NewMapScreen.kt b/core/src/com/unciv/ui/mapeditor/NewMapScreen.kt deleted file mode 100644 index ce5783e2ae..0000000000 --- a/core/src/com/unciv/ui/mapeditor/NewMapScreen.kt +++ /dev/null @@ -1,214 +0,0 @@ -package com.unciv.ui.mapeditor - -import com.badlogic.gdx.Gdx -import com.badlogic.gdx.scenes.scene2d.ui.Table -import com.unciv.Constants -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 -import com.unciv.models.metadata.GameSetupInfo -import com.unciv.models.ruleset.RulesetCache -import com.unciv.models.translations.tr -import com.unciv.ui.crashhandling.crashHandlingThread -import com.unciv.ui.crashhandling.postCrashHandlingRunnable -import com.unciv.ui.images.ImageGetter -import com.unciv.ui.newgamescreen.MapParametersTable -import com.unciv.ui.newgamescreen.ModCheckboxTable -import com.unciv.ui.newgamescreen.TranslatedSelectBox -import com.unciv.ui.pickerscreens.PickerScreen -import com.unciv.ui.popup.Popup -import com.unciv.ui.popup.ToastPopup -import com.unciv.ui.utils.* -import kotlin.math.max -import com.unciv.ui.utils.AutoScrollPane as ScrollPane - -/** New map generation screen */ -class NewMapScreen(val mapParameters: MapParameters = getDefaultParameters()) : PickerScreen() { - - private val ruleset = RulesetCache.getVanillaRuleset() - private var generatedMap: TileMap? = null - private val mapParametersTable: MapParametersTable - private val modCheckBoxes: ModCheckboxTable - - companion object { - private fun getDefaultParameters(): MapParameters { - val lastSetup = UncivGame.Current.settings.lastGameSetup - ?: return MapParameters() - return lastSetup.mapParameters.clone().apply { reseed() } - } - private fun saveDefaultParameters(parameters: MapParameters) { - val settings = UncivGame.Current.settings - val lastSetup = settings.lastGameSetup - ?: GameSetupInfo().also { settings.lastGameSetup = it } - lastSetup.mapParameters = parameters.clone() - settings.save() - } - } - - init { - setDefaultCloseAction(MainMenuScreen()) - - // To load in the mods selected last time this screen was exited - reloadRuleset() - - mapParametersTable = MapParametersTable(mapParameters, isEmptyMapAllowed = true) - val newMapScreenOptionsTable = Table(skin).apply { - pad(10f) - add("Map Options".toLabel(fontSize = Constants.headingFontSize)).row() - - // Add the selector for the base ruleset - val baseRulesetBox = getBaseRulesetSelectBox() - if (baseRulesetBox != null) { - // TODO: For some reason I'm unable to get these two tables to be equally wide - // someone who knows what they're doing should fix this - val maxWidth = max(baseRulesetBox.minWidth, mapParametersTable.minWidth) - baseRulesetBox.width = maxWidth - mapParametersTable.width = maxWidth - add(getBaseRulesetSelectBox()).row() - } - - add(mapParametersTable).row() - - modCheckBoxes = ModCheckboxTable(mapParameters.mods, mapParameters.baseRuleset, this@NewMapScreen) { - reloadRuleset() - } - add(modCheckBoxes) - pack() - } - - - topTable.apply { - add(ScrollPane(newMapScreenOptionsTable).apply { setOverscroll(false, false) }) - pack() - } - - rightButtonSetEnabled(true) - rightSideButton.onClick { - val message = mapParameters.mapSize.fixUndesiredSizes(mapParameters.worldWrap) - if (message != null) { - postCrashHandlingRunnable { - ToastPopup( message, UncivGame.Current.screen as BaseScreen, 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) - - crashHandlingThread(name = "MapGenerator") { - try { - // Map generation can take a while and we don't want ANRs - generatedMap = MapGenerator(ruleset).generateMap(mapParameters) - - postCrashHandlingRunnable { - saveDefaultParameters(mapParameters) - val mapEditorScreen = MapEditorScreen(generatedMap!!) - mapEditorScreen.ruleset = ruleset - game.setScreen(mapEditorScreen) - } - - } catch (exception: Exception) { - println("Map generator exception: ${exception.message}") - postCrashHandlingRunnable { - rightButtonSetEnabled(true) - val cantMakeThatMapPopup = Popup(this) - cantMakeThatMapPopup.addGoodSizedLabel("It looks like we can't make a map with the parameters you requested!".tr()) - .row() - cantMakeThatMapPopup.addCloseButton() - cantMakeThatMapPopup.open() - Gdx.input.inputProcessor = stage - } - } - } - - } - } - - /** Changes the state and the text of the [rightSideButton] */ - private fun rightButtonSetEnabled(enabled: Boolean) { - if (enabled) { - rightSideButton.enable() - rightSideButton.setText("Create".tr()) - } else { - rightSideButton.disable() - rightSideButton.setText("Working...".tr()) - } - } - - private fun getBaseRulesetSelectBox(): Table? { - val rulesetSelectionBox = Table() - - val sortedBaseRulesets = RulesetCache.getSortedBaseRulesets() - if (sortedBaseRulesets.size < 2) return null - - rulesetSelectionBox.add("{Base Ruleset}:".toLabel()).left() - val selectBox = TranslatedSelectBox(sortedBaseRulesets, mapParameters.baseRuleset, skin) - - val onChange = onChange@{ newBaseRuleset: String -> - val previousSelection = mapParameters.baseRuleset - if (newBaseRuleset == previousSelection) return@onChange null - - // Check if this mod is well-defined - val baseRulesetErrors = RulesetCache[newBaseRuleset]!!.checkModLinks() - if (baseRulesetErrors.isError()) { - val toastMessage = "The mod you selected is incorrectly defined!".tr() + "\n\n${baseRulesetErrors.getErrorText()}" - ToastPopup(toastMessage, this@NewMapScreen, 5000L) - return@onChange previousSelection - } - - // If so, add it to the current ruleset - mapParameters.baseRuleset = newBaseRuleset - reloadRuleset() - - // Check if the ruleset in it's entirety is still well-defined - val modLinkErrors = ruleset.checkModLinks() - if (modLinkErrors.isError()) { - mapParameters.mods.clear() - reloadRuleset() - val toastMessage = - "This base ruleset is not compatible with the previously selected\nextension mods. They have been disabled.".tr() - ToastPopup(toastMessage, this@NewMapScreen, 5000L) - - modCheckBoxes.disableAllCheckboxes() - } else if (modLinkErrors.isWarnUser()) { - val toastMessage = - "{The mod combination you selected has problems.}\n{You can play it, but don't expect everything to work!}".tr() + - "\n\n${modLinkErrors.getErrorText()}" - ToastPopup(toastMessage, this@NewMapScreen, 5000L) - } - - - modCheckBoxes.setBaseRuleset(newBaseRuleset) - - null - } - - - selectBox.onChange { - val changedValue = onChange(selectBox.selected.value) - if (changedValue != null) selectBox.setSelected(changedValue) - } - - onChange(mapParameters.baseRuleset) - - rulesetSelectionBox.add(selectBox).fillX().row() - return rulesetSelectionBox - } - - private fun reloadRuleset() { - ruleset.clear() - val newRuleset = RulesetCache.getComplexRuleset(mapParameters.mods, mapParameters.baseRuleset) - ruleset.add(newRuleset) - ruleset.mods += mapParameters.baseRuleset - ruleset.mods += mapParameters.mods - ruleset.modOptions = newRuleset.modOptions - - ImageGetter.setNewRuleset(ruleset) - } -} diff --git a/core/src/com/unciv/ui/mapeditor/SaveAndLoadMapScreen.kt b/core/src/com/unciv/ui/mapeditor/SaveAndLoadMapScreen.kt deleted file mode 100644 index 0f1d0f021e..0000000000 --- a/core/src/com/unciv/ui/mapeditor/SaveAndLoadMapScreen.kt +++ /dev/null @@ -1,215 +0,0 @@ -package com.unciv.ui.mapeditor - -import com.badlogic.gdx.Gdx -import com.badlogic.gdx.files.FileHandle -import com.badlogic.gdx.graphics.Color -import com.badlogic.gdx.scenes.scene2d.ui.Table -import com.badlogic.gdx.scenes.scene2d.ui.TextButton -import com.badlogic.gdx.scenes.scene2d.ui.TextField -import com.unciv.UncivGame -import com.unciv.logic.MapSaver -import com.unciv.logic.UncivShowableException -import com.unciv.logic.map.MapType -import com.unciv.logic.map.TileMap -import com.unciv.models.ruleset.RulesetCache -import com.unciv.models.translations.tr -import com.unciv.ui.crashhandling.crashHandlingThread -import com.unciv.ui.crashhandling.postCrashHandlingRunnable -import com.unciv.ui.pickerscreens.PickerScreen -import com.unciv.ui.popup.Popup -import com.unciv.ui.popup.ToastPopup -import com.unciv.ui.popup.YesNoPopup -import com.unciv.ui.utils.* -import com.unciv.ui.utils.AutoScrollPane as ScrollPane - -class SaveAndLoadMapScreen(mapToSave: TileMap?, save:Boolean = false, previousScreen: BaseScreen) - : PickerScreen(disableScroll = true) { - private var chosenMap: FileHandle? = null - val deleteButton = "Delete map".toTextButton() - val mapsTable = Table().apply { defaults().pad(10f) } - private val mapNameTextField = TextField("", skin).apply { maxLength = 100 } - - init { - val rightSideButtonAction: ()->Unit - if (save) { - rightSideButton.enable() - rightSideButton.setText("Save map".tr()) - rightSideButtonAction = { - mapToSave!!.mapParameters.name = mapNameTextField.text - mapToSave.mapParameters.type = MapType.custom - crashHandlingThread(name = "SaveMap") { - try { - MapSaver.saveMap(mapNameTextField.text, getMapCloneForSave(mapToSave)) - postCrashHandlingRunnable { - Gdx.input.inputProcessor = null // This is to stop ANRs happening here, until the map editor screen sets up. - game.setScreen(MapEditorScreen(mapToSave)) - dispose() - } - } catch (ex: Exception) { - ex.printStackTrace() - postCrashHandlingRunnable { - val cantLoadGamePopup = Popup(this) - cantLoadGamePopup.addGoodSizedLabel("It looks like your map can't be saved!").row() - cantLoadGamePopup.addCloseButton() - cantLoadGamePopup.open(force = true) - } - } - } - } - } else { - rightSideButton.setText("Load map".tr()) - rightSideButtonAction = { - crashHandlingThread(name = "MapLoader") { - var popup: Popup? = null - var needPopup = true // loadMap can fail faster than postRunnable runs - postCrashHandlingRunnable { - if (!needPopup) return@postCrashHandlingRunnable - popup = Popup(this).apply { - addGoodSizedLabel("Loading...") - open() - } - } - try { - val map = MapSaver.loadMap(chosenMap!!, checkSizeErrors = false) - - val missingMods = map.mapParameters.mods.filter { it !in RulesetCache }.toMutableList() - // [TEMPORARY] conversion of old maps with a base ruleset contained in the mods - val newBaseRuleset = map.mapParameters.mods.filter { it !in missingMods }.firstOrNull { RulesetCache[it]!!.modOptions.isBaseRuleset } - if (newBaseRuleset != null) map.mapParameters.baseRuleset = newBaseRuleset - // - - if (map.mapParameters.baseRuleset !in RulesetCache) missingMods += map.mapParameters.baseRuleset - - if (missingMods.isNotEmpty()) { - postCrashHandlingRunnable { - needPopup = false - popup?.close() - ToastPopup("Missing mods: [${missingMods.joinToString()}]", this) - } - } else postCrashHandlingRunnable { - Gdx.input.inputProcessor = null // This is to stop ANRs happening here, until the map editor screen sets up. - try { - // For deprecated maps, set the base ruleset field if it's still saved in the mods field - val modBaseRuleset = map.mapParameters.mods.firstOrNull { RulesetCache[it]!!.modOptions.isBaseRuleset } - if (modBaseRuleset != null) { - map.mapParameters.baseRuleset = modBaseRuleset - map.mapParameters.mods -= modBaseRuleset - } - - game.setScreen(MapEditorScreen(map)) - dispose() - } catch (ex: Throwable) { - ex.printStackTrace() - needPopup = false - popup?.close() - println("Error displaying map \"$chosenMap\": ${ex.localizedMessage}") - Gdx.input.inputProcessor = stage - ToastPopup("Error loading map!", this) - } - } - } catch (ex: Throwable) { - needPopup = false - ex.printStackTrace() - postCrashHandlingRunnable { - popup?.close() - println("Error loading map \"$chosenMap\": ${ex.localizedMessage}") - ToastPopup("Error loading map!".tr() + - (if (ex is UncivShowableException) "\n" + ex.message else ""), this) - } - } - } - } - } - rightSideButton.onClick(rightSideButtonAction) - keyPressDispatcher[KeyCharAndCode.RETURN] = rightSideButtonAction - - topTable.add(ScrollPane(mapsTable)).maxWidth(stage.width / 2) - - val rightSideTable = Table().apply { defaults().pad(10f) } - - if (save) { - mapNameTextField.textFieldFilter = TextField.TextFieldFilter { _, char -> char != '\\' && char != '/' } - mapNameTextField.text = if (mapToSave == null || mapToSave.mapParameters.name.isEmpty()) "My new map" - else mapToSave.mapParameters.name - rightSideTable.add(mapNameTextField).width(300f).pad(10f) - stage.keyboardFocus = mapNameTextField - mapNameTextField.selectAll() - } - - rightSideTable.addSeparator() - - if (save) { - val copyMapAsTextButton = "Copy to clipboard".toTextButton() - val copyMapAsTextAction = { - Gdx.app.clipboard.contents = MapSaver.mapToSavedString(getMapCloneForSave(mapToSave!!)) - } - copyMapAsTextButton.onClick (copyMapAsTextAction) - keyPressDispatcher[KeyCharAndCode.ctrl('C')] = copyMapAsTextAction - rightSideTable.add(copyMapAsTextButton).row() - } else { - val loadFromClipboardButton = "Load copied data".toTextButton() - val couldNotLoadMapLabel = "Could not load map!".toLabel(Color.RED).apply { isVisible = false } - val loadFromClipboardAction = { - try { - val clipboardContentsString = Gdx.app.clipboard.contents.trim() - val loadedMap = MapSaver.mapFromSavedString(clipboardContentsString, checkSizeErrors = false) - game.setScreen(MapEditorScreen(loadedMap)) - } catch (ex: Exception) { - ex.printStackTrace() - couldNotLoadMapLabel.isVisible = true - } - } - loadFromClipboardButton.onClick(loadFromClipboardAction) - keyPressDispatcher[KeyCharAndCode.ctrl('V')] = loadFromClipboardAction - rightSideTable.add(loadFromClipboardButton).row() - rightSideTable.add(couldNotLoadMapLabel).row() - } - - val deleteAction = { - YesNoPopup("Are you sure you want to delete this map?", { - chosenMap!!.delete() - game.setScreen(SaveAndLoadMapScreen(mapToSave, save, previousScreen)) - }, this).open() - } - deleteButton.onClick(deleteAction) - keyPressDispatcher[KeyCharAndCode.DEL] = deleteAction - rightSideTable.add(deleteButton).row() - - topTable.add(rightSideTable) - setDefaultCloseAction(previousScreen) - - update() - } - - fun update() { - chosenMap = null - deleteButton.disable() - deleteButton.color = Color.RED - - deleteButton.setText("Delete map".tr()) - - mapsTable.clear() - val collator = UncivGame.Current.settings.getCollatorFromLocale() - for (map in MapSaver.getMaps().sortedWith(compareBy(collator) { it.name() })) { - val existingMapButton = TextButton(map.name(), skin) - existingMapButton.onClick { - for (cell in mapsTable.cells) cell.actor.color = Color.WHITE - existingMapButton.color = Color.BLUE - - rightSideButton.enable() - chosenMap = map - mapNameTextField.text = map.name() - mapNameTextField.setSelection(Int.MAX_VALUE,Int.MAX_VALUE) // sets caret to end of text - - deleteButton.enable() - deleteButton.color = Color.RED - } - mapsTable.add(existingMapButton).row() - } - } - - private fun getMapCloneForSave(mapToSave: TileMap) = - mapToSave.clone().apply { - setTransients(setUnitCivTransients = false) - } -} diff --git a/core/src/com/unciv/ui/newgamescreen/MapOptionsTable.kt b/core/src/com/unciv/ui/newgamescreen/MapOptionsTable.kt index dfc8413f61..52fd6ae5fb 100644 --- a/core/src/com/unciv/ui/newgamescreen/MapOptionsTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/MapOptionsTable.kt @@ -107,8 +107,9 @@ class MapOptionsTable(private val newGameScreen: NewGameScreen): Table() { } mapParameters.name = mapFile.name() newGameScreen.gameSetupInfo.mapFile = mapFile - newGameScreen.gameSetupInfo.gameParameters.mods = LinkedHashSet(map.mapParameters.mods.filter { RulesetCache[it]?.modOptions?.isBaseRuleset != true }) - newGameScreen.gameSetupInfo.gameParameters.baseRuleset = map.mapParameters.mods.firstOrNull { RulesetCache[it]?.modOptions?.isBaseRuleset == true } ?: map.mapParameters.baseRuleset + val mapMods = map.mapParameters.mods.partition { RulesetCache[it]?.modOptions?.isBaseRuleset == true } + newGameScreen.gameSetupInfo.gameParameters.mods = LinkedHashSet(mapMods.second) + newGameScreen.gameSetupInfo.gameParameters.baseRuleset = mapMods.first.firstOrNull() ?: map.mapParameters.baseRuleset newGameScreen.updateRuleset() newGameScreen.updateTables() } diff --git a/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt b/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt index 9cb72a1918..4ac56048d5 100644 --- a/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt @@ -34,6 +34,7 @@ class MapParametersTable( private lateinit var noRuinsCheckbox: CheckBox private lateinit var noNaturalWondersCheckbox: CheckBox private lateinit var worldWrapCheckbox: CheckBox + private lateinit var seedTextField: TextField // Keep references (in the key) and settings value getters (in the value) of the 'advanced' sliders // in a HashMap for reuse later - in the reset to defaults button. Better here as field than as closure. @@ -52,6 +53,11 @@ class MapParametersTable( addAdvancedSettings() } + fun reseed() { + mapParameters.reseed() + seedTextField.text = mapParameters.seed.toString() + } + private fun addMapShapeSelectBox() { val mapShapes = listOfNotNull( MapShape.hexagonal, @@ -225,7 +231,7 @@ class MapParametersTable( private fun addAdvancedControls(table: Table) { table.defaults().pad(5f) - val seedTextField = TextField(mapParameters.seed.toString(), skin) + seedTextField = TextField(mapParameters.seed.toString(), skin) seedTextField.textFieldFilter = DigitsOnlyFilter() // If the field is empty, fallback seed value to 0 diff --git a/core/src/com/unciv/ui/newgamescreen/ModCheckboxTable.kt b/core/src/com/unciv/ui/newgamescreen/ModCheckboxTable.kt index 2c58ca8d3b..5b0ced89bd 100644 --- a/core/src/com/unciv/ui/newgamescreen/ModCheckboxTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/ModCheckboxTable.kt @@ -9,6 +9,16 @@ import com.unciv.models.translations.tr import com.unciv.ui.popup.ToastPopup import com.unciv.ui.utils.* +/** + * A widget containing one expander for extension mods. + * Manages compatibility checks, warns or prevents incompatibilities. + * + * @param mods In/out set of active mods, modified in place + * @param baseRuleset The selected base Ruleset //todo clarify + * @param screen Parent screen, used only to show [ToastPopup]s + * @param isPortrait Used only for minor layout tweaks, arrangement is always vertical + * @param onUpdate Callback, parameter is the mod name, called after any checks that may prevent mod selection succeed. + */ class ModCheckboxTable( private val mods: LinkedHashSet, private var baseRuleset: String, @@ -26,7 +36,6 @@ class ModCheckboxTable( val checkBox = mod.name.toCheckBox(mod.name in mods) checkBox.onChange { if (checkBoxChanged(checkBox, it!!, mod)) { - //todo: persist ExpanderTab states here onUpdate(mod.name) } } diff --git a/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt b/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt index 184922f03a..448936fc60 100644 --- a/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt +++ b/core/src/com/unciv/ui/utils/KeyPressDispatcher.kt @@ -105,7 +105,7 @@ data class KeyCharAndCode(val char: Char, val code: Int) { * keyPressDispatcher['+'] = { zoomIn() } * ``` * Optionally use [setCheckpoint] and [revertToCheckPoint] to remember and restore one state. - * + * * @param name Optional name of the container screen or popup for debugging */ class KeyPressDispatcher(val name: String? = null) : HashMap Unit)>() { @@ -218,6 +218,8 @@ class KeyPressDispatcher(val name: String? = null) : HashMap @@ -167,11 +167,11 @@ class TabbedPager( group.packIfNeeded() return measure(group.minHeight, group.prefHeight, group.maxHeight) } - fun combine(header: Float, top: DimensionMeasurement, bottom: DimensionMeasurement) { - min = (header + top.min + bottom.min).coerceAtLeast(min).coerceAtMost(limit) - pref = (header + top.pref + bottom.pref).coerceAtLeast(pref).coerceIn(min..limit) + fun combine(top: DimensionMeasurement, bottom: DimensionMeasurement) { + min = (top.min + bottom.min).coerceAtLeast(min).coerceAtMost(limit) + pref = (top.pref + bottom.pref).coerceAtLeast(pref).coerceIn(min..limit) if (growMax) - max = (header + top.max + bottom.max).coerceAtLeast(max).coerceIn(pref..limit) + max = (top.max + bottom.max).coerceAtLeast(max).coerceIn(pref..limit) } } @@ -298,20 +298,23 @@ class TabbedPager( // The following are part of the Widget interface and serve dynamic sizing override fun getPrefWidth() = dimW.pref fun setPrefWidth(width: Float) { + if (dimW.growMax && width > dimW.max) dimW.max = width if (width !in dimW.min..dimW.max) throw IllegalArgumentException() dimW.pref = width invalidateHierarchy() } - override fun getPrefHeight() = dimH.pref + override fun getPrefHeight() = dimH.pref + headerHeight fun setPrefHeight(height: Float) { - if (height !in dimH.min..dimH.max) throw IllegalArgumentException() - dimH.pref = height + val contentHeight = (height - headerHeight).coerceIn(0f..dimH.limit) + if (dimH.growMax && contentHeight > dimH.max) dimH.max = contentHeight + if (contentHeight !in dimH.min..dimH.max) throw IllegalArgumentException() + dimH.pref = contentHeight invalidateHierarchy() } override fun getMinWidth() = dimW.min override fun getMaxWidth() = dimW.max - override fun getMinHeight() = dimH.min - override fun getMaxHeight() = dimH.max + override fun getMinHeight() = dimH.min + headerHeight + override fun getMaxHeight() = dimH.max + headerHeight //endregion //region API @@ -602,7 +605,7 @@ class TabbedPager( page.fixedHeight = dimFixedH.min dimW.measureWidth(page.content as? WidgetGroup) dimContentH.measureHeight(page.content as? WidgetGroup) - dimH.combine(headerHeight, dimFixedH, dimContentH) + dimH.combine(dimFixedH, dimContentH) } private fun addAndShowPage(page: PageState, insertBefore: Int): Int {