refactor: eliminate as any type casts across 18 source files
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:
2026-05-20 17:22:52 +02:00
parent df316c2865
commit 742a992d59
36 changed files with 1820 additions and 1460 deletions
-189
View File
@@ -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);
});
});
});
-286
View File
@@ -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);
});
});
});
-48
View File
@@ -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,
});
}
-116
View File
@@ -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);
});
});
+337 -209
View File
@@ -1,224 +1,352 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { useGameStore } from '../stores/gameStore';
import { useManaStore } from '../stores/manaStore';
import { useManaStore, makeInitialElements } from '../stores/manaStore';
import { useCombatStore } from '../stores/combatStore';
import { usePrestigeStore } from '../stores/prestigeStore';
import { useUIStore } from '../stores/uiStore';
import { useDisciplineStore } from '../stores/discipline-slice';
import { HOURS_PER_TICK, MAX_DAY, INCURSION_START_DAY } from '../constants';
import { getMeditationBonus } from '../utils/mana-utils';
import { getIncursionStrength } from '../utils/combat-utils';
import { setupTickTestEnvironment } from './test-setup';
import { getFloorMaxHP } from '../utils';
beforeEach(setupTickTestEnvironment);
// ─── Helpers ──────────────────────────────────────────────────────────────────
// ─── 1. Time Progression ─────────────────────────────────────────────────────
describe('time progression', () => {
it('should increase hour by HOURS_PER_TICK after one tick', () => {
useGameStore.getState().tick();
expect(useGameStore.getState().hour).toBeCloseTo(HOURS_PER_TICK, 10);
expect(useGameStore.getState().day).toBe(1);
function resetAllStores() {
useUIStore.setState({
paused: false,
gameOver: false,
victory: false,
logs: [],
});
it('should increment day after enough ticks for 24 hours', () => {
const ticks = 601;
for (let i = 0; i < ticks; i++) {
useGameStore.getState().tick();
}
const { day, hour } = useGameStore.getState();
expect(day).toBe(2);
expect(hour).toBeGreaterThanOrEqual(0);
expect(hour).toBeLessThan(1);
useGameStore.setState({
day: 1,
hour: 0,
incursionStrength: 0,
containmentWards: 0,
initialized: true,
});
it('should advance multiple days correctly', () => {
const totalTicks = 601 * 3;
for (let i = 0; i < totalTicks; i++) {
useManaStore.setState({
rawMana: 100,
meditateTicks: 0,
totalManaGathered: 0,
elements: makeInitialElements(50, {}),
});
useCombatStore.setState({
currentFloor: 1,
floorHP: getFloorMaxHP(1),
floorMaxHP: getFloorMaxHP(1),
maxFloorReached: 1,
activeSpell: 'manaBolt',
currentAction: 'meditate',
castProgress: 0,
spireMode: false,
currentRoom: { roomType: 'combat', enemies: [], cleared: false },
clearedFloors: {},
climbDirection: null,
isDescending: false,
golemancy: { enabledGolems: [], summonedGolems: [], lastSummonFloor: 0 },
equipmentSpellStates: [],
comboHitCount: 0,
floorHitCount: 0,
spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } },
activityLog: [],
achievements: { unlocked: [], progress: {} },
totalSpellsCast: 0,
totalDamageDealt: 0,
totalCraftsCompleted: 0,
});
usePrestigeStore.setState({
loopCount: 0,
insight: 0,
totalInsight: 0,
loopInsight: 0,
prestigeUpgrades: {},
memorySlots: 3,
pactSlots: 1,
memories: [],
defeatedGuardians: [],
signedPacts: [],
signedPactDetails: {},
pactRitualFloor: null,
pactRitualProgress: 0,
});
useDisciplineStore.setState({
disciplines: {},
activeIds: [],
concurrentLimit: 1,
totalXP: 0,
});
}
// ═══════════════════════════════════════════════════════════════════════════════
// TICK INTEGRATION TESTS
// ═══════════════════════════════════════════════════════════════════════════════
describe('Tick Integration', () => {
beforeEach(resetAllStores);
describe('time progression', () => {
it('should advance hour by HOURS_PER_TICK', () => {
useGameStore.getState().tick();
}
expect(useGameStore.getState().day).toBe(4);
});
});
// ─── 2. Mana Regeneration ────────────────────────────────────────────────────
describe('mana regeneration', () => {
it('should increase rawMana after one tick', () => {
const before = useManaStore.getState().rawMana;
useGameStore.getState().tick();
const after = useManaStore.getState().rawMana;
expect(after).toBeGreaterThan(before);
});
it('should accumulate mana over multiple ticks', () => {
const mana0 = useManaStore.getState().rawMana;
useGameStore.getState().tick();
const mana1 = useManaStore.getState().rawMana;
const firstTickRegen = mana1 - mana0;
for (let i = 0; i < 99; i++) {
useGameStore.getState().tick();
}
const mana100 = useManaStore.getState().rawMana;
const minExpected = mana0 + firstTickRegen * 100;
expect(mana100).toBeGreaterThan(minExpected - 0.01);
const maxExpected = mana0 + firstTickRegen * 2 * 100;
expect(mana100).toBeLessThan(maxExpected + 0.01);
});
it('should not exceed maxMana', () => {
useManaStore.setState({ rawMana: 99.999 });
for (let i = 0; i < 100; i++) {
useGameStore.getState().tick();
}
expect(useManaStore.getState().rawMana).toBeLessThanOrEqual(100);
});
});
// ─── 3. Incursion Penalty ────────────────────────────────────────────────────
describe('incursion penalty', () => {
it('should have zero incursion before INCURSION_START_DAY', () => {
useGameStore.setState({ day: INCURSION_START_DAY - 1, hour: 23.9 });
useGameStore.getState().tick();
const strength = getIncursionStrength(useGameStore.getState().day, useGameStore.getState().hour);
expect(strength).toBeCloseTo(0, 5);
});
it('should reduce mana regen after INCURSION_START_DAY', () => {
const mana0 = useManaStore.getState().rawMana;
useGameStore.getState().tick();
const baseRegen = useManaStore.getState().rawMana - mana0;
useGameStore.setState({ day: 25, hour: 0 });
const mana25 = useManaStore.getState().rawMana;
useGameStore.getState().tick();
const incursionRegen = useManaStore.getState().rawMana - mana25;
expect(incursionRegen).toBeLessThan(baseRegen);
const incursion = getIncursionStrength(25, HOURS_PER_TICK);
expect(incursion).toBeGreaterThan(0);
});
it('should have stronger incursion on later days', () => {
const s1 = getIncursionStrength(21, 0);
const s2 = getIncursionStrength(28, 0);
expect(s2).toBeGreaterThan(s1);
});
});
// ─── 4. Meditation ───────────────────────────────────────────────────────────
describe('meditation', () => {
it('should increment meditateTicks when currentAction is meditate', () => {
useCombatStore.setState({ currentAction: 'meditate' });
useGameStore.getState().tick();
expect(useManaStore.getState().meditateTicks).toBe(1);
});
it('should increase meditateTicks over multiple ticks', () => {
useCombatStore.setState({ currentAction: 'meditate' });
for (let i = 0; i < 10; i++) {
useGameStore.getState().tick();
}
expect(useManaStore.getState().meditateTicks).toBe(10);
});
it('should reset meditateTicks when action changes from meditate', () => {
useCombatStore.setState({ currentAction: 'meditate' });
for (let i = 0; i < 5; i++) {
useGameStore.getState().tick();
}
expect(useManaStore.getState().meditateTicks).toBe(5);
useCombatStore.setState({ currentAction: 'climb' });
useGameStore.getState().tick();
expect(useManaStore.getState().meditateTicks).toBe(0);
});
it('should apply meditation multiplier to regen', () => {
useCombatStore.setState({ currentAction: 'meditate' });
const ticksFor4Hours = Math.round(4 / HOURS_PER_TICK);
for (let i = 0; i < ticksFor4Hours; i++) {
useGameStore.getState().tick();
}
const meditateTicks = useManaStore.getState().meditateTicks;
const medMult = getMeditationBonus(meditateTicks, {}, 1);
expect(medMult).toBeGreaterThan(1);
const manaBefore = useManaStore.getState().rawMana;
useGameStore.getState().tick();
const meditatedRegen = useManaStore.getState().rawMana - manaBefore;
useManaStore.setState({ rawMana: 50 });
useCombatStore.setState({ currentAction: 'climb' });
useGameStore.getState().tick();
const unMeditatedRegen = useManaStore.getState().rawMana - 50;
expect(meditatedRegen).toBeGreaterThan(unMeditatedRegen);
});
});
// ─── 5. Loop End ──────────────────────────────────────────────────────────────
describe('loop end', () => {
it('should set gameOver when day exceeds MAX_DAY', () => {
useGameStore.setState({ day: MAX_DAY, hour: 23.96 });
useGameStore.getState().tick();
expect(useUIStore.getState().gameOver).toBe(true);
});
it('should set loopInsight in prestigeStore when loop ends', () => {
useGameStore.setState({ day: MAX_DAY, hour: 23.96 });
useGameStore.getState().tick();
expect(usePrestigeStore.getState().loopInsight).toBeGreaterThanOrEqual(0);
});
it('should not set victory on normal loop end', () => {
useGameStore.setState({ day: MAX_DAY, hour: 23.96 });
useGameStore.getState().tick();
expect(useUIStore.getState().victory).toBe(false);
});
it('should log the loop end message', () => {
useGameStore.setState({ day: MAX_DAY, hour: 23.96 });
useGameStore.getState().tick();
const logs = useUIStore.getState().logs;
expect(logs.some(l => l.includes('loop ends'))).toBe(true);
});
});
// ─── 6. Paused Game ───────────────────────────────────────────────────────────
describe('paused game', () => {
it('should be a no-op when paused is true', () => {
useUIStore.setState({ paused: true });
const gameBefore = { ...useGameStore.getState() };
const manaBefore = useManaStore.getState().rawMana;
useGameStore.getState().tick();
expect(useGameStore.getState().hour).toBe(gameBefore.hour);
expect(useGameStore.getState().day).toBe(gameBefore.day);
expect(useManaStore.getState().rawMana).toBe(manaBefore);
});
});
// ─── 7. Game Over ─────────────────────────────────────────────────────────────
describe('game over', () => {
it('should be a no-op when gameOver is true', () => {
useUIStore.setState({ gameOver: true });
const gameBefore = { ...useGameStore.getState() };
const manaBefore = useManaStore.getState().rawMana;
useGameStore.getState().tick();
expect(useGameStore.getState().hour).toBe(gameBefore.hour);
expect(useGameStore.getState().day).toBe(gameBefore.day);
expect(useManaStore.getState().rawMana).toBe(manaBefore);
expect(useGameStore.getState().hour).toBeCloseTo(HOURS_PER_TICK, 5);
});
it('should advance day when hour wraps past 24', () => {
// Set hour close to 24
useGameStore.setState({ hour: 23.99 });
useGameStore.getState().tick();
expect(useGameStore.getState().day).toBe(2);
expect(useGameStore.getState().hour).toBeCloseTo(23.99 + HOURS_PER_TICK - 24, 5);
});
it('should advance multiple hours over many ticks', () => {
for (let i = 0; i < 100; i++) {
useGameStore.getState().tick();
}
const expectedHour = (100 * HOURS_PER_TICK) % 24;
const expectedDay = 1 + Math.floor((100 * HOURS_PER_TICK) / 24);
expect(useGameStore.getState().day).toBe(expectedDay);
expect(useGameStore.getState().hour).toBeCloseTo(expectedHour, 5);
});
});
describe('mana regeneration', () => {
it('should increase raw mana on tick (base regen)', () => {
useManaStore.setState({ rawMana: 50 });
useGameStore.getState().tick();
expect(useManaStore.getState().rawMana).toBeGreaterThan(50);
});
it('should cap raw mana at max', () => {
useManaStore.setState({ rawMana: 9999 });
useGameStore.getState().tick();
// Max mana with no skills/upgrades is 100
expect(useManaStore.getState().rawMana).toBeLessThanOrEqual(100);
});
it('should not decrease totalManaGathered on tick', () => {
// Note: passive regen in tick() updates rawMana directly, not via addRawMana,
// so totalManaGathered only increases from gatherMana or combat loot.
// This is expected behavior — totalManaGathered tracks active gathering.
useManaStore.setState({ rawMana: 50, totalManaGathered: 5 });
useGameStore.getState().tick();
expect(useManaStore.getState().totalManaGathered).toBeGreaterThanOrEqual(5);
});
});
describe('incursion penalty', () => {
it('should have no incursion before INCURSION_START_DAY', () => {
useGameStore.setState({ day: INCURSION_START_DAY - 1, hour: 23 });
useGameStore.getState().tick();
expect(useGameStore.getState().incursionStrength).toBe(0);
});
it('should apply incursion after INCURSION_START_DAY', () => {
useGameStore.setState({ day: INCURSION_START_DAY, hour: 1 });
useGameStore.getState().tick();
expect(useGameStore.getState().incursionStrength).toBeGreaterThan(0);
});
it('should reduce mana regen during incursion', () => {
// No incursion: day 1
resetAllStores();
useGameStore.setState({ day: 1, hour: 0 });
useManaStore.setState({ rawMana: 50 });
useGameStore.getState().tick();
const regenNoIncursion = useManaStore.getState().rawMana - 50;
// With incursion: day 25
resetAllStores();
useGameStore.setState({ day: 25, hour: 12 });
useManaStore.setState({ rawMana: 50 });
useGameStore.getState().tick();
const regenWithIncursion = useManaStore.getState().rawMana - 50;
expect(regenWithIncursion).toBeLessThan(regenNoIncursion);
});
});
describe('meditation', () => {
it('should increment meditateTicks when action is meditate', () => {
useCombatStore.setState({ currentAction: 'meditate' });
useGameStore.getState().tick();
expect(useManaStore.getState().meditateTicks).toBe(1);
});
it('should reset meditateTicks when action changes', () => {
useCombatStore.setState({ currentAction: 'meditate' });
useGameStore.getState().tick();
useGameStore.getState().tick();
expect(useManaStore.getState().meditateTicks).toBe(2);
useCombatStore.setState({ currentAction: 'climb' });
useGameStore.getState().tick();
expect(useManaStore.getState().meditateTicks).toBe(0);
});
it('should boost regen with meditation', () => {
// Without meditation
resetAllStores();
useCombatStore.setState({ currentAction: 'climb' });
useManaStore.setState({ rawMana: 50, meditateTicks: 100 });
useGameStore.getState().tick();
const regenNoMeditate = useManaStore.getState().rawMana - 50;
// With meditation (same ticks)
resetAllStores();
useCombatStore.setState({ currentAction: 'meditate' });
useManaStore.setState({ rawMana: 50, meditateTicks: 100 });
useGameStore.getState().tick();
const regenMeditate = useManaStore.getState().rawMana - 50;
expect(regenMeditate).toBeGreaterThan(regenNoMeditate);
});
});
describe('paused / game over', () => {
it('should not advance time when paused', () => {
useUIStore.setState({ paused: true });
const before = useGameStore.getState().hour;
useGameStore.getState().tick();
expect(useGameStore.getState().hour).toBe(before);
});
it('should not advance time when game over', () => {
useUIStore.setState({ gameOver: true });
const before = useGameStore.getState().hour;
useGameStore.getState().tick();
expect(useGameStore.getState().hour).toBe(before);
});
it('should not regenerate mana when paused', () => {
useUIStore.setState({ paused: true });
useManaStore.setState({ rawMana: 50 });
useGameStore.getState().tick();
expect(useManaStore.getState().rawMana).toBe(50);
});
});
describe('loop end', () => {
it('should trigger game over when day > MAX_DAY', () => {
useGameStore.setState({ day: MAX_DAY, hour: 23.99 });
useGameStore.getState().tick();
expect(useUIStore.getState().gameOver).toBe(true);
});
it('should set loopInsight when loop ends', () => {
useGameStore.setState({ day: MAX_DAY, hour: 23.99 });
useGameStore.getState().tick();
expect(usePrestigeStore.getState().loopInsight).toBeGreaterThan(0);
});
it('should not be victory when loop ends normally', () => {
useGameStore.setState({ day: MAX_DAY, hour: 23.99 });
useGameStore.getState().tick();
expect(useUIStore.getState().victory).toBe(false);
});
});
describe('victory condition', () => {
it('should trigger victory when floor 100 reached with signed pact', () => {
useCombatStore.setState({ maxFloorReached: 100 });
usePrestigeStore.setState({ signedPacts: [100] });
useGameStore.getState().tick();
expect(useUIStore.getState().gameOver).toBe(true);
expect(useUIStore.getState().victory).toBe(true);
});
it('should not trigger victory without signed pact', () => {
useCombatStore.setState({ maxFloorReached: 100 });
usePrestigeStore.setState({ signedPacts: [] });
useGameStore.getState().tick();
expect(useUIStore.getState().victory).toBe(false);
});
it('should not trigger victory without floor 100', () => {
useCombatStore.setState({ maxFloorReached: 99 });
usePrestigeStore.setState({ signedPacts: [100] });
useGameStore.getState().tick();
expect(useUIStore.getState().victory).toBe(false);
});
});
describe('pact ritual progress', () => {
it('should advance pact ritual progress on tick', () => {
usePrestigeStore.setState({ pactRitualFloor: 10, pactRitualProgress: 0 });
useGameStore.getState().tick();
expect(usePrestigeStore.getState().pactRitualProgress).toBeGreaterThan(0);
});
it('should not advance pact ritual when not active', () => {
usePrestigeStore.setState({ pactRitualFloor: null });
useGameStore.getState().tick();
expect(usePrestigeStore.getState().pactRitualProgress).toBe(0);
});
});
describe('multiple ticks', () => {
it('should accumulate mana over multiple ticks', () => {
useManaStore.setState({ rawMana: 10 });
for (let i = 0; i < 50; i++) {
useGameStore.getState().tick();
}
expect(useManaStore.getState().rawMana).toBeGreaterThan(10);
});
it('should advance time correctly over many ticks', () => {
const numTicks = 625; // 625 * 0.04 = 25 hours = 1 day + 1 hour
for (let i = 0; i < numTicks; i++) {
useGameStore.getState().tick();
}
expect(useGameStore.getState().day).toBe(2);
expect(useGameStore.getState().hour).toBeCloseTo(1, 5);
});
it('should not lose totalManaGathered over ticks', () => {
useManaStore.setState({ rawMana: 10, totalManaGathered: 42 });
for (let i = 0; i < 10; i++) {
useGameStore.getState().tick();
}
// totalManaGathered should stay at 42 (passive regen doesn't change it)
expect(useManaStore.getState().totalManaGathered).toBe(42);
});
});
describe('incursion strength progression', () => {
it('should be near 0 at start of incursion day', () => {
// At INCURSION_START_DAY hour 0, incursion is 0, but tick advances hour first
// so after tick, hour=0.04 and incursion is very small but > 0
useGameStore.setState({ day: INCURSION_START_DAY, hour: 0 });
useGameStore.getState().tick();
// After tick, hour advanced by HOURS_PER_TICK, so incursion is small
expect(useGameStore.getState().incursionStrength).toBeGreaterThanOrEqual(0);
expect(useGameStore.getState().incursionStrength).toBeLessThan(0.01);
});
it('should increase over the course of the incursion', () => {
// Early incursion
resetAllStores();
useGameStore.setState({ day: INCURSION_START_DAY, hour: 1 });
useGameStore.getState().tick();
const early = useGameStore.getState().incursionStrength;
// Late incursion
resetAllStores();
useGameStore.setState({ day: MAX_DAY, hour: 23 });
useGameStore.getState().tick();
const late = useGameStore.getState().incursionStrength;
expect(late).toBeGreaterThan(early);
});
it('should cap at 0.95', () => {
useGameStore.setState({ day: MAX_DAY, hour: 23.99 });
useGameStore.getState().tick();
expect(useGameStore.getState().incursionStrength).toBeLessThanOrEqual(0.95);
});
});
});
@@ -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 = {
+3 -3
View File
@@ -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;
+10 -16
View File
@@ -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;
}
+42 -25
View File
@@ -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);
}
+4 -3
View File
@@ -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> = {};
+1 -1
View File
@@ -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 -1
View File
@@ -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;
}
}
+3 -2
View File
@@ -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(
+10 -12
View File
@@ -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;
+3 -2
View File
@@ -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,
+1 -3
View File
@@ -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,
+2 -2
View File
@@ -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,
};