Regions part 1 - subdivide generated maps into regions, and use to place civs (#5556)

* json definitions

* create regions, define region types

* count terrains

* terrain qualities

* tilesInRectangle

* use even q coords

* major civ start locations

* move to separate file

* remove printlns

* unused imports

* strings

* strings

* reviews

* conditionalize qualities

* guess qualities of terrain types without explicit definitions

* guess qualities of terrain types without explicit definitions

* Update template.properties

* Update template.properties

* add HideInCivilopedia to technical uniques

* reviews
This commit is contained in:
SimonCeder 2021-11-05 06:59:48 +01:00 committed by GitHub
parent 6e6192c369
commit 09c40002f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1013 additions and 48 deletions

View File

@ -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 <on water maps>"]
},
{
"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 <in [Jungle] Regions>",
"Considered [Food] when determining start locations <in [Forest] Regions>",
"Considered [Food] when determining start locations <in [Hill] Regions>",
"Considered [Food] when determining start locations <in [Grassland] Regions>",
"Considered [Food] when determining start locations <in [Hybrid] Regions>"]
},
{
"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 <in [Tundra] Regions>",
"Considered [Food] when determining start locations <in [Desert] Regions>",
"Considered [Food] when determining start locations <in [Hill] Regions>",
"Considered [Food] when determining start locations <in [Plains] Regions>",
"Considered [Food] when determining start locations <in [Hybrid] Regions>"]
},
{
"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 <in [Tundra] Regions>",
"Considered [Desirable] when determining start locations <in [Tundra] Regions>"]
},
{
"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 <in all except [Desert] Regions>"]
},
{
"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 <in [Forest] Regions>",
"Considered [Food] when determining start locations <in [Tundra] Regions>"],
"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 <in all except [Grassland] Regions>",
"Considered [Desirable] when determining start locations <in all except [Grassland] Regions>"]
},
{
"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",

View File

@ -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]"]
}
]

View File

@ -1181,6 +1181,9 @@ Strategic resource =
Fresh water =
non-fresh water =
Natural Wonder =
Hybrid =
Undesirable =
Desirable =
# improvementFilters

View File

@ -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<String>()
// 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<String>()
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<String>()
// 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!

View File

@ -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)

View File

@ -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)

View File

@ -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<TileInfo> =
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))

View File

@ -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<CivilizationInfo> = 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)
}

View File

@ -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<Region>()
private val tileData = HashMap<Vector2, MapGenTileData>()
/** 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<Int, Int>() // Continent ID, civs added
val continentFertility = HashMap<Int, Int>() // Continent ID, total fertility
// Keep track of the even-q columns each continent is at, to figure out if they wrap
val continentIsAtCol = HashMap<Int, HashSet<Int>>()
// 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<Region, Region> {
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<CivilizationInfo>) {
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<CivilizationInfo>() // 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<Vector2>()
val wetTiles = HashSet<Vector2>()
val dryTiles = HashSet<Vector2>()
val fallbackTiles = HashSet<Vector2>()
// 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<TileInfo>()
val terrainCounts = HashMap<String, Int>()
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<Int>()
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
}

View File

@ -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,
)

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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 /////////////////////////////////////////