feat: add Invocation System spec for Invoker attunement
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m22s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m22s
- New spec at docs/specs/attunements/invoker/systems/invocation-system-spec.md - Invocation Charge meter (0-100) fills passively, drains while invoking - Channeled guardian auto-casts spells from their elements at reduced cost - Spell selection picks best affordable spell, steps down as mana depletes - No guardian swap mid-invocation; ends when charge depleted or room cleared - Pact Affinity extended with diminishing-returns cast speed bonus (max 50%) - New guardian-invocation discipline with 4 perks (efficiency, speed, sustain, mastery)
This commit is contained in:
@@ -177,25 +177,27 @@ 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:
|
||||
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 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:
|
||||
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
|
||||
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)
|
||||
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 Fallback and Auto-End
|
||||
### 5.3 Auto-End When Unaffordable
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
@@ -207,7 +209,7 @@ 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
|
||||
- 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
|
||||
@@ -439,7 +441,6 @@ export function getGuardianSpellbook(
|
||||
// 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,
|
||||
@@ -476,11 +477,10 @@ spell block, add an **invocation block**:
|
||||
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
|
||||
- If no affordable spell from the invoked guardian: end invocation
|
||||
e. If room cleared: end invocation
|
||||
|
||||
2. If activeInvocation === null AND invocationCharge >= 100
|
||||
@@ -563,9 +563,9 @@ gameStore.tick()
|
||||
| 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-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 guardian has affordable spells, invocation ends. |
|
||||
| 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. |
|
||||
|
||||
Reference in New Issue
Block a user