chore: Extracted all 'next turn' logic from DiplomacyManager to DiplomacyTurnManager

This commit is contained in:
Yair Morgenstern
2023-10-08 11:48:33 +03:00
parent b2a29f1be2
commit 2bd65f9f42
5 changed files with 334 additions and 311 deletions

View File

@ -16,7 +16,6 @@ import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.components.extensions.toPercent
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sign
enum class RelationshipLevel(val color: Color) {
@ -128,7 +127,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
/** Contains various flags (declared war, promised to not settle, declined luxury trade) and the number of turns in which they will expire.
* The JSON serialize/deserialize REFUSES to deserialize hashmap keys as Enums, so I'm forced to use strings instead =(
* This is so sad Alexa play Despacito */
private var flagsCountdown = HashMap<String, Int>()
internal var flagsCountdown = HashMap<String, Int>()
/** For AI. Positive is good relations, negative is bad.
* Baseline is 1 point for each turn of peace - so declaring a war upends 40 years of peace, and e.g. capturing a city can be another 30 or 40.
@ -139,7 +138,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
* Access via getInfluence() and setInfluence() unless you know what you're doing.
* Note that not using the setter skips recalculating the ally and bounds checks,
* and skipping the getter bypasses the modified value when at war */
private var influence = 0f
internal var influence = 0f
/** Total of each turn Science during Research Agreement */
internal var totalOfScienceDuringRA = 0
@ -318,7 +317,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
fun getInfluence() = if (civInfo.isAtWarWith(otherCiv())) MINIMUM_INFLUENCE else influence
// To be run from City-State DiplomacyManager, which holds the influence. Resting point for every major civ can be different.
private fun getCityStateInfluenceRestingPoint(): Float {
internal fun getCityStateInfluenceRestingPoint(): Float {
var restingPoint = 0f
for (unique in otherCiv().getMatchingUniques(UniqueType.CityStateRestingPoint))
@ -336,7 +335,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
return restingPoint
}
private fun getCityStateInfluenceDegrade(): Float {
internal fun getCityStateInfluenceDegrade(): Float {
if (getInfluence() <= getCityStateInfluenceRestingPoint())
return 0f
@ -364,24 +363,6 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
return max(0f, decrement) * max(-100f, modifierPercent).toPercent()
}
private fun getCityStateInfluenceRecovery(): Float {
if (getInfluence() >= getCityStateInfluenceRestingPoint())
return 0f
val increment = 1f // sic: personality does not matter here
var modifierPercent = 0f
if (otherCiv().hasUnique(UniqueType.CityStateInfluenceRecoversTwiceNormalRate))
modifierPercent += 100f
val religion = if (civInfo.cities.isEmpty() || civInfo.getCapital() == null) 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
@ -401,14 +382,6 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
return goldPerTurnForUs
}
private fun scienceFromResearchAgreement() {
// https://forums.civfanatics.com/resources/research-agreements-bnw.25568/
val scienceFromResearchAgreement = min(totalOfScienceDuringRA, otherCivDiplomacy().totalOfScienceDuringRA)
civInfo.tech.scienceFromResearchAgreements += scienceFromResearchAgreement
otherCiv().tech.scienceFromResearchAgreements += scienceFromResearchAgreement
totalOfScienceDuringRA = 0
otherCivDiplomacy().totalOfScienceDuringRA = 0
}
fun resourcesFromTrade(): ResourceSupplyList {
val newResourceSupplyList = ResourceSupplyList()
@ -444,48 +417,6 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
//endregion
//region state-changing functions
private fun removeUntenableTrades() {
for (trade in trades.toList()) {
// Every cancelled trade can change this - if 1 resource is missing,
// don't cancel all trades of that resource, only cancel one (the first one, as it happens, since they're added chronologically)
val negativeCivResources = civInfo.getCivResourceSupply()
.filter { it.amount < 0 && !it.resource.isStockpiled() }.map { it.resource.name }
for (offer in trade.ourOffers) {
if (offer.type in listOf(TradeType.Luxury_Resource, TradeType.Strategic_Resource)
&& (offer.name in negativeCivResources || !civInfo.gameInfo.ruleset.tileResources.containsKey(offer.name))
) {
trades.remove(trade)
val otherCivTrades = otherCiv().getDiplomacyManager(civInfo).trades
otherCivTrades.removeAll { it.equalTrade(trade.reverse()) }
// Can't cut short peace treaties!
if (trade.theirOffers.any { it.name == Constants.peaceTreaty }) {
remakePeaceTreaty(trade.theirOffers.first { it.name == Constants.peaceTreaty }.duration)
}
civInfo.addNotification("One of our trades with [$otherCivName] has been cut short", NotificationCategory.Trade, NotificationIcon.Trade, otherCivName)
otherCiv().addNotification("One of our trades with [${civInfo.civName}] has been cut short", NotificationCategory.Trade, NotificationIcon.Trade, civInfo.civName)
civInfo.cache.updateCivResources()
}
}
}
}
private fun remakePeaceTreaty(durationLeft: Int) {
val treaty = Trade()
treaty.ourOffers.add(
TradeOffer(Constants.peaceTreaty, TradeType.Treaty, duration = durationLeft)
)
treaty.theirOffers.add(
TradeOffer(Constants.peaceTreaty, TradeType.Treaty, duration = durationLeft)
)
trades.add(treaty)
otherCiv().getDiplomacyManager(civInfo).trades.add(treaty)
}
// for performance reasons we don't want to call this every time we want to see if a unit can move through a tile
fun updateHasOpenBorders() {
// City-states can enter ally's territory (the opposite is true anyway even without open borders)
@ -503,229 +434,6 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
}
}
fun nextTurn() {
nextTurnTrades()
removeUntenableTrades()
updateHasOpenBorders()
nextTurnDiplomaticModifiers()
nextTurnFlags()
if (civInfo.isCityState() && otherCiv().isMajorCiv())
nextTurnCityStateInfluence()
}
private fun nextTurnCityStateInfluence() {
val initialRelationshipLevel = relationshipIgnoreAfraid() // Enough since only >= Friend is notified
val restingPoint = getCityStateInfluenceRestingPoint()
// We don't use `getInfluence()` here, as then during war with the ally of this CS,
// our influence would be set to -59, overwriting the old value, which we want to keep
// as it should be restored once the war ends (though we keep influence degradation from time during the war)
if (influence > restingPoint) {
val decrement = getCityStateInfluenceDegrade()
setInfluence(max(restingPoint, influence - decrement))
} else if (influence < restingPoint) {
val increment = getCityStateInfluenceRecovery()
setInfluence(min(restingPoint, influence + increment))
}
if (!civInfo.isDefeated()) { // don't display city state relationship notifications when the city state is currently defeated
val notificationActions = civInfo.cityStateFunctions.getNotificationActions()
if (getTurnsToRelationshipChange() == 1) {
val text = "Your relationship with [${civInfo.civName}] is about to degrade"
otherCiv().addNotification(text, notificationActions, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
}
if (initialRelationshipLevel >= RelationshipLevel.Friend && initialRelationshipLevel != relationshipIgnoreAfraid()) {
val text = "Your relationship with [${civInfo.civName}] degraded"
otherCiv().addNotification(text, notificationActions, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
}
// Potentially notify about afraid status
if (getInfluence() < 30 // We usually don't want to bully our friends
&& !hasFlag(DiplomacyFlags.NotifiedAfraid)
&& civInfo.cityStateFunctions.getTributeWillingness(otherCiv()) > 0
&& otherCiv().isMajorCiv()
) {
setFlag(DiplomacyFlags.NotifiedAfraid, 20) // Wait 20 turns until next reminder
val text = "[${civInfo.civName}] is afraid of your military power!"
otherCiv().addNotification(text, notificationActions, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
}
}
}
private fun nextTurnFlags() {
loop@ for (flag in flagsCountdown.keys.toList()) {
// No need to decrement negative countdown flags: they do not expire
if (flagsCountdown[flag]!! > 0)
flagsCountdown[flag] = flagsCountdown[flag]!! - 1
// If we have uniques that make city states grant military units faster when at war with a common enemy, add higher numbers to this flag
if (flag == DiplomacyFlags.ProvideMilitaryUnit.name && civInfo.isMajorCiv() && otherCiv().isCityState() &&
civInfo.gameInfo.civilizations.any { civInfo.isAtWarWith(it) && otherCiv().isAtWarWith(it) }) {
for (unique in civInfo.getMatchingUniques(UniqueType.CityStateMoreGiftedUnits)) {
flagsCountdown[DiplomacyFlags.ProvideMilitaryUnit.name] =
flagsCountdown[DiplomacyFlags.ProvideMilitaryUnit.name]!! - unique.params[0].toInt() + 1
if (flagsCountdown[DiplomacyFlags.ProvideMilitaryUnit.name]!! <= 0) {
flagsCountdown[DiplomacyFlags.ProvideMilitaryUnit.name] = 0
break
}
}
}
// At the end of every turn
if (flag == DiplomacyFlags.ResearchAgreement.name)
totalOfScienceDuringRA += civInfo.stats.statsForNextTurn.science.toInt()
// These modifiers decrease slightly @ 50
if (flagsCountdown[flag] == 50) {
when (flag) {
DiplomacyFlags.RememberAttackedProtectedMinor.name -> {
addModifier(DiplomaticModifiers.AttackedProtectedMinor, 5f)
}
DiplomacyFlags.RememberBulliedProtectedMinor.name -> {
addModifier(DiplomaticModifiers.BulliedProtectedMinor, 5f)
}
}
}
// Only when flag is expired
if (flagsCountdown[flag] == 0) {
when (flag) {
DiplomacyFlags.ResearchAgreement.name -> {
if (!otherCivDiplomacy().hasFlag(DiplomacyFlags.ResearchAgreement))
scienceFromResearchAgreement()
}
DiplomacyFlags.DefensivePact.name -> {
diplomaticStatus = DiplomaticStatus.Peace
}
// This is confusingly named - in fact, the civ that has the flag set is the MAJOR civ
DiplomacyFlags.ProvideMilitaryUnit.name -> {
// Do not unset the flag - they may return soon, and we'll continue from that point on
if (civInfo.cities.isEmpty() || otherCiv().cities.isEmpty())
continue@loop
else
otherCiv().cityStateFunctions.giveMilitaryUnitToPatron(civInfo)
}
DiplomacyFlags.AgreedToNotSettleNearUs.name -> {
addModifier(DiplomaticModifiers.FulfilledPromiseToNotSettleCitiesNearUs, 10f)
}
DiplomacyFlags.RecentlyAttacked.name -> {
civInfo.cityStateFunctions.askForUnitGifts(otherCiv())
}
// These modifiers don't tick down normally, instead there is a threshold number of turns
DiplomacyFlags.RememberDestroyedProtectedMinor.name -> { // 125
removeModifier(DiplomaticModifiers.DestroyedProtectedMinor)
}
DiplomacyFlags.RememberAttackedProtectedMinor.name -> { // 75
removeModifier(DiplomaticModifiers.AttackedProtectedMinor)
}
DiplomacyFlags.RememberBulliedProtectedMinor.name -> { // 75
removeModifier(DiplomaticModifiers.BulliedProtectedMinor)
}
DiplomacyFlags.RememberSidedWithProtectedMinor.name -> { // 25
removeModifier(DiplomaticModifiers.SidedWithProtectedMinor)
}
}
flagsCountdown.remove(flag)
}
}
}
private fun nextTurnTrades() {
for (trade in trades.toList()) {
for (offer in trade.ourOffers.union(trade.theirOffers).filter { it.duration > 0 }) {
offer.duration--
}
if (trade.ourOffers.all { it.duration <= 0 } && trade.theirOffers.all { it.duration <= 0 }) {
trades.remove(trade)
for (offer in trade.ourOffers.union(trade.theirOffers).filter { it.duration == 0 }) { // this was a timed trade
if (offer in trade.theirOffers)
civInfo.addNotification("[${offer.name}] from [$otherCivName] has ended", NotificationCategory.Trade, otherCivName, NotificationIcon.Trade)
else civInfo.addNotification("[${offer.name}] to [$otherCivName] has ended", NotificationCategory.Trade, otherCivName, NotificationIcon.Trade)
civInfo.updateStatsForNextTurn() // if they were bringing us gold per turn
if (trade.theirOffers.union(trade.ourOffers) // if resources were involved
.any { it.type == TradeType.Luxury_Resource || it.type == TradeType.Strategic_Resource })
civInfo.cache.updateCivResources()
}
}
for (offer in trade.theirOffers.filter { it.duration <= 3 })
{
if (offer.duration == 3)
civInfo.addNotification("[${offer.name}] from [$otherCivName] will end in [3] turns", NotificationCategory.Trade, otherCivName, NotificationIcon.Trade)
else if (offer.duration == 1)
civInfo.addNotification("[${offer.name}] from [$otherCivName] will end next turn", NotificationCategory.Trade, otherCivName, NotificationIcon.Trade)
}
}
}
private fun nextTurnDiplomaticModifiers() {
if (diplomaticStatus == DiplomaticStatus.Peace) {
if (getModifier(DiplomaticModifiers.YearsOfPeace) < 30)
addModifier(DiplomaticModifiers.YearsOfPeace, 0.5f)
} else revertToZero(DiplomaticModifiers.YearsOfPeace, 0.5f) // war makes you forget the good ol' days
var openBorders = 0
if (hasOpenBorders) openBorders += 1
if (otherCivDiplomacy().hasOpenBorders) openBorders += 1
if (openBorders > 0) addModifier(DiplomaticModifiers.OpenBorders, openBorders / 8f) // so if we both have open borders it'll grow by 0.25 per turn
else revertToZero(DiplomaticModifiers.OpenBorders, 1 / 8f)
// Negatives
revertToZero(DiplomaticModifiers.DeclaredWarOnUs, 1 / 8f) // this disappears real slow - it'll take 160 turns to really forget, this is war declaration we're talking about
revertToZero(DiplomaticModifiers.WarMongerer, 1 / 2f) // warmongering gives a big negative boost when it happens but they're forgotten relatively quickly, like WWII amirite
revertToZero(DiplomaticModifiers.CapturedOurCities, 1 / 4f) // if you captured our cities, though, that's harder to forget
revertToZero(DiplomaticModifiers.BetrayedDeclarationOfFriendship, 1 / 8f) // That's a bastardly thing to do
revertToZero(DiplomaticModifiers.BetrayedDefensivePact, 1 / 16f) // That's an outrageous thing to do
revertToZero(DiplomaticModifiers.RefusedToNotSettleCitiesNearUs, 1 / 4f)
revertToZero(DiplomaticModifiers.BetrayedPromiseToNotSettleCitiesNearUs, 1 / 8f) // That's a bastardly thing to do
revertToZero(DiplomaticModifiers.UnacceptableDemands, 1 / 4f)
revertToZero(DiplomaticModifiers.StealingTerritory, 1 / 4f)
revertToZero(DiplomaticModifiers.DenouncedOurAllies, 1 / 4f)
revertToZero(DiplomaticModifiers.DenouncedOurEnemies, 1 / 4f)
revertToZero(DiplomaticModifiers.Denunciation, 1 / 8f) // That's personal, it'll take a long time to fade
// Positives
revertToZero(DiplomaticModifiers.GaveUsUnits, 1 / 4f)
revertToZero(DiplomaticModifiers.LiberatedCity, 1 / 8f)
revertToZero(DiplomaticModifiers.GaveUsGifts, 1 / 4f)
setFriendshipBasedModifier()
setDefensivePactBasedModifier()
if (!hasFlag(DiplomacyFlags.DeclarationOfFriendship))
revertToZero(DiplomaticModifiers.DeclarationOfFriendship, 1 / 2f) //decreases slowly and will revert to full if it is declared later
if (!hasFlag(DiplomacyFlags.DefensivePact))
revertToZero(DiplomaticModifiers.DefensivePact, 1f)
if (!otherCiv().isCityState()) return
if (isRelationshipLevelLT(RelationshipLevel.Friend)) {
if (hasFlag(DiplomacyFlags.ProvideMilitaryUnit))
removeFlag(DiplomacyFlags.ProvideMilitaryUnit)
return
}
val variance = listOf(-1, 0, 1).random()
val provideMilitaryUnitUniques = civInfo.cityStateFunctions.getCityStateBonuses(otherCiv().cityStateType, relationshipIgnoreAfraid(), UniqueType.CityStateMilitaryUnits)
.filter { it.conditionalsApply(civInfo) }.toList()
if (provideMilitaryUnitUniques.isEmpty()) removeFlag(DiplomacyFlags.ProvideMilitaryUnit)
for (unique in provideMilitaryUnitUniques) {
// Reset the countdown if it has ended, or if we have longer to go than the current maximum (can happen when going from friend to ally)
if (!hasFlag(DiplomacyFlags.ProvideMilitaryUnit) || getFlag(DiplomacyFlags.ProvideMilitaryUnit) > unique.params[0].toInt()) {
setFlag(DiplomacyFlags.ProvideMilitaryUnit, unique.params[0].toInt() + variance)
}
}
}
/** Should only be called from makePeace */
private fun makePeaceOneSide() {
diplomaticStatus = DiplomaticStatus.Peace
@ -778,7 +486,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
diplomaticModifiers[modifier.name] = amount
}
private fun getModifier(modifier: DiplomaticModifiers): Float {
internal fun getModifier(modifier: DiplomaticModifiers): Float {
if (!hasModifier(modifier)) return 0f
return diplomaticModifiers[modifier.name]!!
}
@ -786,14 +494,6 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
internal fun removeModifier(modifier: DiplomaticModifiers) = diplomaticModifiers.remove(modifier.name)
fun hasModifier(modifier: DiplomaticModifiers) = diplomaticModifiers.containsKey(modifier.name)
/** @param amount always positive, so you don't need to think about it */
private fun revertToZero(modifier: DiplomaticModifiers, amount: Float) {
if (!hasModifier(modifier)) return
val currentAmount = getModifier(modifier)
if (currentAmount > 0) addModifier(modifier, -amount)
else addModifier(modifier, amount)
}
fun signDeclarationOfFriendship() {
setModifier(DiplomaticModifiers.DeclarationOfFriendship, 35f)
otherCivDiplomacy().setModifier(DiplomaticModifiers.DeclarationOfFriendship, 35f)
@ -816,7 +516,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
UniqueTriggerActivation.triggerCivwideUnique(unique, otherCiv())
}
private fun setFriendshipBasedModifier() {
internal fun setFriendshipBasedModifier() {
removeModifier(DiplomaticModifiers.DeclaredFriendshipWithOurAllies)
removeModifier(DiplomaticModifiers.DeclaredFriendshipWithOurEnemies)
for (thirdCiv in getCommonKnownCivs()
@ -863,7 +563,7 @@ class DiplomacyManager() : IsPartOfGameInfoSerialization {
UniqueTriggerActivation.triggerCivwideUnique(unique, otherCiv())
}
private fun setDefensivePactBasedModifier() {
internal fun setDefensivePactBasedModifier() {
removeModifier(DiplomaticModifiers.SignedDefensivePactWithOurAllies)
removeModifier(DiplomaticModifiers.SignedDefensivePactWithOurEnemies)
for (thirdCiv in getCommonKnownCivs()

View File

@ -0,0 +1,320 @@
package com.unciv.logic.civilization.diplomacy
import com.unciv.Constants
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.trade.Trade
import com.unciv.logic.trade.TradeOffer
import com.unciv.logic.trade.TradeType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.components.extensions.toPercent
import kotlin.math.max
import kotlin.math.min
object DiplomacyTurnManager {
fun DiplomacyManager.nextTurn() {
nextTurnTrades()
removeUntenableTrades()
updateHasOpenBorders()
nextTurnDiplomaticModifiers()
nextTurnFlags()
if (civInfo.isCityState() && otherCiv().isMajorCiv())
nextTurnCityStateInfluence()
}
private fun DiplomacyManager.removeUntenableTrades() {
for (trade in trades.toList()) {
// Every cancelled trade can change this - if 1 resource is missing,
// don't cancel all trades of that resource, only cancel one (the first one, as it happens, since they're added chronologically)
val negativeCivResources = civInfo.getCivResourceSupply()
.filter { it.amount < 0 && !it.resource.isStockpiled() }.map { it.resource.name }
for (offer in trade.ourOffers) {
if (offer.type in listOf(TradeType.Luxury_Resource, TradeType.Strategic_Resource)
&& (offer.name in negativeCivResources || !civInfo.gameInfo.ruleset.tileResources.containsKey(offer.name))
) {
trades.remove(trade)
val otherCivTrades = otherCiv().getDiplomacyManager(civInfo).trades
otherCivTrades.removeAll { it.equalTrade(trade.reverse()) }
// Can't cut short peace treaties!
if (trade.theirOffers.any { it.name == Constants.peaceTreaty }) {
remakePeaceTreaty(trade.theirOffers.first { it.name == Constants.peaceTreaty }.duration)
}
civInfo.addNotification("One of our trades with [$otherCivName] has been cut short", NotificationCategory.Trade, NotificationIcon.Trade, otherCivName)
otherCiv().addNotification("One of our trades with [${civInfo.civName}] has been cut short", NotificationCategory.Trade, NotificationIcon.Trade, civInfo.civName)
civInfo.cache.updateCivResources()
}
}
}
}
private fun DiplomacyManager.remakePeaceTreaty(durationLeft: Int) {
val treaty = Trade()
treaty.ourOffers.add(
TradeOffer(Constants.peaceTreaty, TradeType.Treaty, duration = durationLeft)
)
treaty.theirOffers.add(
TradeOffer(Constants.peaceTreaty, TradeType.Treaty, duration = durationLeft)
)
trades.add(treaty)
otherCiv().getDiplomacyManager(civInfo).trades.add(treaty)
}
private fun DiplomacyManager.nextTurnCityStateInfluence() {
val initialRelationshipLevel = relationshipIgnoreAfraid() // Enough since only >= Friend is notified
val restingPoint = getCityStateInfluenceRestingPoint()
// We don't use `getInfluence()` here, as then during war with the ally of this CS,
// our influence would be set to -59, overwriting the old value, which we want to keep
// as it should be restored once the war ends (though we keep influence degradation from time during the war)
if (influence > restingPoint) {
val decrement = getCityStateInfluenceDegrade()
setInfluence(max(restingPoint, influence - decrement))
} else if (influence < restingPoint) {
val increment = getCityStateInfluenceRecovery()
setInfluence(min(restingPoint, influence + increment))
}
if (!civInfo.isDefeated()) { // don't display city state relationship notifications when the city state is currently defeated
val notificationActions = civInfo.cityStateFunctions.getNotificationActions()
if (getTurnsToRelationshipChange() == 1) {
val text = "Your relationship with [${civInfo.civName}] is about to degrade"
otherCiv().addNotification(text, notificationActions, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
}
if (initialRelationshipLevel >= RelationshipLevel.Friend && initialRelationshipLevel != relationshipIgnoreAfraid()) {
val text = "Your relationship with [${civInfo.civName}] degraded"
otherCiv().addNotification(text, notificationActions, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
}
// Potentially notify about afraid status
if (getInfluence() < 30 // We usually don't want to bully our friends
&& !hasFlag(DiplomacyFlags.NotifiedAfraid)
&& civInfo.cityStateFunctions.getTributeWillingness(otherCiv()) > 0
&& otherCiv().isMajorCiv()
) {
setFlag(DiplomacyFlags.NotifiedAfraid, 20) // Wait 20 turns until next reminder
val text = "[${civInfo.civName}] is afraid of your military power!"
otherCiv().addNotification(text, notificationActions, NotificationCategory.Diplomacy, civInfo.civName, NotificationIcon.Diplomacy)
}
}
}
private fun DiplomacyManager.getCityStateInfluenceRecovery(): Float {
if (getInfluence() >= getCityStateInfluenceRestingPoint())
return 0f
val increment = 1f // sic: personality does not matter here
var modifierPercent = 0f
if (otherCiv().hasUnique(UniqueType.CityStateInfluenceRecoversTwiceNormalRate))
modifierPercent += 100f
val religion = if (civInfo.cities.isEmpty() || civInfo.getCapital() == null) 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()
}
private fun DiplomacyManager.nextTurnFlags() {
loop@ for (flag in flagsCountdown.keys.toList()) {
// No need to decrement negative countdown flags: they do not expire
if (flagsCountdown[flag]!! > 0)
flagsCountdown[flag] = flagsCountdown[flag]!! - 1
// If we have uniques that make city states grant military units faster when at war with a common enemy, add higher numbers to this flag
if (flag == DiplomacyFlags.ProvideMilitaryUnit.name && civInfo.isMajorCiv() && otherCiv().isCityState() &&
civInfo.gameInfo.civilizations.any { civInfo.isAtWarWith(it) && otherCiv().isAtWarWith(it) }) {
for (unique in civInfo.getMatchingUniques(UniqueType.CityStateMoreGiftedUnits)) {
flagsCountdown[DiplomacyFlags.ProvideMilitaryUnit.name] =
flagsCountdown[DiplomacyFlags.ProvideMilitaryUnit.name]!! - unique.params[0].toInt() + 1
if (flagsCountdown[DiplomacyFlags.ProvideMilitaryUnit.name]!! <= 0) {
flagsCountdown[DiplomacyFlags.ProvideMilitaryUnit.name] = 0
break
}
}
}
// At the end of every turn
if (flag == DiplomacyFlags.ResearchAgreement.name)
totalOfScienceDuringRA += civInfo.stats.statsForNextTurn.science.toInt()
// These modifiers decrease slightly @ 50
if (flagsCountdown[flag] == 50) {
when (flag) {
DiplomacyFlags.RememberAttackedProtectedMinor.name -> {
addModifier(DiplomaticModifiers.AttackedProtectedMinor, 5f)
}
DiplomacyFlags.RememberBulliedProtectedMinor.name -> {
addModifier(DiplomaticModifiers.BulliedProtectedMinor, 5f)
}
}
}
// Only when flag is expired
if (flagsCountdown[flag] == 0) {
when (flag) {
DiplomacyFlags.ResearchAgreement.name -> {
if (!otherCivDiplomacy().hasFlag(DiplomacyFlags.ResearchAgreement))
scienceFromResearchAgreement()
}
DiplomacyFlags.DefensivePact.name -> {
diplomaticStatus = DiplomaticStatus.Peace
}
// This is confusingly named - in fact, the civ that has the flag set is the MAJOR civ
DiplomacyFlags.ProvideMilitaryUnit.name -> {
// Do not unset the flag - they may return soon, and we'll continue from that point on
if (civInfo.cities.isEmpty() || otherCiv().cities.isEmpty())
continue@loop
else
otherCiv().cityStateFunctions.giveMilitaryUnitToPatron(civInfo)
}
DiplomacyFlags.AgreedToNotSettleNearUs.name -> {
addModifier(DiplomaticModifiers.FulfilledPromiseToNotSettleCitiesNearUs, 10f)
}
DiplomacyFlags.RecentlyAttacked.name -> {
civInfo.cityStateFunctions.askForUnitGifts(otherCiv())
}
// These modifiers don't tick down normally, instead there is a threshold number of turns
DiplomacyFlags.RememberDestroyedProtectedMinor.name -> { // 125
removeModifier(DiplomaticModifiers.DestroyedProtectedMinor)
}
DiplomacyFlags.RememberAttackedProtectedMinor.name -> { // 75
removeModifier(DiplomaticModifiers.AttackedProtectedMinor)
}
DiplomacyFlags.RememberBulliedProtectedMinor.name -> { // 75
removeModifier(DiplomaticModifiers.BulliedProtectedMinor)
}
DiplomacyFlags.RememberSidedWithProtectedMinor.name -> { // 25
removeModifier(DiplomaticModifiers.SidedWithProtectedMinor)
}
}
flagsCountdown.remove(flag)
}
}
}
private fun DiplomacyManager.scienceFromResearchAgreement() {
// https://forums.civfanatics.com/resources/research-agreements-bnw.25568/
val scienceFromResearchAgreement = min(totalOfScienceDuringRA, otherCivDiplomacy().totalOfScienceDuringRA)
civInfo.tech.scienceFromResearchAgreements += scienceFromResearchAgreement
otherCiv().tech.scienceFromResearchAgreements += scienceFromResearchAgreement
totalOfScienceDuringRA = 0
otherCivDiplomacy().totalOfScienceDuringRA = 0
}
private fun DiplomacyManager.nextTurnTrades() {
for (trade in trades.toList()) {
for (offer in trade.ourOffers.union(trade.theirOffers).filter { it.duration > 0 }) {
offer.duration--
}
if (trade.ourOffers.all { it.duration <= 0 } && trade.theirOffers.all { it.duration <= 0 }) {
trades.remove(trade)
for (offer in trade.ourOffers.union(trade.theirOffers).filter { it.duration == 0 }) { // this was a timed trade
if (offer in trade.theirOffers)
civInfo.addNotification("[${offer.name}] from [$otherCivName] has ended", NotificationCategory.Trade, otherCivName, NotificationIcon.Trade)
else civInfo.addNotification("[${offer.name}] to [$otherCivName] has ended", NotificationCategory.Trade, otherCivName, NotificationIcon.Trade)
civInfo.updateStatsForNextTurn() // if they were bringing us gold per turn
if (trade.theirOffers.union(trade.ourOffers) // if resources were involved
.any { it.type == TradeType.Luxury_Resource || it.type == TradeType.Strategic_Resource })
civInfo.cache.updateCivResources()
}
}
for (offer in trade.theirOffers.filter { it.duration <= 3 })
{
if (offer.duration == 3)
civInfo.addNotification("[${offer.name}] from [$otherCivName] will end in [3] turns", NotificationCategory.Trade, otherCivName, NotificationIcon.Trade)
else if (offer.duration == 1)
civInfo.addNotification("[${offer.name}] from [$otherCivName] will end next turn", NotificationCategory.Trade, otherCivName, NotificationIcon.Trade)
}
}
}
private fun DiplomacyManager.nextTurnDiplomaticModifiers() {
if (diplomaticStatus == DiplomaticStatus.Peace) {
if (getModifier(DiplomaticModifiers.YearsOfPeace) < 30)
addModifier(DiplomaticModifiers.YearsOfPeace, 0.5f)
} else revertToZero(DiplomaticModifiers.YearsOfPeace, 0.5f) // war makes you forget the good ol' days
var openBorders = 0
if (hasOpenBorders) openBorders += 1
if (otherCivDiplomacy().hasOpenBorders) openBorders += 1
if (openBorders > 0) addModifier(DiplomaticModifiers.OpenBorders, openBorders / 8f) // so if we both have open borders it'll grow by 0.25 per turn
else revertToZero(DiplomaticModifiers.OpenBorders, 1 / 8f)
// Negatives
revertToZero(DiplomaticModifiers.DeclaredWarOnUs, 1 / 8f) // this disappears real slow - it'll take 160 turns to really forget, this is war declaration we're talking about
revertToZero(DiplomaticModifiers.WarMongerer, 1 / 2f) // warmongering gives a big negative boost when it happens but they're forgotten relatively quickly, like WWII amirite
revertToZero(DiplomaticModifiers.CapturedOurCities, 1 / 4f) // if you captured our cities, though, that's harder to forget
revertToZero(DiplomaticModifiers.BetrayedDeclarationOfFriendship, 1 / 8f) // That's a bastardly thing to do
revertToZero(DiplomaticModifiers.BetrayedDefensivePact, 1 / 16f) // That's an outrageous thing to do
revertToZero(DiplomaticModifiers.RefusedToNotSettleCitiesNearUs, 1 / 4f)
revertToZero(DiplomaticModifiers.BetrayedPromiseToNotSettleCitiesNearUs, 1 / 8f) // That's a bastardly thing to do
revertToZero(DiplomaticModifiers.UnacceptableDemands, 1 / 4f)
revertToZero(DiplomaticModifiers.StealingTerritory, 1 / 4f)
revertToZero(DiplomaticModifiers.DenouncedOurAllies, 1 / 4f)
revertToZero(DiplomaticModifiers.DenouncedOurEnemies, 1 / 4f)
revertToZero(DiplomaticModifiers.Denunciation, 1 / 8f) // That's personal, it'll take a long time to fade
// Positives
revertToZero(DiplomaticModifiers.GaveUsUnits, 1 / 4f)
revertToZero(DiplomaticModifiers.LiberatedCity, 1 / 8f)
revertToZero(DiplomaticModifiers.GaveUsGifts, 1 / 4f)
setFriendshipBasedModifier()
setDefensivePactBasedModifier()
if (!hasFlag(DiplomacyFlags.DeclarationOfFriendship))
revertToZero(DiplomaticModifiers.DeclarationOfFriendship, 1 / 2f) //decreases slowly and will revert to full if it is declared later
if (!hasFlag(DiplomacyFlags.DefensivePact))
revertToZero(DiplomaticModifiers.DefensivePact, 1f)
if (!otherCiv().isCityState()) return
if (isRelationshipLevelLT(RelationshipLevel.Friend)) {
if (hasFlag(DiplomacyFlags.ProvideMilitaryUnit))
removeFlag(DiplomacyFlags.ProvideMilitaryUnit)
return
}
val variance = listOf(-1, 0, 1).random()
val provideMilitaryUnitUniques = civInfo.cityStateFunctions.getCityStateBonuses(otherCiv().cityStateType, relationshipIgnoreAfraid(), UniqueType.CityStateMilitaryUnits)
.filter { it.conditionalsApply(civInfo) }.toList()
if (provideMilitaryUnitUniques.isEmpty()) removeFlag(DiplomacyFlags.ProvideMilitaryUnit)
for (unique in provideMilitaryUnitUniques) {
// Reset the countdown if it has ended, or if we have longer to go than the current maximum (can happen when going from friend to ally)
if (!hasFlag(DiplomacyFlags.ProvideMilitaryUnit) || getFlag(DiplomacyFlags.ProvideMilitaryUnit) > unique.params[0].toInt()) {
setFlag(DiplomacyFlags.ProvideMilitaryUnit, unique.params[0].toInt() + variance)
}
}
}
/** @param amount always positive, so you don't need to think about it */
private fun DiplomacyManager.revertToZero(modifier: DiplomaticModifiers, amount: Float) {
if (!hasModifier(modifier)) return
val currentAmount = getModifier(modifier)
if (currentAmount > 0) addModifier(modifier, -amount)
else addModifier(modifier, amount)
}
}

View File

@ -11,6 +11,7 @@ import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.civilization.PopupAlert
import com.unciv.logic.civilization.diplomacy.DiplomacyTurnManager.nextTurn
import com.unciv.logic.map.mapunit.UnitTurnManager
import com.unciv.logic.map.tile.Tile
import com.unciv.logic.trade.TradeEvaluation

View File

@ -1,4 +1,5 @@
package com.unciv.logic.civilization
import com.unciv.logic.civilization.diplomacy.DiplomacyTurnManager.nextTurn
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.testing.GdxTestRunner
@ -24,8 +25,8 @@ class DefensivePactTests {
a.diplomacyFunctions.makeCivilizationsMeet(c)
b.diplomacyFunctions.makeCivilizationsMeet(c)
}
@Test
@Test
fun `Civs with Defensive Pacts are called in`() {
meetAll()
a.getDiplomacyManager(b).signDefensivePact(100)
@ -59,7 +60,7 @@ class DefensivePactTests {
Assert.assertFalse(a.getDiplomacyManager(b).diplomaticStatus == DiplomaticStatus.DefensivePact
|| b.getDiplomacyManager(a).diplomaticStatus == DiplomaticStatus.DefensivePact)
}
@Test
fun `Breaking Defensive Pact`() {
meetAll()
@ -70,7 +71,7 @@ class DefensivePactTests {
Assert.assertTrue(abDiploManager.hasModifier(DiplomaticModifiers.DefensivePact)) // Aggressor should still be friendly to the defender, they did nothing wrong
Assert.assertFalse(abDiploManager.otherCivDiplomacy().hasModifier(DiplomaticModifiers.DefensivePact)) // Defender should not be friendly to the aggressor.
}
@Test
fun `Breaking Defensive Pact with friends`() {
meetAll()

View File

@ -2,6 +2,7 @@ package com.unciv.logic.civilization.diplomacy
import com.badlogic.gdx.math.Vector2
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.diplomacy.DiplomacyTurnManager.nextTurn
import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.BeliefType
import com.unciv.testing.GdxTestRunner