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 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:
ownerUserIdis null (abandoned)ownerUserIdstarts with"AI_"- 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_foodkeys inLineageTreaty.terms. - settles automatically each turn during
endTurnprocessing. PROPOSE_TRIBUTEentry point is NOT YET WIRED to voice — the tribute settlement service ships and runs automatically, and already has aPROPOSE_TRIBUTEmatched-key branch, but no option synonym orgame_process.process_one_shot_valuespattern 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:
- Gets AI kingdoms list
- If empty: "No kingdoms to declare war on."
- Declares war on
aiKingdoms.get(0)(first AI kingdom, no player choice) - 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:
- Gets active treaties for player kingdom
- If empty: "No active treaties to break."
- Breaks
active.get(0)(first active treaty, no player choice) - 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).