mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-04 23:40:01 +07:00
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:
@ -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 =
|
||||
|
@ -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<CivilizationInfo>, tileMap: TileMap, startScores: HashMap<TileInfo, Float>): HashMap<CivilizationInfo, TileInfo> {
|
||||
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<TileInfo>()
|
||||
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
|
||||
|
@ -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()
|
||||
|
@ -276,6 +276,10 @@ class CityInfoConquestFunctions(val city: CityInfo){
|
||||
|
||||
tryUpdateRoadStatus()
|
||||
cityStats.update()
|
||||
|
||||
// Update proximity rankings
|
||||
civInfo.updateProximity(oldCiv,
|
||||
oldCiv.updateProximity(civInfo))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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<String, DiplomacyManager>()
|
||||
var proximity = HashMap<String, Proximity>()
|
||||
var notifications = ArrayList<Notification>()
|
||||
val popupAlerts = ArrayList<PopupAlert>()
|
||||
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<String>)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ class TileMap {
|
||||
var mapParameters = MapParameters()
|
||||
|
||||
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)
|
||||
* @param position [Vector2] of the location
|
||||
@ -78,6 +79,9 @@ class TileMap {
|
||||
@Transient
|
||||
val startingLocationsByNation = HashMap<String,HashSet<TileInfo>>()
|
||||
|
||||
@Transient
|
||||
val landTilesInBigEnoughGroup = ArrayList<TileInfo>() // 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
|
||||
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user