- Remove isConversionDiscipline guard in discipline-slice.ts:processTick that was skipping mana drain for all 24 conversion disciplines - Update test to verify conversion disciplines DO drain mana - Add regression tests for auto-pause and XP accrual on conversion disciplines - Fix misleading comments in elemental-regen.ts and elemental-regen-advanced.ts All 1196 tests pass.
22 KiB
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.45chargePerTick = 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-invocationdiscipline 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:
invocationCharge >= 100activeInvocation === null- Player is in
climbaction (combat) - Current room has living enemies
On activation:
invocationChargebegins draining (see §4)- A guardian is selected (see §3.2)
- A spell is selected from that guardian's elements (see §5)
- 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:
invocationChargereaches 0 (depleted)- Current room is cleared (all enemies defeated)
- Player leaves
climbaction - Player cannot afford any spell from any signed pact's elements (see §5.3)
On end:
- Set
activeInvocation = null - 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:
- Try the next guardian in the scoring order (§3.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:
- The player's active spell (primary cast progress)
- Equipment spell states (array of concurrent spells)
In processCombatTick, the invocation spell is processed similarly to equipment
spells:
- It has its own
castProgressaccumulator - It uses the same
HOURS_PER_TICK × spellCastSpeed × effectiveAttackSpeedprogress 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 toinvocationChargeRateBonus. 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): ReducesdrainRateMultiplierby 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 withinvocation-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 unchangedmanaStore.ts— mana pools are unchangedgameStore.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[],
playerSpells: Record<string, SpellState>,
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:
- 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