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:
@@ -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' }
|
||||
)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user