fix: apply mana drain to conversion disciplines (bug #379)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m24s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m24s
- 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.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# Circular Dependencies
|
||||
Generated: 2026-06-12T10:39:16.751Z
|
||||
Generated: 2026-06-12T12:18:22.789Z
|
||||
Found: 4 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||
|
||||
1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_meta": {
|
||||
"generated": "2026-06-12T10:39:14.588Z",
|
||||
"generated": "2026-06-12T12:18:20.580Z",
|
||||
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||
},
|
||||
|
||||
@@ -24,6 +24,7 @@ Mana-Loop/
|
||||
│ │ │ │ └── fabricator-spec.md
|
||||
│ │ │ ├── invoker/
|
||||
│ │ │ │ ├── systems/
|
||||
│ │ │ │ │ ├── invocation-system-spec.md
|
||||
│ │ │ │ │ └── pact-system-spec.md
|
||||
│ │ │ │ └── invoker-spec.md
|
||||
│ │ │ └── attunement-system-spec.md
|
||||
|
||||
@@ -0,0 +1,611 @@
|
||||
# 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<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
|
||||
Reference in New Issue
Block a user