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
+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);
});
});
});