From 049df797f3e7d8ca061f881378b89c7c3788e581 Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Sun, 15 May 2022 09:56:18 +0200 Subject: [PATCH] Minor Map Editor improvements (#6795) * Map Editor - fix changing resources bugged by tileResource lazy * Map Editor - apply resource deposit abundance from map's parameters * Map Editor - resource abundance - readability --- core/src/com/unciv/logic/map/TileInfo.kt | 77 ++++++++++++------- .../logic/map/mapgenerator/MapGenerator.kt | 4 +- .../ui/mapeditor/MapEditorEditSubTabs.kt | 11 ++- .../unciv/ui/mapeditor/MapEditorEditTab.kt | 2 +- .../ui/mapeditor/MapEditorGenerateTab.kt | 10 ++- .../com/unciv/ui/mapeditor/MapEditorScreen.kt | 13 ++-- .../ui/newgamescreen/MapParametersTable.kt | 25 +++--- 7 files changed, 92 insertions(+), 50 deletions(-) diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index fde288abca..e0d107e797 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -20,6 +20,7 @@ import com.unciv.ui.utils.Fonts import com.unciv.ui.utils.toPercent import kotlin.math.abs import kotlin.math.min +import kotlin.random.Random open class TileInfo { @Transient @@ -69,6 +70,10 @@ open class TileInfo { var naturalWonder: String? = null var resource: String? = null + set(value) { + tileResourceCache = null + field = value + } var resourceAmount: Int = 0 var improvement: String? = null var improvementInProgress: String? = null @@ -162,12 +167,17 @@ open class TileInfo { else -> getBaseTerrain() } - @delegate:Transient - val tileResource: TileResource by lazy { - if (resource == null) throw Exception("No resource exists for this tile!") - else if (!ruleset.tileResources.containsKey(resource!!)) throw Exception("Resource $resource does not exist in this ruleset!") - else ruleset.tileResources[resource!!]!! - } + @Transient + private var tileResourceCache: TileResource? = null + val tileResource: TileResource + get() { + if (tileResourceCache == null) { + if (resource == null) throw Exception("No resource exists for this tile!") + if (!ruleset.tileResources.containsKey(resource!!)) throw Exception("Resource $resource does not exist in this ruleset!") + tileResourceCache = ruleset.tileResources[resource!!]!! + } + return tileResourceCache!! + } private fun getNaturalWonder(): Terrain = if (naturalWonder == null) throw Exception("No natural wonder exists for this tile!") @@ -243,7 +253,7 @@ open class TileInfo { fun isRoughTerrain() = getAllTerrains().any{ it.isRough() } - /** Checks whether any of the TERRAINS of this tile has a certain unqiue */ + /** Checks whether any of the TERRAINS of this tile has a certain unique */ fun terrainHasUnique(uniqueType: UniqueType) = getAllTerrains().any { it.hasUnique(uniqueType) } /** Get all uniques of this type that any TERRAIN on this tile has */ fun getTerrainMatchingUniques(uniqueType: UniqueType, stateForConditionals: StateForConditionals = StateForConditionals(tile=this) ): Sequence { @@ -530,15 +540,15 @@ open class TileInfo { RoadStatus.values().any { it.name == improvement.name } -> !isWater && RoadStatus.valueOf(improvement.name) > roadStatus // Then we check if there is any reason to not allow this improvement to be build - + // Can't build if there is already an irremovable improvement here this.improvement != null && getTileImprovement()!!.hasUnique(UniqueType.Irremovable, stateForConditionals) -> false - + // Can't build if any terrain specifically prevents building this improvement getTerrainMatchingUniques(UniqueType.RestrictedBuildableImprovements, stateForConditionals).any { unique -> !improvement.matchesFilter(unique.params[0]) } -> false - + // Can't build if the improvement specifically prevents building on some present feature improvement.getMatchingUniques(UniqueType.CannotBuildOnTile, stateForConditionals).any { unique -> matchesTerrainFilter(unique.params[0]) @@ -549,7 +559,7 @@ open class TileInfo { improvement.getMatchingUniques(UniqueType.CanOnlyBeBuiltOnTile, stateForConditionals).let { it.any() && it.any { unique -> !matchesTerrainFilter(unique.params[0]) } } -> false - + // Can't build if the improvement requires an adjacent terrain that is not present improvement.getMatchingUniques(UniqueType.MustBeNextTo, stateForConditionals).any { !isAdjacentTo(it.params[0]) @@ -557,8 +567,8 @@ open class TileInfo { // Can't build on unbuildable terrains - EXCEPT when specifically allowed to topTerrain.unbuildable && !improvement.isAllowedOnFeature(topTerrain.name) -> false - - // Can't build it if it is only allowed to improve resources and it doesn't improve this reousrce + + // Can't build it if it is only allowed to improve resources and it doesn't improve this resource improvement.hasUnique(UniqueType.CanOnlyImproveResource, stateForConditionals) && ( !resourceIsVisible || !tileResource.isImprovedBy(improvement.name) ) -> false @@ -899,7 +909,15 @@ open class TileInfo { for (unit in this.getUnits()) removeUnit(unit) } - fun setTileResource(newResource: TileResource, majorDeposit: Boolean = false) { + /** + * Sets this tile's [resource] and, if [newResource] is a Strategic resource, [resourceAmount] fields. + * + * [resourceAmount] is determined by [MapParameters.mapResources] and [majorDeposit], and + * if the latter is `null` a random choice between major and minor deposit is made, approximating + * the frequency [MapRegions][com.unciv.logic.map.mapgenerator.MapRegions] would use. + * A randomness source ([rng]) can optionally be provided for that step (not used otherwise). + */ + fun setTileResource(newResource: TileResource, majorDeposit: Boolean? = null, rng: Random = Random.Default) { resource = newResource.name if (newResource.resourceType != ResourceType.Strategic) return @@ -911,22 +929,29 @@ open class TileInfo { } } + val majorDepositFinal = majorDeposit ?: (rng.nextDouble() < approximateMajorDepositDistribution()) + val depositAmounts = if (majorDepositFinal) newResource.majorDepositAmount else newResource.minorDepositAmount resourceAmount = when (tileMap.mapParameters.mapResources) { - MapResources.sparse -> { - if (majorDeposit) newResource.majorDepositAmount.sparse - else newResource.minorDepositAmount.sparse - } - MapResources.abundant -> { - if (majorDeposit) newResource.majorDepositAmount.abundant - else newResource.minorDepositAmount.abundant - } - else -> { - if (majorDeposit) newResource.majorDepositAmount.default - else newResource.minorDepositAmount.default - } + MapResources.sparse -> depositAmounts.sparse + MapResources.abundant -> depositAmounts.abundant + else -> depositAmounts.default } } + private fun approximateMajorDepositDistribution(): Double { + // We can't replicate the MapRegions resource distributor, so let's try to get + // a close probability of major deposits per tile + var probability = 0.0 + for (unique in getAllTerrains().flatMap { it.getMatchingUniques(UniqueType.MajorStrategicFrequency) }) { + val frequency = unique.params[0].toIntOrNull() ?: continue + if (frequency <= 0) continue + // The unique param is literally "every N tiles", so to get a probability p=1/f + probability += 1.0 / frequency + } + return if (probability == 0.0) 0.04 // This is the default of 1 per 25 tiles + else probability + } + fun setTerrainFeatures(terrainFeatureList:List) { terrainFeatures = terrainFeatureList terrainFeatureObjects = terrainFeatureList.mapNotNull { ruleset.terrains[it] } diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt index fc311e64de..f1ac7e37bb 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt @@ -280,7 +280,7 @@ class MapGenerator(val ruleset: Ruleset) { val locations = randomness.chooseSpreadOutLocations(resourcesPerType, suitableTiles, mapRadius) - for (location in locations) location.setTileResource(resource) + for (location in locations) location.setTileResource(resource, rng = randomness.RNG) } } @@ -307,7 +307,7 @@ class MapGenerator(val ruleset: Ruleset) { if (possibleResources.isEmpty()) continue val resourceWithLeastAssignments = possibleResources.minByOrNull { resourceToNumber[it.name]!! }!! resourceToNumber.add(resourceWithLeastAssignments.name, 1) - tile.setTileResource(resourceWithLeastAssignments) + tile.setTileResource(resourceWithLeastAssignments, rng = randomness.RNG) } } diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorEditSubTabs.kt b/core/src/com/unciv/ui/mapeditor/MapEditorEditSubTabs.kt index 8531930f71..478fb7118b 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorEditSubTabs.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorEditSubTabs.kt @@ -10,6 +10,7 @@ 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.ruleset.unique.UniqueType import com.unciv.models.translations.tr import com.unciv.ui.civilopedia.FormattedLine import com.unciv.ui.civilopedia.MarkupRenderer @@ -18,6 +19,7 @@ import com.unciv.ui.mapeditor.MapEditorEditTab.BrushHandlerType import com.unciv.ui.tilegroups.TileGroup import com.unciv.ui.tilegroups.TileSetStrings import com.unciv.ui.utils.* +import kotlin.random.Random internal interface IMapEditorEditSubTabs { fun isDisabled(): Boolean @@ -146,15 +148,16 @@ class MapEditorEditResourcesTab( add(MarkupRenderer.render( getResources(), iconDisplay = FormattedLine.IconDisplay.NoLink - ) { - editTab.setBrush(it, "Resource/$it") { tile -> - tile.resource = it + ) { resourceName -> + val resource = ruleset.tileResources[resourceName]!! + editTab.setBrush(resourceName, resource.makeLink()) { + it.setTileResource(resource, rng = editTab.randomness.RNG) } }).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 + .filter { !it.hasUnique(UniqueType.CityStateOnlyResource) } private fun getResources(): List = sequence { var lastGroup = ResourceType.Bonus for (resource in allowedResources()) { diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorEditTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorEditTab.kt index c7615bd177..d380ae1ee7 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorEditTab.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorEditTab.kt @@ -28,7 +28,7 @@ class MapEditorEditTab( private val brushCell: Cell private var ruleset = editorScreen.ruleset - private val randomness = MapGenerationRandomness() // for auto river + internal val randomness = MapGenerationRandomness() // for auto river enum class BrushHandlerType { None, Direct, Tile, Road, River, RiverFromTo } private var brushHandlerType = BrushHandlerType.None diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorGenerateTab.kt b/core/src/com/unciv/ui/mapeditor/MapEditorGenerateTab.kt index d892b88287..d3c380462f 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorGenerateTab.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorGenerateTab.kt @@ -152,7 +152,9 @@ class MapEditorGenerateTab( private val parent: MapEditorGenerateTab ): Table(BaseScreen.skin) { val generateButton = "".toTextButton() - val mapParametersTable = MapParametersTable(parent.editorScreen.newMapParameters, isEmptyMapAllowed = true) + val mapParametersTable = MapParametersTable(parent.editorScreen.newMapParameters, forMapEditor = true) { + parent.replacePage(0, this) // A kludge to get the ScrollPanes to recognize changes in vertical layout?? + } init { top() @@ -161,6 +163,12 @@ class MapEditorGenerateTab( add(mapParametersTable).row() add(generateButton).padTop(15f).row() generateButton.onClick { parent.generate(MapGeneratorSteps.All) } + mapParametersTable.resourceSelectBox.onChange { + parent.editorScreen.run { + // normally the 'new map' parameters are independent, this needs to be an exception so strategic resource painting will use it + tileMap.mapParameters.mapResources = newMapParameters.mapResources + } + } } } diff --git a/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt b/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt index c18faadd53..a40a64dd27 100644 --- a/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt +++ b/core/src/com/unciv/ui/mapeditor/MapEditorScreen.kt @@ -2,7 +2,6 @@ package com.unciv.ui.mapeditor import com.badlogic.gdx.Gdx import com.badlogic.gdx.graphics.Color -import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane import com.unciv.MainMenuScreen import com.unciv.UncivGame import com.unciv.logic.HexMath @@ -18,18 +17,16 @@ import com.unciv.ui.popup.YesNoPopup import com.unciv.ui.tilegroups.TileGroup import com.unciv.ui.utils.* + //todo normalize properly +//todo Direct Strategic Resource abundance control //todo functional Tab for Units (empty Tab is prepared but commented out in MapEditorEditTab.AllEditSubTabs) //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 New function `convertTerrains` is auto-run after rivers the right decision for step-wise generation? Will paintRiverFromTo need the same? Will painting manually need the conversion? -//todo work in Simon's changes to continent/landmass -//todo work in Simon's regions - check whether generate and store or discard is the way -//todo Regions: If relevant, view and possibly work in Simon's colored visualization -//todo Strategic Resource abundance control //todo Tooltips for Edit items with info on placeability? Place this info as Brush description? In Expander? //todo Civilopedia links from edit items by right-click/long-tap? //todo Mod tab change base ruleset - disableAllCheckboxes - instead some intelligence to leave those mods on that stay compatible? @@ -38,7 +35,9 @@ import com.unciv.ui.utils.* //todo "random nation" starting location (maybe no new internal representation = all major nations) //todo Nat Wonder step generator: Needs tweaks to avoid placing duplicates or wonders too close together //todo Music? Different suffix? Off? 20% Volume? -//todo See #6610 - re-layout after the map size dropdown changes to custom and new widgets are inserted - can reach "Create" only by dragging the _header_ of the sub-TabbedPager +//todo See #6694 - allow placing Barbarian encampments (problem: dead on game start - BarbarianManager.encampments) +//todo See #6694 - allow adding tiles to a map (1 cell all around on hex? world-wrapped hex?? all around on rectangular? top bottom only on world-wrapped??) +//todo move map copy&paste to save/load?? class MapEditorScreen(map: TileMap? = null): BaseScreen() { /** The map being edited, with mod list for that map */ @@ -101,7 +100,7 @@ class MapEditorScreen(map: TileMap? = null): BaseScreen() { ?: return MapParameters() return lastSetup.mapParameters.clone().apply { reseed() - mods.removeAll(RulesetCache.getSortedBaseRulesets()) + mods.removeAll(RulesetCache.getSortedBaseRulesets().toSet()) } } fun saveDefaultParameters(parameters: MapParameters) { diff --git a/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt b/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt index 4ac56048d5..8b31311566 100644 --- a/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt +++ b/core/src/com/unciv/ui/newgamescreen/MapParametersTable.kt @@ -13,11 +13,12 @@ import com.unciv.ui.utils.* * * This is a separate class, because it should be in use both in the New Game screen and the Map Editor screen * - * @param isEmptyMapAllowed whether the [MapType.empty] option should be present. Is used by the Map Editor, but should **never** be used with the New Game + * @param forMapEditor whether the [MapType.empty] option should be present. Is used by the Map Editor, but should **never** be used with the New Game * */ class MapParametersTable( private val mapParameters: MapParameters, - private val isEmptyMapAllowed: Boolean = false + private val forMapEditor: Boolean = false, + private val sizeChangedCallback: (()->Unit)? = null ) : Table() { // These are accessed fom outside the class to read _and_ write values, // namely from MapOptionsTable, NewMapScreen and NewGameScreen @@ -84,7 +85,7 @@ class MapParametersTable( MapType.perlin, MapType.archipelago, MapType.innerSea, - if (isEmptyMapAllowed) MapType.empty else null + if (forMapEditor) MapType.empty else null ) mapTypeSelectBox = TranslatedSelectBox(mapTypes, mapParameters.type, skin) @@ -166,15 +167,21 @@ class MapParametersTable( customWorldSizeTable.add(rectangularSizeTable).grow().row() else mapParameters.mapSize = MapSizeNew(worldSizeSelectBox.selected.value) + + sizeChangedCallback?.invoke() } private fun addResourceSelectBox() { - val mapTypes = listOfNotNull( - MapResources.sparse, - MapResources.default, - MapResources.abundant, - MapResources.strategicBalance, - MapResources.legendaryStart + val mapTypes = if (forMapEditor) listOf( + MapResources.sparse, + MapResources.default, + MapResources.abundant, + ) else listOf( + MapResources.sparse, + MapResources.default, + MapResources.abundant, + MapResources.strategicBalance, + MapResources.legendaryStart ) resourceSelectBox = TranslatedSelectBox(mapTypes, mapParameters.mapResources, skin)