Files
Mana-Loop/docs/specs/attunements/invoker/systems/invocation-system-spec.md
T
n8n-gitea 7dda515a71
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m22s
feat: add Invocation System spec for Invoker attunement
- New spec at docs/specs/attunements/invoker/systems/invocation-system-spec.md
- Invocation Charge meter (0-100) fills passively, drains while invoking
- Channeled guardian auto-casts spells from their elements at reduced cost
- Spell selection picks best affordable spell, steps down as mana depletes
- No guardian swap mid-invocation; ends when charge depleted or room cleared
- Pact Affinity extended with diminishing-returns cast speed bonus (max 50%)
- New guardian-invocation discipline with 4 perks (efficiency, speed, sustain, mastery)
2026-06-13 13:02:21 +02:00

22 KiB
Raw Blame History

Invocation System — Design Spec

Describes the Invocation system: a new combat mechanic for the Invoker attunement that lets the player channel pacted guardians to auto-cast elemental spells at reduced cost. Also extends Pact Affinity with a new combat benefit (cast speed).


1. Objective

The Invoker attunement currently treats pacts as passive, permanent boons. The Invocation system adds an active combat layer: the player builds up an Invocation Charge meter, and when full, can channel a pacted guardian to auto-cast elemental spells at a fraction of their normal cost.

Design goals:

  • Make pacts feel active in combat, not just passive stat sticks
  • Reward players who have signed more/higher-tier pacts (faster charge, stronger spells)
  • Create meaningful decisions: when to invoke, which guardian gets channeled, mana management during invocation
  • Give Pact Affinity a combat role (cast speed) so it matters outside of ritual time reduction
  • Add a new Invoker discipline (guardian-invocation) for vertical progression of the system

2. Invocation Charge

2.1 The Meter

A new resource tracked in the combat store:

Field Type Range Description
invocationCharge number 0100 The invocation meter. Fills passively. Drains while invoking.
activeInvocation object | null See §3.

There is no separate cooldown field. Cooldown is implicit: after invocation ends, the player must wait for invocationCharge to fully recharge to 100 before invoking again. See §2.3.

2.2 Charge Fill Rate

Charge fills passively every combat tick while in climb action and invocation is not active:

chargePerTick = baseFillRate × pactCountMultiplier × disciplineMultiplier
Parameter Value Source
baseFillRate 0.25 per tick Constant
pactCountMultiplier 1 + signedPacts.length × 0.15 More pacts = faster fill
disciplineMultiplier 1 + invocationChargeRateBonus From guardian-invocation discipline (see §7)

Example: 3 signed pacts, no discipline bonus:

  • pactCountMultiplier = 1 + 3 × 0.15 = 1.45
  • chargePerTick = 0.25 × 1.45 = 0.3625
  • Time to 100: ~276 ticks (~55 seconds at 200ms/tick)

At 0 pacts, charge fills at 0.25/tick → 400 ticks to full (~80 seconds).

2.3 Cooldown = Full Recharge

There is no separate cooldown counter. When invocation ends (for any reason — see §3.3), the charge meter is at 0 (or near 0). The player must wait for invocationCharge to reach 100 again before invocation can reactivate.

This means the "cooldown" is directly tied to the fill rate:

  • With 0 pacts: ~80 seconds to recharge
  • With 3 pacts: ~55 seconds to recharge
  • With 6 pacts: ~42 seconds to recharge
  • With guardian-invocation discipline bonuses: even faster

Charge only fills while in climb action. If the player leaves combat, filling pauses.


3. Active Invocation State

When invocation is active, the following state is tracked:

activeInvocation: {
  guardianFloor: number;   // Which guardian is being channeled (their floor number)
  spellId: string;         // Currently auto-cast spell ID
  element: string;         // Element of the chosen spell
  castProgress: number;    // 0-1, cast progress accumulator for invocation spell
} | null

3.1 Activation

Invocation auto-activates when all of the following are true:

  1. invocationCharge >= 100
  2. activeInvocation === null
  3. Player is in climb action (combat)
  4. Current room has living enemies

On activation:

  1. invocationCharge begins draining (see §4)
  2. A guardian is selected (see §3.2)
  3. A spell is selected from that guardian's elements (see §5)
  4. Log: "💜 Invoking {guardianName}'s power!"

3.2 Guardian Selection

The channeled guardian is chosen by scoring all signed pacts:

score(floor) = bestElementalBonus(floor) × tierMultiplier(floor)

bestElementalBonus(floor):

  • Get the guardian's elements
  • For each element, compute getMultiElementBonus(element, currentEnemyElements) using the existing combat-utils function
  • Take the maximum bonus across all the guardian's elements

tierMultiplier(floor):

  • 1.0 + floor × 0.005 — higher-tier guardians are preferred as a tiebreaker

The guardian with the highest score is selected. If no signed pacts exist, invocation cannot activate (but this is prevented by the charge fill being extremely slow without pacts — see §2.2).

3.3 Ending Invocation

Invocation ends when any of the following occurs:

  1. invocationCharge reaches 0 (depleted)
  2. Current room is cleared (all enemies defeated)
  3. Player leaves climb action
  4. Player cannot afford any spell from any signed pact's elements (see §5.3)

On end:

  1. Set activeInvocation = null
  2. Log: "💜 Invocation ends. {guardianName}'s power fades."

Important: If charge reaches 0 mid-cast, the current spell cast completes before invocation ends. The cast-in-progress is allowed to finish (charge check happens at cast start, not during).

After invocation ends, the charge meter begins refilling from its current value (typically 0). Invocation cannot reactivate until charge reaches 100 again (§2.3).


4. Charge Drain During Invocation

While invocation is active, charge drains per tick:

drainPerTick = BASE_DRAIN × spellCostMultiplier × drainRateMultiplier
Parameter Value Source
BASE_DRAIN 1.0 per tick Constant
spellCostMultiplier spell.cost.amount / 10 Scales with the current spell's cost
drainRateMultiplier 1.0 base, reduced by guardian-invocation discipline See §7

Higher-cost spells drain charge faster, creating a natural ramp-down: as the player's mana depletes and they step down to cheaper spells, charge drains more slowly, extending the tail end of invocation.

The drainRateMultiplier starts at 1.0 and is reduced by the invocation-sustain perk (see §7.2). Minimum value: 0.7 (at max 3 tiers).


5. Spell Selection and Casting

5.1 Guardian Spellbook

A guardian "knows" all spells from all their elements. For example, a BlackFlame guardian with element: ['metal', 'fire', 'earth'] knows every spell in SPELLS_DEF where spell.elem is 'metal', 'fire', or 'earth'.

5.2 Spell Selection Algorithm

On activation and after each cast, the invocation spell is re-evaluated. The player is channeling the guardian's power — the invocation spellbook is all spells from the guardian's elements, regardless of what the player has personally learned:

1. Gather all spells from the invoked guardian's elements (the full guardian spellbook)
2. Filter to spells the player can afford at the effective cost multiplier:
   - For raw cost: rawMana >= cost.amount × effectiveCostMultiplier
   - For element cost: elements[element].current >= cost.amount × effectiveCostMultiplier
3. From remaining spells, pick the one with the highest base damage (spell.dmg)
4. If tied, pick the highest tier
5. If still tied, pick the lowest cost (most efficient)

5.3 Auto-End When Unaffordable

The invoked guardian is fixed for the entire invocation — the player cannot swap to a different guardian mid-invocation. If the player cannot afford any spell from the invoked guardian's elements at the effective cost multiplier, invocation ends immediately (§3.3, condition 4).

5.4 Invocation Spell Cast Slot

The invocation spell operates as a third parallel cast track alongside:

  1. The player's active spell (primary cast progress)
  2. Equipment spell states (array of concurrent spells)

In processCombatTick, the invocation spell is processed similarly to equipment spells:

  • It has its own castProgress accumulator
  • It uses the same HOURS_PER_TICK × spellCastSpeed × effectiveAttackSpeed progress formula
  • On cast start, it deducts mana at the effective cost multiplier (same pattern as the active spell)
  • Damage is calculated using the existing calcDamage() with the invocation spell's element

The invocation spell does not interfere with the player's active spell. Both cast simultaneously, draining from the same mana pools.

5.5 Cost Multiplier

The base cost multiplier is 0.1 (1/10th). This is reduced by the guardian-invocation discipline (see §7):

effectiveCostMultiplier = 0.1 - costReductionFromDiscipline

Minimum effective cost multiplier: 0.05 (1/20th).


6. Pact Affinity — Combat Extension

Pact Affinity currently only reduces ritual time. It now also grants a cast speed bonus in combat.

6.1 Cast Speed Formula

castSpeedBonus = MAX_BONUS × (1 - 1 / (1 + pactAffinity × SCALING))
Parameter Value Description
MAX_BONUS 0.5 (50%) Hard cap on cast speed increase
pactAffinity 0.0+ Combined affinity (prestige upgrade + discipline bonus)
SCALING 1.5 Controls the curve shape (diminishing returns)

This gives diminishing returns:

Pact Affinity Cast Speed Bonus
0.0 0%
0.1 5.7%
0.3 14.6%
0.5 21.4%
0.7 26.9%
0.9 31.6%
1.5 37.5%
3.0 42.9%
→ 50% (asymptote, never reached)

6.2 Application

The cast speed bonus applies to all spell casts (active spell, equipment spells, and invocation spells) while in combat (climb action). It is applied as a multiplier to the totalAttackSpeed value used in cast progress calculation:

effectiveAttackSpeed = totalAttackSpeed × (1 + castSpeedBonus)

6.3 Affinity Sources (Unchanged)

Pact affinity is the sum of:

  • pactAffinityUpgrade: prestige upgrade level × 0.1 (max 0.9)
  • pactAffinityBonus: from Pact Attunement discipline (base 0.05 + perks)

Total is capped at 0.9 for the ritual time formula. For the cast speed formula, the raw sum is used (can exceed 0.9 but with heavy diminishing returns per the curve above).


7. New Discipline: Guardian Invocation

A third Invoker discipline that directly enhances the Invocation system.

7.1 Definition

Field Value
ID guardian-invocation
Name Guardian Invocation
Attunement invoker
Mana Type raw
Base Cost 20
Requires ['signed_pact']
Stat Bonus invocationChargeRateBonus +0.10 (base)
Scaling Factor 120
Difficulty Factor 250
Drain Base 8

Main stat: invocationChargeRateBonus

This is the primary stat the discipline scales. It directly feeds into the charge fill rate formula (§2.2):

disciplineMultiplier = 1 + invocationChargeRateBonus

The stat scales with XP via the standard discipline math:

StatBonus = baseValue × (XP / scalingFactor)^0.65
          = 0.10 × (XP / 120)^0.65

At 0 XP: +0.10. At 100 XP: ~0.09. At 500 XP: ~0.22. At 1000 XP: ~0.34.

7.2 Perks

Perk ID Type Threshold Bonus Description
invocation-efficiency once 100 Cost multiplier 0.02 (0.1 → 0.08) Early power spike — invocation spells cost less mana
invocation-speed infinite 200 Every 150 XP: invocationChargeRateBonus +0.05 Core scaling — faster charge cycling with more XP
invocation-sustain capped 400 Every 200 XP: drainRateMultiplier 0.1, max 3 tiers Charge depletes slower while invoking
invocation-mastery capped 500 Every 250 XP: cost multiplier 0.01, max 3 tiers Late-game — pushes cost multiplier down further

Perk details:

  • invocation-efficiency (once @ 100 XP): Immediately reduces the effective cost multiplier from 0.1 to 0.08. This means all invocation spells cost 20% less mana from the start. A noticeable early power spike.

  • invocation-speed (infinite @ 200 XP, every 150 XP): Adds +0.05 directly to invocationChargeRateBonus. This is the primary scaling perk — the more XP earned, the faster the charge meter fills. At 500 XP (150 XP past threshold, 1 interval): +0.05. At 800 XP (4 intervals): +0.20. At 1400 XP (8 intervals): +0.40. This perk has no cap, so it always scales.

  • invocation-sustain (capped @ 400 XP, 3 tiers, every 200 XP): Reduces drainRateMultiplier by 0.1 per tier. At max (3 tiers, 800 XP past threshold): drain multiplier = 1.0 0.3 = 0.7. This means charge drains 30% slower, making each invocation last ~43% longer.

  • invocation-mastery (capped @ 500 XP, 3 tiers, every 250 XP): Further reduces the cost multiplier by 0.01 per tier. At max (3 tiers, 1250 XP past threshold): cost multiplier = 0.08 0.03 = 0.05. Combined with invocation-efficiency, the total reduction is 0.1 → 0.05 (1/20th cost).

7.3 Maximum Theoretical Bonuses

Source Value
Base charge rate bonus +0.10
invocation-speed infinite perk +0.05 per 150 XP (unlimited)
invocation-efficiency once perk cost mult 0.1 → 0.08
invocation-mastery capped perk (3 tiers) cost mult 0.08 → 0.05
invocation-sustain capped perk (3 tiers) drain mult 1.0 → 0.7
Minimum cost multiplier 0.05 (1/20th)
Minimum drain multiplier 0.7 (30% slower drain)

7.4 Discipline Identity

The guardian-invocation discipline's identity is: "I fill my invocation meter faster, my invocation spells cost less mana, and my charge lasts longer." All four perks serve the same fantasy — making the invocation system more efficient and more available.

The discipline is significantly more expensive to run than the other two Invoker disciplines (drainBase: 8 vs 4 and 6), reflecting that it's a combat-active discipline the player will want to keep running during spire climbs. The high drain creates interesting choices about when to activate it relative to other disciplines competing for the raw mana budget.


8. Store Changes

8.1 Combat Store (combatStore.ts)

New state fields:

// Invocation state
invocationCharge: number;           // 0-100, default 0
activeInvocation: {
  guardianFloor: number;
  spellId: string;
  element: string;
  castProgress: number;             // 0-1, cast progress for invocation spell
} | null;

New actions:

// Reset invocation state (called on spire exit)
resetInvocationState: () => void;

8.2 Combat State Types (combat-state.types.ts)

Add to CombatState:

invocationCharge: number;
activeInvocation: { guardianFloor: number; spellId: string; element: string; castProgress: number } | null;

Add to CombatActions:

resetInvocationState: () => void;

8.3 No Changes To

  • prestigeStore.ts — pact state is unchanged
  • manaStore.ts — mana pools are unchanged
  • gameStore.ts — tick pipeline passes invocation state through combat store

9. New Utility: invocation-utils.ts

A new utility file at src/lib/game/utils/invocation-utils.ts:

// Select the best guardian to channel based on current enemy
export function selectInvocationGuardian(
  signedPacts: number[],
  enemyElements: string[],
): number | null;

// Get all spells a guardian knows (union of their elements' spells)
export function getGuardianSpellbook(
  guardian: GuardianDef,
): SpellDef[];

// Select the best affordable spell from a spellbook
export function selectInvocationSpell(
  spellbook: SpellDef[],
  rawMana: number,
  elements: Record<string, { current: number; max: number; unlocked: boolean }>,
  costMultiplier: number,
): { spellId: string; element: string } | null;

// Compute charge fill rate per tick
export function computeChargeFillRate(
  signedPactsLength: number,
  chargeRateBonus: number,
): number;

// Compute cast speed bonus from pact affinity
export function computeCastSpeedBonus(pactAffinity: number): number;

// Compute effective cost multiplier from discipline bonuses
export function computeCostMultiplier(disciplineEffects: DisciplineBonuses): number;

// Compute drain rate multiplier from discipline bonuses
export function computeDrainRateMultiplier(disciplineEffects: DisciplineBonuses): number;

10. Combat Tick Integration

10.1 Modified Flow in combat-actions.ts

In processCombatTick, after the active spell block and before the equipment spell block, add an invocation block:

1. If activeInvocation !== null:
   a. Drain charge: invocationCharge -= drainPerTick
   b. If charge <= 0: end invocation (but let current cast finish)
   c. Accumulate cast progress for invocation spell
   d. On cast completion:
      - Calculate damage using calcDamage() with invocation spell element
      - Apply damage via applyDamageToRoom()
      - Re-evaluate spell selection (§5.2)
      - If no affordable spell from the invoked guardian: end invocation
   e. If room cleared: end invocation

2. If activeInvocation === null AND invocationCharge >= 100
   AND room has enemies:
   a. Select guardian (§3.2)
   b. Select spell (§5.2)
   c. Set activeInvocation
   d. Log activation

3. If NOT invoking AND charge < 100:
   a. invocationCharge += chargePerTick
   b. Clamp to 100

10.2 Pact Affinity Cast Speed

In the cast progress calculation, apply the cast speed bonus:

const castSpeedBonus = computeCastSpeedBonus(pactAffinity);
const effectiveAttackSpeed = totalAttackSpeed × (1 + castSpeedBonus);
const progressPerTick = HOURS_PER_TICK × spellCastSpeed × effectiveAttackSpeed;

This applies to all cast progress calculations (active, equipment, invocation).


11. UI Changes

11.1 SpireCombatPage

Add an Invocation Panel between the SpireHeader and RoomDisplay sections. The panel shows:

  • Charge meter: Progress bar 0100 with purple color gradient
  • Recharge status: "Recharging..." with current charge value when charge < 100 and not invoking
  • Active invocation display: Guardian name + spell name + element icon when invoking
  • Channeled guardian icon: Small element badges for the guardian's elements

11.2 SpireCombatControls

Add a compact invocation status indicator showing:

  • Charge percentage
  • Whether invocation is active (glowing border when active)
  • Recharge progress when not invoking

11.3 No New Tabs

The invocation system does not require a new tab. All information is visible in the existing SpireCombatPage layout.


12. Data Flow Summary

gameStore.tick()
  → buildTickContext() [snapshots all stores]
  → processCombatTick() [combat-actions.ts]
    → If invoking:
        - Drain invocationCharge
        - Accumulate invocation castProgress
        - On cast: deductSpellCost() at reduced multiplier
        - calcDamage() with invocation spell element
        - applyDamageToRoom()
        - Re-evaluate spell selection
    → If not invoking:
        - Fill invocationCharge
        - Auto-activate when charge >= 100
    → applyTickWrites() [writes combat store changes back]

13. Acceptance Criteria

# Criterion
AC-1 invocationCharge fills passively at baseFillRate × pactCountMultiplier × disciplineMultiplier per tick while in climb action and not invoking.
AC-2 Invocation auto-activates when charge ≥ 100, activeInvocation === null, and room has enemies.
AC-3 The channeled guardian is selected by bestElementalBonus × tierMultiplier scoring.
AC-4 The invocation spell is the highest-damage spell from the guardian's elements that the player can afford at the effective cost multiplier (not limited to spells the player has learned).
AC-5 Invocation spell steps down to the next affordable spell when the current one becomes unaffordable.
AC-6 If no spells are affordable from the invoked guardian's elements, invocation ends.
AC-7 Invocation ends when charge reaches 0, room is cleared, or player leaves climb.
AC-8 A cast already in progress completes even if charge hits 0 during the cast.
AC-9 After invocation ends, charge must fully recharge to 100 before invocation can reactivate.
AC-10 Charge only fills while in climb action.
AC-11 Invocation spell casts in parallel with the player's active spell and equipment spells.
AC-12 Pact Affinity grants a cast speed bonus using MAX_BONUS × (1 - 1 / (1 + pactAffinity × 1.5)), capped at 50%.
AC-13 Cast speed bonus applies to all spell casts (active, equipment, invocation).
AC-14 The guardian-invocation discipline requires at least one signed pact.
AC-15 invocation-efficiency once perk reduces cost multiplier by 0.02.
AC-16 invocation-speed infinite perk grants +0.05 charge rate bonus every 150 XP.
AC-17 invocation-sustain capped perk reduces drain rate multiplier by 0.1 per tier, max 3 tiers.
AC-18 invocation-mastery capped perk reduces cost multiplier by 0.01 per tier, max 3 tiers.
AC-19 Minimum effective cost multiplier is 0.05 (1/20th).
AC-20 Minimum drain rate multiplier is 0.7 (30% slower drain).
AC-21 Invocation state resets on spire exit (invocationCharge = 0, activeInvocation = null).
AC-22 Existing saves without invocation fields get default values (0, null).

14. Files Reference

File Role
src/lib/game/stores/combatStore.ts New invocation state fields + resetInvocationState action
src/lib/game/stores/combat-state.types.ts Type definitions for new state
src/lib/game/stores/combat-actions.ts Invocation processing block in processCombatTick
src/lib/game/utils/invocation-utils.ts NEW — Guardian/spell selection, charge rate, cost multiplier, drain multiplier
src/lib/game/data/disciplines/invoker.ts Add guardian-invocation discipline definition
src/lib/game/effects/discipline-effects.ts Add new stat bonus keys (invocationChargeRateBonus, drainRateMultiplier)
src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx Invocation panel UI
src/components/game/tabs/SpireCombatPage/SpireCombatControls.tsx Compact invocation status indicator
docs/specs/attunements/invoker/systems/invocation-system-spec.md THIS FILE

15. Out of Scope

  • Manual invocation toggle (auto-activate only in v1)
  • Guardian-specific signature spells (guardians know all spells from their elements)
  • Invocation affecting non-combat rooms
  • Pact affinity affecting anything beyond ritual time and cast speed
  • Visual effects / animations (UI-only indicator in v1)
  • Invocation interacting with golem combat