fix: resolve priority 4 issues — discipline mutation, skill→discipline migration, uiStore persistence, game loop interval, toast listener leak, page re-renders
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s

- Issue 70: Fix discipline-slice.ts nested element mutation (use immutable spread)
- Issue 71: Add persist middleware to uiStore for paused/gameOver/victory
- Issue 72: Wire discipline effects into calcDamage (spell-casting, void-manipulation)
- Issue 73: Fix useGameLoop interval recreation (use getState() + empty deps)
- Issue 74: Fix use-toast.ts listener leak (change [state] dep to [])
- Issue 75: Reduce page.tsx re-renders with useShallow for multi-field subscriptions
- Issue 76: Fix createGatherMana hardcoded click mana (use computeClickMana with discipline effects)
- Issue 77: Pass discipline effects to computeMaxMana/computeRegen/calcInsight in tick()
- Export DisciplineBonuses type and useDisciplineStore from barrel exports
- Update tests to match new function signatures
This commit is contained in:
2026-05-19 13:53:33 +02:00
parent ebcaab62bf
commit 50a9a62060
17 changed files with 215 additions and 154 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
# Circular Dependencies # Circular Dependencies
Generated: 2026-05-19T10:35:03.017Z Generated: 2026-05-19T10:51:45.659Z
Found: 3 circular chain(s) — these MUST be fixed before modifying involved files. Found: 3 circular chain(s) — these MUST be fixed before modifying involved files.
1. Processed 121 files (1.2s) (4 warnings) 1. Processed 121 files (1.3s) (4 warnings)
2. 1) data/equipment/index.ts > data/equipment/utils.ts 2. 1) data/equipment/index.ts > data/equipment/utils.ts
3. 2) data/golems/index.ts > data/golems/utils.ts 3. 2) data/golems/index.ts > data/golems/utils.ts
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"_meta": { "_meta": {
"generated": "2026-05-19T10:35:01.630Z", "generated": "2026-05-19T10:51:44.106Z",
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.", "description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry." "usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
}, },
+3 -2
View File
@@ -9,7 +9,8 @@ import { ActionButtons } from '@/components/game';
import { AttunementStatus } from '@/components/game/AttunementStatus'; import { AttunementStatus } from '@/components/game/AttunementStatus';
import { ActivityLogPanel } from '@/components/game/ActivityLogPanel'; import { ActivityLogPanel } from '@/components/game/ActivityLogPanel';
import { DebugName } from '@/components/game/debug/debug-context'; import { DebugName } from '@/components/game/debug/debug-context';
import { useGameStore, useManaStore, useCombatStore, useCraftingStore, usePrestigeStore } from '@/lib/game/stores'; import { useGameStore, useManaStore, useCombatStore, useCraftingStore, usePrestigeStore, useDisciplineStore } from '@/lib/game/stores';
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
import { getUnifiedEffects } from '@/lib/game/effects'; import { getUnifiedEffects } from '@/lib/game/effects';
import { getMeditationBonus, getIncursionStrength } from '@/lib/game/stores'; import { getMeditationBonus, getIncursionStrength } from '@/lib/game/stores';
import { computeTotalMaxMana, computeTotalRegen, computeTotalClickMana } from '@/lib/game/effects'; import { computeTotalMaxMana, computeTotalRegen, computeTotalClickMana } from '@/lib/game/effects';
@@ -56,7 +57,7 @@ export function LeftPanel() {
const maxMana = computeTotalMaxMana({ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects); const maxMana = computeTotalMaxMana({ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects);
const baseRegen = computeTotalRegen({ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects); const baseRegen = computeTotalRegen({ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects);
const clickMana = computeTotalClickMana({ skills: {}, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects); const clickMana = computeTotalClickMana({ skills: {}, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects);
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency); const meditationMultiplier = getMeditationBonus(meditateTicks, {}, (upgradeEffects as any).meditationEfficiency);
const incursionStrength = getIncursionStrength(useGameStore((s) => s.day), useGameStore((s) => s.hour)); const incursionStrength = getIncursionStrength(useGameStore((s) => s.day), useGameStore((s) => s.hour));
const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier; const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier;
+19 -22
View File
@@ -1,6 +1,7 @@
'use client'; 'use client';
import { useEffect, useState, lazy, Suspense } from 'react'; import { useEffect, useState, lazy, Suspense } from 'react';
import { useShallow } from 'zustand/react/shallow';
// Import from new modular stores // Import from new modular stores
import { import {
@@ -10,6 +11,7 @@ import {
useCombatStore, useCombatStore,
usePrestigeStore, usePrestigeStore,
useCraftingStore, useCraftingStore,
useDisciplineStore,
fmt, fmt,
computeMaxMana, computeMaxMana,
computeRegen, computeRegen,
@@ -17,6 +19,7 @@ import {
getMeditationBonus, getMeditationBonus,
getIncursionStrength, getIncursionStrength,
} from '@/lib/game/stores'; } from '@/lib/game/stores';
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
import { useGameLoop } from '@/lib/game/stores/gameHooks'; import { useGameLoop } from '@/lib/game/stores/gameHooks';
import { getUnifiedEffects } from '@/lib/game/effects'; import { getUnifiedEffects } from '@/lib/game/effects';
import { SPELLS_DEF } from '@/lib/game/constants'; import { SPELLS_DEF } from '@/lib/game/constants';
@@ -120,21 +123,13 @@ export default function ManaLoopGame() {
const [activeTab, setActiveTab] = useState('spells'); const [activeTab, setActiveTab] = useState('spells');
// ALL hooks must be called before any conditional returns // ALL hooks must be called before any conditional returns
const day = useGameStore((s) => s.day);
const hour = useGameStore((s) => s.hour);
const initGame = useGameStore((s) => s.initGame);
useGameLoop(); useGameLoop();
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades); // Use useShallow to combine multi-field subscriptions and reduce re-renders
const insight = usePrestigeStore((s) => s.insight); const { day, hour, initGame } = useGameStore(useShallow(s => ({ day: s.day, hour: s.hour, initGame: s.initGame })));
const loopInsight = usePrestigeStore((s) => s.loopInsight); const { prestigeUpgrades, insight, loopInsight } = usePrestigeStore(useShallow(s => ({ prestigeUpgrades: s.prestigeUpgrades, insight: s.insight, loopInsight: s.loopInsight })));
const { rawMana, meditateTicks } = useManaStore(useShallow(s => ({ rawMana: s.rawMana, meditateTicks: s.meditateTicks })));
const rawMana = useManaStore((s) => s.rawMana);
const meditateTicks = useManaStore((s) => s.meditateTicks);
const spireMode = useCombatStore((s) => s.spireMode); const spireMode = useCombatStore((s) => s.spireMode);
const gameOver = useUIStore((s) => s.gameOver); const gameOver = useUIStore((s) => s.gameOver);
// Get equipment state from crafting store // Get equipment state from crafting store
@@ -149,40 +144,42 @@ export default function ManaLoopGame() {
equipmentInstances equipmentInstances
}); });
// Compute discipline bonuses from active disciplines
const disciplineStoreState = useDisciplineStore();
const disciplineEffects = computeDisciplineEffects(disciplineStoreState as any);
const maxMana = computeMaxMana({ const maxMana = computeMaxMana({
skills: {}, skills: {},
prestigeUpgrades, prestigeUpgrades,
skillUpgrades: {}, skillUpgrades: {},
skillTiers: {} skillTiers: {}
}, upgradeEffects); }, upgradeEffects as any, disciplineEffects);
const baseRegen = computeRegen({ const baseRegen = computeRegen({
skills: {}, skills: {},
prestigeUpgrades, prestigeUpgrades,
skillUpgrades: {}, skillUpgrades: {},
skillTiers: {} skillTiers: {},
}, upgradeEffects); attunements: {},
}, upgradeEffects as any, disciplineEffects);
const clickMana = computeClickMana({ const clickMana = computeClickMana({
skills: {}, skills: {},
prestigeUpgrades, }, disciplineEffects);
skillUpgrades: {},
skillTiers: {}
});
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency); const meditationMultiplier = getMeditationBonus(meditateTicks, {}, (upgradeEffects as any).meditationEfficiency);
const incursionStrength = getIncursionStrength(day, hour); const incursionStrength = getIncursionStrength(day, hour);
// Effective regen with incursion penalty // Effective regen with incursion penalty
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength); const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
// Mana Cascade bonus // Mana Cascade bonus
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE) const manaCascadeBonus = hasSpecial(upgradeEffects as any, SPECIAL_EFFECTS.MANA_CASCADE)
? Math.floor(maxMana / 100) * 0.1 ? Math.floor(maxMana / 100) * 0.1
: 0; : 0;
// Mana Waterfall bonus // Mana Waterfall bonus
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL) const manaWaterfallBonus = hasSpecial(upgradeEffects as any, SPECIAL_EFFECTS.MANA_WATERFALL)
? Math.floor(maxMana / 100) * 0.25 ? Math.floor(maxMana / 100) * 0.25
: 0; : 0;
+1 -1
View File
@@ -182,7 +182,7 @@ function useToast() {
listeners.splice(index, 1) listeners.splice(index, 1)
} }
} }
}, [state]) }, [])
return { return {
...state, ...state,
+8 -10
View File
@@ -169,7 +169,7 @@ describe('computeMaxMana', () => {
skillUpgrades: {}, skillUpgrades: {},
skillTiers: {}, skillTiers: {},
}; };
const effects = { maxManaBonus: 0, maxManaMultiplier: 1 }; const effects = { maxManaBonus: 0, maxManaMultiplier: 1 } as any;
const result = computeMaxMana(state, effects); const result = computeMaxMana(state, effects);
expect(result).toBe(100); expect(result).toBe(100);
}); });
@@ -181,7 +181,7 @@ describe('computeMaxMana', () => {
skillUpgrades: {}, skillUpgrades: {},
skillTiers: {}, skillTiers: {},
}; };
const effects = { maxManaBonus: 0, maxManaMultiplier: 1 }; const effects = { maxManaBonus: 0, maxManaMultiplier: 1 } as any;
const result = computeMaxMana(state, effects); const result = computeMaxMana(state, effects);
expect(result).toBe(100 + 5 * 500); // Base + 500 per level expect(result).toBe(100 + 5 * 500); // Base + 500 per level
}); });
@@ -193,7 +193,7 @@ describe('computeMaxMana', () => {
skillUpgrades: {}, skillUpgrades: {},
skillTiers: {}, skillTiers: {},
}; };
const effects = { maxManaBonus: 0, maxManaMultiplier: 1.5 }; const effects = { maxManaBonus: 0, maxManaMultiplier: 1.5 } as any;
const result = computeMaxMana(state, effects); const result = computeMaxMana(state, effects);
expect(result).toBe(150); // 100 * 1.5 expect(result).toBe(150); // 100 * 1.5
}); });
@@ -205,7 +205,7 @@ describe('computeMaxMana', () => {
skillUpgrades: {}, skillUpgrades: {},
skillTiers: {}, skillTiers: {},
}; };
const effects = { maxManaBonus: 50, maxManaMultiplier: 1 }; const effects = { maxManaBonus: 50, maxManaMultiplier: 1 } as any;
const result = computeMaxMana(state, effects); const result = computeMaxMana(state, effects);
expect(result).toBe(150); // 100 + 50 expect(result).toBe(150); // 100 + 50
}); });
@@ -218,8 +218,9 @@ describe('computeRegen', () => {
prestigeUpgrades: {}, prestigeUpgrades: {},
skillUpgrades: {}, skillUpgrades: {},
skillTiers: {}, skillTiers: {},
attunements: {},
}; };
const effects = { regenBonus: 0, regenMultiplier: 1, permanentRegenBonus: 0 }; const effects = { regenBonus: 0, regenMultiplier: 1, permanentRegenBonus: 0 } as any;
const result = computeRegen(state, effects); const result = computeRegen(state, effects);
// Base regen is 2 (this test provides effects, so no attunement bonus) // Base regen is 2 (this test provides effects, so no attunement bonus)
expect(result).toBe(2); expect(result).toBe(2);
@@ -230,12 +231,9 @@ describe('computeClickMana', () => {
it('should return base click mana with no skills', () => { it('should return base click mana with no skills', () => {
const state = { const state = {
skills: {}, skills: {},
prestigeUpgrades: {},
skillUpgrades: {},
skillTiers: {},
}; };
const effects = { clickManaBonus: 0, clickManaMultiplier: 1 }; const discipline = { bonuses: {}, multipliers: {} };
const result = computeClickMana(state, effects); const result = computeClickMana(state, discipline);
expect(result).toBeGreaterThanOrEqual(1); expect(result).toBeGreaterThanOrEqual(1);
}); });
}); });
+2 -2
View File
@@ -137,13 +137,13 @@ export function computeAllEffects(
return merged; return merged;
} }
export function getUnifiedEffects(state: Pick<GameState, 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances' | 'disciplines'>): UnifiedEffects { export function getUnifiedEffects(state: Pick<GameState, 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>): UnifiedEffects {
return computeAllEffects( return computeAllEffects(
state.skillUpgrades || {}, state.skillUpgrades || {},
state.skillTiers || {}, state.skillTiers || {},
state.equipmentInstances || {}, state.equipmentInstances || {},
state.equippedInstances || {}, state.equippedInstances || {},
state as GameState state as unknown as GameState
); );
} }
+7
View File
@@ -6,6 +6,8 @@ import type { CombatState } from './combat-state.types';
import type { SpellState } from '../types'; import type { SpellState } from '../types';
import { getFloorMaxHP, getFloorElement, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils'; import { getFloorMaxHP, getFloorElement, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils';
import { usePrestigeStore } from './prestigeStore'; import { usePrestigeStore } from './prestigeStore';
import { computeDisciplineEffects } from '../effects/discipline-effects';
import { useDisciplineStore } from './discipline-slice';
export function processCombatTick( export function processCombatTick(
get: () => CombatState, get: () => CombatState,
@@ -35,6 +37,9 @@ export function processCombatTick(
return { rawMana, elements, logMessages, totalManaGathered }; return { rawMana, elements, logMessages, totalManaGathered };
} }
// Compute discipline bonuses once per tick
const disciplineEffects = computeDisciplineEffects(useDisciplineStore.getState() as any);
// Calculate cast speed (no skill bonus) // Calculate cast speed (no skill bonus)
const totalAttackSpeed = attackSpeedMult; const totalAttackSpeed = attackSpeedMult;
const spellCastSpeed = spellDef.castSpeed || 1; const spellCastSpeed = spellDef.castSpeed || 1;
@@ -57,6 +62,7 @@ export function processCombatTick(
{ skills: {}, signedPacts: usePrestigeStore.getState().signedPacts }, { skills: {}, signedPacts: usePrestigeStore.getState().signedPacts },
spellId, spellId,
floorElement, floorElement,
disciplineEffects,
); );
// Let gameStore apply damage modifiers (executioner, berserker) // Let gameStore apply damage modifiers (executioner, berserker)
@@ -111,6 +117,7 @@ export function processCombatTick(
{ skills: {}, signedPacts: usePrestigeStore.getState().signedPacts }, { skills: {}, signedPacts: usePrestigeStore.getState().signedPacts },
eSpell.spellId, eSpell.spellId,
eFloorElement, eFloorElement,
disciplineEffects,
); );
const eResult = onDamageDealt(eDamage); const eResult = onDamageDealt(eDamage);
+12 -5
View File
@@ -1,8 +1,10 @@
import { computeMaxMana } from '../utils'; import { computeMaxMana, computeClickMana } from '../utils';
import { useUIStore } from './uiStore'; import { useUIStore } from './uiStore';
import { usePrestigeStore } from './prestigeStore'; import { usePrestigeStore } from './prestigeStore';
import { useManaStore } from './manaStore'; import { useManaStore } from './manaStore';
import { useCombatStore } from './combatStore'; 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: any) => void, initialState: any) => () => {
// Clear all persisted state // Clear all persisted state
@@ -14,6 +16,7 @@ export const createResetGame = (set: (state: any) => void, initialState: any) =>
localStorage.removeItem('mana-loop-game-storage'); localStorage.removeItem('mana-loop-game-storage');
localStorage.removeItem('mana-loop-crafting-storage'); localStorage.removeItem('mana-loop-crafting-storage');
localStorage.removeItem('mana-loop-attunement-storage'); localStorage.removeItem('mana-loop-attunement-storage');
localStorage.removeItem('mana-loop-discipline-store');
} }
const startFloor = 1; const startFloor = 1;
@@ -30,11 +33,14 @@ export const createResetGame = (set: (state: any) => void, initialState: any) =>
}; };
export const createGatherMana = () => () => { export const createGatherMana = () => () => {
const manaState = useManaStore.getState();
const prestigeState = usePrestigeStore.getState(); const prestigeState = usePrestigeStore.getState();
const disciplineEffects = computeDisciplineEffects(useDisciplineStore.getState() as any);
// Base click mana (no skill bonuses) // Compute click mana with discipline bonuses (mana-channeling → clickManaMultiplier)
const cm = 1; const cm = computeClickMana(
{ skills: {} },
disciplineEffects,
);
const max = computeMaxMana( const max = computeMaxMana(
{ {
@@ -43,7 +49,8 @@ export const createGatherMana = () => () => {
skillUpgrades: {}, skillUpgrades: {},
skillTiers: {} skillTiers: {}
}, },
undefined undefined,
disciplineEffects,
); );
useManaStore.getState().gatherMana(cm, max); useManaStore.getState().gatherMana(cm, max);
+22 -13
View File
@@ -5,7 +5,9 @@ import { usePrestigeStore } from './prestigeStore';
import { useCombatStore } from './combatStore'; import { useCombatStore } from './combatStore';
import { useUIStore } from './uiStore'; import { useUIStore } from './uiStore';
import { useCraftingStore } from './craftingStore'; import { useCraftingStore } from './craftingStore';
import { useDisciplineStore } from './discipline-slice';
import { getUnifiedEffects } from '../effects'; import { getUnifiedEffects } from '../effects';
import { computeDisciplineEffects } from '../effects/discipline-effects';
import { import {
computeMaxMana, computeMaxMana,
computeRegen, computeRegen,
@@ -16,12 +18,10 @@ import {
import { TICK_MS } from '../constants'; import { TICK_MS } from '../constants';
export function useGameLoop() { export function useGameLoop() {
const tick = useGameStore((s) => s.tick);
useEffect(() => { useEffect(() => {
const interval = setInterval(tick, TICK_MS); const interval = setInterval(() => useGameStore.getState().tick(), TICK_MS);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [tick]); }, []);
} }
// ─── Shared Selector Hooks for Common Derived State ──────────────────────────── // ─── Shared Selector Hooks for Common Derived State ────────────────────────────
@@ -32,13 +32,18 @@ export function useGameLoop() {
export function useUnifiedEffects() { export function useUnifiedEffects() {
const equippedInstances = useCraftingStore((s) => s.equippedInstances); const equippedInstances = useCraftingStore((s) => s.equippedInstances);
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances); const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
const disciplineStoreState = useDisciplineStore();
const disciplineEffects = computeDisciplineEffects(disciplineStoreState as any);
return getUnifiedEffects({ return {
...getUnifiedEffects({
skillUpgrades: {}, skillUpgrades: {},
skillTiers: {}, skillTiers: {},
equippedInstances, equippedInstances,
equipmentInstances, equipmentInstances,
}); }),
disciplineEffects,
};
} }
/** /**
@@ -51,6 +56,8 @@ export function useManaStats() {
const hour = useGameStore((s) => s.hour); const hour = useGameStore((s) => s.hour);
const equippedInstances = useCraftingStore((s) => s.equippedInstances); const equippedInstances = useCraftingStore((s) => s.equippedInstances);
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances); const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
const disciplineStoreState = useDisciplineStore();
const disciplineEffects = computeDisciplineEffects(disciplineStoreState as any);
const upgradeEffects = getUnifiedEffects({ const upgradeEffects = getUnifiedEffects({
skillUpgrades: {}, skillUpgrades: {},
@@ -61,29 +68,31 @@ export function useManaStats() {
const maxMana = computeMaxMana( const maxMana = computeMaxMana(
{ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {} }, { skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {} },
upgradeEffects upgradeEffects as any,
disciplineEffects,
); );
const baseRegen = computeRegen( const baseRegen = computeRegen(
{ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {} }, { skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, attunements: {} },
upgradeEffects upgradeEffects as any,
disciplineEffects,
); );
const clickMana = computeClickMana({ const clickMana = computeClickMana({
skills: {}, skills: {},
}); }, disciplineEffects);
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency); const meditationMultiplier = getMeditationBonus(meditateTicks, {}, (upgradeEffects as any).meditationEfficiency);
const incursionStrength = getIncursionStrength(day, hour); const incursionStrength = getIncursionStrength(day, hour);
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength); const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
// Mana Cascade bonus // Mana Cascade bonus
const manaCascadeBonus = upgradeEffects.specials.has('mana_cascade') const manaCascadeBonus = (upgradeEffects as any).specials.has('mana_cascade')
? Math.floor(maxMana /100) * 0.1 ? Math.floor(maxMana /100) * 0.1
: 0; : 0;
// Mana Waterfall bonus // Mana Waterfall bonus
const manaWaterfallBonus = upgradeEffects.specials.has('mana_waterfall') const manaWaterfallBonus = (upgradeEffects as any).specials.has('mana_waterfall')
? Math.floor(maxMana /100) * 0.25 ? Math.floor(maxMana /100) * 0.25
: 0; : 0;
+4 -1
View File
@@ -5,19 +5,22 @@ import { useUIStore } from './uiStore';
import { usePrestigeStore } from './prestigeStore'; import { usePrestigeStore } from './prestigeStore';
import { useManaStore } from './manaStore'; import { useManaStore } from './manaStore';
import { useCombatStore } from './combatStore'; 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: any) => void) => () => {
const prestigeState = usePrestigeStore.getState(); const prestigeState = usePrestigeStore.getState();
const combatState = useCombatStore.getState(); const combatState = useCombatStore.getState();
const manaState = useManaStore.getState(); const manaState = useManaStore.getState();
const disciplineEffects = computeDisciplineEffects(useDisciplineStore.getState() as any);
const insightGained = prestigeState.loopInsight || calcInsight({ const insightGained = prestigeState.loopInsight || calcInsight({
maxFloorReached: combatState.maxFloorReached, maxFloorReached: combatState.maxFloorReached,
totalManaGathered: manaState.totalManaGathered, totalManaGathered: manaState.totalManaGathered,
signedPacts: prestigeState.signedPacts, signedPacts: prestigeState.signedPacts,
prestigeUpgrades: prestigeState.prestigeUpgrades, prestigeUpgrades: prestigeState.prestigeUpgrades,
skills: {}, skills: {},
}); }, disciplineEffects);
const total = prestigeState.insight + insightGained; const total = prestigeState.insight + insightGained;
+6 -4
View File
@@ -97,11 +97,13 @@ export const useGameStore = create<GameCoordinatorStore>()(
const maxMana = computeMaxMana( const maxMana = computeMaxMana(
{ skills: {}, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: {}, skillTiers: {} }, { skills: {}, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: {}, skillTiers: {} },
undefined undefined,
disciplineEffects,
); );
const baseRegen = computeRegen( const baseRegen = computeRegen(
{ skills: {}, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, attunements: {} }, { skills: {}, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, attunements: {} },
undefined undefined,
disciplineEffects,
); );
// Time progression // Time progression
@@ -120,7 +122,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
signedPacts: prestigeState.signedPacts, signedPacts: prestigeState.signedPacts,
prestigeUpgrades: prestigeState.prestigeUpgrades, prestigeUpgrades: prestigeState.prestigeUpgrades,
skills: {}, skills: {},
}); }, disciplineEffects);
addLog(`⏰ The loop ends. Gained ${insightGained} Insight.`); addLog(`⏰ The loop ends. Gained ${insightGained} Insight.`);
useUIStore.getState().setGameOver(true, false); useUIStore.getState().setGameOver(true, false);
@@ -137,7 +139,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
signedPacts: prestigeState.signedPacts, signedPacts: prestigeState.signedPacts,
prestigeUpgrades: prestigeState.prestigeUpgrades, prestigeUpgrades: prestigeState.prestigeUpgrades,
skills: {}, skills: {},
}) * 3; }, disciplineEffects) * 3;
addLog(`🏆 VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`); addLog(`🏆 VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`);
useUIStore.getState().setGameOver(true, true); useUIStore.getState().setGameOver(true, true);
+2
View File
@@ -20,6 +20,8 @@ export type { CraftingState, CraftingActions } from './craftingStore';
export { useAttunementStore } from './attunementStore'; export { useAttunementStore } from './attunementStore';
export type { AttunementStoreState } from './attunementStore'; export type { AttunementStoreState } from './attunementStore';
export { useDisciplineStore } from './discipline-slice';
export { useGameStore } from './gameStore'; export { useGameStore } from './gameStore';
export { useGameLoop } from './gameHooks'; export { useGameLoop } from './gameHooks';
export type { GameCoordinatorState, GameCoordinatorStore } from './gameStore'; export type { GameCoordinatorState, GameCoordinatorStore } from './gameStore';
+8 -2
View File
@@ -2,6 +2,7 @@
// Handles logs, pause state, and UI-specific state // Handles logs, pause state, and UI-specific state
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export interface LogEntry { export interface LogEntry {
message: string; message: string;
@@ -25,7 +26,9 @@ export interface UIState {
const MAX_LOGS = 50; const MAX_LOGS = 50;
export const useUIStore = create<UIState>((set) => ({ export const useUIStore = create<UIState>()(
persist(
(set) => ({
logs: ['✨ The loop begins. You start with Mana Bolt. Gather your strength, mage.'], logs: ['✨ The loop begins. You start with Mana Bolt. Gather your strength, mage.'],
paused: false, paused: false,
gameOver: false, gameOver: false,
@@ -61,4 +64,7 @@ export const useUIStore = create<UIState>((set) => ({
victory: false, victory: false,
}); });
}, },
})); }),
{ name: 'mana-loop-ui-storage' }
)
);
+17 -7
View File
@@ -1,6 +1,7 @@
// ─── Combat Utilities ──────────────────────────────────────────────────────── // ─── Combat Utilities ────────────────────────────────────────────────────────
import type { GameState, SpellCost, EquipmentInstance } from '../types'; import type { GameState, SpellCost, EquipmentInstance } from '../types';
import type { DisciplineBonuses } from './mana-utils';
import { GUARDIANS, SPELLS_DEF, ELEMENT_OPPOSITES, INCURSION_START_DAY, MAX_DAY } from '../constants'; import { GUARDIANS, SPELLS_DEF, ELEMENT_OPPOSITES, INCURSION_START_DAY, MAX_DAY } from '../constants';
import { ENCHANTMENT_EFFECTS } from '../data/enchantment-effects'; import { ENCHANTMENT_EFFECTS } from '../data/enchantment-effects';
@@ -110,13 +111,20 @@ export function getBoonBonuses(signedPacts: number[]): {
export function calcDamage( export function calcDamage(
state: Pick<GameState, 'skills' | 'signedPacts'>, state: Pick<GameState, 'skills' | 'signedPacts'>,
spellId: string, spellId: string,
floorElem?: string floorElem?: string,
discipline?: DisciplineBonuses,
): number { ): number {
const sp = SPELLS_DEF[spellId]; const sp = SPELLS_DEF[spellId];
if (!sp) return 5; if (!sp) return 5;
const skills = state.skills; const skills = state.skills;
const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5;
const pct = 1 + (skills.arcaneFury || 0) * 0.1; // Base damage: spell base + skill bonus + discipline bonus (spell-casting → baseDamageBonus)
const discBaseDmg = discipline?.bonuses?.baseDamageBonus || 0;
const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5 + discBaseDmg;
// Percentage multiplier: skill arcaneFury + discipline void-manipulation (baseDamageMultiplier)
const discDmgMult = discipline?.bonuses?.baseDamageMultiplier || 0;
const pct = 1 + (skills.arcaneFury || 0) * 0.1 + discDmgMult;
// Elemental mastery bonus // Elemental mastery bonus
const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15; const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15;
@@ -155,9 +163,10 @@ export function calcDamage(
// ─── Insight Calculation ────────────────────────────────────────────────────── // ─── Insight Calculation ──────────────────────────────────────────────────────
export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>): number { export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>, discipline?: DisciplineBonuses): number {
const pu = state.prestigeUpgrades; const pu = state.prestigeUpgrades;
const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1; const discInsightBonus = discipline?.bonuses?.insightGainBonus || 0;
const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1 + discInsightBonus;
// Get boon bonuses for insight gain // Get boon bonuses for insight gain
const boons = getBoonBonuses(state.signedPacts); const boons = getBoonBonuses(state.signedPacts);
@@ -265,7 +274,8 @@ export function getActiveEquipmentSpells(
export function getTotalDPS( export function getTotalDPS(
state: Pick<GameState, 'skills' | 'signedPacts' | 'equippedInstances' | 'equipmentInstances' | 'spells' | 'prestigeUpgrades'>, state: Pick<GameState, 'skills' | 'signedPacts' | 'equippedInstances' | 'equipmentInstances' | 'spells' | 'prestigeUpgrades'>,
upgradeEffects: { spellDamageBonus?: number; attackSpeedMultiplier?: number; [key: string]: unknown }, upgradeEffects: { spellDamageBonus?: number; attackSpeedMultiplier?: number; [key: string]: unknown },
floorElem?: string floorElem?: string,
discipline?: DisciplineBonuses,
): number { ): number {
let totalDPS = 0; let totalDPS = 0;
@@ -278,7 +288,7 @@ export function getTotalDPS(
if (!spellDef) continue; if (!spellDef) continue;
// Calculate damage per cast // Calculate damage per cast
const damage = calcDamage(state, spellId, floorElem); const damage = calcDamage(state, spellId, floorElem, discipline);
// Get cast speed (spells per second) // Get cast speed (spells per second)
// Base cast time is 1 second, modified by casting speed bonuses // Base cast time is 1 second, modified by casting speed bonuses
+2 -1
View File
@@ -9,7 +9,8 @@ export {
computeEffectiveRegen, computeEffectiveRegen,
computeEffectiveRegenForDisplay, computeEffectiveRegenForDisplay,
computeClickMana, computeClickMana,
getMeditationBonus getMeditationBonus,
type DisciplineBonuses,
} from './mana-utils'; } from './mana-utils';
// computeElementMax is now in ../store.ts with support for unlockedManaTypeUpgrades // computeElementMax is now in ../store.ts with support for unlockedManaTypeUpgrades
export { export {
+31 -13
View File
@@ -5,15 +5,22 @@ import type { ComputedEffects } from '../effects/upgrade-effects.types';
import { HOURS_PER_TICK } from '../constants'; import { HOURS_PER_TICK } from '../constants';
import { getTotalAttunementRegen, getTotalAttunementConversionDrain } from '../data/attunements'; import { getTotalAttunementRegen, getTotalAttunementConversionDrain } from '../data/attunements';
export interface DisciplineBonuses {
bonuses: Record<string, number>;
multipliers: Record<string, number>;
}
export function computeMaxMana( export function computeMaxMana(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>, state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
effects?: ComputedEffects effects?: ComputedEffects,
discipline?: DisciplineBonuses,
): number { ): number {
const pu = state.prestigeUpgrades; const pu = state.prestigeUpgrades;
const base = const base =
100 + 100 +
((state.skills || {}).manaWell || 0) * 100 + ((state.skills || {}).manaWell || 0) * 100 +
((pu || {}).manaWell || 0) * 500; ((pu || {}).manaWell || 0) * 500 +
(discipline?.bonuses?.maxManaBonus || 0);
// Apply upgrade effects if provided // Apply upgrade effects if provided
if (effects) { if (effects) {
@@ -28,7 +35,8 @@ export function computeMaxMana(
export function computeRegen( export function computeRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'attunements'>, state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'attunements'>,
effects?: ComputedEffects effects?: ComputedEffects,
discipline?: DisciplineBonuses,
): number { ): number {
const pu = state.prestigeUpgrades; const pu = state.prestigeUpgrades;
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1; const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
@@ -44,6 +52,11 @@ export function computeRegen(
const attunementRegen = getTotalAttunementRegen(state.attunements || {}); const attunementRegen = getTotalAttunementRegen(state.attunements || {});
regen += attunementRegen; regen += attunementRegen;
// Apply discipline regen bonus
if (discipline?.bonuses?.regenBonus) {
regen += discipline.bonuses.regenBonus;
}
// Apply upgrade effects if provided // Apply upgrade effects if provided
if (effects) { if (effects) {
regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier; regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier;
@@ -55,10 +68,11 @@ export function computeRegen(
// Compute the effective regen (raw regen minus conversion drains) for display purposes // Compute the effective regen (raw regen minus conversion drains) for display purposes
export function computeEffectiveRegenForDisplay( export function computeEffectiveRegenForDisplay(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'attunements'>, state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'attunements'>,
effects?: ComputedEffects effects?: ComputedEffects,
discipline?: DisciplineBonuses,
): { rawRegen: number; conversionDrain: number; effectiveRegen: number } { ): { rawRegen: number; conversionDrain: number; effectiveRegen: number } {
// Get the full raw regen (without conversion drain) // Get the full raw regen (without conversion drain)
const rawRegen = computeRegen(state, effects); const rawRegen = computeRegen(state, effects, discipline);
// Calculate conversion drain // Calculate conversion drain
const conversionDrain = getTotalAttunementConversionDrain(state.attunements || {}); const conversionDrain = getTotalAttunementConversionDrain(state.attunements || {});
@@ -74,10 +88,11 @@ export function computeEffectiveRegenForDisplay(
*/ */
export function computeEffectiveRegen( export function computeEffectiveRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'rawMana' | 'incursionStrength' | 'skillUpgrades' | 'skillTiers' | 'attunements'>, state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'rawMana' | 'incursionStrength' | 'skillUpgrades' | 'skillTiers' | 'attunements'>,
effects?: ComputedEffects effects?: ComputedEffects,
discipline?: DisciplineBonuses,
): number { ): number {
// Base regen from existing function // Base regen from existing function
let regen = computeRegen(state, effects); let regen = computeRegen(state, effects, discipline);
const incursionStrength = state.incursionStrength || 0; const incursionStrength = state.incursionStrength || 0;
@@ -87,12 +102,15 @@ export function computeEffectiveRegen(
return regen; return regen;
} }
export function computeClickMana(state: Pick<GameState, 'skills'>): number { export function computeClickMana(
return ( state: Pick<GameState, 'skills'>,
1 + discipline?: DisciplineBonuses,
((state.skills || {}).manaTap || 0) * 1 + ): number {
((state.skills || {}).manaSurge || 0) * 3 const skillTap = ((state.skills || {}).manaTap || 0) * 1;
); const skillSurge = ((state.skills || {}).manaSurge || 0) * 3;
const discClickMult = discipline?.bonuses?.clickManaMultiplier || 0;
return 1 + skillTap + skillSurge + discClickMult;
} }
// Meditation bonus now affects regen rate directly // Meditation bonus now affects regen rate directly