diff --git a/android/assets/jsons/Civ V - Vanilla/Terrains.json b/android/assets/jsons/Civ V - Vanilla/Terrains.json index 8eb21af6f4..c1c04137cf 100644 --- a/android/assets/jsons/Civ V - Vanilla/Terrains.json +++ b/android/assets/jsons/Civ V - Vanilla/Terrains.json @@ -13,7 +13,9 @@ "type": "Water", "food": 1, "movementCost": 1, - "RGB": [107,167,193] + "RGB": [107,167,193], + "uniques": ["[+2] to Fertility for Map Generation", + "Considered [Desirable] when determining start locations "] }, { "name": "Grassland", @@ -28,7 +30,16 @@ "Occurs at temperature between [0.9] and [1] and humidity between [0.2] and [0.9]", "Occurs at temperature between [0.8] and [0.9] and humidity between [0.6] and [0.9]", "Occurs at temperature between [0.7] and [0.8] and humidity between [0.7] and [0.9]", - "Occurs at temperature between [0.6] and [0.8] and humidity between [0.4] and [0.6]"] + "Occurs at temperature between [0.6] and [0.8] and humidity between [0.4] and [0.6]", + "[+3] to Fertility for Map Generation", + "A Region is formed with at least [30]% [Grassland] tiles, with priority [7]", + "A Region can not contain more [Plains] tiles than [Grassland] tiles", + "Considered [Desirable] when determining start locations", + "Considered [Food] when determining start locations ", + "Considered [Food] when determining start locations ", + "Considered [Food] when determining start locations ", + "Considered [Food] when determining start locations ", + "Considered [Food] when determining start locations "] }, { "name": "Plains", @@ -47,7 +58,16 @@ "Occurs at temperature between [0.8] and [0.9] and humidity between [0.2] and [0.6]", "Occurs at temperature between [0.7] and [0.8] and humidity between [0.3] and [0.4]", "Occurs at temperature between [0.6] and [0.8] and humidity between [0.6] and [0.7]", - "Occurs at temperature between [0.5] and [0.7] and humidity between [0.7] and [0.8]"] + "Occurs at temperature between [0.5] and [0.7] and humidity between [0.7] and [0.8]", + "[+4] to Fertility for Map Generation", + "A Region is formed with at least [30]% [Plains] tiles, with priority [6]", + "A Region can not contain more [Grassland] tiles than [Plains] tiles", + "Considered [Desirable] when determining start locations", + "Considered [Food] when determining start locations ", + "Considered [Food] when determining start locations ", + "Considered [Food] when determining start locations ", + "Considered [Food] when determining start locations ", + "Considered [Food] when determining start locations "] }, { "name": "Tundra", @@ -59,7 +79,11 @@ "Occurs at temperature between [-0.8] and [-0.5] and humidity between [0.6] and [0.8]", "Occurs at temperature between [-0.7] and [-0.4] and humidity between [0.4] and [0.6]", "Occurs at temperature between [-0.6] and [-0.4] and humidity between [0.2] and [0.4]", - "Occurs at temperature between [-0.5] and [-0.4] and humidity between [0] and [0.2]"] + "Occurs at temperature between [-0.5] and [-0.4] and humidity between [0] and [0.2]", + "[+2] to Fertility for Map Generation", + "A Region is formed with at least [30]% [Tundra] tiles and [Snow] tiles, with priority [1]", + "Considered [Food] when determining start locations ", + "Considered [Desirable] when determining start locations "] }, { "name": "Desert", @@ -70,7 +94,10 @@ "Occurs at temperature between [0.1] and [0.8] and humidity between [0.2] and [0.3]", "Occurs at temperature between [0.2] and [0.7] and humidity between [0.3] and [0.4]", "Occurs at temperature between [0.4] and [0.6] and humidity between [0.4] and [0.5]", - "Occurs at temperature between [0.5] and [0.6] and humidity between [0.5] and [0.7]"] + "Occurs at temperature between [0.5] and [0.6] and humidity between [0.5] and [0.7]", + "[+1] to Fertility for Map Generation", + "A Region is formed with at least [25]% [Desert] tiles, with priority [4]", + "Considered [Undesirable] when determining start locations "] }, { "name": "Lakes", @@ -78,7 +105,9 @@ "food": 2, "gold": 1, "RGB": [ 123, 202, 226], - "uniques": ["Fresh water"] + "uniques": ["Fresh water", + "Considered [Food] when determining start locations", + "Considered [Desirable] when determining start locations"] }, { "name": "Mountain", @@ -86,7 +115,12 @@ "impassable": true, "defenceBonus": 0.25, "RGB": [120, 120, 120], - "uniques":["Rough terrain", "Has an elevation of [4] for visibility calculations", "Occurs in chains at high elevations", "Units ending their turn on this terrain take [50] damage"] + "uniques": ["Rough terrain", + "Has an elevation of [4] for visibility calculations", + "Occurs in chains at high elevations", + "Units ending their turn on this terrain take [50] damage", + "Always Fertility [-2] for Map Generation", + "Considered [Undesirable] when determining start locations"] }, { "name": "Snow", @@ -97,7 +131,9 @@ "Occurs at temperature between [-0.9] and [-0.8] and humidity between [0] and [0.8]", "Occurs at temperature between [-0.8] and [-0.7] and humidity between [0] and [0.6]", "Occurs at temperature between [-0.7] and [-0.6] and humidity between [0] and [0.4]", - "Occurs at temperature between [-0.6] and [-0.5] and humidity between [0] and [0.2]"] + "Occurs at temperature between [-0.6] and [-0.5] and humidity between [0] and [0.2]", + "Always Fertility [-1] for Map Generation", + "Considered [Undesirable] when determining start locations"] }, // Terrain features @@ -110,8 +146,15 @@ "defenceBonus": 0.25, "RGB": [105,125,72], "occursOn": ["Tundra","Plains","Grassland","Desert","Snow"], - "uniques": ["Rough terrain", "[+5] Strength for cities built on this terrain", - "Has an elevation of [2] for visibility calculations", "Occurs in groups around high elevations"] + "uniques": ["Rough terrain", + "[+5] Strength for cities built on this terrain", + "Has an elevation of [2] for visibility calculations", + "Occurs in groups around high elevations", + "[+1] to Fertility for Map Generation", + "A Region is formed with at least [40]% [Hill] tiles, with priority [5]", + "Base Terrain on this tile is not counted for Region determination", + "Considered [Desirable] when determining start locations", + "Considered [Production] when determining start locations"] }, { "name": "Forest", @@ -123,8 +166,17 @@ "unbuildable": true, "defenceBonus": 0.25, "occursOn": ["Tundra","Plains","Grassland","Hill"], - "uniques": ["Rough terrain", "Provides a one-time Production bonus to the closest city when cut down", - "Blocks line-of-sight from tiles at same elevation", "Resistant to nukes", "Can be destroyed by nukes"], + "uniques": ["Rough terrain", + "Provides a one-time Production bonus to the closest city when cut down", + "Blocks line-of-sight from tiles at same elevation", + "Resistant to nukes", "Can be destroyed by nukes", + "A Region is formed with at least [30]% [Forest] tiles, with priority [3]", + "A Region is formed with at least [35]% [Forest] tiles and [Jungle] tiles, with priority [3]", + "A Region can not contain more [Jungle] tiles than [Forest] tiles", + "Considered [Desirable] when determining start locations", + "Considered [Production] when determining start locations", + "Considered [Food] when determining start locations ", + "Considered [Food] when determining start locations "], "civilopediaText": [{"text":"A Camp can be built here without cutting it down", "link":"Improvement/Camp"}] }, { @@ -136,7 +188,15 @@ "unbuildable": true, "defenceBonus": 0.25, "occursOn": ["Plains","Grassland"], - "uniques": ["Rough terrain", "Blocks line-of-sight from tiles at same elevation", "Resistant to nukes", "Can be destroyed by nukes"] + "uniques": ["Rough terrain", + "Blocks line-of-sight from tiles at same elevation", + "Resistant to nukes", "Can be destroyed by nukes", + "[-1] to Fertility for Map Generation", + "A Region is formed with at least [30]% [Jungle] tiles, with priority [2]", + "A Region is formed with at least [35]% [Jungle] tiles and [Forest] tiles, with priority [2]", + "A Region can not contain more [Forest] tiles than [Jungle] tiles", + "Considered [Food] when determining start locations ", + "Considered [Desirable] when determining start locations "] }, { "name": "Marsh", @@ -146,7 +206,8 @@ "unbuildable": true, "defenceBonus": -0.15, "occursOn": ["Grassland"], - "uniques": ["Rare feature"], + "uniques": ["Rare feature", + "[-2] to Fertility for Map Generation"], "civilopediaText": [{"text":"Only Polders can be built here", "link":"Improvement/Polder"}] }, { @@ -169,7 +230,11 @@ "unbuildable": true, "defenceBonus": -0.1, "occursOn": ["Desert"], - "uniques": ["Fresh water", "Rare feature", "Only [All Road] improvements may be built on this tile"] + "uniques": ["Fresh water", "Rare feature", + "Only [All Road] improvements may be built on this tile", + "Always Fertility [4] for Map Generation", + "Considered [Food] when determining start locations", + "Considered [Desirable] when determining start locations"] }, { "name": "Flood plains", @@ -177,14 +242,19 @@ "food": 2, "movementCost": 1, "defenceBonus": -0.1, - "occursOn": ["Desert"] + "occursOn": ["Desert"], + "uniques": ["Always Fertility [5] for Map Generation", + "Considered [Food] when determining start locations", + "Considered [Desirable] when determining start locations"] }, { "name": "Ice", "type": "TerrainFeature", "impassable": true, "overrideStats": true, - "occursOn": ["Ocean", "Coast"] + "occursOn": ["Ocean", "Coast"], + "uniques": ["[-1] to Fertility for Map Generation", + "Considered [Undesirable] when determining start locations"] }, { "name": "Atoll", diff --git a/android/assets/jsons/Civ V - Vanilla/TileResources.json b/android/assets/jsons/Civ V - Vanilla/TileResources.json index 7c67eaabad..787c83655c 100644 --- a/android/assets/jsons/Civ V - Vanilla/TileResources.json +++ b/android/assets/jsons/Civ V - Vanilla/TileResources.json @@ -143,7 +143,10 @@ "terrainsCanBeFoundOn": ["Forest","Tundra"], "gold": 2, "improvement": "Camp", - "improvementStats": {"gold": 1} + "improvementStats": {"gold": 1}, + "uniques": ["Appears in [Tundra] regions with weight [40]", + "Appears in [Forest] regions with weight [10]", + "Appears near City States with weight [15]"] }, { "name": "Cotton", @@ -151,7 +154,11 @@ "terrainsCanBeFoundOn": ["Grassland","Plains","Desert"], "gold": 2, "improvement": "Plantation", - "improvementStats": {"gold": 1} + "improvementStats": {"gold": 1}, + "uniques": ["Appears in [Desert] regions with weight [15]", + "Appears in [Grassland] regions with weight [30]", + "Appears in [Hybrid] regions with weight [15]", + "Appears near City States with weight [10]"] }, { "name": "Dyes", @@ -159,7 +166,11 @@ "terrainsCanBeFoundOn": ["Jungle","Forest"], "gold": 2, "improvement": "Plantation", - "improvementStats": {"gold": 1} + "improvementStats": {"gold": 1}, + "uniques": ["Appears in [Tundra] regions with weight [5]", + "Appears in [Jungle] regions with weight [5]", + "Appears in [Forest] regions with weight [30]", + "Appears near City States with weight [10]"] }, { "name": "Gems", @@ -167,7 +178,13 @@ "terrainsCanBeFoundOn": ["Jungle","Grassland","Plains","Desert","Tundra","Hill"], "gold": 3, "improvement": "Mine", - "improvementStats": {"production": 1} + "improvementStats": {"production": 1}, + "uniques": ["Appears in [Tundra] regions with weight [5]", + "Appears in [Jungle] regions with weight [20]", + "Appears in [Hill] regions with weight [15]", + "Appears in [Grassland] regions with weight [5]", + "Appears in [Hybrid] regions with weight [5]", + "Appears near City States with weight [10]"] }, { "name": "Gold Ore", // Not called "Gold" in order to not conflict with siege type units for translations @@ -175,7 +192,12 @@ "terrainsCanBeFoundOn": ["Grassland","Plains","Desert","Hill"], "gold": 2, "improvement": "Mine", - "improvementStats": {"production": 1} + "improvementStats": {"production": 1}, + "uniques": ["Appears in [Desert] regions with weight [25]", + "Appears in [Hill] regions with weight [30]", + "Appears in [Plains] regions with weight [5]", + "Appears in [Hybrid] regions with weight [5]", + "Appears near City States with weight [10]"] }, { "name": "Silver", @@ -183,7 +205,12 @@ "terrainsCanBeFoundOn": ["Desert","Tundra","Hill"], "gold": 2, "improvement": "Mine", - "improvementStats": {"production": 1} + "improvementStats": {"production": 1}, + "uniques": ["Appears in [Tundra] regions with weight [25]", + "Appears in [Hill] regions with weight [30]", + "Appears in [Grassland] regions with weight [20]", + "Appears in [Hybrid] regions with weight [10]", + "Appears near City States with weight [10]"] }, { "name": "Incense", @@ -191,7 +218,11 @@ "terrainsCanBeFoundOn": ["Plains","Desert"], "gold": 2, "improvement": "Plantation", - "improvementStats": {"gold": 1} + "improvementStats": {"gold": 1}, + "uniques": ["Appears in [Desert] regions with weight [35]", + "Appears in [Plains] regions with weight [10]", + "Appears in [Hybrid] regions with weight [5]", + "Appears near City States with weight [15]"] }, { "name": "Ivory", @@ -199,7 +230,10 @@ "terrainsCanBeFoundOn": ["Plains"], "gold": 2, "improvement": "Camp", - "improvementStats": {"gold": 1} + "improvementStats": {"gold": 1}, + "uniques": ["Appears in [Plains] regions with weight [35]", + "Appears in [Hybrid] regions with weight [15]", + "Appears near City States with weight [10]"] }, { "name": "Silk", @@ -207,7 +241,11 @@ "terrainsCanBeFoundOn": ["Forest"], "gold": 2, "improvement": "Plantation", - "improvementStats": {"gold": 1} + "improvementStats": {"gold": 1}, + "uniques": ["Appears in [Jungle] regions with weight [5]", + "Appears in [Forest] regions with weight [30]", + "Appears in [Hybrid] regions with weight [5]", + "Appears near City States with weight [15]"] }, { "name": "Spices", @@ -215,7 +253,13 @@ "terrainsCanBeFoundOn": ["Jungle","Forest"], "gold": 2, "improvement": "Plantation", - "improvementStats": {"gold": 1} + "improvementStats": {"gold": 1}, + "uniques": ["Appears in [Jungle] regions with weight [30]", + "Appears in [Forest] regions with weight [10]", + "Appears in [Plains] regions with weight [5]", + "Appears in [Grassland] regions with weight [5]", + "Appears in [Hybrid] regions with weight [5]", + "Appears near City States with weight [15]"] }, { "name": "Wine", @@ -223,7 +267,10 @@ "terrainsCanBeFoundOn": ["Grassland","Plains"], "gold": 2, "improvement": "Plantation", - "improvementStats": {"gold": 1} + "improvementStats": {"gold": 1}, + "uniques": ["Appears in [Plains] regions with weight [35]", + "Appears in [Hybrid] regions with weight [15]", + "Appears near City States with weight [10]"] }, { "name": "Sugar", @@ -231,7 +278,12 @@ "terrainsCanBeFoundOn": ["Plains","Flood plains","Grassland","Marsh"], "gold": 2, "improvement": "Plantation", - "improvementStats": {"gold": 1} + "improvementStats": {"gold": 1}, + "uniques": ["Appears in [Jungle] regions with weight [20]", + "Appears in [Desert] regions with weight [15]", + "Appears in [Grassland] regions with weight [20]", + "Appears in [Hybrid] regions with weight [5]", + "Appears near City States with weight [10]"] }, { "name": "Marble", @@ -249,7 +301,14 @@ "food": 1, "gold": 1, "improvement": "Fishing Boats", - "improvementStats": {"food": 1} + "improvementStats": {"food": 1}, + "uniques": ["Appears in [Tundra] regions with weight [35]", + "Appears in [Forest] regions with weight [10]", + "Appears in [Hill] regions with weight [10]", + "Appears in [Plains] regions with weight [5]", + "Appears in [Grassland] regions with weight [10]", + "Appears in [Hybrid] regions with weight [20]", + "Appears near City States with weight [10]"] }, { "name": "Pearls", @@ -257,7 +316,15 @@ "terrainsCanBeFoundOn": ["Coast"], "gold": 2, "improvement": "Fishing Boats", - "improvementStats": {"food": 1} + "improvementStats": {"food": 1}, + "uniques": ["Appears in [Jungle] regions with weight [20]", + "Appears in [Forest] regions with weight [10]", + "Appears in [Desert] regions with weight [5]", + "Appears in [Hill] regions with weight [15]", + "Appears in [Plains] regions with weight [5]", + "Appears in [Grassland] regions with weight [10]", + "Appears in [Hybrid] regions with weight [20]", + "Appears near City States with weight [15]"] }, { "name": "Jewelry", @@ -279,6 +346,11 @@ "gold": 1, "improvement": "Plantation", "improvementStats": {"gold": 1}, + "uniques": ["Appears in [Jungle] regions with weight [35]", + "Appears in [Forest] regions with weight [5]", + "Appears in [Desert] regions with weight [5]", + "Appears in [Hybrid] regions with weight [5]", + "Appears near City States with weight [15]"] }, { "name": "Copper", @@ -286,7 +358,15 @@ "terrainsCanBeFoundOn": ["Plains","Grassland","Desert","Tundra","Snow"], "gold": 2, "improvement": "Mine", - "improvementStats": {"production": 2} + "improvementStats": {"production": 2}, + "uniques": ["Appears in [Tundra] regions with weight [15]", + "Appears in [Jungle] regions with weight [5]", + "Appears in [Forest] regions with weight [5]", + "Appears in [Desert] regions with weight [10]", + "Appears in [Hill] regions with weight [30]", + "Appears in [Grassland] regions with weight [20]", + "Appears in [Hybrid] regions with weight [20]", + "Appears near City States with weight [10]"] }, /* { @@ -307,6 +387,14 @@ "gold": 1, "improvement": "Fishing Boats", "improvementStats": {"food": 1}, + "uniques": ["Appears in [Tundra] regions with weight [30]", + "Appears in [Jungle] regions with weight [5]", + "Appears in [Forest] regions with weight [10]", + "Appears in [Hill] regions with weight [10]", + "Appears in [Plains] regions with weight [5]", + "Appears in [Grassland] regions with weight [20]", + "Appears in [Hybrid] regions with weight [20]", + "Appears near City States with weight [15]"] }, { "name": "Salt", @@ -315,7 +403,14 @@ "gold": 1, "food": 1, "improvement": "Mine", - "improvementStats": {"food": 1,"production":1} + "improvementStats": {"food": 1,"production":1}, + "uniques": ["Appears in [Tundra] regions with weight [15]", + "Appears in [Forest] regions with weight [5]", + "Appears in [Desert] regions with weight [15]", + "Appears in [Hill] regions with weight [10]", + "Appears in [Plains] regions with weight [25]", + "Appears in [Hybrid] regions with weight [15]", + "Appears near City States with weight [10]"] }, { "name": "Truffles", @@ -323,6 +418,12 @@ "terrainsCanBeFoundOn": ["Forest","Marsh","Jungle"], "gold": 2, "improvement": "Camp", - "improvementStats": {"gold": 1} + "improvementStats": {"gold": 1}, + "uniques": ["Appears in [Jungle] regions with weight [5]", + "Appears in [Forest] regions with weight [30]", + "Appears in [Plains] regions with weight [5]", + "Appears in [Grassland] regions with weight [5]", + "Appears in [Hybrid] regions with weight [10]", + "Appears near City States with weight [15]"] } ] diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index 708e0f47c1..e2876e3bf5 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -1181,6 +1181,9 @@ Strategic resource = Fresh water = non-fresh water = Natural Wonder = +Hybrid = +Undesirable = +Desirable = # improvementFilters diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index c82db31e3b..6de0b877fe 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -41,8 +41,12 @@ object GameStarter { tileMap = MapSaver.loadMap(gameSetupInfo.mapFile!!) // Don't override the map parameters - this can include if we world wrap or not! } else runAndMeasure("generateMap") { - tileMap = mapGen.generateMap(gameSetupInfo.mapParameters) + // The mapgen needs to know what civs are in the game to generate regions, starts and resources + addCivilizations(gameSetupInfo.gameParameters, gameInfo, ruleset, existingMap = false) + tileMap = mapGen.generateMap(gameSetupInfo.mapParameters, gameInfo.civilizations) tileMap.mapParameters = gameSetupInfo.mapParameters + // Now forget them for a moment! MapGen can silently fail to place some city states, so then we'll use the old fallback method to place those. + gameInfo.civilizations.clear() } runAndMeasure("addCivilizations") { @@ -52,7 +56,8 @@ object GameStarter { addCivilizations( gameSetupInfo.gameParameters, gameInfo, - ruleset + ruleset, + existingMap = true ) // this is before gameInfo.setTransients, so gameInfo doesn't yet have the gameBasics } @@ -169,7 +174,7 @@ object GameStarter { } } - private fun addCivilizations(newGameParameters: GameParameters, gameInfo: GameInfo, ruleset: Ruleset) { + private fun addCivilizations(newGameParameters: GameParameters, gameInfo: GameInfo, ruleset: Ruleset, existingMap: Boolean) { val availableCivNames = Stack() // CityState or Spectator civs are not available for Random pick availableCivNames.addAll(ruleset.nations.filter { it.value.isMajorCiv() }.keys.shuffled()) @@ -183,9 +188,16 @@ object GameStarter { gameInfo.civilizations.add(barbarianCivilization) } + val civNamesWithStartingLocations = if(existingMap) gameInfo.tileMap.startingLocationsByNation.keys + else emptySet() + val presetMajors = Stack() + presetMajors.addAll(availableCivNames.filter { it in civNamesWithStartingLocations }) + for (player in newGameParameters.players.sortedBy { it.chosenCiv == "Random" }) { val nationName = if (player.chosenCiv != "Random") player.chosenCiv + else if (presetMajors.isNotEmpty()) presetMajors.pop() else availableCivNames.pop() + availableCivNames.remove(nationName) // In case we got it from a map preset val playerCiv = CivilizationInfo(nationName) for (tech in startingTechs) @@ -195,8 +207,6 @@ object GameStarter { gameInfo.civilizations.add(playerCiv) } - val civNamesWithStartingLocations = gameInfo.tileMap.startingLocationsByNation.keys - val availableCityStatesNames = Stack() // since we shuffle and then order by, we end up with all the City-States with starting tiles first in a random order, // and then all the other City-States in a random order! Because the sortedBy function is stable! diff --git a/core/src/com/unciv/logic/HexMath.kt b/core/src/com/unciv/logic/HexMath.kt index 729876a6c0..c51dadbf6a 100644 --- a/core/src/com/unciv/logic/HexMath.kt +++ b/core/src/com/unciv/logic/HexMath.kt @@ -117,6 +117,13 @@ object HexMath { cubic2HexCoords(evenQ2CubicCoords(evenQCoord)) } + fun hex2EvenQCoords(hexCoord: Vector2): Vector2 { + return if (hexCoord == Vector2.Zero) + Vector2.Zero + else + cubic2EvenQCoords(hex2CubicCoords(hexCoord)) + } + fun roundCubicCoords(cubicCoords: Vector3): Vector3 { var rx = round(cubicCoords.x) var ry = round(cubicCoords.y) diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index 19067c3372..c4e8840de0 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -354,6 +354,23 @@ open class TileInfo { return stats.food + stats.production + stats.gold } + // For dividing the map into Regions to determine start locations + fun getTileFertility(checkCoasts: Boolean): Int { + val terrains = getAllTerrains() + var fertility = 0 + for (terrain in terrains) { + if (terrain.hasUnique(UniqueType.OverrideFertility)) + return terrain.getMatchingUniques(UniqueType.OverrideFertility).first().params[0].toInt() + else + fertility += terrain.getMatchingUniques(UniqueType.AddFertility) + .sumBy { it.params[0].toInt() } + } + if (isAdjacentToRiver()) fertility += 1 + if (isAdjacentToFreshwater) fertility += 1 // meaning total +2 for river + if (checkCoasts && isCoastalTile()) fertility += 2 + return fertility + } + fun getImprovementStats(improvement: TileImprovement, observingCiv: CivilizationInfo, city: CityInfo?): Stats { val stats = improvement.cloneStats() if (hasViewableResource(observingCiv) && tileResource.improvement == improvement.name) diff --git a/core/src/com/unciv/logic/map/TileMap.kt b/core/src/com/unciv/logic/map/TileMap.kt index 749d8e7a98..06c6ffec63 100644 --- a/core/src/com/unciv/logic/map/TileMap.kt +++ b/core/src/com/unciv/logic/map/TileMap.kt @@ -1,5 +1,6 @@ package com.unciv.logic.map +import com.badlogic.gdx.math.Rectangle import com.badlogic.gdx.math.Vector2 import com.unciv.Constants import com.unciv.logic.GameInfo @@ -198,6 +199,27 @@ class TileMap { } }.filterNotNull() + /** @return all tiles within [rectangle], respecting world edges and wrap. + * If using even Q coordinates the rectangle will be "straight" ie parallel with rectangular map edges. */ + fun getTilesInRectangle(rectangle: Rectangle, evenQ: Boolean = false): Sequence = + if (rectangle.width <= 0 || rectangle.height <= 0) + sequenceOf(get(rectangle.x.toInt(), rectangle.y.toInt())) + else + sequence { + for (x in 0 until rectangle.width.toInt()) { + for (y in 0 until rectangle.height.toInt()) { + val currentX = rectangle.x + x + val currentY = rectangle.y + y + if (evenQ) { + val hexCoords = HexMath.evenQ2HexCoords(Vector2(currentX, currentY)) + yield(getIfTileExistsOrNull(hexCoords.x.toInt(), hexCoords.y.toInt())) + } + else + yield(getIfTileExistsOrNull(currentX.toInt(), currentY.toInt())) + } + } + }.filterNotNull() + /** @return tile at hex coordinates ([x],[y]) or null if they are outside the map. Respects map edges and world wrap. */ fun getIfTileExistsOrNull(x: Int, y: Int): TileInfo? { if (contains(x, y)) diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt index 649f62ec8e..3649251ac4 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt @@ -3,17 +3,15 @@ package com.unciv.logic.map.mapgenerator import com.unciv.Constants import com.unciv.UncivGame import com.unciv.logic.HexMath +import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.* import com.unciv.models.Counter import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.tile.Terrain import com.unciv.models.ruleset.tile.TerrainType +import kotlin.math.* import com.unciv.models.ruleset.unique.UniqueType -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.pow -import kotlin.math.sign import kotlin.random.Random @@ -26,7 +24,7 @@ class MapGenerator(val ruleset: Ruleset) { private var randomness = MapGenerationRandomness() - fun generateMap(mapParameters: MapParameters): TileMap { + fun generateMap(mapParameters: MapParameters, civilizations: List = emptyList()): TileMap { val mapSize = mapParameters.mapSize val mapType = mapParameters.type @@ -77,12 +75,19 @@ class MapGenerator(val ruleset: Ruleset) { runAndMeasure("assignContinents") { map.assignContinents(TileMap.AssignContinentsMode.Assign) } - runAndMeasure("NaturalWonderGenerator") { - NaturalWonderGenerator(ruleset, randomness).spawnNaturalWonders(map) - } runAndMeasure("RiverGenerator") { RiverGenerator(map, randomness).spawnRivers() } + val regions = MapRegions(ruleset) + runAndMeasure("generateRegions") { + regions.generateRegions(map, civilizations.count { ruleset.nations[it.civName]!!.isMajorCiv() }) + } + runAndMeasure("assignRegions") { + regions.assignRegions(map, civilizations.filter { ruleset.nations[it.civName]!!.isMajorCiv() }) + } + runAndMeasure("NaturalWonderGenerator") { + NaturalWonderGenerator(ruleset, randomness).spawnNaturalWonders(map) + } runAndMeasure("spreadResources") { spreadResources(map) } diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapRegions.kt b/core/src/com/unciv/logic/map/mapgenerator/MapRegions.kt new file mode 100644 index 0000000000..fe0303c2df --- /dev/null +++ b/core/src/com/unciv/logic/map/mapgenerator/MapRegions.kt @@ -0,0 +1,679 @@ +package com.unciv.logic.map.mapgenerator + +import com.badlogic.gdx.math.Rectangle +import com.badlogic.gdx.math.Vector2 +import com.unciv.logic.HexMath +import com.unciv.logic.civilization.CivilizationInfo +import com.unciv.logic.map.MapShape +import com.unciv.logic.map.TileInfo +import com.unciv.logic.map.TileMap +import com.unciv.models.ruleset.Ruleset +import com.unciv.models.ruleset.tile.TerrainType +import com.unciv.models.ruleset.unique.StateForConditionals +import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.models.translations.equalsPlaceholderText +import com.unciv.models.translations.getPlaceholderParameters +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt + +class MapRegions (val ruleset: Ruleset){ + companion object { + val minimumFoodForRing = mapOf(1 to 1, 2 to 4, 3 to 4) + val minimumProdForRing = mapOf(1 to 0, 2 to 0, 3 to 2) + val minimumGoodForRing = mapOf(1 to 3, 2 to 6, 3 to 8) + const val maximumJunk = 9 + + val firstRingFoodScores = listOf(0, 8, 14, 19, 22, 24, 25) + val firstRingProdScores = listOf(0, 10, 16, 20, 20, 12, 0) + val secondRingFoodScores = listOf(0, 2, 5, 10, 20, 25, 28, 30, 32, 34, 35) + val secondRingProdScores = listOf(0, 10, 20, 25, 30, 35) + + val closeStartPenaltyForRing = + mapOf( 0 to 99, 1 to 97, 2 to 95, + 3 to 92, 4 to 89, 5 to 69, + 6 to 57, 7 to 24, 8 to 15 ) + } + + private val regions = ArrayList() + private val tileData = HashMap() + + /** Creates [numRegions] number of balanced regions for civ starting locations. */ + fun generateRegions(tileMap: TileMap, numRegions: Int) { + if (numRegions <= 0) return // Don't bother about regions, probably map editor + if (tileMap.continentSizes.isEmpty()) throw Exception("No Continents on this map!") + val totalLand = tileMap.continentSizes.values.sum().toFloat() + val largestContinent = tileMap.continentSizes.values.maxOf { it }.toFloat() + + val radius = if (tileMap.mapParameters.shape == MapShape.hexagonal) + tileMap.mapParameters.mapSize.radius.toFloat() + else + (max(tileMap.mapParameters.mapSize.width / 2, tileMap.mapParameters.mapSize.height / 2)).toFloat() + // A huge box including the entire map. + val mapRect = Rectangle(-radius, -radius, radius * 2 + 1, radius * 2 + 1) + + // Lots of small islands - just split ut the map in rectangles while ignoring Continents + // 25% is chosen as limit so Four Corners maps don't fall in this category + if (largestContinent / totalLand < 0.25f) { + // Make a huge rectangle covering the entire map + val hugeRect = Region(tileMap, mapRect, -1) // -1 meaning ignore continent data + hugeRect.affectedByWorldWrap = false // Might as well start at the seam + hugeRect.updateTiles() + divideRegion(hugeRect, numRegions) + return + } + // Continents type - distribute civs according to total fertility, then split as needed + val continents = tileMap.continentSizes.keys.toMutableList() + val civsAddedToContinent = HashMap() // Continent ID, civs added + val continentFertility = HashMap() // Continent ID, total fertility + // Keep track of the even-q columns each continent is at, to figure out if they wrap + val continentIsAtCol = HashMap>() + + // Calculate continent fertilities and columns + for (tile in tileMap.values) { + val continent = tile.getContinent() + if (continent != -1) { + continentFertility[continent] = tile.getTileFertility(true) + + (continentFertility[continent] ?: 0) + + if (continentIsAtCol[continent] == null) + continentIsAtCol[continent] = HashSet() + continentIsAtCol[continent]!!.add(HexMath.hex2EvenQCoords(tile.position).x.toInt()) + } + } + + // Assign regions to the best continents, giving half value for region #2 etc + for (regionToAssign in 1..numRegions) { + val bestContinent = continents + .maxByOrNull { continentFertility[it]!! / (1 + (civsAddedToContinent[it] ?: 0)) }!! + civsAddedToContinent[bestContinent] = (civsAddedToContinent[bestContinent] ?: 0) + 1 + } + + // Split up the continents + for (continent in civsAddedToContinent.keys) { + val continentRegion = Region(tileMap, Rectangle(mapRect), continent) + val cols = continentIsAtCol[continent]!! + // Set origin at the rightmost column which does not have a neighbor on the left + continentRegion.rect.x = cols.filter { !cols.contains(it - 1) }.maxOf { it }.toFloat() + continentRegion.rect.width = cols.count().toFloat() + if (tileMap.mapParameters.worldWrap) { + // Check if the continent is wrapping - if the leftmost col is not the one we set origin by + if (cols.minOf { it } < continentRegion.rect.x) + continentRegion.affectedByWorldWrap = true + } + continentRegion.updateTiles() + divideRegion(continentRegion, civsAddedToContinent[continent]!!) + } + } + + /** Recursive function, divides a region into [numDivisions] pars of equal-ish fertility */ + private fun divideRegion(region: Region, numDivisions: Int) { + if (numDivisions <= 1) { + // We're all set, save the region and return + regions.add(region) + return + } + + val firstDivisions = numDivisions / 2 // Since int division rounds down, works for all numbers + val splitRegions = splitRegion(region, (100 * firstDivisions) / numDivisions) + divideRegion(splitRegions.first, firstDivisions) + divideRegion(splitRegions.second, numDivisions - firstDivisions) + } + + /** Splits a region in 2, with the first having [firstPercent] of total fertility */ + private fun splitRegion(regionToSplit: Region, firstPercent: Int): Pair { + val targetFertility = (regionToSplit.totalFertility * firstPercent) / 100 + + val splitOffRegion = Region(regionToSplit.tileMap, Rectangle(regionToSplit.rect), regionToSplit.continentID) + + val widerThanTall = regionToSplit.rect.width > regionToSplit.rect.height + + var bestSplitPoint = 1 // will be the size of the split-off region + var closestFertility = 0 + var cumulativeFertility = 0 + val pointsToTry = if (widerThanTall) 1..regionToSplit.rect.width.toInt() + else 1..regionToSplit.rect.height.toInt() + + for (splitPoint in pointsToTry) { + val nextRect = if (widerThanTall) + splitOffRegion.tileMap.getTilesInRectangle(Rectangle( + splitOffRegion.rect.x + splitPoint - 1, splitOffRegion.rect.y, + 1f, splitOffRegion.rect.height), + evenQ = true) + else + splitOffRegion.tileMap.getTilesInRectangle(Rectangle( + splitOffRegion.rect.x, splitOffRegion.rect.y + splitPoint - 1, + splitOffRegion.rect.width, 1f), + evenQ = true) + + cumulativeFertility += if (splitOffRegion.continentID == -1) + nextRect.sumOf { it.getTileFertility(false) } + else + nextRect.sumOf { if (it.getContinent() == splitOffRegion.continentID) it.getTileFertility(true) else 0 } + + // Better than last try? + if (abs(cumulativeFertility - targetFertility) <= abs(closestFertility - targetFertility)) { + bestSplitPoint = splitPoint + closestFertility = cumulativeFertility + } + } + + if (widerThanTall) { + splitOffRegion.rect.width = bestSplitPoint.toFloat() + regionToSplit.rect.x = splitOffRegion.rect.x + splitOffRegion.rect.width + regionToSplit.rect.width = regionToSplit.rect.width- bestSplitPoint + } else { + splitOffRegion.rect.height = bestSplitPoint.toFloat() + regionToSplit.rect.y = splitOffRegion.rect.y + splitOffRegion.rect.height + regionToSplit.rect.height = regionToSplit.rect.height - bestSplitPoint + } + splitOffRegion.updateTiles() + regionToSplit.updateTiles() + + return Pair(splitOffRegion, regionToSplit) + } + + fun assignRegions(tileMap: TileMap, civilizations: List) { + if (civilizations.isEmpty()) return + + // first assign region types + val regionTypes = ruleset.terrains.values.filter { it.hasUnique(UniqueType.RegionRequirePercentSingleType) || + it.hasUnique(UniqueType.RegionRequirePercentTwoTypes) } + .sortedBy { if (it.hasUnique(UniqueType.RegionRequirePercentSingleType)) + it.getMatchingUniques(UniqueType.RegionRequirePercentSingleType).first().params[2].toInt() + else + it.getMatchingUniques(UniqueType.RegionRequirePercentTwoTypes).first().params[3].toInt() } + + for (region in regions) { + region.countTerrains() + + for (type in regionTypes) { + // Test exclusion criteria first + if (type.getMatchingUniques(UniqueType.RegionRequireFirstLessThanSecond).any { + region.getTerrainAmount(it.params[0]) >= region.getTerrainAmount(it.params[1]) } ) { + continue + } + // Test inclusion criteria + if (type.getMatchingUniques(UniqueType.RegionRequirePercentSingleType).any { + region.getTerrainAmount(it.params[1]) >= (it.params[0].toInt() * region.tiles.count()) / 100 } + || type.getMatchingUniques(UniqueType.RegionRequirePercentTwoTypes).any { + region.getTerrainAmount(it.params[1]) + region.getTerrainAmount(it.params[2]) >= (it.params[0].toInt() * region.tiles.count()) / 100 } + ) { + region.type = type.name + break + } + } + } + + // Generate tile data for all tiles + for (tile in tileMap.values) { + val newData = MapGenTileData(tile, regions.firstOrNull { it.tiles.contains(tile) }) + newData.evaluate(ruleset) + tileData[tile.position] = newData + } + + // Sort regions by fertility so the worse regions get to pick first + val sortedRegions = regions.sortedBy { it.totalFertility } + // Find a start for each region + for (region in sortedRegions) { + findStart(region) + } + + val coastBiasCivs = civilizations.filter { ruleset.nations[it.civName]!!.startBias.contains("Coast") } + val negativeBiasCivs = civilizations.filter { ruleset.nations[it.civName]!!.startBias.any { bias -> bias.equalsPlaceholderText("Avoid []") } } + .sortedByDescending { ruleset.nations[it.civName]!!.startBias.count() } // Civs with more complex avoids go first + val randomCivs = civilizations.filter { ruleset.nations[it.civName]!!.startBias.isEmpty() }.toMutableList() // We might fill this up as we go + // The rest are positive bias + val positiveBiasCivs = civilizations.filterNot { it in coastBiasCivs || it in negativeBiasCivs || it in randomCivs } + .sortedBy { ruleset.nations[it.civName]!!.startBias.count() } // civs with only one desired region go first + val positiveBiasFallbackCivs = ArrayList() // Civs who couln't get their desired region at first pass + + // First assign coast bias civs + for (civ in coastBiasCivs) { + // Try to find a coastal start, preferably a really coastal one + var startRegion = regions.filter { tileMap[it.startPosition!!].isCoastalTile() } + .maxByOrNull { it.terrainCounts["Coastal"] ?: 0 } + if (startRegion != null) { + assignCivToRegion(civ, startRegion) + continue + } + // Else adjacent to a lake + startRegion = regions.filter { tileMap[it.startPosition!!].neighbors.any { neighbor -> neighbor.getBaseTerrain().hasUnique(UniqueType.FreshWater) } } + .maxByOrNull { it.terrainCounts["Coastal"] ?: 0 } + if (startRegion != null) { + assignCivToRegion(civ, startRegion) + continue + } + // Else adjacent to a river + startRegion = regions.filter { tileMap[it.startPosition!!].isAdjacentToRiver() } + .maxByOrNull { it.terrainCounts["Coastal"] ?: 0 } + if (startRegion != null) { + assignCivToRegion(civ, startRegion) + continue + } + // Else at least close to a river ???? + startRegion = regions.filter { tileMap[it.startPosition!!].neighbors.any { neighbor -> neighbor.isAdjacentToRiver() } } + .maxByOrNull { it.terrainCounts["Coastal"] ?: 0 } + if (startRegion != null) { + assignCivToRegion(civ, startRegion) + continue + } + // Else pick a random region at the end + randomCivs.add(civ) + } + + // Next do positive bias civs + for (civ in positiveBiasCivs) { + // Try to find a start that matches any of the desired regions, ideally with lots of desired terrain + val preferred = ruleset.nations[civ.civName]!!.startBias + val startRegion = regions.filter { it.type in preferred } + .maxByOrNull { it.terrainCounts.filterKeys { terrain -> terrain in preferred }.values.sum() } + if (startRegion != null) { + assignCivToRegion(civ, startRegion) + continue + } else if (ruleset.nations[civ.civName]!!.startBias.count() == 1) { // Civs with a single bias (only) get to look for a fallback region + positiveBiasFallbackCivs.add(civ) + } else { // Others get random starts + randomCivs.add(civ) + } + } + + // Do a second pass for fallback civs, choosing the region most similar to the desired type + for (civ in positiveBiasFallbackCivs) { + assignCivToRegion(civ, getFallbackRegion(ruleset.nations[civ.civName]!!.startBias.first())) + } + + // Next do negative bias ones (ie "Avoid []") + for (civ in negativeBiasCivs) { + val avoided = ruleset.nations[civ.civName]!!.startBias.map { it.getPlaceholderParameters()[0] } + // Try to find a region not of the avoided types, secondary sort by least number of undesired terrains + val startRegion = regions.filterNot { it.type in avoided } + .minByOrNull { it.terrainCounts.filterKeys { terrain -> terrain in avoided }.values.sum() } + if (startRegion != null) { + assignCivToRegion(civ, startRegion) + continue + } else + randomCivs.add(civ) // else pick a random region at the end + } + + // Finally assign the remaining civs randomly + for (civ in randomCivs) { + val startRegion = regions.random() + assignCivToRegion(civ, startRegion) + } + } + + private fun assignCivToRegion(civInfo: CivilizationInfo, region: Region) { + region.tileMap.addStartingLocation(civInfo.civName, region.tileMap[region.startPosition!!]) + regions.remove(region) // This region can no longer be picked + } + + /** Attempts to find a good start close to the center of [region]. Calls setRegionStart with the position*/ + private fun findStart(region: Region) { + // Establish center bias rects + val centerRect = getCentralRectangle(region.rect, 0.33f) + val middleRect = getCentralRectangle(region.rect, 0.67f) + + // Priority: 1. Adjacent to river, 2. Adjacent to coast or fresh water, 3. Other. + // First check center rect, then middle. Only check the outer area if no good sites found + val riverTiles = HashSet() + val wetTiles = HashSet() + val dryTiles = HashSet() + val fallbackTiles = HashSet() + + // First check center + val centerTiles = region.tileMap.getTilesInRectangle(centerRect, evenQ = true) + for (tile in centerTiles) { + if (tileData[tile.position]!!.isTwoFromCoast) + continue // Don't even consider tiles two from coast + if (region.continentID != -1 && region.continentID != tile.getContinent()) + continue // Wrong continent + if (tile.isLand && !tile.isImpassible()) { + evaluateTileForStart(tile) + if (tile.isAdjacentToRiver()) + riverTiles.add(tile.position) + else if (tile.isCoastalTile() || tile.isAdjacentToFreshwater) + wetTiles.add(tile.position) + else + dryTiles.add(tile.position) + } + } + // Did we find a good start position? + for (list in sequenceOf(riverTiles, wetTiles, dryTiles)) { + if (list.any { tileData[it]!!.isGoodStart }) { + setRegionStart(region, list + .filter { tileData[it]!!.isGoodStart }.maxByOrNull { tileData[it]!!.startScore }!!) + return + } + if (list.isNotEmpty()) // Save the best not-good-enough spots for later fallback + fallbackTiles.add(list.maxByOrNull { tileData[it]!!.startScore }!!) + } + + // Now check middle donut + val middleDonut = region.tileMap.getTilesInRectangle(middleRect, evenQ = true).filterNot { it in centerTiles } + riverTiles.clear() + wetTiles.clear() + dryTiles.clear() + for (tile in middleDonut) { + if (tileData[tile.position]!!.isTwoFromCoast) + continue // Don't even consider tiles two from coast + if (region.continentID != -1 && region.continentID != tile.getContinent()) + continue // Wrong continent + if (tile.isLand && !tile.isImpassible()) { + evaluateTileForStart(tile) + if (tile.isAdjacentToRiver()) + riverTiles.add(tile.position) + else if (tile.isCoastalTile() || tile.isAdjacentToFreshwater) + wetTiles.add(tile.position) + else + dryTiles.add(tile.position) + } + } + // Did we find a good start position? + for (list in sequenceOf(riverTiles, wetTiles, dryTiles)) { + if (list.any { tileData[it]!!.isGoodStart }) { + setRegionStart(region, list + .filter { tileData[it]!!.isGoodStart }.maxByOrNull { tileData[it]!!.startScore }!!) + return + } + if (list.isNotEmpty()) // Save the best not-good-enough spots for later fallback + fallbackTiles.add(list.maxByOrNull { tileData[it]!!.startScore }!!) + } + + // Now check the outer tiles. For these we don't care about rivers, coasts etc + val outerDonut = region.tileMap.getTilesInRectangle(region.rect, evenQ = true).filterNot { it in centerTiles || it in middleDonut} + dryTiles.clear() + for (tile in outerDonut) { + if (region.continentID != -1 && region.continentID != tile.getContinent()) + continue // Wrong continent + if (tile.isLand && !tile.isImpassible()) { + evaluateTileForStart(tile) + dryTiles.add(tile.position) + } + } + // Were any of them good? + if (dryTiles.any { tileData[it]!!.isGoodStart }) { + // Find the one closest to the center + val center = region.rect.getCenter(Vector2()) + setRegionStart(region, + dryTiles.filter { tileData[it]!!.isGoodStart }.minByOrNull { + (region.tileMap.getIfTileExistsOrNull(center.x.roundToInt(), center.y.roundToInt()) ?: region.tileMap.values.first()) + .aerialDistanceTo( + region.tileMap.getIfTileExistsOrNull(it.x.toInt(), it.y.toInt()) ?: region.tileMap.values.first() + ) }!!) + return + } + if (dryTiles.isNotEmpty()) + fallbackTiles.add(dryTiles.maxByOrNull { tileData[it]!!.startScore }!!) + + // Fallback time. Just pick the one with best score + val fallbackPosition = fallbackTiles.maxByOrNull { tileData[it]!!.startScore } + if (fallbackPosition != null) { + setRegionStart(region, fallbackPosition) + return + } + + // Something went extremely wrong and there is somehow no place to start. Spawn some land and start there + val panicPosition = region.rect.getPosition(Vector2()) + val panicTerrain = ruleset.terrains.values.first { it.type == TerrainType.Land }.name + region.tileMap[panicPosition].baseTerrain = panicTerrain + region.tileMap[panicPosition].terrainFeatures.clear() + setRegionStart(region, panicPosition) + } + + /** @returns the region most similar to a region of [type] */ + private fun getFallbackRegion(type: String): Region { + return regions.maxByOrNull { it.terrainCounts[type] ?: 0 }!! + } + + private fun setRegionStart(region: Region, position: Vector2) { + region.startPosition = position + setCloseStartPenalty(region.tileMap[position]) + } + + /** @returns a scaled according to [proportion] Rectangle centered over [originalRect] */ + private fun getCentralRectangle(originalRect: Rectangle, proportion: Float): Rectangle { + val scaledRect = Rectangle(originalRect) + + scaledRect.width = (originalRect.width * proportion) + scaledRect.height = (originalRect.height * proportion) + scaledRect.x = originalRect.x + (originalRect.width - scaledRect.width) / 2 + scaledRect.y = originalRect.y + (originalRect.height - scaledRect.height) / 2 + + // round values + scaledRect.x = scaledRect.x.roundToInt().toFloat() + scaledRect.y = scaledRect.y.roundToInt().toFloat() + scaledRect.width = scaledRect.width.roundToInt().toFloat() + scaledRect.height = scaledRect.height.roundToInt().toFloat() + + return scaledRect + } + + private fun setCloseStartPenalty(tile: TileInfo) { + for ((ring, penalty) in closeStartPenaltyForRing) { + for (outerTile in tile.getTilesAtDistance(ring).map { it.position }) + tileData[outerTile]!!.addCloseStartPenalty(penalty) + } + } + + /** Evaluates a tile for starting position, setting isGoodStart and startScore in + * MapGenTileData. Assumes that all tiles have corresponding MapGenTileData. */ + private fun evaluateTileForStart(tile: TileInfo) { + val localData = tileData[tile.position]!! + + var totalFood = 0 + var totalProd = 0 + var totalGood = 0 + var totalJunk = 0 + var totalRivers = 0 + var totalScore = 0 + + if (tile.isCoastalTile()) totalScore += 40 + + // Go through all rings + for (ring in 1..3) { + // Sum up the values for this ring + for (outerTile in tile.getTilesAtDistance(ring)) { + val outerTileData = tileData[outerTile.position]!! + if (outerTileData.isJunk) + totalJunk++ + else { + if (outerTileData.isFood) totalFood++ + if (outerTileData.isProd) totalProd++ + if (outerTileData.isGood) totalGood++ + if (outerTile.isAdjacentToRiver()) totalRivers++ + } + } + // Check for minimum levels. We still keep on calculating final score in case of failure + if (totalFood < minimumFoodForRing[ring]!! + || totalProd < minimumProdForRing[ring]!! + || totalGood < minimumGoodForRing[ring]!!) { + localData.isGoodStart = false + } + + // Ring-specific scoring + when (ring) { + 1 -> { + val foodScore = firstRingFoodScores[totalFood] + val prodScore = firstRingProdScores[totalProd] + totalScore += foodScore + prodScore + totalRivers + + (totalGood * 2) - (totalJunk * 3) + } + 2 -> { + val foodScore = if (totalFood > 10) secondRingFoodScores.last() + else secondRingFoodScores[totalFood] + val effectiveTotalProd = if (totalProd >= totalFood * 2) totalProd + else (totalFood + 1) / 2 // Can't use all that production without food + val prodScore = if (effectiveTotalProd > 5) secondRingProdScores.last() + else secondRingProdScores[effectiveTotalProd] + totalScore += foodScore + prodScore + totalRivers + + (totalGood * 2) - (totalJunk * 3) + } + else -> { + totalScore += totalFood + totalProd + totalGood + totalRivers - (totalJunk * 2) + } + } + } + // Too much junk? + if (totalJunk > maximumJunk) { + localData.isGoodStart = false + } + + // Finally check if this is near another start + if (localData.closeStartPenalty > 0) { + localData.isGoodStart = false + totalScore -= (totalScore * localData.closeStartPenalty) / 100 + } + localData.startScore = totalScore + } + + // Holds a bunch of tile info that is only interesting during map gen + class MapGenTileData(val tile: TileInfo, val region: Region?) { + var closeStartPenalty = 0 + var isFood = false + var isProd = false + var isGood = false + var isJunk = false + var isTwoFromCoast = false + + var isGoodStart = true + var startScore = 0 + + fun addCloseStartPenalty(penalty: Int) { + if (closeStartPenalty == 0) + closeStartPenalty = penalty + else { + // Multiple overlapping values - take the higher one and add 20 % + closeStartPenalty = max(closeStartPenalty, penalty) + closeStartPenalty = min(97, (closeStartPenalty * 1.2f).toInt()) + } + } + + fun evaluate(ruleset: Ruleset) { + // Check if we are two tiles from coast (a bad starting site) + if (!tile.isCoastalTile() && tile.neighbors.any { it.isCoastalTile() }) + isTwoFromCoast = true + + // Check first available out of unbuildable features, then other features, then base terrain + val terrainToCheck = if (tile.terrainFeatures.isEmpty()) tile.getBaseTerrain() + else tile.getTerrainFeatures().firstOrNull { it.unbuildable } + ?: tile.getTerrainFeatures().first() + + // Add all applicable qualities + for (unique in terrainToCheck.getMatchingUniques(UniqueType.HasQuality, StateForConditionals(region = region))) { + when (unique.params[0]) { + "Food" -> isFood = true + "Desirable" -> isGood = true + "Production" -> isProd = true + "Undesirable" -> isJunk = true + } + } + + // Were there in fact no explicit qualities defined for any region at all? If so let's guess at qualities to preserve mod compatibility. + if (terrainToCheck.uniqueObjects.none { it.type == UniqueType.HasQuality }) { + if (tile.isWater) return // Most water type tiles have no qualities + + // is it junk??? + if (terrainToCheck.impassable) { + isJunk = true + return // Don't bother checking the rest, junk is junk + } + + // Take possible improvements into account + val improvements = ruleset.tileImprovements.values.filter { + terrainToCheck.name in it.terrainsCanBeBuiltOn && + it.uniqueTo == null && + !it.hasUnique(UniqueType.GreatImprovement) + } + + val maxFood = terrainToCheck.food + (improvements.maxOfOrNull { it.food } ?: 0f) + val maxProd = terrainToCheck.production + (improvements.maxOfOrNull { it.production } ?: 0f) + val bestImprovementValue = improvements.maxOfOrNull { it.food + it.production + it.gold + it.culture + it.science + it.faith } ?: 0f + val maxOverall = terrainToCheck.food + terrainToCheck.production + terrainToCheck.gold + + terrainToCheck.culture + terrainToCheck.science + terrainToCheck.faith + bestImprovementValue + + if (maxFood >= 2) isFood = true + if (maxProd >= 2) isProd = true + if (maxOverall >= 3) isGood = true + } + } + } +} + +class Region (val tileMap: TileMap, val rect: Rectangle, val continentID: Int = -1) { + val tiles = HashSet() + val terrainCounts = HashMap() + var totalFertility = 0 + var type = "Hybrid" // being an undefined or indeterminate type + var startPosition: Vector2? = null + + var affectedByWorldWrap = false + + /** Recalculates tiles and fertility */ + fun updateTiles(trim: Boolean = true) { + totalFertility = 0 + var minX = 99999f + var maxX = -99999f + var minY = 99999f + var maxY = -99999f + + val columnHasTile = HashSet() + + tiles.clear() + for (tile in tileMap.getTilesInRectangle(rect, evenQ = true).filter { + continentID == -1 || it.getContinent() == continentID } ) { + val fertility = tile.getTileFertility(continentID != -1) + if (fertility != 0) { // If fertility is 0 this is candidate for trimming + tiles.add(tile) + totalFertility += fertility + } + + if (affectedByWorldWrap) + columnHasTile.add(HexMath.hex2EvenQCoords(tile.position).x.toInt()) + + if (trim) { + val evenQCoords = HexMath.hex2EvenQCoords(tile.position) + minX = min(minX, evenQCoords.x) + maxX = max(maxX, evenQCoords.x) + minY = min(minY, evenQCoords.y) + maxY = max(maxY, evenQCoords.y) + } + } + + if (trim) { + if (affectedByWorldWrap) // Need to be more thorough with origin longitude + rect.x = columnHasTile.filter { !columnHasTile.contains(it - 1) }.maxOf { it }.toFloat() + else + rect.x = minX // ez way for non-wrapping regions + rect.y = minY + rect.height = maxY - minY + 1 + if (affectedByWorldWrap && minX < rect.x) { // Thorough way + rect.width = columnHasTile.count().toFloat() + } else { + rect.width = maxX - minX + 1 // ez way + affectedByWorldWrap = false // also we're not wrapping anymore + } + } + } + + /** Counts the terrains in the Region for type and start determination */ + fun countTerrains() { + // Count terrains in the region + terrainCounts.clear() + for (tile in tiles) { + val terrainsToCount = if (tile.getAllTerrains().any { it.hasUnique(UniqueType.IgnoreBaseTerrainForRegion) }) + tile.getTerrainFeatures().map { it.name }.asSequence() + else + tile.getAllTerrains().map { it.name } + for (terrain in terrainsToCount) { + terrainCounts[terrain] = (terrainCounts[terrain] ?: 0) + 1 + } + if (tile.isCoastalTile()) + terrainCounts["Coastal"] = (terrainCounts["Coastal"] ?: 0) + 1 + } + } + + /** Returns number terrains with [name] */ + fun getTerrainAmount(name: String) = terrainCounts[name] ?: 0 +} \ No newline at end of file diff --git a/core/src/com/unciv/models/ruleset/unique/StateForConditionals.kt b/core/src/com/unciv/models/ruleset/unique/StateForConditionals.kt index 4c1c12dc8a..d0a0b4cc0b 100644 --- a/core/src/com/unciv/models/ruleset/unique/StateForConditionals.kt +++ b/core/src/com/unciv/models/ruleset/unique/StateForConditionals.kt @@ -6,6 +6,7 @@ import com.unciv.logic.city.CityInfo import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.MapUnit import com.unciv.logic.map.TileInfo +import com.unciv.logic.map.mapgenerator.Region data class StateForConditionals( val civInfo: CivilizationInfo? = null, @@ -16,4 +17,6 @@ data class StateForConditionals( val theirCombatant: ICombatant? = null, val attackedTile: TileInfo? = null, val combatAction: CombatAction? = null, + + val region: Region? = null, ) \ No newline at end of file diff --git a/core/src/com/unciv/models/ruleset/unique/Unique.kt b/core/src/com/unciv/models/ruleset/unique/Unique.kt index aa8d007904..ebd3317464 100644 --- a/core/src/com/unciv/models/ruleset/unique/Unique.kt +++ b/core/src/com/unciv/models/ruleset/unique/Unique.kt @@ -105,6 +105,11 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s it.matchesFilter(condition.params[2], state.civInfo) && it.matchesFilter(condition.params[3], state.civInfo) } in (condition.params[0].toInt())..(condition.params[1].toInt()) + + UniqueType.ConditionalOnWaterMaps -> state.region?.continentID == -1 + UniqueType.ConditionalInRegionOfType -> state.region?.type == condition.params[0] + UniqueType.ConditionalInRegionExceptOfType -> state.region != null && state.region.type != condition.params[0] + else -> false } } diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt index a4353788f7..58834a7b3b 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueParameterType.kt @@ -155,6 +155,27 @@ enum class UniqueParameterType(val parameterName:String) { return UniqueType.UniqueComplianceErrorSeverity.RulesetSpecific } }, + /** Used for region definitions, can be a terrain type with region unique, or "Hybrid" */ + RegionType("regionType") { + private val knownValues = setOf("Hybrid") + override fun getErrorSeverity(parameterText: String, ruleset: Ruleset): + UniqueType.UniqueComplianceErrorSeverity? { + if (parameterText in knownValues) return null + if (ruleset.terrains[parameterText]?.hasUnique(UniqueType.RegionRequirePercentSingleType) == true || + ruleset.terrains[parameterText]?.hasUnique(UniqueType.RegionRequirePercentTwoTypes) == true) + return null + return UniqueType.UniqueComplianceErrorSeverity.RulesetSpecific + } + }, + /** Used for start placements */ + TerrainQuality("terrainQuality") { + private val knownValues = setOf("Undesirable", "Food", "Desirable", "Production") + override fun getErrorSeverity(parameterText: String, ruleset: Ruleset): + UniqueType.UniqueComplianceErrorSeverity? { + if (parameterText in knownValues) return null + return UniqueType.UniqueComplianceErrorSeverity.RulesetInvariant + } + }, Promotion("promotion") { override fun getErrorSeverity(parameterText: String, ruleset: Ruleset): UniqueType.UniqueComplianceErrorSeverity? = when (parameterText) { diff --git a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt index cd008f7c03..cb52257660 100644 --- a/core/src/com/unciv/models/ruleset/unique/UniqueType.kt +++ b/core/src/com/unciv/models/ruleset/unique/UniqueType.kt @@ -306,11 +306,28 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget, val flags: BlocksLineOfSightAtSameElevation("Blocks line-of-sight from tiles at same elevation", UniqueTarget.Terrain), VisibilityElevation("Has an elevation of [amount] for visibility calculations", UniqueTarget.Terrain), + + OverrideFertility("Always Fertility [amount] for Map Generation", UniqueTarget.Terrain, flags = listOf(UniqueFlag.HideInCivilopedia)), + AddFertility("[amount] to Fertility for Map Generation", UniqueTarget.Terrain, flags = listOf(UniqueFlag.HideInCivilopedia)), + RegionRequirePercentSingleType("A Region is formed with at least [amount]% [simpleTerrain] tiles, with priority [amount]", UniqueTarget.Terrain, flags = listOf(UniqueFlag.HideInCivilopedia)), + RegionRequirePercentTwoTypes("A Region is formed with at least [amount]% [simpleTerrain] tiles and [simpleTerrain] tiles, with priority [amount]", + UniqueTarget.Terrain, flags = listOf(UniqueFlag.HideInCivilopedia)), + RegionRequireFirstLessThanSecond("A Region can not contain more [simpleTerrain] tiles than [simpleTerrain] tiles", UniqueTarget.Terrain, flags = listOf(UniqueFlag.HideInCivilopedia)), + IgnoreBaseTerrainForRegion("Base Terrain on this tile is not counted for Region determination", UniqueTarget.Terrain, flags = listOf(UniqueFlag.HideInCivilopedia)), + + HasQuality("Considered [terrainQuality] when determining start locations", UniqueTarget.Terrain, flags = listOf(UniqueFlag.HideInCivilopedia)), + + LuxuryWeighting("Appears in [regionType] regions with weight [amount]", UniqueTarget.Resource, flags = listOf(UniqueFlag.HideInCivilopedia)), + LuxuryWeightingForCityStates("Appears near City States with weight [amount]", UniqueTarget.Resource, flags = listOf(UniqueFlag.HideInCivilopedia)), + + OverrideDepositAmountOnTileFilter("Deposits in [tileFilter] tiles always provide [amount] resources", UniqueTarget.Resource), + NoNaturalGeneration("Doesn't generate naturally", UniqueTarget.Terrain, flags = listOf(UniqueFlag.HideInCivilopedia)), TileGenerationConditions("Occurs at temperature between [amount] and [amount] and humidity between [amount] and [amount]", UniqueTarget.Terrain, flags = listOf(UniqueFlag.HideInCivilopedia)), OccursInChains("Occurs in chains at high elevations", UniqueTarget.Terrain, flags = listOf(UniqueFlag.HideInCivilopedia)), OccursInGroups("Occurs in groups around high elevations", UniqueTarget.Terrain, flags = listOf(UniqueFlag.HideInCivilopedia)), + RareFeature("Rare feature", UniqueTarget.Terrain), ResistsNukes("Resistant to nukes", UniqueTarget.Terrain), @@ -348,8 +365,8 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget, val flags: IsAncientRuinsEquivalent("Provides a random bonus when entered", UniqueTarget.Improvement), Unpillagable("Unpillagable", UniqueTarget.Improvement), - Indestructible("Indestructible", UniqueTarget.Improvement), + Indestructible("Indestructible", UniqueTarget.Improvement), ///////////////////////////////////////// CONDITIONALS ///////////////////////////////////////// @@ -385,6 +402,11 @@ enum class UniqueType(val text:String, vararg targets: UniqueTarget, val flags: ConditionalNeighborTiles("with [amount] to [amount] neighboring [tileFilter] tiles", UniqueTarget.Conditional), ConditionalNeighborTilesAnd("with [amount] to [amount] neighboring [tileFilter] [tileFilter] tiles", UniqueTarget.Conditional), + /////// region conditionals + ConditionalOnWaterMaps("on water maps", UniqueTarget.Conditional), + ConditionalInRegionOfType("in [regionType] Regions", UniqueTarget.Conditional), + ConditionalInRegionExceptOfType("in all except [regionType] Regions", UniqueTarget.Conditional), + ///////////////////////////////////////// TRIGGERED ONE-TIME /////////////////////////////////////////