Skip to content
Merged
1 change: 1 addition & 0 deletions android/assets/jsons/translations/template.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
23 changes: 23 additions & 0 deletions core/src/com/unciv/logic/city/CityInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -543,6 +556,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()
Expand Down
4 changes: 4 additions & 0 deletions core/src/com/unciv/logic/city/CityInfoConquestFunctions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,10 @@ class CityInfoConquestFunctions(val city: CityInfo){

tryUpdateRoadStatus()
cityStats.update()

// Update proximity rankings
civInfo.updateProximity(oldCiv,
oldCiv.updateProximity(civInfo))
}
}

Expand Down
57 changes: 52 additions & 5 deletions core/src/com/unciv/logic/civilization/CityStateFunctions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -505,10 +504,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
Expand Down
99 changes: 99 additions & 0 deletions core/src/com/unciv/logic/civilization/CivilizationInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +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.MapType
import com.unciv.logic.map.MapUnit
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.UnitMovementAlgorithms
Expand All @@ -33,6 +34,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
Expand Down Expand Up @@ -107,6 +116,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
Expand Down Expand Up @@ -138,6 +148,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) {
Expand All @@ -161,6 +174,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.
Expand All @@ -179,6 +193,7 @@ class CivilizationInfo {
toReturn.temporaryUniques.addAll(temporaryUniques)
toReturn.boughtConstructionsWithGloballyIncreasingPrice.putAll(boughtConstructionsWithGloballyIncreasingPrice)
toReturn.hasEverOwnedOriginalCapital = hasEverOwnedOriginalCapital
toReturn.numMinorCivsAttacked = numMinorCivsAttacked
return toReturn
}

Expand All @@ -191,6 +206,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)
Expand Down Expand Up @@ -541,6 +559,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
Expand Down Expand Up @@ -595,6 +616,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()
Expand Down Expand Up @@ -895,6 +920,80 @@ 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 height = gameInfo.tileMap.mapParameters.mapSize.height
val width = gameInfo.tileMap.mapParameters.mapSize.width
var minDistance = height + width // 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 = (height + width) / 2 // Perhaps slightly lower for hexagonal maps??
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.mapParameters.type != MapType.archipelago) {

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>)
Expand Down
Loading