diff --git a/android/assets/jsons/translations/template.properties b/android/assets/jsons/translations/template.properties index e522759bca..e1c334e74e 100644 --- a/android/assets/jsons/translations/template.properties +++ b/android/assets/jsons/translations/template.properties @@ -182,6 +182,7 @@ Diplomatic Marriage ([amount] Gold) = We have married into the ruling family of [civName], bringing them under our control. = [civName] has married into the ruling family of [civName2], bringing them under their control. = You have broken your Pledge to Protect [civName]! = +City-States grow wary of your aggression. The resting point for Influence has decreased by [amount] for [civName]. = Cultured = Maritime = diff --git a/core/src/com/unciv/logic/GameStarter.kt b/core/src/com/unciv/logic/GameStarter.kt index 4596d3725d..792a532858 100644 --- a/core/src/com/unciv/logic/GameStarter.kt +++ b/core/src/com/unciv/logic/GameStarter.kt @@ -33,23 +33,29 @@ object GameStarter { // In the case where we used to have a mod, and now we don't, we cannot "unselect" it in the UI. // We need to remove the dead mods so there aren't problems later. - gameSetupInfo.gameParameters.mods.removeAll{ !RulesetCache.containsKey(it) } + gameSetupInfo.gameParameters.mods.removeAll { !RulesetCache.containsKey(it) } gameInfo.gameParameters = gameSetupInfo.gameParameters val ruleset = RulesetCache.getComplexRuleset(gameInfo.gameParameters.mods) + val mapGen = MapGenerator(ruleset) if (gameSetupInfo.mapParameters.name != "") runAndMeasure("loadMap") { tileMap = MapSaver.loadMap(gameSetupInfo.mapFile!!) // Don't override the map parameters - this can include if we world wrap or not! } else runAndMeasure("generateMap") { - tileMap = MapGenerator(ruleset).generateMap(gameSetupInfo.mapParameters) + tileMap = mapGen.generateMap(gameSetupInfo.mapParameters) tileMap.mapParameters = gameSetupInfo.mapParameters } runAndMeasure("addCivilizations") { gameInfo.tileMap = tileMap - tileMap.gameInfo = gameInfo // need to set this transient before placing units in the map - addCivilizations(gameSetupInfo.gameParameters, gameInfo, ruleset) // this is before gameInfo.setTransients, so gameInfo doesn't yet have the gameBasics + tileMap.gameInfo = + gameInfo // need to set this transient before placing units in the map + addCivilizations( + gameSetupInfo.gameParameters, + gameInfo, + ruleset + ) // this is before gameInfo.setTransients, so gameInfo doesn't yet have the gameBasics } runAndMeasure("Remove units") { @@ -78,6 +84,11 @@ object GameStarter { addCivStats(gameInfo) } + runAndMeasure("assignContinents?") { + if (tileMap.continentSizes.isEmpty()) // Probably saved map without continent data + mapGen.assignContinents(tileMap) + } + runAndMeasure("addCivStartingUnits") { // and only now do we add units for everyone, because otherwise both the gameInfo.setTransients() and the placeUnit will both add the unit to the civ's unit list! addCivStartingUnits(gameInfo) @@ -334,19 +345,23 @@ object GameStarter { } } + private fun getStartingLocations(civs: List, tileMap: TileMap, startScores: HashMap): HashMap { - var landTiles = tileMap.values + val landTilesInBigEnoughGroup = tileMap.landTilesInBigEnoughGroup + if (landTilesInBigEnoughGroup.isEmpty()) { + // Worst case - a pre-made map with continent data. This means we didn't re-run assignContinents, + // so we don't have a cached landTilesInBigEnoughGroup. So we need to do it the hard way. + var landTiles = tileMap.values // Games starting on snow might as well start over... .filter { it.isLand && !it.isImpassible() && it.baseTerrain != Constants.snow } - - val landTilesInBigEnoughGroup = ArrayList() - while (landTiles.any()) { - val bfs = BFS(landTiles.random()) { it.isLand && !it.isImpassible() } - bfs.stepToEnd() - val tilesInGroup = bfs.getReachedTiles() - landTiles = landTiles.filter { it !in tilesInGroup } - if (tilesInGroup.size > 20) // is this a good number? I dunno, but it's easy enough to change later on - landTilesInBigEnoughGroup.addAll(tilesInGroup) + while (landTiles.any()) { + val bfs = BFS(landTiles.random()) { it.isLand && !it.isImpassible() } + bfs.stepToEnd() + val tilesInGroup = bfs.getReachedTiles() + landTiles = landTiles.filter { it !in tilesInGroup } + if (tilesInGroup.size > 20) // is this a good number? I dunno, but it's easy enough to change later on + landTilesInBigEnoughGroup.addAll(tilesInGroup) + } } val civsOrderedByAvailableLocations = civs.shuffled() // Order should be random since it determines who gets best start diff --git a/core/src/com/unciv/logic/city/CityInfo.kt b/core/src/com/unciv/logic/city/CityInfo.kt index 9faf36a71e..a551fca6e7 100644 --- a/core/src/com/unciv/logic/city/CityInfo.kt +++ b/core/src/com/unciv/logic/city/CityInfo.kt @@ -3,6 +3,7 @@ package com.unciv.logic.city import com.badlogic.gdx.math.Vector2 import com.unciv.logic.battle.CityCombatant import com.unciv.logic.civilization.CivilizationInfo +import com.unciv.logic.civilization.Proximity import com.unciv.logic.civilization.ReligionState import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.map.RoadStatus @@ -129,6 +130,18 @@ class CityInfo { population.autoAssignPopulation() cityStats.update() + // Update proximity rankings for all civs + for (otherCiv in civInfo.gameInfo.getAliveMajorCivs()) { + if (civInfo.getProximity(otherCiv) != Proximity.Neighbors) // unless already neighbors + civInfo.updateProximity(otherCiv, + otherCiv.updateProximity(civInfo)) + } + for (otherCiv in civInfo.gameInfo.getAliveCityStates()) { + if (civInfo.getProximity(otherCiv) != Proximity.Neighbors) // unless already neighbors + civInfo.updateProximity(otherCiv, + otherCiv.updateProximity(civInfo)) + } + triggerCitiesSettledNearOtherCiv() } @@ -556,6 +569,16 @@ class CityInfo { if (isCapital() && civInfo.cities.isNotEmpty()) { // Move the capital if destroyed (by a nuke or by razing) civInfo.cities.first().cityConstructions.addBuilding(capitalCityIndicator()) } + + // Update proximity rankings for all civs + for (otherCiv in civInfo.gameInfo.getAliveMajorCivs()) { + civInfo.updateProximity(otherCiv, + otherCiv.updateProximity(civInfo)) + } + for (otherCiv in civInfo.gameInfo.getAliveCityStates()) { + civInfo.updateProximity(otherCiv, + otherCiv.updateProximity(civInfo)) + } } fun annexCity() = CityInfoConquestFunctions(this).annexCity() diff --git a/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt b/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt index 9ecc936c00..4677d6848d 100644 --- a/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt +++ b/core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt @@ -276,6 +276,10 @@ class CityInfoConquestFunctions(val city: CityInfo){ tryUpdateRoadStatus() cityStats.update() + + // Update proximity rankings + civInfo.updateProximity(oldCiv, + oldCiv.updateProximity(civInfo)) } } diff --git a/core/src/com/unciv/logic/civilization/CityStateFunctions.kt b/core/src/com/unciv/logic/civilization/CityStateFunctions.kt index d151ff89bf..57d1784e75 100644 --- a/core/src/com/unciv/logic/civilization/CityStateFunctions.kt +++ b/core/src/com/unciv/logic/civilization/CityStateFunctions.kt @@ -2,17 +2,16 @@ package com.unciv.logic.civilization import com.unciv.Constants import com.unciv.logic.automation.NextTurnAutomation -import com.unciv.logic.civilization.diplomacy.DiplomacyFlags -import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers -import com.unciv.logic.civilization.diplomacy.DiplomaticStatus -import com.unciv.logic.civilization.diplomacy.RelationshipLevel +import com.unciv.logic.civilization.diplomacy.* import com.unciv.models.metadata.GameSpeed import com.unciv.models.ruleset.Ruleset import com.unciv.models.stats.Stat import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderText import com.unciv.ui.victoryscreen.RankingType +import java.util.* import kotlin.collections.HashMap +import kotlin.collections.HashSet import kotlin.collections.LinkedHashMap import kotlin.math.max import kotlin.math.min @@ -506,10 +505,58 @@ class CityStateFunctions(val civInfo: CivilizationInfo) { } } - /** A city state was attacked. What are its protectors going to do about it??? */ + /** A city state was attacked. What are its protectors going to do about it??? Also checks for Wary */ fun cityStateAttacked(attacker: CivilizationInfo) { if (!civInfo.isCityState()) return // What are we doing here? + // We might become wary! + if (attacker.isMinorCivWarmonger()) { // They've attacked a lot of city-states + civInfo.getDiplomacyManager(attacker).becomeWary() + } + else if (attacker.isMinorCivAggressor()) { // They've attacked a few + if (Random().nextBoolean()) { // 50% chance + civInfo.getDiplomacyManager(attacker).becomeWary() + } + } + // Others might become wary! + if (attacker.isMinorCivAggressor()) { + for (cityState in civInfo.gameInfo.getAliveCityStates()) { + if (cityState == civInfo) // Must be a different minor + continue + if (cityState.getAllyCiv() == attacker.civName) // Must not be allied to the attacker + continue + if (!cityState.knows(attacker)) // Must have met + continue + + var probability: Int + if (attacker.isMinorCivWarmonger()) { + // High probability if very aggressive + probability = when (cityState.getProximity(attacker)) { + Proximity.Neighbors -> 100 + Proximity.Close -> 75 + Proximity.Far -> 50 + Proximity.Distant -> 25 + else -> 0 + } + } else { + // Lower probability if only somewhat aggressive + probability = when (cityState.getProximity(attacker)) { + Proximity.Neighbors -> 50 + Proximity.Close -> 20 + else -> 0 + } + } + + // Higher probability if already at war + if (cityState.isAtWarWith(attacker)) + probability += 50 + + if (Random().nextInt(100) <= probability) { + cityState.getDiplomacyManager(attacker).becomeWary() + } + } + } + for (protector in civInfo.getProtectorCivs()) { if (!protector.knows(attacker)) // Who? continue diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index 4ee3df527e..154fd85530 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -10,9 +10,7 @@ import com.unciv.logic.civilization.RuinsManager.RuinsManager import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomacyManager import com.unciv.logic.civilization.diplomacy.DiplomaticStatus -import com.unciv.logic.map.MapUnit -import com.unciv.logic.map.TileInfo -import com.unciv.logic.map.UnitMovementAlgorithms +import com.unciv.logic.map.* import com.unciv.logic.trade.TradeEvaluation import com.unciv.logic.trade.TradeRequest import com.unciv.models.Counter @@ -34,6 +32,14 @@ import kotlin.math.min import kotlin.math.roundToInt import kotlin.math.sqrt +enum class Proximity { + None, // ie no cities + Neighbors, + Close, + Far, + Distant +} + class CivilizationInfo { @Transient @@ -109,6 +115,7 @@ class CivilizationInfo { var victoryManager = VictoryManager() var ruinsManager = RuinsManager() var diplomacy = HashMap() + var proximity = HashMap() var notifications = ArrayList() val popupAlerts = ArrayList() private var allyCivName: String? = null @@ -143,6 +150,9 @@ class CivilizationInfo { // default false once we no longer want legacy save-game compatibility var hasEverOwnedOriginalCapital: Boolean? = null + // For Aggressor, Warmonger status + private var numMinorCivsAttacked = 0 + constructor() constructor(civName: String) { @@ -167,6 +177,7 @@ class CivilizationInfo { toReturn.allyCivName = allyCivName for (diplomacyManager in diplomacy.values.map { it.clone() }) toReturn.diplomacy[diplomacyManager.otherCivName] = diplomacyManager + toReturn.proximity.putAll(proximity) toReturn.cities = cities.map { it.clone() } // This is the only thing that is NOT switched out, which makes it a source of ConcurrentModification errors. @@ -187,6 +198,7 @@ class CivilizationInfo { toReturn.boughtConstructionsWithGloballyIncreasingPrice.putAll(boughtConstructionsWithGloballyIncreasingPrice) // toReturn.hasEverOwnedOriginalCapital = hasEverOwnedOriginalCapital + toReturn.numMinorCivsAttacked = numMinorCivsAttacked return toReturn } @@ -199,6 +211,9 @@ class CivilizationInfo { fun getDiplomacyManager(civInfo: CivilizationInfo) = getDiplomacyManager(civInfo.civName) fun getDiplomacyManager(civName: String) = diplomacy[civName]!! + fun getProximity(civInfo: CivilizationInfo) = getProximity(civInfo.civName) + fun getProximity(civName: String) = proximity[civName] ?: Proximity.None + /** Returns only undefeated civs, aka the ones we care about */ fun getKnownCivs() = diplomacy.values.map { it.otherCiv() }.filter { !it.isDefeated() } fun knows(otherCivName: String) = diplomacy.containsKey(otherCivName) @@ -556,6 +571,9 @@ class CivilizationInfo { fun hasTechOrPolicy(techOrPolicyName: String) = tech.isResearched(techOrPolicyName) || policies.isAdopted(techOrPolicyName) + fun isMinorCivAggressor() = numMinorCivsAttacked >= 2 + fun isMinorCivWarmonger() = numMinorCivsAttacked >= 4 + //endregion //region state-changing functions @@ -612,6 +630,10 @@ class CivilizationInfo { updateDetailedCivResources() } + fun changeMinorCivsAttacked(count: Int) { + numMinorCivsAttacked += count + } + // implementation in a separate class, to not clog up CivInfo fun initialSetCitiesConnectedToCapitalTransients() = transients().updateCitiesConnectedToCapital(true) fun updateHasActiveGreatWall() = transients().updateHasActiveGreatWall() @@ -912,6 +934,81 @@ class CivilizationInfo { ).toInt() } + fun updateProximity(otherCiv: CivilizationInfo, preCalculated: Proximity? = null): Proximity { + if (otherCiv == this) return Proximity.None + if (preCalculated != null) { + // We usually want to update this for a pair of civs at the same time + // Since this function *should* be symmetrical for both civs, we can just do it once + this.proximity[otherCiv.civName] = preCalculated + return preCalculated + } + if (cities.isEmpty() || otherCiv.cities.isEmpty()) { + proximity[otherCiv.civName] = Proximity.None + return Proximity.None + } + + val mapParams = gameInfo.tileMap.mapParameters + var minDistance = 100000 // a long distance + var totalDistance = 0 + var connections = 0 + + var proximity = Proximity.None + + for (ourCity in cities) { + for (theirCity in otherCiv.cities) { + val distance = ourCity.getCenterTile().aerialDistanceTo(theirCity.getCenterTile()) + totalDistance += distance + connections++ + if (minDistance > distance) minDistance = distance + } + } + + if (minDistance <= 7) { + proximity = Proximity.Neighbors + } else if (connections > 0) { + val averageDistance = totalDistance / connections + val mapFactor = if (mapParams.shape == MapShape.rectangular) + (mapParams.mapSize.height + mapParams.mapSize.width) / 2 + else (mapParams.mapSize.radius * 3) / 2 // slightly less area than equal size rect + + val closeDistance = ((mapFactor * 25) / 100).coerceIn(10, 20) + val farDistance = ((mapFactor * 45) / 100).coerceIn(20, 50) + + proximity = if (minDistance <= 11 && averageDistance <= closeDistance) + Proximity.Close + else if (averageDistance <= farDistance) + Proximity.Far + else + Proximity.Distant + } + + // Check if different continents (unless already max distance, or water map) + if (connections > 0 && proximity != Proximity.Distant + && !gameInfo.tileMap.isWaterMap()) { + + if (getCapital().getCenterTile().getContinent() != otherCiv.getCapital().getCenterTile().getContinent()) { + // Different continents - increase separation by one step + proximity = when (proximity) { + Proximity.Far -> Proximity.Distant + Proximity.Close -> Proximity.Far + Proximity.Neighbors -> Proximity.Close + else -> proximity + } + } + } + + // If there aren't many players (left) we can't be that far + val numMajors = gameInfo.getAliveMajorCivs().count() + if (numMajors <= 2 && proximity > Proximity.Close) + proximity = Proximity.Close + if (numMajors <= 4 && proximity > Proximity.Far) + proximity = Proximity.Far + + this.proximity[otherCiv.civName] = proximity + + return proximity + } + //////////////////////// City State wrapper functions //////////////////////// fun initCityState(ruleset: Ruleset, startingEra: String, unusedMajorCivs: Collection) diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt index 9ae2d47a7b..197d99ac5b 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt @@ -9,6 +9,7 @@ import com.unciv.logic.trade.TradeType import com.unciv.models.ruleset.tile.ResourceSupplyList import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderText +import com.unciv.ui.utils.toPercent import kotlin.math.ceil import kotlin.math.max import kotlin.math.min @@ -49,7 +50,8 @@ enum class DiplomacyFlags { RememberAttackedProtectedMinor, RememberBulliedProtectedMinor, RememberSidedWithProtectedMinor, - Denunciation + Denunciation, + WaryOf, } enum class DiplomaticModifiers { @@ -238,6 +240,9 @@ class DiplomacyManager() { restingPoint += unique.params[0].toInt() if (diplomaticStatus == DiplomaticStatus.Protector) restingPoint += 10 + + if (hasFlag(DiplomacyFlags.WaryOf)) restingPoint -= 20 + return restingPoint } @@ -245,47 +250,47 @@ class DiplomacyManager() { if (influence < getCityStateInfluenceRestingPoint()) return 0f - val decrement = when (civInfo.cityStatePersonality) { - CityStatePersonality.Hostile -> 1.5f - else -> 1f - } - - var modifier = when (civInfo.cityStatePersonality) { - CityStatePersonality.Hostile -> 2f - CityStatePersonality.Irrational -> 1.5f - CityStatePersonality.Friendly -> .5f + val decrement = when { + civInfo.cityStatePersonality == CityStatePersonality.Hostile -> 1.5f + otherCiv().isMinorCivAggressor() -> 2f else -> 1f } + var modifierPercent = 0f for (unique in otherCiv().getMatchingUniques("City-State Influence degrades []% slower")) - modifier *= 1f - unique.params[0].toFloat() / 100f + modifierPercent -= unique.params[0].toFloat() + + val religion = if (civInfo.cities.isEmpty()) null + else civInfo.getCapital().religion.getMajorityReligionName() + if (religion != null && religion == otherCiv().religionManager.religion?.name) + modifierPercent -= 25f // 25% slower degrade when sharing a religion for (civ in civInfo.gameInfo.civilizations.filter { it.isMajorCiv() && it != otherCiv()}) { for (unique in civ.getMatchingUniques("Influence of all other civilizations with all city-states degrades []% faster")) { - modifier *= 1f + unique.params[0].toFloat() / 100f + modifierPercent += unique.params[0].toFloat() } } - return max(0f, decrement) * max(0f, modifier) + return max(0f, decrement) * max(-100f, modifierPercent).toPercent() } private fun getCityStateInfluenceRecovery(): Float { if (influence > getCityStateInfluenceRestingPoint()) return 0f - val increment = 1f + val increment = 1f // sic: personality does not matter here - var modifier = when (civInfo.cityStatePersonality) { - CityStatePersonality.Friendly -> 2f - CityStatePersonality.Irrational -> 1.5f - CityStatePersonality.Hostile -> .5f - else -> 1f - } + var modifierPercent = 0f if (otherCiv().hasUnique("City-State Influence recovers at twice the normal rate")) - modifier *= 2f + modifierPercent += 100f - return max(0f, increment) * max(0f, modifier) + val religion = if (civInfo.cities.isEmpty()) null + else civInfo.getCapital().religion.getMajorityReligionName() + if (religion != null && religion == otherCiv().religionManager.religion?.name) + modifierPercent += 50f // 50% quicker recovery when sharing a religion + + return max(0f, increment) * max(0f, modifierPercent).toPercent() } fun canDeclareWar() = turnsToPeaceTreaty() == 0 && diplomaticStatus != DiplomaticStatus.War @@ -642,6 +647,7 @@ class DiplomacyManager() { otherCivDiplomacy.setModifier(DiplomaticModifiers.DeclaredWarOnUs, -20f) if (otherCiv.isCityState()) { otherCivDiplomacy.setInfluence(-60f) + civInfo.changeMinorCivsAttacked(1) otherCiv.cityStateAttacked(civInfo) } @@ -832,5 +838,11 @@ class DiplomacyManager() { otherCivDiplomacy().setFlag(DiplomacyFlags.RememberSidedWithProtectedMinor, 25) } + fun becomeWary() { + if (hasFlag(DiplomacyFlags.WaryOf)) return // once is enough + setFlag(DiplomacyFlags.WaryOf, -1) // Never expires + otherCiv().addNotification("City-States grow wary of your aggression. The resting point for Influence has decreased by [20] for [${civInfo.civName}].", civInfo.civName) + } + //endregion } diff --git a/core/src/com/unciv/logic/map/BFS.kt b/core/src/com/unciv/logic/map/BFS.kt index f7d1d9bb61..b8587f5239 100644 --- a/core/src/com/unciv/logic/map/BFS.kt +++ b/core/src/com/unciv/logic/map/BFS.kt @@ -23,10 +23,13 @@ class BFS( tilesReached[startingPoint] = startingPoint } - /** Process fully until there's nowhere left to check */ - fun stepToEnd() { + /** Process fully until there's nowhere left to check + * Optionally assigns a continent ID as it goes */ + fun stepToEnd(continent: Int? = null) { + if (continent != null) + startingPoint.setContinent(continent) while (!hasEnded()) - nextStep() + nextStep(continent) } /** @@ -46,13 +49,15 @@ class BFS( * * Will do nothing when [hasEnded] returns `true` */ - fun nextStep() { + fun nextStep(continent: Int? = null) { if (tilesReached.size >= maxSize) { tilesToCheck.clear(); return } val current = tilesToCheck.removeFirstOrNull() ?: return for (neighbor in current.neighbors) { if (neighbor !in tilesReached && predicate(neighbor)) { tilesReached[neighbor] = current tilesToCheck.add(neighbor) + if (continent != null) + neighbor.setContinent(continent) } } } diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index 2b759c8955..74b8775fc2 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -70,6 +70,8 @@ open class TileInfo { var hasBottomRiver = false var hasBottomLeftRiver = false + private var continent = -1 + val latitude: Float get() = HexMath.getLatitude(position) val longitude: Float @@ -92,6 +94,7 @@ open class TileInfo { toReturn.hasBottomLeftRiver = hasBottomLeftRiver toReturn.hasBottomRightRiver = hasBottomRightRiver toReturn.hasBottomRiver = hasBottomRiver + toReturn.continent = continent return toReturn } @@ -654,6 +657,7 @@ open class TileInfo { return out } + fun getContinent() = continent //endregion @@ -773,5 +777,12 @@ open class TileInfo { } } + // Should only be set once at map generation + fun setContinent(continent: Int) { + if (this.continent != -1) + throw Exception("Continent already assigned @ $position") + this.continent = continent + } + //endregion } diff --git a/core/src/com/unciv/logic/map/TileMap.kt b/core/src/com/unciv/logic/map/TileMap.kt index bf8415bce7..0a06926a8e 100644 --- a/core/src/com/unciv/logic/map/TileMap.kt +++ b/core/src/com/unciv/logic/map/TileMap.kt @@ -34,6 +34,7 @@ class TileMap { var mapParameters = MapParameters() private var tileList = ArrayList() + val continentSizes = HashMap() // Continent ID, Continent size /** Structure geared for simple serialization by Gdx.Json (which is a little blind to kotlin collections, especially HashSet) * @param position [Vector2] of the location @@ -78,6 +79,9 @@ class TileMap { @Transient val startingLocationsByNation = HashMap>() + @Transient + val landTilesInBigEnoughGroup = ArrayList() // cached at map gen + //endregion //region Constructors @@ -123,6 +127,7 @@ class TileMap { toReturn.startingLocations.clear() toReturn.startingLocations.ensureCapacity(startingLocations.size) toReturn.startingLocations.addAll(startingLocations) + toReturn.continentSizes.putAll(continentSizes) return toReturn } @@ -344,6 +349,12 @@ class TileMap { return rulesetIncompatibilities } + fun isWaterMap(): Boolean { + val bigIslands = continentSizes.count { it.value > 20 } + val players = gameInfo.gameParameters.players.count() + return bigIslands >= players + } + //endregion //region State-Changing Methods diff --git a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt index 0107ff72ff..7edf695579 100644 --- a/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt +++ b/core/src/com/unciv/logic/map/mapgenerator/MapGenerator.kt @@ -73,6 +73,9 @@ class MapGenerator(val ruleset: Ruleset) { runAndMeasure("spawnIce") { spawnIce(map) } + runAndMeasure("assignContinents") { + assignContinents(map) + } runAndMeasure("NaturalWonderGenerator") { NaturalWonderGenerator(ruleset, randomness).spawnNaturalWonders(map) } @@ -461,6 +464,26 @@ class MapGenerator(val ruleset: Ruleset) { } } + // Set a continent id for each tile, so we can quickly see which tiles are connected. + // Can also be called on saved maps + fun assignContinents(tileMap: TileMap) { + var landTiles = tileMap.values + .filter { it.isLand && !it.isImpassible()} + var currentContinent = 0 + + while (landTiles.any()) { + val bfs = BFS(landTiles.random()) { it.isLand && !it.isImpassible() } + bfs.stepToEnd(currentContinent) + val continent = bfs.getReachedTiles() + tileMap.continentSizes[currentContinent] = continent.size + if (continent.size > 20) { + tileMap.landTilesInBigEnoughGroup.addAll(continent) + } + + currentContinent++ + landTiles = landTiles.filter { it !in continent } + } + } } class MapGenerationRandomness {