Skip to main content

Diplomacy

Diplomacy

Status: IN PROGRESS — Tracked open issues; see Known Issues section

Last updated: August 30, 2026

Lineage -- Diplomacy & AI System Design

Status: IN PROGRESS — Tracked open issues; see Known Issues section

Diplomacy governs inter-kingdom relationships through treaties, war declarations, peace negotiations, and relation tracking. AI kingdoms use personality-driven decision-making to determine diplomatic behavior, military priorities, and betrayal thresholds.

Relation Model

Source: LineageRelation

Relations are stored bidirectionally between kingdom pairs (alphabetically ordered):

Field Type Purpose
relationId String Unique ID
kingdomId1 / kingdomId2 String Kingdom pair (alphabetical order)
relationStatus String Current status
opinionScore1to2 Integer Kingdom 1's opinion of kingdom 2
opinionScore2to1 Integer Kingdom 2's opinion of kingdom 1
turnsAtWar Integer How long at war
turnsSinceContact Integer Inactivity counter
lastInteractionType String Most recent diplomatic action

Relation Statuses

Status Meaning
NEUTRAL Default state
FRIENDLY Non-aggression or trade agreement active
ALLIED Alliance treaty active
HOSTILE Treaty broken or war ending
AT_WAR Active war

Treaty System

Source: , LineageTreaty

Treaty Types

Type Relation Status Opinion Bonus
ALLIANCE ALLIED +25 both directions
TRADE_AGREEMENT FRIENDLY +15 both directions
NON_AGGRESSION FRIENDLY +10 both directions
VASSAL (not fully implemented) -

Treaty Lifecycle

PENDING -> ACTIVE -> BROKEN / EXPIRED

Treaty Proposal Flow

Source:

1. Validate: not at war with target
2. Validate: no existing treaty of same type between kingdoms
3. Create treaty with status=PENDING
4. Treaty stored with terms: {proposedBy, proposedOnTurn}
5. Target kingdom can accept or reject

Treaty Proposal Targeting (Fixed 2026-07-18)

Source: (PROPOSE_TREATY/PROPOSE_ALLIANCE/PROPOSE_TRADE)

A player can name a specific rival ("propose alliance to Goldcrest", "propose trade with the Iron Compact") to court a chosen faction; parseProposeTargetName extracts the target from the utterance (the trailing to/with clause) and resolveRivalByName resolves it (exact, then prefix, then substring match against AI kingdom names). A bare "propose alliance" with no named target keeps the prior behavior — it walks the roster and proposes to the first kingdom that can actually receive that treaty type (not at war, no existing treaty of the same type already). Before this fix a player had no way to choose WHICH rival to court, which — combined with the acceptance-loop defect below — meant pursuing 3 distinct-faction alliances toward Diplomatic victory was not possible even in principle.

Reachability hardening. resolveRivalByName now also resolves a positional index ("propose alliance to 2" courts the 2nd rival) and strips a leading "the" on both sides ("the gilded league" and "gilded league" both resolve "The Gilded League"); a mis-heard name is answered with the list of rival realms instead of a dead-end. The diplomacy menu () now surfaces the rival roster by name with each realm's current standing and a tappable "propose alliance to " option (PROPOSE_<TYPE>_ON:<kingdomId>, handled by ), so a voice player can discover WHICH three factions this campaign rolled and court any of them directly -- a bare "propose alliance" alone only ever reached the first fixed candidate, which is why pursuing three distinct alliances was blind guessing.

Treaty Acceptance

Source:

1. Validate treaty exists and is PENDING
2. Call treaty.acceptTreaty(currentTurn)
3. Update relation status based on treaty type:
   ALLIANCE -> ALLIED
   TRADE_AGREEMENT -> FRIENDLY
   NON_AGGRESSION -> FRIENDLY
4. Apply opinion bonus to both kingdoms

Treaty Breaking

Source:

1. Validate treaty exists and is ACTIVE
2. Validate breaking kingdom is participant
3. Call treaty.breakTreaty(currentTurn, breakingKingdomId)
4. Apply opinion penalty: -30 to the betrayed kingdom's opinion
5. Set relation status to HOSTILE

Treaty Expiration

Source:

Called once per turn during turn processing:

For each active treaty:
  if treaty.isExpired(currentTurn):
    treaty.expireTreaty()
    Update relation to NEUTRAL

War Declaration

Source:

Flow

1. Validate: not already at war
2. Validate: not allied (must break alliance first)
3. Break any existing treaties (non-aggression, trade)
4. Set relation to AT_WAR
5. Apply opinion penalty: -50 to defender's opinion of attacker

Constraints

  • Cannot declare war on an ally without first breaking the alliance
  • Breaking alliance as part of war declaration triggers the -30 opinion penalty from treaty breaking, PLUS the -50 from war declaration

Peace Making

Source:

1. Validate: currently at war
2. Set relation to NEUTRAL
3. Reset turnsAtWar to 0
4. Set lastInteractionType to "PEACE_MADE"

AI Personality System

Source: AIPersonality

7 Personality Types

AGGRESSIVE, PEACEFUL, DIPLOMATIC, SCHOLAR, MERCHANT, MYSTIC, BALANCED

Trait Weights (0-100)

Each AI has 8 personality traits that influence behavior:

Trait Description
aggression Likelihood of declaring war
diplomacy Willingness to negotiate/ally
greed Focus on gold/resources
ambition Desire for expansion
caution Risk aversion level
loyalty Alliance reliability
pragmatism Flexibility vs ideology
mysticism Magic focus

Priority Weights (0-100)

6 strategic priorities that determine resource allocation:

Priority Description
military Army building focus
economy Resource production focus
technology Research focus
magic Spell/mana focus
expansion Territory acquisition focus
defense Fortification focus

Diplomatic Modifiers

Modifier Description
trust_threshold Minimum trust to form alliance (0-100)
betrayal_threshold Trust level when betrayal becomes possible (0-100)
treaty_value How much the AI values existing treaties (0-100)

War Likelihood Formula

Source: AIPersonality.calculateWarLikelihood

strengthRatio = (currentMilitary * 100) / max(enemyMilitary, 1)  // long arithmetic
relationPenalty = max(0, currentRelation)

likelihood = (aggression + ambition) / 2
           + (strengthRatio - 100) / 2
           - caution / 2
           - relationPenalty

return clamp(likelihood, 0, 100)

Example: AI with aggression=80, ambition=60, caution=30 and 2:1 military advantage:

likelihood = (80+60)/2 + (200-100)/2 - 30/2 - 0 = 70 + 50 - 15 = 105 -> clamped to 100

Alliance Likelihood Formula (WIRED into treaty acceptance 2026-07-18)

Source: AIPersonality.calculateAllianceLikelihood, called from

likelihood = diplomacy / 2
           + max(0, relationValue - trustThreshold)
           + mutualEnemyCount * 10
           - abs(powerDifference) / 5
           + pragmatism / 4
           + treatyCommitmentBonus         // NON_AGGRESSION +25, TRADE_AGREEMENT +15, ALLIANCE +0
           + establishedRelationshipBonus  // +15 per active NON_AGGRESSION/TRADE_AGREEMENT already standing between the two kingdoms

return clamp(likelihood, 0, 100) >= TREATY_ACCEPTANCE_THRESHOLD (50)  // accept/reject

The fix (pacing.md DIPLOMATIC finding, 2026-07-18). This formula existed and was unit-tested in isolation, but evaluateTreatyProposal never called it — it instead used a crude ad-hoc gate (relation > trust_threshold || diplomacy trait > 70) with no per-treaty-type weighting. Since trust_threshold is a fixed 40 for every personality and no rejection path builds relation, only ONE of six archetypes (DIPLOMATIC, diplomacy=90) could ever pass that gate at neutral (zero) relation — every other archetype rejected a fresh proposal forever with no route to build toward acceptance. The empirical audit (session 634b4cf5) found exactly this: 3 independent play_sequence runs where a proposed alliance/trade was never resolved and view relations stayed at "No active diplomatic relations yet." indefinitely.

evaluateTreatyProposal now computes the real accrued relation (AIKingdom.getRelation), a computed mutualEnemyCount (kingdoms both parties are independently at war with), powerDifference (military strength delta), and adds a per-treaty-type commitment bonus so a lighter treaty is an easier ask than a full alliance (a player can build trust step by step). At neutral relation and even military power: NON_AGGRESSION is accepted by ALL SIX archetypes (the commitment bonus alone clears the bar); ALLIANCE is accepted immediately by DIPLOMATIC / MERCHANT / PEACEFUL (the factions.md "Courtable ally" roster) and requires relation built up via accepted lighter treaties for AGGRESSIVE / SCHOLAR / MYSTIC — matching their "ancient enemy" / "wary rival" flavor rather than being flatly unreachable.

The 24772 correction (established-relationship bonus). The commitment bonus above was still not enough on its own: a "hard" archetype (AGGRESSIVE, MYSTIC, SCHOLAR) rejects a cold ALLIANCE and a cold TRADE_AGREEMENT -- only a NON_AGGRESSION pact clears the bar cold -- and lighter-treaty relation gains stay below the fixed trust_threshold of 40, so they contribute 0 to the formula. That left those archetypes with no reachable route to an alliance, and because a Campaign seeds only 3 AI kingdoms (a random 3-of-6, LineageWorldInitializer.populateAIKingdoms) while the Diplomatic victory needs 3 alliances, the player must ally EVERY present faction -- so a single unreachable archetype killed the whole path (~95% of rosters contain at least one hard archetype). evaluateTreatyProposal now adds establishedRelationshipBonus = +15 per active NON_AGGRESSION/TRADE_AGREEMENT already standing between the two kingdoms (ESTABLISHED_RELATIONSHIP_BONUS_PER_TREATY), so the step-by-step courtship is real: NON_AGGRESSION (accepted cold) -> TRADE_AGREEMENT (accepted with the pact standing, +15) -> ALLIANCE (accepted with both standing, +30) reaches acceptance for EVERY archetype. Empirically confirmed end-to-end on target:'local': three fresh beelines each courted a different random 3-of-6 roster -- including the AGGRESSIVE Iron Compact, the MYSTIC Ashen Covenant, and the SCHOLAR Sunfire Dynasty -- and each reached the Diplomatic victory on turn 9 (alliances signed turn 4, held past the 5-turn gate). Guarded by JUnit + scenario_test 2916 (end-to-end win).

Betrayal Check

Source: AIPersonality.wouldBetray

if allianceTrust > betrayalThreshold + loyalty:
  return false  // too trustworthy to betray

betrayalScore = (greed + potentialGain) - (loyalty + allianceTrust)
return betrayalScore > 50

AI Personality Generation

Source: , createDefaultAIKingdom

AI personalities are auto-generated on first access via getOrCreateAIKingdom():

New AI Kingdom Creation

1. Random personality type from: AGGRESSIVE, PEACEFUL, DIPLOMATIC, SCHOLAR, MERCHANT, MYSTIC
2. createDefaultTraits(type) → 8 trait values (aggression, diplomacy, greed, ambition, caution, loyalty, pragmatism, mysticism)
3. createDefaultPriorities(type) → 6 priority values (military, economy, technology, magic, expansion, defense)
4. Fixed diplomatic modifiers: trust_threshold=40, betrayal_threshold=80, treaty_value=60
5. Default difficulty level: 5 (enables economy, expansion, military, research, and diplomatic actions -- accepting/rejecting pending treaty proposals, AI-initiated treaty offers, and war declarations)

Persisted AI Kingdom with Null Personality

If a previously saved LineageAIKingdom has personality == null, getOrCreateAIKingdom() assigns a BALANCED default (all traits/priorities at 50).

Personality Trait Profiles

Type Aggression Diplomacy Greed Ambition Caution Loyalty Pragmatism Mysticism
AGGRESSIVE 80 30 60 80 20 40 50 30
PEACEFUL 20 70 40 40 70 70 60 40
DIPLOMATIC 30 90 40 60 50 80 70 30
SCHOLAR 20 60 30 70 60 60 50 80
MERCHANT 30 70 90 70 60 50 80 20
MYSTIC 40 50 30 50 40 60 30 95
BALANCED 50 50 50 50 50 50 50 50

AI Kingdom Detection

Source:

A kingdom is AI-controlled if any of:

  1. ownerUserId is null (abandoned)
  2. ownerUserId starts with "AI_"
  3. Owner has not logged in for 30+ days
daysSinceActive = (currentTime - lastActivityTms) / (1000 * 60 * 60 * 24)
isAbandoned = daysSinceActive >= 30

Global World Treaty Service

Source:

The checks for peace treaties between players via treatyService.hasPeaceTreaty(player1, player2). This checks for active NON_AGGRESSION pacts or ALLIANCE treaties between global world players.


Tribute System (cross-reference)

Tribute is implemented as a treaty term, not as a separate diplomatic action. See kingdom.md § Inter-Kingdom Tribute System for the full design. Highlights:

  • VASSAL and the new TRIBUTE treaty types carry tribute_amount_gold / tribute_amount_food keys in LineageTreaty.terms.
  • settles automatically each turn during endTurn processing.
  • PROPOSE_TRIBUTE entry point is NOT YET WIRED to voice — the tribute settlement service ships and runs automatically, and already has a PROPOSE_TRIBUTE matched-key branch, but no option synonym or game_process.process_one_shot_values pattern routes a spoken utterance to it yet. A player cannot currently say "propose tribute" and reach it. Design intent: AI auto-accepts when player military strength is at least 2x the target's.

GLOBAL-Mode Map-Data Conversion (Fixed 2026-07-18)

Source:

Every GLOBAL-mode (Eternal Realm) handler resolves the player's kingdom via a shared map-data converter. Before this fix, 13 handlers (, , , , , , , , , , , , ) called the one canonical public static , but that canonical method itself built .treaties(new ArrayList<>()), .relations(new ArrayList<>()), and a singleton kingdoms/aiKingdoms list containing only the player's own kingdom on every call — so any treaty proposed, relation adjusted, or AI-kingdom personality read during one GLOBAL-mode request was silently discarded before the next, and diplomacy could never find a real rival target.

The canonical method now sources treaties/relations by reference from the persisted LineageGlobalKingdom.LineageGlobalKingdomData row — mutations (propose/accept/break treaty, adjust relation) land back on the exact object graph LineageGameSaver.saveGlobalKingdom persists at the end of the request, the same way territories/armies/manaPools/enchantments already did — and merges nearby kingdoms (already loaded onto GameBasicProperties by LineageGameLoader.loadGlobalWorld) into kingdoms/aiKingdoms so diplomacy and combat can find real rivals instead of only the player's own row. Regression-protected by JUnit (5 tests) plus game_test.scenario_tests 2913 (treaty persists across two sequential GLOBAL-mode requests) and 2914 (treaty survives being read by a different handler — the magic/spellbook view — in between).

War Declaration & Treaty Break Text (Fixed #6143)

Source:

The DECLARE_WAR and BREAK_TREATY handlers now return text to the player:

DECLARE_WAR Response

"War declared on {kingdom.getKingdomName()}!"

Followed by diplomacyMenu() re-render. The handler:

  1. Gets AI kingdoms list
  2. If empty: "No kingdoms to declare war on."
  3. Declares war on aiKingdoms.get(0) (first AI kingdom, no player choice)
  4. Calls which sets relation to AT_WAR and applies -50 opinion

BREAK_TREATY Response

"You have broken the {treaty.getTreatyType()} treaty. Relations damaged."

Followed by diplomacyMenu() re-render. The handler:

  1. Gets active treaties for player kingdom
  2. If empty: "No active treaties to break."
  3. Breaks active.get(0) (first active treaty, no player choice)
  4. Calls which applies -30 opinion and sets relation to HOSTILE

Root cause of original #6143 bug: The handlers were missing addText() calls. They now include text but still lack target selection (always operates on first item in list).