# 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` | 0–100 | 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: ```typescript 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: ``` 1. Gather all spells from the guardian's elements 2. Filter to spells the player has learned (spells[spellId].learned === true) 3. 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 4. From remaining spells, pick the one with the highest base damage (spell.dmg) 5. If tied, pick the highest tier 6. If still tied, pick the lowest cost (most efficient) ``` ### 5.3 Fallback and Auto-End If no spells are affordable at the effective cost multiplier from the current guardian: 1. Try the next guardian in the scoring order (§3.2) 2. If no guardian has affordable spells, invocation ends (§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 completion, it deducts mana at the effective cost multiplier - 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: ```typescript // 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: ```typescript // Reset invocation state (called on spire exit) resetInvocationState: () => void; ``` ### 8.2 Combat State Types (`combat-state.types.ts`) Add to `CombatState`: ```typescript invocationCharge: number; activeInvocation: { guardianFloor: number; spellId: string; element: string; castProgress: number } | null; ``` Add to `CombatActions`: ```typescript 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`: ```typescript // 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[], playerSpells: Record, rawMana: number, elements: Record, 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: - Deduct mana at effectiveCostMultiplier - Calculate damage using calcDamage() with invocation spell element - Apply damage via applyDamageToRoom() - Re-evaluate spell selection (§5.2) - If no affordable spell from any 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 0–100 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 learned spell the player can afford at the effective cost multiplier. | | AC-5 | Invocation spell steps down to the next affordable spell when the current one becomes unaffordable. | | AC-6 | If no guardian has affordable spells, 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