City-States Influence rates; Wary status; Proximity calculations (#5198)

* Rates for natural influence change

* Minor civ wariness, proximity calculation

* CS can declare permanent war

* CS can in fact not declare permanent war

* adjustments, template.properties

* neater code

* fix failing test? .

* move proximity code, for reals fix failing check

* now?

* revisions

* BFS only once, better check for water map

* assign continents on pre-made maps as well

* now works on all pre-made maps
This commit is contained in:
SimonCeder 2021-09-14 10:01:43 +02:00 committed by GitHub
parent 297618706c
commit 7bd555ac95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 297 additions and 48 deletions

View File

@ -182,6 +182,7 @@ Diplomatic Marriage ([amount] Gold) =
We have married into the ruling family of [civName], bringing them under our control. = 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. = [civName] has married into the ruling family of [civName2], bringing them under their control. =
You have broken your Pledge to Protect [civName]! = 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 = Cultured =
Maritime = Maritime =

View File

@ -37,19 +37,25 @@ object GameStarter {
gameInfo.gameParameters = gameSetupInfo.gameParameters gameInfo.gameParameters = gameSetupInfo.gameParameters
val ruleset = RulesetCache.getComplexRuleset(gameInfo.gameParameters.mods) val ruleset = RulesetCache.getComplexRuleset(gameInfo.gameParameters.mods)
val mapGen = MapGenerator(ruleset)
if (gameSetupInfo.mapParameters.name != "") runAndMeasure("loadMap") { if (gameSetupInfo.mapParameters.name != "") runAndMeasure("loadMap") {
tileMap = MapSaver.loadMap(gameSetupInfo.mapFile!!) tileMap = MapSaver.loadMap(gameSetupInfo.mapFile!!)
// Don't override the map parameters - this can include if we world wrap or not! // Don't override the map parameters - this can include if we world wrap or not!
} else runAndMeasure("generateMap") { } else runAndMeasure("generateMap") {
tileMap = MapGenerator(ruleset).generateMap(gameSetupInfo.mapParameters) tileMap = mapGen.generateMap(gameSetupInfo.mapParameters)
tileMap.mapParameters = gameSetupInfo.mapParameters tileMap.mapParameters = gameSetupInfo.mapParameters
} }
runAndMeasure("addCivilizations") { runAndMeasure("addCivilizations") {
gameInfo.tileMap = tileMap gameInfo.tileMap = tileMap
tileMap.gameInfo = gameInfo // need to set this transient before placing units in the map tileMap.gameInfo =
addCivilizations(gameSetupInfo.gameParameters, gameInfo, ruleset) // this is before gameInfo.setTransients, so gameInfo doesn't yet have the gameBasics 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") { runAndMeasure("Remove units") {
@ -78,6 +84,11 @@ object GameStarter {
addCivStats(gameInfo) addCivStats(gameInfo)
} }
runAndMeasure("assignContinents?") {
if (tileMap.continentSizes.isEmpty()) // Probably saved map without continent data
mapGen.assignContinents(tileMap)
}
runAndMeasure("addCivStartingUnits") { 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! // 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) addCivStartingUnits(gameInfo)
@ -334,12 +345,15 @@ object GameStarter {
} }
} }
private fun getStartingLocations(civs: List<CivilizationInfo>, tileMap: TileMap, startScores: HashMap<TileInfo, Float>): HashMap<CivilizationInfo, TileInfo> { private fun getStartingLocations(civs: List<CivilizationInfo>, tileMap: TileMap, startScores: HashMap<TileInfo, Float>): HashMap<CivilizationInfo, TileInfo> {
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 var landTiles = tileMap.values
// Games starting on snow might as well start over... // Games starting on snow might as well start over...
.filter { it.isLand && !it.isImpassible() && it.baseTerrain != Constants.snow } .filter { it.isLand && !it.isImpassible() && it.baseTerrain != Constants.snow }
val landTilesInBigEnoughGroup = ArrayList<TileInfo>()
while (landTiles.any()) { while (landTiles.any()) {
val bfs = BFS(landTiles.random()) { it.isLand && !it.isImpassible() } val bfs = BFS(landTiles.random()) { it.isLand && !it.isImpassible() }
bfs.stepToEnd() bfs.stepToEnd()
@ -348,6 +362,7 @@ object GameStarter {
if (tilesInGroup.size > 20) // is this a good number? I dunno, but it's easy enough to change later on if (tilesInGroup.size > 20) // is this a good number? I dunno, but it's easy enough to change later on
landTilesInBigEnoughGroup.addAll(tilesInGroup) landTilesInBigEnoughGroup.addAll(tilesInGroup)
} }
}
val civsOrderedByAvailableLocations = civs.shuffled() // Order should be random since it determines who gets best start val civsOrderedByAvailableLocations = civs.shuffled() // Order should be random since it determines who gets best start
.sortedBy { civ -> .sortedBy { civ ->

View File

@ -3,6 +3,7 @@ package com.unciv.logic.city
import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector2
import com.unciv.logic.battle.CityCombatant import com.unciv.logic.battle.CityCombatant
import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.civilization.Proximity
import com.unciv.logic.civilization.ReligionState import com.unciv.logic.civilization.ReligionState
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.map.RoadStatus import com.unciv.logic.map.RoadStatus
@ -129,6 +130,18 @@ class CityInfo {
population.autoAssignPopulation() population.autoAssignPopulation()
cityStats.update() 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() triggerCitiesSettledNearOtherCiv()
} }
@ -556,6 +569,16 @@ class CityInfo {
if (isCapital() && civInfo.cities.isNotEmpty()) { // Move the capital if destroyed (by a nuke or by razing) if (isCapital() && civInfo.cities.isNotEmpty()) { // Move the capital if destroyed (by a nuke or by razing)
civInfo.cities.first().cityConstructions.addBuilding(capitalCityIndicator()) 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() fun annexCity() = CityInfoConquestFunctions(this).annexCity()

View File

@ -276,6 +276,10 @@ class CityInfoConquestFunctions(val city: CityInfo){
tryUpdateRoadStatus() tryUpdateRoadStatus()
cityStats.update() cityStats.update()
// Update proximity rankings
civInfo.updateProximity(oldCiv,
oldCiv.updateProximity(civInfo))
} }
} }

View File

@ -2,17 +2,16 @@ package com.unciv.logic.civilization
import com.unciv.Constants import com.unciv.Constants
import com.unciv.logic.automation.NextTurnAutomation import com.unciv.logic.automation.NextTurnAutomation
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.*
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
import com.unciv.models.metadata.GameSpeed import com.unciv.models.metadata.GameSpeed
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.models.translations.getPlaceholderText import com.unciv.models.translations.getPlaceholderText
import com.unciv.ui.victoryscreen.RankingType import com.unciv.ui.victoryscreen.RankingType
import java.util.*
import kotlin.collections.HashMap import kotlin.collections.HashMap
import kotlin.collections.HashSet
import kotlin.collections.LinkedHashMap import kotlin.collections.LinkedHashMap
import kotlin.math.max import kotlin.math.max
import kotlin.math.min 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) { fun cityStateAttacked(attacker: CivilizationInfo) {
if (!civInfo.isCityState()) return // What are we doing here? 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()) { for (protector in civInfo.getProtectorCivs()) {
if (!protector.knows(attacker)) // Who? if (!protector.knows(attacker)) // Who?
continue continue

View File

@ -10,9 +10,7 @@ import com.unciv.logic.civilization.RuinsManager.RuinsManager
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomacyManager import com.unciv.logic.civilization.diplomacy.DiplomacyManager
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.logic.map.MapUnit import com.unciv.logic.map.*
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.UnitMovementAlgorithms
import com.unciv.logic.trade.TradeEvaluation import com.unciv.logic.trade.TradeEvaluation
import com.unciv.logic.trade.TradeRequest import com.unciv.logic.trade.TradeRequest
import com.unciv.models.Counter import com.unciv.models.Counter
@ -34,6 +32,14 @@ import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.sqrt import kotlin.math.sqrt
enum class Proximity {
None, // ie no cities
Neighbors,
Close,
Far,
Distant
}
class CivilizationInfo { class CivilizationInfo {
@Transient @Transient
@ -109,6 +115,7 @@ class CivilizationInfo {
var victoryManager = VictoryManager() var victoryManager = VictoryManager()
var ruinsManager = RuinsManager() var ruinsManager = RuinsManager()
var diplomacy = HashMap<String, DiplomacyManager>() var diplomacy = HashMap<String, DiplomacyManager>()
var proximity = HashMap<String, Proximity>()
var notifications = ArrayList<Notification>() var notifications = ArrayList<Notification>()
val popupAlerts = ArrayList<PopupAlert>() val popupAlerts = ArrayList<PopupAlert>()
private var allyCivName: String? = null private var allyCivName: String? = null
@ -143,6 +150,9 @@ class CivilizationInfo {
// default false once we no longer want legacy save-game compatibility // default false once we no longer want legacy save-game compatibility
var hasEverOwnedOriginalCapital: Boolean? = null var hasEverOwnedOriginalCapital: Boolean? = null
// For Aggressor, Warmonger status
private var numMinorCivsAttacked = 0
constructor() constructor()
constructor(civName: String) { constructor(civName: String) {
@ -167,6 +177,7 @@ class CivilizationInfo {
toReturn.allyCivName = allyCivName toReturn.allyCivName = allyCivName
for (diplomacyManager in diplomacy.values.map { it.clone() }) for (diplomacyManager in diplomacy.values.map { it.clone() })
toReturn.diplomacy[diplomacyManager.otherCivName] = diplomacyManager toReturn.diplomacy[diplomacyManager.otherCivName] = diplomacyManager
toReturn.proximity.putAll(proximity)
toReturn.cities = cities.map { it.clone() } toReturn.cities = cities.map { it.clone() }
// This is the only thing that is NOT switched out, which makes it a source of ConcurrentModification errors. // 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.boughtConstructionsWithGloballyIncreasingPrice.putAll(boughtConstructionsWithGloballyIncreasingPrice)
// //
toReturn.hasEverOwnedOriginalCapital = hasEverOwnedOriginalCapital toReturn.hasEverOwnedOriginalCapital = hasEverOwnedOriginalCapital
toReturn.numMinorCivsAttacked = numMinorCivsAttacked
return toReturn return toReturn
} }
@ -199,6 +211,9 @@ class CivilizationInfo {
fun getDiplomacyManager(civInfo: CivilizationInfo) = getDiplomacyManager(civInfo.civName) fun getDiplomacyManager(civInfo: CivilizationInfo) = getDiplomacyManager(civInfo.civName)
fun getDiplomacyManager(civName: String) = diplomacy[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 */ /** Returns only undefeated civs, aka the ones we care about */
fun getKnownCivs() = diplomacy.values.map { it.otherCiv() }.filter { !it.isDefeated() } fun getKnownCivs() = diplomacy.values.map { it.otherCiv() }.filter { !it.isDefeated() }
fun knows(otherCivName: String) = diplomacy.containsKey(otherCivName) fun knows(otherCivName: String) = diplomacy.containsKey(otherCivName)
@ -556,6 +571,9 @@ class CivilizationInfo {
fun hasTechOrPolicy(techOrPolicyName: String) = fun hasTechOrPolicy(techOrPolicyName: String) =
tech.isResearched(techOrPolicyName) || policies.isAdopted(techOrPolicyName) tech.isResearched(techOrPolicyName) || policies.isAdopted(techOrPolicyName)
fun isMinorCivAggressor() = numMinorCivsAttacked >= 2
fun isMinorCivWarmonger() = numMinorCivsAttacked >= 4
//endregion //endregion
//region state-changing functions //region state-changing functions
@ -612,6 +630,10 @@ class CivilizationInfo {
updateDetailedCivResources() updateDetailedCivResources()
} }
fun changeMinorCivsAttacked(count: Int) {
numMinorCivsAttacked += count
}
// implementation in a separate class, to not clog up CivInfo // implementation in a separate class, to not clog up CivInfo
fun initialSetCitiesConnectedToCapitalTransients() = transients().updateCitiesConnectedToCapital(true) fun initialSetCitiesConnectedToCapitalTransients() = transients().updateCitiesConnectedToCapital(true)
fun updateHasActiveGreatWall() = transients().updateHasActiveGreatWall() fun updateHasActiveGreatWall() = transients().updateHasActiveGreatWall()
@ -912,6 +934,81 @@ class CivilizationInfo {
).toInt() ).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 //////////////////////// //////////////////////// City State wrapper functions ////////////////////////
fun initCityState(ruleset: Ruleset, startingEra: String, unusedMajorCivs: Collection<String>) fun initCityState(ruleset: Ruleset, startingEra: String, unusedMajorCivs: Collection<String>)

View File

@ -9,6 +9,7 @@ import com.unciv.logic.trade.TradeType
import com.unciv.models.ruleset.tile.ResourceSupplyList import com.unciv.models.ruleset.tile.ResourceSupplyList
import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.models.translations.getPlaceholderText import com.unciv.models.translations.getPlaceholderText
import com.unciv.ui.utils.toPercent
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -49,7 +50,8 @@ enum class DiplomacyFlags {
RememberAttackedProtectedMinor, RememberAttackedProtectedMinor,
RememberBulliedProtectedMinor, RememberBulliedProtectedMinor,
RememberSidedWithProtectedMinor, RememberSidedWithProtectedMinor,
Denunciation Denunciation,
WaryOf,
} }
enum class DiplomaticModifiers { enum class DiplomaticModifiers {
@ -238,6 +240,9 @@ class DiplomacyManager() {
restingPoint += unique.params[0].toInt() restingPoint += unique.params[0].toInt()
if (diplomaticStatus == DiplomaticStatus.Protector) restingPoint += 10 if (diplomaticStatus == DiplomaticStatus.Protector) restingPoint += 10
if (hasFlag(DiplomacyFlags.WaryOf)) restingPoint -= 20
return restingPoint return restingPoint
} }
@ -245,47 +250,47 @@ class DiplomacyManager() {
if (influence < getCityStateInfluenceRestingPoint()) if (influence < getCityStateInfluenceRestingPoint())
return 0f return 0f
val decrement = when (civInfo.cityStatePersonality) { val decrement = when {
CityStatePersonality.Hostile -> 1.5f civInfo.cityStatePersonality == CityStatePersonality.Hostile -> 1.5f
else -> 1f otherCiv().isMinorCivAggressor() -> 2f
}
var modifier = when (civInfo.cityStatePersonality) {
CityStatePersonality.Hostile -> 2f
CityStatePersonality.Irrational -> 1.5f
CityStatePersonality.Friendly -> .5f
else -> 1f else -> 1f
} }
var modifierPercent = 0f
for (unique in otherCiv().getMatchingUniques("City-State Influence degrades []% slower")) 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 (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")) { 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 { private fun getCityStateInfluenceRecovery(): Float {
if (influence > getCityStateInfluenceRestingPoint()) if (influence > getCityStateInfluenceRestingPoint())
return 0f return 0f
val increment = 1f val increment = 1f // sic: personality does not matter here
var modifier = when (civInfo.cityStatePersonality) { var modifierPercent = 0f
CityStatePersonality.Friendly -> 2f
CityStatePersonality.Irrational -> 1.5f
CityStatePersonality.Hostile -> .5f
else -> 1f
}
if (otherCiv().hasUnique("City-State Influence recovers at twice the normal rate")) 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 fun canDeclareWar() = turnsToPeaceTreaty() == 0 && diplomaticStatus != DiplomaticStatus.War
@ -642,6 +647,7 @@ class DiplomacyManager() {
otherCivDiplomacy.setModifier(DiplomaticModifiers.DeclaredWarOnUs, -20f) otherCivDiplomacy.setModifier(DiplomaticModifiers.DeclaredWarOnUs, -20f)
if (otherCiv.isCityState()) { if (otherCiv.isCityState()) {
otherCivDiplomacy.setInfluence(-60f) otherCivDiplomacy.setInfluence(-60f)
civInfo.changeMinorCivsAttacked(1)
otherCiv.cityStateAttacked(civInfo) otherCiv.cityStateAttacked(civInfo)
} }
@ -832,5 +838,11 @@ class DiplomacyManager() {
otherCivDiplomacy().setFlag(DiplomacyFlags.RememberSidedWithProtectedMinor, 25) 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 //endregion
} }

View File

@ -23,10 +23,13 @@ class BFS(
tilesReached[startingPoint] = startingPoint tilesReached[startingPoint] = startingPoint
} }
/** Process fully until there's nowhere left to check */ /** Process fully until there's nowhere left to check
fun stepToEnd() { * Optionally assigns a continent ID as it goes */
fun stepToEnd(continent: Int? = null) {
if (continent != null)
startingPoint.setContinent(continent)
while (!hasEnded()) while (!hasEnded())
nextStep() nextStep(continent)
} }
/** /**
@ -46,13 +49,15 @@ class BFS(
* *
* Will do nothing when [hasEnded] returns `true` * Will do nothing when [hasEnded] returns `true`
*/ */
fun nextStep() { fun nextStep(continent: Int? = null) {
if (tilesReached.size >= maxSize) { tilesToCheck.clear(); return } if (tilesReached.size >= maxSize) { tilesToCheck.clear(); return }
val current = tilesToCheck.removeFirstOrNull() ?: return val current = tilesToCheck.removeFirstOrNull() ?: return
for (neighbor in current.neighbors) { for (neighbor in current.neighbors) {
if (neighbor !in tilesReached && predicate(neighbor)) { if (neighbor !in tilesReached && predicate(neighbor)) {
tilesReached[neighbor] = current tilesReached[neighbor] = current
tilesToCheck.add(neighbor) tilesToCheck.add(neighbor)
if (continent != null)
neighbor.setContinent(continent)
} }
} }
} }

View File

@ -70,6 +70,8 @@ open class TileInfo {
var hasBottomRiver = false var hasBottomRiver = false
var hasBottomLeftRiver = false var hasBottomLeftRiver = false
private var continent = -1
val latitude: Float val latitude: Float
get() = HexMath.getLatitude(position) get() = HexMath.getLatitude(position)
val longitude: Float val longitude: Float
@ -92,6 +94,7 @@ open class TileInfo {
toReturn.hasBottomLeftRiver = hasBottomLeftRiver toReturn.hasBottomLeftRiver = hasBottomLeftRiver
toReturn.hasBottomRightRiver = hasBottomRightRiver toReturn.hasBottomRightRiver = hasBottomRightRiver
toReturn.hasBottomRiver = hasBottomRiver toReturn.hasBottomRiver = hasBottomRiver
toReturn.continent = continent
return toReturn return toReturn
} }
@ -654,6 +657,7 @@ open class TileInfo {
return out return out
} }
fun getContinent() = continent
//endregion //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 //endregion
} }

View File

@ -34,6 +34,7 @@ class TileMap {
var mapParameters = MapParameters() var mapParameters = MapParameters()
private var tileList = ArrayList<TileInfo>() private var tileList = ArrayList<TileInfo>()
val continentSizes = HashMap<Int, Int>() // Continent ID, Continent size
/** Structure geared for simple serialization by Gdx.Json (which is a little blind to kotlin collections, especially HashSet) /** Structure geared for simple serialization by Gdx.Json (which is a little blind to kotlin collections, especially HashSet)
* @param position [Vector2] of the location * @param position [Vector2] of the location
@ -78,6 +79,9 @@ class TileMap {
@Transient @Transient
val startingLocationsByNation = HashMap<String,HashSet<TileInfo>>() val startingLocationsByNation = HashMap<String,HashSet<TileInfo>>()
@Transient
val landTilesInBigEnoughGroup = ArrayList<TileInfo>() // cached at map gen
//endregion //endregion
//region Constructors //region Constructors
@ -123,6 +127,7 @@ class TileMap {
toReturn.startingLocations.clear() toReturn.startingLocations.clear()
toReturn.startingLocations.ensureCapacity(startingLocations.size) toReturn.startingLocations.ensureCapacity(startingLocations.size)
toReturn.startingLocations.addAll(startingLocations) toReturn.startingLocations.addAll(startingLocations)
toReturn.continentSizes.putAll(continentSizes)
return toReturn return toReturn
} }
@ -344,6 +349,12 @@ class TileMap {
return rulesetIncompatibilities return rulesetIncompatibilities
} }
fun isWaterMap(): Boolean {
val bigIslands = continentSizes.count { it.value > 20 }
val players = gameInfo.gameParameters.players.count()
return bigIslands >= players
}
//endregion //endregion
//region State-Changing Methods //region State-Changing Methods

View File

@ -73,6 +73,9 @@ class MapGenerator(val ruleset: Ruleset) {
runAndMeasure("spawnIce") { runAndMeasure("spawnIce") {
spawnIce(map) spawnIce(map)
} }
runAndMeasure("assignContinents") {
assignContinents(map)
}
runAndMeasure("NaturalWonderGenerator") { runAndMeasure("NaturalWonderGenerator") {
NaturalWonderGenerator(ruleset, randomness).spawnNaturalWonders(map) 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 { class MapGenerationRandomness {