diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt index 16523ff2cd..c948b8f439 100644 --- a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyManager.kt @@ -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() + internal var flagsCountdown = HashMap() /** 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() diff --git a/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyTurnManager.kt b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyTurnManager.kt new file mode 100644 index 0000000000..13f380f2ae --- /dev/null +++ b/core/src/com/unciv/logic/civilization/diplomacy/DiplomacyTurnManager.kt @@ -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) + } + +} diff --git a/core/src/com/unciv/logic/civilization/managers/TurnManager.kt b/core/src/com/unciv/logic/civilization/managers/TurnManager.kt index fd79195f1b..847bae3a55 100644 --- a/core/src/com/unciv/logic/civilization/managers/TurnManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/TurnManager.kt @@ -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 diff --git a/tests/src/com/unciv/logic/civilization/DefensivePactTests.kt b/tests/src/com/unciv/logic/civilization/DefensivePactTests.kt index b0ac79a7d8..062478c5e8 100644 --- a/tests/src/com/unciv/logic/civilization/DefensivePactTests.kt +++ b/tests/src/com/unciv/logic/civilization/DefensivePactTests.kt @@ -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() diff --git a/tests/src/com/unciv/logic/civilization/diplomacy/DiplomacyManagerTests.kt b/tests/src/com/unciv/logic/civilization/diplomacy/DiplomacyManagerTests.kt index fd49ab7fd9..11826a3ae2 100644 --- a/tests/src/com/unciv/logic/civilization/diplomacy/DiplomacyManagerTests.kt +++ b/tests/src/com/unciv/logic/civilization/diplomacy/DiplomacyManagerTests.kt @@ -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