505481cefc
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
Previously (commit 62638d6), the pact affinity cast speed bonus was applied
to ALL spell casts (active, equipment, invocation). Per design intent, it
should only apply to invocation system spells.
Changes:
- invocation-system-spec.md: Update §6.2, §10.2, AC-13 to clarify bonus
applies to invocation spells only
- gameStore.ts: Stop computing pact affinity bonus into attackSpeedMult;
pass base 1.0 instead
- combat-actions.ts: Remove stale AC-13 comment
- combat-invocation.ts: Compute pact affinity cast speed bonus locally
for invocation spells only, using prestige upgrade level + discipline bonus
- invocation-utils.ts: Remove computeAttackSpeedMultFromPactAffinity (no
longer needed)
All 1235 tests pass.
613 lines
22 KiB
Markdown
613 lines
22 KiB
Markdown
# 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 for invocation spells) 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.
|
||
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 **invocation spells only** — the spells auto-cast
|
||
by the Invocation system while channeling a guardian. It does **not** apply to the
|
||
player's active spell or equipment spells. It is applied as a multiplier to the
|
||
attack speed used in the invocation spell's cast progress calculation:
|
||
|
||
```
|
||
effectiveAttackSpeed = baseAttackSpeed × (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[],
|
||
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 invocation spell cast progress calculation, apply the cast speed bonus:
|
||
|
||
```
|
||
const castSpeedBonus = computeCastSpeedBonus(pactAffinity);
|
||
const effectiveAttackSpeed = baseAttackSpeed × (1 + castSpeedBonus);
|
||
const invProgressPerTick = HOURS_PER_TICK × invCastSpeed × effectiveAttackSpeed;
|
||
```
|
||
|
||
This applies to **invocation spells only** (not to the player's active spell or
|
||
equipment spells).
|
||
|
||
---
|
||
|
||
## 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 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 invocation spells only (not active/equipment spells). |
|
||
| 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
|