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.