diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index c1db7f5..7f8509c 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,8 +1,9 @@ # Circular Dependencies -Generated: 2026-06-08T18:36:58.404Z -Found: 1 circular chain(s) — these MUST be fixed before modifying involved files. +Generated: 2026-06-08T20:08:33.456Z +Found: 2 circular chain(s) — these MUST be fixed before modifying involved files. 1. 1) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts +2. 2) stores/combatStore.ts > stores/combat-descent-actions.ts > stores/attunementStore.ts ## How to fix 1. Identify which import in the chain can be extracted to a shared types/utils file. diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index 26918f1..50a6a3b 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-06-08T18:36:56.442Z", + "generated": "2026-06-08T20:08:31.411Z", "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." }, @@ -539,6 +539,7 @@ ], "stores/attunementStore.ts": [ "data/attunements.ts", + "stores/combatStore.ts", "types.ts", "utils/safe-persist.ts" ], diff --git a/docs/specs/attunements/invoker/systems/pact-system-spec.md b/docs/specs/attunements/invoker/systems/pact-system-spec.md index 9e8997a..b538d10 100644 --- a/docs/specs/attunements/invoker/systems/pact-system-spec.md +++ b/docs/specs/attunements/invoker/systems/pact-system-spec.md @@ -259,8 +259,8 @@ guardian's `unlocksMana` is derived from `resolveMultiUnlockChain(element)`: | 110 | lightning | `fire`, `air`, `lightning` | | 120 | frost | `air`, `water`, `frost` | | 130 | blackflame | `fire`, `earth`, `metal` | -| 140 | radiantflames | `light`, `fire` | -| 150 | miasma | `air`, `death` | +| 140 | radiantflames | `light`, `fire`, `radiantflames` | +| 150 | miasma | `air`, `death`, `miasma` | | 160 | shadowglass | `earth`, `dark` | | 170+ | exotic | varies (see guardian-data.ts) | @@ -298,8 +298,8 @@ mana to any elemental type. All elemental mana must come from: | 110 | lightning | 22% | 13h | | 120 | frost | 28% | 14h | | 130 | blackflame | 32% | 15h | -| 140 | sand+earth+water | 25% | 16h | -| 150 | lightning+fire+air | 28% | 17h | +| 140 | light+fire+radiantflames | 25% | 16h | +| 150 | air+death+miasma | 28% | 17h | | 160 | shadowglass | 33% | 18h | ### 8.3 Tier 3 — Exotic Elements (Floors 170–240) diff --git a/src/lib/game/__tests__/spire-utils.test.ts b/src/lib/game/__tests__/spire-utils.test.ts index bf24a98..9717594 100644 --- a/src/lib/game/__tests__/spire-utils.test.ts +++ b/src/lib/game/__tests__/spire-utils.test.ts @@ -273,8 +273,8 @@ describe('getGuardianForFloor (unified lookup)', () => { it('should return multi-element guardians for tier 3 floors', () => { expect(getGuardianForFloor(130)!.element).toEqual(['metal', 'fire', 'earth']); - expect(getGuardianForFloor(140)!.element).toEqual(['sand', 'earth', 'water']); - expect(getGuardianForFloor(150)!.element).toEqual(['lightning', 'fire', 'air']); + expect(getGuardianForFloor(140)!.element).toEqual(['light', 'fire', 'radiantflames']); + expect(getGuardianForFloor(150)!.element).toEqual(['air', 'death', 'miasma']); }); it('should return exotic guardians for floors 170-200', () => { diff --git a/src/lib/game/data/guardian-data.ts b/src/lib/game/data/guardian-data.ts index d2bd9ad..d7023d3 100644 --- a/src/lib/game/data/guardian-data.ts +++ b/src/lib/game/data/guardian-data.ts @@ -165,7 +165,7 @@ const TIER2: Record = { 90: mk(90, '', ['metal'], '#BDC3C7', 0.30, 3.5, [ { type: 'elementalDamage', value: 15, desc: '+15% Metal damage' }, - { type: 'maxMana', value: 150, desc: '+150 max mana' }, + { type: 'critChance', value: 8, desc: '+8% crit chance' }, ], 'Metal spells pierce 20% armor', [{ type: 'armor_pierce', value: 0.2 }], @@ -183,7 +183,7 @@ const TIER2: Record = { 110: mk(110, '', ['lightning'], '#FFEB3B', 0.22, 4.0, [ { type: 'elementalDamage', value: 15, desc: '+15% Lightning damage' }, - { type: 'castingSpeed', value: 15, desc: '+15% casting speed' }, + { type: 'manaGain', value: 10, desc: '+10% mana gain' }, ], 'Lightning spells chain to 2 additional targets', [{ type: 'chain', value: 2 }], @@ -207,21 +207,21 @@ const TIER2: Record = { [{ type: 'curse', value: 0.15 }, { type: 'burn', value: 0.15 }, { type: 'armor_pierce', value: 0.2 }], { shield: 1000, shieldRegen: 25, healthRegen: 5, healthRegenIsPercent: true }, ), - 140: mk(140, '', ['sand', 'earth', 'water'], '#FFAA33', 0.25, 4.75, + 140: mk(140, '', ['light', 'fire', 'radiantflames'], '#FFAA33', 0.25, 4.75, [ - { type: 'elementalDamage', value: 15, desc: '+15% Radiant Earth damage' }, + { type: 'elementalDamage', value: 15, desc: '+15% Radiant Flames damage' }, { type: 'maxMana', value: 200, desc: '+200 max mana' }, ], - 'Radiant Earth spells blind enemies, reducing their accuracy and damage by 15%', + 'Radiant Flames spells blind enemies, reducing their accuracy and damage by 15%', [{ type: 'blind', value: 0.15 }, { type: 'armor_pierce', value: 0.1 }], { barrier: 0.12, barrierRegen: 0.03, healthRegen: 6, healthRegenIsPercent: true }, ), - 150: mk(150, '', ['lightning', 'fire', 'air'], '#6B8E23', 0.28, 5.0, + 150: mk(150, '', ['air', 'death', 'miasma'], '#6B8E23', 0.28, 5.0, [ - { type: 'elementalDamage', value: 15, desc: '+15% Storm Lightning damage' }, + { type: 'elementalDamage', value: 15, desc: '+15% Miasma damage' }, { type: 'castingSpeed', value: 15, desc: '+15% casting speed' }, ], - 'Storm Lightning spells corrode armor and spread chain lightning in swarm rooms', + 'Miasma spells corrode armor and spread toxic clouds in swarm rooms', [{ type: 'chain', value: 2 }, { type: 'cast_speed', value: 0.1 }, { type: 'burn', value: 0.1 }], { shield: 1100, shieldRegen: 28, barrier: 0.05, barrierRegen: 0.01 }, ), @@ -273,7 +273,7 @@ const TIER3: Record = { 200: mk(200, '', ['crystal', 'stellar', 'void'], '#E8D5F5', 0.35, 7.0, [ { type: 'elementalDamage', value: 30, desc: '+30% Exotic damage' }, - { type: 'maxMana', value: 400, desc: '+400 max mana' }, + { type: 'prestigeInsight', value: 50, desc: '+50 prestige insight' }, ], 'Exotic convergence: Crystal/Stellar/Void spells bypass all defenses and shields', [{ type: 'defense_pierce', value: 0.3 }, { type: 'resist_ignore', value: 0.2 }, { type: 'reflect', value: 0.1 }], @@ -291,7 +291,7 @@ const TIER3: Record = { 220: mk(220, '', ['plasma'], '#FF6B9D', 0.28, 8.0, [ { type: 'elementalDamage', value: 25, desc: '+25% Plasma damage' }, - { type: 'manaRegen', value: 2.5, desc: '+2.5 mana regen' }, + { type: 'studySpeed', value: 15, desc: '+15% study speed' }, ], 'Plasma spells chain to 3 targets with 30% damage each', [{ type: 'chain', value: 3 }, { type: 'burn', value: 0.1 }], diff --git a/src/lib/game/stores/gameStore.ts b/src/lib/game/stores/gameStore.ts index b1999f1..4cb300b 100644 --- a/src/lib/game/stores/gameStore.ts +++ b/src/lib/game/stores/gameStore.ts @@ -209,9 +209,11 @@ export const useGameStore = create()( rawMana = Math.max(0, Math.min(rawMana + netRawRegen * HOURS_PER_TICK, maxMana)); let totalManaGathered = ctx.mana.totalManaGathered + Math.max(0, actualRegen); - const pactResult = processPactRitual(ctx.prestige.pactRitualFloor, ctx.prestige.pactRitualProgress, ctx.prestige.signedPacts, ctx.prestige.defeatedGuardians, ctx.prestige.prestigeUpgrades.pactAffinity || 0, disciplineEffects.bonuses.pactAffinityBonus || 0, ctx.prestige.signedPactDetails, day, hour); + const pactResult = processPactRitual(ctx.prestige.pactRitualFloor, ctx.prestige.pactRitualProgress, ctx.prestige.prestigeUpgrades.pactAffinity || 0, disciplineEffects.bonuses.pactAffinityBonus || 0); if (pactResult.writes) writes.prestige = { ...(writes.prestige || {}), ...pactResult.writes }; - pactResult.logs.forEach(l => addLog(l)); + if (pactResult.completed) { + usePrestigeStore.getState().completePactRitual(addLog); + } const dr = useDisciplineStore.getState().processTick({ rawMana, elements }); rawMana = dr.rawMana; elements = dr.elements; diff --git a/src/lib/game/stores/pipelines/pact-ritual.ts b/src/lib/game/stores/pipelines/pact-ritual.ts index 8451335..6943abe 100644 --- a/src/lib/game/stores/pipelines/pact-ritual.ts +++ b/src/lib/game/stores/pipelines/pact-ritual.ts @@ -1,76 +1,48 @@ // ─── Pact Ritual Pipeline Phase ─────────────────────────────────────────────── // Processes pact ritual signing during the game tick. +// Progress advancement only — completion is delegated to prestigeStore.completePactRitual(). -import { useManaStore } from '../manaStore'; import { getGuardianForFloor } from '../../data/guardian-encounters'; import { HOURS_PER_TICK } from '../../constants'; -import type { PrestigeState } from '../prestigeStore'; export interface PactRitualResult { + /** Null when no ritual is in progress. */ writes: { - signedPacts?: number[]; - defeatedGuardians?: number[]; - signedPactDetails?: PrestigeState['signedPactDetails']; pactRitualFloor: number | null; pactRitualProgress: number; } | null; - logs: string[]; + /** True when the ritual reached completion threshold this tick. */ + completed: boolean; } /** - * Process pact ritual progression. Advances progress and completes signing - * when enough enough hours have accumulated. + * Process pact ritual progression. Advances progress each tick. + * Returns `completed: true` when enough hours have accumulated; + * the caller (gameStore tick) must then invoke + * `usePrestigeStore.getState().completePactRitual(addLog)` to + * finalise signing — this avoids duplicating the completion logic. */ export function processPactRitual( pactRitualFloor: number | null, pactRitualProgress: number, - signedPacts: number[], - defeatedGuardians: number[], pactAffinityUpgrade: number, pactAffinityBonus: number, - signedPactDetails: PrestigeState['signedPactDetails'], - currentDay: number, - currentHour: number, ): PactRitualResult { - if (pactRitualFloor === null) return { writes: null, logs: [] }; - const logs: string[] = []; + if (pactRitualFloor === null) return { writes: null, completed: false }; + const guardian = getGuardianForFloor(pactRitualFloor); - if (!guardian) return { writes: null, logs: [] }; + if (!guardian) return { writes: null, completed: false }; const pactAffinity = Math.min(0.9, pactAffinityUpgrade * 0.1 + pactAffinityBonus); const requiredTime = guardian.pactTime * (1 - pactAffinity); if (pactRitualProgress + HOURS_PER_TICK >= requiredTime) { - logs.push(`📜 Pact signed with ${guardian.name}! You have gained their boons.`); - const manaStore = useManaStore.getState(); - for (const manaType of guardian.unlocksMana || []) { - const result = manaStore.unlockElement(manaType, 0); - if (result.success) { - logs.push(`✨ ${manaType.charAt(0).toUpperCase() + manaType.slice(1)} mana unlocked!`); - } - } - return { - writes: { - signedPacts: [...signedPacts, pactRitualFloor], - defeatedGuardians: defeatedGuardians.filter(f => f !== pactRitualFloor), - signedPactDetails: { - ...signedPactDetails, - [pactRitualFloor]: { - floor: pactRitualFloor, - guardianId: guardian.name || `floor-${pactRitualFloor}`, - signedAt: { day: currentDay, hour: currentHour }, - skillLevels: {}, - }, - }, - pactRitualFloor: null, - pactRitualProgress: 0, - }, - logs, - }; + // Signal completion — state writes happen inside completePactRitual() + return { writes: null, completed: true }; } return { - writes: { pactRitualFloor, pactRitualProgress: pactRitualProgress + HOURS_PER_TICK, signedPacts, defeatedGuardians }, - logs, + writes: { pactRitualFloor, pactRitualProgress: pactRitualProgress + HOURS_PER_TICK }, + completed: false, }; } diff --git a/src/lib/game/stores/prestigeStore.ts b/src/lib/game/stores/prestigeStore.ts index 74ae7df..ef28928 100644 --- a/src/lib/game/stores/prestigeStore.ts +++ b/src/lib/game/stores/prestigeStore.ts @@ -246,6 +246,8 @@ export const usePrestigeStore = create()( pactRitualFloor: null, pactRitualProgress: 0, loopInsight: 0, + // NOTE: signedPactDetails is intentionally NOT reset here. + // Per spec §5.1, it persists across loops for historical tracking. }); },