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
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:
@@ -1,8 +1,8 @@
|
||||
# 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.
|
||||
|
||||
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
|
||||
3. 2) data/golems/index.ts > data/golems/utils.ts
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_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.",
|
||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||
},
|
||||
|
||||
@@ -9,7 +9,8 @@ import { ActionButtons } from '@/components/game';
|
||||
import { AttunementStatus } from '@/components/game/AttunementStatus';
|
||||
import { ActivityLogPanel } from '@/components/game/ActivityLogPanel';
|
||||
import { DebugName } from '@/components/game/debug/debug-context';
|
||||
import { useGameStore, useManaStore, useCombatStore, useCraftingStore, usePrestigeStore } 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 { getMeditationBonus, getIncursionStrength } from '@/lib/game/stores';
|
||||
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 baseRegen = computeTotalRegen({ skills: {}, prestigeUpgrades, 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 effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier;
|
||||
|
||||
|
||||
+19
-22
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState, lazy, Suspense } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
// Import from new modular stores
|
||||
import {
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
useCombatStore,
|
||||
usePrestigeStore,
|
||||
useCraftingStore,
|
||||
useDisciplineStore,
|
||||
fmt,
|
||||
computeMaxMana,
|
||||
computeRegen,
|
||||
@@ -17,6 +19,7 @@ import {
|
||||
getMeditationBonus,
|
||||
getIncursionStrength,
|
||||
} from '@/lib/game/stores';
|
||||
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
|
||||
import { useGameLoop } from '@/lib/game/stores/gameHooks';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import { SPELLS_DEF } from '@/lib/game/constants';
|
||||
@@ -120,21 +123,13 @@ export default function ManaLoopGame() {
|
||||
const [activeTab, setActiveTab] = useState('spells');
|
||||
|
||||
// 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();
|
||||
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
const insight = usePrestigeStore((s) => s.insight);
|
||||
const loopInsight = usePrestigeStore((s) => s.loopInsight);
|
||||
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
const meditateTicks = useManaStore((s) => s.meditateTicks);
|
||||
|
||||
|
||||
// Use useShallow to combine multi-field subscriptions and reduce re-renders
|
||||
const { day, hour, initGame } = useGameStore(useShallow(s => ({ day: s.day, hour: s.hour, initGame: s.initGame })));
|
||||
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 spireMode = useCombatStore((s) => s.spireMode);
|
||||
|
||||
const gameOver = useUIStore((s) => s.gameOver);
|
||||
|
||||
// Get equipment state from crafting store
|
||||
@@ -149,40 +144,42 @@ export default function ManaLoopGame() {
|
||||
equipmentInstances
|
||||
});
|
||||
|
||||
// Compute discipline bonuses from active disciplines
|
||||
const disciplineStoreState = useDisciplineStore();
|
||||
const disciplineEffects = computeDisciplineEffects(disciplineStoreState as any);
|
||||
|
||||
const maxMana = computeMaxMana({
|
||||
skills: {},
|
||||
prestigeUpgrades,
|
||||
skillUpgrades: {},
|
||||
skillTiers: {}
|
||||
}, upgradeEffects);
|
||||
}, upgradeEffects as any, disciplineEffects);
|
||||
|
||||
const baseRegen = computeRegen({
|
||||
skills: {},
|
||||
prestigeUpgrades,
|
||||
skillUpgrades: {},
|
||||
skillTiers: {}
|
||||
}, upgradeEffects);
|
||||
skillTiers: {},
|
||||
attunements: {},
|
||||
}, upgradeEffects as any, disciplineEffects);
|
||||
|
||||
const clickMana = computeClickMana({
|
||||
skills: {},
|
||||
prestigeUpgrades,
|
||||
skillUpgrades: {},
|
||||
skillTiers: {}
|
||||
});
|
||||
}, disciplineEffects);
|
||||
|
||||
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency);
|
||||
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, (upgradeEffects as any).meditationEfficiency);
|
||||
const incursionStrength = getIncursionStrength(day, hour);
|
||||
|
||||
// Effective regen with incursion penalty
|
||||
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
||||
|
||||
// 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
|
||||
: 0;
|
||||
|
||||
// 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
|
||||
: 0;
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ function useToast() {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
}, [])
|
||||
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -169,7 +169,7 @@ describe('computeMaxMana', () => {
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
};
|
||||
const effects = { maxManaBonus: 0, maxManaMultiplier: 1 };
|
||||
const effects = { maxManaBonus: 0, maxManaMultiplier: 1 } as any;
|
||||
const result = computeMaxMana(state, effects);
|
||||
expect(result).toBe(100);
|
||||
});
|
||||
@@ -181,7 +181,7 @@ describe('computeMaxMana', () => {
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
};
|
||||
const effects = { maxManaBonus: 0, maxManaMultiplier: 1 };
|
||||
const effects = { maxManaBonus: 0, maxManaMultiplier: 1 } as any;
|
||||
const result = computeMaxMana(state, effects);
|
||||
expect(result).toBe(100 + 5 * 500); // Base + 500 per level
|
||||
});
|
||||
@@ -193,7 +193,7 @@ describe('computeMaxMana', () => {
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
};
|
||||
const effects = { maxManaBonus: 0, maxManaMultiplier: 1.5 };
|
||||
const effects = { maxManaBonus: 0, maxManaMultiplier: 1.5 } as any;
|
||||
const result = computeMaxMana(state, effects);
|
||||
expect(result).toBe(150); // 100 * 1.5
|
||||
});
|
||||
@@ -205,7 +205,7 @@ describe('computeMaxMana', () => {
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
};
|
||||
const effects = { maxManaBonus: 50, maxManaMultiplier: 1 };
|
||||
const effects = { maxManaBonus: 50, maxManaMultiplier: 1 } as any;
|
||||
const result = computeMaxMana(state, effects);
|
||||
expect(result).toBe(150); // 100 + 50
|
||||
});
|
||||
@@ -218,8 +218,9 @@ describe('computeRegen', () => {
|
||||
prestigeUpgrades: {},
|
||||
skillUpgrades: {},
|
||||
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);
|
||||
// Base regen is 2 (this test provides effects, so no attunement bonus)
|
||||
expect(result).toBe(2);
|
||||
@@ -230,12 +231,9 @@ describe('computeClickMana', () => {
|
||||
it('should return base click mana with no skills', () => {
|
||||
const state = {
|
||||
skills: {},
|
||||
prestigeUpgrades: {},
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
};
|
||||
const effects = { clickManaBonus: 0, clickManaMultiplier: 1 };
|
||||
const result = computeClickMana(state, effects);
|
||||
const discipline = { bonuses: {}, multipliers: {} };
|
||||
const result = computeClickMana(state, discipline);
|
||||
expect(result).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -137,13 +137,13 @@ export function computeAllEffects(
|
||||
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(
|
||||
state.skillUpgrades || {},
|
||||
state.skillTiers || {},
|
||||
state.equipmentInstances || {},
|
||||
state.equippedInstances || {},
|
||||
state as GameState
|
||||
state as unknown as GameState
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import type { CombatState } from './combat-state.types';
|
||||
import type { SpellState } from '../types';
|
||||
import { getFloorMaxHP, getFloorElement, calcDamage, canAffordSpellCost, deductSpellCost } from '../utils';
|
||||
import { usePrestigeStore } from './prestigeStore';
|
||||
import { computeDisciplineEffects } from '../effects/discipline-effects';
|
||||
import { useDisciplineStore } from './discipline-slice';
|
||||
|
||||
export function processCombatTick(
|
||||
get: () => CombatState,
|
||||
@@ -35,6 +37,9 @@ export function processCombatTick(
|
||||
return { rawMana, elements, logMessages, totalManaGathered };
|
||||
}
|
||||
|
||||
// Compute discipline bonuses once per tick
|
||||
const disciplineEffects = computeDisciplineEffects(useDisciplineStore.getState() as any);
|
||||
|
||||
// Calculate cast speed (no skill bonus)
|
||||
const totalAttackSpeed = attackSpeedMult;
|
||||
const spellCastSpeed = spellDef.castSpeed || 1;
|
||||
@@ -57,6 +62,7 @@ export function processCombatTick(
|
||||
{ skills: {}, signedPacts: usePrestigeStore.getState().signedPacts },
|
||||
spellId,
|
||||
floorElement,
|
||||
disciplineEffects,
|
||||
);
|
||||
|
||||
// Let gameStore apply damage modifiers (executioner, berserker)
|
||||
@@ -111,6 +117,7 @@ export function processCombatTick(
|
||||
{ skills: {}, signedPacts: usePrestigeStore.getState().signedPacts },
|
||||
eSpell.spellId,
|
||||
eFloorElement,
|
||||
disciplineEffects,
|
||||
);
|
||||
|
||||
const eResult = onDamageDealt(eDamage);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { computeMaxMana } from '../utils';
|
||||
import { computeMaxMana, computeClickMana } from '../utils';
|
||||
import { useUIStore } from './uiStore';
|
||||
import { usePrestigeStore } from './prestigeStore';
|
||||
import { useManaStore } from './manaStore';
|
||||
import { useCombatStore } from './combatStore';
|
||||
import { computeDisciplineEffects } from '../effects/discipline-effects';
|
||||
import { useDisciplineStore } from './discipline-slice';
|
||||
|
||||
export const createResetGame = (set: (state: any) => void, initialState: any) => () => {
|
||||
// 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-crafting-storage');
|
||||
localStorage.removeItem('mana-loop-attunement-storage');
|
||||
localStorage.removeItem('mana-loop-discipline-store');
|
||||
}
|
||||
|
||||
const startFloor = 1;
|
||||
@@ -30,11 +33,14 @@ export const createResetGame = (set: (state: any) => void, initialState: any) =>
|
||||
};
|
||||
|
||||
export const createGatherMana = () => () => {
|
||||
const manaState = useManaStore.getState();
|
||||
const prestigeState = usePrestigeStore.getState();
|
||||
const disciplineEffects = computeDisciplineEffects(useDisciplineStore.getState() as any);
|
||||
|
||||
// Base click mana (no skill bonuses)
|
||||
const cm = 1;
|
||||
// Compute click mana with discipline bonuses (mana-channeling → clickManaMultiplier)
|
||||
const cm = computeClickMana(
|
||||
{ skills: {} },
|
||||
disciplineEffects,
|
||||
);
|
||||
|
||||
const max = computeMaxMana(
|
||||
{
|
||||
@@ -43,7 +49,8 @@ export const createGatherMana = () => () => {
|
||||
skillUpgrades: {},
|
||||
skillTiers: {}
|
||||
},
|
||||
undefined
|
||||
undefined,
|
||||
disciplineEffects,
|
||||
);
|
||||
|
||||
useManaStore.getState().gatherMana(cm, max);
|
||||
|
||||
@@ -5,7 +5,9 @@ import { usePrestigeStore } from './prestigeStore';
|
||||
import { useCombatStore } from './combatStore';
|
||||
import { useUIStore } from './uiStore';
|
||||
import { useCraftingStore } from './craftingStore';
|
||||
import { useDisciplineStore } from './discipline-slice';
|
||||
import { getUnifiedEffects } from '../effects';
|
||||
import { computeDisciplineEffects } from '../effects/discipline-effects';
|
||||
import {
|
||||
computeMaxMana,
|
||||
computeRegen,
|
||||
@@ -16,12 +18,10 @@ import {
|
||||
import { TICK_MS } from '../constants';
|
||||
|
||||
export function useGameLoop() {
|
||||
const tick = useGameStore((s) => s.tick);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(tick, TICK_MS);
|
||||
const interval = setInterval(() => useGameStore.getState().tick(), TICK_MS);
|
||||
return () => clearInterval(interval);
|
||||
}, [tick]);
|
||||
}, []);
|
||||
}
|
||||
|
||||
// ─── Shared Selector Hooks for Common Derived State ────────────────────────────
|
||||
@@ -32,13 +32,18 @@ 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);
|
||||
|
||||
return getUnifiedEffects({
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
equippedInstances,
|
||||
equipmentInstances,
|
||||
});
|
||||
return {
|
||||
...getUnifiedEffects({
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
equippedInstances,
|
||||
equipmentInstances,
|
||||
}),
|
||||
disciplineEffects,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +56,8 @@ 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 upgradeEffects = getUnifiedEffects({
|
||||
skillUpgrades: {},
|
||||
@@ -61,29 +68,31 @@ export function useManaStats() {
|
||||
|
||||
const maxMana = computeMaxMana(
|
||||
{ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {} },
|
||||
upgradeEffects
|
||||
upgradeEffects as any,
|
||||
disciplineEffects,
|
||||
);
|
||||
|
||||
const baseRegen = computeRegen(
|
||||
{ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {} },
|
||||
upgradeEffects
|
||||
{ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, attunements: {} },
|
||||
upgradeEffects as any,
|
||||
disciplineEffects,
|
||||
);
|
||||
|
||||
const clickMana = computeClickMana({
|
||||
skills: {},
|
||||
});
|
||||
}, disciplineEffects);
|
||||
|
||||
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency);
|
||||
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, (upgradeEffects as any).meditationEfficiency);
|
||||
const incursionStrength = getIncursionStrength(day, hour);
|
||||
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
||||
|
||||
// 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
|
||||
: 0;
|
||||
|
||||
// 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
|
||||
: 0;
|
||||
|
||||
|
||||
@@ -5,19 +5,22 @@ import { useUIStore } from './uiStore';
|
||||
import { usePrestigeStore } from './prestigeStore';
|
||||
import { useManaStore } from './manaStore';
|
||||
import { useCombatStore } from './combatStore';
|
||||
import { computeDisciplineEffects } from '../effects/discipline-effects';
|
||||
import { useDisciplineStore } from './discipline-slice';
|
||||
|
||||
export const createStartNewLoop = (set: (state: any) => void) => () => {
|
||||
const prestigeState = usePrestigeStore.getState();
|
||||
const combatState = useCombatStore.getState();
|
||||
const manaState = useManaStore.getState();
|
||||
|
||||
const disciplineEffects = computeDisciplineEffects(useDisciplineStore.getState() as any);
|
||||
const insightGained = prestigeState.loopInsight || calcInsight({
|
||||
maxFloorReached: combatState.maxFloorReached,
|
||||
totalManaGathered: manaState.totalManaGathered,
|
||||
signedPacts: prestigeState.signedPacts,
|
||||
prestigeUpgrades: prestigeState.prestigeUpgrades,
|
||||
skills: {},
|
||||
});
|
||||
}, disciplineEffects);
|
||||
|
||||
const total = prestigeState.insight + insightGained;
|
||||
|
||||
|
||||
@@ -97,11 +97,13 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
|
||||
const maxMana = computeMaxMana(
|
||||
{ skills: {}, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: {}, skillTiers: {} },
|
||||
undefined
|
||||
undefined,
|
||||
disciplineEffects,
|
||||
);
|
||||
const baseRegen = computeRegen(
|
||||
{ skills: {}, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, attunements: {} },
|
||||
undefined
|
||||
undefined,
|
||||
disciplineEffects,
|
||||
);
|
||||
|
||||
// Time progression
|
||||
@@ -120,7 +122,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
signedPacts: prestigeState.signedPacts,
|
||||
prestigeUpgrades: prestigeState.prestigeUpgrades,
|
||||
skills: {},
|
||||
});
|
||||
}, disciplineEffects);
|
||||
|
||||
addLog(`⏰ The loop ends. Gained ${insightGained} Insight.`);
|
||||
useUIStore.getState().setGameOver(true, false);
|
||||
@@ -137,7 +139,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
signedPacts: prestigeState.signedPacts,
|
||||
prestigeUpgrades: prestigeState.prestigeUpgrades,
|
||||
skills: {},
|
||||
}) * 3;
|
||||
}, disciplineEffects) * 3;
|
||||
|
||||
addLog(`🏆 VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`);
|
||||
useUIStore.getState().setGameOver(true, true);
|
||||
|
||||
@@ -20,6 +20,8 @@ export type { CraftingState, CraftingActions } from './craftingStore';
|
||||
export { useAttunementStore } from './attunementStore';
|
||||
export type { AttunementStoreState } from './attunementStore';
|
||||
|
||||
export { useDisciplineStore } from './discipline-slice';
|
||||
|
||||
export { useGameStore } from './gameStore';
|
||||
export { useGameLoop } from './gameHooks';
|
||||
export type { GameCoordinatorState, GameCoordinatorStore } from './gameStore';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Handles logs, pause state, and UI-specific state
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
export interface LogEntry {
|
||||
message: string;
|
||||
@@ -25,40 +26,45 @@ export interface UIState {
|
||||
|
||||
const MAX_LOGS = 50;
|
||||
|
||||
export const useUIStore = create<UIState>((set) => ({
|
||||
logs: ['✨ The loop begins. You start with Mana Bolt. Gather your strength, mage.'],
|
||||
paused: false,
|
||||
gameOver: false,
|
||||
victory: false,
|
||||
|
||||
addLog: (message: string) => {
|
||||
set((state) => ({
|
||||
logs: [message, ...state.logs.slice(0, MAX_LOGS - 1)],
|
||||
}));
|
||||
},
|
||||
|
||||
clearLogs: () => {
|
||||
set({ logs: [] });
|
||||
},
|
||||
|
||||
togglePause: () => {
|
||||
set((state) => ({ paused: !state.paused }));
|
||||
},
|
||||
|
||||
setPaused: (paused: boolean) => {
|
||||
set({ paused });
|
||||
},
|
||||
|
||||
setGameOver: (gameOver: boolean, victory: boolean = false) => {
|
||||
set({ gameOver, victory });
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
set({
|
||||
export const useUIStore = create<UIState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
logs: ['✨ The loop begins. You start with Mana Bolt. Gather your strength, mage.'],
|
||||
paused: false,
|
||||
gameOver: false,
|
||||
victory: false,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
addLog: (message: string) => {
|
||||
set((state) => ({
|
||||
logs: [message, ...state.logs.slice(0, MAX_LOGS - 1)],
|
||||
}));
|
||||
},
|
||||
|
||||
clearLogs: () => {
|
||||
set({ logs: [] });
|
||||
},
|
||||
|
||||
togglePause: () => {
|
||||
set((state) => ({ paused: !state.paused }));
|
||||
},
|
||||
|
||||
setPaused: (paused: boolean) => {
|
||||
set({ paused });
|
||||
},
|
||||
|
||||
setGameOver: (gameOver: boolean, victory: boolean = false) => {
|
||||
set({ gameOver, victory });
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
set({
|
||||
logs: ['✨ The loop begins. You start with Mana Bolt. Gather your strength, mage.'],
|
||||
paused: false,
|
||||
gameOver: false,
|
||||
victory: false,
|
||||
});
|
||||
},
|
||||
}),
|
||||
{ name: 'mana-loop-ui-storage' }
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// ─── Combat Utilities ────────────────────────────────────────────────────────
|
||||
|
||||
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 { ENCHANTMENT_EFFECTS } from '../data/enchantment-effects';
|
||||
|
||||
@@ -108,56 +109,64 @@ export function getBoonBonuses(signedPacts: number[]): {
|
||||
// ─── Damage Calculation ───────────────────────────────────────────────────────
|
||||
|
||||
export function calcDamage(
|
||||
state: Pick<GameState, 'skills' | 'signedPacts'>,
|
||||
spellId: string,
|
||||
floorElem?: string
|
||||
state: Pick<GameState, 'skills' | 'signedPacts'>,
|
||||
spellId: string,
|
||||
floorElem?: string,
|
||||
discipline?: DisciplineBonuses,
|
||||
): number {
|
||||
const sp = SPELLS_DEF[spellId];
|
||||
if (!sp) return 5;
|
||||
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
|
||||
const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15;
|
||||
|
||||
|
||||
// Guardian bane bonus - check if current floor has a guardian with matching element
|
||||
const isGuardianFloor = floorElem && Object.values(GUARDIANS).some(g => g.element === floorElem);
|
||||
const guardianBonus = isGuardianFloor
|
||||
? 1 + (skills.guardianBane || 0) * 0.2
|
||||
: 1;
|
||||
|
||||
|
||||
// Get boon bonuses from pacts
|
||||
const boons = getBoonBonuses(state.signedPacts);
|
||||
|
||||
|
||||
// Apply raw damage and elemental damage bonuses
|
||||
const rawDamageMult = 1 + boons.rawDamage / 100;
|
||||
const elemDamageMult = 1 + boons.elementalDamage / 100;
|
||||
|
||||
|
||||
// Apply crit chance and damage from boons
|
||||
const critChance = (skills.precision || 0) * 0.05 + boons.critChance / 100;
|
||||
const critDamageMult = 1.5 + boons.critDamage / 100;
|
||||
|
||||
|
||||
let damage = baseDmg * pct * elemMasteryBonus * guardianBonus * rawDamageMult * elemDamageMult;
|
||||
|
||||
|
||||
// Apply elemental bonus if floor element provided
|
||||
if (floorElem) {
|
||||
damage *= getElementalBonus(sp.elem, floorElem);
|
||||
}
|
||||
|
||||
|
||||
// Apply crit
|
||||
if (Math.random() < critChance) {
|
||||
damage *= critDamageMult;
|
||||
}
|
||||
|
||||
|
||||
return damage;
|
||||
}
|
||||
|
||||
// ─── 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 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
|
||||
const boons = getBoonBonuses(state.signedPacts);
|
||||
@@ -265,20 +274,21 @@ export function getActiveEquipmentSpells(
|
||||
export function getTotalDPS(
|
||||
state: Pick<GameState, 'skills' | 'signedPacts' | 'equippedInstances' | 'equipmentInstances' | 'spells' | 'prestigeUpgrades'>,
|
||||
upgradeEffects: { spellDamageBonus?: number; attackSpeedMultiplier?: number; [key: string]: unknown },
|
||||
floorElem?: string
|
||||
floorElem?: string,
|
||||
discipline?: DisciplineBonuses,
|
||||
): number {
|
||||
let totalDPS = 0;
|
||||
|
||||
|
||||
// Get active equipment spells
|
||||
const activeSpells = getActiveEquipmentSpells(state.equippedInstances, state.equipmentInstances);
|
||||
|
||||
|
||||
// Calculate DPS for each active spell
|
||||
for (const { spellId } of activeSpells) {
|
||||
const spellDef = SPELLS_DEF[spellId];
|
||||
if (!spellDef) continue;
|
||||
|
||||
|
||||
// Calculate damage per cast
|
||||
const damage = calcDamage(state, spellId, floorElem);
|
||||
const damage = calcDamage(state, spellId, floorElem, discipline);
|
||||
|
||||
// Get cast speed (spells per second)
|
||||
// Base cast time is 1 second, modified by casting speed bonuses
|
||||
|
||||
@@ -9,7 +9,8 @@ export {
|
||||
computeEffectiveRegen,
|
||||
computeEffectiveRegenForDisplay,
|
||||
computeClickMana,
|
||||
getMeditationBonus
|
||||
getMeditationBonus,
|
||||
type DisciplineBonuses,
|
||||
} from './mana-utils';
|
||||
// computeElementMax is now in ../store.ts with support for unlockedManaTypeUpgrades
|
||||
export {
|
||||
|
||||
@@ -5,16 +5,23 @@ import type { ComputedEffects } from '../effects/upgrade-effects.types';
|
||||
import { HOURS_PER_TICK } from '../constants';
|
||||
import { getTotalAttunementRegen, getTotalAttunementConversionDrain } from '../data/attunements';
|
||||
|
||||
export interface DisciplineBonuses {
|
||||
bonuses: Record<string, number>;
|
||||
multipliers: Record<string, number>;
|
||||
}
|
||||
|
||||
export function computeMaxMana(
|
||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
|
||||
effects?: ComputedEffects
|
||||
effects?: ComputedEffects,
|
||||
discipline?: DisciplineBonuses,
|
||||
): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const base =
|
||||
const base =
|
||||
100 +
|
||||
((state.skills || {}).manaWell || 0) * 100 +
|
||||
((pu || {}).manaWell || 0) * 500;
|
||||
|
||||
((pu || {}).manaWell || 0) * 500 +
|
||||
(discipline?.bonuses?.maxManaBonus || 0);
|
||||
|
||||
// Apply upgrade effects if provided
|
||||
if (effects) {
|
||||
return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
|
||||
@@ -28,7 +35,8 @@ export function computeMaxMana(
|
||||
|
||||
export function computeRegen(
|
||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'attunements'>,
|
||||
effects?: ComputedEffects
|
||||
effects?: ComputedEffects,
|
||||
discipline?: DisciplineBonuses,
|
||||
): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
|
||||
@@ -37,35 +45,41 @@ export function computeRegen(
|
||||
((state.skills || {}).manaFlow || 0) * 1 +
|
||||
((state.skills || {}).manaSpring || 0) * 2 +
|
||||
((pu || {}).manaFlow || 0) * 0.5;
|
||||
|
||||
|
||||
let regen = base * temporalBonus;
|
||||
|
||||
|
||||
// Add attunement raw mana regen
|
||||
const attunementRegen = getTotalAttunementRegen(state.attunements || {});
|
||||
regen += attunementRegen;
|
||||
|
||||
|
||||
// Apply discipline regen bonus
|
||||
if (discipline?.bonuses?.regenBonus) {
|
||||
regen += discipline.bonuses.regenBonus;
|
||||
}
|
||||
|
||||
// Apply upgrade effects if provided
|
||||
if (effects) {
|
||||
regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier;
|
||||
}
|
||||
|
||||
|
||||
return regen;
|
||||
}
|
||||
|
||||
// Compute the effective regen (raw regen minus conversion drains) for display purposes
|
||||
export function computeEffectiveRegenForDisplay(
|
||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'attunements'>,
|
||||
effects?: ComputedEffects
|
||||
effects?: ComputedEffects,
|
||||
discipline?: DisciplineBonuses,
|
||||
): { rawRegen: number; conversionDrain: number; effectiveRegen: number } {
|
||||
// Get the full raw regen (without conversion drain)
|
||||
const rawRegen = computeRegen(state, effects);
|
||||
|
||||
const rawRegen = computeRegen(state, effects, discipline);
|
||||
|
||||
// Calculate conversion drain
|
||||
const conversionDrain = getTotalAttunementConversionDrain(state.attunements || {});
|
||||
|
||||
|
||||
// Effective regen is what actually increases raw mana
|
||||
const effectiveRegen = Math.max(0, rawRegen - conversionDrain);
|
||||
|
||||
|
||||
return { rawRegen, conversionDrain, effectiveRegen };
|
||||
}
|
||||
|
||||
@@ -74,25 +88,29 @@ export function computeEffectiveRegenForDisplay(
|
||||
*/
|
||||
export function computeEffectiveRegen(
|
||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'rawMana' | 'incursionStrength' | 'skillUpgrades' | 'skillTiers' | 'attunements'>,
|
||||
effects?: ComputedEffects
|
||||
effects?: ComputedEffects,
|
||||
discipline?: DisciplineBonuses,
|
||||
): number {
|
||||
// Base regen from existing function
|
||||
let regen = computeRegen(state, effects);
|
||||
|
||||
let regen = computeRegen(state, effects, discipline);
|
||||
|
||||
const incursionStrength = state.incursionStrength || 0;
|
||||
|
||||
|
||||
// Apply incursion penalty
|
||||
regen *= (1 - incursionStrength);
|
||||
|
||||
|
||||
return regen;
|
||||
}
|
||||
|
||||
export function computeClickMana(state: Pick<GameState, 'skills'>): number {
|
||||
return (
|
||||
1 +
|
||||
((state.skills || {}).manaTap || 0) * 1 +
|
||||
((state.skills || {}).manaSurge || 0) * 3
|
||||
);
|
||||
export function computeClickMana(
|
||||
state: Pick<GameState, 'skills'>,
|
||||
discipline?: DisciplineBonuses,
|
||||
): number {
|
||||
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
|
||||
@@ -100,29 +118,29 @@ export function getMeditationBonus(meditateTicks: number, skills: Record<string,
|
||||
const hasMeditation = skills.meditation === 1;
|
||||
const hasDeepTrance = skills.deepTrance === 1;
|
||||
const hasVoidMeditation = skills.voidMeditation === 1;
|
||||
|
||||
|
||||
const hours = meditateTicks * HOURS_PER_TICK;
|
||||
|
||||
|
||||
// Base meditation: ramps up over 4 hours to 1.5x
|
||||
let bonus = 1 + Math.min(hours / 4, 0.5);
|
||||
|
||||
|
||||
// With Meditation Focus: up to 2.5x after 4 hours
|
||||
if (hasMeditation && hours >= 4) {
|
||||
bonus = 2.5;
|
||||
}
|
||||
|
||||
|
||||
// With Deep Trance: up to 3.0x after 6 hours
|
||||
if (hasDeepTrance && hours >= 6) {
|
||||
bonus = 3.0;
|
||||
}
|
||||
|
||||
|
||||
// With Void Meditation: up to 5.0x after 8 hours
|
||||
if (hasVoidMeditation && hours >= 8) {
|
||||
bonus = 5.0;
|
||||
}
|
||||
|
||||
|
||||
// Apply meditation efficiency from upgrades (Deep Wellspring, etc.)
|
||||
bonus *= meditationEfficiency;
|
||||
|
||||
|
||||
return bonus;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user