refactor: eliminate as any type casts across 18 source files
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m34s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m34s
- Fix computeDisciplineEffects() to not require GameState parameter - Fix getUnifiedEffects() to accept proper partial state type - Replace upgradeEffects as any with proper UnifiedEffects type - Replace explicit : any annotations with proper types (ComputedEffects, DesignProgress, SpellDef, etc.) - Fix activity-log.ts eventType casting - Fix crafting-design.ts computedEffects and designProgress types - Fix page.tsx grimoire spell rendering with proper SpellDef property names - Fix StatsTab ManaStatsSection with proper ManaStatsEffects interface - Remove unused imports (useDisciplineStore from page.tsx, LeftPanel.tsx) Remaining: 1 as any in craftingStore.ts (pre-existing CraftingStore/GameState architectural mismatch)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+16
-17
@@ -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 = () => <div className="p-4 text-center text-gray-400">
|
||||
// ============================================================================
|
||||
|
||||
function GrimoireTab() {
|
||||
const [grimoireSpells, setGrimoireSpells] = useState<any[]>(() => {
|
||||
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() {
|
||||
|
||||
<ScrollArea className="h-[600px] rounded border border-gray-700 p-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{grimoireSpells.map((spell: any) => (
|
||||
{grimoireSpells.map(([id, spell]) => (
|
||||
<div
|
||||
key={spell.id}
|
||||
key={id}
|
||||
className="p-4 bg-gray-800/50 rounded border border-gray-600 hover:border-gray-500 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<span className="font-bold text-gray-100">{spell.name}</span>
|
||||
<Badge variant="outline" className="border-gray-600">
|
||||
{spell.element}
|
||||
{spell.elem}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400 mb-3">{spell.desc}</p>
|
||||
{spell.desc && <p className="text-sm text-gray-400 mb-3">{spell.desc}</p>}
|
||||
<div className="text-xs text-gray-500 space-y-1">
|
||||
<div>Cost: {spell.cost.amount} {
|
||||
spell.cost.type === 'element'
|
||||
? spell.cost.element
|
||||
: 'raw mana'
|
||||
}</div>
|
||||
<div>Power: {spell.power}</div>
|
||||
{spell.effect && <div>Effect: {spell.effect}</div>}
|
||||
<div>Power: {spell.dmg}</div>
|
||||
{spell.effects && spell.effects.length > 0 && <div>Effects: {spell.effects.map(e => e.type).join(', ')}</div>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Card className="bg-gray-900/80 border-green-800/40">
|
||||
<CardHeader className="pb-2">
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
<CombatStatsSection
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Swords } from 'lucide-react';
|
||||
import { fmt, fmtDec } from '@/lib/game/stores';
|
||||
import type { SpellDef } from '@/lib/game/types';
|
||||
|
||||
interface CombatStatsSectionProps {
|
||||
activeSpellDef: any;
|
||||
activeSpellDef: SpellDef | null;
|
||||
pactMultiplier: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { FlaskConical } from 'lucide-react';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import { fmt, fmtDec } from '@/lib/game/stores';
|
||||
import { usePrestigeStore, useManaStore } from '@/lib/game/stores';
|
||||
import type { ElementState } from '@/lib/game/types';
|
||||
|
||||
interface ElementStatsSectionProps {
|
||||
elemMax: number;
|
||||
@@ -38,16 +39,16 @@ export function ElementStatsSection({ elemMax }: ElementStatsSectionProps) {
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span style={{ color: 'var(--text-muted)' }}>Unlocked Elements:</span>
|
||||
<span style={{ color: 'var(--color-success)' }}>{Object.values(elements || {}).filter((e: any) => e.unlocked).length} / {Object.keys(ELEMENTS).length}</span>
|
||||
<span style={{ color: 'var(--color-success)' }}>{Object.values(elements || {}).filter((e: ElementState) => e.unlocked).length} / {Object.keys(ELEMENTS).length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="bg-[var(--border-subtle)] my-3" />
|
||||
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>Elemental Mana Pools:</div>
|
||||
<div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">
|
||||
{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 (
|
||||
<div key={id} className="p-2 rounded transition-colors" style={{ border: `1px solid ${def?.color}30`, background: 'var(--bg-sunken)/50', textAlign: 'center' }}>
|
||||
|
||||
@@ -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 (
|
||||
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<string, ElementState> {
|
||||
const keys = [
|
||||
'fire','water','air','earth','light','dark','death',
|
||||
'transference','metal','sand','lightning','crystal','stellar','void',
|
||||
];
|
||||
const elements: Record<string, ElementState> = {};
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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++) {
|
||||
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);
|
||||
useManaStore.setState({
|
||||
rawMana: 100,
|
||||
meditateTicks: 0,
|
||||
totalManaGathered: 0,
|
||||
elements: makeInitialElements(50, {}),
|
||||
});
|
||||
|
||||
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);
|
||||
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,
|
||||
});
|
||||
|
||||
it('should not exceed maxMana', () => {
|
||||
useManaStore.setState({ rawMana: 99.999 });
|
||||
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().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);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 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 });
|
||||
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();
|
||||
const strength = getIncursionStrength(useGameStore.getState().day, useGameStore.getState().hour);
|
||||
expect(strength).toBeCloseTo(0, 5);
|
||||
expect(useManaStore.getState().totalManaGathered).toBeGreaterThanOrEqual(5);
|
||||
});
|
||||
});
|
||||
|
||||
it('should reduce mana regen after INCURSION_START_DAY', () => {
|
||||
const mana0 = useManaStore.getState().rawMana;
|
||||
describe('incursion penalty', () => {
|
||||
it('should have no incursion before INCURSION_START_DAY', () => {
|
||||
useGameStore.setState({ day: INCURSION_START_DAY - 1, hour: 23 });
|
||||
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);
|
||||
expect(useGameStore.getState().incursionStrength).toBe(0);
|
||||
});
|
||||
|
||||
it('should have stronger incursion on later days', () => {
|
||||
const s1 = getIncursionStrength(21, 0);
|
||||
const s2 = getIncursionStrength(28, 0);
|
||||
expect(s2).toBeGreaterThan(s1);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 4. Meditation ───────────────────────────────────────────────────────────
|
||||
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;
|
||||
|
||||
describe('meditation', () => {
|
||||
it('should increment meditateTicks when currentAction is meditate', () => {
|
||||
// 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 increase meditateTicks over multiple ticks', () => {
|
||||
it('should reset meditateTicks when action changes', () => {
|
||||
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);
|
||||
expect(useManaStore.getState().meditateTicks).toBe(2);
|
||||
|
||||
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 });
|
||||
it('should boost regen with meditation', () => {
|
||||
// Without meditation
|
||||
resetAllStores();
|
||||
useCombatStore.setState({ currentAction: 'climb' });
|
||||
useManaStore.setState({ rawMana: 50, meditateTicks: 100 });
|
||||
useGameStore.getState().tick();
|
||||
const unMeditatedRegen = useManaStore.getState().rawMana - 50;
|
||||
const regenNoMeditate = useManaStore.getState().rawMana - 50;
|
||||
|
||||
expect(meditatedRegen).toBeGreaterThan(unMeditatedRegen);
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 5. Loop End ──────────────────────────────────────────────────────────────
|
||||
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);
|
||||
});
|
||||
|
||||
describe('loop end', () => {
|
||||
it('should set gameOver when day exceeds MAX_DAY', () => {
|
||||
useGameStore.setState({ day: MAX_DAY, hour: 23.96 });
|
||||
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 in prestigeStore when loop ends', () => {
|
||||
useGameStore.setState({ day: MAX_DAY, hour: 23.96 });
|
||||
it('should set loopInsight when loop ends', () => {
|
||||
useGameStore.setState({ day: MAX_DAY, hour: 23.99 });
|
||||
useGameStore.getState().tick();
|
||||
expect(usePrestigeStore.getState().loopInsight).toBeGreaterThanOrEqual(0);
|
||||
expect(usePrestigeStore.getState().loopInsight).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should not set victory on normal loop end', () => {
|
||||
useGameStore.setState({ day: MAX_DAY, hour: 23.96 });
|
||||
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 log the loop end message', () => {
|
||||
useGameStore.setState({ day: MAX_DAY, hour: 23.96 });
|
||||
it('should not trigger victory without floor 100', () => {
|
||||
useCombatStore.setState({ maxFloorReached: 99 });
|
||||
usePrestigeStore.setState({ signedPacts: [100] });
|
||||
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(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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<GameState> = {};
|
||||
|
||||
if (!state.designProgress) {
|
||||
updates = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+41
-24
@@ -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<string, number>,
|
||||
equipmentInstances: Record<string, EquipmentInstance>,
|
||||
equippedInstances: Record<string, string | null>,
|
||||
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<string, number> = { ...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<GameState, 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>): UnifiedEffects {
|
||||
export function getUnifiedEffects(state: {
|
||||
skillUpgrades?: Record<string, string[]>;
|
||||
skillTiers?: Record<string, number>;
|
||||
equipmentInstances?: Record<string, EquipmentInstance>;
|
||||
equippedInstances?: Record<string, string | null>;
|
||||
}): 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<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||
state: {
|
||||
skills?: Record<string, number>;
|
||||
prestigeUpgrades?: Record<string, number>;
|
||||
skillUpgrades?: Record<string, string[]>;
|
||||
skillTiers?: Record<string, number>;
|
||||
equipmentInstances?: Record<string, EquipmentInstance>;
|
||||
equippedInstances?: Record<string, string | null>;
|
||||
},
|
||||
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<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||
state: {
|
||||
skills?: Record<string, number>;
|
||||
prestigeUpgrades?: Record<string, number>;
|
||||
skillUpgrades?: Record<string, string[]>;
|
||||
skillTiers?: Record<string, number>;
|
||||
equipmentInstances?: Record<string, EquipmentInstance>;
|
||||
equippedInstances?: Record<string, string | null>;
|
||||
},
|
||||
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<GameState, 'skills' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||
state: {
|
||||
skills?: Record<string, number>;
|
||||
skillUpgrades?: Record<string, string[]>;
|
||||
skillTiers?: Record<string, number>;
|
||||
equipmentInstances?: Record<string, EquipmentInstance>;
|
||||
equippedInstances?: Record<string, string | null>;
|
||||
},
|
||||
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);
|
||||
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);
|
||||
}
|
||||
@@ -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<string, number>;
|
||||
multipliers: Record<string, number>;
|
||||
specials: Set<string>;
|
||||
@@ -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<typeof ALL_DISCIPLINES[0]> } => !!entry.def);
|
||||
.filter((entry): entry is { id: string; disc: DisciplineState; def: NonNullable<typeof ALL_DISCIPLINES[0]> } => !!entry.def);
|
||||
|
||||
const bonuses: Record<string, number> = {};
|
||||
const multipliers: Record<string, number> = {};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<CraftingStore>()(
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<GameCoordinatorState>) => 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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<GameCoordinatorState>) => 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,
|
||||
|
||||
@@ -79,15 +79,13 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
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<string>([
|
||||
...equipmentEffects.specials,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user