Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
d809b59
baseline - working diplomacy. still work to do. performance is bad, d…
Z3rca Jan 16, 2026
06637b3
reduce bandit counts for performance improvements
Z3rca Jan 17, 2026
f42b1c6
alliance decision
Z3rca Jan 17, 2026
b796a50
working honor alliance diplomacy
Z3rca Jan 17, 2026
1de555b
wip culture considerations
Z3rca Jan 17, 2026
5a1c0d7
Refactor religion system: Replace Affinity with Pantheon
Z3rca Jan 17, 2026
b53de2c
dll
Z3rca Jan 17, 2026
131c2a2
Trade Agreement AI behavior
Z3rca Jan 17, 2026
f38f2c6
Alliance agreements
Z3rca Jan 18, 2026
d24e269
some cleanup
Z3rca Jan 18, 2026
23b6b35
some streamlining and refactor
Z3rca Jan 18, 2026
b0eb9e6
more fixes. also added new Kingdom/faction constants
Z3rca Jan 18, 2026
5445dd1
wip war declaration model (broken?)
Z3rca Jan 18, 2026
0473504
WIP war declaration model
Z3rca Jan 19, 2026
ae2ce63
personality benefits and wip vilages
Z3rca Jan 19, 2026
452e35c
distance fix
Z3rca Jan 19, 2026
17d526b
wip trade model
Z3rca Jan 19, 2026
b391e7c
wip helpers instead of doubling code
Z3rca Jan 19, 2026
b732b6a
refactor to helpers
Z3rca Jan 19, 2026
dd1a94b
Merge branch 'diplomacy2' into diplomacy
Z3rca Jan 19, 2026
c155c7d
refactor an enhanced trade agreement model
Z3rca Jan 19, 2026
b0e46a1
just additional reduction for tradeagreement chances
Z3rca Jan 19, 2026
a2afd50
enhance alliance formation model
Z3rca Jan 19, 2026
ffe32ca
dll
Z3rca Jan 19, 2026
29a4eef
reduced distance , removed unnecssary comments
Z3rca Jan 19, 2026
5fdf021
Merge branch 'development' into diplomacy
Z3rca Jan 19, 2026
c227003
removed faction extension
Z3rca Jan 19, 2026
9e80ee3
remove unused behavior
Z3rca Jan 19, 2026
362c4cc
merge: reduce bloat methods
Z3rca Jan 19, 2026
30bfa55
some refactor and chance adjustment
Z3rca Jan 19, 2026
1637053
remove diplomacy test log
Z3rca Jan 19, 2026
cc93685
remove redundant script
Z3rca Jan 19, 2026
ba822e1
dll
Z3rca Jan 19, 2026
f4edd44
some little fixes
Z3rca Jan 20, 2026
1edd1ee
adjust agreement count
Z3rca Jan 20, 2026
0bff5cd
WIP deactivated irrelevant kingdom messages
Z3rca Jan 21, 2026
0528eda
dll
Z3rca Jan 21, 2026
0635963
cleanup notification
Z3rca Jan 21, 2026
e6eee31
remove unused notifation . works entirely via patch more reliable
Z3rca Jan 21, 2026
f734d29
wip consider honor alliance considerations
Z3rca Jan 21, 2026
17c33e3
relationship finalize honor agreement
Z3rca Jan 21, 2026
cbaa02d
update tor strings
Z3rca Jan 21, 2026
5f1181d
removed unused class (its in the patch)
Z3rca Jan 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,4 @@ public class SpecializationOption
public string[] AttributesToIncrease;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
411 changes: 411 additions & 0 deletions CSharpSourceCode/CampaignMechanics/Diplomacy/HonorAllianceDecision.cs

Large diffs are not rendered by default.

265 changes: 265 additions & 0 deletions CSharpSourceCode/CampaignMechanics/Diplomacy/TORAllianceWarBehavior.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
public class TORAllianceWarBehavior : CampaignBehaviorBase
{
// Track which wars were joined due to alliance obligations (kingdom StringId -> list of enemy kingdom StringIds)
private Dictionary<string, List<string>> _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);
}

/// <summary>
/// Checks if a war is an alliance war (defensive) rather than an offensive war.
/// </summary>
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;
}

/// <summary>
/// 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.
/// </summary>
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
}
}

/// <summary>
/// Resolves the HonorAllianceDecision for AI kingdoms immediately.
/// </summary>
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<IAllianceCampaignBehavior>();
allianceBehavior?.EndAlliance(kingdom, decision.AttackedAlly);
}
}

/// <summary>
/// Determines if an ally should join an offensive war (ally declared war).
/// Less obligatory than defensive.
/// </summary>
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;
}

/// <summary>
/// Marks a war as an alliance war (defensive) so it doesn't count toward offensive war limits.
/// </summary>
public void MarkAsAllianceWar(Kingdom kingdom, Kingdom enemy)
{
if (!_allianceWars.ContainsKey(kingdom.StringId))
{
_allianceWars[kingdom.StringId] = new List<string>();
}

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);
}
}
}

/// <summary>
/// Type definer for save/load of alliance war tracking.
/// </summary>
public class TORAllianceWarBehaviorTypeDefiner : SaveableTypeDefiner
{
public TORAllianceWarBehaviorTypeDefiner() : base(789_123) { }

protected override void DefineContainerDefinitions()
{
ConstructContainerDefinition(typeof(Dictionary<string, List<string>>));
ConstructContainerDefinition(typeof(List<string>));
}
}
}
Loading