ca86b6268c
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 55s
- Fix broken barrel exports in components/game/index.ts - Remove skill system from stores (gameStore, gameActions, gameLoopActions, gameHooks, craftingStore, combat) - Remove skill system from components (page.tsx, LeftPanel, StatsTab, SpellsTab, EnchantmentDesigner, EnchantmentPreparer, GameContext/Provider) - Delete dead code: stats/ directory, attunements/ directory, layout/ Header+TabBar, shared/ StudyProgress+UpgradeDialog duplicates, effects.ts.fix, study-slice.ts, navigation-slice.ts - Delete legacy store/ and store-modules/ directories, redirect remaining callers - Merge root formatting.ts into utils/formatting.ts - Move effects files (dynamic-compute, upgrade-effects, special-effects, upgrade-effects.types) into effects/ directory - Move debug-context.tsx into components/game/debug/ - Create tabs/index.ts barrel for tab components - Fix page.tsx lazy imports to use tabs barrel - Fix all broken import paths across codebase - Remove SKILLS_DEF and skill-evolution references - Trim store.ts to under 400 lines by removing dead skill actions
356 lines
11 KiB
TypeScript
Executable File
356 lines
11 KiB
TypeScript
Executable File
// ─── Game Store (Refactored) ──────────────────────────────────────────────
|
|
// Main entry point - imports from modular store components
|
|
|
|
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
import type { GameState, GameAction, ActivityLogEntry } from './types';
|
|
|
|
import { addActivityLogEntry } from './utils/activity-log';
|
|
import {
|
|
computeMaxMana, computeRegen, computeClickMana,
|
|
getMeditationBonus,
|
|
} from './utils/mana-utils';
|
|
import {
|
|
calcDamage, calcInsight, getIncursionStrength, canAffordSpellCost, deductSpellCost,
|
|
} from './utils/combat-utils';
|
|
import { generateFloorState } from './utils/room-utils';
|
|
|
|
// Re-export formatting functions for backward compatibility
|
|
export { fmt, fmtDec } from './utils/formatting';
|
|
export { getFloorMaxHP, getFloorElement } from './utils/floor-utils';
|
|
|
|
// Re-export computed stats functions for backward compatibility and tests
|
|
export {
|
|
computeMaxMana, computeRegen, computeClickMana,
|
|
getMeditationBonus,
|
|
} from './utils/mana-utils';
|
|
export {
|
|
calcDamage, calcInsight, getIncursionStrength, canAffordSpellCost, deductSpellCost,
|
|
} from './utils/combat-utils';
|
|
|
|
// ─── Initial State ───────────────────────────────────────────────────────
|
|
|
|
interface MakeInitialOptions {
|
|
loopCount?: number;
|
|
totalInsight?: number;
|
|
insight?: number;
|
|
prestigeUpgrades?: Record<string, number>;
|
|
}
|
|
|
|
export function makeInitial(opts?: MakeInitialOptions): GameState {
|
|
return {
|
|
day: 1,
|
|
hour: 0,
|
|
rawMana: 100,
|
|
maxMana: 100,
|
|
elements: {},
|
|
skills: {},
|
|
skillUpgrades: {},
|
|
skillTiers: {},
|
|
spells: {},
|
|
currentAction: 'meditate' as GameAction,
|
|
currentStudyTarget: null,
|
|
parallelStudyTarget: null,
|
|
activeSpell: null,
|
|
currentFloor: 100,
|
|
floorHP: 1000,
|
|
floorMaxHP: 1000,
|
|
currentRoom: generateFloorState(100),
|
|
maxFloorReached: 100,
|
|
paused: false,
|
|
gameOver: false,
|
|
victory: false,
|
|
loopCount: opts?.loopCount ?? 0,
|
|
totalInsight: opts?.totalInsight ?? 0,
|
|
insight: opts?.insight ?? 0,
|
|
loopInsight: 0,
|
|
prestigeUpgrades: opts?.prestigeUpgrades ?? {},
|
|
signedPacts: [],
|
|
attunements: {},
|
|
golemancy: { enabledGolems: [] },
|
|
memories: [],
|
|
memorySlots: 0,
|
|
log: [],
|
|
activityLog: [],
|
|
meditateTicks: 0,
|
|
totalManaGathered: 0,
|
|
equippedInstances: {},
|
|
equipmentInstances: {},
|
|
lootInventory: {},
|
|
blueprints: {},
|
|
spireMode: false,
|
|
};
|
|
}
|
|
|
|
// ─── Game Store Interface ─────────────────────────────────────────────────
|
|
|
|
export interface GameStore extends GameState {
|
|
tick: () => void;
|
|
gatherMana: () => void;
|
|
setAction: (action: GameAction) => void;
|
|
addActivityLog: (eventType: string, message: string, details?: ActivityLogEntry['details']) => void;
|
|
setSpell: (spellId: string) => void;
|
|
cancelStudy: () => void;
|
|
convertMana: (element: string, amount: number) => void;
|
|
unlockElement: (element: string) => void;
|
|
doPrestige: (id: string, selectedManaType?: string) => void;
|
|
startNewLoop: () => void;
|
|
togglePause: () => void;
|
|
resetGame: () => void;
|
|
addLog: (message: string) => void;
|
|
addAttunementXP: (attunementId: string, amount: number) => void;
|
|
toggleGolem: (golemId: string) => void;
|
|
setEnabledGolems: (golemIds: string[]) => void;
|
|
debugUnlockAttunement: (attunementId: string) => void;
|
|
debugAddElementalMana: (element: string, amount: number) => void;
|
|
debugSetTime: (day: number, hour: number) => void;
|
|
debugAddAttunementXP: (attunementId: string, amount: number) => void;
|
|
debugSetFloor: (floor: number) => void;
|
|
resetFloorHP: () => void;
|
|
getMaxMana: () => number;
|
|
getRegen: () => number;
|
|
getClickMana: () => number;
|
|
getDamage: (spellId: string) => number;
|
|
getMeditationMultiplier: () => number;
|
|
canCastSpell: (spellId: string) => boolean;
|
|
enterSpireMode: () => void;
|
|
climbDownFloor: () => void;
|
|
exitSpireMode: () => void;
|
|
}
|
|
|
|
// ─── Store Implementation ────────────────────────────────────────────────
|
|
|
|
export const useGameStore = create<GameStore>()(
|
|
persist(
|
|
(set, get) => ({
|
|
...makeInitial(),
|
|
|
|
getMaxMana: () => computeMaxMana(get()),
|
|
getRegen: () => computeRegen(get()),
|
|
getClickMana: () => computeClickMana(get()),
|
|
getDamage: (spellId: string) => calcDamage(get(), spellId),
|
|
getMeditationMultiplier: () => getMeditationBonus(get().meditateTicks, {}),
|
|
|
|
canCastSpell: (spellId: string) => {
|
|
const state = get();
|
|
const spell = state.spells?.[spellId];
|
|
if (!spell) return false;
|
|
return true;
|
|
},
|
|
|
|
addLog: (message: string) => {
|
|
set((state) => ({
|
|
log: [message, ...(state.log || []).slice(0, 49)],
|
|
}));
|
|
},
|
|
|
|
addActivityLog: (eventType: string, message: string, details?: ActivityLogEntry['details']) => {
|
|
set((state) => ({
|
|
activityLog: addActivityLogEntry(state, eventType, message, details),
|
|
}));
|
|
},
|
|
|
|
tick: () => {
|
|
const state = get();
|
|
if (state.gameOver || state.paused) return;
|
|
|
|
const maxMana = computeMaxMana(state);
|
|
const baseRegen = computeRegen(state);
|
|
|
|
let hour = state.hour + 1;
|
|
let day = state.day;
|
|
if (hour >= 24) { hour -= 24; day += 1; }
|
|
|
|
if (day > 100) {
|
|
const insightGained = calcInsight(state);
|
|
set({
|
|
day, hour, gameOver: true, victory: false, loopInsight: insightGained,
|
|
log: [`⏰ The loop ends. Gained ${insightGained} Insight.`, ...state.log.slice(0, 49)],
|
|
});
|
|
return;
|
|
}
|
|
|
|
let rawMana = state.rawMana + baseRegen;
|
|
rawMana = Math.min(rawMana, maxMana);
|
|
|
|
set({
|
|
day, hour, rawMana,
|
|
meditateTicks: state.currentAction === 'meditate' ? state.meditateTicks + 1 : 0,
|
|
});
|
|
},
|
|
|
|
gatherMana: () => {
|
|
const state = get();
|
|
const clickMana = computeClickMana(state);
|
|
const maxMana = computeMaxMana(state);
|
|
set((s) => ({
|
|
rawMana: Math.min(s.rawMana + clickMana, maxMana),
|
|
totalManaGathered: s.totalManaGathered + clickMana,
|
|
}));
|
|
},
|
|
|
|
setAction: (action: GameAction) => {
|
|
set({ currentAction: action });
|
|
},
|
|
|
|
setSpell: (spellId: string) => {
|
|
set({ activeSpell: spellId });
|
|
},
|
|
|
|
cancelStudy: () => {
|
|
set({ currentStudyTarget: null, currentAction: 'meditate' });
|
|
},
|
|
|
|
convertMana: (element: string, amount: number) => {
|
|
set((s) => {
|
|
const elem = s.elements?.[element];
|
|
if (!elem || !elem.unlocked) return s;
|
|
const canConvert = Math.min(amount, Math.floor(s.rawMana / 10), elem.max - elem.current);
|
|
if (canConvert > 0) {
|
|
return {
|
|
rawMana: s.rawMana - canConvert * 10,
|
|
elements: { ...s.elements, [element]: { ...elem, current: elem.current + canConvert } },
|
|
};
|
|
}
|
|
return s;
|
|
});
|
|
},
|
|
|
|
unlockElement: (element: string) => {
|
|
set((s) => ({
|
|
elements: { ...s.elements, [element]: { ...s.elements[element], unlocked: true } },
|
|
}));
|
|
},
|
|
|
|
doPrestige: (id: string, selectedManaType?: string) => {
|
|
set((s) => ({
|
|
prestigeUpgrades: { ...s.prestigeUpgrades, [id]: (s.prestigeUpgrades[id] || 0) + 1 },
|
|
}));
|
|
},
|
|
|
|
startNewLoop: () => {
|
|
const state = get();
|
|
const insightGained = state.loopInsight || 0;
|
|
set({
|
|
...makeInitial({
|
|
loopCount: state.loopCount + 1,
|
|
totalInsight: (state.totalInsight || 0) + insightGained,
|
|
insight: (state.insight || 0) + insightGained,
|
|
prestigeUpgrades: state.prestigeUpgrades,
|
|
}),
|
|
});
|
|
},
|
|
|
|
togglePause: () => {
|
|
set((s) => ({ paused: !s.paused }));
|
|
},
|
|
|
|
resetGame: () => {
|
|
set(makeInitial());
|
|
},
|
|
|
|
addAttunementXP: (attunementId: string, amount: number) => {
|
|
set((s) => {
|
|
const attState = s.attunements?.[attunementId];
|
|
if (!attState) return s;
|
|
return {
|
|
attunements: { ...s.attunements, [attunementId]: { ...attState, experience: attState.experience + amount } },
|
|
};
|
|
});
|
|
},
|
|
|
|
toggleGolem: (golemId: string) => {
|
|
set((s) => {
|
|
const enabledGolems = s.golemancy?.enabledGolems || [];
|
|
const isEnabled = enabledGolems.includes(golemId);
|
|
return {
|
|
golemancy: { ...s.golemancy, enabledGolems: isEnabled ? enabledGolems.filter(id => id !== golemId) : [...enabledGolems, golemId] },
|
|
};
|
|
});
|
|
},
|
|
|
|
setEnabledGolems: (golemIds: string[]) => {
|
|
set((s) => ({
|
|
golemancy: { ...s.golemancy, enabledGolems: golemIds },
|
|
}));
|
|
},
|
|
|
|
debugUnlockAttunement: (attunementId: string) => {
|
|
set((s) => ({
|
|
attunements: { ...s.attunements, [attunementId]: { id: attunementId, active: true, level: 1, experience: 0 } },
|
|
}));
|
|
},
|
|
|
|
debugAddElementalMana: (element: string, amount: number) => {
|
|
set((s) => {
|
|
const elem = s.elements?.[element];
|
|
if (!elem) return s;
|
|
return {
|
|
elements: { ...s.elements, [element]: { ...elem, current: Math.min(elem.current + amount, elem.max) } },
|
|
};
|
|
});
|
|
},
|
|
|
|
debugSetTime: (day: number, hour: number) => {
|
|
set({ day, hour });
|
|
},
|
|
|
|
debugAddAttunementXP: (attunementId: string, amount: number) => {
|
|
get().addAttunementXP(attunementId, amount);
|
|
},
|
|
|
|
debugSetFloor: (floor: number) => {
|
|
set((s) => ({
|
|
currentFloor: floor,
|
|
currentRoom: generateFloorState(floor),
|
|
floorMaxHP: 100 + floor * 50,
|
|
floorHP: 100 + floor * 50,
|
|
}));
|
|
},
|
|
|
|
resetFloorHP: () => {
|
|
set((s) => ({
|
|
floorHP: s.floorMaxHP,
|
|
currentRoom: generateFloorState(s.currentFloor),
|
|
}));
|
|
},
|
|
|
|
enterSpireMode: () => {
|
|
set({ spireMode: true });
|
|
},
|
|
|
|
climbDownFloor: () => {
|
|
set((s) => {
|
|
if (s.currentFloor <= 1) return s;
|
|
const newFloor = s.currentFloor - 1;
|
|
return {
|
|
currentFloor: newFloor,
|
|
currentRoom: generateFloorState(newFloor),
|
|
floorMaxHP: 100 + newFloor * 50,
|
|
floorHP: 100 + newFloor * 50,
|
|
};
|
|
});
|
|
},
|
|
|
|
exitSpireMode: () => {
|
|
set({ spireMode: false });
|
|
},
|
|
}),
|
|
{
|
|
name: 'mana-loop-game-store',
|
|
}
|
|
)
|
|
);
|
|
|
|
// ─── Game Loop Hook ───────────────────────────────────────────────────────────
|
|
|
|
export function useGameLoop() {
|
|
const tick = useGameStore((s) => s.tick);
|
|
return {
|
|
start: () => {
|
|
const interval = setInterval(tick, 1000);
|
|
return () => clearInterval(interval);
|
|
},
|
|
};
|
|
}
|