Skip to main content

Magic

Magic

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

Last updated: July 2, 2026

Lineage -- Magic System Design

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

Magic in Lineage provides 16 spells across 5 categories, from basic combat spells to legendary game-changing abilities. Spells consume mana, may require specific technologies, and apply effects to armies, territories, or kingdoms. Enchantments persist as timed buffs/debuffs with duration and stacking prevention.

Mana Pool

Source:

Field Default Cap
currentMana 75 100
maxMana 100 -
manaRegen 5 per turn -

Mana pool initial value is sourced from kingdom.resources["mana"] (seeded to 75 in ). If manaPool is missing AND the kingdom has no mana resource entry, falls back to 50.

Mana Regeneration

Source:

currentMana = min(currentMana + manaRegen, maxMana)

Called during turn processing. Excess mana above maxMana is clamped.

Cap legibility (finding 914). When the pool is at its maximum, names the cap and the lever instead of a bare "Mana is full": it states that regeneration above the cap is lost each turn, so the player should cast a spell before ending the turn. Guard: .

Mana Sync

Source:

kingdom.resources["mana"] = manaPool.currentMana

After any mana operation, the mana pool value is synced back to the kingdom's resource map. This ensures the mana resource displayed in kingdom status reflects the mana pool state.


Spell Definitions (16 Spells — ACTUAL CODE)

Source: lineage.lineage_spells table via , cached on the service through getSpellDefinition(spellId) / getAllSpellDefinitions() using LINEAGE_SPELL_CACHE / LINEAGE_SPELL_LIST_CACHE. Each row = one spell (columns: spell_id, name, mana_cost, target_type, duration, category, required_techs, description, effects JSON). Balance changes are data edits — update the row, evict the caches, done (no code deploy).

Spells are defined as SpellDefinition records with: spellId, name, manaCost, targetType, duration, effects (Map<String, Object>).

IMPORTANT: The spell catalog in code differs significantly from the original design doc. The code is canonical.

Combat Spells (ARMY target)

Key Name Mana Duration Effects
FIREBALL Fireball 20 0 damage: 50
HEAL Healing Light 15 0 heal: 30
SHIELD Arcane Shield 25 3 defenseBonus: 25
HASTE Haste 20 2 movementBonus: 2

Territory Spells (TERRITORY target)

Key Name Mana Duration Effects
BLESS_LAND Bless the Land 30 5 productionBonus: 25
WARD Protective Ward 35 10 defenseBonus: 30
REVEAL Reveal Secrets 15 0 revealResources: true

Kingdom/Self Spells

Key Name Mana Target Duration Effects
MANA_SURGE Mana Surge 10 SELF 0 manaRegen: 20 (instant mana add)
PROSPERITY Prosperity 40 KINGDOM 5 goldBonus: 50
INSPIRATION Inspiration 25 KINGDOM 3 moraleBonus: 15

Advanced Spells

Key Name Mana Target Duration Effects
METEOR Meteor Strike 60 TERRITORY 0 damage: 100, buildingDamage: true
RESURRECTION Resurrection 50 ARMY 0 resurrectPercent: 25
EARTHQUAKE Earthquake 45 TERRITORY 0 siegeBonus: 50

Legendary Spells

Key Name Mana Target Duration Effects
TIME_STOP Time Stop 200 GLOBAL 0 victorySpell: true
APOCALYPSE Apocalypse 150 GLOBAL 0 globalDamage: 200
DIVINE_INTERVENTION Divine Intervention 100 KINGDOM 10 allBonuses: 10

Key Differences from Original Design

Aspect Original Doc Actual Code
FIREBALL cost 15 mana 20 mana
FIREBALL damage 30 50
TIME_STOP cost 80 mana 200 mana
TIME_STOP target KINGDOM (skip_turn) GLOBAL (victorySpell)
LIGHTNING_BOLT existed removed from code
ICE_STORM existed removed from code
SHADOW_STRIKE existed removed from code
FERTILE_LAND existed replaced by BLESS_LAND
STONE_WALL existed replaced by WARD
NATURE_BLESSING existed removed
PLAGUE existed removed
DIVINE_SHIELD existed replaced by SHIELD
METEOR_STORM existed replaced by METEOR
MANA_SURGE mana_regen: 3 (permanent) manaRegen: 20 (instant add)
RESURRECTION did not exist NEW: 50 mana, resurrects 25% units
APOCALYPSE did not exist NEW: 150 mana, damages all enemy armies
DIVINE_INTERVENTION did not exist NEW: 100 mana, +10% all stats for 10 turns

Spell Requirements

Source: lineage.lineage_spells.required_techs column (comma-separated list), read via SpellDefinition.requiredTechs() and enforced by .

For each spell:
  requiredTechs = parse(row.required_techs)   // "" | null -> no requirement
  spell is castable iff discoveredTechs containsAll(requiredTechs)

The old hardcoded rules (per-spell IDs + manaCost >= 35 -> BASIC_MAGIC threshold) have been replaced by explicit required_techs values on each row so tech gating is data-driven rather than derived.

Technology Gating Summary

Spell Required Tech
FIREBALL (20), HEAL (15), HASTE (20), REVEAL (15), MANA_SURGE (10) None
SHIELD (25), INSPIRATION (25) None (cost < 35)
BLESS_LAND (30) None (cost < 35)
WARD (35), PROSPERITY (40), EARTHQUAKE (45), RESURRECTION (50), METEOR (60) BASIC_MAGIC
TIME_STOP (200) LEGENDARY_MAGIC
APOCALYPSE (150) LEGENDARY_MAGIC + LEGENDARY_WARFARE
DIVINE_INTERVENTION (100) LEGENDARY_MAGIC

Spell Casting Flow

Source: +

Handler-Level Routing (NLU -> Handler)

Source:

1. Get spellId from storyKeySelected.matchedKey
2. If null/empty -> show spellCastingMenu
3. If "SPELLS ALL" / "SPELLS_ALL" -> displayAllSpells (full catalog)
4. If "CAST_SPELL" / "SPELLS" / "MAGIC" / "MENU" etc -> spellCastingMenu
5. Normalize: uppercase, spaces to underscores
   Aliases: BLESS_TERRITORY -> BLESS_LAND, LIGHTNING_BOLT -> LIGHTNING,
            STONE_SKIN -> SHIELD, DIVINE_SHIELD -> SHIELD
6. Determine targetType from the DB-backed spell definition ()
7. Determine targetId (first army, first territory, or kingdom)
8. Call lineageMagicService.castSpell()

How NLU routes to LINEAGE_CAST_SPELL: Commands are registered via addOption() in and with synonyms like "fireball,fire", "heal,healing", "mana surge,regenerate". The NLU matches user text against these synonyms and routes to LINEAGE_CAST_SPELL with the spell key as matchedKey. Free-text "cast fireball" also matches.

Service-Level Casting

Source:

1. Look up SpellDefinition in SPELLS map by spellId
2. Validate: spell exists
3. Validate: kingdom has enough mana (manaPool.hasMana(spell.manaCost))
4. Validate target (army ownership, territory ownership, kingdom existence)
5. Check for duplicate enchantment stacking (if spell has duration > 0)
6. Apply spell effects BEFORE spending mana (mana preserved on failure)
7. Spend mana only after successful application
8. Sync mana pool to kingdom resources
9. Complete spell (status -> COMPLETED)
10. Add to spellCasts history (important for Magical Victory check)
11. Create enchantment if spell has duration > 0
12. Return success message

Failure Cases

Condition Message
Unknown spell "Unknown spell: {spellId}"
Not enough mana "Insufficient mana. Need {cost}, have {current}"
Invalid target "Army/Territory/Kingdom not found: {targetId}"
Wrong target owner "Can only cast buff spells on your own armies/territories"
Duplicate enchantment "{name} is already active on this target. Wait for it to expire."
Effect exception "Spell casting failed: {error}"

Spell Effect Application

Spell Effect Application

Source: , applyDamageToArmy

Effects are applied based on keys in the spell's effects map:

Effect Key Target Behavior
damage ARMY target Total casualties = min(totalUnits, damage/10), distributed proportionally across unit types. Remainder assigned to largest unit group. Morale loss = damage/5.
damage TERRITORY target Text-only "Territory damaged" (no actual effect)
heal ARMY target Morale healed = heal/3. Units restored via inverse scaling: restorePercent = max(10, 25 - totalUnits/10). Small armies (~23%), large armies (10%). Min 1 unit restored per type.
manaRegen SELF Instant mana add via manaPool.addMana(amount) + sync
moraleBonus KINGDOM caster.morale += bonus (clamped to 100)
goldBonus KINGDOM Instant gold add (only if duration == 0)
victorySpell GLOBAL Text-only "LEGENDARY SPELL CAST!" (victory checked separately via spellCasts history)
globalDamage GLOBAL Full damage to ALL enemy armies via applyDamageToArmy()
revealResources TERRITORY Shows naturalResources map for target territory

Damage-to-Casualties Conversion (Fixed)

totalToKill = min(totalUnits, damage / 10)
Distributed proportionally across unit types:
  share[type] = totalToKill * count[type] / totalUnits
  Remainder assigned to largest unit group

Example: FIREBALL (damage: 50) on army with 50 warriors + 50 archers:
  totalToKill = min(100, 50/10) = 5
  warriors lose: 5 * 50/100 = 2
  archers lose: 5 * 50/100 = 2
  remainder (1) -> warriors (largest group, tied -> first encountered)
  moraleLoss = 50 / 5 = 10

Enchantment System

Source: (enchantment creation block)

Enchantment Creation

Spells with a duration effect create persistent enchantments:

If spell.effects contains "duration":
  Create enchantment:
    enchantmentId = "ench_" + 12 random hex chars
    spellName = spell.name
    castByKingdomId = caster's kingdom ID
    targetId = target army/territory/kingdom ID
    targetType = spell's targetType
    effects = spell.effects (excluding "duration")
    turnsRemaining = duration value
    createdOnTurn = current turn number

Any spell with duration > 0 creates an enchantment. Spells that create enchantments: SHIELD (3 turns), HASTE (2 turns), BLESS_LAND (5 turns), WARD (10 turns), PROSPERITY (5 turns), INSPIRATION (3 turns), DIVINE_INTERVENTION (10 turns).

Enchantment Processing

Enchantments are processed during turn processing:

For each active enchantment on kingdom:
  turnsRemaining -= 1
  if turnsRemaining <= 0:
    Remove enchantment
    Reverse effects (remove combat bonuses, etc.)

Duplicate Stacking Prevention

Source:

Before creating enchantment:
  Check if target already has an active enchantment from same spell
  If duplicate found:
    REJECT the cast entirely
    Return error: "{spell name} is already active on this target. Wait for it to expire before recasting."
    Mana is NOT spent

This prevents stacking the same spell on a target entirely. The spell must expire before it can be recast — duration is NOT refreshed.


Spell List Command

Source:

Returns all 16 spells grouped by category, with mana costs and descriptions. No filtering based on available technology -- all spells are shown regardless of whether the kingdom can cast them.