diff --git a/core/src/com/unciv/logic/map/mapgenerator/mapregions/LuxuryResourcePlacementLogic.kt b/core/src/com/unciv/logic/map/mapgenerator/mapregions/LuxuryResourcePlacementLogic.kt new file mode 100644 index 0000000000..5068bd27eb --- /dev/null +++ b/core/src/com/unciv/logic/map/mapgenerator/mapregions/LuxuryResourcePlacementLogic.kt @@ -0,0 +1,163 @@ +package com.unciv.logic.map.mapgenerator.mapregions + +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.tile.ResourceType +import com.unciv.models.ruleset.tile.TerrainType +import com.unciv.models.ruleset.tile.TileResource +import com.unciv.models.ruleset.unique.StateForConditionals +import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.ui.components.extensions.randomWeighted +import kotlin.math.min +import kotlin.math.pow + +object LuxuryResourcePlacementLogic { + + /** Assigns a luxury to each region. No luxury can be assigned to too many regions. + * Some luxuries are earmarked for city states. The rest are randomly distributed or + * don't occur att all in the map */ + fun assignLuxuries(regions: ArrayList, tileData: TileDataMap, ruleset: Ruleset): Pair, List> { + + // If there are any weightings defined in json, assume they are complete. If there are none, use flat weightings instead + val fallbackWeightings = ruleset.tileResources.values.none { + it.resourceType == ResourceType.Luxury && + (it.uniqueObjects.any { unique -> unique.isOfType(UniqueType.ResourceWeighting) } || it.hasUnique( + UniqueType.LuxuryWeightingForCityStates)) } + + val maxRegionsWithLuxury = when { + regions.size > 12 -> 3 + regions.size > 8 -> 2 + else -> 1 + } + val targetCityStateLuxuries = 3 // was probably intended to be "if (tileData.size > 5000) 4 else 3" + val assignableLuxuries = ruleset.tileResources.values.filter { + it.resourceType == ResourceType.Luxury && + !it.hasUnique(UniqueType.LuxurySpecialPlacement) && + !it.hasUnique(UniqueType.CityStateOnlyResource) } + val amountRegionsWithLuxury = HashMap() + // Init map + ruleset.tileResources.values + .forEach { amountRegionsWithLuxury[it.name] = 0 } + + for (region in regions.sortedBy { getRegionPriority(ruleset.terrains[it.type]) } ) { + val candidateLuxuries = getCandidateLuxuries( + assignableLuxuries, + amountRegionsWithLuxury, + maxRegionsWithLuxury, + fallbackWeightings, + region, + ruleset + ) + // If there are no candidates (mad modders???) just skip this region + if (candidateLuxuries.isEmpty()) continue + + // Pick a luxury at random. Weight is reduced if the luxury has been picked before + val regionConditional = StateForConditionals(region = region) + val modifiedWeights = candidateLuxuries.map { + val weightingUnique = it.getMatchingUniques(UniqueType.ResourceWeighting, regionConditional).firstOrNull() + val relativeWeight = if (weightingUnique == null) 1f else weightingUnique.params[0].toFloat() + relativeWeight / (1f + amountRegionsWithLuxury[it.name]!!) + }.shuffled() + region.luxury = candidateLuxuries.randomWeighted(modifiedWeights).name + amountRegionsWithLuxury[region.luxury!!] = amountRegionsWithLuxury[region.luxury]!! + 1 + } + + + val cityStateLuxuries = assignCityStateLuxuries( + targetCityStateLuxuries, + assignableLuxuries, + amountRegionsWithLuxury, + fallbackWeightings + ) + + val randomLuxuries = getLuxuriesForRandomPlacement(assignableLuxuries, amountRegionsWithLuxury, tileData, ruleset) + + return Pair(cityStateLuxuries, randomLuxuries) + } + + private fun getLuxuriesForRandomPlacement( + assignableLuxuries: List, + amountRegionsWithLuxury: HashMap, + tileData: TileDataMap, + ruleset: Ruleset + ): List { + val remainingLuxuries = assignableLuxuries.filter { + amountRegionsWithLuxury[it.name] == 0 + }.map { it.name }.shuffled() + + val disabledPercent = + 100 - min(tileData.size.toFloat().pow(0.2f) * 16, 100f).toInt() // Approximately + val targetDisabledLuxuries = (ruleset.tileResources.values + .count { it.resourceType == ResourceType.Luxury } * disabledPercent) / 100 + val randomLuxuries = remainingLuxuries.drop(targetDisabledLuxuries) + return randomLuxuries + } + + private fun getCandidateLuxuries( + assignableLuxuries: List, + amountRegionsWithLuxury: HashMap, + maxRegionsWithLuxury: Int, + fallbackWeightings: Boolean, + region: Region, + ruleset: Ruleset + ): List { + val regionConditional = StateForConditionals(region = region) + + var candidateLuxuries = assignableLuxuries.filter { + amountRegionsWithLuxury[it.name]!! < maxRegionsWithLuxury && + // Check that it has a weight for this region type + (fallbackWeightings || + it.hasUnique(UniqueType.ResourceWeighting, regionConditional)) && + // Check that there is enough coast if it is a water based resource + ((region.terrainCounts["Coastal"] ?: 0) >= 12 || + it.terrainsCanBeFoundOn.any { terrain -> ruleset.terrains[terrain]!!.type != TerrainType.Water }) + } + + // If we couldn't find any options, pick from all luxuries. First try to not pick water luxuries on land regions + if (candidateLuxuries.isEmpty()) { + candidateLuxuries = assignableLuxuries.filter { + amountRegionsWithLuxury[it.name]!! < maxRegionsWithLuxury && + // Ignore weightings for this pass + // Check that there is enough coast if it is a water based resource + ((region.terrainCounts["Coastal"] ?: 0) >= 12 || + it.terrainsCanBeFoundOn.any { terrain -> ruleset.terrains[terrain]!!.type != TerrainType.Water }) + } + } + // If there are still no candidates, ignore water restrictions + if (candidateLuxuries.isEmpty()) { + candidateLuxuries = assignableLuxuries.filter { + amountRegionsWithLuxury[it.name]!! < maxRegionsWithLuxury + // Ignore weightings and water for this pass + } + } + return candidateLuxuries + } + + private fun assignCityStateLuxuries( + targetCityStateLuxuries: Int, + assignableLuxuries: List, + amountRegionsWithLuxury: HashMap, + fallbackWeightings: Boolean + ): ArrayList { + val cityStateLuxuries = ArrayList() + repeat(targetCityStateLuxuries) { + val candidateLuxuries = assignableLuxuries.filter { + amountRegionsWithLuxury[it.name] == 0 && + (fallbackWeightings || it.hasUnique(UniqueType.LuxuryWeightingForCityStates)) + } + if (candidateLuxuries.isEmpty()) return@repeat + + val weights = candidateLuxuries.map { + val weightingUnique = + it.getMatchingUniques(UniqueType.LuxuryWeightingForCityStates).firstOrNull() + if (weightingUnique == null) + 1f + else + weightingUnique.params[0].toFloat() + } + val luxury = candidateLuxuries.randomWeighted(weights).name + cityStateLuxuries.add(luxury) + amountRegionsWithLuxury[luxury] = 1 + } + return cityStateLuxuries + } +} diff --git a/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegions.kt b/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegions.kt index 94245fc682..f2fe51bfc5 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegions.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/mapregions/MapRegions.kt @@ -88,8 +88,6 @@ class MapRegions (val ruleset: Ruleset){ private val regions = ArrayList() private var usingArchipelagoRegions = false private val tileData = TileDataMap() - private val cityStateLuxuries = ArrayList() - private val randomLuxuries = ArrayList() /** Creates [numRegions] number of balanced regions for civ starting locations. */ fun generateRegions(tileMap: TileMap, numRegions: Int) { @@ -426,18 +424,6 @@ class MapRegions (val ruleset: Ruleset){ Log.debug(Tag("assignRegions"), msg, startBiasType, logCiv, region) } - private fun getRegionPriority(terrain: Terrain?): Int? { - if (terrain == null) // ie "hybrid" - return 99999 // a big number - return if (!terrain.hasUnique(UniqueType.RegionRequirePercentSingleType) - && !terrain.hasUnique(UniqueType.RegionRequirePercentTwoTypes)) - null - else - if (terrain.hasUnique(UniqueType.RegionRequirePercentSingleType)) - terrain.getMatchingUniques(UniqueType.RegionRequirePercentSingleType).first().params[2].toInt() - else - terrain.getMatchingUniques(UniqueType.RegionRequirePercentTwoTypes).first().params[3].toInt() - } private fun assignCivToRegion(civ: Civilization, region: Region) { val tile = region.tileMap[region.startPosition!!] @@ -671,9 +657,10 @@ class MapRegions (val ruleset: Ruleset){ fun placeResourcesAndMinorCivs(tileMap: TileMap, minorCivs: List) { placeNaturalWonderImpacts(tileMap) - assignLuxuries() + + val (cityStateLuxuries, randomLuxuries) = LuxuryResourcePlacementLogic.assignLuxuries(regions, tileData, ruleset) MinorCivPlacer.placeMinorCivs(regions, tileMap, minorCivs, usingArchipelagoRegions, tileData, ruleset) - placeLuxuries(tileMap) + placeLuxuries(tileMap, cityStateLuxuries, randomLuxuries) placeStrategicAndBonuses(tileMap) } @@ -687,104 +674,14 @@ class MapRegions (val ruleset: Ruleset){ } } - /** Assigns a luxury to each region. No luxury can be assigned to too many regions. - * Some luxuries are earmarked for city states. The rest are randomly distributed or - * don't occur att all in the map */ - private fun assignLuxuries() { - // If there are any weightings defined in json, assume they are complete. If there are none, use flat weightings instead - val fallbackWeightings = ruleset.tileResources.values.none { - it.resourceType == ResourceType.Luxury && - (it.uniqueObjects.any { unique -> unique.isOfType(UniqueType.ResourceWeighting) } || it.hasUnique(UniqueType.LuxuryWeightingForCityStates)) } - - val maxRegionsWithLuxury = when { - regions.size > 12 -> 3 - regions.size > 8 -> 2 - else -> 1 - } - val targetCityStateLuxuries = 3 // was probably intended to be "if (tileData.size > 5000) 4 else 3" - val disabledPercent = 100 - min(tileData.size.toFloat().pow(0.2f) * 16, 100f).toInt() // Approximately - val targetDisabledLuxuries = (ruleset.tileResources.values - .count { it.resourceType == ResourceType.Luxury } * disabledPercent) / 100 - val assignableLuxuries = ruleset.tileResources.values.filter { - it.resourceType == ResourceType.Luxury && - !it.hasUnique(UniqueType.LuxurySpecialPlacement) && - !it.hasUnique(UniqueType.CityStateOnlyResource) } - val amountRegionsWithLuxury = HashMap() - // Init map - ruleset.tileResources.values - .forEach { amountRegionsWithLuxury[it.name] = 0 } - - for (region in regions.sortedBy { getRegionPriority(ruleset.terrains[it.type]) } ) { - val regionConditional = StateForConditionals(region = region) - var candidateLuxuries = assignableLuxuries.filter { - amountRegionsWithLuxury[it.name]!! < maxRegionsWithLuxury && - // Check that it has a weight for this region type - (fallbackWeightings || - it.hasUnique(UniqueType.ResourceWeighting, regionConditional)) && - // Check that there is enough coast if it is a water based resource - ((region.terrainCounts["Coastal"] ?: 0) >= 12 || - it.terrainsCanBeFoundOn.any { terrain -> ruleset.terrains[terrain]!!.type != TerrainType.Water } ) - } - - // If we couldn't find any options, pick from all luxuries. First try to not pick water luxuries on land regions - if (candidateLuxuries.isEmpty()) { - candidateLuxuries = assignableLuxuries.filter { - amountRegionsWithLuxury[it.name]!! < maxRegionsWithLuxury && - // Ignore weightings for this pass - // Check that there is enough coast if it is a water based resource - ((region.terrainCounts["Coastal"] ?: 0) >= 12 || - it.terrainsCanBeFoundOn.any { terrain -> ruleset.terrains[terrain]!!.type != TerrainType.Water }) - } - } - // If there are still no candidates, ignore water restrictions - if (candidateLuxuries.isEmpty()) { - candidateLuxuries = assignableLuxuries.filter { - amountRegionsWithLuxury[it.name]!! < maxRegionsWithLuxury - // Ignore weightings and water for this pass - } - } - // If there are still no candidates (mad modders???) just skip this region - if (candidateLuxuries.isEmpty()) continue - - // Pick a luxury at random. Weight is reduced if the luxury has been picked before - val modifiedWeights = candidateLuxuries.map { - val weightingUnique = it.getMatchingUniques(UniqueType.ResourceWeighting, regionConditional).firstOrNull() - val relativeWeight = if (weightingUnique == null) 1f else weightingUnique.params[0].toFloat() - relativeWeight / (1f + amountRegionsWithLuxury[it.name]!!) - }.shuffled() - region.luxury = candidateLuxuries.randomWeighted(modifiedWeights).name - amountRegionsWithLuxury[region.luxury!!] = amountRegionsWithLuxury[region.luxury]!! + 1 - } - - // Assign luxuries to City States - repeat(targetCityStateLuxuries) { - val candidateLuxuries = assignableLuxuries.filter { - amountRegionsWithLuxury[it.name] == 0 && - (fallbackWeightings || it.hasUnique(UniqueType.LuxuryWeightingForCityStates)) - } - if (candidateLuxuries.isEmpty()) return@repeat - - val weights = candidateLuxuries.map { - val weightingUnique = it.getMatchingUniques(UniqueType.LuxuryWeightingForCityStates).firstOrNull() - if (weightingUnique == null) - 1f - else - weightingUnique.params[0].toFloat() - } - val luxury = candidateLuxuries.randomWeighted(weights).name - cityStateLuxuries.add(luxury) - amountRegionsWithLuxury[luxury] = 1 - } - - // Assign some resources as random placement. - val remainingLuxuries = assignableLuxuries.filter { - amountRegionsWithLuxury[it.name] == 0 - }.map { it.name }.shuffled() - randomLuxuries.addAll(remainingLuxuries.drop(targetDisabledLuxuries)) - } /** Places all Luxuries onto [tileMap]. Assumes that assignLuxuries and placeMinorCivs have been called. */ - private fun placeLuxuries(tileMap: TileMap) { + private fun placeLuxuries( + tileMap: TileMap, + cityStateLuxuries: List, + randomLuxuries: List + ) { + // First place luxuries at major civ start locations val averageFertilityDensity = regions.sumOf { it.totalFertility } / regions.sumOf { it.tiles.size }.toFloat() for (region in regions) { @@ -1124,6 +1021,20 @@ class MapRegions (val ruleset: Ruleset){ } + +fun getRegionPriority(terrain: Terrain?): Int? { + if (terrain == null) // ie "hybrid" + return 99999 // a big number + return if (!terrain.hasUnique(UniqueType.RegionRequirePercentSingleType) + && !terrain.hasUnique(UniqueType.RegionRequirePercentTwoTypes)) + null + else + if (terrain.hasUnique(UniqueType.RegionRequirePercentSingleType)) + terrain.getMatchingUniques(UniqueType.RegionRequirePercentSingleType).first().params[2].toInt() + else + terrain.getMatchingUniques(UniqueType.RegionRequirePercentTwoTypes).first().params[3].toInt() +} + /** @return a fake unique with the same conditionals, but sorted alphabetically. * Used to save some memory and time when building resource lists. */ internal fun anonymizeUnique(unique: Unique) = Unique(