feat: add Invocation System spec for Invoker attunement
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:
2026-06-13 13:02:21 +02:00
parent 99a5f498c0
commit 7dda515a71
3 changed files with 20 additions and 20 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# Circular Dependencies # Circular Dependencies
Generated: 2026-06-12T17:00:42.616Z Generated: 2026-06-12T17:03:20.112Z
Found: 4 circular chain(s) — these MUST be fixed before modifying involved files. Found: 4 circular chain(s) — these MUST be fixed before modifying involved files.
1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts 1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"_meta": { "_meta": {
"generated": "2026-06-12T17:00:40.332Z", "generated": "2026-06-12T17:03:17.869Z",
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.", "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." "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."
}, },
@@ -177,25 +177,27 @@ spell in `SPELLS_DEF` where `spell.elem` is `'metal'`, `'fire'`, or `'earth'`.
### 5.2 Spell Selection Algorithm ### 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 1. Gather all spells from the invoked guardian's elements (the full guardian spellbook)
2. Filter to spells the player has learned (spells[spellId].learned === true) 2. Filter to spells the player can afford at the effective cost multiplier:
3. Filter to spells the player can afford at the effective cost multiplier:
- For raw cost: rawMana >= cost.amount × effectiveCostMultiplier - For raw cost: rawMana >= cost.amount × effectiveCostMultiplier
- For element cost: elements[element].current >= 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) 3. From remaining spells, pick the one with the highest base damage (spell.dmg)
5. If tied, pick the highest tier 4. If tied, pick the highest tier
6. If still tied, pick the lowest cost (most efficient) 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 The invoked guardian is fixed for the entire invocation — the player cannot swap
guardian: to a different guardian mid-invocation. If the player cannot afford **any** spell
1. Try the next guardian in the scoring order (§3.2) from the **invoked guardian's** elements at the effective cost multiplier,
2. If no guardian has affordable spells, invocation ends (§3.3, condition 4) invocation ends immediately (§3.3, condition 4).
### 5.4 Invocation Spell Cast Slot ### 5.4 Invocation Spell Cast Slot
@@ -207,7 +209,7 @@ In `processCombatTick`, the invocation spell is processed similarly to equipment
spells: spells:
- It has its own `castProgress` accumulator - It has its own `castProgress` accumulator
- It uses the same `HOURS_PER_TICK × spellCastSpeed × effectiveAttackSpeed` progress formula - 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 - 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 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 // Select the best affordable spell from a spellbook
export function selectInvocationSpell( export function selectInvocationSpell(
spellbook: SpellDef[], spellbook: SpellDef[],
playerSpells: Record<string, SpellState>,
rawMana: number, rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>, elements: Record<string, { current: number; max: number; unlocked: boolean }>,
costMultiplier: number, costMultiplier: number,
@@ -476,11 +477,10 @@ spell block, add an **invocation block**:
b. If charge <= 0: end invocation (but let current cast finish) b. If charge <= 0: end invocation (but let current cast finish)
c. Accumulate cast progress for invocation spell c. Accumulate cast progress for invocation spell
d. On cast completion: d. On cast completion:
- Deduct mana at effectiveCostMultiplier
- Calculate damage using calcDamage() with invocation spell element - Calculate damage using calcDamage() with invocation spell element
- Apply damage via applyDamageToRoom() - Apply damage via applyDamageToRoom()
- Re-evaluate spell selection (§5.2) - 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 e. If room cleared: end invocation
2. If activeInvocation === null AND invocationCharge >= 100 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-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-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-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-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-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-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-9 | After invocation ends, charge must fully recharge to 100 before invocation can reactivate. |