diff --git a/android/assets/jsons/Civ V - Vanilla/Terrains.json b/android/assets/jsons/Civ V - Vanilla/Terrains.json index 3da711d1c8..ca4c8bd801 100644 --- a/android/assets/jsons/Civ V - Vanilla/Terrains.json +++ b/android/assets/jsons/Civ V - Vanilla/Terrains.json @@ -62,7 +62,7 @@ "impassable": true, "defenceBonus": 0.25, "RGB": [120, 120, 120], - "uniques":["Rough terrain", "Has an elevation of [4] for visibility calculations"] + "uniques":["Rough terrain", "Has an elevation of [4] for visibility calculations", "Occurs in chains at high elevations"] }, { "name": "Snow", @@ -83,7 +83,8 @@ "RGB": [105,125,72], "occursOn": ["Tundra","Plains","Grassland","Desert","Snow"], "uniques": ["Rough terrain", "[+5] Strength for cities built on this terrain", - "[+1] Sight for [Land] units", "Has an elevation of [2] for visibility calculations"] + "[+1] Sight for [Land] units", "Has an elevation of [2] for visibility calculations", + "Occurs in groups around high elevations"] }, { "name": "Forest", diff --git a/core/src/com/unciv/Constants.kt b/core/src/com/unciv/Constants.kt index cf1b937cdd..b72b8b105f 100644 --- a/core/src/com/unciv/Constants.kt +++ b/core/src/com/unciv/Constants.kt @@ -75,4 +75,7 @@ object Constants { const val barbarians = "Barbarians" const val spectator = "Spectator" const val custom = "Custom" + + const val rising = "Rising" + const val lowering = "Lowering" } diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index b7eb061c40..9f7882992b 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -11,6 +11,7 @@ import com.unciv.models.metadata.GameParameters import com.unciv.models.ruleset.Era import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.RulesetCache +import com.unciv.models.ruleset.tile.ResourceType import com.unciv.ui.newgamescreen.GameSetupInfo import java.util.* import kotlin.NoSuchElementException @@ -180,7 +181,24 @@ object GameStarter { val startingEra = gameInfo.gameParameters.startingEra var startingUnits: MutableList var eraUnitReplacement: String - + + // First we get start locations for the major civs, on the second pass the city states (without predetermined starts) can squeeze in wherever + // I hear copying code is good + val cityStatesWithStartingLocations = + gameInfo.tileMap.values + .filter { it.improvement != null && it.improvement!!.startsWith("StartingLocation ") } + .map { it.improvement!!.replace("StartingLocation ", "") } + val bestCivs = gameInfo.civilizations.filter { !it.isBarbarian() && (!it.isCityState() || it.civName in cityStatesWithStartingLocations) } + val bestLocations = getStartingLocations(bestCivs, gameInfo.tileMap) + for (civ in bestCivs) + { + if (civ.isCityState()) // Already have explicit starting locations + continue + + // Mark the best start locations so we remember them for the second pass + bestLocations[civ]!!.improvement = "StartingLocation " + civ.civName + } + val startingLocations = getStartingLocations( gameInfo.civilizations.filter { !it.isBarbarian() }, gameInfo.tileMap) @@ -188,10 +206,18 @@ object GameStarter { val settlerLikeUnits = ruleSet.units.filter { it.value.uniqueObjects.any { it.placeholderText == Constants.settlerUnique } } - + // no starting units for Barbarians and Spectators for (civ in gameInfo.civilizations.filter { !it.isBarbarian() && !it.isSpectator() }) { val startingLocation = startingLocations[civ]!! + + if(civ.isMajorCiv() && startingLocation.getTileStartScore() < 45) { + // An unusually bad spawning location + addConsolationPrize(gameInfo, startingLocation, 45 - startingLocation.getTileStartScore().toInt()) + } + if(civ.isCityState()) + addCityStateLuxury(gameInfo, startingLocation) + for (tile in startingLocation.getTilesInDistance(3)) if (tile.improvement == Constants.ancientRuins) tile.improvement = null // Remove ancient ruins in immediate vicinity @@ -296,27 +322,36 @@ object GameStarter { val tilesWithStartingLocations = tileMap.values .filter { it.improvement != null && it.improvement!!.startsWith("StartingLocation ") } - val civsOrderedByAvailableLocations = civs.sortedBy { civ -> + + val civsOrderedByAvailableLocations = civs.shuffled() // Order should be random since it determines who gets best start + .sortedBy { civ -> when { tilesWithStartingLocations.any { it.improvement == "StartingLocation " + civ.civName } -> 1 // harshest requirements - civ.nation.startBias.isNotEmpty() -> 2 // less harsh - else -> 3 + civ.nation.startBias.contains("Tundra") -> 2 // Tundra starts are hard to find, so let's do them first + civ.nation.startBias.isNotEmpty() -> 3 // less harsh + else -> 4 } // no requirements } - for (minimumDistanceBetweenStartingLocations in tileMap.tileMatrix.size / 3 downTo 0) { + for (minimumDistanceBetweenStartingLocations in tileMap.tileMatrix.size / 4 downTo 0) { val freeTiles = landTilesInBigEnoughGroup - .filter { vectorIsAtLeastNTilesAwayFromEdge(it.position, minimumDistanceBetweenStartingLocations, tileMap) } + .filter { vectorIsAtLeastNTilesAwayFromEdge(it.position, (minimumDistanceBetweenStartingLocations * 2) /3, tileMap) } .toMutableList() val startingLocations = HashMap() - for (civ in civsOrderedByAvailableLocations) { var startingLocation: TileInfo val presetStartingLocation = tilesWithStartingLocations.firstOrNull { it.improvement == "StartingLocation " + civ.civName } + var distanceToNext = minimumDistanceBetweenStartingLocations + if (presetStartingLocation != null) startingLocation = presetStartingLocation else { if (freeTiles.isEmpty()) break // we failed to get all the starting tiles with this minimum distance + if (civ.isCityState()) + distanceToNext = minimumDistanceBetweenStartingLocations / 2 // We allow random city states to squeeze in tighter + + freeTiles.sortBy { it.getTileStartScore() } + var preferredTiles = freeTiles.toList() for (startBias in civ.nation.startBias) { @@ -327,10 +362,10 @@ object GameStarter { else preferredTiles = preferredTiles.filter { it.matchesTerrainFilter(startBias) } } - startingLocation = if (preferredTiles.isNotEmpty()) preferredTiles.random() else freeTiles.random() + startingLocation = if (preferredTiles.isNotEmpty()) preferredTiles.last() else freeTiles.last() } startingLocations[civ] = startingLocation - freeTiles.removeAll(tileMap.getTilesInDistance(startingLocation.position, minimumDistanceBetweenStartingLocations)) + freeTiles.removeAll(tileMap.getTilesInDistance(startingLocation.position, distanceToNext)) } if (startingLocations.size < civs.size) continue // let's try again with less minimum distance! @@ -339,6 +374,53 @@ object GameStarter { throw Exception("Didn't manage to get starting tiles even with distance of 1?") } + private fun addConsolationPrize(gameInfo: GameInfo, spawn: TileInfo, points: Int) { + val relevantTiles = spawn.getTilesInDistanceRange(1..2).shuffled() + var addedPoints = 0 + var addedBonuses = 0 + + for (tile in relevantTiles) { + if (addedPoints >= points || addedBonuses >= 4) // At some point enough is enough + break + if (tile.resource != null || tile.baseTerrain == Constants.snow) // Snow is quite irredeemable + continue + + val bonusToAdd = gameInfo.ruleSet.tileResources.values + .filter { it.terrainsCanBeFoundOn.contains(tile.getLastTerrain().name) && it.resourceType == ResourceType.Bonus } + .randomOrNull() + + if (bonusToAdd != null) { + tile.resource = bonusToAdd.name + addedPoints += (bonusToAdd.food + bonusToAdd.production + bonusToAdd.gold + 1).toInt() // +1 because resources can be improved + addedBonuses++ + } + } + } + + private fun addCityStateLuxury(gameInfo: GameInfo, spawn: TileInfo) { + // Every city state should have at least one luxury to trade + val relevantTiles = spawn.getTilesInDistance(2).shuffled() + + for (tile in relevantTiles) { + if(tile.resource != null && tile.getTileResource().resourceType == ResourceType.Luxury) + return // At least one luxury; all set + } + + for (tile in relevantTiles) { + // Add a luxury to the first eligible tile + if (tile.resource != null) + continue + + val luxuryToAdd = gameInfo.ruleSet.tileResources.values + .filter { it.terrainsCanBeFoundOn.contains(tile.getLastTerrain().name) && it.resourceType == ResourceType.Luxury } + .randomOrNull() + if (luxuryToAdd != null) { + tile.resource = luxuryToAdd.name + return + } + } + } + private fun vectorIsAtLeastNTilesAwayFromEdge(vector: Vector2, n: Int, tileMap: TileMap): Boolean { // Since all maps are HEXAGONAL, the easiest way of checking if a tile is n steps away from the // edge is checking the distance to the CENTER POINT diff --git a/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt b/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt index 877ad6ffc3..5e2fa8bb74 100644 --- a/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt @@ -146,6 +146,14 @@ object SpecificUnitAutomation { } fun automateSettlerActions(unit: MapUnit) { + if (unit.civInfo.gameInfo.turns == 0) { // Special case, we want AI to settle in place on turn 1. + val foundCityAction = UnitActions.getFoundCityAction(unit, unit.getTile()) + if(foundCityAction?.action != null) { + foundCityAction.action.invoke() + return + } + } + if (unit.getTile().militaryUnit == null) return // Don't move until you're accompanied by a military unit val tilesNearCities = unit.civInfo.gameInfo.getCities().asSequence() diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index 14b9c2320c..a6c537e431 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -278,6 +278,41 @@ open class TileInfo { return stats } + fun getTileStartScore(): Float { + var sum = 0f + for (tile in getTilesInDistance(2)) { + if (tile == this) + continue + sum += tile.getTileStartYield() + if (tile in neighbors) + sum += tile.getTileStartYield() + } + + if (isHill()) + sum -= 2 + if (isAdjacentToRiver()) + sum += 2 + if (neighbors.any { it.baseTerrain == Constants.mountain }) + sum += 2 + + return sum + } + + private fun getTileStartYield(): Float { + var stats = getBaseTerrain().clone() + + for (terrainFeatureBase in getTerrainFeatures()) { + if (terrainFeatureBase.overrideStats) + stats = terrainFeatureBase.clone() + else + stats.add(terrainFeatureBase) + } + if (resource != null) stats.add(getTileResource()) + if (stats.production < 0) stats.production = 0f + + return stats.food + stats.production + stats.gold + } + fun getImprovementStats(improvement: TileImprovement, observingCiv: CivilizationInfo, city: CityInfo?): Stats { val stats = improvement.clone() // clones the stats of the improvement, not the improvement itself if (hasViewableResource(observingCiv) && getTileResource().improvement == improvement.name) diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt index 957d559a15..519e2ebb7d 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt @@ -8,10 +8,7 @@ import com.unciv.models.Counter import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.tile.TerrainType -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.pow -import kotlin.math.sign +import kotlin.math.* import kotlin.random.Random @@ -179,6 +176,20 @@ class MapGenerator(val ruleset: Ruleset) { * [MapParameters.elevationExponent] favors high elevation */ private fun raiseMountainsAndHills(tileMap: TileMap) { + val mountain = ruleset.terrains.values.filter { it.uniques.contains("Occurs in chains at high elevations") }.firstOrNull()?.name + val hill = ruleset.terrains.values.filter { it.uniques.contains("Occurs in groups around high elevations") }.firstOrNull()?.name + val flat = ruleset.terrains.values.filter { !it.impassable && it.type == TerrainType.Land && !it.uniques.contains("Rough Terrain") }.firstOrNull()?.name + + if (flat == null) { + println("Ruleset seems to contain no flat terrain - can't generate heightmap") + return + } + + if (mountain != null) + println("Mountainlike generation for " + mountain) + if (hill != null) + println("Hill-like generation for " + hill) + val elevationSeed = randomness.RNG.nextInt().toDouble() tileMap.setTransients(ruleset) for (tile in tileMap.values.filter { !it.isWater }) { @@ -186,12 +197,102 @@ class MapGenerator(val ruleset: Ruleset) { elevation = abs(elevation).pow(1.0 - tileMap.mapParameters.elevationExponent.toDouble()) * elevation.sign when { - elevation <= 0.5 -> tile.baseTerrain = Constants.plains - elevation <= 0.7 -> tile.terrainFeatures.add(Constants.hill) - elevation <= 1.0 -> tile.baseTerrain = Constants.mountain + elevation <= 0.5 -> tile.baseTerrain = flat + elevation <= 0.7 && hill != null -> tile.terrainFeatures.add(hill) + elevation <= 0.7 && hill == null -> tile.baseTerrain = flat // otherwise would be hills become mountains + elevation <= 1.0 && mountain != null -> tile.baseTerrain = mountain } tile.setTerrainTransients() } + + if (mountain != null) + cellularMountainRanges(tileMap, mountain, hill, flat) + if (hill != null) + cellularHills(tileMap, mountain, hill) + } + + private fun cellularMountainRanges(tileMap: TileMap, mountain: String, hill: String?, flat: String) { + val targetMountains = tileMap.values.count { it.baseTerrain == mountain } * 2 + + for (i in 1..5) { + var totalMountains = tileMap.values.count { it.baseTerrain == mountain } + + for (tile in tileMap.values.filter { !it.isWater }) { + val adjacentMountains = + tile.neighbors.count { it.baseTerrain == mountain } + val adjacentImpassible = + tile.neighbors.count { ruleset.terrains[it.baseTerrain]?.impassable == true } + + if (adjacentMountains == 0 && tile.baseTerrain == mountain) { + if (randomness.RNG.nextInt(until = 4) == 0) + tile.terrainFeatures.add(Constants.lowering) + } else if (adjacentMountains == 1) { + if (randomness.RNG.nextInt(until = 10) == 0) + tile.terrainFeatures.add(Constants.rising) + } else if (adjacentImpassible == 3) { + if (randomness.RNG.nextInt(until = 2) == 0) + tile.terrainFeatures.add(Constants.lowering) + } else if (adjacentImpassible > 3) { + tile.terrainFeatures.add(Constants.lowering) + } + } + + for (tile in tileMap.values.filter { !it.isWater }) { + if (tile.terrainFeatures.remove(Constants.rising) && totalMountains < targetMountains) { + if (hill != null) + tile.terrainFeatures.remove(hill) + tile.baseTerrain = mountain + totalMountains++ + } + if (tile.terrainFeatures.remove(Constants.lowering) && totalMountains > targetMountains * 0.5f) { + if (tile.baseTerrain == mountain) { + if (hill != null && !tile.terrainFeatures.contains(hill)) + tile.terrainFeatures.add(hill) + totalMountains-- + } + tile.baseTerrain = flat + } + } + } + } + + private fun cellularHills(tileMap: TileMap, mountain: String?, hill: String) { + val targetHills = tileMap.values.count { it.terrainFeatures.contains(hill) } + + for (i in 1..5) { + var totalHills = tileMap.values.count { it.terrainFeatures.contains(hill) } + + for (tile in tileMap.values.filter { !it.isWater && (mountain == null || it.baseTerrain != mountain) }) { + val adjacentMountains = if (mountain == null) 0 else + tile.neighbors.count { it.baseTerrain == mountain } + val adjacentHills = + tile.neighbors.count { it.terrainFeatures.contains(hill) } + + if (adjacentHills <= 1 && adjacentMountains == 0 && randomness.RNG.nextInt(until = 2) == 0) { + tile.terrainFeatures.add(Constants.lowering) + } else if (adjacentHills > 3 && adjacentMountains == 0 && randomness.RNG.nextInt(until = 2) == 0) { + tile.terrainFeatures.add(Constants.lowering) + } else if (adjacentHills + adjacentMountains in 2..3 && randomness.RNG.nextInt(until = 2) == 0) { + tile.terrainFeatures.add(Constants.rising) + } + + } + + for (tile in tileMap.values.filter { !it.isWater && (mountain == null || it.baseTerrain != mountain) }) { + if (tile.terrainFeatures.remove(Constants.rising) && (totalHills <= targetHills || i == 1) ) { + if (!tile.terrainFeatures.contains(hill)) { + tile.terrainFeatures.add(hill) + totalHills++ + } + } + if (tile.terrainFeatures.remove(Constants.lowering) && (totalHills >= targetHills * 0.9f || i == 1)) { + if (tile.terrainFeatures.contains(hill)) { + tile.terrainFeatures.remove(hill) + totalHills-- + } + } + } + } } /**