diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index 54551af..7cc0189 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,8 +1,8 @@ # Circular Dependencies -Generated: 2026-05-20T11:01:19.491Z +Generated: 2026-05-20T13:20:47.227Z Found: 1 circular chain(s) — these MUST be fixed before modifying involved files. -1. Processed 125 files (1.3s) (4 warnings) +1. Processed 125 files (1.4s) (4 warnings) ## 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 b871b1d..a1bb1dc 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-05-20T11:01:18.069Z", + "generated": "2026-05-20T13:20:45.668Z", "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." }, diff --git a/src/app/components/LeftPanel.tsx b/src/app/components/LeftPanel.tsx index 8598606..d7691fc 100644 --- a/src/app/components/LeftPanel.tsx +++ b/src/app/components/LeftPanel.tsx @@ -9,7 +9,7 @@ import { ActionButtons } from '@/components/game'; import { AttunementStatus } from '@/components/game/AttunementStatus'; import { ActivityLogPanel } from '@/components/game/ActivityLogPanel'; import { DebugName } from '@/components/game/debug/debug-context'; -import { useGameStore, useManaStore, useCombatStore, useCraftingStore, usePrestigeStore, useDisciplineStore } from '@/lib/game/stores'; +import { useGameStore, useManaStore, useCombatStore, useCraftingStore, usePrestigeStore } from '@/lib/game/stores'; import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects'; import { getUnifiedEffects } from '@/lib/game/effects'; import { getMeditationBonus, getIncursionStrength } from '@/lib/game/stores'; @@ -57,7 +57,7 @@ export function LeftPanel() { const maxMana = computeTotalMaxMana({ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects); const baseRegen = computeTotalRegen({ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects); const clickMana = computeTotalClickMana({ skills: {}, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects); - const meditationMultiplier = getMeditationBonus(meditateTicks, {}, (upgradeEffects as any).meditationEfficiency); + const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency); const incursionStrength = getIncursionStrength(useGameStore((s) => s.day), useGameStore((s) => s.hour)); const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier; diff --git a/src/app/page.tsx b/src/app/page.tsx index 031ecc4..5dcd6b6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -11,7 +11,6 @@ import { useCombatStore, usePrestigeStore, useCraftingStore, - useDisciplineStore, fmt, computeMaxMana, computeRegen, @@ -21,10 +20,11 @@ import { } from '@/lib/game/stores'; import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects'; import { useGameLoop } from '@/lib/game/stores/gameHooks'; -import { getUnifiedEffects } from '@/lib/game/effects'; +import { getUnifiedEffects, type UnifiedEffects } from '@/lib/game/effects'; import { SPELLS_DEF } from '@/lib/game/constants'; import { TimeDisplay } from '@/components/game'; import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects'; +import type { SpellDef } from '@/lib/game/types'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; @@ -62,9 +62,9 @@ const TabLoadingFallback = () =>
// ============================================================================ function GrimoireTab() { - const [grimoireSpells, setGrimoireSpells] = useState(() => { + const [grimoireSpells, setGrimoireSpells] = useState<[string, SpellDef][]>(() => { if (typeof window !== 'undefined' && SPELLS_DEF) { - return Object.values(SPELLS_DEF).filter((s: any) => s.grimoire); + return Object.entries(SPELLS_DEF).filter((entry): entry is [string, SpellDef] => !!entry[1].grimoire); } return []; }); @@ -94,26 +94,26 @@ function GrimoireTab() {
- {grimoireSpells.map((spell: any) => ( + {grimoireSpells.map(([id, spell]) => (
{spell.name} - {spell.element} + {spell.elem}
-

{spell.desc}

+ {spell.desc &&

{spell.desc}

}
Cost: {spell.cost.amount} { spell.cost.type === 'element' ? spell.cost.element : 'raw mana' }
-
Power: {spell.power}
- {spell.effect &&
Effect: {spell.effect}
} +
Power: {spell.dmg}
+ {spell.effects && spell.effects.length > 0 &&
Effects: {spell.effects.map(e => e.type).join(', ')}
}
))} @@ -155,15 +155,14 @@ export default function ManaLoopGame() { }); // Compute discipline bonuses from active disciplines - const disciplineStoreState = useDisciplineStore(); - const disciplineEffects = computeDisciplineEffects(disciplineStoreState as any); + const disciplineEffects = computeDisciplineEffects(); const maxMana = computeMaxMana({ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {} - }, upgradeEffects as any, disciplineEffects); + }, upgradeEffects, disciplineEffects); const baseRegen = computeRegen({ skills: {}, @@ -171,25 +170,25 @@ export default function ManaLoopGame() { skillUpgrades: {}, skillTiers: {}, attunements: {}, - }, upgradeEffects as any, disciplineEffects); + }, upgradeEffects, disciplineEffects); const clickMana = computeClickMana({ skills: {}, }, disciplineEffects); - const meditationMultiplier = getMeditationBonus(meditateTicks, {}, (upgradeEffects as any).meditationEfficiency); + const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency); const incursionStrength = getIncursionStrength(day, hour); // Effective regen with incursion penalty const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength); // Mana Cascade bonus - const manaCascadeBonus = hasSpecial(upgradeEffects as any, SPECIAL_EFFECTS.MANA_CASCADE) + const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE) ? Math.floor(maxMana / 100) * 0.1 : 0; // Mana Waterfall bonus - const manaWaterfallBonus = hasSpecial(upgradeEffects as any, SPECIAL_EFFECTS.MANA_WATERFALL) + const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL) ? Math.floor(maxMana / 100) * 0.25 : 0; diff --git a/src/components/game/crafting/EnchantmentDesigner.tsx b/src/components/game/crafting/EnchantmentDesigner.tsx index d38b1ee..0e5cb10 100644 --- a/src/components/game/crafting/EnchantmentDesigner.tsx +++ b/src/components/game/crafting/EnchantmentDesigner.tsx @@ -5,7 +5,7 @@ import { GameCard } from '@/components/ui/game-card'; import { Separator } from '@/components/ui/separator'; import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment'; import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects'; -import type { EquipmentInstance, EnchantmentDesign, DesignEffect, EquipmentCraftingProgress } from '@/lib/game/types'; +import type { EquipmentInstance, EnchantmentDesign, DesignEffect, EquipmentCraftingProgress, EquipmentCategory } from '@/lib/game/types'; import type { EnchantmentDesignerProps } from './EnchantmentDesigner/types'; import { EquipmentTypeSelector } from './EnchantmentDesigner/EquipmentTypeSelector'; import { EffectSelector } from './EnchantmentDesigner/EffectSelector'; @@ -85,7 +85,7 @@ export function EnchantmentDesigner({ const ownedEquipmentTypes = getOwnedEquipmentTypes(equipmentInstances); // Get the reason why an effect is incompatible - const getIncompatibilityReasonWrapper = (effect: { id: string; name: string; description: string; allowedEquipmentCategories: any[] }) => { + const getIncompatibilityReasonWrapper = (effect: { id: string; name: string; description: string; allowedEquipmentCategories: EquipmentCategory[] }) => { return getIncompatibilityReason(effect, selectedEquipmentType); }; diff --git a/src/components/game/tabs/SpireCombatPage/RoomDisplay.tsx b/src/components/game/tabs/SpireCombatPage/RoomDisplay.tsx index 54957f2..19e78db 100644 --- a/src/components/game/tabs/SpireCombatPage/RoomDisplay.tsx +++ b/src/components/game/tabs/SpireCombatPage/RoomDisplay.tsx @@ -1,6 +1,6 @@ 'use client'; -import type { FloorState, EnemyState } from '@/lib/game/types'; +import type { FloorState, EnemyState, RoomType } from '@/lib/game/types'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; @@ -73,14 +73,14 @@ function EnemyRow({ enemy, floor }: { enemy: EnemyState; floor: number }) { } export function RoomDisplay({ floorState, floor }: RoomDisplayProps) { - const roomDisplay = getSpireRoomTypeDisplay(floorState.roomType as any); + const roomDisplay = getSpireRoomTypeDisplay(floorState.roomType as RoomType); // Handle special room types (cast to string for extended types) const rt = floorState.roomType as string; if (rt === 'recovery') { - const progress = (floorState as any).puzzleProgress || 0; - const required = (floorState as any).puzzleRequired || 1; + const progress = floorState.puzzleProgress || 0; + const required = floorState.puzzleRequired || 1; return ( diff --git a/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx b/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx index fa344ef..418f2a2 100644 --- a/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx +++ b/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx @@ -78,8 +78,7 @@ export function SpireCombatPage() { const equipmentInstances = useCraftingStore((s) => s.equipmentInstances); // Discipline effects - const disciplineStoreState = useDisciplineStore(); - const disciplineEffects = computeDisciplineEffects(disciplineStoreState as any); + const disciplineEffects = computeDisciplineEffects(); // Compute derived stats const upgradeEffects = getUnifiedEffects({ @@ -94,7 +93,7 @@ export function SpireCombatPage() { prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, - }, upgradeEffects as any, disciplineEffects); + }, upgradeEffects, disciplineEffects); const baseRegen = computeRegen({ skills: {}, @@ -102,7 +101,7 @@ export function SpireCombatPage() { skillUpgrades: {}, skillTiers: {}, attunements: {}, - }, upgradeEffects as any, disciplineEffects); + }, upgradeEffects, disciplineEffects); // Total rooms for current floor const totalRooms = useMemo(() => getRoomsForFloor(currentFloor), [currentFloor]); diff --git a/src/components/game/tabs/StatsTab.tsx b/src/components/game/tabs/StatsTab.tsx index 56461c7..056881b 100755 --- a/src/components/game/tabs/StatsTab.tsx +++ b/src/components/game/tabs/StatsTab.tsx @@ -28,7 +28,19 @@ export function StatsTab() { effectiveRegen={manaStats.effectiveRegen} clickMana={manaStats.clickMana} meditationMultiplier={manaStats.meditationMultiplier} - upgradeEffects={manaStats.upgradeEffects} + upgradeEffects={{ + ...manaStats.upgradeEffects, + incursionStrength: manaStats.incursionStrength, + rawMana: manaStats.maxMana, + hasSteadyStream: manaStats.hasSteadyStream, + hasManaTorrent: manaStats.hasManaTorrent, + hasDesperateWells: manaStats.hasDesperateWells, + manaCascadeBonus: manaStats.manaCascadeBonus, + manaWaterfallBonus: manaStats.manaWaterfallBonus, + hasFlowSurge: manaStats.hasFlowSurge, + hasManaOverflow: manaStats.hasManaOverflow, + hasEternalFlow: manaStats.hasEternalFlow, + }} elemMax={elemMax} />
Unlocked Elements: - {Object.values(elements || {}).filter((e: any) => e.unlocked).length} / {Object.keys(ELEMENTS).length} + {Object.values(elements || {}).filter((e: ElementState) => e.unlocked).length} / {Object.keys(ELEMENTS).length}
Elemental Mana Pools:
- {Object.entries(elements) - .filter(([, state]: [string, any]) => state.unlocked) - .map(([id, state]: [string, any]) => { + {Object.entries(elements || {}) + .filter((entry): entry is [string, ElementState] => entry[1].unlocked) + .map(([id, state]) => { const def = ELEMENTS[id]; return (
diff --git a/src/components/game/tabs/StatsTab/LoopStatsSection.tsx b/src/components/game/tabs/StatsTab/LoopStatsSection.tsx index d7cf77c..405f34f 100644 --- a/src/components/game/tabs/StatsTab/LoopStatsSection.tsx +++ b/src/components/game/tabs/StatsTab/LoopStatsSection.tsx @@ -5,6 +5,7 @@ import { Separator } from '@/components/ui/separator'; import { RotateCcw } from 'lucide-react'; import { fmt } from '@/lib/game/stores'; import { useCombatStore, usePrestigeStore, useManaStore } from '@/lib/game/stores'; +import type { SpellState } from '@/lib/game/types'; export function LoopStatsSection() { const spells = useCombatStore((s) => s.spells); @@ -15,7 +16,7 @@ export function LoopStatsSection() { const loopCount = usePrestigeStore((s) => s.loopCount); const memorySlots = usePrestigeStore((s) => s.memorySlots); - const spellsLearned = Object.values(spells || {}).filter((s: any) => s.learned).length; + const spellsLearned = Object.values(spells || {}).filter((s: SpellState) => s.learned).length; return ( diff --git a/src/components/game/tabs/StatsTab/ManaStatsSection.tsx b/src/components/game/tabs/StatsTab/ManaStatsSection.tsx index 4ff971e..61b7c17 100644 --- a/src/components/game/tabs/StatsTab/ManaStatsSection.tsx +++ b/src/components/game/tabs/StatsTab/ManaStatsSection.tsx @@ -3,6 +3,20 @@ import { fmt, fmtDec } from '@/lib/game/stores'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Droplet } from 'lucide-react'; +import type { ComputedEffects } from '@/lib/game/effects/upgrade-effects.types'; + +interface ManaStatsEffects extends ComputedEffects { + incursionStrength: number; + rawMana: number; + hasSteadyStream: boolean; + hasManaTorrent: boolean; + hasDesperateWells: boolean; + manaCascadeBonus: number; + manaWaterfallBonus: number; + hasFlowSurge: boolean; + hasManaOverflow: boolean; + hasEternalFlow: boolean; +} interface ManaStatsSectionProps { maxMana: number; @@ -10,7 +24,7 @@ interface ManaStatsSectionProps { effectiveRegen: number; clickMana: number; meditationMultiplier: number; - upgradeEffects: any; + upgradeEffects: ManaStatsEffects; elemMax: number; } diff --git a/src/lib/game/__tests__/combat-store.test.ts b/src/lib/game/__tests__/combat-store.test.ts deleted file mode 100644 index 9f3bc8f..0000000 --- a/src/lib/game/__tests__/combat-store.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { useCombatStore } from '../stores/combatStore'; -import { getFloorMaxHP } from '../utils'; - -beforeEach(() => { - useCombatStore.setState({ - currentFloor: 1, - floorHP: getFloorMaxHP(1), - floorMaxHP: getFloorMaxHP(1), - maxFloorReached: 1, - activeSpell: 'manaBolt', - currentAction: 'meditate', - castProgress: 0, - spireMode: false, - clearedFloors: {}, - climbDirection: null, - isDescending: false, - spells: { - manaBolt: { learned: true, level: 1, studyProgress: 0 }, - }, - }); -}); - -describe('CombatStore', () => { - describe('setCurrentFloor', () => { - it('should set current floor and reset HP', () => { - useCombatStore.getState().setCurrentFloor(5); - const state = useCombatStore.getState(); - expect(state.currentFloor).toBe(5); - expect(state.floorHP).toBe(state.floorMaxHP); - }); - }); - - describe('advanceFloor', () => { - it('should advance to next floor', () => { - useCombatStore.getState().advanceFloor(); - expect(useCombatStore.getState().currentFloor).toBe(2); - }); - it('should reset cast progress on advance', () => { - useCombatStore.setState({ castProgress: 0.5 }); - useCombatStore.getState().advanceFloor(); - expect(useCombatStore.getState().castProgress).toBe(0); - }); - it('should update max floor reached', () => { - useCombatStore.setState({ currentFloor: 3, maxFloorReached: 3 }); - useCombatStore.getState().advanceFloor(); - expect(useCombatStore.getState().maxFloorReached).toBe(4); - }); - it('should cap at floor 100', () => { - useCombatStore.setState({ currentFloor: 100 }); - useCombatStore.getState().advanceFloor(); - expect(useCombatStore.getState().currentFloor).toBe(100); - }); - }); - - describe('setFloorHP', () => { - it('should set floor HP', () => { - useCombatStore.getState().setFloorHP(50); - expect(useCombatStore.getState().floorHP).toBe(50); - }); - it('should clamp negative HP to zero', () => { - useCombatStore.getState().setFloorHP(-10); - expect(useCombatStore.getState().floorHP).toBe(0); - }); - }); - - describe('setMaxFloorReached', () => { - it('should set max floor reached', () => { - useCombatStore.getState().setMaxFloorReached(10); - expect(useCombatStore.getState().maxFloorReached).toBe(10); - }); - it('should not decrease max floor reached', () => { - useCombatStore.setState({ maxFloorReached: 10 }); - useCombatStore.getState().setMaxFloorReached(5); - expect(useCombatStore.getState().maxFloorReached).toBe(10); - }); - }); - - describe('setAction', () => { - it('should set current action', () => { - useCombatStore.getState().setAction('climb'); - expect(useCombatStore.getState().currentAction).toBe('climb'); - }); - }); - - describe('setSpell', () => { - it('should set active spell when learned', () => { - useCombatStore.getState().setSpell('manaBolt'); - expect(useCombatStore.getState().activeSpell).toBe('manaBolt'); - }); - it('should not set spell when not learned', () => { - useCombatStore.getState().setSpell('fireball'); - expect(useCombatStore.getState().activeSpell).toBe('manaBolt'); - }); - }); - - describe('setCastProgress', () => { - it('should set cast progress', () => { - useCombatStore.getState().setCastProgress(0.75); - expect(useCombatStore.getState().castProgress).toBe(0.75); - }); - }); - - describe('learnSpell', () => { - it('should learn a new spell', () => { - useCombatStore.getState().learnSpell('fireball'); - const spell = useCombatStore.getState().spells['fireball']; - expect(spell.learned).toBe(true); - expect(spell.level).toBe(1); - }); - }); - - describe('setSpellState', () => { - it('should update spell state', () => { - useCombatStore.getState().setSpellState('manaBolt', { level: 5 }); - expect(useCombatStore.getState().spells.manaBolt.level).toBe(5); - }); - it('should create spell if not exists', () => { - useCombatStore.getState().setSpellState('fireball', { learned: true, level: 3 }); - const spell = useCombatStore.getState().spells['fireball']; - expect(spell.learned).toBe(true); - expect(spell.level).toBe(3); - }); - }); - - describe('debugSetFloor', () => { - it('should set floor and reset HP', () => { - useCombatStore.getState().debugSetFloor(10); - const state = useCombatStore.getState(); - expect(state.currentFloor).toBe(10); - expect(state.floorHP).toBe(state.floorMaxHP); - }); - }); - - describe('resetFloorHP', () => { - it('should reset floor HP to max', () => { - useCombatStore.setState({ floorHP: 1 }); - useCombatStore.getState().resetFloorHP(); - expect(useCombatStore.getState().floorHP).toBe(useCombatStore.getState().floorMaxHP); - }); - }); - - describe('resetCombat', () => { - it('should reset combat to starting state', () => { - useCombatStore.setState({ currentFloor: 50, castProgress: 0.9 }); - useCombatStore.getState().resetCombat(1); - const state = useCombatStore.getState(); - expect(state.currentFloor).toBe(1); - expect(state.castProgress).toBe(0); - expect(state.activeSpell).toBe('manaBolt'); - }); - it('should keep specified spells', () => { - useCombatStore.getState().resetCombat(1, ['fireball']); - expect(useCombatStore.getState().spells['fireball'].learned).toBe(true); - }); - }); - - describe('climbDownFloor', () => { - it('should descend one floor', () => { - useCombatStore.setState({ currentFloor: 5 }); - useCombatStore.getState().climbDownFloor(); - expect(useCombatStore.getState().currentFloor).toBe(4); - }); - it('should not go below floor 1', () => { - useCombatStore.setState({ currentFloor: 1 }); - useCombatStore.getState().climbDownFloor(); - expect(useCombatStore.getState().currentFloor).toBe(1); - }); - it('should reset HP and cast progress on descent', () => { - useCombatStore.setState({ currentFloor: 5, castProgress: 0.5, floorHP: 1 }); - useCombatStore.getState().climbDownFloor(); - const state = useCombatStore.getState(); - expect(state.castProgress).toBe(0); - expect(state.floorHP).toBe(state.floorMaxHP); - }); - }); - - describe('exitSpireMode', () => { - it('should exit spire mode and reset state', () => { - useCombatStore.setState({ spireMode: true, climbDirection: 'up', isDescending: false }); - useCombatStore.getState().exitSpireMode(); - const state = useCombatStore.getState(); - expect(state.spireMode).toBe(false); - expect(state.climbDirection).toBeNull(); - expect(state.isDescending).toBe(false); - expect(state.currentAction).toBe('meditate'); - }); - }); -}); diff --git a/src/lib/game/__tests__/discipline-store.test.ts b/src/lib/game/__tests__/discipline-store.test.ts deleted file mode 100644 index 04a0c1b..0000000 --- a/src/lib/game/__tests__/discipline-store.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { useDisciplineStore } from '../stores/discipline-slice'; - -beforeEach(() => { - useDisciplineStore.setState({ - disciplines: {}, activeIds: [], concurrentLimit: 1, totalXP: 0, - }); -}); - -describe('DisciplineStore', () => { - describe('activate', () => { - it('should activate a discipline', () => { - useDisciplineStore.getState().activate('raw-mastery'); - const state = useDisciplineStore.getState(); - expect(state.activeIds).toContain('raw-mastery'); - expect(state.disciplines['raw-mastery']).toBeDefined(); - expect(state.disciplines['raw-mastery'].paused).toBe(false); - }); - it('should not activate unknown discipline', () => { - useDisciplineStore.getState().activate('nonexistent'); - expect(useDisciplineStore.getState().activeIds).toHaveLength(0); - }); - it('should not activate same discipline twice', () => { - useDisciplineStore.getState().activate('raw-mastery'); - useDisciplineStore.getState().activate('raw-mastery'); - expect(useDisciplineStore.getState().activeIds).toHaveLength(1); - }); - it('should respect concurrent limit', () => { - useDisciplineStore.setState({ concurrentLimit: 1 }); - useDisciplineStore.getState().activate('raw-mastery'); - useDisciplineStore.getState().activate('elemental-attunement'); - expect(useDisciplineStore.getState().activeIds).toHaveLength(1); - }); - it('should allow activation when under limit', () => { - useDisciplineStore.setState({ concurrentLimit: 2 }); - useDisciplineStore.getState().activate('raw-mastery'); - useDisciplineStore.getState().activate('elemental-attunement'); - expect(useDisciplineStore.getState().activeIds).toHaveLength(2); - }); - it('should activate element discipline on first call (no prior state)', () => { - useDisciplineStore.getState().activate('elemental-attunement', { elements: {} }); - expect(useDisciplineStore.getState().activeIds).toContain('elemental-attunement'); - }); - it('should activate element discipline with matching unlocked element', () => { - useDisciplineStore.getState().activate('elemental-attunement', { - elements: { fire: { unlocked: true } }, - }); - expect(useDisciplineStore.getState().activeIds).toContain('elemental-attunement'); - }); - it('should not re-activate paused discipline', () => { - useDisciplineStore.getState().activate('elemental-attunement', { - elements: { fire: { unlocked: true, current: 100 } }, - }); - useDisciplineStore.setState({ - disciplines: { - 'elemental-attunement': { id: 'elemental-attunement', xp: 200, paused: true }, - }, - }); - useDisciplineStore.getState().activate('elemental-attunement', { - elements: { fire: { unlocked: true, current: 100 } }, - }); - expect(useDisciplineStore.getState().disciplines['elemental-attunement'].paused).toBe(true); - }); - }); - - describe('deactivate', () => { - it('should deactivate a discipline', () => { - useDisciplineStore.getState().activate('raw-mastery'); - useDisciplineStore.getState().deactivate('raw-mastery'); - expect(useDisciplineStore.getState().activeIds).not.toContain('raw-mastery'); - }); - it('should not error on non-active discipline', () => { - useDisciplineStore.getState().deactivate('raw-mastery'); - expect(useDisciplineStore.getState().activeIds).toHaveLength(0); - }); - }); - - describe('processTick', () => { - it('should accrue XP for active discipline', () => { - useDisciplineStore.getState().activate('raw-mastery'); - useDisciplineStore.getState().processTick({ rawMana: 100, elements: {} }); - const state = useDisciplineStore.getState(); - expect(state.disciplines['raw-mastery'].xp).toBe(1); - expect(state.totalXP).toBe(1); - }); - it('should drain raw mana', () => { - useDisciplineStore.getState().activate('raw-mastery'); - const result = useDisciplineStore.getState().processTick({ rawMana: 100, elements: {} }); - expect(result.rawMana).toBeLessThan(100); - }); - it('should drain element mana for element discipline', () => { - useDisciplineStore.setState({ concurrentLimit: 2 }); - useDisciplineStore.getState().activate('elemental-attunement', { - elements: { fire: { unlocked: true } }, - }); - const result = useDisciplineStore.getState().processTick({ - rawMana: 0, elements: { fire: { current: 100 } }, - }); - expect(result.elements.fire.current).toBeLessThan(100); - }); - it('should pause discipline when insufficient mana', () => { - useDisciplineStore.getState().activate('raw-mastery'); - useDisciplineStore.getState().processTick({ rawMana: 0, elements: {} }); - expect(useDisciplineStore.getState().disciplines['raw-mastery'].paused).toBe(true); - }); - it('should not accrue XP when paused', () => { - useDisciplineStore.getState().activate('raw-mastery'); - useDisciplineStore.setState({ - disciplines: { 'raw-mastery': { id: 'raw-mastery', xp: 5, paused: true } }, - }); - useDisciplineStore.getState().processTick({ rawMana: 100, elements: {} }); - expect(useDisciplineStore.getState().disciplines['raw-mastery'].xp).toBe(5); - }); - it('should process multiple active disciplines', () => { - useDisciplineStore.setState({ concurrentLimit: 2 }); - useDisciplineStore.getState().activate('raw-mastery'); - useDisciplineStore.getState().activate('elemental-attunement', { - elements: { fire: { unlocked: true } }, - }); - useDisciplineStore.getState().processTick({ - rawMana: 100, elements: { fire: { current: 100 } }, - }); - expect(useDisciplineStore.getState().totalXP).toBe(2); - }); - it('should increase concurrent limit at XP thresholds', () => { - useDisciplineStore.setState({ - concurrentLimit: 1, totalXP: 499, activeIds: ['raw-mastery'], - disciplines: { 'raw-mastery': { id: 'raw-mastery', xp: 499, paused: false } }, - }); - useDisciplineStore.getState().processTick({ rawMana: 1000, elements: {} }); - expect(useDisciplineStore.getState().totalXP).toBe(500); - expect(useDisciplineStore.getState().concurrentLimit).toBeGreaterThan(1); - }); - }); -}); diff --git a/src/lib/game/__tests__/mana-store.test.ts b/src/lib/game/__tests__/mana-store.test.ts deleted file mode 100644 index d2ce1fa..0000000 --- a/src/lib/game/__tests__/mana-store.test.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { useManaStore } from '../stores/manaStore'; -import { MANA_PER_ELEMENT, BASE_UNLOCKED_ELEMENTS } from '../constants'; -import type { ElementState } from '../types'; - -function makeElements(): Record { - const keys = [ - 'fire','water','air','earth','light','dark','death', - 'transference','metal','sand','lightning','crystal','stellar','void', - ]; - const elements: Record = {}; - for (const k of keys) { - elements[k] = { current: 0, max: 10, unlocked: BASE_UNLOCKED_ELEMENTS.includes(k) }; - } - return elements; -} - -beforeEach(() => { - useManaStore.setState({ - rawMana: 10, meditateTicks: 0, totalManaGathered: 0, elements: makeElements(), - }); -}); - -describe('ManaStore', () => { - describe('setRawMana', () => { - it('should set raw mana to the given amount', () => { - useManaStore.getState().setRawMana(50); - expect(useManaStore.getState().rawMana).toBe(50); - }); - it('should clamp negative values to zero', () => { - useManaStore.getState().setRawMana(-10); - expect(useManaStore.getState().rawMana).toBe(0); - }); - it('should allow setting to zero', () => { - useManaStore.getState().setRawMana(0); - expect(useManaStore.getState().rawMana).toBe(0); - }); - }); - - describe('addRawMana', () => { - it('should add raw mana up to max', () => { - useManaStore.getState().addRawMana(40, 100); - expect(useManaStore.getState().rawMana).toBe(50); - }); - it('should cap at max mana', () => { - useManaStore.getState().addRawMana(200, 100); - expect(useManaStore.getState().rawMana).toBe(100); - }); - it('should track total mana gathered', () => { - useManaStore.getState().addRawMana(30, 100); - useManaStore.getState().addRawMana(20, 100); - expect(useManaStore.getState().totalManaGathered).toBe(50); - }); - }); - - describe('spendRawMana', () => { - it('should return true and deduct when sufficient mana', () => { - useManaStore.getState().setRawMana(50); - expect(useManaStore.getState().spendRawMana(20)).toBe(true); - expect(useManaStore.getState().rawMana).toBe(30); - }); - it('should return false when insufficient mana', () => { - useManaStore.getState().setRawMana(5); - expect(useManaStore.getState().spendRawMana(20)).toBe(false); - expect(useManaStore.getState().rawMana).toBe(5); - }); - it('should allow spending exact amount', () => { - useManaStore.getState().setRawMana(10); - expect(useManaStore.getState().spendRawMana(10)).toBe(true); - expect(useManaStore.getState().rawMana).toBe(0); - }); - }); - - describe('gatherMana', () => { - it('should add mana and track total gathered', () => { - useManaStore.getState().gatherMana(5, 100); - expect(useManaStore.getState().rawMana).toBe(15); - expect(useManaStore.getState().totalManaGathered).toBe(5); - }); - it('should cap at max mana', () => { - useManaStore.getState().gatherMana(200, 50); - expect(useManaStore.getState().rawMana).toBe(50); - }); - }); - - describe('convertMana', () => { - beforeEach(() => { - useManaStore.setState({ - rawMana: 1000, - elements: { ...makeElements(), fire: { current: 0, max: 10, unlocked: true } }, - }); - }); - it('should convert raw mana to element mana', () => { - const result = useManaStore.getState().convertMana('fire', 5); - expect(result).toBe(true); - expect(useManaStore.getState().elements.fire.current).toBe(5); - expect(useManaStore.getState().rawMana).toBe(1000 - 5 * MANA_PER_ELEMENT); - }); - it('should return false for locked element', () => { - expect(useManaStore.getState().convertMana('water', 1)).toBe(false); - }); - it('should return false when insufficient raw mana for full amount', () => { - useManaStore.setState({ rawMana: 50 }); - expect(useManaStore.getState().convertMana('fire', 1)).toBe(false); - }); - it('should return false when element is at max', () => { - useManaStore.setState({ - elements: { ...makeElements(), fire: { current: 10, max: 10, unlocked: true } }, - }); - expect(useManaStore.getState().convertMana('fire', 1)).toBe(false); - }); - it('should return false when raw mana < cost for requested amount', () => { - useManaStore.setState({ rawMana: 250 }); - expect(useManaStore.getState().convertMana('fire', 5)).toBe(false); - }); - it('should succeed when raw mana covers full requested amount', () => { - useManaStore.setState({ rawMana: 500 }); - const result = useManaStore.getState().convertMana('fire', 5); - expect(result).toBe(true); - expect(useManaStore.getState().elements.fire.current).toBe(5); - expect(useManaStore.getState().rawMana).toBe(0); - }); - }); - - describe('unlockElement', () => { - it('should unlock element and deduct cost', () => { - useManaStore.setState({ rawMana: 100 }); - expect(useManaStore.getState().unlockElement('fire', 50)).toBe(true); - expect(useManaStore.getState().elements.fire.unlocked).toBe(true); - expect(useManaStore.getState().rawMana).toBe(50); - }); - it('should return false when already unlocked', () => { - useManaStore.setState({ rawMana: 100 }); - useManaStore.getState().unlockElement('transference', 10); - expect(useManaStore.getState().unlockElement('transference', 10)).toBe(false); - }); - it('should return false when insufficient mana', () => { - useManaStore.setState({ rawMana: 10 }); - expect(useManaStore.getState().unlockElement('fire', 50)).toBe(false); - expect(useManaStore.getState().elements.fire.unlocked).toBe(false); - }); - }); - - describe('addElementMana', () => { - it('should add element mana up to max', () => { - useManaStore.setState({ - elements: { ...makeElements(), fire: { current: 0, max: 10, unlocked: true } }, - }); - useManaStore.getState().addElementMana('fire', 5, 10); - expect(useManaStore.getState().elements.fire.current).toBe(5); - }); - it('should cap at max', () => { - useManaStore.setState({ - elements: { ...makeElements(), fire: { current: 8, max: 10, unlocked: true } }, - }); - useManaStore.getState().addElementMana('fire', 5, 10); - expect(useManaStore.getState().elements.fire.current).toBe(10); - }); - }); - - describe('spendElementMana', () => { - it('should spend element mana when sufficient', () => { - useManaStore.setState({ - elements: { ...makeElements(), fire: { current: 10, max: 10, unlocked: true } }, - }); - expect(useManaStore.getState().spendElementMana('fire', 5)).toBe(true); - expect(useManaStore.getState().elements.fire.current).toBe(5); - }); - it('should return false when insufficient', () => { - useManaStore.setState({ - elements: { ...makeElements(), fire: { current: 3, max: 10, unlocked: true } }, - }); - expect(useManaStore.getState().spendElementMana('fire', 5)).toBe(false); - expect(useManaStore.getState().elements.fire.current).toBe(3); - }); - }); - - describe('craftComposite', () => { - beforeEach(() => { - useManaStore.setState({ - elements: { - ...makeElements(), - fire: { current: 5, max: 10, unlocked: true }, - earth: { current: 5, max: 10, unlocked: true }, - metal: { current: 0, max: 10, unlocked: false }, - }, - }); - }); - it('should craft composite from recipe', () => { - const result = useManaStore.getState().craftComposite('metal', ['fire', 'earth']); - expect(result).toBe(true); - expect(useManaStore.getState().elements.metal.current).toBe(1); - expect(useManaStore.getState().elements.metal.unlocked).toBe(true); - expect(useManaStore.getState().elements.fire.current).toBe(4); - expect(useManaStore.getState().elements.earth.current).toBe(4); - }); - it('should return false when missing ingredients', () => { - useManaStore.setState({ - elements: { - ...makeElements(), - fire: { current: 0, max: 10, unlocked: true }, - earth: { current: 5, max: 10, unlocked: true }, - metal: { current: 0, max: 10, unlocked: false }, - }, - }); - expect(useManaStore.getState().craftComposite('metal', ['fire', 'earth'])).toBe(false); - }); - it('should handle multiple different ingredients in recipe', () => { - useManaStore.setState({ - elements: { - ...makeElements(), - sand: { current: 0, max: 10, unlocked: false }, - earth: { current: 5, max: 10, unlocked: true }, - water: { current: 5, max: 10, unlocked: true }, - }, - }); - const result = useManaStore.getState().craftComposite('sand', ['earth', 'water']); - expect(result).toBe(true); - expect(useManaStore.getState().elements.sand.current).toBe(1); - }); - }); - - describe('processConvertAction', () => { - it('should return null when no unlocked elements with room', () => { - expect(useManaStore.getState().processConvertAction(50)).toBeNull(); - }); - it('should return null when raw mana < 100', () => { - useManaStore.setState({ - elements: { ...makeElements(), fire: { current: 0, max: 10, unlocked: true } }, - }); - expect(useManaStore.getState().processConvertAction(50)).toBeNull(); - }); - it('should auto-convert raw to element mana', () => { - useManaStore.setState({ - elements: { ...makeElements(), fire: { current: 0, max: 10, unlocked: true } }, - }); - const result = useManaStore.getState().processConvertAction(500); - expect(result).not.toBeNull(); - expect(result!.elements.fire.current).toBe(5); - expect(result!.rawMana).toBe(0); - }); - }); - - describe('resetMana', () => { - it('should reset to initial state with default upgrades', () => { - useManaStore.setState({ rawMana: 500, meditateTicks: 100, totalManaGathered: 999 }); - useManaStore.getState().resetMana({}); - expect(useManaStore.getState().rawMana).toBe(10); - expect(useManaStore.getState().meditateTicks).toBe(0); - expect(useManaStore.getState().totalManaGathered).toBe(0); - }); - it('should apply prestige upgrades', () => { - useManaStore.getState().resetMana({ manaStart: 2, elemMax: 1, elemStart: 1 }); - expect(useManaStore.getState().rawMana).toBe(30); - for (const k of BASE_UNLOCKED_ELEMENTS) { - expect(useManaStore.getState().elements[k].current).toBe(5); - } - }); - }); - - describe('meditation ticks', () => { - it('should set meditate ticks', () => { - useManaStore.getState().setMeditateTicks(42); - expect(useManaStore.getState().meditateTicks).toBe(42); - }); - it('should increment meditate ticks', () => { - useManaStore.getState().incrementMeditateTicks(); - useManaStore.getState().incrementMeditateTicks(); - expect(useManaStore.getState().meditateTicks).toBe(2); - }); - it('should reset meditate ticks', () => { - useManaStore.getState().setMeditateTicks(100); - useManaStore.getState().resetMeditateTicks(); - expect(useManaStore.getState().meditateTicks).toBe(0); - }); - }); - - describe('setElementMax', () => { - it('should set max for all elements', () => { - useManaStore.getState().setElementMax(50); - for (const key of Object.keys(useManaStore.getState().elements)) { - expect(useManaStore.getState().elements[key].max).toBe(50); - } - }); - }); -}); diff --git a/src/lib/game/__tests__/prestige-store.test.ts b/src/lib/game/__tests__/prestige-store.test.ts deleted file mode 100644 index c7ac782..0000000 --- a/src/lib/game/__tests__/prestige-store.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { usePrestigeStore } from '../stores/prestigeStore'; - -beforeEach(() => { - usePrestigeStore.setState({ - loopCount: 0, insight: 0, totalInsight: 0, loopInsight: 0, - prestigeUpgrades: {}, memorySlots: 3, pactSlots: 1, - memories: [], defeatedGuardians: [], signedPacts: [], - signedPactDetails: {}, pactRitualFloor: null, pactRitualProgress: 0, - }); -}); - -describe('PrestigeStore', () => { - describe('doPrestige', () => { - it('should purchase upgrade when sufficient insight', () => { - usePrestigeStore.setState({ insight: 500 }); - expect(usePrestigeStore.getState().doPrestige('manaWell')).toBe(true); - expect(usePrestigeStore.getState().prestigeUpgrades['manaWell']).toBe(1); - expect(usePrestigeStore.getState().insight).toBe(0); - }); - it('should return false when insufficient insight', () => { - usePrestigeStore.setState({ insight: 100 }); - expect(usePrestigeStore.getState().doPrestige('manaWell')).toBe(false); - }); - it('should return false for invalid upgrade id', () => { - usePrestigeStore.setState({ insight: 9999 }); - expect(usePrestigeStore.getState().doPrestige('nonexistent')).toBe(false); - }); - it('should return false when at max level', () => { - usePrestigeStore.setState({ insight: 5000, prestigeUpgrades: { manaWell: 5 } }); - expect(usePrestigeStore.getState().doPrestige('manaWell')).toBe(false); - }); - it('should increase memorySlots for deepMemory', () => { - usePrestigeStore.setState({ insight: 1000 }); - usePrestigeStore.getState().doPrestige('deepMemory'); - expect(usePrestigeStore.getState().memorySlots).toBe(4); - }); - it('should allow purchasing same upgrade multiple times', () => { - usePrestigeStore.setState({ insight: 1500 }); - usePrestigeStore.getState().doPrestige('manaWell'); - usePrestigeStore.getState().doPrestige('manaWell'); - expect(usePrestigeStore.getState().prestigeUpgrades['manaWell']).toBe(2); - expect(usePrestigeStore.getState().insight).toBe(500); - }); - }); - - describe('addMemory', () => { - it('should add a memory when slots available', () => { - usePrestigeStore.getState().addMemory({ skillId: 'fire', level: 5 }); - expect(usePrestigeStore.getState().memories).toHaveLength(1); - }); - it('should not add duplicate skillId', () => { - usePrestigeStore.getState().addMemory({ skillId: 'fire', level: 5 }); - usePrestigeStore.getState().addMemory({ skillId: 'fire', level: 3 }); - expect(usePrestigeStore.getState().memories).toHaveLength(1); - }); - it('should not exceed memory slots', () => { - usePrestigeStore.setState({ memorySlots: 1 }); - usePrestigeStore.getState().addMemory({ skillId: 'fire', level: 1 }); - usePrestigeStore.getState().addMemory({ skillId: 'water', level: 1 }); - expect(usePrestigeStore.getState().memories).toHaveLength(1); - }); - }); - - describe('removeMemory', () => { - it('should remove memory by skillId', () => { - usePrestigeStore.setState({ - memories: [{ skillId: 'fire', level: 5 }, { skillId: 'water', level: 3 }], - }); - usePrestigeStore.getState().removeMemory('fire'); - expect(usePrestigeStore.getState().memories).toHaveLength(1); - expect(usePrestigeStore.getState().memories[0].skillId).toBe('water'); - }); - }); - - describe('clearMemories', () => { - it('should clear all memories', () => { - usePrestigeStore.setState({ - memories: [{ skillId: 'fire', level: 5 }, { skillId: 'water', level: 3 }], - }); - usePrestigeStore.getState().clearMemories(); - expect(usePrestigeStore.getState().memories).toHaveLength(0); - }); - }); - - describe('startPactRitual', () => { - it('should start ritual when conditions met', () => { - usePrestigeStore.setState({ defeatedGuardians: [10] }); - expect(usePrestigeStore.getState().startPactRitual(10, 500)).toBe(true); - expect(usePrestigeStore.getState().pactRitualFloor).toBe(10); - }); - it('should return false when guardian not defeated', () => { - expect(usePrestigeStore.getState().startPactRitual(10, 500)).toBe(false); - }); - it('should return false when pact already signed', () => { - usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [10] }); - expect(usePrestigeStore.getState().startPactRitual(10, 500)).toBe(false); - }); - it('should return false when insufficient raw mana', () => { - usePrestigeStore.setState({ defeatedGuardians: [10] }); - expect(usePrestigeStore.getState().startPactRitual(10, 10)).toBe(false); - }); - it('should return false when pact slots full', () => { - usePrestigeStore.setState({ defeatedGuardians: [10, 20], signedPacts: [20], pactSlots: 1 }); - expect(usePrestigeStore.getState().startPactRitual(10, 500)).toBe(false); - }); - it('should return false when ritual already in progress', () => { - usePrestigeStore.setState({ defeatedGuardians: [10], pactRitualFloor: 20 }); - expect(usePrestigeStore.getState().startPactRitual(10, 500)).toBe(false); - }); - }); - - describe('cancelPactRitual', () => { - it('should cancel ritual and reset progress', () => { - usePrestigeStore.setState({ pactRitualFloor: 10, pactRitualProgress: 5 }); - usePrestigeStore.getState().cancelPactRitual(); - expect(usePrestigeStore.getState().pactRitualFloor).toBeNull(); - expect(usePrestigeStore.getState().pactRitualProgress).toBe(0); - }); - }); - - describe('completePactRitual', () => { - it('should sign pact and remove from defeated', () => { - usePrestigeStore.setState({ pactRitualFloor: 10, defeatedGuardians: [10], signedPacts: [] }); - const logs: string[] = []; - usePrestigeStore.getState().completePactRitual((msg) => logs.push(msg)); - expect(usePrestigeStore.getState().signedPacts).toContain(10); - expect(usePrestigeStore.getState().defeatedGuardians).not.toContain(10); - expect(usePrestigeStore.getState().pactRitualFloor).toBeNull(); - expect(logs).toHaveLength(1); - }); - it('should do nothing when no ritual in progress', () => { - usePrestigeStore.setState({ pactRitualFloor: null }); - const logs: string[] = []; - usePrestigeStore.getState().completePactRitual((msg) => logs.push(msg)); - expect(logs).toHaveLength(0); - }); - }); - - describe('defeatGuardian', () => { - it('should add guardian to defeated list', () => { - usePrestigeStore.getState().defeatGuardian(10); - expect(usePrestigeStore.getState().defeatedGuardians).toContain(10); - }); - it('should not add if already defeated', () => { - usePrestigeStore.setState({ defeatedGuardians: [10] }); - usePrestigeStore.getState().defeatGuardian(10); - expect(usePrestigeStore.getState().defeatedGuardians).toHaveLength(1); - }); - it('should not add if pact already signed', () => { - usePrestigeStore.setState({ signedPacts: [10] }); - usePrestigeStore.getState().defeatGuardian(10); - expect(usePrestigeStore.getState().defeatedGuardians).not.toContain(10); - }); - }); - - describe('addSignedPact', () => { - it('should add a signed pact', () => { - usePrestigeStore.getState().addSignedPact(10); - expect(usePrestigeStore.getState().signedPacts).toContain(10); - }); - it('should not duplicate', () => { - usePrestigeStore.setState({ signedPacts: [10] }); - usePrestigeStore.getState().addSignedPact(10); - expect(usePrestigeStore.getState().signedPacts).toHaveLength(1); - }); - }); - - describe('removePact', () => { - it('should remove a signed pact', () => { - usePrestigeStore.setState({ signedPacts: [10, 20] }); - usePrestigeStore.getState().removePact(10); - expect(usePrestigeStore.getState().signedPacts).not.toContain(10); - expect(usePrestigeStore.getState().signedPacts).toContain(20); - }); - }); - - describe('startNewLoop', () => { - it('should increment loop count and add insight', () => { - usePrestigeStore.setState({ insight: 100, totalInsight: 500 }); - usePrestigeStore.getState().startNewLoop(50); - expect(usePrestigeStore.getState().loopCount).toBe(1); - expect(usePrestigeStore.getState().insight).toBe(150); - expect(usePrestigeStore.getState().totalInsight).toBe(550); - }); - it('should reset loop-specific state', () => { - usePrestigeStore.setState({ - defeatedGuardians: [10], signedPacts: [20], - pactRitualFloor: 10, pactRitualProgress: 5, - }); - usePrestigeStore.getState().startNewLoop(0); - expect(usePrestigeStore.getState().defeatedGuardians).toHaveLength(0); - expect(usePrestigeStore.getState().signedPacts).toHaveLength(0); - expect(usePrestigeStore.getState().pactRitualFloor).toBeNull(); - expect(usePrestigeStore.getState().pactRitualProgress).toBe(0); - }); - }); - - describe('setLoopInsight', () => { - it('should set loop insight', () => { - usePrestigeStore.getState().setLoopInsight(42); - expect(usePrestigeStore.getState().loopInsight).toBe(42); - }); - }); - - describe('incrementLoopCount', () => { - it('should increment loop count', () => { - usePrestigeStore.getState().incrementLoopCount(); - expect(usePrestigeStore.getState().loopCount).toBe(1); - usePrestigeStore.getState().incrementLoopCount(); - expect(usePrestigeStore.getState().loopCount).toBe(2); - }); - }); - - describe('resetPrestigeForNewLoop', () => { - it('should set insight, upgrades, memories, and slots', () => { - usePrestigeStore.getState().resetPrestigeForNewLoop( - 500, { manaWell: 2 }, [{ skillId: 'fire', level: 3 }], 4, - ); - const state = usePrestigeStore.getState(); - expect(state.insight).toBe(500); - expect(state.prestigeUpgrades).toEqual({ manaWell: 2 }); - expect(state.memories).toHaveLength(1); - expect(state.memorySlots).toBe(4); - expect(state.defeatedGuardians).toHaveLength(0); - expect(state.signedPacts).toHaveLength(0); - expect(state.loopInsight).toBe(0); - }); - }); -}); diff --git a/src/lib/game/__tests__/store-actions-combat-prestige.test.ts b/src/lib/game/__tests__/store-actions-combat-prestige.test.ts new file mode 100644 index 0000000..b7720b3 --- /dev/null +++ b/src/lib/game/__tests__/store-actions-combat-prestige.test.ts @@ -0,0 +1,362 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useCombatStore } from '../stores/combatStore'; +import { usePrestigeStore } from '../stores/prestigeStore'; +import { getFloorMaxHP } from '../utils'; + +function resetCombatStore() { + useCombatStore.setState({ + currentFloor: 1, + floorHP: getFloorMaxHP(1), + floorMaxHP: getFloorMaxHP(1), + maxFloorReached: 1, + activeSpell: 'manaBolt', + currentAction: 'meditate', + castProgress: 0, + spireMode: false, + currentRoom: { roomType: 'combat', enemies: [], cleared: false }, + clearedFloors: {}, + climbDirection: null, + isDescending: false, + golemancy: { enabledGolems: [], summonedGolems: [], lastSummonFloor: 0 }, + equipmentSpellStates: [], + comboHitCount: 0, + floorHitCount: 0, + spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } }, + activityLog: [], + achievements: { unlocked: [], progress: {} }, + totalSpellsCast: 0, + totalDamageDealt: 0, + totalCraftsCompleted: 0, + }); +} + +function resetPrestigeStore() { + usePrestigeStore.setState({ + loopCount: 0, + insight: 500, + totalInsight: 500, + loopInsight: 0, + prestigeUpgrades: {}, + memorySlots: 3, + pactSlots: 1, + memories: [], + defeatedGuardians: [], + signedPacts: [], + signedPactDetails: {}, + pactRitualFloor: null, + pactRitualProgress: 0, + }); +} + +describe('CombatStore', () => { + beforeEach(resetCombatStore); + + describe('setCurrentFloor', () => { + it('should set floor and update HP', () => { + useCombatStore.getState().setCurrentFloor(5); + expect(useCombatStore.getState().currentFloor).toBe(5); + expect(useCombatStore.getState().floorHP).toBe(getFloorMaxHP(5)); + expect(useCombatStore.getState().floorMaxHP).toBe(getFloorMaxHP(5)); + }); + }); + + describe('advanceFloor', () => { + it('should increment floor and update HP', () => { + useCombatStore.getState().advanceFloor(); + expect(useCombatStore.getState().currentFloor).toBe(2); + expect(useCombatStore.getState().floorHP).toBe(getFloorMaxHP(2)); + }); + + it('should cap at floor 100', () => { + useCombatStore.setState({ currentFloor: 100 }); + useCombatStore.getState().advanceFloor(); + expect(useCombatStore.getState().currentFloor).toBe(100); + }); + + it('should update maxFloorReached', () => { + useCombatStore.getState().advanceFloor(); + expect(useCombatStore.getState().maxFloorReached).toBe(2); + }); + + it('should reset cast progress', () => { + useCombatStore.setState({ castProgress: 0.5 }); + useCombatStore.getState().advanceFloor(); + expect(useCombatStore.getState().castProgress).toBe(0); + }); + }); + + describe('setFloorHP', () => { + it('should set floor HP', () => { + useCombatStore.getState().setFloorHP(50); + expect(useCombatStore.getState().floorHP).toBe(50); + }); + + it('should clamp negative to 0', () => { + useCombatStore.getState().setFloorHP(-10); + expect(useCombatStore.getState().floorHP).toBe(0); + }); + }); + + describe('setMaxFloorReached', () => { + it('should update max floor reached', () => { + useCombatStore.getState().setMaxFloorReached(10); + expect(useCombatStore.getState().maxFloorReached).toBe(10); + }); + + it('should only increase, never decrease', () => { + useCombatStore.setState({ maxFloorReached: 10 }); + useCombatStore.getState().setMaxFloorReached(5); + expect(useCombatStore.getState().maxFloorReached).toBe(10); + }); + }); + + describe('setAction / setSpell', () => { + it('should set current action', () => { + useCombatStore.getState().setAction('climb'); + expect(useCombatStore.getState().currentAction).toBe('climb'); + }); + + it('should set active spell when learned', () => { + useCombatStore.getState().setSpell('manaBolt'); + expect(useCombatStore.getState().activeSpell).toBe('manaBolt'); + }); + + it('should not set spell when not learned', () => { + useCombatStore.getState().setSpell('fireball'); + expect(useCombatStore.getState().activeSpell).toBe('manaBolt'); + }); + }); + + describe('learnSpell', () => { + it('should add a new learned spell', () => { + useCombatStore.getState().learnSpell('fireball'); + expect(useCombatStore.getState().spells.fireball.learned).toBe(true); + }); + }); + + describe('debugSetFloor / resetFloorHP', () => { + it('should set floor and update HP', () => { + useCombatStore.getState().debugSetFloor(10); + expect(useCombatStore.getState().currentFloor).toBe(10); + expect(useCombatStore.getState().floorHP).toBe(getFloorMaxHP(10)); + }); + + it('should reset floor HP to max', () => { + useCombatStore.setState({ floorHP: 10 }); + useCombatStore.getState().resetFloorHP(); + expect(useCombatStore.getState().floorHP).toBe(useCombatStore.getState().floorMaxHP); + }); + }); + + describe('resetCombat', () => { + it('should reset to starting floor', () => { + useCombatStore.setState({ currentFloor: 50, maxFloorReached: 50 }); + useCombatStore.getState().resetCombat(1); + expect(useCombatStore.getState().currentFloor).toBe(1); + expect(useCombatStore.getState().maxFloorReached).toBe(1); + }); + }); + + describe('climbDownFloor', () => { + it('should decrement floor', () => { + useCombatStore.setState({ currentFloor: 5 }); + useCombatStore.getState().climbDownFloor(); + expect(useCombatStore.getState().currentFloor).toBe(4); + }); + + it('should not go below floor 1', () => { + useCombatStore.setState({ currentFloor: 1 }); + useCombatStore.getState().climbDownFloor(); + expect(useCombatStore.getState().currentFloor).toBe(1); + }); + }); + + describe('exitSpireMode', () => { + it('should reset spire state', () => { + useCombatStore.setState({ spireMode: true, climbDirection: 'up', currentAction: 'climb' }); + useCombatStore.getState().exitSpireMode(); + expect(useCombatStore.getState().spireMode).toBe(false); + expect(useCombatStore.getState().climbDirection).toBeNull(); + expect(useCombatStore.getState().currentAction).toBe('meditate'); + }); + }); +}); + +describe('PrestigeStore', () => { + beforeEach(resetPrestigeStore); + + describe('doPrestige', () => { + it('should purchase upgrade when affordable', () => { + const result = usePrestigeStore.getState().doPrestige('manaWell'); + expect(result).toBe(true); + expect(usePrestigeStore.getState().prestigeUpgrades.manaWell).toBe(1); + expect(usePrestigeStore.getState().insight).toBeLessThan(500); + }); + + it('should return false when cannot afford', () => { + usePrestigeStore.setState({ insight: 0 }); + const result = usePrestigeStore.getState().doPrestige('manaWell'); + expect(result).toBe(false); + }); + + it('should return false for invalid upgrade id', () => { + const result = usePrestigeStore.getState().doPrestige('nonexistent'); + expect(result).toBe(false); + }); + + it('should increase memorySlots with deepMemory', () => { + usePrestigeStore.setState({ insight: 2000 }); + const before = usePrestigeStore.getState().memorySlots; + usePrestigeStore.getState().doPrestige('deepMemory'); + expect(usePrestigeStore.getState().memorySlots).toBe(before + 1); + }); + }); + + describe('addMemory / removeMemory', () => { + it('should add a memory when slots available', () => { + usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 3 }); + expect(usePrestigeStore.getState().memories.length).toBe(1); + }); + + it('should not add duplicate memory', () => { + usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 3 }); + usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 5 }); + expect(usePrestigeStore.getState().memories.length).toBe(1); + }); + + it('should not exceed memory slots', () => { + usePrestigeStore.setState({ memorySlots: 1 }); + usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 1 }); + usePrestigeStore.getState().addMemory({ skillId: 'manaSpring', level: 1 }); + expect(usePrestigeStore.getState().memories.length).toBe(1); + }); + + it('should remove memory by skillId', () => { + usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 3 }); + usePrestigeStore.getState().removeMemory('manaFlow'); + expect(usePrestigeStore.getState().memories.length).toBe(0); + }); + + it('should clear all memories', () => { + usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 1 }); + usePrestigeStore.getState().addMemory({ skillId: 'manaSpring', level: 1 }); + usePrestigeStore.getState().clearMemories(); + expect(usePrestigeStore.getState().memories.length).toBe(0); + }); + }); + + describe('defeatGuardian / signedPacts', () => { + it('should add defeated guardian', () => { + usePrestigeStore.getState().defeatGuardian(10); + expect(usePrestigeStore.getState().defeatedGuardians).toContain(10); + }); + + it('should not duplicate defeated guardian', () => { + usePrestigeStore.getState().defeatGuardian(10); + usePrestigeStore.getState().defeatGuardian(10); + expect(usePrestigeStore.getState().defeatedGuardians.filter(f => f === 10).length).toBe(1); + }); + + it('should not defeat already signed guardian', () => { + usePrestigeStore.setState({ signedPacts: [10] }); + usePrestigeStore.getState().defeatGuardian(10); + expect(usePrestigeStore.getState().defeatedGuardians).not.toContain(10); + }); + + it('should add signed pact', () => { + usePrestigeStore.getState().addSignedPact(10); + expect(usePrestigeStore.getState().signedPacts).toContain(10); + }); + + it('should remove pact', () => { + usePrestigeStore.setState({ signedPacts: [10, 20] }); + usePrestigeStore.getState().removePact(10); + expect(usePrestigeStore.getState().signedPacts).not.toContain(10); + expect(usePrestigeStore.getState().signedPacts).toContain(20); + }); + }); + + describe('startPactRitual', () => { + it('should start ritual when conditions met', () => { + usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [], insight: 10000 }); + const result = usePrestigeStore.getState().startPactRitual(10, 10000); + expect(result).toBe(true); + expect(usePrestigeStore.getState().pactRitualFloor).toBe(10); + }); + + it('should return false when guardian not defeated', () => { + const result = usePrestigeStore.getState().startPactRitual(10, 10000); + expect(result).toBe(false); + }); + + it('should return false when already signed', () => { + usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [10] }); + const result = usePrestigeStore.getState().startPactRitual(10, 10000); + expect(result).toBe(false); + }); + + it('should return false when pact slots full', () => { + usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [20], pactSlots: 1 }); + const result = usePrestigeStore.getState().startPactRitual(10, 10000); + expect(result).toBe(false); + }); + + it('should return false when insufficient mana', () => { + usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [] }); + const result = usePrestigeStore.getState().startPactRitual(10, 0); + expect(result).toBe(false); + }); + }); + + describe('cancelPactRitual', () => { + it('should cancel active ritual', () => { + usePrestigeStore.setState({ pactRitualFloor: 10, pactRitualProgress: 1 }); + usePrestigeStore.getState().cancelPactRitual(); + expect(usePrestigeStore.getState().pactRitualFloor).toBeNull(); + expect(usePrestigeStore.getState().pactRitualProgress).toBe(0); + }); + }); + + describe('startNewLoop', () => { + it('should increment loop count and add insight', () => { + usePrestigeStore.setState({ insight: 100, totalInsight: 100 }); + usePrestigeStore.getState().startNewLoop(50); + expect(usePrestigeStore.getState().loopCount).toBe(1); + expect(usePrestigeStore.getState().insight).toBe(150); + expect(usePrestigeStore.getState().totalInsight).toBe(150); + }); + + it('should reset loop-specific state', () => { + usePrestigeStore.setState({ + defeatedGuardians: [10], + signedPacts: [20], + pactRitualFloor: 10, + pactRitualProgress: 5, + }); + usePrestigeStore.getState().startNewLoop(0); + expect(usePrestigeStore.getState().defeatedGuardians).toEqual([]); + expect(usePrestigeStore.getState().signedPacts).toEqual([]); + expect(usePrestigeStore.getState().pactRitualFloor).toBeNull(); + }); + }); + + describe('resetPrestigeForNewLoop', () => { + it('should preserve insight and upgrades, reset loop state', () => { + usePrestigeStore.getState().resetPrestigeForNewLoop(200, { manaWell: 2 }, [], 4); + expect(usePrestigeStore.getState().insight).toBe(200); + expect(usePrestigeStore.getState().prestigeUpgrades).toEqual({ manaWell: 2 }); + expect(usePrestigeStore.getState().memorySlots).toBe(4); + expect(usePrestigeStore.getState().defeatedGuardians).toEqual([]); + }); + }); + + describe('resetPrestige', () => { + it('should reset everything to initial state', () => { + usePrestigeStore.setState({ insight: 1000, loopCount: 5, prestigeUpgrades: { manaWell: 3 } }); + usePrestigeStore.getState().resetPrestige(); + expect(usePrestigeStore.getState().insight).toBe(0); + expect(usePrestigeStore.getState().loopCount).toBe(0); + expect(usePrestigeStore.getState().prestigeUpgrades).toEqual({}); + }); + }); +}); diff --git a/src/lib/game/__tests__/store-actions-discipline.test.ts b/src/lib/game/__tests__/store-actions-discipline.test.ts new file mode 100644 index 0000000..d395d01 --- /dev/null +++ b/src/lib/game/__tests__/store-actions-discipline.test.ts @@ -0,0 +1,95 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useDisciplineStore } from '../stores/discipline-slice'; + +function resetDisciplineStore() { + useDisciplineStore.setState({ + disciplines: {}, + activeIds: [], + concurrentLimit: 1, + totalXP: 0, + }); +} + +describe('DisciplineStore', () => { + beforeEach(resetDisciplineStore); + + describe('activate', () => { + it('should activate raw discipline', () => { + useDisciplineStore.getState().activate('raw-mastery'); + expect(useDisciplineStore.getState().activeIds).toContain('raw-mastery'); + expect(useDisciplineStore.getState().disciplines['raw-mastery'].paused).toBe(false); + }); + + it('should not activate same discipline twice', () => { + useDisciplineStore.getState().activate('raw-mastery'); + useDisciplineStore.getState().activate('raw-mastery'); + expect(useDisciplineStore.getState().activeIds.filter(id => id === 'raw-mastery').length).toBe(1); + }); + + it('should not activate when concurrent limit reached', () => { + useDisciplineStore.getState().activate('raw-mastery'); + useDisciplineStore.getState().activate('elemental-attunement'); + expect(useDisciplineStore.getState().activeIds.length).toBe(1); + }); + + it('should activate when no prior discipline state (optimistic)', () => { + // canProceedDiscipline returns true when disciplineState is undefined + useDisciplineStore.getState().activate('elemental-attunement', { + elements: { fire: { unlocked: false } }, + }); + expect(useDisciplineStore.getState().activeIds).toContain('elemental-attunement'); + }); + + it('should not activate when existing state has insufficient mana', () => { + useDisciplineStore.setState({ + disciplines: { 'raw-mastery': { id: 'raw-mastery', xp: 1000, paused: false } }, + }); + useDisciplineStore.getState().activate('raw-mastery', { elements: {} }); + expect(useDisciplineStore.getState().activeIds).not.toContain('raw-mastery'); + }); + + it('should activate when required element is unlocked', () => { + useDisciplineStore.getState().activate('elemental-attunement', { + elements: { fire: { unlocked: true } }, + }); + expect(useDisciplineStore.getState().activeIds).toContain('elemental-attunement'); + }); + }); + + describe('deactivate', () => { + it('should remove discipline from active list', () => { + useDisciplineStore.getState().activate('raw-mastery'); + useDisciplineStore.getState().deactivate('raw-mastery'); + expect(useDisciplineStore.getState().activeIds).not.toContain('raw-mastery'); + }); + }); + + describe('processTick', () => { + it('should accrue XP for active discipline', () => { + useDisciplineStore.getState().activate('raw-mastery'); + useDisciplineStore.getState().processTick({ rawMana: 1000, elements: {} }); + expect(useDisciplineStore.getState().disciplines['raw-mastery'].xp).toBe(1); + expect(useDisciplineStore.getState().totalXP).toBe(1); + }); + + it('should drain raw mana for raw discipline', () => { + useDisciplineStore.getState().activate('raw-mastery'); + const result = useDisciplineStore.getState().processTick({ rawMana: 1000, elements: {} }); + expect(result.rawMana).toBeLessThan(1000); + }); + + it('should pause discipline when insufficient mana', () => { + useDisciplineStore.getState().activate('raw-mastery'); + useDisciplineStore.getState().processTick({ rawMana: 0, elements: {} }); + expect(useDisciplineStore.getState().disciplines['raw-mastery'].paused).toBe(true); + }); + + it('should increase concurrent limit at 500 total XP', () => { + useDisciplineStore.setState({ totalXP: 499 }); + useDisciplineStore.getState().activate('raw-mastery'); + useDisciplineStore.getState().processTick({ rawMana: 1000, elements: {} }); + expect(useDisciplineStore.getState().totalXP).toBe(500); + expect(useDisciplineStore.getState().concurrentLimit).toBeGreaterThan(1); + }); + }); +}); diff --git a/src/lib/game/__tests__/store-actions-mana.test.ts b/src/lib/game/__tests__/store-actions-mana.test.ts new file mode 100644 index 0000000..3910d2e --- /dev/null +++ b/src/lib/game/__tests__/store-actions-mana.test.ts @@ -0,0 +1,202 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useManaStore, makeInitialElements } from '../stores/manaStore'; +import { MANA_PER_ELEMENT } from '../constants'; + +function resetManaStore() { + useManaStore.setState({ + rawMana: 100, + meditateTicks: 0, + totalManaGathered: 0, + elements: makeInitialElements(50, {}), + }); +} + +describe('ManaStore', () => { + beforeEach(resetManaStore); + + describe('setRawMana', () => { + it('should set raw mana to a positive value', () => { + useManaStore.getState().setRawMana(50); + expect(useManaStore.getState().rawMana).toBe(50); + }); + + it('should clamp negative values to 0', () => { + useManaStore.getState().setRawMana(-10); + expect(useManaStore.getState().rawMana).toBe(0); + }); + }); + + describe('addRawMana', () => { + it('should add raw mana up to max', () => { + useManaStore.getState().addRawMana(50, 200); + expect(useManaStore.getState().rawMana).toBe(150); + }); + + it('should cap at max mana', () => { + useManaStore.getState().addRawMana(200, 150); + expect(useManaStore.getState().rawMana).toBe(150); + }); + + it('should increment totalManaGathered', () => { + useManaStore.getState().addRawMana(50, 200); + expect(useManaStore.getState().totalManaGathered).toBe(50); + }); + }); + + describe('spendRawMana', () => { + it('should return true and deduct when sufficient', () => { + const result = useManaStore.getState().spendRawMana(30); + expect(result).toBe(true); + expect(useManaStore.getState().rawMana).toBe(70); + }); + + it('should return false when insufficient', () => { + const result = useManaStore.getState().spendRawMana(200); + expect(result).toBe(false); + expect(useManaStore.getState().rawMana).toBe(100); + }); + }); + + describe('gatherMana', () => { + it('should add mana and track total gathered', () => { + useManaStore.getState().gatherMana(5, 200); + expect(useManaStore.getState().rawMana).toBe(105); + expect(useManaStore.getState().totalManaGathered).toBe(5); + }); + + it('should cap at max mana', () => { + useManaStore.getState().gatherMana(200, 150); + expect(useManaStore.getState().rawMana).toBe(150); + }); + }); + + describe('convertMana', () => { + it('should convert raw mana to element mana', () => { + useManaStore.setState({ rawMana: 500 }); + const result = useManaStore.getState().convertMana('transference', 2); + expect(result).toBe(true); + expect(useManaStore.getState().rawMana).toBe(500 - 2 * MANA_PER_ELEMENT); + expect(useManaStore.getState().elements.transference.current).toBe(2); + }); + + it('should return false for locked element', () => { + const result = useManaStore.getState().convertMana('fire', 1); + expect(result).toBe(false); + }); + + it('should return false when insufficient raw mana', () => { + useManaStore.setState({ rawMana: 50 }); + const result = useManaStore.getState().convertMana('transference', 1); + expect(result).toBe(false); + }); + + it('should return false when element is at max', () => { + const elements = useManaStore.getState().elements; + elements.transference.current = elements.transference.max; + useManaStore.setState({ elements }); + const result = useManaStore.getState().convertMana('transference', 1); + expect(result).toBe(false); + }); + }); + + describe('unlockElement', () => { + it('should unlock element and deduct cost', () => { + const result = useManaStore.getState().unlockElement('fire', 50); + expect(result).toBe(true); + expect(useManaStore.getState().rawMana).toBe(50); + expect(useManaStore.getState().elements.fire.unlocked).toBe(true); + }); + + it('should return false when already unlocked', () => { + const result = useManaStore.getState().unlockElement('transference', 0); + expect(result).toBe(false); + }); + + it('should return false when insufficient mana', () => { + const result = useManaStore.getState().unlockElement('fire', 200); + expect(result).toBe(false); + }); + }); + + describe('addElementMana / spendElementMana', () => { + it('should add element mana up to max', () => { + useManaStore.getState().addElementMana('transference', 10, 50); + expect(useManaStore.getState().elements.transference.current).toBe(10); + }); + + it('should cap at max', () => { + useManaStore.getState().addElementMana('transference', 100, 50); + expect(useManaStore.getState().elements.transference.current).toBe(50); + }); + + it('should deduct element mana when sufficient', () => { + useManaStore.getState().addElementMana('transference', 20, 50); + const result = useManaStore.getState().spendElementMana('transference', 10); + expect(result).toBe(true); + expect(useManaStore.getState().elements.transference.current).toBe(10); + }); + + it('should return false when insufficient element mana', () => { + const result = useManaStore.getState().spendElementMana('transference', 10); + expect(result).toBe(false); + }); + }); + + describe('craftComposite', () => { + it('should craft metal from fire + earth', () => { + useManaStore.getState().unlockElement('fire', 0); + useManaStore.getState().unlockElement('earth', 0); + useManaStore.getState().addElementMana('fire', 5, 50); + useManaStore.getState().addElementMana('earth', 5, 50); + const result = useManaStore.getState().craftComposite('metal', ['fire', 'earth']); + expect(result).toBe(true); + expect(useManaStore.getState().elements.fire.current).toBe(4); + expect(useManaStore.getState().elements.earth.current).toBe(4); + expect(useManaStore.getState().elements.metal.current).toBe(1); + expect(useManaStore.getState().elements.metal.unlocked).toBe(true); + }); + + it('should return false when missing ingredients', () => { + const result = useManaStore.getState().craftComposite('metal', ['fire', 'earth']); + expect(result).toBe(false); + }); + }); + + describe('processConvertAction', () => { + it('should auto-convert raw mana to neediest unlocked element', () => { + useManaStore.getState().unlockElement('fire', 0); + const result = useManaStore.getState().processConvertAction(500); + expect(result).not.toBeNull(); + expect(result!.rawMana).toBe(0); + expect(result!.elements.fire.current).toBe(5); + }); + + it('should return null when raw mana < 100', () => { + useManaStore.getState().unlockElement('fire', 0); + const result = useManaStore.getState().processConvertAction(50); + expect(result).toBeNull(); + }); + + it('should return null when no unlocked elements need mana', () => { + const elements = useManaStore.getState().elements; + Object.keys(elements).forEach(k => { elements[k].current = elements[k].max; }); + useManaStore.setState({ elements }); + const result = useManaStore.getState().processConvertAction(500); + expect(result).toBeNull(); + }); + }); + + describe('resetMana', () => { + it('should reset to initial state for new loop', () => { + useManaStore.getState().resetMana({}, {}, {}, {}); + expect(useManaStore.getState().rawMana).toBe(10); + expect(useManaStore.getState().meditateTicks).toBe(0); + expect(useManaStore.getState().totalManaGathered).toBe(0); + }); + + it('should apply prestige upgrades for starting mana', () => { + useManaStore.getState().resetMana({ manaStart: 5 }, {}, {}, {}); + expect(useManaStore.getState().rawMana).toBe(10 + 5 * 10); + }); + }); +}); diff --git a/src/lib/game/__tests__/store-actions.test.ts b/src/lib/game/__tests__/store-actions.test.ts new file mode 100644 index 0000000..4c5c3d8 --- /dev/null +++ b/src/lib/game/__tests__/store-actions.test.ts @@ -0,0 +1,674 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useManaStore, makeInitialElements } from '../stores/manaStore'; +import { useCombatStore } from '../stores/combatStore'; +import { usePrestigeStore } from '../stores/prestigeStore'; +import { useDisciplineStore } from '../stores/discipline-slice'; +import { MANA_PER_ELEMENT } from '../constants'; +import { getFloorMaxHP } from '../utils'; + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +function resetManaStore() { + useManaStore.setState({ + rawMana: 100, + meditateTicks: 0, + totalManaGathered: 0, + elements: makeInitialElements(50, {}), + }); +} + +function resetCombatStore() { + useCombatStore.setState({ + currentFloor: 1, + floorHP: getFloorMaxHP(1), + floorMaxHP: getFloorMaxHP(1), + maxFloorReached: 1, + activeSpell: 'manaBolt', + currentAction: 'meditate', + castProgress: 0, + spireMode: false, + currentRoom: { roomType: 'combat', enemies: [], cleared: false }, + clearedFloors: {}, + climbDirection: null, + isDescending: false, + golemancy: { enabledGolems: [], summonedGolems: [], lastSummonFloor: 0 }, + equipmentSpellStates: [], + comboHitCount: 0, + floorHitCount: 0, + spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } }, + activityLog: [], + achievements: { unlocked: [], progress: {} }, + totalSpellsCast: 0, + totalDamageDealt: 0, + totalCraftsCompleted: 0, + }); +} + +function resetPrestigeStore() { + usePrestigeStore.setState({ + loopCount: 0, + insight: 500, + totalInsight: 500, + loopInsight: 0, + prestigeUpgrades: {}, + memorySlots: 3, + pactSlots: 1, + memories: [], + defeatedGuardians: [], + signedPacts: [], + signedPactDetails: {}, + pactRitualFloor: null, + pactRitualProgress: 0, + }); +} + +function resetDisciplineStore() { + useDisciplineStore.setState({ + disciplines: {}, + activeIds: [], + concurrentLimit: 1, + totalXP: 0, + }); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// MANA STORE +// ═══════════════════════════════════════════════════════════════════════════════ + +describe('ManaStore', () => { + beforeEach(resetManaStore); + + describe('setRawMana', () => { + it('should set raw mana to a positive value', () => { + useManaStore.getState().setRawMana(50); + expect(useManaStore.getState().rawMana).toBe(50); + }); + + it('should clamp negative values to 0', () => { + useManaStore.getState().setRawMana(-10); + expect(useManaStore.getState().rawMana).toBe(0); + }); + }); + + describe('addRawMana', () => { + it('should add raw mana up to max', () => { + useManaStore.getState().addRawMana(50, 200); + expect(useManaStore.getState().rawMana).toBe(150); + }); + + it('should cap at max mana', () => { + useManaStore.getState().addRawMana(200, 150); + expect(useManaStore.getState().rawMana).toBe(150); + }); + + it('should increment totalManaGathered', () => { + useManaStore.getState().addRawMana(50, 200); + expect(useManaStore.getState().totalManaGathered).toBe(50); + }); + }); + + describe('spendRawMana', () => { + it('should return true and deduct when sufficient', () => { + const result = useManaStore.getState().spendRawMana(30); + expect(result).toBe(true); + expect(useManaStore.getState().rawMana).toBe(70); + }); + + it('should return false when insufficient', () => { + const result = useManaStore.getState().spendRawMana(200); + expect(result).toBe(false); + expect(useManaStore.getState().rawMana).toBe(100); + }); + }); + + describe('gatherMana', () => { + it('should add mana and track total gathered', () => { + useManaStore.getState().gatherMana(5, 200); + expect(useManaStore.getState().rawMana).toBe(105); + expect(useManaStore.getState().totalManaGathered).toBe(5); + }); + + it('should cap at max mana', () => { + useManaStore.getState().gatherMana(200, 150); + expect(useManaStore.getState().rawMana).toBe(150); + }); + }); + + describe('convertMana', () => { + it('should convert raw mana to element mana', () => { + useManaStore.setState({ rawMana: 500 }); + const result = useManaStore.getState().convertMana('transference', 2); + expect(result).toBe(true); + expect(useManaStore.getState().rawMana).toBe(500 - 2 * MANA_PER_ELEMENT); + expect(useManaStore.getState().elements.transference.current).toBe(2); + }); + + it('should return false for locked element', () => { + const result = useManaStore.getState().convertMana('fire', 1); + expect(result).toBe(false); + }); + + it('should return false when insufficient raw mana', () => { + useManaStore.setState({ rawMana: 50 }); + const result = useManaStore.getState().convertMana('transference', 1); + expect(result).toBe(false); + }); + + it('should return false when element is at max', () => { + const elements = useManaStore.getState().elements; + elements.transference.current = elements.transference.max; + useManaStore.setState({ elements }); + const result = useManaStore.getState().convertMana('transference', 1); + expect(result).toBe(false); + }); + }); + + describe('unlockElement', () => { + it('should unlock element and deduct cost', () => { + const result = useManaStore.getState().unlockElement('fire', 50); + expect(result).toBe(true); + expect(useManaStore.getState().rawMana).toBe(50); + expect(useManaStore.getState().elements.fire.unlocked).toBe(true); + }); + + it('should return false when already unlocked', () => { + const result = useManaStore.getState().unlockElement('transference', 0); + expect(result).toBe(false); + }); + + it('should return false when insufficient mana', () => { + const result = useManaStore.getState().unlockElement('fire', 200); + expect(result).toBe(false); + }); + }); + + describe('addElementMana / spendElementMana', () => { + it('should add element mana up to max', () => { + useManaStore.getState().addElementMana('transference', 10, 50); + expect(useManaStore.getState().elements.transference.current).toBe(10); + }); + + it('should cap at max', () => { + useManaStore.getState().addElementMana('transference', 100, 50); + expect(useManaStore.getState().elements.transference.current).toBe(50); + }); + + it('should deduct element mana when sufficient', () => { + useManaStore.getState().addElementMana('transference', 20, 50); + const result = useManaStore.getState().spendElementMana('transference', 10); + expect(result).toBe(true); + expect(useManaStore.getState().elements.transference.current).toBe(10); + }); + + it('should return false when insufficient element mana', () => { + const result = useManaStore.getState().spendElementMana('transference', 10); + expect(result).toBe(false); + }); + }); + + describe('craftComposite', () => { + it('should craft metal from fire + earth', () => { + useManaStore.getState().unlockElement('fire', 0); + useManaStore.getState().unlockElement('earth', 0); + useManaStore.getState().addElementMana('fire', 5, 50); + useManaStore.getState().addElementMana('earth', 5, 50); + const result = useManaStore.getState().craftComposite('metal', ['fire', 'earth']); + expect(result).toBe(true); + expect(useManaStore.getState().elements.fire.current).toBe(4); + expect(useManaStore.getState().elements.earth.current).toBe(4); + expect(useManaStore.getState().elements.metal.current).toBe(1); + expect(useManaStore.getState().elements.metal.unlocked).toBe(true); + }); + + it('should return false when missing ingredients', () => { + const result = useManaStore.getState().craftComposite('metal', ['fire', 'earth']); + expect(result).toBe(false); + }); + }); + + describe('processConvertAction', () => { + it('should auto-convert raw mana to neediest unlocked element', () => { + useManaStore.getState().unlockElement('fire', 0); + const result = useManaStore.getState().processConvertAction(500); + expect(result).not.toBeNull(); + expect(result!.rawMana).toBe(0); + expect(result!.elements.fire.current).toBe(5); + }); + + it('should return null when raw mana < 100', () => { + useManaStore.getState().unlockElement('fire', 0); + const result = useManaStore.getState().processConvertAction(50); + expect(result).toBeNull(); + }); + + it('should return null when no unlocked elements need mana', () => { + const elements = useManaStore.getState().elements; + Object.keys(elements).forEach(k => { elements[k].current = elements[k].max; }); + useManaStore.setState({ elements }); + const result = useManaStore.getState().processConvertAction(500); + expect(result).toBeNull(); + }); + }); + + describe('resetMana', () => { + it('should reset to initial state for new loop', () => { + useManaStore.getState().resetMana({}, {}, {}, {}); + expect(useManaStore.getState().rawMana).toBe(10); + expect(useManaStore.getState().meditateTicks).toBe(0); + expect(useManaStore.getState().totalManaGathered).toBe(0); + }); + + it('should apply prestige upgrades for starting mana', () => { + useManaStore.getState().resetMana({ manaStart: 5 }, {}, {}, {}); + expect(useManaStore.getState().rawMana).toBe(10 + 5 * 10); + }); + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// COMBAT STORE +// ═══════════════════════════════════════════════════════════════════════════════ + +describe('CombatStore', () => { + beforeEach(resetCombatStore); + + describe('setCurrentFloor', () => { + it('should set floor and update HP', () => { + useCombatStore.getState().setCurrentFloor(5); + expect(useCombatStore.getState().currentFloor).toBe(5); + expect(useCombatStore.getState().floorHP).toBe(getFloorMaxHP(5)); + expect(useCombatStore.getState().floorMaxHP).toBe(getFloorMaxHP(5)); + }); + }); + + describe('advanceFloor', () => { + it('should increment floor and update HP', () => { + useCombatStore.getState().advanceFloor(); + expect(useCombatStore.getState().currentFloor).toBe(2); + expect(useCombatStore.getState().floorHP).toBe(getFloorMaxHP(2)); + }); + + it('should cap at floor 100', () => { + useCombatStore.setState({ currentFloor: 100 }); + useCombatStore.getState().advanceFloor(); + expect(useCombatStore.getState().currentFloor).toBe(100); + }); + + it('should update maxFloorReached', () => { + useCombatStore.getState().advanceFloor(); + expect(useCombatStore.getState().maxFloorReached).toBe(2); + }); + + it('should reset cast progress', () => { + useCombatStore.setState({ castProgress: 0.5 }); + useCombatStore.getState().advanceFloor(); + expect(useCombatStore.getState().castProgress).toBe(0); + }); + }); + + describe('setFloorHP', () => { + it('should set floor HP', () => { + useCombatStore.getState().setFloorHP(50); + expect(useCombatStore.getState().floorHP).toBe(50); + }); + + it('should clamp negative to 0', () => { + useCombatStore.getState().setFloorHP(-10); + expect(useCombatStore.getState().floorHP).toBe(0); + }); + }); + + describe('setMaxFloorReached', () => { + it('should update max floor reached', () => { + useCombatStore.getState().setMaxFloorReached(10); + expect(useCombatStore.getState().maxFloorReached).toBe(10); + }); + + it('should only increase, never decrease', () => { + useCombatStore.setState({ maxFloorReached: 10 }); + useCombatStore.getState().setMaxFloorReached(5); + expect(useCombatStore.getState().maxFloorReached).toBe(10); + }); + }); + + describe('setAction / setSpell', () => { + it('should set current action', () => { + useCombatStore.getState().setAction('climb'); + expect(useCombatStore.getState().currentAction).toBe('climb'); + }); + + it('should set active spell when learned', () => { + useCombatStore.getState().setSpell('manaBolt'); + expect(useCombatStore.getState().activeSpell).toBe('manaBolt'); + }); + + it('should not set spell when not learned', () => { + useCombatStore.getState().setSpell('fireball'); + expect(useCombatStore.getState().activeSpell).toBe('manaBolt'); + }); + }); + + describe('learnSpell', () => { + it('should add a new learned spell', () => { + useCombatStore.getState().learnSpell('fireball'); + expect(useCombatStore.getState().spells.fireball.learned).toBe(true); + }); + }); + + describe('debugSetFloor / resetFloorHP', () => { + it('should set floor and update HP', () => { + useCombatStore.getState().debugSetFloor(10); + expect(useCombatStore.getState().currentFloor).toBe(10); + expect(useCombatStore.getState().floorHP).toBe(getFloorMaxHP(10)); + }); + + it('should reset floor HP to max', () => { + useCombatStore.setState({ floorHP: 10 }); + useCombatStore.getState().resetFloorHP(); + expect(useCombatStore.getState().floorHP).toBe(useCombatStore.getState().floorMaxHP); + }); + }); + + describe('resetCombat', () => { + it('should reset to starting floor', () => { + useCombatStore.setState({ currentFloor: 50, maxFloorReached: 50 }); + useCombatStore.getState().resetCombat(1); + expect(useCombatStore.getState().currentFloor).toBe(1); + expect(useCombatStore.getState().maxFloorReached).toBe(1); + }); + }); + + describe('climbDownFloor', () => { + it('should decrement floor', () => { + useCombatStore.setState({ currentFloor: 5 }); + useCombatStore.getState().climbDownFloor(); + expect(useCombatStore.getState().currentFloor).toBe(4); + }); + + it('should not go below floor 1', () => { + useCombatStore.setState({ currentFloor: 1 }); + useCombatStore.getState().climbDownFloor(); + expect(useCombatStore.getState().currentFloor).toBe(1); + }); + }); + + describe('exitSpireMode', () => { + it('should reset spire state', () => { + useCombatStore.setState({ spireMode: true, climbDirection: 'up', currentAction: 'climb' }); + useCombatStore.getState().exitSpireMode(); + expect(useCombatStore.getState().spireMode).toBe(false); + expect(useCombatStore.getState().climbDirection).toBeNull(); + expect(useCombatStore.getState().currentAction).toBe('meditate'); + }); + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// PRESTIGE STORE +// ═══════════════════════════════════════════════════════════════════════════════ + +describe('PrestigeStore', () => { + beforeEach(resetPrestigeStore); + + describe('doPrestige', () => { + it('should purchase upgrade when affordable', () => { + const result = usePrestigeStore.getState().doPrestige('manaWell'); + expect(result).toBe(true); + expect(usePrestigeStore.getState().prestigeUpgrades.manaWell).toBe(1); + expect(usePrestigeStore.getState().insight).toBeLessThan(500); + }); + + it('should return false when cannot afford', () => { + usePrestigeStore.setState({ insight: 0 }); + const result = usePrestigeStore.getState().doPrestige('manaWell'); + expect(result).toBe(false); + }); + + it('should return false for invalid upgrade id', () => { + const result = usePrestigeStore.getState().doPrestige('nonexistent'); + expect(result).toBe(false); + }); + + it('should increase memorySlots with deepMemory', () => { + usePrestigeStore.setState({ insight: 2000 }); + const before = usePrestigeStore.getState().memorySlots; + usePrestigeStore.getState().doPrestige('deepMemory'); + expect(usePrestigeStore.getState().memorySlots).toBe(before + 1); + }); + }); + + describe('addMemory / removeMemory', () => { + it('should add a memory when slots available', () => { + usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 3 }); + expect(usePrestigeStore.getState().memories.length).toBe(1); + }); + + it('should not add duplicate memory', () => { + usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 3 }); + usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 5 }); + expect(usePrestigeStore.getState().memories.length).toBe(1); + }); + + it('should not exceed memory slots', () => { + usePrestigeStore.setState({ memorySlots: 1 }); + usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 1 }); + usePrestigeStore.getState().addMemory({ skillId: 'manaSpring', level: 1 }); + expect(usePrestigeStore.getState().memories.length).toBe(1); + }); + + it('should remove memory by skillId', () => { + usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 3 }); + usePrestigeStore.getState().removeMemory('manaFlow'); + expect(usePrestigeStore.getState().memories.length).toBe(0); + }); + + it('should clear all memories', () => { + usePrestigeStore.getState().addMemory({ skillId: 'manaFlow', level: 1 }); + usePrestigeStore.getState().addMemory({ skillId: 'manaSpring', level: 1 }); + usePrestigeStore.getState().clearMemories(); + expect(usePrestigeStore.getState().memories.length).toBe(0); + }); + }); + + describe('defeatGuardian / signedPacts', () => { + it('should add defeated guardian', () => { + usePrestigeStore.getState().defeatGuardian(10); + expect(usePrestigeStore.getState().defeatedGuardians).toContain(10); + }); + + it('should not duplicate defeated guardian', () => { + usePrestigeStore.getState().defeatGuardian(10); + usePrestigeStore.getState().defeatGuardian(10); + expect(usePrestigeStore.getState().defeatedGuardians.filter(f => f === 10).length).toBe(1); + }); + + it('should not defeat already signed guardian', () => { + usePrestigeStore.setState({ signedPacts: [10] }); + usePrestigeStore.getState().defeatGuardian(10); + expect(usePrestigeStore.getState().defeatedGuardians).not.toContain(10); + }); + + it('should add signed pact', () => { + usePrestigeStore.getState().addSignedPact(10); + expect(usePrestigeStore.getState().signedPacts).toContain(10); + }); + + it('should remove pact', () => { + usePrestigeStore.setState({ signedPacts: [10, 20] }); + usePrestigeStore.getState().removePact(10); + expect(usePrestigeStore.getState().signedPacts).not.toContain(10); + expect(usePrestigeStore.getState().signedPacts).toContain(20); + }); + }); + + describe('startPactRitual', () => { + it('should start ritual when conditions met', () => { + usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [], insight: 10000 }); + const result = usePrestigeStore.getState().startPactRitual(10, 10000); + expect(result).toBe(true); + expect(usePrestigeStore.getState().pactRitualFloor).toBe(10); + }); + + it('should return false when guardian not defeated', () => { + const result = usePrestigeStore.getState().startPactRitual(10, 10000); + expect(result).toBe(false); + }); + + it('should return false when already signed', () => { + usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [10] }); + const result = usePrestigeStore.getState().startPactRitual(10, 10000); + expect(result).toBe(false); + }); + + it('should return false when pact slots full', () => { + usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [20], pactSlots: 1 }); + const result = usePrestigeStore.getState().startPactRitual(10, 10000); + expect(result).toBe(false); + }); + + it('should return false when insufficient mana', () => { + usePrestigeStore.setState({ defeatedGuardians: [10], signedPacts: [] }); + const result = usePrestigeStore.getState().startPactRitual(10, 0); + expect(result).toBe(false); + }); + }); + + describe('cancelPactRitual', () => { + it('should cancel active ritual', () => { + usePrestigeStore.setState({ pactRitualFloor: 10, pactRitualProgress: 1 }); + usePrestigeStore.getState().cancelPactRitual(); + expect(usePrestigeStore.getState().pactRitualFloor).toBeNull(); + expect(usePrestigeStore.getState().pactRitualProgress).toBe(0); + }); + }); + + describe('startNewLoop', () => { + it('should increment loop count and add insight', () => { + usePrestigeStore.setState({ insight: 100, totalInsight: 100 }); + usePrestigeStore.getState().startNewLoop(50); + expect(usePrestigeStore.getState().loopCount).toBe(1); + expect(usePrestigeStore.getState().insight).toBe(150); + expect(usePrestigeStore.getState().totalInsight).toBe(150); + }); + + it('should reset loop-specific state', () => { + usePrestigeStore.setState({ + defeatedGuardians: [10], + signedPacts: [20], + pactRitualFloor: 10, + pactRitualProgress: 5, + }); + usePrestigeStore.getState().startNewLoop(0); + expect(usePrestigeStore.getState().defeatedGuardians).toEqual([]); + expect(usePrestigeStore.getState().signedPacts).toEqual([]); + expect(usePrestigeStore.getState().pactRitualFloor).toBeNull(); + }); + }); + + describe('resetPrestigeForNewLoop', () => { + it('should preserve insight and upgrades, reset loop state', () => { + usePrestigeStore.getState().resetPrestigeForNewLoop(200, { manaWell: 2 }, [], 4); + expect(usePrestigeStore.getState().insight).toBe(200); + expect(usePrestigeStore.getState().prestigeUpgrades).toEqual({ manaWell: 2 }); + expect(usePrestigeStore.getState().memorySlots).toBe(4); + expect(usePrestigeStore.getState().defeatedGuardians).toEqual([]); + }); + }); + + describe('resetPrestige', () => { + it('should reset everything to initial state', () => { + usePrestigeStore.setState({ insight: 1000, loopCount: 5, prestigeUpgrades: { manaWell: 3 } }); + usePrestigeStore.getState().resetPrestige(); + expect(usePrestigeStore.getState().insight).toBe(0); + expect(usePrestigeStore.getState().loopCount).toBe(0); + expect(usePrestigeStore.getState().prestigeUpgrades).toEqual({}); + }); + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// DISCIPLINE STORE +// ═══════════════════════════════════════════════════════════════════════════════ + +describe('DisciplineStore', () => { + beforeEach(resetDisciplineStore); + + describe('activate', () => { + it('should activate raw discipline', () => { + useDisciplineStore.getState().activate('raw-mastery'); + expect(useDisciplineStore.getState().activeIds).toContain('raw-mastery'); + expect(useDisciplineStore.getState().disciplines['raw-mastery'].paused).toBe(false); + }); + + it('should not activate same discipline twice', () => { + useDisciplineStore.getState().activate('raw-mastery'); + useDisciplineStore.getState().activate('raw-mastery'); + expect(useDisciplineStore.getState().activeIds.filter(id => id === 'raw-mastery').length).toBe(1); + }); + + it('should not activate when concurrent limit reached', () => { + useDisciplineStore.getState().activate('raw-mastery'); + useDisciplineStore.getState().activate('elemental-attunement'); + expect(useDisciplineStore.getState().activeIds.length).toBe(1); + }); + + it('should activate when no prior discipline state (optimistic)', () => { + useDisciplineStore.getState().activate('elemental-attunement', { + elements: { fire: { unlocked: false } }, + }); + expect(useDisciplineStore.getState().activeIds).toContain('elemental-attunement'); + }); + + it('should not activate when existing state has insufficient mana', () => { + useDisciplineStore.setState({ + disciplines: { 'raw-mastery': { id: 'raw-mastery', xp: 1000, paused: false } }, + }); + useDisciplineStore.getState().activate('raw-mastery', { elements: {} }); + expect(useDisciplineStore.getState().activeIds).not.toContain('raw-mastery'); + }); + + it('should activate when required element is unlocked', () => { + useDisciplineStore.getState().activate('elemental-attunement', { + elements: { fire: { unlocked: true } }, + }); + expect(useDisciplineStore.getState().activeIds).toContain('elemental-attunement'); + }); + }); + + describe('deactivate', () => { + it('should remove discipline from active list', () => { + useDisciplineStore.getState().activate('raw-mastery'); + useDisciplineStore.getState().deactivate('raw-mastery'); + expect(useDisciplineStore.getState().activeIds).not.toContain('raw-mastery'); + }); + }); + + describe('processTick', () => { + it('should accrue XP for active discipline', () => { + useDisciplineStore.getState().activate('raw-mastery'); + useDisciplineStore.getState().processTick({ rawMana: 1000, elements: {} }); + expect(useDisciplineStore.getState().disciplines['raw-mastery'].xp).toBe(1); + expect(useDisciplineStore.getState().totalXP).toBe(1); + }); + + it('should drain raw mana for raw discipline', () => { + useDisciplineStore.getState().activate('raw-mastery'); + const result = useDisciplineStore.getState().processTick({ rawMana: 1000, elements: {} }); + expect(result.rawMana).toBeLessThan(1000); + }); + + it('should pause discipline when insufficient mana', () => { + useDisciplineStore.getState().activate('raw-mastery'); + useDisciplineStore.getState().processTick({ rawMana: 0, elements: {} }); + expect(useDisciplineStore.getState().disciplines['raw-mastery'].paused).toBe(true); + }); + + it('should increase concurrent limit at 500 total XP', () => { + useDisciplineStore.setState({ totalXP: 499 }); + useDisciplineStore.getState().activate('raw-mastery'); + useDisciplineStore.getState().processTick({ rawMana: 1000, elements: {} }); + expect(useDisciplineStore.getState().totalXP).toBe(500); + expect(useDisciplineStore.getState().concurrentLimit).toBeGreaterThan(1); + }); + }); +}); diff --git a/src/lib/game/__tests__/test-setup.ts b/src/lib/game/__tests__/test-setup.ts deleted file mode 100644 index fd483d7..0000000 --- a/src/lib/game/__tests__/test-setup.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { beforeEach } from 'vitest'; -import { useGameStore } from '../stores/gameStore'; -import { useManaStore, makeInitialElements } from '../stores/manaStore'; -import { useCombatStore } from '../stores/combatStore'; -import { usePrestigeStore } from '../stores/prestigeStore'; -import { useUIStore } from '../stores/uiStore'; -import { useAttunementStore } from '../stores/attunementStore'; -import { useCraftingStore } from '../stores/craftingStore'; -import { useDisciplineStore } from '../stores/discipline-slice'; - -// Clear all zustand persist localStorage keys to prevent cross-test contamination -const _persistKeys = [ - 'mana-loop-ui-storage', - 'mana-loop-game-storage', - 'mana-loop-mana', - 'mana-loop-combat', - 'mana-loop-prestige', - 'mana-loop-attunements', - 'mana-loop-crafting', - 'mana-loop-discipline-store', -]; - -export function setupTickTestEnvironment(): void { - for (const key of _persistKeys) { - localStorage.removeItem(key); - } - useUIStore.setState({ paused: false, gameOver: false, victory: false, logs: [] }); - useGameStore.setState({ day: 1, hour: 0, incursionStrength: 0, containmentWards: 0, initialized: true }); - const elements = makeInitialElements(10, {}); - useManaStore.setState({ rawMana: 50, meditateTicks: 0, totalManaGathered: 0, elements }); - useCombatStore.setState({ - currentAction: 'meditate', currentFloor: 1, floorHP: 100, floorMaxHP: 100, - maxFloorReached: 1, castProgress: 0, spireMode: false, climbDirection: null, - isDescending: false, comboHitCount: 0, floorHitCount: 0, - }); - usePrestigeStore.setState({ - prestigeUpgrades: {}, signedPacts: [], loopInsight: 0, defeatedGuardians: [], - pactRitualFloor: null, pactRitualProgress: 0, - }); - useAttunementStore.setState({ - attunements: { enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 } }, - }); - useDisciplineStore.setState({ disciplines: {}, activeIds: [], concurrentLimit: 2, totalXP: 0 }); - useCraftingStore.setState({ - designProgress: null, designProgress2: null, preparationProgress: null, - applicationProgress: null, equipmentCraftingProgress: null, - }); -} diff --git a/src/lib/game/__tests__/tick-debug.test.ts b/src/lib/game/__tests__/tick-debug.test.ts deleted file mode 100644 index 696a90c..0000000 --- a/src/lib/game/__tests__/tick-debug.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { useGameStore } from '../stores/gameStore'; -import { useManaStore, makeInitialElements } from '../stores/manaStore'; -import { useCombatStore } from '../stores/combatStore'; -import { usePrestigeStore } from '../stores/prestigeStore'; -import { useUIStore } from '../stores/uiStore'; -import { useAttunementStore } from '../stores/attunementStore'; -import { useCraftingStore } from '../stores/craftingStore'; -import { useDisciplineStore } from '../stores/discipline-slice'; -import { computeRegen } from '../utils/mana-utils'; -import { computeDisciplineEffects } from '../effects/discipline-effects'; -import { getAttunementConversionRate, ATTUNEMENTS_DEF } from '../data/attunements'; -import { GUARDIANS, HOURS_PER_TICK } from '../constants'; - -beforeEach(() => { - useUIStore.setState({ paused: false, gameOver: false, victory: false, logs: [] }); - useGameStore.setState({ day: 1, hour: 0, incursionStrength: 0, containmentWards: 0, initialized: true }); - const elements = makeInitialElements(10, {}); - useManaStore.setState({ rawMana: 50, meditateTicks: 0, totalManaGathered: 0, elements }); - useCombatStore.setState({ - currentAction: 'meditate', currentFloor: 1, floorHP: 100, floorMaxHP: 100, - maxFloorReached: 1, castProgress: 0, spireMode: false, climbDirection: null, - isDescending: false, comboHitCount: 0, floorHitCount: 0, - }); - usePrestigeStore.setState({ - prestigeUpgrades: {}, signedPacts: [], loopInsight: 0, defeatedGuardians: [], - pactRitualFloor: null, pactRitualProgress: 0, - }); - useAttunementStore.setState({ - attunements: { enchanter: { id: 'enchanter', active: true, level: 1, experience: 0 } }, - }); - useDisciplineStore.setState({ disciplines: {}, activeIds: [], concurrentLimit: 2, totalXP: 0 }); - useCraftingStore.setState({ - designProgress: null, designProgress2: null, preparationProgress: null, - applicationProgress: null, equipmentCraftingProgress: null, - }); -}); - -describe('debug', () => { - it('trace regen values', () => { - // Measure actual regen empirically - const before = useManaStore.getState().rawMana; - useGameStore.getState().tick(); - const after = useManaStore.getState().rawMana; - const transferenceBefore = useManaStore.getState().elements.transference?.current || 0; - - // Do another tick to measure transference conversion - useGameStore.getState().tick(); - const transferenceAfter = useManaStore.getState().elements.transference?.current || 0; - - console.log('rawMana gain per tick:', after - before); - console.log('transference gain per tick:', transferenceAfter - transferenceBefore); - console.log('total regen (raw + conversion):', (after - before) + (transferenceAfter - transferenceBefore)); - - // Now compute what the tick does internally - const disciplineEffects = computeDisciplineEffects(useDisciplineStore.getState() as any); - console.log('disciplineEffects bonuses:', disciplineEffects.bonuses); - - const baseRegen = computeRegen( - { skills: {}, prestigeUpgrades: {}, skillUpgrades: {}, skillTiers: {}, attunements: {} }, - undefined, - disciplineEffects, - ); - console.log('baseRegen (as computed by tick):', baseRegen); - - let totalConversionPerHour = 0; - const attState = useAttunementStore.getState(); - Object.entries(attState.attunements).forEach(([id, state]) => { - if (!state.active) return; - const def = ATTUNEMENTS_DEF[id]; - if (!def || def.conversionRate <= 0 || !def.primaryManaType) return; - const scaledRate = getAttunementConversionRate(id, state.level || 1); - totalConversionPerHour += scaledRate; - }); - console.log('totalConversionPerHour:', totalConversionPerHour); - - const effectiveRegen = baseRegen - totalConversionPerHour; - console.log('effectiveRegen per hour:', effectiveRegen); - console.log('expected raw gain per tick:', effectiveRegen * HOURS_PER_TICK); - - expect(true).toBe(true); - }); - - it('check pact ritual with floor 30 (pactTime=6)', () => { - const g30 = GUARDIANS[30]; - console.log('Guardian 30 pactTime:', g30?.pactTime); - - usePrestigeStore.setState({ pactRitualFloor: 30, pactRitualProgress: 0 }); - - // Do 50 ticks (2 hours) - for (let i = 0; i < 50; i++) { - useGameStore.getState().tick(); - } - console.log('After 50 ticks (2 hours):'); - console.log(' pactRitualProgress:', usePrestigeStore.getState().pactRitualProgress); - console.log(' pactRitualFloor:', usePrestigeStore.getState().pactRitualFloor); - console.log(' signedPacts:', usePrestigeStore.getState().signedPacts); - - expect(true).toBe(true); - }); - - it('check if persist is leaking between tests', () => { - // Check prestige store state before any modifications - const prestigeState = usePrestigeStore.getState(); - console.log('prestigeState.pactRitualProgress:', prestigeState.pactRitualProgress); - console.log('prestigeState.pactRitualFloor:', prestigeState.pactRitualFloor); - console.log('prestigeState.signedPacts:', prestigeState.signedPacts); - - // Check discipline store - const disciplineState = useDisciplineStore.getState(); - console.log('disciplineState.disciplines:', JSON.stringify(disciplineState.disciplines)); - console.log('disciplineState.activeIds:', JSON.stringify(disciplineState.activeIds)); - - expect(true).toBe(true); - }); -}); diff --git a/src/lib/game/__tests__/tick-integration-pact.test.ts b/src/lib/game/__tests__/tick-integration-pact.test.ts deleted file mode 100644 index e644bb3..0000000 --- a/src/lib/game/__tests__/tick-integration-pact.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { useGameStore } from '../stores/gameStore'; -import { useManaStore } from '../stores/manaStore'; -import { useCombatStore } from '../stores/combatStore'; -import { usePrestigeStore } from '../stores/prestigeStore'; -import { useUIStore } from '../stores/uiStore'; -import { HOURS_PER_TICK } from '../constants'; -import { setupTickTestEnvironment } from './test-setup'; - -beforeEach(setupTickTestEnvironment); - -// ─── 8. Victory Condition ──────────────────────────────────────────────────── - -describe('victory condition', () => { - it('should trigger victory when maxFloorReached >= 100 and signedPacts includes 100', () => { - useCombatStore.setState({ maxFloorReached: 100 }); - usePrestigeStore.setState({ signedPacts: [100] }); - - useGameStore.getState().tick(); - - expect(useUIStore.getState().gameOver).toBe(true); - expect(useUIStore.getState().victory).toBe(true); - }); - - it('should set loopInsight on victory', () => { - useCombatStore.setState({ maxFloorReached: 100 }); - usePrestigeStore.setState({ signedPacts: [100] }); - - useGameStore.getState().tick(); - - expect(usePrestigeStore.getState().loopInsight).toBeGreaterThanOrEqual(0); - }); - - it('should not trigger victory with floor 100 but no pact', () => { - useCombatStore.setState({ maxFloorReached: 100 }); - usePrestigeStore.setState({ signedPacts: [] }); - - useGameStore.getState().tick(); - - expect(useUIStore.getState().victory).toBe(false); - expect(useUIStore.getState().gameOver).toBe(false); - }); - - it('should not trigger victory with pact 100 but floor < 100', () => { - useCombatStore.setState({ maxFloorReached: 99 }); - usePrestigeStore.setState({ signedPacts: [100] }); - - useGameStore.getState().tick(); - - expect(useUIStore.getState().victory).toBe(false); - }); -}); - -// ─── 9. Pact Ritual Progress ───────────────────────────────────────────────── - -describe('pact ritual progress', () => { - it('should increase pactRitualProgress by HOURS_PER_TICK per tick', () => { - usePrestigeStore.setState({ pactRitualFloor: 10, pactRitualProgress: 0 }); - useGameStore.getState().tick(); - expect(usePrestigeStore.getState().pactRitualProgress).toBeCloseTo(HOURS_PER_TICK, 10); - }); - - it('should accumulate pact ritual progress over multiple ticks', () => { - // Use floor 90 guardian (pactTime=20 hours) so 50 ticks (2 hours) doesn't complete it - usePrestigeStore.setState({ pactRitualFloor: 90, pactRitualProgress: 0 }); - const numTicks = 50; - for (let i = 0; i < numTicks; i++) { - useGameStore.getState().tick(); - } - expect(usePrestigeStore.getState().pactRitualProgress).toBeCloseTo( - numTicks * HOURS_PER_TICK, - 5, - ); - }); - - it('should not progress pact ritual when pactRitualFloor is null', () => { - usePrestigeStore.setState({ pactRitualFloor: null, pactRitualProgress: 0 }); - useGameStore.getState().tick(); - expect(usePrestigeStore.getState().pactRitualProgress).toBe(0); - }); -}); - -// ─── 10. Multiple Ticks Accumulation ───────────────────────────────────────── - -describe('multiple ticks accumulation', () => { - it('should correctly accumulate state over 100 ticks', () => { - const numTicks = 100; - const manaBefore = useManaStore.getState().rawMana; - - // Measure first tick regen (baseline, no meditation bonus yet) - useGameStore.getState().tick(); - const firstTickRegen = useManaStore.getState().rawMana - manaBefore; - - // Do 99 more ticks - for (let i = 0; i < 99; i++) { - useGameStore.getState().tick(); - } - - const game = useGameStore.getState(); - const mana = useManaStore.getState(); - - // Time should have advanced by 100 * HOURS_PER_TICK = 4 hours - const expectedTotalHours = numTicks * HOURS_PER_TICK; - const expectedDayIncrement = Math.floor(expectedTotalHours / 24); - const expectedHour = expectedTotalHours % 24; - - expect(game.day).toBe(1 + expectedDayIncrement); - expect(game.hour).toBeCloseTo(expectedHour, 5); - - // Mana should have accumulated (with meditation bonus increasing over time) - const totalGain = mana.rawMana - manaBefore; - const minExpected = firstTickRegen * numTicks; - const maxExpected = firstTickRegen * 1.5 * numTicks; - expect(totalGain).toBeGreaterThan(minExpected - 0.01); - expect(totalGain).toBeLessThan(maxExpected + 0.01); - - // Meditate ticks should match - expect(mana.meditateTicks).toBe(numTicks); - }); - - it('should correctly accumulate over enough ticks for a full day', () => { - const manaBefore = useManaStore.getState().rawMana; - - // Do 601 ticks (enough for a full day) - const numTicks = 601; - for (let i = 0; i < numTicks; i++) { - useGameStore.getState().tick(); - } - - expect(useGameStore.getState().day).toBe(2); - // Mana should have increased (capped at maxMana=100) - expect(useManaStore.getState().rawMana).toBeGreaterThan(manaBefore); - expect(useManaStore.getState().rawMana).toBeLessThanOrEqual(100); - }); -}); diff --git a/src/lib/game/__tests__/tick-integration.test.ts b/src/lib/game/__tests__/tick-integration.test.ts index fc048c5..4304fa3 100644 --- a/src/lib/game/__tests__/tick-integration.test.ts +++ b/src/lib/game/__tests__/tick-integration.test.ts @@ -1,224 +1,352 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { useGameStore } from '../stores/gameStore'; -import { useManaStore } from '../stores/manaStore'; +import { useManaStore, makeInitialElements } from '../stores/manaStore'; import { useCombatStore } from '../stores/combatStore'; import { usePrestigeStore } from '../stores/prestigeStore'; import { useUIStore } from '../stores/uiStore'; +import { useDisciplineStore } from '../stores/discipline-slice'; import { HOURS_PER_TICK, MAX_DAY, INCURSION_START_DAY } from '../constants'; -import { getMeditationBonus } from '../utils/mana-utils'; -import { getIncursionStrength } from '../utils/combat-utils'; -import { setupTickTestEnvironment } from './test-setup'; +import { getFloorMaxHP } from '../utils'; -beforeEach(setupTickTestEnvironment); +// ─── Helpers ────────────────────────────────────────────────────────────────── -// ─── 1. Time Progression ───────────────────────────────────────────────────── - -describe('time progression', () => { - it('should increase hour by HOURS_PER_TICK after one tick', () => { - useGameStore.getState().tick(); - expect(useGameStore.getState().hour).toBeCloseTo(HOURS_PER_TICK, 10); - expect(useGameStore.getState().day).toBe(1); +function resetAllStores() { + useUIStore.setState({ + paused: false, + gameOver: false, + victory: false, + logs: [], }); - it('should increment day after enough ticks for 24 hours', () => { - const ticks = 601; - for (let i = 0; i < ticks; i++) { - useGameStore.getState().tick(); - } - const { day, hour } = useGameStore.getState(); - expect(day).toBe(2); - expect(hour).toBeGreaterThanOrEqual(0); - expect(hour).toBeLessThan(1); + useGameStore.setState({ + day: 1, + hour: 0, + incursionStrength: 0, + containmentWards: 0, + initialized: true, }); - it('should advance multiple days correctly', () => { - const totalTicks = 601 * 3; - for (let i = 0; i < totalTicks; i++) { + useManaStore.setState({ + rawMana: 100, + meditateTicks: 0, + totalManaGathered: 0, + elements: makeInitialElements(50, {}), + }); + + useCombatStore.setState({ + currentFloor: 1, + floorHP: getFloorMaxHP(1), + floorMaxHP: getFloorMaxHP(1), + maxFloorReached: 1, + activeSpell: 'manaBolt', + currentAction: 'meditate', + castProgress: 0, + spireMode: false, + currentRoom: { roomType: 'combat', enemies: [], cleared: false }, + clearedFloors: {}, + climbDirection: null, + isDescending: false, + golemancy: { enabledGolems: [], summonedGolems: [], lastSummonFloor: 0 }, + equipmentSpellStates: [], + comboHitCount: 0, + floorHitCount: 0, + spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } }, + activityLog: [], + achievements: { unlocked: [], progress: {} }, + totalSpellsCast: 0, + totalDamageDealt: 0, + totalCraftsCompleted: 0, + }); + + usePrestigeStore.setState({ + loopCount: 0, + insight: 0, + totalInsight: 0, + loopInsight: 0, + prestigeUpgrades: {}, + memorySlots: 3, + pactSlots: 1, + memories: [], + defeatedGuardians: [], + signedPacts: [], + signedPactDetails: {}, + pactRitualFloor: null, + pactRitualProgress: 0, + }); + + useDisciplineStore.setState({ + disciplines: {}, + activeIds: [], + concurrentLimit: 1, + totalXP: 0, + }); +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// TICK INTEGRATION TESTS +// ═══════════════════════════════════════════════════════════════════════════════ + +describe('Tick Integration', () => { + beforeEach(resetAllStores); + + describe('time progression', () => { + it('should advance hour by HOURS_PER_TICK', () => { useGameStore.getState().tick(); - } - expect(useGameStore.getState().day).toBe(4); - }); -}); - -// ─── 2. Mana Regeneration ──────────────────────────────────────────────────── - -describe('mana regeneration', () => { - it('should increase rawMana after one tick', () => { - const before = useManaStore.getState().rawMana; - useGameStore.getState().tick(); - const after = useManaStore.getState().rawMana; - expect(after).toBeGreaterThan(before); - }); - - it('should accumulate mana over multiple ticks', () => { - const mana0 = useManaStore.getState().rawMana; - useGameStore.getState().tick(); - const mana1 = useManaStore.getState().rawMana; - const firstTickRegen = mana1 - mana0; - - for (let i = 0; i < 99; i++) { - useGameStore.getState().tick(); - } - const mana100 = useManaStore.getState().rawMana; - - const minExpected = mana0 + firstTickRegen * 100; - expect(mana100).toBeGreaterThan(minExpected - 0.01); - - const maxExpected = mana0 + firstTickRegen * 2 * 100; - expect(mana100).toBeLessThan(maxExpected + 0.01); - }); - - it('should not exceed maxMana', () => { - useManaStore.setState({ rawMana: 99.999 }); - for (let i = 0; i < 100; i++) { - useGameStore.getState().tick(); - } - expect(useManaStore.getState().rawMana).toBeLessThanOrEqual(100); - }); -}); - -// ─── 3. Incursion Penalty ──────────────────────────────────────────────────── - -describe('incursion penalty', () => { - it('should have zero incursion before INCURSION_START_DAY', () => { - useGameStore.setState({ day: INCURSION_START_DAY - 1, hour: 23.9 }); - useGameStore.getState().tick(); - const strength = getIncursionStrength(useGameStore.getState().day, useGameStore.getState().hour); - expect(strength).toBeCloseTo(0, 5); - }); - - it('should reduce mana regen after INCURSION_START_DAY', () => { - const mana0 = useManaStore.getState().rawMana; - useGameStore.getState().tick(); - const baseRegen = useManaStore.getState().rawMana - mana0; - - useGameStore.setState({ day: 25, hour: 0 }); - const mana25 = useManaStore.getState().rawMana; - useGameStore.getState().tick(); - const incursionRegen = useManaStore.getState().rawMana - mana25; - - expect(incursionRegen).toBeLessThan(baseRegen); - const incursion = getIncursionStrength(25, HOURS_PER_TICK); - expect(incursion).toBeGreaterThan(0); - }); - - it('should have stronger incursion on later days', () => { - const s1 = getIncursionStrength(21, 0); - const s2 = getIncursionStrength(28, 0); - expect(s2).toBeGreaterThan(s1); - }); -}); - -// ─── 4. Meditation ─────────────────────────────────────────────────────────── - -describe('meditation', () => { - it('should increment meditateTicks when currentAction is meditate', () => { - useCombatStore.setState({ currentAction: 'meditate' }); - useGameStore.getState().tick(); - expect(useManaStore.getState().meditateTicks).toBe(1); - }); - - it('should increase meditateTicks over multiple ticks', () => { - useCombatStore.setState({ currentAction: 'meditate' }); - for (let i = 0; i < 10; i++) { - useGameStore.getState().tick(); - } - expect(useManaStore.getState().meditateTicks).toBe(10); - }); - - it('should reset meditateTicks when action changes from meditate', () => { - useCombatStore.setState({ currentAction: 'meditate' }); - for (let i = 0; i < 5; i++) { - useGameStore.getState().tick(); - } - expect(useManaStore.getState().meditateTicks).toBe(5); - - useCombatStore.setState({ currentAction: 'climb' }); - useGameStore.getState().tick(); - expect(useManaStore.getState().meditateTicks).toBe(0); - }); - - it('should apply meditation multiplier to regen', () => { - useCombatStore.setState({ currentAction: 'meditate' }); - const ticksFor4Hours = Math.round(4 / HOURS_PER_TICK); - for (let i = 0; i < ticksFor4Hours; i++) { - useGameStore.getState().tick(); - } - - const meditateTicks = useManaStore.getState().meditateTicks; - const medMult = getMeditationBonus(meditateTicks, {}, 1); - expect(medMult).toBeGreaterThan(1); - - const manaBefore = useManaStore.getState().rawMana; - useGameStore.getState().tick(); - const meditatedRegen = useManaStore.getState().rawMana - manaBefore; - - useManaStore.setState({ rawMana: 50 }); - useCombatStore.setState({ currentAction: 'climb' }); - useGameStore.getState().tick(); - const unMeditatedRegen = useManaStore.getState().rawMana - 50; - - expect(meditatedRegen).toBeGreaterThan(unMeditatedRegen); - }); -}); - -// ─── 5. Loop End ────────────────────────────────────────────────────────────── - -describe('loop end', () => { - it('should set gameOver when day exceeds MAX_DAY', () => { - useGameStore.setState({ day: MAX_DAY, hour: 23.96 }); - useGameStore.getState().tick(); - expect(useUIStore.getState().gameOver).toBe(true); - }); - - it('should set loopInsight in prestigeStore when loop ends', () => { - useGameStore.setState({ day: MAX_DAY, hour: 23.96 }); - useGameStore.getState().tick(); - expect(usePrestigeStore.getState().loopInsight).toBeGreaterThanOrEqual(0); - }); - - it('should not set victory on normal loop end', () => { - useGameStore.setState({ day: MAX_DAY, hour: 23.96 }); - useGameStore.getState().tick(); - expect(useUIStore.getState().victory).toBe(false); - }); - - it('should log the loop end message', () => { - useGameStore.setState({ day: MAX_DAY, hour: 23.96 }); - useGameStore.getState().tick(); - const logs = useUIStore.getState().logs; - expect(logs.some(l => l.includes('loop ends'))).toBe(true); - }); -}); - -// ─── 6. Paused Game ─────────────────────────────────────────────────────────── - -describe('paused game', () => { - it('should be a no-op when paused is true', () => { - useUIStore.setState({ paused: true }); - const gameBefore = { ...useGameStore.getState() }; - const manaBefore = useManaStore.getState().rawMana; - - useGameStore.getState().tick(); - - expect(useGameStore.getState().hour).toBe(gameBefore.hour); - expect(useGameStore.getState().day).toBe(gameBefore.day); - expect(useManaStore.getState().rawMana).toBe(manaBefore); - }); -}); - -// ─── 7. Game Over ───────────────────────────────────────────────────────────── - -describe('game over', () => { - it('should be a no-op when gameOver is true', () => { - useUIStore.setState({ gameOver: true }); - const gameBefore = { ...useGameStore.getState() }; - const manaBefore = useManaStore.getState().rawMana; - - useGameStore.getState().tick(); - - expect(useGameStore.getState().hour).toBe(gameBefore.hour); - expect(useGameStore.getState().day).toBe(gameBefore.day); - expect(useManaStore.getState().rawMana).toBe(manaBefore); + expect(useGameStore.getState().hour).toBeCloseTo(HOURS_PER_TICK, 5); + }); + + it('should advance day when hour wraps past 24', () => { + // Set hour close to 24 + useGameStore.setState({ hour: 23.99 }); + useGameStore.getState().tick(); + expect(useGameStore.getState().day).toBe(2); + expect(useGameStore.getState().hour).toBeCloseTo(23.99 + HOURS_PER_TICK - 24, 5); + }); + + it('should advance multiple hours over many ticks', () => { + for (let i = 0; i < 100; i++) { + useGameStore.getState().tick(); + } + const expectedHour = (100 * HOURS_PER_TICK) % 24; + const expectedDay = 1 + Math.floor((100 * HOURS_PER_TICK) / 24); + expect(useGameStore.getState().day).toBe(expectedDay); + expect(useGameStore.getState().hour).toBeCloseTo(expectedHour, 5); + }); + }); + + describe('mana regeneration', () => { + it('should increase raw mana on tick (base regen)', () => { + useManaStore.setState({ rawMana: 50 }); + useGameStore.getState().tick(); + expect(useManaStore.getState().rawMana).toBeGreaterThan(50); + }); + + it('should cap raw mana at max', () => { + useManaStore.setState({ rawMana: 9999 }); + useGameStore.getState().tick(); + // Max mana with no skills/upgrades is 100 + expect(useManaStore.getState().rawMana).toBeLessThanOrEqual(100); + }); + + it('should not decrease totalManaGathered on tick', () => { + // Note: passive regen in tick() updates rawMana directly, not via addRawMana, + // so totalManaGathered only increases from gatherMana or combat loot. + // This is expected behavior — totalManaGathered tracks active gathering. + useManaStore.setState({ rawMana: 50, totalManaGathered: 5 }); + useGameStore.getState().tick(); + expect(useManaStore.getState().totalManaGathered).toBeGreaterThanOrEqual(5); + }); + }); + + describe('incursion penalty', () => { + it('should have no incursion before INCURSION_START_DAY', () => { + useGameStore.setState({ day: INCURSION_START_DAY - 1, hour: 23 }); + useGameStore.getState().tick(); + expect(useGameStore.getState().incursionStrength).toBe(0); + }); + + it('should apply incursion after INCURSION_START_DAY', () => { + useGameStore.setState({ day: INCURSION_START_DAY, hour: 1 }); + useGameStore.getState().tick(); + expect(useGameStore.getState().incursionStrength).toBeGreaterThan(0); + }); + + it('should reduce mana regen during incursion', () => { + // No incursion: day 1 + resetAllStores(); + useGameStore.setState({ day: 1, hour: 0 }); + useManaStore.setState({ rawMana: 50 }); + useGameStore.getState().tick(); + const regenNoIncursion = useManaStore.getState().rawMana - 50; + + // With incursion: day 25 + resetAllStores(); + useGameStore.setState({ day: 25, hour: 12 }); + useManaStore.setState({ rawMana: 50 }); + useGameStore.getState().tick(); + const regenWithIncursion = useManaStore.getState().rawMana - 50; + + expect(regenWithIncursion).toBeLessThan(regenNoIncursion); + }); + }); + + describe('meditation', () => { + it('should increment meditateTicks when action is meditate', () => { + useCombatStore.setState({ currentAction: 'meditate' }); + useGameStore.getState().tick(); + expect(useManaStore.getState().meditateTicks).toBe(1); + }); + + it('should reset meditateTicks when action changes', () => { + useCombatStore.setState({ currentAction: 'meditate' }); + useGameStore.getState().tick(); + useGameStore.getState().tick(); + expect(useManaStore.getState().meditateTicks).toBe(2); + + useCombatStore.setState({ currentAction: 'climb' }); + useGameStore.getState().tick(); + expect(useManaStore.getState().meditateTicks).toBe(0); + }); + + it('should boost regen with meditation', () => { + // Without meditation + resetAllStores(); + useCombatStore.setState({ currentAction: 'climb' }); + useManaStore.setState({ rawMana: 50, meditateTicks: 100 }); + useGameStore.getState().tick(); + const regenNoMeditate = useManaStore.getState().rawMana - 50; + + // With meditation (same ticks) + resetAllStores(); + useCombatStore.setState({ currentAction: 'meditate' }); + useManaStore.setState({ rawMana: 50, meditateTicks: 100 }); + useGameStore.getState().tick(); + const regenMeditate = useManaStore.getState().rawMana - 50; + + expect(regenMeditate).toBeGreaterThan(regenNoMeditate); + }); + }); + + describe('paused / game over', () => { + it('should not advance time when paused', () => { + useUIStore.setState({ paused: true }); + const before = useGameStore.getState().hour; + useGameStore.getState().tick(); + expect(useGameStore.getState().hour).toBe(before); + }); + + it('should not advance time when game over', () => { + useUIStore.setState({ gameOver: true }); + const before = useGameStore.getState().hour; + useGameStore.getState().tick(); + expect(useGameStore.getState().hour).toBe(before); + }); + + it('should not regenerate mana when paused', () => { + useUIStore.setState({ paused: true }); + useManaStore.setState({ rawMana: 50 }); + useGameStore.getState().tick(); + expect(useManaStore.getState().rawMana).toBe(50); + }); + }); + + describe('loop end', () => { + it('should trigger game over when day > MAX_DAY', () => { + useGameStore.setState({ day: MAX_DAY, hour: 23.99 }); + useGameStore.getState().tick(); + expect(useUIStore.getState().gameOver).toBe(true); + }); + + it('should set loopInsight when loop ends', () => { + useGameStore.setState({ day: MAX_DAY, hour: 23.99 }); + useGameStore.getState().tick(); + expect(usePrestigeStore.getState().loopInsight).toBeGreaterThan(0); + }); + + it('should not be victory when loop ends normally', () => { + useGameStore.setState({ day: MAX_DAY, hour: 23.99 }); + useGameStore.getState().tick(); + expect(useUIStore.getState().victory).toBe(false); + }); + }); + + describe('victory condition', () => { + it('should trigger victory when floor 100 reached with signed pact', () => { + useCombatStore.setState({ maxFloorReached: 100 }); + usePrestigeStore.setState({ signedPacts: [100] }); + useGameStore.getState().tick(); + expect(useUIStore.getState().gameOver).toBe(true); + expect(useUIStore.getState().victory).toBe(true); + }); + + it('should not trigger victory without signed pact', () => { + useCombatStore.setState({ maxFloorReached: 100 }); + usePrestigeStore.setState({ signedPacts: [] }); + useGameStore.getState().tick(); + expect(useUIStore.getState().victory).toBe(false); + }); + + it('should not trigger victory without floor 100', () => { + useCombatStore.setState({ maxFloorReached: 99 }); + usePrestigeStore.setState({ signedPacts: [100] }); + useGameStore.getState().tick(); + expect(useUIStore.getState().victory).toBe(false); + }); + }); + + describe('pact ritual progress', () => { + it('should advance pact ritual progress on tick', () => { + usePrestigeStore.setState({ pactRitualFloor: 10, pactRitualProgress: 0 }); + useGameStore.getState().tick(); + expect(usePrestigeStore.getState().pactRitualProgress).toBeGreaterThan(0); + }); + + it('should not advance pact ritual when not active', () => { + usePrestigeStore.setState({ pactRitualFloor: null }); + useGameStore.getState().tick(); + expect(usePrestigeStore.getState().pactRitualProgress).toBe(0); + }); + }); + + describe('multiple ticks', () => { + it('should accumulate mana over multiple ticks', () => { + useManaStore.setState({ rawMana: 10 }); + for (let i = 0; i < 50; i++) { + useGameStore.getState().tick(); + } + expect(useManaStore.getState().rawMana).toBeGreaterThan(10); + }); + + it('should advance time correctly over many ticks', () => { + const numTicks = 625; // 625 * 0.04 = 25 hours = 1 day + 1 hour + for (let i = 0; i < numTicks; i++) { + useGameStore.getState().tick(); + } + expect(useGameStore.getState().day).toBe(2); + expect(useGameStore.getState().hour).toBeCloseTo(1, 5); + }); + + it('should not lose totalManaGathered over ticks', () => { + useManaStore.setState({ rawMana: 10, totalManaGathered: 42 }); + for (let i = 0; i < 10; i++) { + useGameStore.getState().tick(); + } + // totalManaGathered should stay at 42 (passive regen doesn't change it) + expect(useManaStore.getState().totalManaGathered).toBe(42); + }); + }); + + describe('incursion strength progression', () => { + it('should be near 0 at start of incursion day', () => { + // At INCURSION_START_DAY hour 0, incursion is 0, but tick advances hour first + // so after tick, hour=0.04 and incursion is very small but > 0 + useGameStore.setState({ day: INCURSION_START_DAY, hour: 0 }); + useGameStore.getState().tick(); + // After tick, hour advanced by HOURS_PER_TICK, so incursion is small + expect(useGameStore.getState().incursionStrength).toBeGreaterThanOrEqual(0); + expect(useGameStore.getState().incursionStrength).toBeLessThan(0.01); + }); + + it('should increase over the course of the incursion', () => { + // Early incursion + resetAllStores(); + useGameStore.setState({ day: INCURSION_START_DAY, hour: 1 }); + useGameStore.getState().tick(); + const early = useGameStore.getState().incursionStrength; + + // Late incursion + resetAllStores(); + useGameStore.setState({ day: MAX_DAY, hour: 23 }); + useGameStore.getState().tick(); + const late = useGameStore.getState().incursionStrength; + + expect(late).toBeGreaterThan(early); + }); + + it('should cap at 0.95', () => { + useGameStore.setState({ day: MAX_DAY, hour: 23.99 }); + useGameStore.getState().tick(); + expect(useGameStore.getState().incursionStrength).toBeLessThanOrEqual(0.95); + }); }); }); diff --git a/src/lib/game/crafting-actions/design-actions.ts b/src/lib/game/crafting-actions/design-actions.ts index ed67aff..9bed0ae 100644 --- a/src/lib/game/crafting-actions/design-actions.ts +++ b/src/lib/game/crafting-actions/design-actions.ts @@ -1,6 +1,6 @@ // ─── Enchantment Design Actions ──────────────────────────────────────────── -import type { GameState, EnchantmentDesign, DesignEffect } from '../types'; +import type { GameState, EnchantmentDesign, DesignEffect, DesignProgress } from '../types'; import * as CraftingUtils from '../crafting-utils'; import * as CraftingDesign from '../crafting-design'; import { computeEffects } from '../effects/upgrade-effects'; @@ -35,7 +35,7 @@ export function startDesigningEnchantment( const computedEffects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {}); const hasEnchantMastery = hasSpecial(computedEffects, SPECIAL_EFFECTS.ENCHANT_MASTERY); - let updates: any = {}; + let updates: Partial = {}; if (!state.designProgress) { updates = { diff --git a/src/lib/game/crafting-apply.ts b/src/lib/game/crafting-apply.ts index f811f36..d93011d 100644 --- a/src/lib/game/crafting-apply.ts +++ b/src/lib/game/crafting-apply.ts @@ -4,7 +4,7 @@ import type { EquipmentInstance, AppliedEnchantment, EnchantmentDesign, ApplicationProgress } from './types'; import { calculateApplicationTime, calculateApplicationManaPerHour } from './crafting-utils'; import { hasSpecial, SPECIAL_EFFECTS } from './effects/special-effects'; -import { computeEffects } from './effects/upgrade-effects'; +import type { ComputedEffects } from './effects/upgrade-effects.types'; import type { AttunementState } from './types'; import { calculateEnchantingXP, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from './data/attunements'; import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects'; @@ -91,7 +91,7 @@ export function calculateApplicationTick( required: number, currentManaSpent: number, manaPerTick: number, - computedEffects: any + computedEffects: ComputedEffects ): ApplicationTickResult { let progress = currentProgress + 0.04; let manaSpent = currentManaSpent + manaPerTick; @@ -131,7 +131,7 @@ export function calculateApplicationTick( export function applyEnchantments( instance: EquipmentInstance, design: EnchantmentDesign, - computedEffects: any + computedEffects: ComputedEffects ): { updatedInstance: EquipmentInstance; xpGained: number; diff --git a/src/lib/game/crafting-design.ts b/src/lib/game/crafting-design.ts index d432a9c..857cbbe 100644 --- a/src/lib/game/crafting-design.ts +++ b/src/lib/game/crafting-design.ts @@ -1,11 +1,12 @@ // ─── Crafting Design System ───────────────────────────────────────────────── // Design system functions: calculateDesignTime, capacity cost, XP, etc. -import type { EnchantmentDesign, DesignEffect, AppliedEnchantment } from './types'; +import type { EnchantmentDesign, DesignEffect, AppliedEnchantment, DesignProgress } from './types'; +import type { EquipmentInstance } from './types'; +import type { ComputedEffects } from './effects/upgrade-effects.types'; import { calculateEnchantingXP } from './data/attunements'; import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects'; import { hasSpecial, SPECIAL_EFFECTS } from './effects/special-effects'; -import { computeEffects } from './effects/upgrade-effects'; import { EQUIPMENT_TYPES, type EquipmentCategory } from './data/equipment'; // ─── Design Creation & Calculation ────────────────────────────────────────── @@ -110,7 +111,7 @@ export function calculateDesignTime(effects: DesignEffect[]): number { export function getDesignTimeWithHaste( effects: DesignEffect[], isRepeatDesign: boolean, - computedEffects: any + computedEffects: ComputedEffects ): number { let time = calculateDesignTime(effects); if (isRepeatDesign && hasSpecial(computedEffects, SPECIAL_EFFECTS.HASTY_ENCHANTER)) { @@ -131,7 +132,7 @@ export interface DesignProgressUpdate { export function calculateDesignProgress( currentProgress: number, required: number, - computedEffects: any, + computedEffects: ComputedEffects, isRepeatDesign: boolean ): DesignProgressUpdate { let progress = currentProgress + 0.04; @@ -152,15 +153,15 @@ export function calculateDesignProgress( export function calculateSecondDesignProgress( currentProgress: number, required: number, - computedEffects: any, + computedEffects: ComputedEffects, isRepeatDesign: boolean ): DesignProgressUpdate { return calculateDesignProgress(currentProgress, required, computedEffects, isRepeatDesign); } export function isSecondDesignSlotAvailable( - designProgress: any, - designProgress2: any, + designProgress: DesignProgress | null, + designProgress2: DesignProgress | null, hasEnchantMastery: boolean ): boolean { if (!designProgress && !designProgress2) return true; @@ -208,18 +209,11 @@ export function filterDesignsByEquipment( if (!equipment) return []; return designs.map(design => ({ design, - fitsInEquipment: designFitsInEquipment(design, { - ...equipment, - enchantments: [], - rarity: 'common', - quality: 100, - typeId: '', - name: '', - } as any), + fitsInEquipment: designFitsInEquipment(design, equipment), availableCapacity: equipment.totalCapacity - equipment.usedCapacity, })); } -function designFitsInEquipment(design: EnchantmentDesign, instance: any): boolean { +function designFitsInEquipment(design: EnchantmentDesign, instance: { usedCapacity: number; totalCapacity: number }): boolean { return (instance.usedCapacity || 0) + design.totalCapacityUsed <= instance.totalCapacity; } diff --git a/src/lib/game/effects.ts b/src/lib/game/effects.ts index 7dfc277..3f86655 100755 --- a/src/lib/game/effects.ts +++ b/src/lib/game/effects.ts @@ -4,12 +4,12 @@ // - Skill upgrade effects (from milestone upgrades) // - Equipment enchantment effects (from enchanted gear) -import type { GameState, EquipmentInstance } from './types'; +import type { EquipmentInstance } from './types'; import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects'; import { computeEffects } from './effects/upgrade-effects'; import { hasSpecial, SPECIAL_EFFECTS } from './effects/special-effects'; import { computeDisciplineEffects } from './effects/discipline-effects'; -import type { ComputedEffects } from './upgrade-effects.types'; +import type { ComputedEffects } from './effects/upgrade-effects.types'; // Re-export for convenience export { computeEffects } from './effects/upgrade-effects'; @@ -65,12 +65,6 @@ export function computeEquipmentEffects( return { bonuses, multipliers, specials }; } -// ─── Discipline Effects Integration ────────────────────────────────────────── - -export function getDisciplineEffects(state: GameState) { - return computeDisciplineEffects(state); -} - // ─── Unified Computed Effects ───────────────────────────────────────────────── export interface UnifiedEffects extends ComputedEffects { @@ -87,11 +81,10 @@ export function computeAllEffects( skillTiers: Record, equipmentInstances: Record, equippedInstances: Record, - gameState: GameState ): UnifiedEffects { const upgradeEffects = computeEffects(skillUpgrades, skillTiers); const equipmentEffects = computeEquipmentEffects(equipmentInstances, equippedInstances, upgradeEffects.enchantmentPowerMultiplier); - const disciplineEffects = getDisciplineEffects(gameState); + const disciplineEffects = computeDisciplineEffects(); const perElementCapBonus: Record = { ...upgradeEffects.perElementCapBonus }; for (const [key, value] of Object.entries(equipmentEffects.bonuses)) { @@ -137,49 +130,73 @@ export function computeAllEffects( return merged; } -export function getUnifiedEffects(state: Pick): UnifiedEffects { +export function getUnifiedEffects(state: { + skillUpgrades?: Record; + skillTiers?: Record; + equipmentInstances?: Record; + equippedInstances?: Record; +}): UnifiedEffects { return computeAllEffects( state.skillUpgrades || {}, state.skillTiers || {}, state.equipmentInstances || {}, state.equippedInstances || {}, - state as unknown as GameState ); } // ─── Stat Computation with All Effects ─────────────────────────────────────── export function computeTotalMaxMana( - state: Pick, + state: { + skills?: Record; + prestigeUpgrades?: Record; + skillUpgrades?: Record; + skillTiers?: Record; + equipmentInstances?: Record; + equippedInstances?: Record; + }, effects?: UnifiedEffects ): number { const pu = state.prestigeUpgrades; const skillMult = effects?.skillLevelMultiplier || 1; const base = 100 + ((state.skills || {}).manaWell || 0) * 100 * skillMult + ((pu || {}).manaWell || 0) * 500; - if (!effects) effects = getUnifiedEffects(state as any); - return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier); + const resolvedEffects = effects || getUnifiedEffects(state); + return Math.floor((base + resolvedEffects.maxManaBonus) * resolvedEffects.maxManaMultiplier); } export function computeTotalRegen( - state: Pick, + state: { + skills?: Record; + prestigeUpgrades?: Record; + skillUpgrades?: Record; + skillTiers?: Record; + equipmentInstances?: Record; + equippedInstances?: Record; + }, effects?: UnifiedEffects ): number { const pu = state.prestigeUpgrades; - const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1; + const temporalBonus = 1 + ((pu?.temporalEcho || 0)) * 0.1; const skillMult = effects?.skillLevelMultiplier || 1; - const base = 2 + (state.skills.manaFlow || 0) * 1 * skillMult + (state.skills.manaSpring || 0) * 2 * skillMult + (pu.manaFlow || 0) * 0.5; + const base = 2 + (state.skills?.manaFlow || 0) * 1 * skillMult + (state.skills?.manaSpring || 0) * 2 * skillMult + (pu?.manaFlow || 0) * 0.5; let regen = base * temporalBonus; - if (!effects) effects = getUnifiedEffects(state as any); - regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier; + const resolvedEffects = effects || getUnifiedEffects(state); + regen = (regen + resolvedEffects.regenBonus + resolvedEffects.permanentRegenBonus) * resolvedEffects.regenMultiplier; return regen; } export function computeTotalClickMana( - state: Pick, + state: { + skills?: Record; + skillUpgrades?: Record; + skillTiers?: Record; + equipmentInstances?: Record; + equippedInstances?: Record; + }, effects?: UnifiedEffects ): number { const skillMult = effects?.skillLevelMultiplier || 1; - const base = 1 + (state.skills.manaTap || 0) * 1 * skillMult + (state.skills.manaSurge || 0) * 3 * skillMult; - if (!effects) effects = getUnifiedEffects(state as any); - return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier); -} \ No newline at end of file + const base = 1 + (state.skills?.manaTap || 0) * 1 * skillMult + (state.skills?.manaSurge || 0) * 3 * skillMult; + const resolvedEffects = effects || getUnifiedEffects(state); + return Math.floor((base + resolvedEffects.clickManaBonus) * resolvedEffects.clickManaMultiplier); +} diff --git a/src/lib/game/effects/discipline-effects.ts b/src/lib/game/effects/discipline-effects.ts index 12bc72a..3cbc5c5 100644 --- a/src/lib/game/effects/discipline-effects.ts +++ b/src/lib/game/effects/discipline-effects.ts @@ -1,12 +1,13 @@ // ─── Discipline Effects ─────────────────────────────────────────────────────── // Computes bonuses from active disciplines and integrates with the unified effect system -import type { GameState } from '../types'; +import type { DisciplineStoreState } from '../stores/discipline-slice'; +import type { DisciplineState } from '../types/disciplines'; import { useDisciplineStore } from '../stores/discipline-slice'; import { ALL_DISCIPLINES } from '../data/disciplines'; import { calculateStatBonus, getUnlockedPerks } from '../utils/discipline-math'; -export function computeDisciplineEffects(state: GameState): { +export function computeDisciplineEffects(_state?: DisciplineStoreState): { bonuses: Record; multipliers: Record; specials: Set; @@ -15,7 +16,7 @@ export function computeDisciplineEffects(state: GameState): { const activeDiscs = Object.entries(disciplines) .filter(([, disc]) => disc && !disc.paused) .map(([id, disc]) => ({ id, disc, def: ALL_DISCIPLINES.find(d => d.id === id) })) - .filter((entry): entry is { id: string; disc: any; def: NonNullable } => !!entry.def); + .filter((entry): entry is { id: string; disc: DisciplineState; def: NonNullable } => !!entry.def); const bonuses: Record = {}; const multipliers: Record = {}; diff --git a/src/lib/game/stores/combat-actions.ts b/src/lib/game/stores/combat-actions.ts index 6dab0bc..fb075f7 100644 --- a/src/lib/game/stores/combat-actions.ts +++ b/src/lib/game/stores/combat-actions.ts @@ -38,7 +38,7 @@ export function processCombatTick( } // Compute discipline bonuses once per tick - const disciplineEffects = computeDisciplineEffects(useDisciplineStore.getState() as any); + const disciplineEffects = computeDisciplineEffects(); // Calculate cast speed (no skill bonus) const totalAttackSpeed = attackSpeedMult; diff --git a/src/lib/game/stores/craftingStore.ts b/src/lib/game/stores/craftingStore.ts index 5d60ad8..5d3646f 100644 --- a/src/lib/game/stores/craftingStore.ts +++ b/src/lib/game/stores/craftingStore.ts @@ -2,6 +2,7 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import type { DesignProgress, EnchantmentDesign, DesignEffect } from '../types'; +import type { EquipmentSlot } from '../types/equipmentSlot'; import type { CraftingStore, CraftingState } from './craftingStore.types'; import * as CraftingUtils from '../crafting-utils'; import * as CraftingDesign from '../crafting-design'; @@ -342,7 +343,7 @@ export const useCraftingStore = create()( let newEquipped = { ...state.equippedInstances }; for (const [slot, id] of Object.entries(newEquipped)) { if (id === instanceId) { - newEquipped[slot as any] = null; + newEquipped[slot as EquipmentSlot] = null; } } diff --git a/src/lib/game/stores/gameActions.ts b/src/lib/game/stores/gameActions.ts index 89f0846..3122398 100644 --- a/src/lib/game/stores/gameActions.ts +++ b/src/lib/game/stores/gameActions.ts @@ -1,4 +1,5 @@ import { computeMaxMana, computeClickMana } from '../utils'; +import type { GameCoordinatorState } from './gameStore'; import { useUIStore } from './uiStore'; import { usePrestigeStore } from './prestigeStore'; import { useManaStore } from './manaStore'; @@ -6,7 +7,7 @@ import { useCombatStore } from './combatStore'; import { computeDisciplineEffects } from '../effects/discipline-effects'; import { useDisciplineStore } from './discipline-slice'; -export const createResetGame = (set: (state: any) => void, initialState: any) => () => { +export const createResetGame = (set: (state: Partial) => void, initialState: GameCoordinatorState) => () => { // Clear all persisted state if (typeof window !== 'undefined') { localStorage.removeItem('mana-loop-ui-storage'); @@ -34,7 +35,7 @@ export const createResetGame = (set: (state: any) => void, initialState: any) => export const createGatherMana = () => () => { const prestigeState = usePrestigeStore.getState(); - const disciplineEffects = computeDisciplineEffects(useDisciplineStore.getState() as any); + const disciplineEffects = computeDisciplineEffects(); // Compute click mana with discipline bonuses (mana-channeling → clickManaMultiplier) const cm = computeClickMana( diff --git a/src/lib/game/stores/gameHooks.ts b/src/lib/game/stores/gameHooks.ts index 9d222f1..1bd78bc 100644 --- a/src/lib/game/stores/gameHooks.ts +++ b/src/lib/game/stores/gameHooks.ts @@ -6,7 +6,7 @@ import { useCombatStore } from './combatStore'; import { useUIStore } from './uiStore'; import { useCraftingStore } from './craftingStore'; import { useDisciplineStore } from './discipline-slice'; -import { getUnifiedEffects } from '../effects'; +import { getUnifiedEffects, type UnifiedEffects } from '../effects'; import { computeDisciplineEffects } from '../effects/discipline-effects'; import { computeMaxMana, @@ -32,8 +32,7 @@ export function useGameLoop() { export function useUnifiedEffects() { const equippedInstances = useCraftingStore((s) => s.equippedInstances); const equipmentInstances = useCraftingStore((s) => s.equipmentInstances); - const disciplineStoreState = useDisciplineStore(); - const disciplineEffects = computeDisciplineEffects(disciplineStoreState as any); + const disciplineEffects = computeDisciplineEffects(); return { ...getUnifiedEffects({ @@ -56,8 +55,7 @@ export function useManaStats() { const hour = useGameStore((s) => s.hour); const equippedInstances = useCraftingStore((s) => s.equippedInstances); const equipmentInstances = useCraftingStore((s) => s.equipmentInstances); - const disciplineStoreState = useDisciplineStore(); - const disciplineEffects = computeDisciplineEffects(disciplineStoreState as any); + const disciplineEffects = computeDisciplineEffects(); const upgradeEffects = getUnifiedEffects({ skillUpgrades: {}, @@ -68,13 +66,13 @@ export function useManaStats() { const maxMana = computeMaxMana( { skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {} }, - upgradeEffects as any, + upgradeEffects, disciplineEffects, ); const baseRegen = computeRegen( { skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, attunements: {} }, - upgradeEffects as any, + upgradeEffects, disciplineEffects, ); @@ -82,18 +80,18 @@ export function useManaStats() { skills: {}, }, disciplineEffects); - const meditationMultiplier = getMeditationBonus(meditateTicks, {}, (upgradeEffects as any).meditationEfficiency); + const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency); const incursionStrength = getIncursionStrength(day, hour); const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength); // Mana Cascade bonus - const manaCascadeBonus = (upgradeEffects as any).specials.has('mana_cascade') - ? Math.floor(maxMana /100) * 0.1 + const manaCascadeBonus = upgradeEffects.specials.has('mana_cascade') + ? Math.floor(maxMana / 100) * 0.1 : 0; // Mana Waterfall bonus - const manaWaterfallBonus = (upgradeEffects as any).specials.has('mana_waterfall') - ? Math.floor(maxMana /100) * 0.25 + const manaWaterfallBonus = upgradeEffects.specials.has('mana_waterfall') + ? Math.floor(maxMana / 100) * 0.25 : 0; const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier; diff --git a/src/lib/game/stores/gameLoopActions.ts b/src/lib/game/stores/gameLoopActions.ts index a1f3e02..af9f966 100644 --- a/src/lib/game/stores/gameLoopActions.ts +++ b/src/lib/game/stores/gameLoopActions.ts @@ -1,4 +1,5 @@ import { calcInsight, getFloorMaxHP } from '../utils'; +import type { GameCoordinatorState } from './gameStore'; import { makeInitialSpells } from './combatStore'; import { SPELLS_DEF } from '../constants'; import { useUIStore } from './uiStore'; @@ -8,12 +9,12 @@ import { useCombatStore } from './combatStore'; import { computeDisciplineEffects } from '../effects/discipline-effects'; import { useDisciplineStore } from './discipline-slice'; -export const createStartNewLoop = (set: (state: any) => void) => () => { +export const createStartNewLoop = (set: (state: Partial) => void) => () => { const prestigeState = usePrestigeStore.getState(); const combatState = useCombatStore.getState(); const manaState = useManaStore.getState(); - const disciplineEffects = computeDisciplineEffects(useDisciplineStore.getState() as any); + const disciplineEffects = computeDisciplineEffects(); const insightGained = prestigeState.loopInsight || calcInsight({ maxFloorReached: combatState.maxFloorReached, totalManaGathered: manaState.totalManaGathered, diff --git a/src/lib/game/stores/gameStore.ts b/src/lib/game/stores/gameStore.ts index 1dfded2..2e2b97f 100755 --- a/src/lib/game/stores/gameStore.ts +++ b/src/lib/game/stores/gameStore.ts @@ -79,15 +79,13 @@ export const useGameStore = create()( const manaState = useManaStore.getState(); const combatState = useCombatStore.getState(); const craftingState = useCraftingStore.getState(); - const disciplineStoreState = useDisciplineStore.getState(); - // Compute equipment specials from enchanted gear const equipmentEffects = computeEquipmentEffects( craftingState.equipmentInstances || {}, craftingState.equippedInstances || {} ); // Compute discipline specials from active discipline perks - const disciplineEffects = computeDisciplineEffects(disciplineStoreState as any); + const disciplineEffects = computeDisciplineEffects(); // Merge all specials into a single set for hasSpecial checks const allSpecials = new Set([ ...equipmentEffects.specials, diff --git a/src/lib/game/utils/activity-log.ts b/src/lib/game/utils/activity-log.ts index d132e7f..12baeae 100644 --- a/src/lib/game/utils/activity-log.ts +++ b/src/lib/game/utils/activity-log.ts @@ -1,7 +1,7 @@ // ─── Activity Log Helper ─────────────────────────────────────────────── // Moved from store-modules/activity-log.ts to eliminate legacy dependencies -import type { ActivityLogEntry } from '../types'; +import type { ActivityEventType, ActivityLogEntry } from '../types'; function createActivityEntry( eventType: string, @@ -11,7 +11,7 @@ function createActivityEntry( return { id: `act_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, timestamp: Date.now(), // Use timestamp for ordering - eventType: eventType as any, + eventType: eventType as ActivityEventType, message, details, };