diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index 3481919..4d91a20 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,5 +1,5 @@ # Circular Dependencies -Generated: 2026-06-13T11:42:23.525Z +Generated: 2026-06-13T15:26:15.912Z Found: 4 circular chain(s) — these MUST be fixed before modifying involved files. 1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index 755a312..f96f0a0 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-06-13T11:42:21.085Z", + "generated": "2026-06-13T15:26:13.488Z", "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." }, @@ -760,6 +760,7 @@ "utils/element-cap-bonus.ts", "utils/element-distance.ts", "utils/index.ts", + "utils/invocation-utils.ts", "utils/safe-persist.ts" ], "stores/gameStore.types.ts": [], diff --git a/docs/specs/attunements/invoker/systems/invocation-system-spec.md b/docs/specs/attunements/invoker/systems/invocation-system-spec.md index 434e5d2..42495e0 100644 --- a/docs/specs/attunements/invoker/systems/invocation-system-spec.md +++ b/docs/specs/attunements/invoker/systems/invocation-system-spec.md @@ -17,7 +17,7 @@ auto-cast elemental spells at a fraction of their normal cost. - 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 +- 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 --- @@ -261,13 +261,13 @@ This gives diminishing returns: ### 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: +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 = totalAttackSpeed × (1 + castSpeedBonus) +effectiveAttackSpeed = baseAttackSpeed × (1 + castSpeedBonus) ``` ### 6.3 Affinity Sources (Unchanged) @@ -497,15 +497,16 @@ spell block, add an **invocation block**: ### 10.2 Pact Affinity Cast Speed -In the cast progress calculation, apply the cast speed bonus: +In the invocation spell cast progress calculation, apply the cast speed bonus: ``` const castSpeedBonus = computeCastSpeedBonus(pactAffinity); -const effectiveAttackSpeed = totalAttackSpeed × (1 + castSpeedBonus); -const progressPerTick = HOURS_PER_TICK × spellCastSpeed × effectiveAttackSpeed; +const effectiveAttackSpeed = baseAttackSpeed × (1 + castSpeedBonus); +const invProgressPerTick = HOURS_PER_TICK × invCastSpeed × effectiveAttackSpeed; ``` -This applies to **all** cast progress calculations (active, equipment, invocation). +This applies to **invocation spells only** (not to the player's active spell or +equipment spells). --- @@ -572,7 +573,7 @@ gameStore.tick() | 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-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. | diff --git a/src/lib/game/stores/combat-actions.ts b/src/lib/game/stores/combat-actions.ts index 7343a87..9e46b43 100644 --- a/src/lib/game/stores/combat-actions.ts +++ b/src/lib/game/stores/combat-actions.ts @@ -92,6 +92,7 @@ export function processCombatTick( bypassBarrier?: boolean, ) => number, equippedSwords?: Record, + pactAffinityUpgrade?: number, ): CombatTickResult { const state = get(); const logMessages: string[] = []; @@ -134,7 +135,6 @@ export function processCombatTick( // ─── Spell casting (only when a valid spell is configured) ──────────────── if (spellDef) { const disciplineEffects = computeDisciplineEffects(); - // AC-13: Pact affinity cast speed bonus already applied to attackSpeedMult by gameStore const totalAttackSpeed = attackSpeedMult; const spellCastSpeed = spellDef.castSpeed || 1; const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed; @@ -263,7 +263,7 @@ export function processCombatTick( // ─── Invocation system (spec §10.1) ─────────────────────────────────── const invResult = processInvocationTick( - { get, set, rawMana, elements, attackSpeedMult, signedPacts, currentRoom, floorHP }, + { get, set, rawMana, elements, attackSpeedMult, signedPacts, currentRoom, floorHP, pactAffinityUpgrade: pactAffinityUpgrade || 0 }, onFloorCleared, onDamageDealt, ); diff --git a/src/lib/game/stores/combat-invocation.ts b/src/lib/game/stores/combat-invocation.ts index 604e481..751d6fd 100644 --- a/src/lib/game/stores/combat-invocation.ts +++ b/src/lib/game/stores/combat-invocation.ts @@ -15,9 +15,9 @@ import { selectInvocationSpell, deductInvocationSpellCost, computeChargeFillRate, + computeCastSpeedBonus, computeCostMultiplier, computeDrainRateMultiplier, - computeDrainPerTick, type ActiveInvocation, } from '../utils/invocation-utils'; @@ -30,6 +30,7 @@ export interface InvocationTickParams { signedPacts: number[]; currentRoom: FloorState; floorHP: number; + pactAffinityUpgrade: number; } export interface InvocationTickResult { @@ -53,7 +54,7 @@ export function processInvocationTick( modifiedDamage?: number; }, ): InvocationTickResult { - const { get, set, rawMana: startRawMana, elements: startElements, attackSpeedMult, signedPacts, currentRoom, floorHP: startFloorHP } = params; + const { get, set, rawMana: startRawMana, elements: startElements, attackSpeedMult, signedPacts, currentRoom, floorHP: startFloorHP, pactAffinityUpgrade } = params; let rawMana = startRawMana; let elements = startElements; let floorHP = startFloorHP; @@ -67,7 +68,7 @@ export function processInvocationTick( if (activeInvocation !== null) { // ── Invocation is active: drain charge and process cast ── const invResult = processActiveInvocation({ - get, set, rawMana, elements, attackSpeedMult, signedPacts, + get, set, rawMana, elements, attackSpeedMult, signedPacts, pactAffinityUpgrade, currentFloor, floorHP, floorMaxHP, currentRoom: currentRoomState, invocationCharge, activeInvocation, logMessages, }, onFloorCleared, onDamageDealt); @@ -138,6 +139,7 @@ interface ActiveInvParams { elements: Record; attackSpeedMult: number; signedPacts: number[]; + pactAffinityUpgrade: number; currentFloor: number; floorHP: number; floorMaxHP: number; @@ -156,7 +158,7 @@ function processActiveInvocation( modifiedDamage?: number; }, ): InvocationTickResult { - let { get, set, rawMana, elements, attackSpeedMult, signedPacts } = p; + let { get, set, rawMana, elements, attackSpeedMult, signedPacts, pactAffinityUpgrade } = p; let { currentFloor, floorHP, floorMaxHP, currentRoom } = p; let { invocationCharge, activeInvocation } = p; const logMessages = p.logMessages; @@ -171,9 +173,13 @@ function processActiveInvocation( const drainPerTick = computeDrainPerTick(invSpellDef.cost.amount, drainMult); invocationCharge = Math.max(0, invocationCharge - drainPerTick); - // Cast progress uses attackSpeedMult which already includes pact affinity bonus (AC-12/AC-13) + // AC-12/AC-13: Apply pact affinity cast speed bonus to invocation spells only + // pactAffinityUpgrade is the prestige level (0-9), each level = +0.1; pactAffinityBonus comes from discipline + const pactAffinity = pactAffinityUpgrade * 0.1 + (disciplineEffects.bonuses.pactAffinityBonus || 0); + const castSpeedBonus = computeCastSpeedBonus(pactAffinity); + const effectiveAttackSpeed = attackSpeedMult * (1 + castSpeedBonus); const invCastSpeed = invSpellDef.castSpeed || 1; - const invProgressPerTick = HOURS_PER_TICK * invCastSpeed * attackSpeedMult; + const invProgressPerTick = HOURS_PER_TICK * invCastSpeed * effectiveAttackSpeed; const newCastProgress = activeInvocation.castProgress + invProgressPerTick; if (newCastProgress >= 1 && floorHP > 0) { diff --git a/src/lib/game/stores/gameStore.ts b/src/lib/game/stores/gameStore.ts index 9b7bab4..0dfa30b 100644 --- a/src/lib/game/stores/gameStore.ts +++ b/src/lib/game/stores/gameStore.ts @@ -32,7 +32,7 @@ import type { TickContext, TickWrites } from './tick-pipeline'; import type { GameCoordinatorState } from './gameStore.types'; import type { EnemyState } from '../types'; import { applyEnemyDefenses as applyEnemyDefensesFromPipeline } from './pipelines/combat-tick'; -import { computeAttackSpeedMultFromPactAffinity } from '../utils/invocation-utils'; + // Track paused conversions already logged to avoid flooding the activity log every tick const loggedPausedConversions = new Set(); @@ -290,11 +290,9 @@ export const useGameStore = create()( })); useCombatStore.setState({ equipmentSpellStates }); - // AC-12/AC-13: Pact affinity cast speed bonus (prestige + discipline) - const attackSpeedMult = computeAttackSpeedMultFromPactAffinity( - ctx.prestige.prestigeUpgrades.pactAffinity || 0, - disciplineEffects.bonuses.pactAffinityBonus || 0, - ); + // Pact affinity cast speed bonus only applies to invocation spells (AC-12/AC-13), + // computed locally in combat-invocation.ts. Active/equipment spells use base 1.0. + const attackSpeedMult = 1; const cr = useCombatStore.getState().processCombatTick( rawMana, elements, maxMana, attackSpeedMult, combatCbs.onFloorCleared, @@ -305,6 +303,7 @@ export const useGameStore = create()( (dmg, enemy, roomType, addLogFn, bypassArmor, bypassBarrier) => applyEnemyDefensesFromPipeline(dmg, enemy, roomType, addLogFn, bypassArmor, bypassBarrier), equippedSwords, + ctx.prestige.prestigeUpgrades.pactAffinity || 0, ); rawMana = cr.rawMana; elements = cr.elements; totalManaGathered += cr.totalManaGathered || 0; diff --git a/src/lib/game/utils/invocation-utils.ts b/src/lib/game/utils/invocation-utils.ts index c485fc5..48e0059 100644 --- a/src/lib/game/utils/invocation-utils.ts +++ b/src/lib/game/utils/invocation-utils.ts @@ -198,21 +198,6 @@ export function computeDrainRateMultiplier(disciplineBonuses: DisciplineBonuses) return Math.max(MIN_DRAIN_MULTIPLIER, BASE_DRAIN_MULTIPLIER + reduction); } -// ─── Pact Affinity Attack Speed ─────────────────────────────────────────────── - -/** - * Compute the effective attack speed multiplier from pact affinity. - * Combines prestige upgrade + discipline bonus, applies diminishing returns formula. - * Used by gameStore to compute the attackSpeedMult passed to processCombatTick. - */ -export function computeAttackSpeedMultFromPactAffinity( - pactAffinityUpgrade: number, - pactAffinityBonus: number, -): number { - const total = pactAffinityUpgrade + pactAffinityBonus; - return 1 * (1 + computeCastSpeedBonus(total)); -} - // ─── Drain Per Tick ──────────────────────────────────────────────────────────── /**