diff --git a/CSharpSourceCode/CampaignMechanics/CharacterCreation/SpecializationOption.cs b/CSharpSourceCode/CampaignMechanics/CharacterCreation/SpecializationOption.cs
index f8a49cc..0c39219 100644
--- a/CSharpSourceCode/CampaignMechanics/CharacterCreation/SpecializationOption.cs
+++ b/CSharpSourceCode/CampaignMechanics/CharacterCreation/SpecializationOption.cs
@@ -93,3 +93,4 @@ public class SpecializationOption
public string[] AttributesToIncrease;
}
}
+
diff --git a/CSharpSourceCode/CampaignMechanics/Crafting/PriestBehavior.cs b/CSharpSourceCode/CampaignMechanics/Crafting/PriestBehavior.cs
index 969724e..f271035 100644
--- a/CSharpSourceCode/CampaignMechanics/Crafting/PriestBehavior.cs
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/PriestBehavior.cs
@@ -203,7 +203,10 @@ bool PlayerMeetsRequirements(string cult)
{
if (!Hero.MainHero.HasAnyReligion()) return false;
- if (Hero.MainHero.IsPriest() && Hero.MainHero.GetDominantReligion().Affinity == ReligionAffinity.Order) return true;
+ // A priest can access any priest of the same Pantheon
+ var cultReligion = ReligionObject.All.FirstOrDefault(x => x.StringId == cult);
+ if (Hero.MainHero.IsPriest() && cultReligion != null &&
+ Hero.MainHero.GetDominantReligion().Pantheon == cultReligion.Pantheon) return true;
if (Hero.MainHero.GetDominantReligion().StringId == cult) return true;
diff --git a/CSharpSourceCode/CampaignMechanics/Diplomacy/HonorAllianceDecision.cs b/CSharpSourceCode/CampaignMechanics/Diplomacy/HonorAllianceDecision.cs
new file mode 100644
index 0000000..6c5cd77
--- /dev/null
+++ b/CSharpSourceCode/CampaignMechanics/Diplomacy/HonorAllianceDecision.cs
@@ -0,0 +1,411 @@
+using System.Collections.Generic;
+using System.Linq;
+using TaleWorlds.CampaignSystem;
+using TaleWorlds.CampaignSystem.Actions;
+using TaleWorlds.CampaignSystem.CampaignBehaviors;
+using TaleWorlds.CampaignSystem.CharacterDevelopment;
+using TaleWorlds.CampaignSystem.Election;
+using TaleWorlds.Core.ImageIdentifiers;
+using TaleWorlds.Library;
+using TaleWorlds.Localization;
+using TaleWorlds.SaveSystem;
+using TOR_Core.Models;
+
+namespace TOR_Core.CampaignMechanics.Diplomacy
+{
+ ///
+ /// Total War-style alliance decision: When an ally is attacked, you must choose to
+ /// either join the war or break the alliance. There is no option to refuse and keep the alliance.
+ ///
+ public class HonorAllianceDecision : KingdomDecision
+ {
+ [SaveableField(101)]
+ public readonly Kingdom AttackedAlly;
+
+ [SaveableField(102)]
+ public readonly Kingdom Attacker;
+
+ public HonorAllianceDecision(Clan proposerClan, Kingdom attackedAlly, Kingdom attacker)
+ : base(proposerClan)
+ {
+ AttackedAlly = attackedAlly;
+ Attacker = attacker;
+ }
+
+ // Immediate decision - no waiting
+ protected override int HoursToWait => 0;
+
+ public override bool IsAllowed()
+ {
+ // Check if the alliance still exists and the war is still ongoing
+ return Kingdom.IsAllyWith(AttackedAlly) &&
+ AttackedAlly.IsAtWarWith(Attacker) &&
+ !Kingdom.IsAtWarWith(Attacker) &&
+ !AttackedAlly.IsEliminated &&
+ !Attacker.IsEliminated;
+ }
+
+ // No influence cost - this is an automatic obligation
+ public override int GetProposalInfluenceCost() => 0;
+
+ public override TextObject GetGeneralTitle()
+ {
+ var text = new TextObject("{=TOR_Honor_Alliance_Title}Honor your alliance with {ALLY} against {ATTACKER}");
+ text.SetTextVariable("ALLY", AttackedAlly.Name);
+ text.SetTextVariable("ATTACKER", Attacker.Name);
+ return text;
+ }
+
+ public override TextObject GetSupportTitle()
+ {
+ var text = new TextObject("{=TOR_Honor_Alliance_Support}Vote on whether to honor the alliance with {ALLY} by joining the war against {ATTACKER}, or to break the alliance.");
+ text.SetTextVariable("ALLY", AttackedAlly.Name);
+ text.SetTextVariable("ATTACKER", Attacker.Name);
+ return text;
+ }
+
+ public override TextObject GetChooseTitle()
+ {
+ var text = new TextObject("{=TOR_Honor_Alliance_Choose}Honor Alliance with {ALLY} Against {ATTACKER}");
+ text.SetTextVariable("ALLY", AttackedAlly.Name);
+ text.SetTextVariable("ATTACKER", Attacker.Name);
+ return text;
+ }
+
+ public override TextObject GetSupportDescription()
+ {
+ var text = new TextObject("{=TOR_Honor_Alliance_Support_Desc}{KINGDOM_LEADER} must decide: Will your realm honor its alliance with {ALLY} by joining the war against {ATTACKER}, or will the alliance be dissolved?");
+ text.SetTextVariable("KINGDOM_LEADER", DetermineChooser().Leader.Name);
+ text.SetTextVariable("ALLY", AttackedAlly.Name);
+ text.SetTextVariable("ATTACKER", Attacker.Name);
+ return text;
+ }
+
+ public override TextObject GetChooseDescription()
+ {
+ var text = new TextObject("{=TOR_Honor_Alliance_Choose_Desc}Your ally {ALLY} has been attacked by {ATTACKER}. As a true ally, you must choose: Join the war to defend your ally, or dissolve the alliance and stay out of the conflict.");
+ text.SetTextVariable("ALLY", AttackedAlly.Name);
+ text.SetTextVariable("ATTACKER", Attacker.Name);
+ return text;
+ }
+
+ public override IEnumerable DetermineInitialCandidates()
+ {
+ // Option 1: Join the war (honor alliance)
+ yield return new HonorAllianceOutcome(true, Kingdom, AttackedAlly, Attacker);
+ // Option 2: Break the alliance (refuse to join)
+ yield return new HonorAllianceOutcome(false, Kingdom, AttackedAlly, Attacker);
+ }
+
+ public override Clan DetermineChooser() => Kingdom.RulingClan;
+
+ protected override bool ShouldBeCancelledInternal()
+ {
+ // Only cancel if the decision conditions are no longer valid
+ // Do NOT cancel based on proposer support (base class does this)
+ return !CanMakeDecision(out _, false);
+ }
+
+ // Allow proposer clan to change opinion without cancelling the decision
+ // This is important for HonorAllianceDecision - even if the ruling clan
+ // changes their mind, the decision should still be voted on
+ protected override bool CanProposerClanChangeOpinion() => true;
+
+ public override void DetermineSponsors(MBReadOnlyList possibleOutcomes)
+ {
+ foreach (var outcome in possibleOutcomes)
+ {
+ var honorOutcome = (HonorAllianceOutcome)outcome;
+ if (honorOutcome.ShouldJoinWar)
+ {
+ // Proposer sponsors joining the war
+ outcome.SetSponsor(ProposerClan);
+ }
+ else
+ {
+ AssignDefaultSponsor(outcome);
+ }
+ }
+ }
+
+ public override float DetermineSupport(Clan clan, DecisionOutcome possibleOutcome)
+ {
+ var honorOutcome = (HonorAllianceOutcome)possibleOutcome;
+ float support = CalculateJoinWarSupport(clan);
+
+ // Return positive support for join, negative for break (or vice versa)
+ return honorOutcome.ShouldJoinWar ? support : -support;
+ }
+
+ ///
+ /// Public accessor for AI decision resolution.
+ ///
+ public float CalculateJoinWarSupportPublic(Clan clan) => CalculateJoinWarSupport(clan);
+
+ ///
+ /// Calculate how much a clan supports joining the war.
+ /// Delegates to TORAllianceModel for trait-modified scoring.
+ ///
+ private float CalculateJoinWarSupport(Clan clan)
+ {
+ var allianceModel = Campaign.Current?.Models?.AllianceModel as TORAllianceModel;
+ if (allianceModel != null)
+ {
+ return allianceModel.CalculateHonorAllianceSupport(clan, Kingdom, AttackedAlly, Attacker);
+ }
+
+ // Fallback if model not available
+ return 0f;
+ }
+
+ public override void ApplyChosenOutcome(DecisionOutcome chosenOutcome)
+ {
+ var honorOutcome = (HonorAllianceOutcome)chosenOutcome;
+
+ if (honorOutcome.ShouldJoinWar)
+ {
+ // Mark as alliance war (doesn't count toward offensive war limit)
+ var allianceWarBehavior = Campaign.Current.GetCampaignBehavior();
+ allianceWarBehavior?.MarkAsAllianceWar(Kingdom, Attacker);
+
+ // Declare war
+ //DeclareWarAction.ApplyByKingdomDecision(Kingdom, Attacker);
+
+ DeclareWarAction.ApplyByCallToWarAgreement(Kingdom, Attacker);
+
+ // Notify
+ if (Kingdom == Clan.PlayerClan?.Kingdom)
+ {
+ var message = new TextObject("{=TOR_Alliance_War_Joined}Your kingdom has joined the war against {ATTACKER} to honor your alliance with {ALLY}.");
+ message.SetTextVariable("ATTACKER", Attacker.Name);
+ message.SetTextVariable("ALLY", AttackedAlly.Name);
+ InformationManager.DisplayMessage(new InformationMessage(message.ToString(), Colors.Yellow));
+ }
+ }
+ else
+ {
+ // Break the alliance
+ var allianceBehavior = Campaign.Current.GetCampaignBehavior();
+ allianceBehavior?.EndAlliance(Kingdom, AttackedAlly);
+
+ // Major relationship penalty for breaking alliance in time of need
+ // This is a betrayal - the ally will remember
+ ApplyBrokenAllianceRelationPenalty();
+
+ // Notify
+ if (Kingdom == Clan.PlayerClan?.Kingdom)
+ {
+ var message = new TextObject("{=TOR_Alliance_Broken}Your alliance with {ALLY} has been dissolved - your kingdom refused to join the war against {ATTACKER}. This betrayal will not be forgotten.");
+ message.SetTextVariable("ALLY", AttackedAlly.Name);
+ message.SetTextVariable("ATTACKER", Attacker.Name);
+ InformationManager.DisplayMessage(new InformationMessage(message.ToString(), Colors.Red));
+ }
+ }
+ }
+
+ ///
+ /// Applies relationship penalties when breaking an alliance during wartime.
+ /// This is considered a major betrayal.
+ ///
+ private void ApplyBrokenAllianceRelationPenalty()
+ {
+ // Major penalty between rulers (-40 is significant)
+ const int rulerPenalty = -40;
+ const int clanPenalty = -20;
+
+ // Ruler to ruler relationship damage
+ if (Kingdom.Leader != null && AttackedAlly.Leader != null)
+ {
+ ChangeRelationAction.ApplyRelationChangeBetweenHeroes(
+ Kingdom.Leader,
+ AttackedAlly.Leader,
+ rulerPenalty,
+ true);
+ }
+
+ // All clans in the betrayed kingdom lose respect for the betrayer's ruler
+ foreach (var clan in AttackedAlly.Clans)
+ {
+ if (clan.Leader != null && clan.Leader != AttackedAlly.Leader && Kingdom.Leader != null)
+ {
+ ChangeRelationAction.ApplyRelationChangeBetweenHeroes(
+ Kingdom.Leader,
+ clan.Leader,
+ clanPenalty,
+ false); // Don't show notification for each clan
+ }
+ }
+
+ // Only honorable lords in the betrayer's kingdom disapprove of the dishonorable act
+ foreach (var clan in Kingdom.Clans)
+ {
+ if (clan != Kingdom.RulingClan && clan.Leader != null && Kingdom.Leader != null)
+ {
+ int honorLevel = clan.Leader.GetTraitLevel(DefaultTraits.Honor);
+
+ // Only honorable lords (honor > 0) care about this betrayal
+ if (honorLevel > 0)
+ {
+ int disapproval = -10 * honorLevel; // -10 per honor level (max -30 at honor 3)
+
+ ChangeRelationAction.ApplyRelationChangeBetweenHeroes(
+ Kingdom.Leader,
+ clan.Leader,
+ disapproval,
+ false);
+ }
+ }
+ }
+ }
+
+ public override void ApplySecondaryEffects(MBReadOnlyList possibleOutcomes, DecisionOutcome chosenOutcome)
+ {
+ // Relation changes are handled in ApplyChosenOutcome via ApplyBrokenAllianceRelationPenalty
+ }
+
+ public override TextObject GetSecondaryEffects()
+ {
+ return new TextObject("{=TOR_Honor_Alliance_Effects}Breaking the alliance may damage your realm's reputation.");
+ }
+
+ public override TextObject GetChosenOutcomeText(DecisionOutcome chosenOutcome, SupportStatus supportStatus, bool isShortVersion = false)
+ {
+ var honorOutcome = (HonorAllianceOutcome)chosenOutcome;
+ TextObject text;
+
+ if (honorOutcome.ShouldJoinWar)
+ {
+ text = new TextObject("{=TOR_Honor_Alliance_Joined}{KINGDOM} has honored its alliance with {ALLY} and joined the war against {ATTACKER}.");
+ }
+ else
+ {
+ text = new TextObject("{=TOR_Honor_Alliance_Dissolved}{KINGDOM} has dissolved its alliance with {ALLY} rather than join the war against {ATTACKER}.");
+ }
+
+ text.SetTextVariable("KINGDOM", Kingdom.Name);
+ text.SetTextVariable("ALLY", AttackedAlly.Name);
+ text.SetTextVariable("ATTACKER", Attacker.Name);
+ return text;
+ }
+
+ public override DecisionOutcome GetQueriedDecisionOutcome(MBReadOnlyList possibleOutcomes)
+ {
+ // Default query is for joining the war
+ return possibleOutcomes.FirstOrDefault(t => ((HonorAllianceOutcome)t).ShouldJoinWar);
+ }
+
+ public override bool CanMakeDecision(out TextObject reason, bool includeReason = false)
+ {
+ reason = includeReason ? TextObject.GetEmpty() : null;
+
+ if (AttackedAlly.IsEliminated || Kingdom.IsEliminated || Attacker.IsEliminated)
+ {
+ if (includeReason)
+ reason = new TextObject("{=TOR_Realm_Eliminated}That realm has been eliminated.");
+ return false;
+ }
+
+ if (Kingdom.IsAtWarWith(Attacker))
+ {
+ if (includeReason)
+ {
+ reason = new TextObject("{=TOR_Already_At_War}Your realm is already at war with {KINGDOM}.");
+ reason.SetTextVariable("KINGDOM", Attacker.Name);
+ }
+ return false;
+ }
+
+ if (!AttackedAlly.IsAtWarWith(Attacker))
+ {
+ if (includeReason)
+ {
+ reason = new TextObject("{=TOR_War_Ended}{ALLY} is no longer at war with {ATTACKER}.");
+ reason.SetTextVariable("ALLY", AttackedAlly.Name);
+ reason.SetTextVariable("ATTACKER", Attacker.Name);
+ }
+ return false;
+ }
+
+ if (!Kingdom.IsAllyWith(AttackedAlly))
+ {
+ if (includeReason)
+ {
+ reason = new TextObject("{=TOR_No_Alliance}Your realm is no longer allied with {ALLY}.");
+ reason.SetTextVariable("ALLY", AttackedAlly.Name);
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// Decision outcome for honoring alliance.
+ ///
+ public class HonorAllianceOutcome : DecisionOutcome
+ {
+ [SaveableField(100)]
+ public readonly bool ShouldJoinWar;
+
+ [SaveableField(101)]
+ public readonly Kingdom Kingdom;
+
+ [SaveableField(102)]
+ public readonly Kingdom AttackedAlly;
+
+ [SaveableField(103)]
+ public readonly Kingdom Attacker;
+
+ public HonorAllianceOutcome(bool shouldJoinWar, Kingdom kingdom, Kingdom attackedAlly, Kingdom attacker)
+ {
+ ShouldJoinWar = shouldJoinWar;
+ Kingdom = kingdom;
+ AttackedAlly = attackedAlly;
+ Attacker = attacker;
+ }
+
+ public override TextObject GetDecisionTitle()
+ {
+ if (ShouldJoinWar)
+ return new TextObject("{=TOR_Join_War}Join the War");
+ else
+ return new TextObject("{=TOR_Break_Alliance}Break the Alliance");
+ }
+
+ public override TextObject GetDecisionDescription()
+ {
+ if (ShouldJoinWar)
+ {
+ var text = new TextObject("{=TOR_Join_War_Desc}Honor our alliance with {ALLY} and declare war on {ATTACKER}.");
+ text.SetTextVariable("ALLY", AttackedAlly.Name);
+ text.SetTextVariable("ATTACKER", Attacker.Name);
+ return text;
+ }
+ else
+ {
+ var text = new TextObject("{=TOR_Break_Alliance_Desc}Dissolve our alliance with {ALLY} rather than join this war.");
+ text.SetTextVariable("ALLY", AttackedAlly.Name);
+ return text;
+ }
+ }
+
+ public override string GetDecisionLink() => null;
+
+ public override ImageIdentifier GetDecisionImageIdentifier() => null;
+ }
+ }
+
+ ///
+ /// Type definer for save/load of HonorAllianceDecision.
+ ///
+ public class HonorAllianceDecisionTypeDefiner : SaveableTypeDefiner
+ {
+ public HonorAllianceDecisionTypeDefiner() : base(789_124) { }
+
+ protected override void DefineClassTypes()
+ {
+ AddClassDefinition(typeof(HonorAllianceDecision), 1);
+ AddClassDefinition(typeof(HonorAllianceDecision.HonorAllianceOutcome), 2);
+ }
+ }
+}
diff --git a/CSharpSourceCode/CampaignMechanics/Diplomacy/TORAllianceWarBehavior.cs b/CSharpSourceCode/CampaignMechanics/Diplomacy/TORAllianceWarBehavior.cs
new file mode 100644
index 0000000..f44f4fb
--- /dev/null
+++ b/CSharpSourceCode/CampaignMechanics/Diplomacy/TORAllianceWarBehavior.cs
@@ -0,0 +1,265 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using TaleWorlds.CampaignSystem;
+using TaleWorlds.CampaignSystem.Actions;
+using TaleWorlds.CampaignSystem.CampaignBehaviors;
+using TaleWorlds.Core;
+using TaleWorlds.Library;
+using TaleWorlds.Localization;
+using TaleWorlds.SaveSystem;
+using TOR_Core.Extensions;
+using TOR_Core.Utilities;
+
+namespace TOR_Core.CampaignMechanics.Diplomacy
+{
+ ///
+ /// Handles Total War-style alliance behavior where allies must join wars or break the alliance.
+ /// Also tracks which wars are "alliance wars" (defensive) vs "offensive wars" for war limit purposes.
+ ///
+ public class TORAllianceWarBehavior : CampaignBehaviorBase
+ {
+ // Track which wars were joined due to alliance obligations (kingdom StringId -> list of enemy kingdom StringIds)
+ private Dictionary> _allianceWars = new();
+
+ public override void RegisterEvents()
+ {
+ CampaignEvents.WarDeclared.AddNonSerializedListener(this, OnWarDeclared);
+ CampaignEvents.MakePeace.AddNonSerializedListener(this, OnPeaceMade);
+ }
+
+ public override void SyncData(IDataStore dataStore)
+ {
+ dataStore.SyncData("_allianceWars", ref _allianceWars);
+ }
+
+ ///
+ /// Checks if a war is an alliance war (defensive) rather than an offensive war.
+ ///
+ public bool IsAllianceWar(Kingdom kingdom, Kingdom enemy)
+ {
+ if (kingdom == null || enemy == null) return false;
+
+ if (_allianceWars.TryGetValue(kingdom.StringId, out var enemies))
+ {
+ return enemies.Contains(enemy.StringId);
+ }
+ return false;
+ }
+
+ ///
+ /// Gets the number of offensive wars (excluding alliance/defensive wars and Chaos wars).
+ /// Chaos wars don't count because they are forced/eternal and cannot be ended through diplomacy.
+ ///
+ public int GetOffensiveWarCount(Kingdom kingdom)
+ {
+ if (kingdom == null) return 0;
+
+ int offensiveWars = 0;
+
+ foreach (var enemy in Kingdom.All)
+ {
+ if (enemy == kingdom || !kingdom.IsAtWarWith(enemy)) continue;
+
+ // Skip Chaos wars - they're forced and eternal, don't count towards limit
+ if (enemy.Culture?.StringId == TORConstants.Cultures.CHAOS) continue;
+
+ // Skip alliance wars
+ if (_allianceWars.TryGetValue(kingdom.StringId, out var allianceEnemies) &&
+ allianceEnemies.Contains(enemy.StringId))
+ {
+ continue;
+ }
+
+ offensiveWars++;
+ }
+
+ return offensiveWars;
+ }
+
+ private void OnWarDeclared(IFaction faction1, IFaction faction2, DeclareWarAction.DeclareWarDetail detail)
+ {
+ if (!faction1.IsKingdomFaction || !faction2.IsKingdomFaction) return;
+
+ var attacker = (Kingdom)faction1;
+ var defender = (Kingdom)faction2;
+
+ // Get all allies of the defender - they must decide to join or break alliance
+ var defenderAllies = defender.AlliedKingdoms.ToList();
+
+ foreach (var ally in defenderAllies)
+ {
+ if (ally == attacker) continue; // Shouldn't happen, but safety check
+ if (ally.IsAtWarWith(attacker)) continue; // Already at war
+
+ // Create an internal kingdom decision for the ally
+ // Total War style: must join or break alliance
+ var decision = new HonorAllianceDecision(ally.RulingClan, defender, attacker);
+
+ // For AI kingdoms, resolve immediately
+ // For player kingdom, add as enforced decision requiring player choice
+ if (ally == Clan.PlayerClan?.Kingdom)
+ {
+ // Player gets to choose - add as enforced decision
+ decision.IsEnforced = true;
+ ally.AddDecision(decision, true); // true = ignoreInfluenceCost
+ }
+ else
+ {
+ // AI resolves immediately based on scoring
+ ResolveAIDecision(ally, decision);
+ }
+ }
+
+ // Also handle allies of the attacker (offensive alliance call)
+ // These are less obligatory - don't break alliance for refusing
+ var attackerAllies = attacker.AlliedKingdoms.ToList();
+ foreach (var ally in attackerAllies)
+ {
+ if (ally == defender) continue;
+ if (ally.IsAtWarWith(defender)) continue;
+
+ // For offensive calls, use the native Call to War system
+ // or simply have AI decide without breaking alliance
+ if (ally != Clan.PlayerClan?.Kingdom)
+ {
+ bool willJoin = ShouldAllyJoinOffensiveWar(ally, attacker, defender);
+ if (willJoin)
+ {
+ MarkAsAllianceWar(ally, defender);
+ DeclareWarAction.ApplyByKingdomDecision(ally, defender);
+ }
+ }
+ // Note: Not creating decision for offensive calls - less obligatory
+ }
+ }
+
+ ///
+ /// Resolves the HonorAllianceDecision for AI kingdoms immediately.
+ ///
+ private void ResolveAIDecision(Kingdom kingdom, HonorAllianceDecision decision)
+ {
+ if (!decision.IsAllowed()) return;
+
+ // Calculate total support for joining war
+ float joinSupport = 0f;
+ float breakSupport = 0f;
+ int clanCount = 0;
+
+ foreach (var clan in kingdom.Clans)
+ {
+ if (clan.IsUnderMercenaryService) continue;
+
+ float clanSupport = decision.CalculateJoinWarSupportPublic(clan);
+
+ // Weight by clan tier/power
+ float weight = 1f + clan.Tier * 0.2f;
+ if (clan == kingdom.RulingClan) weight *= 2f; // Ruler has more say
+
+ if (clanSupport > 0)
+ joinSupport += clanSupport * weight;
+ else
+ breakSupport += -clanSupport * weight;
+
+ clanCount++;
+ }
+
+ // Decide based on total support
+ bool shouldJoin = joinSupport >= breakSupport;
+
+ // Apply the outcome
+ if (shouldJoin)
+ {
+ MarkAsAllianceWar(kingdom, decision.Attacker);
+ DeclareWarAction.ApplyByKingdomDecision(kingdom, decision.Attacker);
+ }
+ else
+ {
+ var allianceBehavior = Campaign.Current.GetCampaignBehavior();
+ allianceBehavior?.EndAlliance(kingdom, decision.AttackedAlly);
+ }
+ }
+
+ ///
+ /// Determines if an ally should join an offensive war (ally declared war).
+ /// Less obligatory than defensive.
+ ///
+ private bool ShouldAllyJoinOffensiveWar(Kingdom ally, Kingdom attackingAlly, Kingdom target)
+ {
+ // Player kingdom - don't auto-join offensive wars
+ if (ally == Clan.PlayerClan?.Kingdom)
+ {
+ return false;
+ }
+
+ // Chaos factions
+ if (ally.Culture.StringId == TORConstants.Cultures.CHAOS)
+ {
+ return false;
+ }
+
+ var allyReligion = ally.Leader?.GetDominantReligion();
+ var targetReligion = target.Leader?.GetDominantReligion();
+
+ if (allyReligion != null && targetReligion != null)
+ {
+ if (allyReligion.HostileReligions?.Contains(targetReligion) == true)
+ {
+ return true;
+ }
+ }
+
+ return MBRandom.RandomFloat < 0.4f;
+ }
+
+ ///
+ /// Marks a war as an alliance war (defensive) so it doesn't count toward offensive war limits.
+ ///
+ public void MarkAsAllianceWar(Kingdom kingdom, Kingdom enemy)
+ {
+ if (!_allianceWars.ContainsKey(kingdom.StringId))
+ {
+ _allianceWars[kingdom.StringId] = new List();
+ }
+
+ if (!_allianceWars[kingdom.StringId].Contains(enemy.StringId))
+ {
+ _allianceWars[kingdom.StringId].Add(enemy.StringId);
+ }
+ }
+
+ private void OnPeaceMade(IFaction faction1, IFaction faction2, MakePeaceAction.MakePeaceDetail detail)
+ {
+ if (!faction1.IsKingdomFaction || !faction2.IsKingdomFaction) return;
+
+ var kingdom1 = (Kingdom)faction1;
+ var kingdom2 = (Kingdom)faction2;
+
+ // Remove from alliance war tracking
+ RemoveAllianceWarTracking(kingdom1, kingdom2);
+ RemoveAllianceWarTracking(kingdom2, kingdom1);
+ }
+
+ private void RemoveAllianceWarTracking(Kingdom kingdom, Kingdom enemy)
+ {
+ if (_allianceWars.TryGetValue(kingdom.StringId, out var enemies))
+ {
+ enemies.Remove(enemy.StringId);
+ }
+ }
+ }
+
+ ///
+ /// Type definer for save/load of alliance war tracking.
+ ///
+ public class TORAllianceWarBehaviorTypeDefiner : SaveableTypeDefiner
+ {
+ public TORAllianceWarBehaviorTypeDefiner() : base(789_123) { }
+
+ protected override void DefineContainerDefinitions()
+ {
+ ConstructContainerDefinition(typeof(Dictionary>));
+ ConstructContainerDefinition(typeof(List));
+ }
+ }
+}
diff --git a/CSharpSourceCode/CampaignMechanics/Diplomacy/TORKingdomDecisionsCampaignBehavior.cs b/CSharpSourceCode/CampaignMechanics/Diplomacy/TORKingdomDecisionsCampaignBehavior.cs
new file mode 100644
index 0000000..66cb2b7
--- /dev/null
+++ b/CSharpSourceCode/CampaignMechanics/Diplomacy/TORKingdomDecisionsCampaignBehavior.cs
@@ -0,0 +1,349 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using NLog;
+using TaleWorlds.CampaignSystem;
+using TaleWorlds.CampaignSystem.Actions;
+using TaleWorlds.CampaignSystem.CampaignBehaviors;
+using TaleWorlds.CampaignSystem.Election;
+using TaleWorlds.Core;
+using TaleWorlds.LinQuick;
+using TOR_Core.CampaignMechanics.Religion;
+using TOR_Core.Extensions;
+using TOR_Core.Models;
+using TOR_Core.Utilities;
+
+namespace TOR_Core.CampaignMechanics.Diplomacy
+{
+ public class TORKingdomDecisionsCampaignBehavior : CampaignBehaviorBase
+ {
+ // War/Peace decision settings
+ private List _kingdomDecisionsList = [];
+ private const float MinDaysBetweenDecisions = 10f;
+ private const float VariationDaysBetweenDecisions = 10f; //0-10 days
+ private const float AgreementConsiderationChance = 0.15f; //0-10 days
+ private Dictionary _lastDecisionTime = [];
+ private float _influenceReserveToKeep = 300f;
+ private float _outnumberRatioForEmergencyPeace = 5f;
+
+
+ public override void RegisterEvents()
+ {
+ CampaignEvents.DailyTickClanEvent.AddNonSerializedListener(this, DailyTickClan);
+ CampaignEvents.HourlyTickEvent.AddNonSerializedListener(this, HourlyTick);
+ CampaignEvents.DailyTickEvent.AddNonSerializedListener(this, DailyTick);
+ CampaignEvents.MakePeace.AddNonSerializedListener(this, OnPeaceMade);
+ CampaignEvents.WarDeclared.AddNonSerializedListener(this, OnWarDeclared);
+ CampaignEvents.OnSessionLaunchedEvent.AddNonSerializedListener(this, OnSessionLaunched);
+ CampaignEvents.KingdomCreatedEvent.AddNonSerializedListener(this, new Action(this.KingdomCreation));
+ }
+
+ ///
+ /// Add player created kingdom's to the _lastDecisionTime dictionary immediately.
+ ///
+ private void KingdomCreation(Kingdom kingdom)
+ {
+ if (!_lastDecisionTime.TryGetValue(kingdom.StringId, out _))
+ {
+ _lastDecisionTime.Add(kingdom.StringId, CampaignTime.Now);
+ }
+ }
+
+ private void OnSessionLaunched(CampaignGameStarter starter)
+ {
+ Kingdom.All.ForEach(k => _lastDecisionTime[k.StringId] = CampaignTime.Now);
+ }
+
+ private void DailyTickClan(Clan clan)
+ {
+ if (clan == null || clan.IsEliminated || clan.Kingdom == null)
+ return;
+
+ if (_lastDecisionTime.Keys.ContainsQ(clan.Kingdom.StringId))
+ {
+ if (_lastDecisionTime[clan.Kingdom.StringId].ElapsedDaysUntilNow < MinDaysBetweenDecisions + MBRandom.RandomFloatRanged(0,VariationDaysBetweenDecisions))
+ {
+ return;
+ }
+ }
+
+ // War/Peace decisions - all eligible clans
+ if (!IsEligibleForDecisionMaking(clan)) return;
+
+ // Agreement consideration - only ruling clan, separate timing
+ if (clan.Kingdom != Clan.PlayerClan?.Kingdom &&
+ ShouldConsiderAgreementsToday(clan.Kingdom))
+ {
+ if (MBRandom.RandomFloat < 0.5f)
+ {
+ ConsiderTradeAgreements(clan);
+ }
+ else
+ {
+ ConsiderAlliances(clan);
+ }
+ }
+
+
+
+ if (Campaign.Current?.Models?.DiplomacyModel is not TORDiplomacyModel model) return;
+
+ var kingdom = clan.Kingdom;
+ Kingdom peaceCandidate = null;
+ Kingdom warCandidate;
+
+ // Emergency peace check - bypass normal cooldowns
+ if (!kingdom.UnresolvedDecisions.AnyQ(x => x is MakePeaceKingdomDecision) && ConsiderEmergencyPeace(kingdom))
+ {
+ peaceCandidate = model.GetPeaceDeclarationTargetCandidate(kingdom, true);
+ if (peaceCandidate != null && !peaceCandidate.UnresolvedDecisions.AnyQ(x => x is MakePeaceKingdomDecision))
+ {
+ var peaceDecision = new MakePeaceKingdomDecision(clan, peaceCandidate, MBRandom.RandomInt(1000, 3000));
+ _kingdomDecisionsList.Add(peaceDecision);
+ clan.Kingdom.AddDecision(peaceDecision, true);
+ _lastDecisionTime[kingdom.StringId] = CampaignTime.Now;
+ }
+ }
+ else
+ {
+ if (_lastDecisionTime[clan.Kingdom.StringId].ElapsedDaysUntilNow < MinDaysBetweenDecisions) return;
+
+ // Calculate candidates only if there isn't already another clan that has proposed a decision
+ KingdomDecision decision = null;
+
+ // Check for peace
+ if (clan.Influence > model.GetInfluenceCostOfProposingPeace(clan) + _influenceReserveToKeep &&
+ !kingdom.UnresolvedDecisions.AnyQ(x => x is MakePeaceKingdomDecision))
+ {
+ peaceCandidate = model.GetPeaceDeclarationTargetCandidate(kingdom);
+ if (peaceCandidate != null)
+ {
+ decision = new MakePeaceKingdomDecision(clan, peaceCandidate);
+ }
+ }
+
+ // Check for war if no peace decision
+ if (decision == null &&
+ clan.Influence > model.GetInfluenceCostOfProposingWar(clan) + _influenceReserveToKeep &&
+ !kingdom.UnresolvedDecisions.AnyQ(x => x is DeclareWarDecision))
+ {
+ warCandidate = model.GetWarDeclarationTargetCandidate(kingdom);
+ if (warCandidate != null)
+ {
+ decision = new DeclareWarDecision(clan, warCandidate);
+ }
+ }
+
+ if (decision != null)
+ {
+ _kingdomDecisionsList.Add(decision);
+ clan.Kingdom.AddDecision(decision, false);
+ _lastDecisionTime[kingdom.StringId] = CampaignTime.Now;
+ }
+ }
+
+ UpdateKingdomDecisions(clan.Kingdom);
+ }
+
+ private bool ConsiderEmergencyPeace(Kingdom kingdom)
+ {
+ if (kingdom.GetTotalEnemyAllianceStrength() > kingdom.GetAllianceTotalStrength() * _outnumberRatioForEmergencyPeace) return true;
+ return false;
+ }
+
+ private bool IsEligibleForDecisionMaking(Clan clan)
+ {
+ return CampaignTime.Now.ToDays > 5f &&
+ !clan.IsEliminated &&
+ !clan.IsBanditFaction &&
+ clan != Clan.PlayerClan &&
+ clan.CurrentTotalStrength > 0f &&
+ clan.Kingdom != null &&
+ clan.Influence > 0f &&
+ !clan.IsMinorFaction &&
+ !clan.IsUnderMercenaryService;
+ }
+
+ private void HourlyTick()
+ {
+ if (Clan.PlayerClan.Kingdom != null)
+ {
+ UpdateKingdomDecisions(Clan.PlayerClan.Kingdom);
+ }
+ }
+
+ private void DailyTick()
+ {
+ // Clean up old decisions
+ if (_kingdomDecisionsList != null)
+ {
+ int count = _kingdomDecisionsList.Count;
+ int num = 0;
+ for (int i = 0; i < count; i++)
+ {
+ if (_kingdomDecisionsList[i - num].TriggerTime.ElapsedDaysUntilNow > 15f)
+ {
+ _kingdomDecisionsList.RemoveAt(i - num);
+ num++;
+ }
+ }
+ }
+ }
+
+ private void OnPeaceMade(IFaction side1Faction, IFaction side2Faction, MakePeaceAction.MakePeaceDetail detail)
+ {
+ HandleDiplomaticChangeBetweenFactions(side1Faction, side2Faction);
+ }
+
+ private void OnWarDeclared(IFaction side1Faction, IFaction side2Faction, DeclareWarAction.DeclareWarDetail detail)
+ {
+ HandleDiplomaticChangeBetweenFactions(side1Faction, side2Faction);
+ }
+
+ private void HandleDiplomaticChangeBetweenFactions(IFaction side1Faction, IFaction side2Faction)
+ {
+ if (side1Faction.IsKingdomFaction && side2Faction.IsKingdomFaction)
+ {
+ UpdateKingdomDecisions((Kingdom)side1Faction);
+ UpdateKingdomDecisions((Kingdom)side2Faction);
+ }
+ }
+
+ public void UpdateKingdomDecisions(Kingdom kingdom)
+ {
+ List cancelList = [];
+ List electionList = [];
+ foreach (KingdomDecision kingdomDecision in kingdom.UnresolvedDecisions)
+ {
+ if (kingdomDecision.ShouldBeCancelled())
+ {
+ cancelList.Add(kingdomDecision);
+ }
+ else if (kingdomDecision.TriggerTime.IsPast && Clan.PlayerClan.IsUnderMercenaryService)
+ {
+ electionList.Add(kingdomDecision);
+ }
+ }
+ foreach (KingdomDecision decisionToCancel in cancelList)
+ {
+ bool isPlayerInvolved;
+ if (!decisionToCancel.DetermineChooser().Leader.IsHumanPlayerCharacter)
+ {
+ isPlayerInvolved = decisionToCancel.DetermineSupporters().Any((Supporter x) => x.IsPlayer);
+ }
+ else
+ {
+ isPlayerInvolved = true;
+ }
+ kingdom.RemoveDecision(decisionToCancel);
+ CampaignEventDispatcher.Instance.OnKingdomDecisionCancelled(decisionToCancel, isPlayerInvolved);
+ }
+ foreach (KingdomDecision decisionToVote in electionList)
+ {
+ new KingdomElection(decisionToVote).StartElectionWithoutPlayer();
+ _lastDecisionTime[kingdom.StringId] = CampaignTime.Now;
+ }
+ }
+
+ public override void SyncData(IDataStore dataStore)
+ {
+ dataStore.SyncData("_kingdomDecisionsList", ref _kingdomDecisionsList);
+ }
+
+ ///
+ /// Updates war/peace status for all allies of a kingdom when diplomatic status changes.
+ ///
+ public static void UpdateWarPeaceForAlliance(IFaction kingdom)
+ {
+ var allKingdoms = Kingdom.All.Where(k => !k.IsEliminated);
+ var allAllies = kingdom.GetAlliedKingdoms().ToList();
+
+ foreach (var ally in allAllies)
+ {
+ foreach (var otherKingdom in allKingdoms.Where(k => k != kingdom && k != ally))
+ {
+ if (kingdom.GetStanceWith(otherKingdom).IsAtWar != ally.GetStanceWith(otherKingdom).IsAtWar)
+ {
+ if (kingdom.IsAtWarWith(otherKingdom))
+ {
+ ally.SetAllyTriggered(true);
+ DeclareWarAction.ApplyByKingdomDecision(ally, otherKingdom);
+ }
+ else
+ {
+ ally.SetAllyTriggered(true);
+ MakePeaceAction.ApplyByKingdomDecision(ally, otherKingdom, 0, 0);
+ }
+ }
+ }
+ }
+ }
+
+
+ private void ConsiderTradeAgreements(Clan consideringClan)
+ {
+ var tradeModel = Campaign.Current?.Models?.TradeAgreementModel as TORTradeAgreementModel;
+ if (tradeModel == null)
+ return;
+
+ Kingdom kingdom = consideringClan.Kingdom;
+
+ var potentialPartners = tradeModel.GetPotentialTradePartners(kingdom);
+ if (!potentialPartners.Any())
+ return;
+
+ foreach (var targetKingdom in potentialPartners)
+ {
+ float score = tradeModel.GetScoreOfStartingTradeAgreement(kingdom, targetKingdom, consideringClan, out _);
+
+ if (MBRandom.RandomFloat * 100f < score)
+ {
+ kingdom.AddDecision(new TradeAgreementDecision(consideringClan, targetKingdom), true);
+ _lastDecisionTime[kingdom.StringId] = CampaignTime.Now;
+ return;
+ }
+ }
+ }
+
+
+ private void ConsiderAlliances(Clan consideringClan)
+ {
+ var allianceModel = Campaign.Current?.Models?.AllianceModel as TORAllianceModel;
+ if (allianceModel == null)
+ return;
+
+ var kingdom = consideringClan.Kingdom;
+
+ var potentialAllies = allianceModel.GetPotentialAlliancePartners(kingdom);
+ if (!potentialAllies.Any())
+ return;
+
+ var alliesByScore = new List<(Kingdom kingdom, float value)>();
+ foreach (var targetKingdom in potentialAllies)
+ {
+ float score = allianceModel.GetScoreOfStartingAlliance(kingdom, targetKingdom, kingdom.RulingClan, out _).ResultNumber;
+ alliesByScore.Add((targetKingdom, (int)score));
+ }
+
+ var bestCandidate = alliesByScore.MaxBy(value => value.value).kingdom;
+ var result = alliesByScore.MaxBy(value => value.value).value;
+
+ if (result > 50 && MBRandom.RandomFloat * 1000f < result)
+ {
+ kingdom.AddDecision(new StartAllianceDecision(kingdom.RulingClan, bestCandidate), true);
+ _lastDecisionTime[kingdom.StringId] = CampaignTime.Now;
+ }
+ }
+
+ private bool ShouldConsiderAgreementsToday(Kingdom kingdom)
+ {
+ if (MBRandom.RandomFloat < AgreementConsiderationChance) return false;
+
+ return true;
+ }
+
+
+
+ }
+}
\ No newline at end of file
diff --git a/CSharpSourceCode/CampaignMechanics/Diplomacy/TORTradeAgreementAIBehavior.cs b/CSharpSourceCode/CampaignMechanics/Diplomacy/TORTradeAgreementAIBehavior.cs
new file mode 100644
index 0000000..7cb07d1
--- /dev/null
+++ b/CSharpSourceCode/CampaignMechanics/Diplomacy/TORTradeAgreementAIBehavior.cs
@@ -0,0 +1,284 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using TaleWorlds.CampaignSystem;
+using TaleWorlds.CampaignSystem.Election;
+using TaleWorlds.Core;
+using TOR_Core.CampaignMechanics.Religion;
+using TOR_Core.Extensions;
+using TOR_Core.Utilities;
+
+namespace TOR_Core.CampaignMechanics.Diplomacy
+{
+ ///
+ /// AI behavior for proactively proposing trade agreements.
+ /// Vanilla Bannerlord has no AI logic for this - this adds it.
+ ///
+ public class TORTradeAgreementAIBehavior : CampaignBehaviorBase
+ {
+ // How often AI considers trade agreements (in days)
+ private const int ConsiderationIntervalMinDays = 5;
+ private const int ConsiderationIntervalMaxDays = 10;
+
+ public override void RegisterEvents()
+ {
+ CampaignEvents.DailyTickClanEvent.AddNonSerializedListener(this, OnDailyTickClan);
+ }
+
+ public override void SyncData(IDataStore dataStore)
+ {
+ // No persistent data needed for now
+ }
+
+ ///
+ /// Daily tick for each clan - ruling clans trigger kingdom-wide trade considerations.
+ ///
+ private void OnDailyTickClan(Clan clan)
+ {
+ if (clan == null || clan.IsEliminated || clan.Kingdom == null)
+ return;
+
+ // Only ruling clan triggers the kingdom's trade considerations
+ if (clan != clan.Kingdom.RulingClan)
+ return;
+
+ // Skip player kingdom - let player decide
+ if (clan.Kingdom == Clan.PlayerClan?.Kingdom)
+ return;
+
+ // Each kingdom has its own consideration interval based on its ID
+ // This spreads out trade considerations across different days
+ if (!ShouldConsiderTradeToday(clan.Kingdom))
+ return;
+
+ ConsiderTradeAgreements(clan.Kingdom);
+ }
+
+ ///
+ /// Main entry point for a kingdom considering trade agreements.
+ /// Selects lords to evaluate and processes their recommendations.
+ ///
+ private void ConsiderTradeAgreements(Kingdom kingdom)
+ {
+ if (!CanKingdomConsiderTrade(kingdom))
+ return;
+
+ var potentialPartners = GetPotentialTradePartners(kingdom);
+ if (!potentialPartners.Any())
+ return;
+
+ var lordsConsidering = GetLordsConsideringTrade(kingdom);
+
+ foreach (var targetKingdom in potentialPartners)
+ {
+ foreach (var lord in lordsConsidering)
+ {
+ float chance = CalculateTradeProposalChance(lord, kingdom, targetKingdom);
+
+ if (MBRandom.RandomFloat * 100f < chance)
+ {
+ ProposeTradeAgreement(kingdom, targetKingdom, lord);
+ return; // Only one proposal per tick
+ }
+ }
+ }
+ }
+
+ ///
+ /// Checks if a kingdom is allowed to consider trade agreements at all.
+ /// Handles lore restrictions (Chaos, Greenskins, etc.)
+ ///
+ private bool CanKingdomConsiderTrade(Kingdom kingdom)
+ {
+ var pantheon = GetKingdomPantheon(kingdom);
+
+ // Chaos never trades
+ if (pantheon == Pantheon.Chaos)
+ return false;
+
+ // Greenskins never trade
+ if (pantheon == Pantheon.Greenskin)
+ return false;
+
+ // All other factions (including Undead) can trade
+ return true;
+ }
+
+ ///
+ /// Gets list of potential trade partners for a kingdom.
+ /// Takes 5 closest kingdoms, filters valid targets, scores them, returns top 3.
+ ///
+ private List GetPotentialTradePartners(Kingdom kingdom)
+ {
+ var tradeModel = Campaign.Current?.Models?.TradeAgreementModel;
+ if (tradeModel == null)
+ return new List();
+
+ // Get all kingdoms with distance
+ var kingdomsWithDistance = Kingdom.All
+ .Where(k => k != kingdom && !k.IsEliminated)
+ .Select(k => new { Kingdom = k, Distance = GetKingdomDistance(kingdom, k) })
+ .OrderBy(x => x.Distance)
+ .Take(5) // Take 5 closest
+ .Select(x => x.Kingdom)
+ .ToList();
+
+ // Filter by lore and game rules
+ var validPartners = kingdomsWithDistance
+ .Where(k => IsTradeLoreCompatible(GetKingdomPantheon(kingdom), GetKingdomPantheon(k)))
+ .Where(k => tradeModel.CanMakeTradeAgreement(kingdom, k, true, out _))
+ .ToList();
+
+ if (!validPartners.Any())
+ return new List();
+
+ // Score each valid partner and take top 3
+ var scoredPartners = validPartners
+ .Select(k => new
+ {
+ Kingdom = k,
+ Score = tradeModel.GetScoreOfStartingTradeAgreement(kingdom, k, kingdom.RulingClan, out _)
+ })
+ .Where(x => x.Score > 0)
+ .OrderByDescending(x => x.Score)
+ .Take(3)
+ .Select(x => x.Kingdom)
+ .ToList();
+
+ return scoredPartners;
+ }
+
+ ///
+ /// Gets the lords who will consider trade this tick.
+ /// Always includes faction leader, plus 1-2 random influential clan leaders.
+ ///
+ private List GetLordsConsideringTrade(Kingdom kingdom)
+ {
+ var lords = new List();
+
+ // Always include the faction leader
+ if (kingdom.Leader != null && kingdom.Leader.IsAlive)
+ {
+ lords.Add(kingdom.Leader);
+ }
+
+ // Add 1-2 random clan leaders
+ var otherClanLeaders = kingdom.Clans
+ .Where(c => c != kingdom.RulingClan && !c.IsUnderMercenaryService && c.Leader?.IsAlive == true)
+ .Select(c => c.Leader)
+ .ToList();
+
+ int additionalLords = Math.Min(MBRandom.RandomInt(1, 3), otherClanLeaders.Count);
+
+ for (int i = 0; i < additionalLords; i++)
+ {
+ var randomLord = otherClanLeaders.GetRandomElement();
+ if (randomLord != null && !lords.Contains(randomLord))
+ {
+ lords.Add(randomLord);
+ otherClanLeaders.Remove(randomLord);
+ }
+ }
+
+ return lords;
+ }
+
+ ///
+ /// Calculates the chance (0-100) that a lord will propose a trade agreement.
+ /// Uses the model's score which already includes culture, religion, and pantheon factors.
+ ///
+ private float CalculateTradeProposalChance(Hero lord, Kingdom proposingKingdom, Kingdom targetKingdom)
+ {
+ var tradeModel = Campaign.Current?.Models?.TradeAgreementModel;
+ if (tradeModel == null)
+ return 0f;
+
+ // Use the model's score directly - it already includes all the factors
+ float modelScore = tradeModel.GetScoreOfStartingTradeAgreement(
+ proposingKingdom, targetKingdom, lord.Clan, out _);
+
+ // Model score is 0-100, use it as our chance
+ return modelScore;
+ }
+
+ ///
+ /// Actually proposes the trade agreement by creating a kingdom decision.
+ ///
+ private void ProposeTradeAgreement(Kingdom proposer, Kingdom target, Hero proposingLord)
+ {
+ if (proposingLord?.Clan == null)
+ return;
+
+ // Create the trade agreement decision
+ var decision = new TradeAgreementDecision(proposingLord.Clan, target);
+
+ // Add to kingdom's unresolved decisions for council vote
+ proposer.AddDecision(decision, true);
+ }
+
+ #region Helper Methods
+
+ ///
+ /// Determines if a kingdom should consider trade today based on its unique interval.
+ /// Each kingdom has a different interval (5-10 days) and offset, spreading out considerations.
+ ///
+ private bool ShouldConsiderTradeToday(Kingdom kingdom)
+ {
+ // Use hash of kingdom ID for consistent but varied timing per kingdom
+ int hash = kingdom.StringId?.GetHashCode() ?? 0;
+ if (hash < 0) hash = -hash;
+
+ // Each kingdom gets an interval between min and max days
+ int intervalRange = ConsiderationIntervalMaxDays - ConsiderationIntervalMinDays + 1;
+ int kingdomInterval = ConsiderationIntervalMinDays + (hash % intervalRange);
+
+ // Each kingdom also gets a unique offset within its interval
+ int kingdomOffset = (hash / intervalRange) % kingdomInterval;
+
+ int currentDay = (int)CampaignTime.Now.ToDays;
+ return (currentDay + kingdomOffset) % kingdomInterval == 0;
+ }
+
+ ///
+ /// Gets the approximate distance between two kingdoms based on their mid settlements.
+ ///
+ private float GetKingdomDistance(Kingdom kingdom1, Kingdom kingdom2)
+ {
+ if (kingdom1.FactionMidSettlement == null || kingdom2.FactionMidSettlement == null)
+ return float.MaxValue;
+
+ return kingdom1.FactionMidSettlement.Position.Distance(kingdom2.FactionMidSettlement.Position);
+ }
+
+ ///
+ /// Gets the dominant pantheon for a kingdom based on its leader's religion or culture.
+ ///
+ private Pantheon GetKingdomPantheon(Kingdom kingdom)
+ {
+ var leaderReligion = kingdom.Leader?.GetDominantReligion();
+ if (leaderReligion != null)
+ return leaderReligion.Pantheon;
+
+ return ReligionObjectHelper.GetPantheon(kingdom.Culture?.StringId);
+ }
+
+ ///
+ /// Checks if trade is allowed between two pantheons based on lore.
+ ///
+ private bool IsTradeLoreCompatible(Pantheon pantheon1, Pantheon pantheon2)
+ {
+ // Chaos never trades
+ if (pantheon1 == Pantheon.Chaos || pantheon2 == Pantheon.Chaos)
+ return false;
+
+ // Greenskins never trade
+ if (pantheon1 == Pantheon.Greenskin || pantheon2 == Pantheon.Greenskin)
+ return false;
+
+ // All other factions (including Undead) can trade with each other
+ return true;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/CSharpSourceCode/CampaignMechanics/Religion/ReligionCampaignBehavior.cs b/CSharpSourceCode/CampaignMechanics/Religion/ReligionCampaignBehavior.cs
index a94cbce..e18a0d9 100644
--- a/CSharpSourceCode/CampaignMechanics/Religion/ReligionCampaignBehavior.cs
+++ b/CSharpSourceCode/CampaignMechanics/Religion/ReligionCampaignBehavior.cs
@@ -23,6 +23,12 @@ namespace TOR_Core.CampaignMechanics.Religion
{
public class ReligionCampaignBehavior : CampaignBehaviorBase, IDisposable
{
+ // Initial relation adjustment weights for religion compatibility
+ private const int SameReligionBonusMin = 25;
+ private const int SameReligionBonusMax = 50;
+ private const int CompatiblePantheonBonusMax = 25;
+ private const int HostilePantheonMalusMax = 75;
+
public override void RegisterEvents()
{
CampaignEvents.OnNewGameCreatedPartialFollowUpEvent.AddNonSerializedListener(this, AfterNewGameStart);
@@ -312,28 +318,23 @@ private void ChangeRelationBasedOnReligion(Hero hero, Hero otherHero)
if (heroDomReligion == otherHeroDomReligion)
{
- var bonus = MBRandom.RandomInt(25, 50);
+ var bonus = MBRandom.RandomInt(SameReligionBonusMin, SameReligionBonusMax);
hero.SetPersonalRelation(otherHero, currentRelation + bonus);
return;
}
- if (heroDomReligion.Affinity == otherHeroDomReligion.Affinity)
- {//this causes chaos religions to have a bonus towards each other despite being on each other's hostile religions list - shouldn't matter until chaos is implemented more fully
- var bonus = MBRandom.RandomInt(0, 25);
+
+ // Use Pantheon compatibility for relation adjustments
+ float compatibility = ReligionObjectHelper.GetPantheonCompatibility(heroDomReligion.Pantheon, otherHeroDomReligion.Pantheon);
+ if (compatibility > 0)
+ {
+ var bonus = MBRandom.RandomInt(0, (int)(CompatiblePantheonBonusMax * compatibility));
hero.SetPersonalRelation(otherHero, currentRelation + bonus);
- return;
}
- else //this is the default for all heroes who have different religions and different affinities (order, chaos, or nagash)
+ else if (compatibility < 0)
{
- var malus = MBRandom.RandomInt(25, 75);
+ var malus = MBRandom.RandomInt(0, (int)(HostilePantheonMalusMax * -compatibility));
hero.SetPersonalRelation(otherHero, currentRelation - malus);
}
- //this doesn't make sense in the context of how affinities and hostile religions are grouped - all religion pairs where affinities differ contain one another and could instead be covered by a more efficient affinity1 != affinity2 check except for chaos cults which have already received a relation bonus because of identical affinities
- //if (heroDomReligion.HostileReligions.Contains(otherHeroDomReligion))
- //{
- // var malus = MBRandom.RandomInt(25, 75);
- // hero.SetPersonalRelation(otherHero, currentRelation - malus);
- // return;
- //}
}
}
}
\ No newline at end of file
diff --git a/CSharpSourceCode/CampaignMechanics/Religion/ReligionObject.cs b/CSharpSourceCode/CampaignMechanics/Religion/ReligionObject.cs
index d590b32..ff5ea9d 100644
--- a/CSharpSourceCode/CampaignMechanics/Religion/ReligionObject.cs
+++ b/CSharpSourceCode/CampaignMechanics/Religion/ReligionObject.cs
@@ -29,7 +29,7 @@ public class ReligionObject : MBObjectBase
public List EliteUnits { get; private set; } = [];
public List ReligiousArtifacts { get; private set; } = [];
public List InitialClans { get; private set; } = [];
- public ReligionAffinity Affinity { get; private set; }
+ public Pantheon Pantheon { get; private set; }
public static MBReadOnlyList All => _all ?? [];
public static void FillAll() => _all = MBObjectManager.Instance.GetObjectTypeList();
@@ -41,17 +41,16 @@ public class ReligionObject : MBObjectBase
public TextObject EncyclopediaLinkWithName => HyperlinkTexts.GetSettlementHyperlinkText(EncyclopediaLink, Name);
///
- /// Gets similarity score between this and another religion
+ /// Gets the hostility factor between this religion and another.
+ /// Returns a negative value if the other religion is in the HostileReligions list.
+ /// This is meant to be added on top of Pantheon compatibility calculations.
///
- /// The other to calculare similarity with
- /// Similarity score between -1 (hostile) and 1 (same culture, same religion)
- public float GetSimilarityScore(ReligionObject other)
+ /// The other religion to check hostility against
+ /// -0.5 if hostile, 0 otherwise
+ public float GetHostilityFactor(ReligionObject other)
{
- if (HostileReligions.Contains(other) && Culture != other.Culture) return -1f;
- else if (HostileReligions.Contains(other) && Culture == other.Culture) return -0.75f;
- else if (!HostileReligions.Contains(other) && Culture != other.Culture) return 0.25f;
- else if (!HostileReligions.Contains(other) && Culture == other.Culture) return 1f;
- else return 0f;
+ if (other == null) return 0f;
+ return HostileReligions.Contains(other) ? -0.5f : 0f;
}
public override void Deserialize(MBObjectManager objectManager, XmlNode node)
@@ -60,7 +59,7 @@ public override void Deserialize(MBObjectManager objectManager, XmlNode node)
Name = new TextObject(node.Attributes.GetNamedItem("Name").Value);
DeityName = new TextObject(node.Attributes.GetNamedItem("DeityName").Value);
Culture = MBObjectManager.Instance.ReadObjectReferenceFromXml("Culture", node);
- Affinity = (ReligionAffinity)Enum.Parse(typeof(ReligionAffinity), node.Attributes.GetNamedItem("Affinity").Value);
+ Pantheon = (Pantheon)Enum.Parse(typeof(Pantheon), node.Attributes.GetNamedItem("Pantheon").Value);
LoreText = GameTexts.FindText("tor_religion_description", StringId);
if (GameTexts.TryGetText("tor_religion_blessing_name", out var blessingName, this.StringId))
@@ -140,11 +139,23 @@ public enum DevotionLevel
Fanatic
}
- public enum ReligionAffinity
+ ///
+ /// Represents the broad pantheon/faction grouping for diplomatic compatibility.
+ /// Used to determine cultural and religious alignment between factions.
+ ///
+ public enum Pantheon
{
- Order,
- Chaos,
- Vampire,
- Destruction
+ /// Human gods: Sigmar, Ulric, Morr, Taal, Lady of the Lake, etc.
+ Human,
+ /// Elven gods: Isha, Kurnous, Lileath, Khaine, etc.
+ Elven,
+ /// Dwarven Ancestor Gods: Grungni, Valaya, Grimnir
+ Dwarven,
+ /// Undead worship: Nagash, dark necromancy
+ Undead,
+ /// Greenskin gods: Gork and Mork
+ Greenskin,
+ /// Chaos gods: Khorne, Nurgle, Tzeentch, Slaanesh
+ Chaos
}
}
\ No newline at end of file
diff --git a/CSharpSourceCode/CampaignMechanics/Religion/ReligionObjectHelper.cs b/CSharpSourceCode/CampaignMechanics/Religion/ReligionObjectHelper.cs
index cf54e6b..8301e44 100644
--- a/CSharpSourceCode/CampaignMechanics/Religion/ReligionObjectHelper.cs
+++ b/CSharpSourceCode/CampaignMechanics/Religion/ReligionObjectHelper.cs
@@ -1,10 +1,128 @@
-namespace TOR_Core.CampaignMechanics.Religion;
+using TaleWorlds.CampaignSystem;
+using TaleWorlds.TwoDimension;
+using TOR_Core.Utilities;
-public static class ReligionObjectHelper
+namespace TOR_Core.CampaignMechanics.Religion
{
- public static float CalculateSimilarityScore(ReligionObject x, ReligionObject y)
+ ///
+ /// Helper class for religion and pantheon-based compatibility calculations.
+ /// Returns scores from -1.0 (bitter enemies) to +1.0 (same religion/strong allies).
+ ///
+ public static class ReligionObjectHelper
{
- if (x == null || y == null) return 0;
- return x.GetSimilarityScore(y);
+ ///
+ /// Calculates the overall compatibility score between two religions.
+ /// Combines: same religion check, Pantheon compatibility, and specific hostility factors.
+ ///
+ /// Score from -1.0 (enemies) to +1.0 (same religion)
+ public static float CalculateReligionCompatibility(ReligionObject x, ReligionObject y)
+ {
+ if (x == null || y == null) return 0f;
+
+ // Same religion = perfect compatibility
+ if (x == y) return 1.0f;
+
+ // Base compatibility from Pantheon
+ float compatibility = GetPantheonCompatibility(x.Pantheon, y.Pantheon);
+
+ // Add hostility factor if religions are specifically hostile to each other
+ compatibility += x.GetHostilityFactor(y);
+
+ // Clamp to valid range
+ return Mathf.Clamp(compatibility, -1.0f, 1.0f);
+ }
+
+ ///
+ /// Gets the Pantheon for a culture string ID.
+ ///
+ public static Pantheon GetPantheon(string cultureId)
+ {
+ if (string.IsNullOrEmpty(cultureId))
+ return Pantheon.Human;
+
+ return cultureId switch
+ {
+ TORConstants.Cultures.EMPIRE => Pantheon.Human,
+ TORConstants.Cultures.BRETONNIA => Pantheon.Human,
+ TORConstants.Cultures.ASRAI => Pantheon.Elven,
+ TORConstants.Cultures.EONIR => Pantheon.Elven,
+ TORConstants.Cultures.DRUCHII => Pantheon.Elven,
+ TORConstants.Cultures.DAWI => Pantheon.Dwarven,
+ TORConstants.Cultures.SYLVANIA => Pantheon.Undead,
+ TORConstants.Cultures.MOUSILLON => Pantheon.Undead,
+ TORConstants.Cultures.GREENSKIN => Pantheon.Greenskin,
+ TORConstants.Cultures.CHAOS => Pantheon.Chaos,
+ TORConstants.Cultures.BEASTMEN => Pantheon.Chaos,
+ _ => Pantheon.Human
+ };
+ }
+
+ ///
+ /// Gets the compatibility score between two Pantheons.
+ ///
+ public static float GetPantheonCompatibility(Pantheon p1, Pantheon p2)
+ {
+ if (p1 == p2)
+ {
+ return p1 switch
+ {
+ Pantheon.Human => 0.5f,
+ Pantheon.Elven => 0.5f,
+ Pantheon.Dwarven => 1f,
+ Pantheon.Undead => 0.3f,
+ Pantheon.Greenskin => 0.1f,
+ Pantheon.Chaos => 0.1f,
+ _ => 0.5f
+ };
+ }
+
+ // Normalize ordering for consistent lookups
+ var (first, second) = p1 < p2 ? (p1, p2) : (p2, p1);
+
+ return (first, second) switch
+ {
+ // Order vs Chaos - eternal enemies
+ (Pantheon.Human, Pantheon.Chaos) => -1.0f,
+ (Pantheon.Elven, Pantheon.Chaos) => -1.0f,
+ (Pantheon.Dwarven, Pantheon.Chaos) => -1.0f,
+
+ // Order vs Greenskin
+ (Pantheon.Human, Pantheon.Greenskin) => -0.8f,
+ (Pantheon.Elven, Pantheon.Greenskin) => -0.8f,
+ (Pantheon.Dwarven, Pantheon.Greenskin) => -1.0f,
+
+ // Order vs Undead
+ (Pantheon.Human, Pantheon.Undead) => -0.8f,
+ (Pantheon.Elven, Pantheon.Undead) => -0.8f,
+ (Pantheon.Dwarven, Pantheon.Undead) => -0.8f,
+
+ // Destruction forces
+ (Pantheon.Chaos, Pantheon.Greenskin) => -0.4f,
+ (Pantheon.Chaos, Pantheon.Undead) => -0.8f,
+ (Pantheon.Greenskin, Pantheon.Undead) => -0.4f,
+
+ // Order factions
+ (Pantheon.Human, Pantheon.Elven) => 0.1f,
+ (Pantheon.Human, Pantheon.Dwarven) => 0.4f,
+ (Pantheon.Dwarven, Pantheon.Elven) => -0.1f,
+
+ _ => 0f
+ };
+ }
+
+ ///
+ /// Calculates the cultural compatibility score between two cultures.
+ /// Returns a value from -1.0 (bitter enemies) to +1.0 (same culture/strong allies).
+ ///
+ public static float CalculateCultureCompatibility(string culture1Id, string culture2Id)
+ {
+ if (string.IsNullOrEmpty(culture1Id) || string.IsNullOrEmpty(culture2Id))
+ return 0f;
+
+ if (culture1Id == culture2Id)
+ return 1.0f;
+
+ return GetPantheonCompatibility(GetPantheon(culture1Id), GetPantheon(culture2Id));
+ }
}
-}
\ No newline at end of file
+}
diff --git a/CSharpSourceCode/CampaignMechanics/TORCustomSettlement/CustomSettlementMenus/ShrineMenuLogic.cs b/CSharpSourceCode/CampaignMechanics/TORCustomSettlement/CustomSettlementMenus/ShrineMenuLogic.cs
index 82d3519..b65fc24 100644
--- a/CSharpSourceCode/CampaignMechanics/TORCustomSettlement/CustomSettlementMenus/ShrineMenuLogic.cs
+++ b/CSharpSourceCode/CampaignMechanics/TORCustomSettlement/CustomSettlementMenus/ShrineMenuLogic.cs
@@ -248,9 +248,10 @@ private bool DonationCondition(MenuCallbackArgs args)
return true;
}
- // Only Order affinity + Greenskins (Destruction) can donate
+ // Undead, Chaos, and Greenskin pantheon cannot donate
var playerReligion = Hero.MainHero.GetDominantReligion();
- if (playerReligion != null && playerReligion.Affinity != ReligionAffinity.Order && playerReligion.Affinity != ReligionAffinity.Destruction)
+ if (playerReligion != null && (playerReligion.Pantheon == Pantheon.Undead ||
+ playerReligion.Pantheon == Pantheon.Chaos || playerReligion.Pantheon == Pantheon.Greenskin))
{
args.Tooltip = TORTextHelper.GetTextObject("tor_custom_settlement_shrine_no_donate_affinity", "You do not honor the gods through offerings.");
args.IsEnabled = false;
@@ -486,7 +487,7 @@ private void DefileResultConsequence()
}
}
- if (shrineReligion.Affinity == dominantReligion.Affinity) hero.SetPersonalRelation(Hero.MainHero, (int)relation - 10);
+ if (shrineReligion.Pantheon == dominantReligion.Pantheon) hero.SetPersonalRelation(Hero.MainHero, (int)relation - 10);
}
Campaign.Current.GetCampaignBehavior().SetLastDefileTime(Hero.MainHero, (int)CampaignTime.Now.ToDays);
}
@@ -550,37 +551,21 @@ private void LootResultConsequence()
}
}
- if (shrineReligion.Affinity == dominantReligion.Affinity) hero.SetPersonalRelation(Hero.MainHero, (int)relation - 10);
+ if (shrineReligion.Pantheon == dominantReligion.Pantheon) hero.SetPersonalRelation(Hero.MainHero, (int)relation - 10);
}
Campaign.Current.GetCampaignBehavior().SetLastDefileTime(Hero.MainHero, (int)CampaignTime.Now.ToDays);
}
///
- /// Checks if a hero's culture is compatible with a shrine's religion culture for praying.
+ /// Checks if a hero's culture is compatible with a shrine's religion for praying.
+ /// Uses Pantheon matching - Undead cultures (Mousillon/Sylvania) won't match Human shrines.
///
private static bool IsCultureCompatibleWithShrine(Hero hero, ReligionObject religion)
{
- if (hero == null || religion == null || religion.Culture == null) return false;
+ if (hero == null || religion == null) return false;
- // Direct culture match
- if (hero.Culture == religion.Culture) return true;
-
- var heroCultureId = hero.Culture.StringId;
- var religionCultureId = religion.Culture.StringId;
-
- // Special case: Wood Elves (battania) and High Elves (eonir) can pray at each other's shrines
- bool heroIsElf = heroCultureId == "battania" || heroCultureId == "eonir";
- bool shrineIsElf = religionCultureId == "battania" || religionCultureId == "eonir";
- if (heroIsElf && shrineIsElf) return true;
-
- // Special case: Human cultures (Empire, Bretonnia) share the same pantheon
- // Note: Mousillon and Sylvania are NOT included - they cannot pray at human shrines
- bool heroIsHuman = heroCultureId == "empire" || heroCultureId == "vlandia";
- bool shrineIsHuman = religionCultureId == "empire" || religionCultureId == "vlandia";
- if (heroIsHuman && shrineIsHuman) return true;
-
- return false;
+ return ReligionObjectHelper.GetPantheon(hero.Culture?.StringId) == religion.Pantheon;
}
public static bool CanPartyGoToShrine(MobileParty party)
diff --git a/CSharpSourceCode/CampaignMechanics/TORFactionDiscontinuationCampaignBehavior.cs b/CSharpSourceCode/CampaignMechanics/TORFactionDiscontinuationCampaignBehavior.cs
index 2860601..9880ce8 100644
--- a/CSharpSourceCode/CampaignMechanics/TORFactionDiscontinuationCampaignBehavior.cs
+++ b/CSharpSourceCode/CampaignMechanics/TORFactionDiscontinuationCampaignBehavior.cs
@@ -7,6 +7,7 @@
using TaleWorlds.Core;
using TaleWorlds.LinQuick;
using TOR_Core.Extensions;
+using TOR_Core.Utilities;
namespace TOR_Core.CampaignMechanics
{
@@ -69,7 +70,7 @@ private void DailyTickClan(Clan clan)
}
if (MBRandom.RandomFloat > 0.7f)
{
- var candidateKingdoms = Kingdom.All.WhereQ(x => !x.IsEliminated && x.Culture == clan.Culture);
+ var candidateKingdoms = GetCandidateKingdomsForJoiningClan(clan);
if (candidateKingdoms != null && candidateKingdoms.Count() > 0)
{
var targetKingdom = candidateKingdoms.MinBy(x => x.CurrentTotalStrength);
@@ -84,6 +85,36 @@ private void DailyTickClan(Clan clan)
}
}
+ ///
+ /// Gets candidate kingdoms for a destroyed clan to join.
+ /// Includes special faction pairings for closely-related factions.
+ ///
+ private IEnumerable GetCandidateKingdomsForJoiningClan(Clan clan)
+ {
+ var originalKingdomId = clan.StringId?.Split('_')[0]; // Extract faction from clan ID (e.g., "laurelorn" from "laurelorn_clan_1")
+
+ // Special faction pairings - these factions should merge with each other
+ var specialPairings = new Dictionary
+ {
+ { TORConstants.Factions.LAURELORN, TORConstants.Factions.ATHEL_LOREN },
+ { TORConstants.Factions.ATHEL_LOREN, TORConstants.Factions.LAURELORN },
+ { TORConstants.Factions.MOUSILLON, TORConstants.Factions.SYLVANIA },
+ { TORConstants.Factions.SYLVANIA, TORConstants.Factions.MOUSILLON }
+ };
+
+ if (specialPairings.TryGetValue(originalKingdomId, out string pairedFactionId))
+ {
+ var pairedKingdom = Kingdom.All.FirstOrDefault(k => !k.IsEliminated && k.StringId == pairedFactionId);
+ if (pairedKingdom != null)
+ {
+ // Prioritize the paired faction - return only it
+ return [pairedKingdom];
+ }
+ }
+
+ return Kingdom.All.WhereQ(x => !x.IsEliminated && x.Culture == clan.Culture);
+ }
+
private bool CanKingdomBeDiscontinued(Kingdom kingdom)
{
bool flag = !kingdom.IsEliminated && kingdom != Clan.PlayerClan.Kingdom && kingdom.Settlements.IsEmpty();
diff --git a/CSharpSourceCode/CampaignMechanics/TORStartupBehavior.cs b/CSharpSourceCode/CampaignMechanics/TORStartupBehavior.cs
index e819a78..218dfe1 100644
--- a/CSharpSourceCode/CampaignMechanics/TORStartupBehavior.cs
+++ b/CSharpSourceCode/CampaignMechanics/TORStartupBehavior.cs
@@ -6,6 +6,7 @@
using TaleWorlds.Core;
using TOR_Core.CampaignMechanics.Diplomacy;
using TOR_Core.Extensions;
+using TOR_Core.Utilities;
namespace TOR_Core.CampaignMechanics
{
@@ -13,7 +14,7 @@ public class TORStartupBehavior : CampaignBehaviorBase
{
public override void RegisterEvents()
{
- //CampaignEvents.OnNewGameCreatedEvent.AddNonSerializedListener(this, OnNewGameCreated);
+ CampaignEvents.OnNewGameCreatedEvent.AddNonSerializedListener(this, OnNewGameCreated);
CampaignEvents.OnNewGameCreatedPartialFollowUpEvent.AddNonSerializedListener(this, new Action(this.SpawnAiHeroParties));
}
@@ -22,13 +23,13 @@ public override void RegisterEvents()
///
///
/// The game already calls this through the OnNewGameCreatedPartialFollowUp event in HeroSpawnCampaignBehavior with the OnNonBanditClanDailyTick method call, but evidently that's occuring too soon as tor's nobles aren't spawning with parties.
- ///
+ ///
/// LordPartyComponent.InitializeLordPartyProperties will check if GameStarted != true (ie. during the campaign initialization), then it will pass through a larger value to InitializeMobilePartyAroundPosition(PartyTemplate, ...) which in turn passes it into FillPartyStacks
/// SpawnLordParty after Game start will grant the minimum between 19 troops + leader or 10% of their max party size
- ///
+ ///
/// Sly : This could be at risk of causing a crash due to mobParty data caching not being fast enough to complete before the method will attempt to spawn parties; no issues so far, but I ran into a crash with another mod when attempting to run a similar method on the CharacterCreationOverEvent where the parallel methods for data caching were too slow compared to the time it took to exit char creation.
///
- ///
+ ///
//Sly : this is likely resolved by clans/kingdoms having an initial home settlement now
private void SpawnAiHeroParties(CampaignGameStarter starter, int i)
@@ -49,16 +50,34 @@ public override void SyncData(IDataStore dataStore)
{
}
- /* hunharibo: Need to redo alliances as base game has a complete overhaul in 1.3
+ ///
+ /// Sets up initial alliances when a new game is created.
+ ///
private void OnNewGameCreated(CampaignGameStarter starter)
{
- var moot = Kingdom.All.Find(m => m.StringId == "moot");
- var stirland = Kingdom.All.Find(s => s.StringId == "stirland");
- moot.SetAlliance(stirland);
+ // Set up initial alliances between friendly kingdoms
+ SetupInitialAlliances();
+ }
+
+ ///
+ /// Creates initial alliances between kingdoms that should start the game allied.
+ ///
+ private void SetupInitialAlliances()
+ {
+ // Moot and Stirland alliance (Dwarves and Empire)
+ var moot = Kingdom.All.Find(m => m.StringId == TORConstants.Factions.MOOT);
+ var stirland = Kingdom.All.Find(s => s.StringId == TORConstants.Factions.STIRLAND);
+ if (moot != null && stirland != null)
+ {
+ moot.SetAlliance(stirland);
+ TORKingdomDecisionsCampaignBehavior.UpdateWarPeaceForAlliance(stirland);
+ stirland.SetAllyTriggered(false);
+ }
- TORKingdomDecisionsCampaignBehavior.UpdateWarPeaceForAlliance(stirland);
- stirland.SetAllyTriggered(false);
+ // Additional alliances can be added here as needed
+ // Example: Empire provinces that should be allied
+ // var reikland = Kingdom.All.Find(k => k.StringId == "reikland");
+ // var middenland = Kingdom.All.Find(k => k.StringId == "middenland");
}
- */
}
}
\ No newline at end of file
diff --git a/CSharpSourceCode/CharacterDevelopment/CareerSystem/Choices/BloodKnightCareerChoices.cs b/CSharpSourceCode/CharacterDevelopment/CareerSystem/Choices/BloodKnightCareerChoices.cs
index ba2fc5a..778c057 100644
--- a/CSharpSourceCode/CharacterDevelopment/CareerSystem/Choices/BloodKnightCareerChoices.cs
+++ b/CSharpSourceCode/CharacterDevelopment/CareerSystem/Choices/BloodKnightCareerChoices.cs
@@ -425,7 +425,8 @@ public override void InitialCareerSetup()
Hero.MainHero.Culture = sylvaniaCulture;
}
- var religions = ReligionObject.All.FindAll(x => x.Affinity == ReligionAffinity.Order);
+ // Become hostile to Human pantheon religions (your former faith)
+ var religions = ReligionObject.All.FindAll(x => x.Pantheon == Pantheon.Human);
foreach (var religion in religions)
{
Hero.MainHero.AddReligiousInfluence(religion, -100, true);
diff --git a/CSharpSourceCode/CharacterDevelopment/CareerSystem/Choices/NecrarchCareerChoices.cs b/CSharpSourceCode/CharacterDevelopment/CareerSystem/Choices/NecrarchCareerChoices.cs
index 23990dc..7e392a2 100644
--- a/CSharpSourceCode/CharacterDevelopment/CareerSystem/Choices/NecrarchCareerChoices.cs
+++ b/CSharpSourceCode/CharacterDevelopment/CareerSystem/Choices/NecrarchCareerChoices.cs
@@ -307,7 +307,8 @@ public override void InitialCareerSetup()
Hero.MainHero.Culture = sylvaniaCulture;
}
- var religions = ReligionObject.All.FindAll(x => x.Affinity == ReligionAffinity.Order);
+ // Become hostile to Human pantheon religions (your former faith)
+ var religions = ReligionObject.All.FindAll(x => x.Pantheon == Pantheon.Human);
foreach (var religion in religions)
{
Hero.MainHero.AddReligiousInfluence(religion, -100, true);
diff --git a/CSharpSourceCode/CharacterDevelopment/CareerSystem/Choices/NecromancerCareerChoices.cs b/CSharpSourceCode/CharacterDevelopment/CareerSystem/Choices/NecromancerCareerChoices.cs
index f091eb7..30bfed7 100644
--- a/CSharpSourceCode/CharacterDevelopment/CareerSystem/Choices/NecromancerCareerChoices.cs
+++ b/CSharpSourceCode/CharacterDevelopment/CareerSystem/Choices/NecromancerCareerChoices.cs
@@ -236,8 +236,8 @@ protected override void InitializePassives()
public override void InitialCareerSetup()
{
- var religions = ReligionObject.All.FindAll(x => x.Affinity == ReligionAffinity.Order);
-
+ // Become hostile to Human pantheon religions (your former faith)
+ var religions = ReligionObject.All.FindAll(x => x.Pantheon == Pantheon.Human);
foreach (var religion in religions)
{
Hero.MainHero.AddReligiousInfluence(religion, -100, true);
diff --git a/CSharpSourceCode/CharacterDevelopment/CareerSystem/Choices/VampireCountCareerChoices.cs b/CSharpSourceCode/CharacterDevelopment/CareerSystem/Choices/VampireCountCareerChoices.cs
index e390475..50a8fac 100644
--- a/CSharpSourceCode/CharacterDevelopment/CareerSystem/Choices/VampireCountCareerChoices.cs
+++ b/CSharpSourceCode/CharacterDevelopment/CareerSystem/Choices/VampireCountCareerChoices.cs
@@ -331,7 +331,8 @@ public override void InitialCareerSetup()
}
- var religions = ReligionObject.All.FindAll(x => x.Affinity == ReligionAffinity.Order);
+ // Become hostile to Human pantheon religions (your former faith)
+ var religions = ReligionObject.All.FindAll(x => x.Pantheon == Pantheon.Human);
foreach (var religion in religions)
{
Hero.MainHero.AddReligiousInfluence(religion, -100, true);
diff --git a/CSharpSourceCode/Extensions/KingdomExtension.cs b/CSharpSourceCode/Extensions/KingdomExtension.cs
index a1e8939..9118506 100644
--- a/CSharpSourceCode/Extensions/KingdomExtension.cs
+++ b/CSharpSourceCode/Extensions/KingdomExtension.cs
@@ -1,6 +1,11 @@
-using System.Linq;
+using System.Collections.Generic;
+using System.Linq;
using System.Runtime.CompilerServices;
using TaleWorlds.CampaignSystem;
+using TaleWorlds.CampaignSystem.Actions;
+using TaleWorlds.CampaignSystem.CampaignBehaviors;
+using TOR_Core.CampaignMechanics.Diplomacy;
+using TOR_Core.Utilities;
namespace TOR_Core.Extensions;
@@ -30,25 +35,146 @@ public static void SetAllyTriggered(this IFaction obj, bool value)
public static bool IsCoastalKingdom(this Kingdom kingdom)
{
+ return kingdom.StringId == TORConstants.Factions.NORDLAND ||
+ kingdom.StringId == TORConstants.Factions.OSTLAND ||
+ kingdom.StringId == TORConstants.Factions.WASTELAND ||
+ kingdom.StringId == TORConstants.Factions.COURONNE ||
+ kingdom.StringId == TORConstants.Factions.ANGUILLE ||
+ kingdom.StringId == TORConstants.Factions.LYONESSE ||
+ kingdom.StringId == TORConstants.Factions.MOUSILLON ||
+ kingdom.StringId == TORConstants.Factions.BORDELEAUX ||
+ kingdom.StringId == TORConstants.Factions.BRIONNE;
+ }
- //Nordland
- //Marienburg
- //Ostland
- //Mousillon
- //Lyonesse
- //Bordeleaux
- //Coronne
- //Brionne
- //Languille
+ public static bool IsCastleFaction(this Kingdom kingdom)
+ {
+ return kingdom.RulingClan.IsCastleFaction();
+ }
+
+ public static IEnumerable GetTradeAgreementKingdoms(this Kingdom kingdom)
+ {
+ var tradeAgreementBehavior = Campaign.Current?.GetCampaignBehavior();
+ if (tradeAgreementBehavior == null)
+ return Enumerable.Empty();
- string[] coastalKingdoms = { "nordland", "ostland", "wasteland", "couronne", "anguille", "lyonesse", "mousillon", "bordeleaux", "brionne" };
+ return Kingdom.All
+ .Where(k => k != kingdom && !k.IsEliminated)
+ .Where(k => tradeAgreementBehavior.HasTradeAgreement(kingdom, k));
+ }
+
+ public static int GetTradeAgreementCount(this Kingdom kingdom)
+ {
+ return kingdom.GetTradeAgreementKingdoms().Count();
+ }
+
+ public static bool HasTradeAgreementWith(this Kingdom kingdom, Kingdom otherKingdom)
+ {
+ var tradeAgreementBehavior = Campaign.Current?.GetCampaignBehavior();
+ if (tradeAgreementBehavior == null)
+ return false;
+ return tradeAgreementBehavior.HasTradeAgreement(kingdom, otherKingdom);
+ }
+
+ public static IEnumerable GetEnemyKingdoms(this Kingdom kingdom)
+ {
+ if (kingdom == null)
+ return Enumerable.Empty();
- return coastalKingdoms.Any(id => kingdom.StringId == id);
+ return Kingdom.All
+ .Where(k => k != kingdom && !k.IsEliminated && kingdom.IsAtWarWith(k));
+ }
+
+ public static float GetTotalEnemyStrength(this Kingdom kingdom)
+ {
+ return kingdom.GetEnemyKingdoms().Sum(k => k.CurrentTotalStrength);
+ }
+
+ public static IEnumerable GetAlliedKingdoms(this IFaction faction)
+ {
+ Kingdom kingdom = faction as Kingdom;
+ return kingdom?.GetAlliedKingdoms();
}
- public static bool IsCastleFaction(this Kingdom kingdom)
+ public static IEnumerable GetAlliedKingdoms(this Kingdom kingdom)
{
- return kingdom.RulingClan.IsCastleFaction();
+ if (kingdom == null)
+ return Enumerable.Empty();
+
+ return Kingdom.All
+ .Where(k => k != kingdom && !k.IsEliminated && kingdom.IsAllyWith(k));
+ }
+
+ public static int GetAllianceCount(this Kingdom kingdom)
+ {
+ return kingdom.GetAlliedKingdoms().Count();
+ }
+
+ public static int GetWarCount(this Kingdom kingdom)
+ {
+ return kingdom.GetEnemyKingdoms().Count();
+ }
+
+ public static float GetAllianceTotalStrength(this Kingdom kingdom)
+ {
+ if (kingdom == null) return 0f;
+
+ float totalStrength = kingdom.CurrentTotalStrength;
+ foreach (var ally in kingdom.GetAlliedKingdoms())
+ {
+ totalStrength += ally.CurrentTotalStrength;
+ }
+ return totalStrength;
+ }
+
+ public static float GetTotalEnemyAllianceStrength(this Kingdom kingdom)
+ {
+ if (kingdom == null) return 0f;
+
+ float sum = 0f;
+ foreach (var enemy in kingdom.GetEnemyKingdoms())
+ {
+ sum += enemy.GetAllianceTotalStrength();
+ }
+ return sum;
+ }
+
+ public static void SetAlliance(this Kingdom kingdom1, Kingdom kingdom2)
+ {
+ if (kingdom1 == null || kingdom2 == null)
+ return;
+
+ var allianceBehavior = Campaign.Current?.GetCampaignBehavior();
+ allianceBehavior?.StartAlliance(kingdom1, kingdom2);
+ }
+
+ public static void SetAllianceClean(this Kingdom kingdom1, Kingdom kingdom2)
+ {
+ if (kingdom1 == null || kingdom2 == null)
+ return;
+
+ var enemies1 = kingdom1.GetEnemyKingdoms().ToList();
+ var enemies2 = kingdom2.GetEnemyKingdoms().ToList();
+
+ var allianceBehavior = Campaign.Current?.GetCampaignBehavior();
+ allianceBehavior?.StartAlliance(kingdom1, kingdom2);
+
+ JoinAllyWars(kingdom1, kingdom2, enemies2);
+ JoinAllyWars(kingdom2, kingdom1, enemies1);
+ }
+
+ private static void JoinAllyWars(Kingdom kingdom, Kingdom ally, List allyEnemies)
+ {
+ var allianceWarBehavior = Campaign.Current?.GetCampaignBehavior();
+
+ foreach (var enemy in allyEnemies)
+ {
+ if (kingdom.IsAtWarWith(enemy)) continue;
+ if (enemy.Culture?.StringId == TORConstants.Cultures.CHAOS) continue;
+ if (enemy == kingdom) continue;
+
+ allianceWarBehavior?.MarkAsAllianceWar(kingdom, enemy);
+ DeclareWarAction.ApplyByKingdomDecision(kingdom, enemy);
+ }
}
}
\ No newline at end of file
diff --git a/CSharpSourceCode/HarmonyPatches/LogEntryNotificationPatches.cs b/CSharpSourceCode/HarmonyPatches/LogEntryNotificationPatches.cs
new file mode 100644
index 0000000..c3903b4
--- /dev/null
+++ b/CSharpSourceCode/HarmonyPatches/LogEntryNotificationPatches.cs
@@ -0,0 +1,170 @@
+using System.Reflection;
+using HarmonyLib;
+using TaleWorlds.CampaignSystem;
+using TaleWorlds.CampaignSystem.LogEntries;
+using TaleWorlds.CampaignSystem.Settlements;
+using TOR_Core.Utilities;
+
+namespace TOR_Core.HarmonyPatches
+{
+ [HarmonyPatch(typeof(CampaignInformationManager), "NewLogEntryAdded")]
+ public static class LogEntryNotificationPatch
+ {
+ private static readonly bool DebugkingdomDecisions=false;
+
+ public static bool Prefix(LogEntry log)
+ {
+ if (!(log is IChatNotification chatNotification) || !chatNotification.IsVisibleNotification)
+ return true;
+
+ switch (log)
+ {
+ case TakePrisonerLogEntry prisonerLog:
+ return IsRelevantPrisonerLog(prisonerLog);
+
+ case EndCaptivityLogEntry endCaptivityLog:
+ return IsRelevantEndCaptivityLog(endCaptivityLog);
+
+ case CharacterKilledLogEntry killedLog:
+ return IsRelevantKilledLog(killedLog);
+
+ case MakePeaceLogEntry peaceLog:
+ return IsRelevantPeaceLog(peaceLog);
+
+ case DeclareWarLogEntry warLog:
+ return IsRelevantWarLog(warLog);
+
+ case ChangeSettlementOwnerLogEntry settlementLog:
+ return IsRelevantSettlementOwnerLog(settlementLog);
+
+ case BattleStartedLogEntry battleLog:
+ return IsRelevantBattleLog(battleLog);
+
+ case MercenaryClanChangedKingdomLogEntry mercenaryLog:
+ return IsRelevantMercenaryLog(mercenaryLog);
+
+ case ClanChangeKingdomLogEntry clanChangeLog:
+ return IsRelevantClanChangeLog(clanChangeLog);
+
+ case KingdomDecisionConcludedLogEntry decisionLog:
+ return IsRelevantKingdomDecisionLog(decisionLog);
+
+ default:
+ return true;
+ }
+ }
+
+ private static bool IsRelevantPrisonerLog(TakePrisonerLogEntry log)
+ {
+ if (TORNotificationHelper.IsHeroRelevantToPlayer(log.Prisoner))
+ return true;
+ if (TORNotificationHelper.IsHeroRelevantToPlayer(log.CapturerHero))
+ return true;
+ if (TORNotificationHelper.IsFactionRelevantToPlayer(log.CapturerPartyMapFaction))
+ return true;
+ return false;
+ }
+
+ private static bool IsRelevantEndCaptivityLog(EndCaptivityLogEntry log)
+ {
+ if (TORNotificationHelper.IsHeroRelevantToPlayer(log.Prisoner))
+ return true;
+ return false;
+ }
+
+ private static bool IsRelevantKilledLog(CharacterKilledLogEntry log)
+ {
+ if (TORNotificationHelper.IsHeroRelevantToPlayer(log.Victim))
+ return true;
+ if (log.Killer != null && TORNotificationHelper.IsHeroRelevantToPlayer(log.Killer))
+ return true;
+ return false;
+ }
+
+ private static bool IsRelevantPeaceLog(MakePeaceLogEntry log)
+ {
+ if (log.Faction1 is Kingdom k1 && TORNotificationHelper.IsKingdomRelevantToPlayer(k1))
+ return true;
+ if (log.Faction2 is Kingdom k2 && TORNotificationHelper.IsKingdomRelevantToPlayer(k2))
+ return true;
+ if (TORNotificationHelper.IsFactionRelevantToPlayer(log.Faction1))
+ return true;
+ if (TORNotificationHelper.IsFactionRelevantToPlayer(log.Faction2))
+ return true;
+ return false;
+ }
+
+ private static bool IsRelevantWarLog(DeclareWarLogEntry log)
+ {
+ if (log.Faction1 is Kingdom k1 && TORNotificationHelper.IsKingdomRelevantToPlayer(k1))
+ return true;
+ if (log.Faction2 is Kingdom k2 && TORNotificationHelper.IsKingdomRelevantToPlayer(k2))
+ return true;
+ if (TORNotificationHelper.IsFactionRelevantToPlayer(log.Faction1))
+ return true;
+ if (TORNotificationHelper.IsFactionRelevantToPlayer(log.Faction2))
+ return true;
+ return false;
+ }
+
+ private static bool IsRelevantSettlementOwnerLog(ChangeSettlementOwnerLogEntry log)
+ {
+ if (log.Settlement != null && log.Settlement.OwnerClan == Clan.PlayerClan)
+ return true;
+ if (log.Settlement?.OwnerClan?.Kingdom != null && TORNotificationHelper.IsKingdomRelevantToPlayer(log.Settlement.OwnerClan.Kingdom))
+ return true;
+ return false;
+ }
+
+ private static readonly FieldInfo AttackerFactionField = typeof(BattleStartedLogEntry).GetField("_attackerFaction", BindingFlags.NonPublic | BindingFlags.Instance);
+ private static readonly FieldInfo SettlementField = typeof(BattleStartedLogEntry).GetField("_settlement", BindingFlags.NonPublic | BindingFlags.Instance);
+
+ private static bool IsRelevantBattleLog(BattleStartedLogEntry log)
+ {
+ var attackerFaction = AttackerFactionField?.GetValue(log) as IFaction;
+ var settlement = SettlementField?.GetValue(log) as Settlement;
+
+ if (attackerFaction != null && TORNotificationHelper.IsFactionRelevantToPlayer(attackerFaction))
+ return true;
+
+ if (settlement?.OwnerClan?.Kingdom != null && TORNotificationHelper.IsKingdomRelevantToPlayer(settlement.OwnerClan.Kingdom))
+ return true;
+
+ if (settlement?.MapFaction != null && TORNotificationHelper.IsFactionRelevantToPlayer(settlement.MapFaction))
+ return true;
+
+ return false;
+ }
+
+ private static bool IsRelevantMercenaryLog(MercenaryClanChangedKingdomLogEntry log)
+ {
+ if (log.Clan == Clan.PlayerClan)
+ return true;
+ if (log.OldKingdom != null && TORNotificationHelper.IsKingdomRelevantToPlayer(log.OldKingdom))
+ return true;
+ if (log.NewKingdom != null && TORNotificationHelper.IsKingdomRelevantToPlayer(log.NewKingdom))
+ return true;
+ return false;
+ }
+
+ private static bool IsRelevantClanChangeLog(ClanChangeKingdomLogEntry log)
+ {
+ if (log.Clan == Clan.PlayerClan)
+ return true;
+ if (log.OldKingdom != null && TORNotificationHelper.IsKingdomRelevantToPlayer(log.OldKingdom))
+ return true;
+ if (log.NewKingdom != null && TORNotificationHelper.IsKingdomRelevantToPlayer(log.NewKingdom))
+ return true;
+ return false;
+ }
+
+ private static bool IsRelevantKingdomDecisionLog(KingdomDecisionConcludedLogEntry log)
+ {
+ if (DebugkingdomDecisions) return true;
+
+ if (log.Kingdom != null && TORNotificationHelper.IsKingdomRelevantToPlayer(log.Kingdom))
+ return true;
+ return false;
+ }
+ }
+}
diff --git a/CSharpSourceCode/Models/DiplomacyHelpers.cs b/CSharpSourceCode/Models/DiplomacyHelpers.cs
new file mode 100644
index 0000000..403f034
--- /dev/null
+++ b/CSharpSourceCode/Models/DiplomacyHelpers.cs
@@ -0,0 +1,244 @@
+using System;
+using System.Linq;
+using TaleWorlds.CampaignSystem;
+using TaleWorlds.CampaignSystem.CharacterDevelopment;
+using TOR_Core.CampaignMechanics.Religion;
+using TOR_Core.Extensions;
+
+namespace TOR_Core.Models
+{
+ ///
+ /// Shared helper methods for diplomacy calculations (war, alliance, trade).
+ /// Centralizes distance calculations and personality trait modifiers.
+ ///
+ public static class DiplomacyHelpers
+ {
+ // Distance thresholds for different diplomatic actions
+ public const float MaxWarDistance = 300f;
+ public const float MaxAllianceDistance = 500f;
+ public const float MaxTradeDistance = 600f;
+
+ // Personality trait modifier step (trait level -2 to +2 maps to 0.25 to 1.75)
+ private const float TraitModifierStep = 0.375f;
+
+ ///
+ /// Gets the approximate distance between two kingdoms based on their mid settlements.
+ ///
+ public static float GetKingdomDistance(Kingdom kingdom1, Kingdom kingdom2)
+ {
+ if (kingdom1?.FactionMidSettlement == null || kingdom2?.FactionMidSettlement == null)
+ return float.MaxValue;
+
+ return kingdom1.FactionMidSettlement.Position.Distance(kingdom2.FactionMidSettlement.Position);
+ }
+
+ ///
+ /// Checks if two kingdoms are within war declaration distance.
+ ///
+ public static bool IsWithinWarDistance(Kingdom kingdom1, Kingdom kingdom2)
+ {
+ return GetKingdomDistance(kingdom1, kingdom2) <= MaxWarDistance;
+ }
+
+ ///
+ /// Checks if two kingdoms are within alliance distance.
+ ///
+ public static bool IsWithinAllianceDistance(Kingdom kingdom1, Kingdom kingdom2)
+ {
+ return GetKingdomDistance(kingdom1, kingdom2) <= MaxAllianceDistance;
+ }
+
+ ///
+ /// Checks if two kingdoms are within trade agreement distance.
+ ///
+ public static bool IsWithinTradeDistance(Kingdom kingdom1, Kingdom kingdom2)
+ {
+ return GetKingdomDistance(kingdom1, kingdom2) <= MaxTradeDistance;
+ }
+
+ ///
+ /// Gets a trait-based modifier for diplomacy scoring.
+ /// Maps trait level (-2 to +2) to multiplier (0.25 to 1.75).
+ /// Higher trait level = higher modifier.
+ ///
+ public static float GetTraitModifier(Hero leader, TraitObject trait)
+ {
+ if (leader == null || trait == null) return 1f;
+ int traitLevel = leader.GetTraitLevel(trait);
+ return 1f + (traitLevel * TraitModifierStep);
+ }
+
+ ///
+ /// Gets an inverse trait-based modifier for diplomacy scoring.
+ /// Maps trait level (-2 to +2) to multiplier (1.75 to 0.25).
+ /// Higher trait level = lower modifier.
+ ///
+ public static float GetInverseTraitModifier(Hero leader, TraitObject trait)
+ {
+ if (leader == null || trait == null) return 1f;
+ int traitLevel = leader.GetTraitLevel(trait);
+ return 1f - (traitLevel * TraitModifierStep);
+ }
+
+ ///
+ /// Gets the dominant pantheon for a kingdom based on its leader's religion or culture.
+ ///
+ public static Pantheon GetKingdomPantheon(Kingdom kingdom)
+ {
+ if (kingdom == null) return Pantheon.Human;
+
+ var leaderReligion = kingdom.Leader?.GetDominantReligion();
+ if (leaderReligion != null)
+ return leaderReligion.Pantheon;
+
+ return ReligionObjectHelper.GetPantheon(kingdom.Culture?.StringId);
+ }
+
+ ///
+ /// Checks if two kingdoms share the same culture.
+ ///
+ public static bool AreSameCulture(Kingdom kingdom1, Kingdom kingdom2)
+ {
+ var culture1 = kingdom1?.Culture?.StringId;
+ var culture2 = kingdom2?.Culture?.StringId;
+ return !string.IsNullOrEmpty(culture1) && culture1 == culture2;
+ }
+
+ ///
+ /// Gets the culture compatibility between two kingdoms.
+ /// Returns: -1 (hostile) to +1 (friendly), 0 = neutral
+ ///
+ public static float GetCultureCompatibility(Kingdom kingdom1, Kingdom kingdom2)
+ {
+ var culture1 = kingdom1?.Culture?.StringId;
+ var culture2 = kingdom2?.Culture?.StringId;
+
+ if (string.IsNullOrEmpty(culture1) || string.IsNullOrEmpty(culture2))
+ return 0f;
+
+ return ReligionObjectHelper.CalculateCultureCompatibility(culture1, culture2);
+ }
+
+ ///
+ /// Checks if two kingdoms share the same religion.
+ ///
+ public static bool AreSameReligion(Kingdom kingdom1, Kingdom kingdom2)
+ {
+ var religion1 = kingdom1?.Leader?.GetDominantReligion();
+ var religion2 = kingdom2?.Leader?.GetDominantReligion();
+ return religion1 != null && religion1 == religion2;
+ }
+
+ ///
+ /// Checks if two kingdoms have hostile religions.
+ ///
+ public static bool AreReligionsHostile(Kingdom kingdom1, Kingdom kingdom2)
+ {
+ var religion1 = kingdom1?.Leader?.GetDominantReligion();
+ var religion2 = kingdom2?.Leader?.GetDominantReligion();
+
+ if (religion1 == null || religion2 == null)
+ return false;
+
+ return religion1.HostileReligions != null && religion1.HostileReligions.Contains(religion2);
+ }
+
+ ///
+ /// Gets the religion compatibility between two kingdoms.
+ /// Returns: -1 (hostile) to +1 (friendly), 0 = neutral
+ ///
+ public static float GetReligionCompatibility(Kingdom kingdom1, Kingdom kingdom2)
+ {
+ var religion1 = kingdom1?.Leader?.GetDominantReligion();
+ var religion2 = kingdom2?.Leader?.GetDominantReligion();
+
+ if (religion1 == null || religion2 == null)
+ return 0f;
+
+ return ReligionObjectHelper.CalculateReligionCompatibility(religion1, religion2);
+ }
+
+ ///
+ /// Gets the pantheon compatibility between two kingdoms.
+ /// Returns: -1 (hostile) to +1 (friendly), 0 = neutral
+ ///
+ public static float GetPantheonCompatibility(Kingdom kingdom1, Kingdom kingdom2)
+ {
+ var religion1 = kingdom1?.Leader?.GetDominantReligion();
+ var religion2 = kingdom2?.Leader?.GetDominantReligion();
+
+ if (religion1 == null || religion2 == null)
+ return 0f;
+
+ return ReligionObjectHelper.GetPantheonCompatibility(religion1.Pantheon, religion2.Pantheon);
+ }
+
+ ///
+ /// Calculates the average relation between a clan's members and a kingdom's clan leaders.
+ ///
+ public static float CalculateClanToKingdomRelation(Clan clan, Kingdom kingdom)
+ {
+ if (clan == null || kingdom == null)
+ return 0f;
+
+ var clanHeroes = clan.Heroes.Where(h => h.IsAlive && !h.IsChild).ToList();
+ var kingdomLeaders = kingdom.Clans
+ .Where(c => c.Leader != null && c.Leader.IsAlive)
+ .Select(c => c.Leader)
+ .ToList();
+
+ if (!clanHeroes.Any() || !kingdomLeaders.Any())
+ return 0f;
+
+ float totalRelation = 0f;
+ int count = 0;
+
+ foreach (var clanHero in clanHeroes)
+ {
+ foreach (var kingdomLeader in kingdomLeaders)
+ {
+ totalRelation += clanHero.GetRelation(kingdomLeader);
+ count++;
+ }
+ }
+
+ return count > 0 ? totalRelation / count : 0f;
+ }
+
+ ///
+ /// Calculates the average relation between two kingdoms based on all clan leaders.
+ ///
+ public static float CalculateKingdomToKingdomRelation(Kingdom kingdom1, Kingdom kingdom2)
+ {
+ if (kingdom1 == null || kingdom2 == null)
+ return 0f;
+
+ var leaders1 = kingdom1.Clans
+ .Where(c => c.Leader != null && c.Leader.IsAlive)
+ .Select(c => c.Leader)
+ .ToList();
+
+ var leaders2 = kingdom2.Clans
+ .Where(c => c.Leader != null && c.Leader.IsAlive)
+ .Select(c => c.Leader)
+ .ToList();
+
+ if (!leaders1.Any() || !leaders2.Any())
+ return 0f;
+
+ float totalRelation = 0f;
+ int count = 0;
+
+ foreach (var leader1 in leaders1)
+ {
+ foreach (var leader2 in leaders2)
+ {
+ totalRelation += leader1.GetRelation(leader2);
+ count++;
+ }
+ }
+
+ return count > 0 ? totalRelation / count : 0f;
+ }
+ }
+}
diff --git a/CSharpSourceCode/Models/TORAllianceModel.cs b/CSharpSourceCode/Models/TORAllianceModel.cs
new file mode 100644
index 0000000..5cfec8d
--- /dev/null
+++ b/CSharpSourceCode/Models/TORAllianceModel.cs
@@ -0,0 +1,753 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using TaleWorlds.CampaignSystem;
+using TaleWorlds.CampaignSystem.CharacterDevelopment;
+using TaleWorlds.CampaignSystem.GameComponents;
+using TaleWorlds.Core;
+using TaleWorlds.Library;
+using TaleWorlds.Localization;
+using TOR_Core.CampaignMechanics.Religion;
+using TOR_Core.Extensions;
+using TOR_Core.Utilities;
+
+namespace TOR_Core.Models
+{
+ ///
+ /// TOR Alliance Model - Determines AI willingness to form alliances.
+ /// Alliances are defensive pacts that pull allies into wars.
+ ///
+ public class TORAllianceModel : DefaultAllianceModel
+ {
+ // Distance scoring
+ private const float CloseDistanceThreshold = 150f;
+ private const float DistancePenaltyMultiplier = 1.5f;
+
+ // Reliability scoring (culture/religion)
+ private const float SameReligionBonus = 30f;
+ private const float SameCultureBonus = 20f;
+ private const float HostileReligionPenalty = -50f;
+ private const float ReligionCompatibilityWeight = 20f;
+ private const float CultureCompatibilityWeight = 15f;
+
+ // Power scoring
+ private const float PowerRatioWeight = 20f;
+ private const float MinimumUsefulStrengthRatio = 0.3f; // Ally should have at least 30% of our strength
+
+ // Strategic positioning
+ private const float ProximityToEnemyWeight = 25f;
+ private const float SharedBorderWithEnemyBonus = 15f;
+
+ // Common enemies
+ private const float SharedActiveWarWeight = 40f;
+ private const float SharedHostileFactionWeight = 20f;
+ private const float SharedThreatWeight = 15f;
+
+ // Entanglement risk
+ private const float EnemyCountPenaltyWeight = -10f;
+ private const float StrongEnemyPenaltyWeight = -15f;
+ private const float InheritedWarRiskWeight = -20f;
+
+ // Alliance network
+ private const float CompatibleAllyBonus = 15f;
+ private const float IncompatibleAllyPenalty = -20f;
+ private const float HostileAllyPenalty = -40f;
+
+ // Protective alliance
+ private const float ProtectThreatenedWeight = 25f;
+ private const float ProtectWeakerWeight = 15f;
+
+ // Lore considerations
+ private const float EonirDiplomacyBonus = 15f;
+ private const float DawiAsraiRivalryPenalty = -60f; // War of the Beard grudge
+
+ // Personality trait weights
+ private const float HonorTraitWeight = 15f;
+
+ // Kingdom relations
+ private const float KingdomRelationWeight = 0.3f; // -100 to +100 relation = -30 to +30 score
+
+ // Honor Alliance Decision weights
+ private const float HonorAllianceBaseHonorWeight = 30f; // Honorable lords keep their word
+ private const float HonorAllianceBaseValorWeight = 20f; // Brave lords want to fight
+ private const float HonorAllianceHostileReligionBonus = 50f;
+ private const float HonorAllianceAttackerReligionWeight = 20f;
+ private const float HonorAllianceAllyReligionWeight = 15f;
+ private const float HonorAllianceAttackerCultureWeight = 25f;
+ private const float HonorAllianceAllyCultureWeight = 15f;
+ private const float HonorAllianceRelationWeight = 0.5f;
+ private const float HonorAllianceHighRelationBonus = 20f;
+ private const float HonorAllianceVeryHighRelationBonus = 40f;
+ private const float HonorAllianceLowRelationPenalty = -15f;
+ private const float HonorAllianceStrengthHopelessPenalty = -30f;
+ private const float HonorAllianceStrengthConfidentBonus = 15f;
+
+ private static TextObject _loreText => TORTextHelper.GetTextObject("tor_alliance_lore", "Faction disposition");
+ private static TextObject _distanceText => TORTextHelper.GetTextObject("tor_alliance_distance", "Geographic distance");
+ private static TextObject _reliabilityText => TORTextHelper.GetTextObject("tor_alliance_reliability", "Partner reliability");
+ private static TextObject _powerText => TORTextHelper.GetTextObject("tor_alliance_power", "Military strength");
+ private static TextObject _positioningText => TORTextHelper.GetTextObject("tor_alliance_position", "Strategic positioning");
+ private static TextObject _commonEnemiesText => TORTextHelper.GetTextObject("tor_alliance_common_enemy", "Common enemies");
+ private static TextObject _entanglementText => TORTextHelper.GetTextObject("tor_alliance_entangle", "Entanglement risk");
+ private static TextObject _protectiveText => TORTextHelper.GetTextObject("tor_alliance_protect", "Protective instinct");
+ private static TextObject _honorText => TORTextHelper.GetTextObject("tor_alliance_honor", "Alliance commitment");
+ private static TextObject _allianceNetworkText => TORTextHelper.GetTextObject("tor_alliance_network", "Alliance network");
+ private static TextObject _relationText => TORTextHelper.GetTextObject("tor_alliance_relation", "Personal relations");
+
+ private static TextObject _chaosCannotAllyText => TORTextHelper.GetTextObject("tor_alliance_chaos", "The forces of Chaos do not form alliances.");
+ private static TextObject _greenskinCannotAllyText => TORTextHelper.GetTextObject("tor_alliance_greenskin", "Greenskins do not understand alliances.");
+ private static TextObject _allianceLimitText => TORTextHelper.GetTextObject("tor_alliance_limit", "Alliance limit reached");
+
+ public override int MaxNumberOfAlliances => 2;
+
+ public override ExplainedNumber GetScoreOfStartingAlliance(
+ Kingdom proposingKingdom,
+ Kingdom targetKingdom,
+ IFaction evaluatingFaction,
+ out TextObject explanationText,
+ bool includeDescription = false)
+ {
+ var score = base.GetScoreOfStartingAlliance(
+ proposingKingdom, targetKingdom, evaluatingFaction,
+ out explanationText, includeDescription);
+
+ // Hard lore restrictions - return early with massive penalty
+ if (!CanFormAlliance(proposingKingdom, targetKingdom))
+ {
+ score.Add(-10000f, _loreText);
+ return score;
+ }
+
+ // Alliance limit check - can't ally if either kingdom is at max
+ if (proposingKingdom.GetAllianceCount() >= MaxNumberOfAlliances ||
+ targetKingdom.GetAllianceCount() >= MaxNumberOfAlliances)
+ {
+ score.Add(-10000f, _allianceLimitText);
+ return score;
+ }
+
+ // Distance check - too far to be useful allies
+ if (!DiplomacyHelpers.IsWithinAllianceDistance(proposingKingdom, targetKingdom))
+ {
+ score.Add(-500f, _distanceText);
+ return score;
+ }
+
+ // Get the evaluating leader for personality traits
+ Hero evaluatingLeader = GetEvaluatingLeader(evaluatingFaction);
+
+ // Get trait modifiers
+ float honorModifier = DiplomacyHelpers.GetTraitModifier(evaluatingLeader, DefaultTraits.Honor);
+ float calculatingModifier = DiplomacyHelpers.GetTraitModifier(evaluatingLeader, DefaultTraits.Calculating);
+ float mercyModifier = DiplomacyHelpers.GetTraitModifier(evaluatingLeader, DefaultTraits.Mercy);
+ float generosityModifier = DiplomacyHelpers.GetTraitModifier(evaluatingLeader, DefaultTraits.Generosity);
+ float valorModifier = DiplomacyHelpers.GetTraitModifier(evaluatingLeader, DefaultTraits.Valor);
+ float valorInverseModifier = DiplomacyHelpers.GetInverseTraitModifier(evaluatingLeader, DefaultTraits.Valor);
+ float generosityInverseModifier = DiplomacyHelpers.GetInverseTraitModifier(evaluatingLeader, DefaultTraits.Generosity);
+
+ // Check compatibility for common enemies calculation
+ bool isCompatible = IsCompatiblePartner(proposingKingdom, targetKingdom);
+
+ // Calculate individual scores
+ float loreScore = CalculateLoreConsiderations(proposingKingdom, targetKingdom);
+ float distanceScore = CalculateDistanceScore(proposingKingdom, targetKingdom);
+ float reliabilityScore = CalculateReliabilityScore(proposingKingdom, targetKingdom) * honorModifier;
+ float powerScore = CalculatePowerScore(proposingKingdom, targetKingdom) * calculatingModifier * generosityInverseModifier;
+ float positioningScore = CalculateStrategicPositioningScore(proposingKingdom, targetKingdom) * calculatingModifier;
+ float commonEnemiesScore = CalculateCommonEnemiesScore(proposingKingdom, targetKingdom, isCompatible, calculatingModifier);
+ float entanglementScore = CalculateEntanglementRiskScore(proposingKingdom, targetKingdom) * calculatingModifier * valorInverseModifier;
+ float allianceNetworkScore = CalculateAllianceNetworkScore(proposingKingdom, targetKingdom) * calculatingModifier;
+
+ // Kingdom relations - average of all clan leaders' relations
+ float kingdomRelation = DiplomacyHelpers.CalculateKingdomToKingdomRelation(proposingKingdom, targetKingdom);
+ float relationScore = kingdomRelation * KingdomRelationWeight * mercyModifier;
+
+ // Protective alliances require both mercy AND generosity - cruel or greedy lords don't consider them
+ float protectiveScore = 0f;
+ if (mercyModifier > 1f && generosityModifier > 1f)
+ {
+ // Only merciful AND generous lords consider protective alliances
+ // Scale by how merciful/generous they are above baseline
+ float mercyFactor = mercyModifier - 1f; // 0 to 0.75
+ float generosityFactor = generosityModifier - 1f; // 0 to 0.75
+ protectiveScore = CalculateProtectiveAllianceScore(proposingKingdom, targetKingdom) * (1f + mercyFactor + generosityFactor);
+ }
+
+ // Honor bonus - honorable lords value alliance commitments
+ float honorScore = (honorModifier - 1f) * HonorTraitWeight;
+
+ // Add scores to explained number
+ if (loreScore != 0) score.Add(loreScore, _loreText);
+ if (distanceScore != 0) score.Add(distanceScore, _distanceText);
+ if (reliabilityScore != 0) score.Add(reliabilityScore, _reliabilityText);
+ if (powerScore != 0) score.Add(powerScore, _powerText);
+ if (positioningScore != 0) score.Add(positioningScore, _positioningText);
+ if (commonEnemiesScore != 0) score.Add(commonEnemiesScore, _commonEnemiesText);
+ if (entanglementScore != 0) score.Add(entanglementScore, _entanglementText);
+ if (allianceNetworkScore != 0) score.Add(allianceNetworkScore, _allianceNetworkText);
+ if (relationScore != 0) score.Add(relationScore, _relationText);
+ if (protectiveScore != 0) score.Add(protectiveScore, _protectiveText);
+ if (honorScore != 0) score.Add(honorScore, _honorText);
+
+ return score;
+ }
+
+ ///
+ /// Gets the leader of the evaluating faction.
+ ///
+ private Hero GetEvaluatingLeader(IFaction evaluatingFaction)
+ {
+ if (evaluatingFaction is Clan clan)
+ return clan.Leader;
+ if (evaluatingFaction is Kingdom kingdom)
+ return kingdom.Leader;
+ return null;
+ }
+
+ ///
+ /// Checks if two kingdoms are compatible (same religion or culture).
+ /// Used to determine if common enemy score applies fully or only for calculating lords.
+ ///
+ private bool IsCompatiblePartner(Kingdom kingdom1, Kingdom kingdom2)
+ {
+ if (DiplomacyHelpers.AreSameReligion(kingdom1, kingdom2))
+ return true;
+ if (DiplomacyHelpers.AreSameCulture(kingdom1, kingdom2))
+ return true;
+
+ // Check pantheon compatibility
+ var pantheon1 = DiplomacyHelpers.GetKingdomPantheon(kingdom1);
+ var pantheon2 = DiplomacyHelpers.GetKingdomPantheon(kingdom2);
+
+ // Same pantheon = compatible
+ if (pantheon1 == pantheon2)
+ return true;
+
+ // Human-Dwarven-Elven are generally compatible (Order vs Chaos)
+ if (IsOrderPantheon(pantheon1) && IsOrderPantheon(pantheon2))
+ return true;
+
+ return false;
+ }
+
+ private bool IsOrderPantheon(Pantheon pantheon)
+ {
+ return pantheon == Pantheon.Human ||
+ pantheon == Pantheon.Dwarven ||
+ pantheon == Pantheon.Elven;
+ }
+
+ ///
+ /// Lore-based alliance considerations.
+ /// Eonir are natural diplomats.
+ /// Dwarfs and Wood Elves have ancient grudges (War of the Beard).
+ ///
+ private float CalculateLoreConsiderations(Kingdom proposingKingdom, Kingdom targetKingdom)
+ {
+ float score = 0f;
+
+ // Eonir are skilled diplomats
+ if (proposingKingdom.Culture?.StringId == TORConstants.Cultures.EONIR)
+ score += EonirDiplomacyBonus;
+
+ // War of the Beard - Dwarfs and Wood Elves distrust each other
+ var proposingCulture = proposingKingdom.Culture?.StringId;
+ var targetCulture = targetKingdom.Culture?.StringId;
+ if ((proposingCulture == TORConstants.Cultures.DAWI && targetCulture == TORConstants.Cultures.ASRAI) ||
+ (proposingCulture == TORConstants.Cultures.ASRAI && targetCulture == TORConstants.Cultures.DAWI))
+ {
+ score += DawiAsraiRivalryPenalty;
+ }
+
+ return score;
+ }
+
+ ///
+ /// Distance score - closer allies are more useful.
+ /// Returns: 0 (close) to -90 (far but within range)
+ ///
+ private float CalculateDistanceScore(Kingdom proposingKingdom, Kingdom targetKingdom)
+ {
+ float distance = DiplomacyHelpers.GetKingdomDistance(proposingKingdom, targetKingdom);
+
+ if (distance <= CloseDistanceThreshold)
+ return 0f;
+
+ // Gradual penalty for distance
+ return (CloseDistanceThreshold - distance) * DistancePenaltyMultiplier;
+ }
+
+ ///
+ /// Reliability score based on religion and culture compatibility.
+ /// Trustworthy partners make better allies.
+ /// Returns: -50 (hostile) to +50 (very reliable)
+ ///
+ private float CalculateReliabilityScore(Kingdom proposingKingdom, Kingdom targetKingdom)
+ {
+ // Same religion = very reliable
+ if (DiplomacyHelpers.AreSameReligion(proposingKingdom, targetKingdom))
+ return SameReligionBonus;
+
+ // Hostile religions = unreliable (will backstab)
+ if (DiplomacyHelpers.AreReligionsHostile(proposingKingdom, targetKingdom))
+ return HostileReligionPenalty;
+
+ float score = 0f;
+
+ // Same culture = reliable
+ if (DiplomacyHelpers.AreSameCulture(proposingKingdom, targetKingdom))
+ score += SameCultureBonus;
+
+ // Otherwise use compatibility scores
+ float religionCompat = DiplomacyHelpers.GetReligionCompatibility(proposingKingdom, targetKingdom);
+ float cultureCompat = DiplomacyHelpers.GetCultureCompatibility(proposingKingdom, targetKingdom);
+
+ score += religionCompat * ReligionCompatibilityWeight;
+ score += cultureCompat * CultureCompatibilityWeight;
+
+ return score;
+ }
+
+ ///
+ /// Power score - evaluate if ally has enough strength to be useful.
+ /// Uses alliance strength (includes their current allies).
+ /// Returns: -20 (too weak) to +30 (powerful ally)
+ ///
+ private float CalculatePowerScore(Kingdom proposingKingdom, Kingdom targetKingdom)
+ {
+ // Use alliance strength - their allies would become our allies too
+ float ourStrength = Math.Max(proposingKingdom.GetAllianceTotalStrength(), 1f);
+ float theirStrength = targetKingdom.GetAllianceTotalStrength();
+
+ float strengthRatio = theirStrength / ourStrength;
+
+ // Too weak to be useful
+ if (strengthRatio < MinimumUsefulStrengthRatio)
+ return -20f;
+
+ // Scale: 0.3 ratio = 0, 1.0 ratio = ~14, 2.0 ratio = ~28
+ float score = (strengthRatio - MinimumUsefulStrengthRatio) * PowerRatioWeight;
+
+ return MBMath.ClampFloat(score, -20f, 30f);
+ }
+
+ ///
+ /// Strategic positioning - can this ally help against our current enemies?
+ /// Returns: 0 (no positioning value) to +40 (excellent position)
+ ///
+ private float CalculateStrategicPositioningScore(Kingdom proposingKingdom, Kingdom targetKingdom)
+ {
+ float score = 0f;
+
+ // Check if target kingdom is close to any of our enemies
+ foreach (var enemyKingdom in proposingKingdom.GetEnemyKingdoms())
+ {
+ float distanceToEnemy = DiplomacyHelpers.GetKingdomDistance(targetKingdom, enemyKingdom);
+
+ // Close to our enemy = can help us fight
+ if (distanceToEnemy <= DiplomacyHelpers.MaxWarDistance)
+ {
+ score += ProximityToEnemyWeight;
+
+ // Shares border with enemy = even better (buffer state)
+ if (distanceToEnemy <= 150f)
+ score += SharedBorderWithEnemyBonus;
+ }
+ }
+
+ return score;
+ }
+
+ ///
+ /// Common enemies score - shared threats create natural alliances.
+ /// For incompatible partners, only calculating lords see value.
+ /// Returns: 0 to +80 (many shared enemies)
+ ///
+ private float CalculateCommonEnemiesScore(Kingdom proposingKingdom, Kingdom targetKingdom, bool isCompatible, float calculatingModifier)
+ {
+ float score = 0f;
+
+ // Shared active wars - both fighting the same enemy
+ foreach (var enemyKingdom in proposingKingdom.GetEnemyKingdoms())
+ {
+ if (targetKingdom.IsAtWarWith(enemyKingdom))
+ {
+ score += SharedActiveWarWeight;
+ }
+ }
+
+ // Shared hostile factions (religions/cultures that hate both of us)
+ var ourPantheon = DiplomacyHelpers.GetKingdomPantheon(proposingKingdom);
+ var theirPantheon = DiplomacyHelpers.GetKingdomPantheon(targetKingdom);
+
+ // Both threatened by Chaos
+ if (ourPantheon != Pantheon.Chaos && theirPantheon != Pantheon.Chaos)
+ {
+ bool chaosExists = Kingdom.All.Any(k => !k.IsEliminated &&
+ DiplomacyHelpers.GetKingdomPantheon(k) == Pantheon.Chaos);
+ if (chaosExists)
+ score += SharedThreatWeight;
+ }
+
+ // Both threatened by Greenskins
+ if (ourPantheon != Pantheon.Greenskin && theirPantheon != Pantheon.Greenskin)
+ {
+ bool greenskinExists = Kingdom.All.Any(k => !k.IsEliminated &&
+ DiplomacyHelpers.GetKingdomPantheon(k) == Pantheon.Greenskin);
+ if (greenskinExists)
+ score += SharedThreatWeight;
+ }
+
+ // Shared hostile religions
+ if (DiplomacyHelpers.AreReligionsHostile(proposingKingdom, targetKingdom) == false)
+ {
+ // Check if we share enemies based on religion
+ var ourReligion = proposingKingdom.Leader?.GetDominantReligion();
+ var theirReligion = targetKingdom.Leader?.GetDominantReligion();
+
+ if (ourReligion?.HostileReligions != null && theirReligion?.HostileReligions != null)
+ {
+ foreach (var hostile in ourReligion.HostileReligions)
+ {
+ if (theirReligion.HostileReligions.Contains(hostile))
+ score += SharedHostileFactionWeight;
+ }
+ }
+ }
+
+ // Apply compatibility modifier
+ if (isCompatible)
+ {
+ return score;
+ }
+ else
+ {
+ // Incompatible partners - only calculating lords see value
+ // calculatingModifier ranges from 0.25 (-2 trait) to 1.75 (+2 trait)
+ // We want: -2 trait = 0, 0 trait = 0, +2 trait = full score
+ float pragmatismFactor = Math.Max(0f, calculatingModifier - 1f); // 0 to 0.75
+ return score * pragmatismFactor * 1.33f; // Scale so +2 calculating gets full score
+ }
+ }
+
+ ///
+ /// Entanglement risk - will this alliance drag us into unwanted wars?
+ /// Returns: 0 (low risk) to -60 (high risk)
+ ///
+ private float CalculateEntanglementRiskScore(Kingdom proposingKingdom, Kingdom targetKingdom)
+ {
+ float score = 0f;
+
+ // How many wars are they fighting?
+ int theirWarCount = targetKingdom.GetWarCount();
+ score += theirWarCount * EnemyCountPenaltyWeight;
+
+ // Are their enemies strong? Would we inherit dangerous wars?
+ float ourStrength = proposingKingdom.CurrentTotalStrength;
+
+ foreach (var enemyKingdom in targetKingdom.GetEnemyKingdoms())
+ {
+ // Skip if we're already at war with them (not additional risk)
+ if (proposingKingdom.IsAtWarWith(enemyKingdom))
+ continue;
+
+ float enemyStrength = enemyKingdom.CurrentTotalStrength;
+
+ // Strong enemy we'd inherit = risk
+ if (enemyStrength > ourStrength * 0.5f)
+ score += StrongEnemyPenaltyWeight;
+
+ // Any inherited war is a risk
+ score += InheritedWarRiskWeight;
+ }
+
+ return MBMath.ClampFloat(score, -60f, 0f);
+ }
+
+ ///
+ /// Alliance network score - evaluates the compatibility of their current allies with us.
+ /// Compatible allies = good (joining a friendly network)
+ /// Hostile allies = bad (their allies might cause problems for us)
+ /// Returns: -60 (hostile network) to +45 (very compatible network)
+ ///
+ private float CalculateAllianceNetworkScore(Kingdom proposingKingdom, Kingdom targetKingdom)
+ {
+ float score = 0f;
+
+ // Check each of their current allies
+ foreach (var theirAlly in targetKingdom.GetAlliedKingdoms())
+ {
+ // Skip if it's us (shouldn't happen but be safe)
+ if (theirAlly == proposingKingdom)
+ continue;
+
+ // Check if their ally is hostile to us
+ if (DiplomacyHelpers.AreReligionsHostile(proposingKingdom, theirAlly))
+ {
+ score += HostileAllyPenalty;
+ continue;
+ }
+
+ // Check if their ally is compatible with us
+ if (IsCompatiblePartner(proposingKingdom, theirAlly))
+ {
+ score += CompatibleAllyBonus;
+ }
+ else
+ {
+ // Incompatible but not hostile - mild concern
+ score += IncompatibleAllyPenalty;
+ }
+ }
+
+ return MBMath.ClampFloat(score, -60f, 45f);
+ }
+
+ ///
+ /// Protective alliance score - merciful lords want to protect threatened kingdoms.
+ /// Returns: 0 (not threatened) to +40 (severely threatened)
+ ///
+ private float CalculateProtectiveAllianceScore(Kingdom proposingKingdom, Kingdom targetKingdom)
+ {
+ float score = 0f;
+
+ // Is target kingdom under threat?
+ int theirWarCount = targetKingdom.GetWarCount();
+ if (theirWarCount > 0)
+ {
+ score += theirWarCount * ProtectThreatenedWeight;
+ }
+
+ // Is target kingdom weaker than their enemies?
+ float theirStrength = targetKingdom.CurrentTotalStrength;
+ float enemyStrength = targetKingdom.GetTotalEnemyStrength();
+
+ if (enemyStrength > theirStrength)
+ {
+ float vulnerabilityRatio = Math.Min(enemyStrength / Math.Max(theirStrength, 1f), 3f);
+ score += vulnerabilityRatio * ProtectWeakerWeight;
+ }
+
+ return MBMath.ClampFloat(score, 0f, 40f);
+ }
+
+ public bool CanFormAlliance(Kingdom kingdom1, Kingdom kingdom2)
+ {
+ var pantheon1 = DiplomacyHelpers.GetKingdomPantheon(kingdom1);
+ var pantheon2 = DiplomacyHelpers.GetKingdomPantheon(kingdom2);
+
+ // Chaos cannot ally
+ if (pantheon1 == Pantheon.Chaos || pantheon2 == Pantheon.Chaos)
+ return false;
+
+ // Greenskins cannot ally
+ if (pantheon1 == Pantheon.Greenskin || pantheon2 == Pantheon.Greenskin)
+ return false;
+
+ return true;
+ }
+
+ ///
+ /// Calculates how much a clan supports joining a war to honor an alliance.
+ /// Uses personality traits to modify various factors.
+ /// Positive = support joining, Negative = support breaking alliance.
+ ///
+ public float CalculateHonorAllianceSupport(Clan clan, Kingdom ourKingdom, Kingdom attackedAlly, Kingdom attacker)
+ {
+ if (clan?.Leader == null)
+ return 0f;
+
+ var leader = clan.Leader;
+ float support = 0f;
+
+ // Get trait modifiers
+ float honorModifier = DiplomacyHelpers.GetTraitModifier(leader, DefaultTraits.Honor);
+ float valorModifier = DiplomacyHelpers.GetTraitModifier(leader, DefaultTraits.Valor);
+ float valorInverseModifier = DiplomacyHelpers.GetInverseTraitModifier(leader, DefaultTraits.Valor);
+ float mercyModifier = DiplomacyHelpers.GetTraitModifier(leader, DefaultTraits.Mercy);
+ float calculatingModifier = DiplomacyHelpers.GetTraitModifier(leader, DefaultTraits.Calculating);
+ float calculatingInverseModifier = DiplomacyHelpers.GetInverseTraitModifier(leader, DefaultTraits.Calculating);
+
+ // === BASE TRAIT BONUSES ===
+ // Honorable lords keep their word
+ support += (honorModifier - 1f) * HonorAllianceBaseHonorWeight / 0.375f;
+ // Brave lords want to fight
+ support += (valorModifier - 1f) * HonorAllianceBaseValorWeight / 0.375f;
+
+ // === RELIGION FACTORS (modified by Calculating inverse + Mercy) ===
+ var clanReligion = leader.GetDominantReligion();
+ var attackerReligion = attacker.Leader?.GetDominantReligion();
+ var allyReligion = attackedAlly.Leader?.GetDominantReligion();
+
+ float religionScore = 0f;
+
+ // Strong bonus for fighting religious enemies
+ if (clanReligion != null && attackerReligion != null)
+ {
+ if (clanReligion.HostileReligions?.Contains(attackerReligion) == true)
+ {
+ religionScore += HonorAllianceHostileReligionBonus;
+ }
+ else
+ {
+ // Similarity with attacker reduces support for war
+ float attackerSimilarity = ReligionObjectHelper.CalculateReligionCompatibility(clanReligion, attackerReligion);
+ religionScore -= attackerSimilarity * HonorAllianceAttackerReligionWeight;
+ }
+ }
+
+ // Bonus for defending co-religionists
+ if (clanReligion != null && allyReligion != null)
+ {
+ float allySimilarity = ReligionObjectHelper.CalculateReligionCompatibility(clanReligion, allyReligion);
+ religionScore += allySimilarity * HonorAllianceAllyReligionWeight;
+ }
+
+ // Apply trait modifiers: Pragmatists ignore religion, Merciful defend faith
+ support += religionScore * calculatingInverseModifier * mercyModifier;
+
+ // === RELATION WITH ALLY (modified by Mercy + Calculating inverse) ===
+ float allyRelation = DiplomacyHelpers.CalculateClanToKingdomRelation(clan, attackedAlly);
+ float allyRelationScore = allyRelation * HonorAllianceRelationWeight;
+
+ // Very high relations make breaking almost unthinkable
+ if (allyRelation > 80)
+ allyRelationScore += HonorAllianceVeryHighRelationBonus;
+ else if (allyRelation > 50)
+ allyRelationScore += HonorAllianceHighRelationBonus;
+
+ // Very low relations make breaking more acceptable
+ if (allyRelation < -20)
+ allyRelationScore += HonorAllianceLowRelationPenalty;
+
+ support += allyRelationScore * mercyModifier * calculatingInverseModifier;
+
+ // === RELATION WITH ATTACKER (modified by Calculating inverse only) ===
+ float attackerRelation = DiplomacyHelpers.CalculateClanToKingdomRelation(clan, attacker);
+ float attackerRelationScore = -attackerRelation * 0.4f; // Good relations = less willing to fight
+ support += attackerRelationScore * calculatingInverseModifier;
+
+ // === STRENGTH CONSIDERATION (modified by Calculating + Valor inverse) ===
+ float allianceStrength = ourKingdom.CurrentTotalStrength + attackedAlly.CurrentTotalStrength;
+ float enemyStrength = attacker.CurrentTotalStrength;
+
+ float strengthScore = 0f;
+ if (enemyStrength > allianceStrength * 3f)
+ {
+ strengthScore = HonorAllianceStrengthHopelessPenalty; // Hesitant to join hopeless war
+ }
+ else if (allianceStrength > enemyStrength * 2f)
+ {
+ strengthScore = HonorAllianceStrengthConfidentBonus; // Confident in victory
+ }
+
+ // Calculating lords weigh odds carefully, Brave lords ignore odds
+ support += strengthScore * calculatingModifier * valorInverseModifier;
+
+ // === CULTURE COMPATIBILITY (modified by Honor + Calculating inverse) ===
+ // Hostile cultures with attacker = more support for war
+ float attackerCultureCompat = ReligionObjectHelper.CalculateCultureCompatibility(
+ ourKingdom.Culture?.StringId, attacker.Culture?.StringId);
+ float attackerCultureScore = -attackerCultureCompat * HonorAllianceAttackerCultureWeight;
+
+ // Friendly cultures with ally = more support for honoring alliance
+ float allyCultureCompat = ReligionObjectHelper.CalculateCultureCompatibility(
+ ourKingdom.Culture?.StringId, attackedAlly.Culture?.StringId);
+ float allyCultureScore = allyCultureCompat * HonorAllianceAllyCultureWeight;
+
+ support += (attackerCultureScore + allyCultureScore) * honorModifier * calculatingInverseModifier;
+
+ return support;
+ }
+
+ ///
+ /// Checks if a kingdom can consider forming alliances at all.
+ /// Returns false for Chaos/Greenskins or if at max alliances.
+ ///
+ public bool CanKingdomConsiderAlliance(Kingdom kingdom)
+ {
+ if (kingdom == null)
+ return false;
+
+ var pantheon = DiplomacyHelpers.GetKingdomPantheon(kingdom);
+ if (pantheon == Pantheon.Chaos || pantheon == Pantheon.Greenskin)
+ return false;
+
+ if (kingdom.AlliedKingdoms.Count >= MaxNumberOfAlliances)
+ return false;
+
+ return true;
+ }
+
+ ///
+ /// Gets potential alliance partners for a kingdom.
+ /// Undead factions prioritize other Undead, others use distance-based selection.
+ /// Returns top scored candidates that pass all filters.
+ ///
+ public List GetPotentialAlliancePartners(Kingdom kingdom, int maxCandidates = 3)
+ {
+ if (!CanKingdomConsiderAlliance(kingdom))
+ return new List();
+
+ var myPantheon = DiplomacyHelpers.GetKingdomPantheon(kingdom);
+ var isUndead = myPantheon == Pantheon.Undead;
+
+ // Get candidate kingdoms based on faction type
+ List candidateKingdoms = GetCandidateKingdoms(kingdom, isUndead);
+
+ // Filter by alliance rules
+ var validPartners = candidateKingdoms
+ .Where(k => CanFormAlliance(kingdom, k))
+ .Where(k => !kingdom.IsAtWarWith(k))
+ .Where(k => !kingdom.IsAllyWith(k))
+ .Where(k => k.AlliedKingdoms.Count < MaxNumberOfAlliances)
+ .ToList();
+
+ if (!validPartners.Any())
+ return new List();
+
+ // Score and return top candidates
+ var scoredPartners = validPartners
+ .Select(k => new
+ {
+ Kingdom = k,
+ Score = GetScoreOfStartingAlliance(kingdom, k, kingdom.RulingClan, out _).ResultNumber
+ })
+ .Where(x => x.Score > 50)
+ .OrderByDescending(x => x.Score)
+ .TakeRandom(maxCandidates)
+ .Select(x => x.Kingdom)
+ .ToList();
+
+ return scoredPartners;
+ }
+
+ private List GetCandidateKingdoms(Kingdom kingdom, bool isUndead)
+ {
+ if (isUndead)
+ {
+ // Undead prioritize other Undead factions regardless of distance
+ var undeadKingdoms = Kingdom.All
+ .Where(k => k != kingdom && !k.IsEliminated)
+ .Where(k => DiplomacyHelpers.GetKingdomPantheon(k) == Pantheon.Undead)
+ .ToList();
+
+ if (undeadKingdoms.Any())
+ return undeadKingdoms;
+
+ // No other undead - use all kingdoms sorted by distance
+ return Kingdom.All
+ .Where(k => k != kingdom && !k.IsEliminated)
+ .OrderBy(k => DiplomacyHelpers.GetKingdomDistance(kingdom, k))
+ .ToList();
+ }
+
+ // Normal factions - take 5 closest
+ return Kingdom.All
+ .Where(k => k != kingdom && !k.IsEliminated)
+ .OrderBy(k => DiplomacyHelpers.GetKingdomDistance(kingdom, k))
+ .Take(5)
+ .ToList();
+ }
+ }
+}
diff --git a/CSharpSourceCode/Models/TORDiplomacyModel.cs b/CSharpSourceCode/Models/TORDiplomacyModel.cs
new file mode 100644
index 0000000..3e08134
--- /dev/null
+++ b/CSharpSourceCode/Models/TORDiplomacyModel.cs
@@ -0,0 +1,804 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using TaleWorlds.CampaignSystem;
+using TaleWorlds.CampaignSystem.GameComponents;
+using TaleWorlds.Core;
+using TaleWorlds.LinQuick;
+using TaleWorlds.Localization;
+using TaleWorlds.CampaignSystem.CharacterDevelopment;
+using TOR_Core.CampaignMechanics.Diplomacy;
+using TOR_Core.CampaignMechanics.Religion;
+using TOR_Core.CharacterDevelopment;
+using TOR_Core.Extensions;
+using TOR_Core.Utilities;
+
+namespace TOR_Core.Models
+{
+ public class TORDiplomacyModel : DefaultDiplomacyModel
+ {
+ // War declaration weights (scaled by 100 for DenarsToInfluence compatibility)
+ private const float WarCultureCompatibilityWeight = 5000f;
+
+ // Territorial Integrity weights (hierarchical claims)
+ private const float DirectClaimWeight = 5000f; // My own settlement
+ private const float CulturalClaimWeight = 2500f; // My culture's settlement held by foreigners
+ private const float PantheonClaimWeight = 1000f; // My pantheon's settlement held by other pantheons
+
+ // Lorewise Rivalry weights
+ private const float KarakReclamationWeight = 15000f; // Dwarfs vs Greenskins holding Karaks
+ private const float AntiChaosWeight = 12000f; // Good factions vs Chaos
+ private const float NordlandLaurelornRivalryWeight = 5000f; // Nordland vs Laurelorn (territorial dispute)
+ private const float DawiAsraiRivalryWeight = 8000f; // Dwarfs vs Wood Elves (War of the Beard grudge)
+
+ // Territorial distance factor settings
+ private const float TerritorialDistanceScaling = 100f; // Distance at which factor is ~0.5
+ private const float TerritorialMinDistanceFactor = 0.2f; // Minimum factor for very distant claims
+
+ // Alliance candidate weights
+ private const float AllianceReligionCompatibilityWeight = 100f;
+ private const float AllianceCultureCompatibilityWeight = 75f;
+ private const float AllianceStrengthWeight = 50f;
+ private const float AllianceDistancePenaltyFactor = 0.01f;
+ private const float AllianceMinimumScoreThreshold = 50f;
+
+ // Note: Trait modifiers and distance thresholds are now in DiplomacyHelpers
+
+ public override int GetInfluenceCostOfProposingPeace(Clan proposingClan) => 150;
+ public override int GetInfluenceCostOfProposingWar(Clan proposingClan) => 150;
+
+ public override float GetRelationIncreaseFactor(Hero hero1, Hero hero2, float relationChange)
+ {
+ var baseValue = base.GetRelationIncreaseFactor(hero1, hero2, relationChange);
+ var values = new ExplainedNumber(baseValue);
+
+ var playerHero = hero1.IsHumanPlayerCharacter || hero2.IsHumanPlayerCharacter ? (hero1.IsHumanPlayerCharacter ? hero1 : hero2) : null;
+ if (playerHero == null) return baseValue;
+
+ var conversationHero = !hero1.IsHumanPlayerCharacter || !hero2.IsHumanPlayerCharacter ? (!hero1.IsHumanPlayerCharacter ? hero1 : hero2) : null;
+ if (playerHero.HasAnyCareer())
+ {
+ var choices = playerHero.GetAllCareerChoices();
+
+ if (choices.Contains("CourtleyPassive1"))
+ {
+ if (baseValue > 0)
+ {
+ var choice = TORCareerChoices.GetChoice("CourtleyPassive1");
+ if (choice != null)
+ {
+ var value = choice.Passive.InterpretAsPercentage ? choice.Passive.EffectMagnitude / 100 : choice.Passive.EffectMagnitude;
+ values.AddFactor(value);
+ }
+ }
+ }
+
+ if (choices.Contains("JustCausePassive4"))
+ {
+ if (baseValue > 0)
+ {
+ if (conversationHero != null && conversationHero.Culture.StringId == TORConstants.Cultures.BRETONNIA)
+ {
+ var choice = TORCareerChoices.GetChoice("JustCausePassive4");
+ if (choice != null)
+ {
+ var value = choice.Passive.InterpretAsPercentage ? choice.Passive.EffectMagnitude / 100 : choice.Passive.EffectMagnitude;
+ values.AddFactor(value);
+ }
+ }
+ }
+ }
+ }
+ return values.ResultNumber;
+ }
+
+ public override float GetScoreOfDeclaringWar(IFaction factionDeclaresWar, IFaction factionDeclaredWar, Clan evaluatingClan, out TextObject reason, bool includeReason = false)
+ {
+ reason = new TextObject("It is time to declare war!");
+
+ if (factionDeclaresWar is Kingdom declaringKingdom && factionDeclaredWar is Kingdom targetKingdom)
+ {
+ // Use offensive war count (excludes alliance/defensive wars)
+ int offensiveWars = GetOffensiveWarCount(declaringKingdom);
+
+ // Maximum war limit - cannot declare more wars
+ if (offensiveWars >= TORConfig.NumMaxKingdomWars)
+ return -100000;
+
+ // War count is acceptable - combine base game score with TOR factors
+
+ // Add TOR custom scoring factors
+ float customScore = CalculateWarTargetScore(declaringKingdom, targetKingdom, evaluatingClan);
+
+ // Small random variance to prevent identical scores
+ customScore += MBRandom.RandomInt(-25, 25);
+
+ return customScore;
+ }
+
+ return base.GetScoreOfDeclaringWar(factionDeclaresWar, factionDeclaredWar, evaluatingClan, out reason, includeReason);
+ }
+
+ public override float GetScoreOfDeclaringPeace(IFaction factionDeclaresPeace, IFaction factionDeclaredPeace)
+ {
+ // Chaos really shouldn't be allowed to make peace
+ if (factionDeclaresPeace.Culture.StringId == TORConstants.Cultures.CHAOS || factionDeclaredPeace.Culture.StringId == TORConstants.Cultures.CHAOS)
+ {
+ return float.MinValue;
+ }
+
+ if (factionDeclaresPeace is Kingdom kingdom && factionDeclaredPeace is Kingdom enemyKingdom)
+ {
+ int offensiveWars = GetOffensiveWarCount(kingdom);
+ int totalWars = kingdom.GetWarCount();
+
+ // If under minimum wars, don't seek peace
+ if (totalWars <= TORConfig.NumMinKingdomWars) return -100000;
+
+ // If over maximum offensive wars, strongly favor peace
+ if (offensiveWars > TORConfig.NumMaxKingdomWars) return 100000;
+
+ // If we have alliance wars pushing us over the limit, prioritize peace with NON-alliance enemies
+ if (totalWars > TORConfig.NumMaxKingdomWars)
+ {
+ var allianceWarBehavior = Campaign.Current?.GetCampaignBehavior();
+ if (allianceWarBehavior != null)
+ {
+ bool isAllianceWar = allianceWarBehavior.IsAllianceWar(kingdom, enemyKingdom);
+ if (!isAllianceWar)
+ {
+ // Strongly favor peace with non-alliance enemies when stretched thin
+ return 50000;
+ }
+ // Less likely to abandon alliance wars
+ return -50000;
+ }
+ }
+ }
+
+ return base.GetScoreOfDeclaringPeace(factionDeclaresPeace, factionDeclaredPeace);
+ }
+
+ ///
+ /// Calculates TOR custom war scoring factors for a specific target kingdom.
+ /// Used both for target selection and for voting on war decisions.
+ ///
+ private float CalculateWarTargetScore(Kingdom declaringKingdom, Kingdom targetKingdom, Clan evaluatingClan)
+ {
+ // DISTANCE CHECK FIRST - prevent proxy wars against distant kingdoms
+ if (!DiplomacyHelpers.IsWithinWarDistance(declaringKingdom, targetKingdom))
+ return -200000f; // Too far - don't even consider this war
+
+ var leader = evaluatingClan?.Leader;
+
+ // Get trait modifiers for the evaluating clan's leader
+ float honorModifier = DiplomacyHelpers.GetTraitModifier(leader, DefaultTraits.Honor);
+ float generosityInverseModifier = DiplomacyHelpers.GetInverseTraitModifier(leader, DefaultTraits.Generosity);
+ float calculatingModifier = DiplomacyHelpers.GetTraitModifier(leader, DefaultTraits.Calculating);
+ float calculatingInverseModifier = DiplomacyHelpers.GetInverseTraitModifier(leader, DefaultTraits.Calculating);
+ float mercyModifier = DiplomacyHelpers.GetTraitModifier(leader, DefaultTraits.Mercy);
+ float valorModifier = DiplomacyHelpers.GetTraitModifier(leader, DefaultTraits.Valor);
+
+ // Traitorous behavior penalties (scaled by 100, modified by Honor)
+ // Dishonorable lords (-2 Honor) barely care about breaking agreements
+ float tradeAgreementPenalty = declaringKingdom.HasTradeAgreementWith(targetKingdom) ? -3000f * honorModifier : 0f;
+ float alliancePenalty = declaringKingdom.IsAllyWith(targetKingdom) ? -7000f * honorModifier : 0f;
+
+ // Individual scoring factors
+ // Riches: Greedy lords (low Generosity) care more, Calculating lords amplify awareness
+ float richesScore = CalculateRichesScore(declaringKingdom, targetKingdom) * generosityInverseModifier * calculatingModifier;
+ // Relations: Cruel lords (-2 Mercy) ignore personal relationships
+ float relationScore = CalculateRelationScore(evaluatingClan, targetKingdom) * mercyModifier;
+ float distanceScore = CalculateDistanceScore(declaringKingdom, targetKingdom);
+ // Religion: Pragmatists (high Calculating) and cruel lords (low Mercy) ignore religion
+ float religionScore = CalculateReligionScore(declaringKingdom, targetKingdom) * calculatingInverseModifier * mercyModifier;
+ // Culture: Honorable lords respect cultural bonds, Pragmatists ignore cultural sentiment
+ float cultureScore = CalculateCultureScore(declaringKingdom, targetKingdom) * honorModifier * calculatingInverseModifier;
+ // Territorial: Greedy lords (low Generosity) protect direct claims, Merciful lords liberate kin
+ float territorialScore = CalculateTerritorialIntegrityScore(declaringKingdom, targetKingdom, leader);
+ // Rivalry: Brave lords (+2 Valor) are more motivated by glory/grudges
+ float rivalryScore = CalculateLorewiseRivalryScore(declaringKingdom, targetKingdom) * valorModifier;
+ // Tactical: Cruel lords strike harder when strong, Brave/Merciful lords ignore unfavorable odds, Calculating amplifies
+ float baseTacticalScore = CalculateTacticalScore(declaringKingdom, targetKingdom);
+ float mercyInverseModifier = DiplomacyHelpers.GetInverseTraitModifier(leader, DefaultTraits.Mercy);
+ float adjustedTactical = baseTacticalScore >= 0
+ ? baseTacticalScore * valorModifier * mercyInverseModifier // Positive: brave + cruel hit harder
+ : baseTacticalScore / Math.Max(valorModifier * mercyModifier, 0.1f); // Negative: brave + merciful ignore bad odds
+ float tacticalScore = adjustedTactical * calculatingModifier;
+
+ float totalScore = tradeAgreementPenalty
+ + alliancePenalty
+ + richesScore
+ + relationScore
+ + distanceScore
+ + religionScore
+ + cultureScore
+ + territorialScore
+ + rivalryScore
+ + tacticalScore;
+
+ return totalScore;
+ }
+
+ ///
+ /// Calculates lorewise rivalry bonuses based on Warhammer lore.
+ ///
+ private float CalculateLorewiseRivalryScore(Kingdom declaringKingdom, Kingdom targetKingdom)
+ {
+ float score = 0f;
+
+ // 1. KARAK RECLAMATION - Dwarfs vs Greenskins holding Karaks
+ if (declaringKingdom.Culture?.StringId == TORConstants.Cultures.DAWI)
+ {
+ var targetPantheon = targetKingdom.Leader.GetDominantReligion().Pantheon;
+ if (targetPantheon == Pantheon.Greenskin)
+ {
+ // Count how many Karaks (dwarf settlements) the greenskins hold
+ int karakCount = 0;
+ foreach (var settlement in targetKingdom.Settlements)
+ {
+ if (settlement.Town == null) continue;
+
+
+ if(!settlement.IsDwarfKarak())continue;
+
+ karakCount++;
+ }
+ score += karakCount * KarakReclamationWeight;
+ }
+ }
+
+ // 2. ANTI-CHAOS - Good factions vs Chaos
+ var targetCulture = targetKingdom.Culture?.StringId;
+ if (targetCulture == TORConstants.Cultures.CHAOS)
+ {
+ var myPantheon = targetKingdom.Leader.GetDominantReligion().Pantheon;
+ // Good pantheons: Empire, Bretonnia, Dwarfs, Elves
+ if (myPantheon == Pantheon.Human ||
+ myPantheon == Pantheon.Dwarven ||
+ myPantheon == Pantheon.Elven)
+ {
+ // Count Chaos-held settlements (any settlement they hold is an affront)
+ int chaosSettlements = targetKingdom.Settlements.Count(s => s.Town != null);
+ score += chaosSettlements * AntiChaosWeight;
+ }
+ }
+
+ // 3. WAR OF THE BEARD - Dwarfs vs Wood Elves (ancient grudge)
+ var myCulture = declaringKingdom.Culture?.StringId;
+ if ((myCulture == TORConstants.Cultures.DAWI && targetCulture == TORConstants.Cultures.ASRAI) ||
+ (myCulture == TORConstants.Cultures.ASRAI && targetCulture == TORConstants.Cultures.DAWI))
+ {
+ score += DawiAsraiRivalryWeight;
+ }
+
+ switch (declaringKingdom.StringId)
+ {
+ // NORDLAND vs LAURELORN - Territorial forest dispute
+ case TORConstants.Factions.NORDLAND when
+ targetKingdom.StringId == TORConstants.Factions.LAURELORN:
+ case TORConstants.Factions.LAURELORN when
+ targetKingdom.StringId == TORConstants.Factions.NORDLAND:
+ score += NordlandLaurelornRivalryWeight;
+ break;
+
+ // Wissenland and Montfort rivalry
+ case TORConstants.Factions.WISSENLAND when
+ targetKingdom.StringId == TORConstants.Factions.MONTFORT:
+ case TORConstants.Factions.WISSENLAND when
+ targetKingdom.StringId == TORConstants.Factions.MONTFORT:
+ score += NordlandLaurelornRivalryWeight;
+ break;
+ }
+
+ return score;
+ }
+
+ ///
+ /// Calculates territorial integrity score based on claims.
+ /// - Direct claims: Settlements that rightfully belong to the declaring kingdom (modified by Generosity inverse)
+ /// - Cultural claims: Settlements of same culture held by foreign culture (modified by Mercy)
+ /// - Pantheon claims: Settlements of same pantheon held by different pantheon (modified by Mercy)
+ /// Distance factor applied: nearby claims worth more than distant ones.
+ ///
+ private float CalculateTerritorialIntegrityScore(Kingdom declaringKingdom, Kingdom targetKingdom, Hero leader = null)
+ {
+ // Trait modifiers for territorial claims
+ float generosityInverseMod = DiplomacyHelpers.GetInverseTraitModifier(leader, DefaultTraits.Generosity);
+ float mercyMod = DiplomacyHelpers.GetTraitModifier(leader, DefaultTraits.Mercy);
+ float directClaimScore = 0f;
+ float culturalClaimScore = 0f;
+ float pantheonClaimScore = 0f;
+
+ var myPantheon = declaringKingdom.Leader?.GetDominantReligion()?.Pantheon ?? Pantheon.Human;
+ var myMidSettlement = declaringKingdom.FactionMidSettlement;
+
+ foreach (var settlement in targetKingdom.Settlements)
+ {
+ // Skip villages - only towns and castles
+ if (settlement.Town == null)
+ continue;
+
+ string rightfulOwner = TORConstants.SettlementPrefixToFaction.GetRightfulOwner(settlement.StringId);
+ if (string.IsNullOrEmpty(rightfulOwner))
+ continue;
+
+ // Calculate distance factor - closer settlements get higher weight
+ float distanceFactor = CalculateSettlementDistanceFactor(myMidSettlement, settlement);
+
+ // Direct Claim - this settlement belongs to us
+ if (rightfulOwner == declaringKingdom.StringId)
+ {
+ directClaimScore += distanceFactor;
+ continue;
+ }
+
+ // Cultural Claim - settlement belongs to our culture but held by foreign culture
+ string rightfulCulture = TORConstants.SettlementPrefixToFaction.GetFactionCulture(rightfulOwner);
+ if (rightfulCulture != null &&
+ rightfulCulture == declaringKingdom.Culture?.StringId &&
+ targetKingdom.Culture?.StringId != rightfulCulture)
+ {
+ culturalClaimScore += distanceFactor;
+ continue;
+ }
+
+ // Pantheon Claim - settlement belongs to our pantheon but held by different pantheon
+ var rightfulPantheon = ReligionObjectHelper.GetPantheon(rightfulCulture);
+ var holderPantheon = targetKingdom.Leader?.GetDominantReligion()?.Pantheon ?? Pantheon.Human;
+ if (rightfulPantheon == myPantheon && holderPantheon != rightfulPantheon)
+ {
+ pantheonClaimScore += distanceFactor;
+ }
+ }
+
+ // Apply trait modifiers: Greedy lords care about direct claims, Merciful lords care about liberating kin
+ return directClaimScore * DirectClaimWeight * generosityInverseMod
+ + culturalClaimScore * CulturalClaimWeight * mercyMod
+ + pantheonClaimScore * PantheonClaimWeight * mercyMod;
+ }
+
+ // Resource value tiers for missing resource bonus
+ private static readonly Dictionary ResourceValues = new()
+ {
+ // Strategic (high value)
+ { "iron_mine", 1500f },
+ { "silver_mine", 1500f },
+ // Valuable
+ { "salt_mine", 800f },
+ { "europe_horse_ranch", 600f },
+ { "steppe_horse_ranch", 600f },
+ { "desert_horse_ranch", 600f },
+ { "battanian_horse_ranch", 600f },
+ { "sturgian_horse_ranch", 600f },
+ { "vlandian_horse_ranch", 600f },
+ // Useful materials
+ { "lumberjack", 400f },
+ { "clay_mine", 300f },
+ { "flax_plant", 300f },
+ // Luxury
+ { "silk_plant", 500f },
+ { "trapper", 400f },
+ { "vineyard", 300f },
+ // Food (common but still valuable)
+ { "wheat_farm", 200f },
+ { "fisherman", 200f },
+ { "cattle_farm", 200f },
+ { "sheep_farm", 200f },
+ { "swine_farm", 200f },
+ { "date_farm", 150f },
+ { "olive_trees", 150f },
+ };
+
+ ///
+ /// Calculates score based on target kingdom's overall wealth.
+ /// Considers: village hearth, town prosperity, and missing resources.
+ ///
+ private float CalculateRichesScore(Kingdom declaringKingdom, Kingdom targetKingdom)
+ {
+ if (targetKingdom.Settlements == null || !targetKingdom.Settlements.Any())
+ return 0f;
+
+ float totalHearth = 0f;
+ float totalProsperity = 0f;
+ HashSet ourResources = new();
+ HashSet theirResources = new();
+
+ // Gather our kingdom's resources
+ foreach (var settlement in declaringKingdom.Settlements)
+ {
+ if (settlement.IsVillage && settlement.Village?.VillageType != null)
+ ourResources.Add(settlement.Village.VillageType.StringId);
+ }
+
+ // Gather target kingdom's wealth and resources
+ foreach (var settlement in targetKingdom.Settlements)
+ {
+ if (settlement.IsVillage && settlement.Village != null)
+ {
+ totalHearth += settlement.Village.Hearth;
+ if (settlement.Village.VillageType != null)
+ theirResources.Add(settlement.Village.VillageType.StringId);
+ }
+ else if (settlement.IsTown && settlement.Town != null)
+ {
+ totalProsperity += settlement.Town.Prosperity;
+ }
+ }
+
+ // Calculate missing resource bonus (resources they have that we don't)
+ float missingResourceScore = 0f;
+ foreach (var resource in theirResources)
+ {
+ if (!ourResources.Contains(resource))
+ {
+ missingResourceScore += ResourceValues.GetValueOrDefault(resource, 100f);
+ }
+ }
+
+ // Weight components (scaled for war scoring)
+ // Hearth: ~200-800 per village, total could be 2000-8000 for a kingdom
+ float hearthScore = totalHearth * 0.5f; // 1000-4000 range
+ // Prosperity: ~3000-6000 per town, total could be 10000-30000
+ float prosperityScore = totalProsperity * 0.1f; // 1000-3000 range
+ // Missing resources: 0-5000+ depending on what they have
+
+ float totalScore = hearthScore + prosperityScore + missingResourceScore;
+
+ // Competition factor: reduce if others are already at war with target
+ int competitorCount = targetKingdom.GetWarCount();
+ float competitionFactor = Math.Max(0.2f, 1f - competitorCount * 0.25f);
+
+ return totalScore * competitionFactor;
+ }
+
+ ///
+ /// Calculates score based on evaluating clan's relations with target kingdom's lords.
+ /// Returns: -3000 (friendly relations) to +3000 (hostile relations) (scaled by 100)
+ ///
+ private float CalculateRelationScore(Clan evaluatingClan, Kingdom targetKingdom)
+ {
+ if (evaluatingClan?.Leader == null || targetKingdom == null)
+ return 0f;
+
+ var clanLeader = evaluatingClan.Leader;
+ float totalRelation = 0f;
+ int lordCount = 0;
+
+ foreach (var targetClan in targetKingdom.Clans)
+ {
+ if (targetClan.Leader != null && targetClan.Leader.IsAlive)
+ {
+ totalRelation += clanLeader.GetRelation(targetClan.Leader);
+ lordCount++;
+ }
+ }
+
+ if (lordCount == 0)
+ return 1000f; // No lords = neutral
+
+ float averageRelation = totalRelation / lordCount;
+
+ // Map: +80 -> -3000, +-25 -> +1000, -80 -> +3000 (scaled by 100)
+ // Higher relation = less likely to declare war
+ if (averageRelation >= 80)
+ return -3000f;
+ if (averageRelation >= 25)
+ return -1500f;
+ if (averageRelation >= -25)
+ return 1000f;
+ if (averageRelation >= -80)
+ return 2000f;
+ return 3000f;
+ }
+
+ ///
+ /// Calculates score based on distance between kingdoms.
+ /// Close targets get bonus, distant targets get penalty.
+ /// Neutral point at ~500 distance. (scaled by 100)
+ ///
+ private float CalculateDistanceScore(Kingdom declaringKingdom, Kingdom targetKingdom)
+ {
+ float distance = DiplomacyHelpers.GetKingdomDistance(declaringKingdom, targetKingdom);
+
+ if (distance >= 300)
+ return -5000f; // No valid distance = strong penalty
+
+ // Neutral distance where score is 0
+ const float neutralDistance = 500f;
+ // Scale factor for how much distance affects score (scaled by 100)
+ const float distanceScaleFactor = 5f;
+
+ // Below neutral: positive, above neutral: negative
+ // At 0 distance: +2500, at 500: 0, at 1000: -2500, at 1500: -5000
+ float distanceScore = (neutralDistance - distance) * distanceScaleFactor;
+
+ // Clamp to reasonable range
+ return Math.Max(-5000f, Math.Min(2500f, distanceScore));
+ }
+
+ ///
+ /// Calculates score based on religious compatibility.
+ /// Hostile religions are more attractive targets.
+ ///
+ private float CalculateReligionScore(Kingdom declaringKingdom, Kingdom targetKingdom)
+ {
+ float religionCompatibility = DiplomacyHelpers.GetReligionCompatibility(declaringKingdom, targetKingdom);
+ // Negative compatibility = more likely to declare war (scaled by 100)
+ return -religionCompatibility * TORConfig.DeclareWarScoreReligiousEffectMultiplier * 100f;
+ }
+
+ ///
+ /// Calculates score based on cultural compatibility.
+ /// Hostile cultures are more attractive targets.
+ ///
+ private float CalculateCultureScore(Kingdom declaringKingdom, Kingdom targetKingdom)
+ {
+ float cultureCompat = DiplomacyHelpers.GetCultureCompatibility(declaringKingdom, targetKingdom);
+ // Negative compatibility = more likely to declare war
+ return -cultureCompat * WarCultureCompatibilityWeight;
+ }
+
+ ///
+ /// Calculates tactical score based on war situations of both kingdoms. (scaled by 100)
+ /// - Strength comparison: weaker targets are more attractive
+ /// - Target already fighting wars = opportunity (they're stretched thin)
+ /// - We already fighting wars = penalty (we're stretched thin)
+ ///
+ private float CalculateTacticalScore(Kingdom declaringKingdom, Kingdom targetKingdom)
+ {
+ float score = 0f;
+
+ // Strength comparison - weaker kingdoms are more attractive targets
+ // Uses alliance strength to account for defensive pacts
+ float ourStrength = declaringKingdom.GetAllianceTotalStrength();
+ float theirStrength = Math.Max(targetKingdom.GetAllianceTotalStrength(), 1f);
+ float strengthRatio = ourStrength / theirStrength;
+ // ratio > 1 = we're stronger (bonus), ratio < 1 = they're stronger (penalty)
+ // Scaled by 100 for DenarsToInfluence compatibility
+ score += strengthRatio * TORConfig.DeclareWarScoreFactionStrengthMultiplier * 100f;
+
+ // Target's current enemy count - opportunity to strike while they're distracted
+ // Bonus for each enemy, but plateau after 2 (scaled by 100)
+ // 0 enemies: 0, 1 enemy: +1500, 2+ enemies: +3000 (cap)
+ int targetEnemyCount = targetKingdom.GetWarCount();
+ score += Math.Min(targetEnemyCount * 1500f, 3000f);
+
+ // Our current war count - penalty for overextension (scaled by 100)
+ // Each offensive war we're fighting reduces appetite for new wars
+ int ourOffensiveWars = GetOffensiveWarCount(declaringKingdom);
+ // 0 wars: +2000 bonus (eager), 1 war: 0, 2 wars: -2000, 3+ wars: -4000 (cap)
+ score += (1 - ourOffensiveWars) * 2000f;
+ score = Math.Max(score, -4000f); // Floor at -4000
+
+ return score;
+ }
+
+ ///
+ /// Gets the number of offensive wars (excluding alliance/defensive wars).
+ ///
+ private int GetOffensiveWarCount(Kingdom kingdom)
+ {
+ var allianceWarBehavior = Campaign.Current?.GetCampaignBehavior();
+ if (allianceWarBehavior != null)
+ {
+ return allianceWarBehavior.GetOffensiveWarCount(kingdom);
+ }
+ // Fallback to total wars if behavior not available
+ return kingdom.GetWarCount();
+ }
+
+ public override float GetScoreOfMercenaryToJoinKingdom(Clan mercenaryClan, Kingdom kingdom)
+ {
+ var score = base.GetScoreOfMercenaryToJoinKingdom(mercenaryClan, kingdom);
+
+ if (kingdom == null || mercenaryClan == null) return score;
+
+ if (kingdom.Culture.StringId == TORConstants.Cultures.BRETONNIA)
+ {
+ if (mercenaryClan.Culture.StringId == TORConstants.Cultures.BRETONNIA)
+ {
+ score = +1000;
+ }
+ else
+ {
+ score = -10000;
+ }
+ }
+
+ if (kingdom.Culture.StringId != TORConstants.Cultures.BRETONNIA && mercenaryClan.Culture.StringId == TORConstants.Cultures.BRETONNIA)
+ {
+ score = -10000;
+ }
+
+ if (mercenaryClan.StringId == "tor_dog_clan_hero_curse" && (kingdom.Culture.StringId == TORConstants.Cultures.SYLVANIA || kingdom.Culture.StringId == TORConstants.Cultures.MOUSILLON || kingdom.Culture.StringId == TORConstants.Cultures.BRETONNIA))
+ {
+ score = -10000;
+ }
+
+ if (mercenaryClan.Culture.StringId == TORConstants.Cultures.DRUCHII)
+ {
+ score = -10000;
+ }
+
+ return score;
+ }
+
+ ///
+ /// Calculates the best war target candidate for a kingdom.
+ ///
+ /// BASE GAME (DefaultDiplomacyModel) considerations:
+ /// - War declaration permission checks (not at war, not recently made peace, etc.)
+ /// - Clan influences and kingdom decision mechanics
+ ///
+ /// TOR ADDITIONS - War Target Scoring Factors (higher score = more likely target):
+ /// 1. DISTANCE: Closer kingdoms score higher (configurable multiplier)
+ /// 2. STRENGTH: Weaker kingdoms score higher (our strength / their strength × multiplier)
+ /// 3. RELIGION: Hostile religions score higher (negative compatibility × multiplier)
+ /// 4. CULTURE: Hostile cultures score higher (negative compatibility × 50)
+ /// 5. TERRITORIAL INTEGRITY (Hierarchical claims):
+ /// - Direct Claims (+200 per settlement): Our faction's rightful settlements
+ /// - Cultural Claims (+80 per settlement): Our culture's settlements held by foreigners
+ /// - Pantheon Claims (+30 per settlement): Our pantheon's settlements held by other faiths
+ ///
+ public Kingdom GetWarDeclarationTargetCandidate(Kingdom consideringKingdom)
+ {
+ if (consideringKingdom == null) return null;
+
+ var permissionModel = Campaign.Current?.Models?.KingdomDecisionPermissionModel;
+ if (permissionModel == null) return null;
+
+ var kingdomCandidates = Kingdom.All.WhereQ(x =>
+ !x.IsEliminated &&
+ x != consideringKingdom &&
+ permissionModel.IsWarDecisionAllowedBetweenKingdoms(consideringKingdom, x, out _) &&
+ !consideringKingdom.IsAtWarWith(x) &&
+ (x.GetStanceWith(consideringKingdom)?.PeaceDeclarationDate == null ||
+ x.GetStanceWith(consideringKingdom)?.PeaceDeclarationDate.ElapsedDaysUntilNow > TORConfig.MinPeaceDays)).ToListQ();
+
+ if (kingdomCandidates.Count > 0)
+ {
+ // Calculate scores for all candidates using shared scoring method
+ Dictionary candidateScores = [];
+
+ foreach (var targetKingdom in kingdomCandidates)
+ {
+ candidateScores[targetKingdom] = CalculateWarTargetScore(consideringKingdom, targetKingdom, consideringKingdom.RulingClan);
+ }
+
+ var candidate = candidateScores.OrderByDescending(x => x.Value).Take(3).GetRandomElementInefficiently().Key;
+ return candidate;
+ }
+ return null;
+ }
+
+ ///
+ /// Calculates the best peace target candidate for a kingdom.
+ ///
+ public Kingdom GetPeaceDeclarationTargetCandidate(Kingdom consideringKingdom, bool isEmergency = false)
+ {
+ if (consideringKingdom == null) return null;
+
+ var permissionModel = Campaign.Current?.Models?.KingdomDecisionPermissionModel;
+ if (permissionModel == null) return null;
+
+ var kingdomCandidates = Kingdom.All.WhereQ(x =>
+ !x.IsEliminated &&
+ x != consideringKingdom &&
+ permissionModel.IsPeaceDecisionAllowedBetweenKingdoms(consideringKingdom, x, out _) &&
+ consideringKingdom.IsAtWarWith(x) &&
+ (x.GetStanceWith(consideringKingdom)?.WarStartDate.ElapsedDaysUntilNow > TORConfig.MinWarDays ||
+ isEmergency)).ToListQ();
+
+ if (kingdomCandidates.Count > 0)
+ {
+ var kingdomListByStrength = kingdomCandidates.SelectQ(x =>
+ new Tuple(x, x.GetAllianceTotalStrength())).ToListQ();
+
+ Dictionary candidateScores = [];
+
+ foreach (var tuple in kingdomListByStrength)
+ {
+ // Prefer making peace with stronger enemies
+ candidateScores[tuple.Item1] = tuple.Item1.GetAllianceTotalStrength() / consideringKingdom.GetAllianceTotalStrength();
+ }
+
+ var maxvalue = candidateScores.Values.Max();
+ if (maxvalue > 1)
+ {
+ var candidate = candidateScores.MaxBy(x => x.Value).Key;
+ return candidate;
+ }
+ }
+ return null;
+ }
+
+ ///
+ /// Calculates the best alliance target candidate for a kingdom.
+ ///
+ public Kingdom GetAllianceDeclarationTargetCandidate(Kingdom consideringKingdom)
+ {
+ if (consideringKingdom == null) return null;
+
+ var permissionModel = Campaign.Current?.Models?.KingdomDecisionPermissionModel;
+ if (permissionModel == null) return null;
+
+ var kingdomCandidates = Kingdom.All.WhereQ(x =>
+ !x.IsEliminated &&
+ x != consideringKingdom &&
+ !consideringKingdom.IsAtWarWith(x) &&
+ !consideringKingdom.IsAllyWith(x) &&
+ permissionModel.IsStartAllianceDecisionAllowedBetweenKingdoms(consideringKingdom, x, out _)).ToListQ();
+
+ if (kingdomCandidates.Count > 0)
+ {
+ Dictionary candidateScores = [];
+
+ foreach (var candidate in kingdomCandidates)
+ {
+ float score = 0;
+
+ // Religious similarity bonus
+ var religionScore = ReligionObjectHelper.CalculateReligionCompatibility(
+ candidate.Leader.GetDominantReligion(),
+ consideringKingdom.Leader.GetDominantReligion());
+ score += religionScore * AllianceReligionCompatibilityWeight;
+
+ // Cultural compatibility bonus
+ float cultureCompat = ReligionObjectHelper.CalculateCultureCompatibility(
+ consideringKingdom.Culture?.StringId, candidate.Culture?.StringId);
+ score += cultureCompat * AllianceCultureCompatibilityWeight;
+
+ // Strength consideration - prefer allying with stronger kingdoms when threatened
+ var totalEnemyStrength = consideringKingdom.GetTotalEnemyAllianceStrength();
+ if (totalEnemyStrength > consideringKingdom.CurrentTotalStrength)
+ {
+ score += candidate.CurrentTotalStrength / consideringKingdom.CurrentTotalStrength * AllianceStrengthWeight;
+ }
+
+ // Distance consideration - prefer nearby allies
+ float distance = DiplomacyHelpers.GetKingdomDistance(consideringKingdom, candidate);
+ score -= distance * AllianceDistancePenaltyFactor;
+
+ candidateScores[candidate] = score;
+ }
+
+ if (candidateScores.Any())
+ {
+ var bestCandidate = candidateScores.MaxBy(x => x.Value);
+ if (bestCandidate.Value > AllianceMinimumScoreThreshold)
+ {
+ return bestCandidate.Key;
+ }
+ }
+ }
+ return null;
+ }
+
+ // Note: Trade agreements were removed in 1.3. Alliances are now handled by IAllianceCampaignBehavior.
+ // Note: GetKingdomDistance moved to DiplomacyHelpers
+
+ ///
+ /// Calculates a distance factor for territorial claims.
+ /// Returns 1.0 for nearby settlements, decreasing to TerritorialMinDistanceFactor for distant ones.
+ /// Uses inverse formula: factor = 1 / (1 + distance / scaling)
+ ///
+ private float CalculateSettlementDistanceFactor(TaleWorlds.CampaignSystem.Settlements.Settlement fromSettlement, TaleWorlds.CampaignSystem.Settlements.Settlement toSettlement)
+ {
+ if (fromSettlement == null || toSettlement == null)
+ return 0;
+
+ float distance = fromSettlement.Position.Distance(toSettlement.Position);
+
+ // Inverse decay formula: closer = higher factor
+ // At distance 0: factor = 1.0
+ // At distance = TerritorialDistanceScaling (500): factor ≈ 0.5
+ // At very large distances: approaches TerritorialMinDistanceFactor
+ float rawFactor = 1f / (1f + distance / TerritorialDistanceScaling);
+
+ // Scale to range [TerritorialMinDistanceFactor, 1.0]
+ return TerritorialMinDistanceFactor + rawFactor * (1f - TerritorialMinDistanceFactor);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/CSharpSourceCode/Models/TORKingdomDecisionPermissionModel.cs b/CSharpSourceCode/Models/TORKingdomDecisionPermissionModel.cs
index 05e3d02..010009b 100644
--- a/CSharpSourceCode/Models/TORKingdomDecisionPermissionModel.cs
+++ b/CSharpSourceCode/Models/TORKingdomDecisionPermissionModel.cs
@@ -38,6 +38,14 @@ public override bool IsPeaceDecisionAllowedBetweenKingdoms(
public override bool IsStartAllianceDecisionAllowedBetweenKingdoms(Kingdom kingdom1, Kingdom kingdom2, out TextObject reason)
{
+ // Chaos cannot form alliances
+ if (kingdom1.Culture.StringId == TORConstants.Cultures.CHAOS || kingdom2.Culture.StringId == TORConstants.Cultures.CHAOS)
+ {
+ reason = TORTextHelper.GetTextObject("TOR_Alliance_Chaos_Forbidden", "The forces of Chaos cannot form alliances.");
+ return false;
+ }
+
+ // Check for hostile religions
if ((bool)(kingdom1?.Leader?.GetDominantReligion()?.HostileReligions?.Contains(kingdom2?.Leader?.GetDominantReligion())))
{
reason = TORTextHelper.GetTextObject("TOR_Alliance_Religion_Conflict", "The dominant religions of the two kingdoms are hostile towards each other.");
diff --git a/CSharpSourceCode/Models/TORTradeAgreementModel.cs b/CSharpSourceCode/Models/TORTradeAgreementModel.cs
new file mode 100644
index 0000000..d418f74
--- /dev/null
+++ b/CSharpSourceCode/Models/TORTradeAgreementModel.cs
@@ -0,0 +1,466 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using TaleWorlds.CampaignSystem;
+using TaleWorlds.CampaignSystem.CharacterDevelopment;
+using TaleWorlds.CampaignSystem.GameComponents;
+using TaleWorlds.Core;
+using TaleWorlds.Library;
+using TaleWorlds.Localization;
+using TOR_Core.CampaignMechanics.Religion;
+using TOR_Core.Extensions;
+using TOR_Core.Utilities;
+
+namespace TOR_Core.Models
+{
+ ///
+ /// TOR Trade Agreement Model - Determines AI willingness to form trade agreements.
+ /// Trade agreements provide a diplomatic penalty when declaring war on trade partners.
+ ///
+ public class TORTradeAgreementModel : DefaultTradeAgreementModel
+ {
+ // Culture scoring
+ private const float SameCultureBonus = 15f;
+ private const float CultureCompatibilityWeight = 15f;
+
+ // Religion scoring
+ private const float SameReligionBonus = 25f;
+ private const float HostileReligionPenalty = -30f;
+ private const float PantheonCompatibilityWeight = 10f;
+ private const float ReligionCompatibilityWeight = 15f;
+
+ // Personality trait weights
+ private const float CalculatingTraitWeight = 10f;
+ private const float GenerosityTraitWeight = 8f;
+ private const float HonorTraitWeight = 6f;
+
+ // Economic benefit weights
+ private const float ComplementaryResourceWeight = 3f; // Per unique resource they have that we lack
+ private const float FoodScarcityBonusWeight = 10f; // Bonus if we lack food and they have it
+ private const float ProsperityDifferenceWeight = 0.002f; // Slight bonus for trading with prosperous kingdoms
+
+ // Faction-specific bonuses
+ private const float EonirTradeBonus = 10f;
+
+ private const float WastelandBonus = 20f;
+
+ private const float MontfortBonus = 20f;
+
+ // Kingdom relations
+ private const float KingdomRelationWeight = 0.2f; // -100 to +100 relation = -20 to +20 score
+
+ // Resource categories for trade value
+ private static readonly HashSet FoodResources = new()
+ {
+ "grain_farm", "cattle_farm", "sheep_farm", "swine_farm",
+ "fisherman", "date_farm", "olive_trees"
+ };
+
+ private static readonly HashSet LuxuryResources = new()
+ {
+ "silver_mine", "vineyard", "silk", "silkworm_farm", "fur_trader"
+ };
+
+ private static readonly HashSet MilitaryResources = new()
+ {
+ "iron_mine", "europe_horse_ranch", "steppe_horse_ranch",
+ "desert_horse_ranch", "lumberjack"
+ };
+
+ // Note: Distance thresholds are in DiplomacyHelpers (MaxTradeDistance = 600)
+
+ public override int GetMaximumTradeAgreementCount(Kingdom kingdom) => 3;
+
+ ///
+ /// Calculates TOR custom trade agreement scoring factors.
+ /// Used for AI decision making on trade agreements.
+ ///
+ public override float GetScoreOfStartingTradeAgreement(
+ Kingdom kingdom,
+ Kingdom targetKingdom,
+ Clan clan,
+ out TextObject explanation,
+ bool includeExplanation = false)
+ {
+ float baseScore = base.GetScoreOfStartingTradeAgreement(
+ kingdom, targetKingdom, clan, out explanation, includeExplanation);
+
+ if (baseScore <= 0)
+ return 0f;
+
+ // DISTANCE CHECK FIRST - prevent trade with distant kingdoms
+ if (!DiplomacyHelpers.IsWithinTradeDistance(kingdom, targetKingdom))
+ return 0f; // Too far - don't even consider this trade agreement
+
+ var leader = clan?.Leader;
+
+ // Get trait modifiers for the evaluating clan's leader
+ float calculatingModifier = DiplomacyHelpers.GetTraitModifier(leader, DefaultTraits.Calculating);
+ float generosityModifier = DiplomacyHelpers.GetTraitModifier(leader, DefaultTraits.Generosity);
+ float honorModifier = DiplomacyHelpers.GetTraitModifier(leader, DefaultTraits.Honor);
+ float mercyModifier = DiplomacyHelpers.GetTraitModifier(leader, DefaultTraits.Mercy);
+
+ // Individual scoring factors
+ float distanceScore = CalculateDistanceScore(kingdom, targetKingdom);
+ float cultureScore = CalculateCultureScore(kingdom, targetKingdom);
+ float religionScore = CalculateReligionScore(kingdom, targetKingdom);
+ float loreConsiderations = CalculateLoreConsiderations(kingdom, targetKingdom);
+ // Economic: What trade benefits can we gain? (affected by Calculating trait)
+ float economicScore = CalculateEconomicBenefitScore(kingdom, targetKingdom) * calculatingModifier;
+ // War alternative: Calculating lords consider if war would be more profitable
+ float warAlternativePenalty = CalculateWarAlternativePenalty(kingdom, targetKingdom, calculatingModifier);
+
+ // Personality scoring
+ float calculatingScore = CalculateCalculatingScore(calculatingModifier);
+ float generosityScore = CalculateGenerosityScore(generosityModifier);
+ float honorScore = CalculateHonorScore(kingdom, targetKingdom, honorModifier);
+
+ // Kingdom relations - average of all clan leaders' relations
+ float kingdomRelation = DiplomacyHelpers.CalculateKingdomToKingdomRelation(kingdom, targetKingdom);
+ float relationScore = kingdomRelation * KingdomRelationWeight * mercyModifier;
+
+ float totalScore = baseScore
+ + distanceScore
+ + cultureScore
+ + religionScore
+ + loreConsiderations
+ + economicScore
+ + warAlternativePenalty
+ + calculatingScore
+ + generosityScore
+ + honorScore
+ + relationScore;
+
+ return MBMath.ClampFloat(totalScore, 0f, 100f);
+ }
+
+ ///
+ /// Calculates distance score for trade agreements.
+ /// Closer kingdoms are better trade partners (shorter routes).
+ /// Returns: 0 (close) to -60 (far but within range)
+ ///
+ private float CalculateDistanceScore(Kingdom kingdom, Kingdom targetKingdom)
+ {
+ float distance = DiplomacyHelpers.GetKingdomDistance(kingdom, targetKingdom);
+
+ // No penalty for close kingdoms (below 300)
+ if (distance <= 300f)
+ return 0f;
+
+ // Gradual penalty: -0.2 per unit beyond 300
+ return (300f - distance) * 0.2f;
+ }
+
+ ///
+ /// Calculates culture compatibility score for trade.
+ /// Same culture or compatible cultures trade more willingly.
+ /// Returns: -15 to +15
+ ///
+ private float CalculateCultureScore(Kingdom kingdom, Kingdom targetKingdom)
+ {
+ if (DiplomacyHelpers.AreSameCulture(kingdom, targetKingdom))
+ return SameCultureBonus;
+
+ float compatibility = DiplomacyHelpers.GetCultureCompatibility(kingdom, targetKingdom);
+ return compatibility * CultureCompatibilityWeight;
+ }
+
+ ///
+ /// Calculates religion compatibility score for trade.
+ /// Same religion or compatible pantheons increase trust.
+ /// Hostile religions severely penalize trade willingness.
+ /// Returns: -30 to +25
+ ///
+ private float CalculateReligionScore(Kingdom kingdom, Kingdom targetKingdom)
+ {
+ // Same religion - strong bonus
+ if (DiplomacyHelpers.AreSameReligion(kingdom, targetKingdom))
+ return SameReligionBonus;
+
+ // Hostile religions - major penalty
+ if (DiplomacyHelpers.AreReligionsHostile(kingdom, targetKingdom))
+ return HostileReligionPenalty;
+
+ // Otherwise use compatibility
+ float pantheonCompat = DiplomacyHelpers.GetPantheonCompatibility(kingdom, targetKingdom);
+ float religionCompat = DiplomacyHelpers.GetReligionCompatibility(kingdom, targetKingdom);
+
+ return pantheonCompat * PantheonCompatibilityWeight + religionCompat * ReligionCompatibilityWeight;
+ }
+
+ ///
+ /// Calculates faction-specific trade bonuses.
+ /// Some factions are naturally more inclined to trade.
+ ///
+ private float CalculateLoreConsiderations(Kingdom kingdom, Kingdom targetKingdom)
+ {
+ // Eonir are natural merchants
+ if (kingdom.Culture?.StringId == TORConstants.Cultures.EONIR)
+ return EonirTradeBonus;
+
+ // Marienburg is the biggest harbor in world - there are goods that nobody can aquire
+ if (targetKingdom.StringId == TORConstants.Factions.WASTELAND)
+ return WastelandBonus;
+
+ // Montfort like to trade with humans
+ if (kingdom.StringId == TORConstants.Factions.MONTFORT && DiplomacyHelpers.GetKingdomPantheon(targetKingdom) == Pantheon.Human)
+ return MontfortBonus;
+
+ return 0f;
+ }
+
+ ///
+ /// Calculates economic benefit of trading with target kingdom.
+ /// Considers: complementary resources, food scarcity, prosperity.
+ /// Returns: 0 to ~30 (higher = more beneficial trade partner)
+ ///
+ private float CalculateEconomicBenefitScore(Kingdom kingdom, Kingdom targetKingdom)
+ {
+ if (kingdom.Settlements == null || targetKingdom.Settlements == null)
+ return 0f;
+
+ // Gather our resources
+ HashSet ourResources = new();
+ bool weHaveFood = false;
+ float ourProsperity = 0f;
+
+ foreach (var settlement in kingdom.Settlements)
+ {
+ if (settlement.IsVillage && settlement.Village?.VillageType != null)
+ {
+ string resourceId = settlement.Village.VillageType.StringId;
+ ourResources.Add(resourceId);
+ if (FoodResources.Contains(resourceId))
+ weHaveFood = true;
+ }
+ else if (settlement.IsTown && settlement.Town != null)
+ {
+ ourProsperity += settlement.Town.Prosperity;
+ }
+ }
+
+ // Gather their resources
+ HashSet theirResources = new();
+ bool theyHaveFood = false;
+ float theirProsperity = 0f;
+
+ foreach (var settlement in targetKingdom.Settlements)
+ {
+ if (settlement.IsVillage && settlement.Village?.VillageType != null)
+ {
+ string resourceId = settlement.Village.VillageType.StringId;
+ theirResources.Add(resourceId);
+ if (FoodResources.Contains(resourceId))
+ theyHaveFood = true;
+ }
+ else if (settlement.IsTown && settlement.Town != null)
+ {
+ theirProsperity += settlement.Town.Prosperity;
+ }
+ }
+
+ float score = 0f;
+
+ // Complementary resources - resources they have that we lack
+ int complementaryCount = 0;
+ foreach (var resource in theirResources)
+ {
+ if (!ourResources.Contains(resource))
+ {
+ complementaryCount++;
+ // Bonus for luxury and military resources
+ if (LuxuryResources.Contains(resource) || MilitaryResources.Contains(resource))
+ complementaryCount++; // Double count valuable resources
+ }
+ }
+ score += complementaryCount * ComplementaryResourceWeight;
+
+ // Food scarcity bonus - if we lack food and they have it
+ if (!weHaveFood && theyHaveFood)
+ score += FoodScarcityBonusWeight;
+
+ // Prosperity difference - slight bonus for trading with prosperous kingdoms
+ if (theirProsperity > ourProsperity)
+ {
+ float prosperityDiff = theirProsperity - ourProsperity;
+ score += prosperityDiff * ProsperityDifferenceWeight;
+ }
+
+ return score;
+ }
+
+ ///
+ /// Calculating lords consider if war would be more profitable than trade.
+ /// Uses actual war scoring to determine if conquest is preferable.
+ /// Returns: 0 (war not attractive) to -30 (war much better option)
+ ///
+ private float CalculateWarAlternativePenalty(Kingdom kingdom, Kingdom targetKingdom, float calculatingModifier)
+ {
+ // Only calculating lords think this way
+ if (calculatingModifier <= 1f)
+ return 0f;
+
+ // Already at war - trade not relevant
+ if (kingdom.IsAtWarWith(targetKingdom))
+ return 0f;
+
+ // Get the actual war score using the diplomacy model
+ var diplomacyModel = Campaign.Current?.Models?.DiplomacyModel as TORDiplomacyModel;
+ if (diplomacyModel == null)
+ return 0f;
+
+ float warScore = diplomacyModel.GetScoreOfDeclaringWar(kingdom, targetKingdom, kingdom.RulingClan, out _);
+
+ // If war score is negative or low, trade is the better option
+ if (warScore <= 0f)
+ return 0f;
+
+ // War looks attractive - penalty scales with how good war looks
+ // Normalize war score (typically ranges from 0 to ~50000 for very attractive wars)
+ float normalizedWarScore = Math.Min(warScore / 10000f, 3f); // Cap at 3x multiplier
+
+ // Scale by how calculating the lord is
+ float penalty = -normalizedWarScore * 10f * (calculatingModifier - 1f);
+
+ return Math.Max(penalty, -30f); // Cap at -30
+ }
+
+ ///
+ /// Calculating lords value trade for strategic economic benefit.
+ /// Returns: -7.5 to +7.5
+ ///
+ private float CalculateCalculatingScore(float calculatingModifier)
+ {
+ return (calculatingModifier - 1f) * CalculatingTraitWeight;
+ }
+
+ ///
+ /// Generous lords are more open to mutual partnerships.
+ /// Returns: -6 to +6
+ ///
+ private float CalculateGenerosityScore(float generosityModifier)
+ {
+ return (generosityModifier - 1f) * GenerosityTraitWeight;
+ }
+
+ ///
+ /// Honorable lords prefer trading with same background (culture/religion).
+ /// Only applies if kingdoms share culture or religion.
+ /// Returns: 0, or -4.5 to +4.5
+ ///
+ private float CalculateHonorScore(Kingdom kingdom, Kingdom targetKingdom, float honorModifier)
+ {
+ bool sameBackground = DiplomacyHelpers.AreSameCulture(kingdom, targetKingdom) ||
+ DiplomacyHelpers.AreSameReligion(kingdom, targetKingdom);
+
+ if (!sameBackground)
+ return 0f;
+
+ return (honorModifier - 1f) * HonorTraitWeight;
+ }
+
+ private static TextObject _chaosCannotTradeText => TORTextHelper.GetTextObject("tor_trade_chaos", "The forces of Chaos do not engage in trade.");
+ private static TextObject _greenskinCannotTradeText => TORTextHelper.GetTextObject("tor_trade_greenskin", "Greenskins do not understand the concept of trade.");
+
+ ///
+ /// Checks if two kingdoms can form a trade agreement.
+ /// Lore restrictions: Chaos and Greenskins cannot trade.
+ ///
+ public override bool CanMakeTradeAgreement(
+ Kingdom kingdom,
+ Kingdom other,
+ bool checkOtherSideTradeSupport,
+ out TextObject reason,
+ bool includeReason = false)
+ {
+ reason = includeReason ? TextObject.GetEmpty() : null;
+
+ // Check lore restrictions for both kingdoms
+ var ourPantheon = DiplomacyHelpers.GetKingdomPantheon(kingdom);
+ var theirPantheon = DiplomacyHelpers.GetKingdomPantheon(other);
+
+ if (ourPantheon == Pantheon.Chaos || theirPantheon == Pantheon.Chaos)
+ {
+ reason = _chaosCannotTradeText;
+ return false;
+ }
+
+ if (ourPantheon == Pantheon.Greenskin || theirPantheon == Pantheon.Greenskin)
+ {
+ reason = _greenskinCannotTradeText;
+ return false;
+ }
+
+ return base.CanMakeTradeAgreement(kingdom, other, checkOtherSideTradeSupport, out reason, includeReason);
+ }
+
+ ///
+ /// Gets potential trade partners for a kingdom.
+ /// Undead factions prioritize other Undead, others use distance-based selection.
+ /// Returns top scored candidates that pass all filters.
+ ///
+ public List GetPotentialTradePartners(Kingdom kingdom, int maxCandidates = 3)
+ {
+ if (kingdom == null)
+ return new List();
+
+ var myPantheon = DiplomacyHelpers.GetKingdomPantheon(kingdom);
+ var isUndead = myPantheon == Pantheon.Undead;
+
+ // Get candidate kingdoms based on faction type
+ List candidateKingdoms = GetCandidateKingdoms(kingdom, isUndead);
+
+ // Filter by trade rules
+ var validPartners = candidateKingdoms
+ .Where(k => CanMakeTradeAgreement(kingdom, k, true, out _))
+ .ToList();
+
+ if (!validPartners.Any())
+ return new List();
+
+ // Score and return top candidates
+ var scoredPartners = validPartners
+ .Select(k => new
+ {
+ Kingdom = k,
+ Score = GetScoreOfStartingTradeAgreement(kingdom, k, kingdom.RulingClan, out _)
+ })
+ .Where(x => x.Score > 0)
+ .OrderByDescending(x => x.Score)
+ .Take(maxCandidates)
+ .Select(x => x.Kingdom)
+ .ToList();
+
+ return scoredPartners;
+ }
+
+ private List GetCandidateKingdoms(Kingdom kingdom, bool isUndead)
+ {
+ if (isUndead)
+ {
+ // Undead prioritize other Undead factions regardless of distance
+ var undeadKingdoms = Kingdom.All
+ .Where(k => k != kingdom && !k.IsEliminated)
+ .Where(k => DiplomacyHelpers.GetKingdomPantheon(k) == Pantheon.Undead)
+ .ToList();
+
+ if (undeadKingdoms.Any())
+ return undeadKingdoms;
+
+ // No other undead - use 10 closest kingdoms
+ return Kingdom.All
+ .Where(k => k != kingdom && !k.IsEliminated)
+ .OrderBy(k => DiplomacyHelpers.GetKingdomDistance(kingdom, k))
+ .Take(10)
+ .ToList();
+ }
+
+ // Normal factions - take 5 closest
+ return Kingdom.All
+ .Where(k => k != kingdom && !k.IsEliminated)
+ .OrderBy(k => DiplomacyHelpers.GetKingdomDistance(kingdom, k))
+ .Take(5)
+ .ToList();
+ }
+ }
+}
diff --git a/CSharpSourceCode/SubModule.cs b/CSharpSourceCode/SubModule.cs
index 79f6b65..6398239 100644
--- a/CSharpSourceCode/SubModule.cs
+++ b/CSharpSourceCode/SubModule.cs
@@ -172,6 +172,8 @@ protected override void InitializeGameStarter(Game game, IGameStarter starterObj
starter.AddBehavior(new TORFactionDiscontinuationCampaignBehavior());
starter.AddBehavior(new ServeAsAHirelingCampaignBehavior());
starter.AddBehavior(new TORStartupBehavior());
+ starter.AddBehavior(new TORKingdomDecisionsCampaignBehavior());
+ starter.AddBehavior(new TORAllianceWarBehavior());
starter.AddBehavior(new TORArtisanDistrictCampaignBehavior());
starter.AddBehavior(new PriestBehavior());
starter.AddBehavior(new SkillTrainerBehavior());
@@ -240,6 +242,9 @@ protected override void OnGameStart(Game game, IGameStarter gameStarterObject)
gameStarterObject.AddModel(new TORRaidModel());
gameStarterObject.AddModel(new TORBattleBannerBearersModel());
gameStarterObject.AddModel(new TORKingdomDecisionPermissionModel());
+ gameStarterObject.AddModel(new TORDiplomacyModel());
+ gameStarterObject.AddModel(new TORAllianceModel());
+ gameStarterObject.AddModel(new TORTradeAgreementModel());
gameStarterObject.AddModel(new TORSettlementLoyaltyModel());
gameStarterObject.AddModel(new TORBattleRewardModel());
gameStarterObject.AddModel(new TORTroopSupplierModel());
diff --git a/CSharpSourceCode/TOR_Core.csproj b/CSharpSourceCode/TOR_Core.csproj
index e633a13..009b14d 100644
--- a/CSharpSourceCode/TOR_Core.csproj
+++ b/CSharpSourceCode/TOR_Core.csproj
@@ -583,6 +583,9 @@
+
+
+
@@ -710,6 +713,7 @@
+
@@ -787,6 +791,10 @@
+
+
+
+
@@ -850,6 +858,7 @@
+
diff --git a/CSharpSourceCode/Utilities/TORConsoleCommands.cs b/CSharpSourceCode/Utilities/TORConsoleCommands.cs
index 765fc20..a5ca026 100644
--- a/CSharpSourceCode/Utilities/TORConsoleCommands.cs
+++ b/CSharpSourceCode/Utilities/TORConsoleCommands.cs
@@ -5,6 +5,7 @@
using System.Linq;
using TaleWorlds.CampaignSystem;
using TaleWorlds.CampaignSystem.Actions;
+using TaleWorlds.CampaignSystem.CampaignBehaviors;
using TaleWorlds.CampaignSystem.Party;
using TaleWorlds.Core;
using TaleWorlds.Library;
@@ -139,39 +140,132 @@ public static string ShowCompanionPosition(List arguments)
return result;
}
- [CommandLineFunctionality.CommandLineArgumentFunction("declare_peace", "tor")]
- public static string DeclarePeace(List strings)
+ [CommandLineFunctionality.CommandLineArgumentFunction("set_alliance", "tor")]
+ public static string SetAlliance(List strings)
{
if (!CampaignCheats.CheckCheatUsage(ref CampaignCheats.ErrorType))
return CampaignCheats.ErrorType;
- string str1 = "campaign.declare_peace [Faction1] | [Faction2]";
+
+ string usage = "tor.set_alliance [Kingdom1] | [Kingdom2]\nCreates an alliance between two kingdoms. Use kingdom StringIds (e.g., 'empire', 'bretonnia').";
+
+ if (CampaignCheats.CheckParameters(strings, 0) || CampaignCheats.CheckParameters(strings, 1) || CampaignCheats.CheckHelp(strings))
+ return usage;
+
+ List separatedNames = CampaignCheats.GetSeparatedNames(strings, true);
+ if (separatedNames.Count != 2)
+ return usage;
+
+ string kingdom_str1 = separatedNames[0].ToLower().Replace(" ", "");
+ string kingdom_str2 = separatedNames[1].ToLower().Replace(" ", "");
+
+ Kingdom faction1 = null;
+ Kingdom faction2 = null;
+
+ foreach (var kingdom in Campaign.Current.Kingdoms)
+ {
+ if (kingdom_str1 == kingdom.StringId.ToLower())
+ {
+ faction1 = kingdom;
+ }
+ if (kingdom_str2 == kingdom.StringId.ToLower())
+ {
+ faction2 = kingdom;
+ }
+ }
+
+ if (faction1 == null)
+ return "Kingdom not found: " + kingdom_str1 + "\n" + usage;
+
+ if (faction2 == null)
+ return "Kingdom not found: " + kingdom_str2 + "\n" + usage;
+
+ if (faction1 == faction2)
+ return "Cannot create alliance with self.\n" + usage;
+
+ if (faction1.IsAtWarWith(faction2))
+ return "Cannot create alliance - kingdoms are at war. Use tor.declare_peace first.\n";
+
+ if (faction1.IsAllyWith(faction2))
+ return faction1.Name + " and " + faction2.Name + " are already allies.\n";
+
+ // Use the clean alliance method that removes native Call to War decisions
+ // This is preferred for TOR since we use our own HonorAllianceDecision system
+ faction1.SetAlliance(faction2);
+
+ return "Alliance created between " + faction1.Name + " and " + faction2.Name + ".\n";
+ }
+
+ [CommandLineFunctionality.CommandLineArgumentFunction("break_alliance", "tor")]
+ public static string BreakAlliance(List strings)
+ {
+ if (!CampaignCheats.CheckCheatUsage(ref CampaignCheats.ErrorType))
+ return CampaignCheats.ErrorType;
+
+ string usage = "tor.break_alliance [Kingdom1] | [Kingdom2]\nBreaks an alliance between two kingdoms.";
+
if (CampaignCheats.CheckParameters(strings, 0) || CampaignCheats.CheckParameters(strings, 1) || CampaignCheats.CheckHelp(strings))
- return str1;
+ return usage;
+
List separatedNames = CampaignCheats.GetSeparatedNames(strings, true);
if (separatedNames.Count != 2)
- return str1;
+ return usage;
+
string kingdom_str1 = separatedNames[0].ToLower().Replace(" ", "");
string kingdom_str2 = separatedNames[1].ToLower().Replace(" ", "");
+
Kingdom faction1 = null;
Kingdom faction2 = null;
+
foreach (var kingdom in Campaign.Current.Kingdoms)
{
- if (kingdom_str1 == kingdom.StringId)
+ if (kingdom_str1 == kingdom.StringId.ToLower())
{
faction1 = kingdom;
}
- if (kingdom_str2 == kingdom.StringId)
+ if (kingdom_str2 == kingdom.StringId.ToLower())
{
faction2 = kingdom;
}
}
- if (faction1 != null && faction2 != null)
+ if (faction1 == null)
+ return "Kingdom not found: " + kingdom_str1 + "\n" + usage;
+
+ if (faction2 == null)
+ return "Kingdom not found: " + kingdom_str2 + "\n" + usage;
+
+ if (!faction1.IsAllyWith(faction2))
+ return faction1.Name + " and " + faction2.Name + " are not allies.\n";
+
+ // Use the IAllianceCampaignBehavior to break the alliance
+ var allianceBehavior = Campaign.Current.GetCampaignBehavior();
+ if (allianceBehavior != null)
{
- MakePeaceAction.Apply(faction1, faction2);
- return "Peace declared between " + (object)faction1.Name + " and " + (object)faction2.Name;
+ allianceBehavior.EndAlliance(faction1, faction2);
+ return "Alliance broken between " + faction1.Name + " and " + faction2.Name + ".\n";
}
- return faction1 == null ? "Faction is not found: " + kingdom_str1 + "\n" + str1 : "Faction is not found: " + kingdom_str2;
+
+ return "Error: Could not find alliance behavior.\n";
+ }
+
+ [CommandLineFunctionality.CommandLineArgumentFunction("list_kingdoms", "tor")]
+ public static string ListKingdoms(List strings)
+ {
+ if (!CampaignCheats.CheckCheatUsage(ref CampaignCheats.ErrorType))
+ return CampaignCheats.ErrorType;
+
+ if (CampaignCheats.CheckHelp(strings))
+ return "Lists all kingdoms with their StringIds for use with alliance/war commands.";
+
+ string result = "Available Kingdoms:\n";
+ foreach (var kingdom in Campaign.Current.Kingdoms.OrderBy(k => k.StringId))
+ {
+ string allies = kingdom.AlliedKingdoms.Count > 0
+ ? " [Allies: " + string.Join(", ", kingdom.AlliedKingdoms.Select(a => a.StringId)) + "]"
+ : "";
+ result += $" {kingdom.StringId} - {kingdom.Name}{allies}\n";
+ }
+ return result;
}
[CommandLineFunctionality.CommandLineArgumentFunction("add_enchantment_blueprint", "tor")]
diff --git a/CSharpSourceCode/Utilities/TORConstants.cs b/CSharpSourceCode/Utilities/TORConstants.cs
index 813eb2d..f72c957 100644
--- a/CSharpSourceCode/Utilities/TORConstants.cs
+++ b/CSharpSourceCode/Utilities/TORConstants.cs
@@ -1,4 +1,6 @@
using System.Collections.Generic;
+using TaleWorlds.CampaignSystem;
+using TaleWorlds.CampaignSystem.Settlements;
namespace TOR_Core.Utilities
{
@@ -52,5 +54,308 @@ public readonly struct Cultures
];
}
+ public readonly struct Factions
+ {
+ // Empire Provinces
+ public const string REIKLAND = "reikland";
+ public const string MIDDENLAND = "middenland";
+ public const string OSTLAND = "ostland";
+ public const string OSTERMARK = "ostermark";
+ public const string STIRLAND = "stirland";
+ public const string HOCHLAND = "hochland";
+ public const string AVERLAND = "averland";
+ public const string WISSENLAND = "wissenland";
+ public const string TALABECLAND = "talabecland";
+ public const string NORDLAND = "nordland";
+ public const string MOOT = "moot";
+
+ // Bretonnia Duchies
+ public const string COURONNE = "couronne";
+ public const string AQUITAINE = "aquitaine";
+ public const string ARTOIS = "artois";
+ public const string BORDELEAUX = "bordeleaux";
+ public const string GISOREUX = "gisoreux";
+ public const string MONTFORT = "montfort";
+ public const string PARRAVON = "parravon";
+ public const string QUENELLES = "quenelles";
+ public const string CARCASSONNE = "carcassonne";
+ public const string BASTONNE = "bastonne";
+ public const string BRIONNE = "brionne";
+ public const string ANGUILLE = "anguille";
+ public const string LYONESSE = "lyonesse";
+
+ // Vampire Counts
+ public const string SYLVANIA = "sylvania";
+ public const string MOUSILLON = "mousillon";
+ public const string NECRACHS = "necrachs";
+ public const string BLOODDRAGONS = "blooddragons";
+
+ // Dwarf Holds
+ public const string KARAK_KADRIN = "karak_kadrin";
+ public const string KARAK_NORN = "karak_norn";
+ public const string KARAK_HIRN = "karak_hirn";
+ public const string KARAK_IZOR = "karak_izor";
+ public const string KARAK_AZGARAZ = "karak_azgaraz";
+ public const string KARAK_KAFERKAMMAZ = "karak_kaferkammaz";
+ public const string KARAK_ZIFLIN = "karak_ziflin";
+ public const string KARAK_ZHUFBAR = "karak_zhufbar";
+ public const string KARAK_GANTUK = "karak_gantuk";
+ public const string KARAK_EKSFILAZ = "karak_eksfilaz";
+ public const string KARAK_ANGAZHAR = "karak_angazhar";
+
+ // Elf Kingdoms
+ public const string ATHEL_LOREN = "athel_loren";
+ public const string LAURELORN = "laurelorn";
+
+ // Greenskin Tribes
+ public const string BAD_AXES = "bad_axes";
+ public const string BLACK_PIT = "black_pit";
+ public const string BLACK_SUNZ = "black_sunz";
+ public const string BLOODY_SPEARZ = "bloody_spearz";
+ public const string BRASSKEEP = "brasskeep";
+ public const string CROOKED_EYE = "crooked_eye";
+ public const string DEFF_GRINDAZ = "deff_grindaz";
+ public const string IRON_TRIBE = "iron_tribe";
+ public const string MASSIF_CHOPPAS = "massif_choppas";
+ public const string NECK_SNAPPERS = "neck_snappers";
+ public const string RED_EYE = "red_eye";
+ public const string SKULL_SMASHERZ = "skull_smasherz";
+
+ // Other
+ public const string REAVAZ = "reavaz";
+ public const string WASTELAND = "wasteland";
+
+ public static readonly List AllEmpire =
+ [
+ REIKLAND, MIDDENLAND, OSTLAND, OSTERMARK, STIRLAND, HOCHLAND,
+ AVERLAND, WISSENLAND, TALABECLAND, NORDLAND, MOOT
+ ];
+
+ public static readonly List AllBretonnia =
+ [
+ COURONNE, AQUITAINE, ARTOIS, BORDELEAUX, GISOREUX, MONTFORT,
+ PARRAVON, QUENELLES, CARCASSONNE, BASTONNE, BRIONNE, ANGUILLE, LYONESSE
+ ];
+
+ public static readonly List AllVampire =
+ [
+ SYLVANIA, MOUSILLON, NECRACHS, BLOODDRAGONS
+ ];
+
+ public static readonly List AllDwarfs =
+ [
+ KARAK_KADRIN, KARAK_NORN, KARAK_HIRN, KARAK_IZOR, KARAK_AZGARAZ,
+ KARAK_KAFERKAMMAZ, KARAK_ZIFLIN, KARAK_ZHUFBAR, KARAK_GANTUK,
+ KARAK_EKSFILAZ, KARAK_ANGAZHAR
+ ];
+
+ public static readonly List AllElves =
+ [
+ ATHEL_LOREN, LAURELORN
+ ];
+
+ public static readonly List AllGreenskins =
+ [
+ BAD_AXES, BLACK_PIT, BLACK_SUNZ, BLOODY_SPEARZ, BRASSKEEP,
+ CROOKED_EYE, DEFF_GRINDAZ, IRON_TRIBE, MASSIF_CHOPPAS,
+ NECK_SNAPPERS, RED_EYE, SKULL_SMASHERZ
+ ];
+
+ public static readonly List All =
+ [
+ // Empire
+ REIKLAND, MIDDENLAND, OSTLAND, OSTERMARK, STIRLAND, HOCHLAND,
+ AVERLAND, WISSENLAND, TALABECLAND, NORDLAND, MOOT, WASTELAND,
+ // Bretonnia
+ COURONNE, AQUITAINE, ARTOIS, BORDELEAUX, GISOREUX, MONTFORT,
+ PARRAVON, QUENELLES, CARCASSONNE, BASTONNE, BRIONNE, ANGUILLE, LYONESSE,
+ // Vampires
+ SYLVANIA, MOUSILLON, NECRACHS, BLOODDRAGONS,
+ // Dwarfs
+ KARAK_KADRIN, KARAK_NORN, KARAK_HIRN, KARAK_IZOR, KARAK_AZGARAZ,
+ KARAK_KAFERKAMMAZ, KARAK_ZIFLIN, KARAK_ZHUFBAR, KARAK_GANTUK,
+ KARAK_EKSFILAZ, KARAK_ANGAZHAR,
+ // Elves
+ ATHEL_LOREN, LAURELORN,
+ // Greenskins
+ BAD_AXES, BLACK_PIT, BLACK_SUNZ, BLOODY_SPEARZ, BRASSKEEP,
+ CROOKED_EYE, DEFF_GRINDAZ, IRON_TRIBE, MASSIF_CHOPPAS,
+ NECK_SNAPPERS, RED_EYE, SKULL_SMASHERZ, REAVAZ
+
+ ];
+ }
+
+ ///
+ /// Maps settlement prefix codes (e.g., "RL", "ST", "AV") to their rightful faction StringIds.
+ /// Used for territorial integrity calculations in war scoring.
+ ///
+ public static class SettlementPrefixToFaction
+ {
+ private static readonly Dictionary _prefixMap = new()
+ {
+ // Empire Provinces
+ { "RL", Factions.REIKLAND },
+ { "ML", Factions.MIDDENLAND },
+ { "OL", Factions.OSTLAND },
+ { "OM", Factions.OSTERMARK },
+ { "ST", Factions.STIRLAND },
+ { "HL", Factions.HOCHLAND },
+ { "AV", Factions.AVERLAND },
+ { "WI", Factions.WISSENLAND },
+ { "TB", Factions.TALABECLAND },
+ { "NL", Factions.NORDLAND },
+ { "MT", Factions.MOOT },
+
+ // Bretonnia Duchies
+ { "CO", Factions.COURONNE },
+ { "AQ", Factions.AQUITAINE },
+ { "AS", Factions.ARTOIS },
+ { "BL", Factions.BORDELEAUX },
+ { "GX", Factions.GISOREUX },
+ { "MO", Factions.MONTFORT },
+ { "PA", Factions.PARRAVON },
+ { "QU", Factions.QUENELLES },
+ { "CC", Factions.CARCASSONNE },
+ { "BA", Factions.BASTONNE },
+ { "BE", Factions.BRIONNE },
+ { "LA", Factions.ANGUILLE },
+ { "LY", Factions.LYONESSE },
+
+ // Vampire Counts
+ { "SY", Factions.SYLVANIA },
+ { "MS", Factions.MOUSILLON },
+ // Note: Necrachs and Blooddragons share "BK" prefix with Brasskeep
+ // BK1 = Blooddragons, BK2 = Brasskeep - cannot map cleanly
+
+ // Dwarf Holds
+ { "KK", Factions.KARAK_KADRIN },
+ { "NO", Factions.KARAK_NORN },
+ { "KH", Factions.KARAK_HIRN },
+ { "KI", Factions.KARAK_IZOR },
+ { "AZ", Factions.KARAK_AZGARAZ },
+ { "KF", Factions.KARAK_KAFERKAMMAZ },
+ { "ZI", Factions.KARAK_ZIFLIN },
+ { "ZH", Factions.KARAK_ZHUFBAR },
+ { "KG", Factions.KARAK_GANTUK },
+ { "EZ", Factions.KARAK_EKSFILAZ },
+ { "AN", Factions.KARAK_ANGAZHAR },
+
+ // Elf Kingdoms
+ { "AL", Factions.ATHEL_LOREN },
+ { "LL", Factions.LAURELORN },
+
+ // Greenskin Tribes
+ { "BX", Factions.BAD_AXES },
+ { "BP", Factions.BLACK_PIT },
+ { "BZ", Factions.BLACK_SUNZ },
+ { "BS", Factions.BLOODY_SPEARZ },
+ { "BK", Factions.BRASSKEEP },
+ { "CE", Factions.CROOKED_EYE },
+ { "DG", Factions.DEFF_GRINDAZ },
+ { "IT", Factions.IRON_TRIBE },
+ { "MC", Factions.MASSIF_CHOPPAS },
+ { "NS", Factions.NECK_SNAPPERS },
+ { "RE", Factions.RED_EYE },
+ { "SM", Factions.SKULL_SMASHERZ },
+
+ // Other Greenskin
+ { "RZ", Factions.REAVAZ },
+
+ // Other
+ { "WA", Factions.WASTELAND },
+ };
+
+ ///
+ /// Gets the faction StringId that a settlement with the given prefix belongs to.
+ ///
+ /// The settlement ID (e.g., "town_RL1", "castle_ST2")
+ /// The faction StringId that historically owns this settlement, or null if not found
+ public static string GetRightfulOwner(string settlementId)
+ {
+ if (string.IsNullOrEmpty(settlementId))
+ return null;
+
+ // Extract the prefix (two letters after "town_" or "castle_")
+ // Format: town_XX# or castle_XX#
+ string prefix = null;
+
+ if (settlementId.StartsWith("town_") && settlementId.Length >= 7)
+ {
+ prefix = settlementId.Substring(5, 2).ToUpper();
+ }
+ else if (settlementId.StartsWith("castle_") && settlementId.Length >= 9)
+ {
+ prefix = settlementId.Substring(7, 2).ToUpper();
+ }
+
+ if (prefix != null && _prefixMap.TryGetValue(prefix, out string factionId))
+ {
+ return factionId;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Checks if a settlement belongs originally to a specific faction based on its prefix.
+ ///
+ public static bool SettlementBelongsOriginallyToFaction(Settlement settlement, Kingdom faction)
+ {
+ var id = "";
+ var kingdom="";
+ if (settlement != null)
+ {
+ id = settlement.StringId;
+ }
+ var rightfulOwner = GetRightfulOwner(id);
+ return rightfulOwner != null && rightfulOwner == faction.StringId;
+ }
+
+ ///
+ /// Gets the culture StringId for a faction StringId.
+ ///
+ public static string GetFactionCulture(string factionId)
+ {
+ if (string.IsNullOrEmpty(factionId))
+ return null;
+
+ // Empire provinces
+ if (Factions.AllEmpire.Contains(factionId))
+ return Cultures.EMPIRE;
+
+ // Bretonnia duchies
+ if (Factions.AllBretonnia.Contains(factionId))
+ return Cultures.BRETONNIA;
+
+ // Vampire Counts - mixed cultures
+ if (factionId == Factions.SYLVANIA)
+ return Cultures.SYLVANIA;
+ if (factionId == Factions.MOUSILLON)
+ return Cultures.MOUSILLON;
+ if (factionId == Factions.NECRACHS || factionId == Factions.BLOODDRAGONS)
+ return Cultures.SYLVANIA; // Generic vampire culture
+
+ // Dwarf Holds
+ if (Factions.AllDwarfs.Contains(factionId))
+ return Cultures.DAWI;
+
+ // Elf Kingdoms
+ if (factionId == Factions.ATHEL_LOREN)
+ return Cultures.ASRAI;
+ if (factionId == Factions.LAURELORN)
+ return Cultures.EONIR;
+
+ // Greenskin Tribes
+ if (Factions.AllGreenskins.Contains(factionId) || factionId == Factions.REAVAZ)
+ return Cultures.GREENSKIN;
+
+ // Wasteland
+ if (factionId == Factions.WASTELAND)
+ return Cultures.EMPIRE;
+
+ return null;
+ }
+ }
+
}
}
\ No newline at end of file
diff --git a/CSharpSourceCode/Utilities/TORNotificationHelper.cs b/CSharpSourceCode/Utilities/TORNotificationHelper.cs
new file mode 100644
index 0000000..a9002fa
--- /dev/null
+++ b/CSharpSourceCode/Utilities/TORNotificationHelper.cs
@@ -0,0 +1,83 @@
+using System.Linq;
+using TaleWorlds.CampaignSystem;
+using TaleWorlds.CampaignSystem.Party;
+using TOR_Core.Extensions;
+using TOR_Core.Models;
+
+namespace TOR_Core.Utilities
+{
+ public static class TORNotificationHelper
+ {
+ private const float MaxRelevantDistance = 75f;
+
+ public static bool IsPartyRelevantToPlayer(PartyBase partyBase)
+ {
+ if (partyBase != null && PartyBase.MainParty != null)
+ {
+ var distance = partyBase.Position.Distance(PartyBase.MainParty.Position);
+ if (distance <= MaxRelevantDistance)
+ return true;
+ }
+ return false;
+ }
+
+ public static bool IsHeroRelevantToPlayer(Hero hero)
+ {
+ if (hero == null) return false;
+ if (hero == Hero.MainHero) return true;
+ if (hero.Clan == Clan.PlayerClan) return true;
+
+ if (hero.Clan?.Kingdom != null)
+ {
+ if (hero.Clan.Kingdom == Clan.PlayerClan?.Kingdom) return true;
+ if (IsKingdomRelevantToPlayer(hero.Clan.Kingdom)) return true;
+ }
+
+ if (hero.PartyBelongedTo != null)
+ {
+ if (IsPartyRelevantToPlayer(hero.PartyBelongedTo.Party))
+ return true;
+ }
+ return false;
+ }
+
+ public static bool IsKingdomRelevantToPlayer(Kingdom kingdom)
+ {
+ if (kingdom == null) return false;
+
+ var playerKingdom = Clan.PlayerClan?.Kingdom;
+ if (playerKingdom == null) return false;
+ if (kingdom == playerKingdom) return true;
+
+ var distance = DiplomacyHelpers.GetKingdomDistance(kingdom, playerKingdom);
+ if (distance <= MaxRelevantDistance)
+ return true;
+
+ if (playerKingdom.IsAtWarWith(kingdom)) return true;
+ if (playerKingdom.IsAllyWith(kingdom)) return true;
+ if (playerKingdom.HasTradeAgreementWith(kingdom)) return true;
+
+ return false;
+ }
+
+ public static bool AreKingdomsRelevantToPlayer(params Kingdom[] kingdoms)
+ {
+ return kingdoms.Any(IsKingdomRelevantToPlayer);
+ }
+
+ public static bool IsFactionRelevantToPlayer(IFaction faction)
+ {
+ if (faction == null) return false;
+ if (faction == Clan.PlayerClan) return true;
+ if (faction == Clan.PlayerClan?.Kingdom) return true;
+
+ if (faction is Kingdom kingdom)
+ return IsKingdomRelevantToPlayer(kingdom);
+
+ if (faction is Clan clan)
+ return clan.Kingdom != null && IsKingdomRelevantToPlayer(clan.Kingdom);
+
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ModuleData/tor_custom_xmls/tor_config.xml b/ModuleData/tor_custom_xmls/tor_config.xml
index cfe2169..bf5ab41 100644
--- a/ModuleData/tor_custom_xmls/tor_config.xml
+++ b/ModuleData/tor_custom_xmls/tor_config.xml
@@ -8,16 +8,16 @@
NumberOfMaximumLooterPartiesEarly="100"
NumberOfMaximumLooterParties="200"
NumberOfMaximumLooterPartiesLate="300"
- NumberOfMaximumBanditPartiesAroundEachHideout="8"
+ NumberOfMaximumBanditPartiesAroundEachHideout="4"
NumberOfMaximumBanditPartiesInEachHideout="2"
- NumberOfInitialHideoutsAtEachBanditFaction="10"
- NumberOfMaximumHideoutsAtEachBanditFaction="30"
+ NumberOfInitialHideoutsAtEachBanditFaction="5"
+ NumberOfMaximumHideoutsAtEachBanditFaction="20"
MaximumNumberOfCareerPerkPoints="30"
DeclareWarScoreDistanceMultiplier="50"
DeclareWarScoreFactionStrengthMultiplier="0.5"
DeclareWarScoreReligiousEffectMultiplier="5"
NumMinKingdomWars="0"
- NumMaxKingdomWars="3"
+ NumMaxKingdomWars="5"
MinPeaceDays="20"
MinWarDays="40"
AIGoldAdjustmentAmount="20000"
diff --git a/ModuleData/tor_religions.xml b/ModuleData/tor_religions.xml
index 8348360..78d9258 100644
--- a/ModuleData/tor_religions.xml
+++ b/ModuleData/tor_religions.xml
@@ -1,7 +1,7 @@
-
+
@@ -29,7 +29,7 @@
-
+
@@ -60,7 +60,7 @@
-
+
@@ -88,7 +88,7 @@
-
+
@@ -112,7 +112,7 @@
-
+
@@ -138,7 +138,7 @@
-
+
@@ -164,7 +164,7 @@
-
+
@@ -189,7 +189,7 @@
-
+
@@ -218,7 +218,7 @@
-
+
@@ -242,7 +242,7 @@
-
+
@@ -264,7 +264,7 @@
-
+
@@ -288,7 +288,7 @@
-
+
@@ -310,7 +310,7 @@
-
+
@@ -335,7 +335,7 @@
-
+
@@ -351,7 +351,7 @@
-
+
@@ -370,7 +370,7 @@
-
+
@@ -387,7 +387,7 @@
-
+
@@ -408,7 +408,7 @@
-
+
@@ -424,7 +424,7 @@
-
+
@@ -441,7 +441,7 @@
-
+
@@ -458,7 +458,7 @@
-
+
@@ -476,7 +476,7 @@
-
+
@@ -506,7 +506,7 @@
-
+
diff --git a/ModuleData/tor_strings.xml b/ModuleData/tor_strings.xml
index f7e0cf2..aab88c2 100644
--- a/ModuleData/tor_strings.xml
+++ b/ModuleData/tor_strings.xml
@@ -5051,4 +5051,22 @@ If a Rune ability is available to cast, the next Rune cast in 5 seconds shall be
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bin/Win64_Shipping_Client/TOR_Core.dll b/bin/Win64_Shipping_Client/TOR_Core.dll
index 5cd89d1..60cd8a7 100644
Binary files a/bin/Win64_Shipping_Client/TOR_Core.dll and b/bin/Win64_Shipping_Client/TOR_Core.dll differ
diff --git a/bin/Win64_Shipping_wEditor/TOR_Core.dll b/bin/Win64_Shipping_wEditor/TOR_Core.dll
index 5cd89d1..60cd8a7 100644
Binary files a/bin/Win64_Shipping_wEditor/TOR_Core.dll and b/bin/Win64_Shipping_wEditor/TOR_Core.dll differ