refactor: remove legacy store.ts and crafting-slice.ts, complete modular store migration
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m18s

- Delete store.ts (355 LOC monolithic store, zero imports)
- Delete crafting-slice.ts (379 LOC legacy crafting module)
- Inline createStartingEquipment() into craftingStore.ts
- Remove legacy equipment/inventory fields from GameState
- Remove EquipmentDef from game.ts imports (unused)
- Fix duplicate EquipmentSpellState export in types.ts
- Fix bluePrintId typo in craftingStore.ts
- Update stores/index.ts to import CraftingState/CraftingActions from craftingStore.types
- Update EquipmentTab.test.ts to test store state instead of deleted module
- Clean up stale comments referencing crafting-slice.ts
- Reduce TS errors from 83 to 72 by removing conflicting legacy types
This commit is contained in:
2026-05-20 12:36:00 +02:00
parent 56ac50f465
commit cba42e01ff
17 changed files with 104 additions and 781 deletions
+3 -5
View File
@@ -1,10 +1,8 @@
# Circular Dependencies
Generated: 2026-05-20T07:28:09.937Z
Found: 3 circular chain(s) — these MUST be fixed before modifying involved files.
Generated: 2026-05-20T10:00:51.281Z
Found: 1 circular chain(s) — these MUST be fixed before modifying involved files.
1. Processed 125 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. Processed 127 files (1.4s) (4 warnings)
## How to fix
1. Identify which import in the chain can be extracted to a shared types/utils file.
+21 -6
View File
@@ -1,6 +1,6 @@
{
"_meta": {
"generated": "2026-05-20T07:28:08.406Z",
"generated": "2026-05-20T10:00:49.725Z",
"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."
},
@@ -299,6 +299,17 @@
"data/equipment/catalysts.ts": [
"data/equipment/types.ts"
],
"data/equipment/equipment-types-data.ts": [
"data/equipment/accessories.ts",
"data/equipment/body.ts",
"data/equipment/casters.ts",
"data/equipment/catalysts.ts",
"data/equipment/feet.ts",
"data/equipment/hands.ts",
"data/equipment/head.ts",
"data/equipment/shields.ts",
"data/equipment/swords.ts"
],
"data/equipment/feet.ts": [
"data/equipment/types.ts"
],
@@ -313,6 +324,7 @@
"data/equipment/body.ts",
"data/equipment/casters.ts",
"data/equipment/catalysts.ts",
"data/equipment/equipment-types-data.ts",
"data/equipment/feet.ts",
"data/equipment/hands.ts",
"data/equipment/head.ts",
@@ -331,7 +343,7 @@
"types/equipmentSlot.ts"
],
"data/equipment/utils.ts": [
"data/equipment/index.ts",
"data/equipment/equipment-types-data.ts",
"data/equipment/types.ts"
],
"data/fabricator-recipes.ts": [
@@ -343,19 +355,22 @@
"data/golems/elemental-golems.ts": [
"data/golems/types.ts"
],
"data/golems/golems-data.ts": [
"data/golems/base-golems.ts",
"data/golems/elemental-golems.ts",
"data/golems/hybrid-golems.ts"
],
"data/golems/hybrid-golems.ts": [
"data/golems/types.ts"
],
"data/golems/index.ts": [
"data/golems/base-golems.ts",
"data/golems/elemental-golems.ts",
"data/golems/hybrid-golems.ts",
"data/golems/golems-data.ts",
"data/golems/types.ts",
"data/golems/utils.ts"
],
"data/golems/types.ts": [],
"data/golems/utils.ts": [
"data/golems/index.ts",
"data/golems/golems-data.ts",
"data/golems/types.ts"
],
"data/guardian-encounters.ts": [
-2
View File
@@ -330,10 +330,8 @@ Mana-Loop/
│ │ ├── crafting-equipment.ts
│ │ ├── crafting-loot.ts
│ │ ├── crafting-prep.ts
│ │ ├── crafting-slice.ts
│ │ ├── crafting-utils.ts
│ │ ├── effects.ts
│ │ ├── store.ts
│ │ └── types.ts
│ └── utils.ts
├── test-results/
+14 -14
View File
@@ -83,25 +83,25 @@ describe('Equipment type definitions', () => {
// ─── Test: Starting equipment ──────────────────────────────────────────────────
describe('Starting equipment', () => {
it('createStartingEquipment returns valid equippedInstances', async () => {
const { createStartingEquipment } = await import('@/lib/game/crafting-slice');
const { equippedInstances, equipmentInstances } = createStartingEquipment();
it('crafting store initial state has valid equippedInstances', async () => {
const { useCraftingStore } = await import('@/lib/game/stores/craftingStore');
const state = useCraftingStore.getState();
expect(equippedInstances.mainHand).toBeTruthy();
expect(equippedInstances.body).toBeTruthy();
expect(equippedInstances.feet).toBeTruthy();
expect(equippedInstances.offHand).toBeNull();
expect(equippedInstances.head).toBeNull();
expect(equippedInstances.hands).toBeNull();
expect(equippedInstances.accessory1).toBeNull();
expect(equippedInstances.accessory2).toBeNull();
expect(state.equippedInstances.mainHand).toBeTruthy();
expect(state.equippedInstances.body).toBeTruthy();
expect(state.equippedInstances.feet).toBeTruthy();
expect(state.equippedInstances.offHand).toBeNull();
expect(state.equippedInstances.head).toBeNull();
expect(state.equippedInstances.hands).toBeNull();
expect(state.equippedInstances.accessory1).toBeNull();
expect(state.equippedInstances.accessory2).toBeNull();
expect(Object.keys(equipmentInstances).length).toBe(3);
expect(Object.keys(state.equipmentInstances).length).toBe(3);
});
it('starting equipment instances have valid fields', async () => {
const { createStartingEquipment } = await import('@/lib/game/crafting-slice');
const { equipmentInstances } = createStartingEquipment();
const { useCraftingStore } = await import('@/lib/game/stores/craftingStore');
const { equipmentInstances } = useCraftingStore.getState();
for (const instance of Object.values(equipmentInstances)) {
expect(instance.instanceId).toBeTruthy();
+1 -1
View File
@@ -1,5 +1,5 @@
// ─── Crafting Application System ────────────────────────────────────────────
// Application system functions extracted from crafting-slice.ts
// Application system functions
import type { EquipmentInstance, AppliedEnchantment, EnchantmentDesign, ApplicationProgress } from './types';
import { calculateApplicationTime, calculateApplicationManaPerHour } from './crafting-utils';
+1 -1
View File
@@ -1,5 +1,5 @@
// ─── Attunement System ──────────────────────────────────────────────────────
// Attunement system functions extracted from crafting-slice.ts
// Attunement system functions
import type { AttunementState } from './types';
import { calculateEnchantingXP, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from './data/attunements';
+1 -1
View File
@@ -1,5 +1,5 @@
// ─── Equipment Crafting System ──────────────────────────────────────────────
// Equipment crafting functions extracted from crafting-slice.ts
// Equipment crafting functions
import type { EquipmentInstance, EquipmentCraftingProgress } from './types';
import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/crafting-recipes';
+1 -1
View File
@@ -1,5 +1,5 @@
// ─── Crafting Loot Inventory System ────────────────────────────────────────────
// Loot inventory functions extracted from crafting-slice.ts
// Loot inventory functions
import type { LootInventory } from './types';
import type { CraftingRecipe } from './data/crafting-recipes';
+1 -1
View File
@@ -1,5 +1,5 @@
// ─── Crafting Preparation System ────────────────────────────────────────────
// Preparation system functions extracted from crafting-slice.ts
// Preparation system functions
import type { EquipmentInstance, PreparationProgress } from './types';
import { calculatePrepTime, calculatePrepManaCost, calculateManaPerHourForPrep } from './crafting-utils';
-379
View File
@@ -1,379 +0,0 @@
// ─── Crafting Store Slice ─────────────────────────────────────────────────────
// Core slice logic for equipment and enchantment system. Extracted logic lives
// in focused modules: crafting-utils, crafting-design, crafting-prep,
// crafting-apply, crafting-equipment, crafting-loot, crafting-attunements.
import type { GameState, EquipmentInstance, AppliedEnchantment, EnchantmentDesign, DesignEffect, EquipmentCraftingProgress, LootInventory, AttunementState } from './types';
import { EQUIPMENT_TYPES } from './data/equipment';
import type { EquipmentSlot } from './data/equipment';
import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects';
import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/crafting-recipes';
import { SPELLS_DEF } from './constants';
import { calculateEnchantingXP, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from './data/attunements';
import { computeEffects } from './effects/upgrade-effects';
import { hasSpecial, SPECIAL_EFFECTS } from './effects/special-effects';
import type { ComputedEffects } from './effects/upgrade-effects.types';
// ─── Crafting Modules ───────────────────────────────────────────────────────
import * as CraftingUtils from './crafting-utils';
import * as CraftingDesign from './crafting-design';
import * as CraftingPrep from './crafting-prep';
import * as CraftingApply from './crafting-apply';
import * as CraftingEquipment from './crafting-equipment';
import * as CraftingLoot from './crafting-loot';
import * as CraftingAttunements from './crafting-attunements';
import * as CraftingActions from './crafting-actions';
// ─── Initial Equipment Setup ─────────────────────────────────────────────────
export function createStartingEquipment() {
const staffId = CraftingUtils.generateInstanceId();
const staffInstance: EquipmentInstance = {
instanceId: staffId,
typeId: 'basicStaff',
name: 'Basic Staff',
enchantments: [{ effectId: 'spell_manaBolt', stacks: 1, actualCost: 50 }],
usedCapacity: 50,
totalCapacity: 50,
rarity: 'common',
quality: 100,
tags: [],
};
const shirtId = CraftingUtils.generateInstanceId();
const shirtInstance: EquipmentInstance = {
instanceId: shirtId,
typeId: 'civilianShirt',
name: 'Civilian Shirt',
enchantments: [],
usedCapacity: 0,
totalCapacity: 30,
rarity: 'common',
quality: 100,
tags: [],
};
const shoesId = CraftingUtils.generateInstanceId();
const shoesInstance: EquipmentInstance = {
instanceId: shoesId,
typeId: 'civilianShoes',
name: 'Civilian Shoes',
enchantments: [],
usedCapacity: 0,
totalCapacity: 15,
rarity: 'common',
quality: 100,
tags: [],
};
return {
equippedInstances: {
mainHand: staffId,
offHand: null,
head: null,
body: shirtId,
hands: null,
feet: shoesId,
accessory1: null,
accessory2: null,
},
equipmentInstances: {
[staffId]: staffInstance,
[shirtId]: shirtInstance,
[shoesId]: shoesInstance,
},
};
}
// ─── Crafting Actions Interface ─────────────────────────────────────────────
export type CraftingActions = {
createEquipmentInstance: (typeId: string) => string | null;
equipItem: (instanceId: string, slot: EquipmentSlot) => boolean;
unequipItem: (slot: EquipmentSlot) => void;
deleteEquipmentInstance: (instanceId: string) => void;
startDesigningEnchantment: (name: string, equipmentTypeId: string, effects: DesignEffect[]) => boolean;
cancelDesign: () => void;
saveDesign: (design: EnchantmentDesign) => void;
deleteDesign: (designId: string) => void;
startPreparing: (equipmentInstanceId: string) => boolean;
cancelPreparation: () => void;
startApplying: (equipmentInstanceId: string, designId: string) => boolean;
pauseApplication: () => void;
resumeApplication: () => void;
cancelApplication: () => void;
disenchantEquipment: (instanceId: string) => void;
startCraftingEquipment: (blueprintId: string) => boolean;
cancelEquipmentCrafting: () => void;
deleteMaterial: (materialId: string, amount: number) => void;
getEquipmentSpells: () => string[];
getEquipmentEffects: () => Record<string, number>;
getAvailableCapacity: (instanceId: string) => number;
};
// ─── Crafting Store Slice ───────────────────────────────────────────────────
export function createCraftingSlice(
set: (fn: (state: GameState) => Partial<GameState>) => void,
get: () => GameState & CraftingActions
): CraftingActions {
return {
createEquipmentInstance: (typeId) => CraftingActions.createEquipmentInstance(typeId, set),
equipItem: (instanceId, slot) => CraftingActions.equipItem(instanceId, slot, get, set),
unequipItem: (slot) => CraftingActions.unequipItem(slot, set),
deleteEquipmentInstance: (instanceId) => CraftingActions.deleteEquipmentInstance(instanceId, get, set),
startDesigningEnchantment: (name, equipmentTypeId, effects) =>
CraftingActions.startDesigningEnchantment(name, equipmentTypeId, effects, get, set),
cancelDesign: () => CraftingActions.cancelDesign(get, set),
saveDesign: (design) => CraftingActions.saveDesign(design, get, set),
deleteDesign: (designId) => CraftingActions.deleteDesign(designId, set),
startPreparing: (equipmentInstanceId) => CraftingActions.startPreparing(equipmentInstanceId, get, set),
cancelPreparation: () => CraftingActions.cancelPreparation(set),
startApplying: (equipmentInstanceId, designId) =>
CraftingActions.startApplying(equipmentInstanceId, designId, get, set),
pauseApplication: () => CraftingActions.pauseApplication(get, set),
resumeApplication: () => CraftingActions.resumeApplication(get, set),
cancelApplication: () => CraftingActions.cancelApplication(set),
disenchantEquipment: (instanceId) => CraftingActions.disenchantEquipment(instanceId, get, set),
startCraftingEquipment: (blueprintId) => CraftingActions.startCraftingEquipment(blueprintId, get, set),
cancelEquipmentCrafting: () => CraftingActions.cancelEquipmentCrafting(get, set),
deleteMaterial: (materialId, amount) => CraftingActions.deleteMaterial(materialId, amount, get, set),
getEquipmentSpells: () => CraftingActions.getEquipmentSpells(get),
getEquipmentEffects: () => CraftingActions.getEquipmentEffects(get),
getAvailableCapacity: (instanceId) => CraftingActions.getAvailableCapacity(instanceId, get),
};
}
// ─── Tick Processing for Crafting ────────────────────────────────────────────
export function processCraftingTick(
state: GameState,
effects: { rawMana: number; log: string[] }
): Partial<GameState> {
const { rawMana, log } = effects;
let updates: Partial<GameState> = {};
const computedEffects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
// Process design progress (slot 1)
if (state.currentAction === 'design' && state.designProgress) {
const designResult = CraftingDesign.calculateDesignProgress(
state.designProgress.progress,
state.designProgress.required,
computedEffects,
false
);
if (designResult.isComplete) {
const completedDesign = CraftingDesign.createCompletedDesignFromProgress(
{
designId: state.designProgress.designId,
name: state.designProgress.name,
equipmentType: state.designProgress.equipmentType,
effects: state.designProgress.effects,
required: state.designProgress.required,
},
((state.skillUpgrades || {})['efficientEnchant'] || []).length * 0.05
);
updates = {
...updates,
designProgress: null,
currentAction: 'meditate' as const,
enchantmentDesigns: [...state.enchantmentDesigns, completedDesign],
log: [`✅ Enchantment design "${completedDesign.name}" complete!`, ...log],
};
} else {
updates = {
...updates,
designProgress: { ...state.designProgress, progress: designResult.progress },
};
}
}
// Process second design progress (slot 2)
if (state.designProgress2) {
const designResult2 = CraftingDesign.calculateSecondDesignProgress(
state.designProgress2.progress,
state.designProgress2.required,
computedEffects,
false
);
if (designResult2.isComplete) {
const completedDesign = CraftingDesign.createCompletedDesignFromProgress(
{
designId: state.designProgress2.designId,
name: state.designProgress2.name,
equipmentType: state.designProgress2.equipmentType,
effects: state.designProgress2.effects,
required: state.designProgress2.required,
},
((state.skillUpgrades || {})['efficientEnchant'] || []).length * 0.05
);
const shouldTransitionToMeditate = !state.designProgress;
updates = {
...updates,
designProgress2: null,
currentAction: shouldTransitionToMeditate ? 'meditate' as const : state.currentAction,
enchantmentDesigns: [...state.enchantmentDesigns, completedDesign],
log: [`✅ Enchantment design "${completedDesign.name}" complete! (2nd slot)`, ...log],
};
} else {
updates = {
...updates,
designProgress2: { ...state.designProgress2, progress: designResult2.progress },
};
}
}
// Process preparation progress
if (state.currentAction === 'prepare' && state.preparationProgress) {
const instance = state.equipmentInstances[state.preparationProgress.equipmentInstanceId];
const manaPerTick = instance ? CraftingPrep.getPreparationManaCostForTick(instance) : 0;
if (rawMana >= manaPerTick) {
const tickResult = CraftingPrep.calculatePreparationTick(
state.preparationProgress.progress,
state.preparationProgress.required,
manaPerTick
);
if (tickResult.isComplete) {
if (instance) {
const completeResult = CraftingPrep.completePreparation(instance, state.preparationProgress.manaCostPaid);
updates = {
...updates,
rawMana: rawMana - tickResult.manaConsumed + completeResult.manaRecovered,
preparationProgress: null,
currentAction: 'meditate' as const,
equipmentInstances: {
...state.equipmentInstances,
[instance.instanceId]: completeResult.updatedInstance,
},
log: [completeResult.logMessage, ...log],
};
} else {
updates = {
...updates,
preparationProgress: null,
currentAction: 'meditate' as const,
rawMana: rawMana - tickResult.manaConsumed,
log: ['✅ Preparation complete!', ...log],
};
}
} else {
updates = {
...updates,
rawMana: rawMana - tickResult.manaConsumed,
preparationProgress: {
...state.preparationProgress,
progress: tickResult.progress,
manaCostPaid: tickResult.manaCostPaid,
},
};
}
}
}
// Process application progress
if (state.currentAction === 'enchant' && state.applicationProgress && !state.applicationProgress.paused) {
const app = state.applicationProgress;
const manaPerTick = CraftingApply.getApplicationManaCostForTick(app.manaPerHour);
if (rawMana >= manaPerTick) {
const tickResult = CraftingApply.calculateApplicationTick(
app.progress,
app.required,
app.manaSpent,
manaPerTick,
computedEffects
);
if (tickResult.isComplete) {
const instance = state.equipmentInstances[app.equipmentInstanceId];
const design = state.enchantmentDesigns.find(d => d.id === app.designId);
if (instance && design) {
const applyResult = CraftingApply.applyEnchantments(instance, design, computedEffects);
const xpGain = CraftingAttunements.gainEnchantingXP(state.attunements, applyResult.xpGained);
updates = {
...updates,
rawMana: rawMana - tickResult.manaConsumed,
applicationProgress: null,
currentAction: 'meditate' as const,
attunements: {
...state.attunements,
enchanter: xpGain.attunements.enchanter,
},
equipmentInstances: {
...state.equipmentInstances,
[instance.instanceId]: applyResult.updatedInstance,
},
log: [applyResult.logMessage, ...log],
};
}
} else {
updates = {
...updates,
rawMana: rawMana - tickResult.manaConsumed,
applicationProgress: { ...app, progress: tickResult.progress, manaSpent: tickResult.manaSpent },
};
}
}
}
// Process equipment crafting progress
if (state.currentAction === 'craft' && state.equipmentCraftingProgress) {
const craft = state.equipmentCraftingProgress;
const tickResult = CraftingEquipment.calculateCraftingTick(craft.progress, craft.required);
if (tickResult.isComplete) {
const recipe = CraftingEquipment.getRecipe(craft.blueprintId);
if (recipe) {
const craftResult = CraftingEquipment.completeEquipmentCrafting(craft.blueprintId, recipe);
updates = {
...updates,
equipmentCraftingProgress: null,
currentAction: 'meditate' as const,
equipmentInstances: {
...state.equipmentInstances,
[craftResult.instanceId]: craftResult.instance,
},
totalCraftsCompleted: (state.totalCraftsCompleted || 0) + 1,
log: [craftResult.logMessage, ...log],
};
} else {
updates = {
...updates,
equipmentCraftingProgress: null,
currentAction: 'meditate' as const,
log: ['⚠️ Crafting failed - invalid recipe!', ...log],
};
}
} else {
updates = {
...updates,
equipmentCraftingProgress: { ...craft, progress: tickResult.progress },
};
}
}
return updates;
}
// ─── Export helper to get equipment instance spells ─────────────────────────
export function getSpellsFromEquipment(instances: Record<string, EquipmentInstance>, equippedIds: (string | null)[]): string[] {
const spells: string[] = [];
for (const id of equippedIds) {
if (!id) continue;
const instance = instances[id];
if (!instance) continue;
for (const ench of instance.enchantments) {
const effectDef = ENCHANTMENT_EFFECTS[ench.effectId];
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
spells.push(effectDef.effect.spellId);
}
}
}
return [...new Set(spells)];
}
+1 -1
View File
@@ -1,5 +1,5 @@
// ─── Crafting Helper Utilities ─────────────────────────────────────────────────
// Instance/ID generation and helper functions extracted from crafting-slice.ts
// Instance/ID generation and helper functions
import type { EquipmentInstance, EnchantmentDesign, DesignEffect } from './types';
import { EQUIPMENT_TYPES, type EquipmentCategory, type EquipmentSlot } from './data/equipment';
-355
View File
@@ -1,355 +0,0 @@
// ─── 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);
},
};
}
+57 -5
View File
@@ -7,7 +7,7 @@ import * as CraftingUtils from '../crafting-utils';
import * as CraftingDesign from '../crafting-design';
import { useManaStore } from './manaStore';
import { useCombatStore } from './combatStore';
import { createStartingEquipment } from '../crafting-slice';
import { useUIStore } from './uiStore';
import * as ApplicationActions from '../crafting-actions/application-actions';
import * as PreparationActions from '../crafting-actions/preparation-actions';
@@ -16,7 +16,45 @@ import * as CraftingEquipment from '../crafting-equipment';
export const useCraftingStore = create<CraftingStore>()(
persist(
(set, get) => {
const startingEquipment = createStartingEquipment();
const staffId = CraftingUtils.generateInstanceId();
const staffInstance = {
instanceId: staffId,
typeId: 'basicStaff',
name: 'Basic Staff',
enchantments: [{ effectId: 'spell_manaBolt', stacks: 1, actualCost: 50 }],
usedCapacity: 50,
totalCapacity: 50,
rarity: 'common',
quality: 100,
tags: [],
};
const shirtId = CraftingUtils.generateInstanceId();
const shirtInstance = {
instanceId: shirtId,
typeId: 'civilianShirt',
name: 'Civilian Shirt',
enchantments: [],
usedCapacity: 0,
totalCapacity: 30,
rarity: 'common',
quality: 100,
tags: [],
};
const shoesId = CraftingUtils.generateInstanceId();
const shoesInstance = {
instanceId: shoesId,
typeId: 'civilianShoes',
name: 'Civilian Shoes',
enchantments: [],
usedCapacity: 0,
totalCapacity: 15,
rarity: 'common',
quality: 100,
tags: [],
};
return {
// Initial state
designProgress: null,
@@ -26,7 +64,21 @@ export const useCraftingStore = create<CraftingStore>()(
equipmentCraftingProgress: null,
enchantmentDesigns: [],
unlockedEffects: [],
...startingEquipment,
equippedInstances: {
mainHand: staffId,
offHand: null,
head: null,
body: shirtId,
hands: null,
feet: shoesId,
accessory1: null,
accessory2: null,
},
equipmentInstances: {
[staffId]: staffInstance,
[shirtId]: shirtInstance,
[shoesId]: shoesInstance,
},
lootInventory: {
materials: {},
blueprints: [],
@@ -177,7 +229,7 @@ export const useCraftingStore = create<CraftingStore>()(
// Check if we can start crafting
const check = CraftingEquipment.canStartEquipmentCrafting(
bluePrintId,
blueprintId,
state.lootInventory.blueprints.includes(blueprintId),
state.lootInventory.materials,
rawMana,
@@ -188,7 +240,7 @@ export const useCraftingStore = create<CraftingStore>()(
// Initialize crafting
const result = CraftingEquipment.initializeEquipmentCrafting(
bluePrintId,
blueprintId,
state.lootInventory.materials,
rawMana
);
+1 -1
View File
@@ -15,7 +15,7 @@ export { useCombatStore, makeInitialSpells } from './combatStore';
export type { CombatState } from './combat-state.types';
export { useCraftingStore } from './craftingStore';
export type { CraftingState, CraftingActions } from './craftingStore';
export type { CraftingState, CraftingActions } from './craftingStore.types';
export { useAttunementStore } from './attunementStore';
export type { AttunementStoreState } from './attunementStore';
-1
View File
@@ -55,7 +55,6 @@ export type {
GameActionType,
ActivityEventType,
ActivityLogEntry,
EquipmentSpellState,
} from './types/game';
// ─── New: Memory Type Definition ─────────────────────────────────────────────
+1 -4
View File
@@ -3,7 +3,7 @@
import type { AttunementState } from './attunements';
import type { ElementState } from './elements';
import type { SpellState } from './spells';
import type { EquipmentInstance, EnchantmentDesign, DesignProgress, PreparationProgress, ApplicationProgress, EquipmentCraftingProgress, EquipmentDef, BlueprintDef, LootInventory, EquipmentSpellState } from './equipment';
import type { EquipmentInstance, EnchantmentDesign, DesignProgress, PreparationProgress, ApplicationProgress, EquipmentCraftingProgress, BlueprintDef, LootInventory, EquipmentSpellState } from './equipment';
// ─── Activity Log Types ─────────────────────────────────────────────────
export type ActivityEventType =
@@ -182,9 +182,6 @@ export interface GameState {
// Equipment spell states for multi-casting
equipmentSpellStates: EquipmentSpellState[];
// Legacy Equipment (for backward compatibility)
equipment: Record<string, EquipmentDef | null>;
inventory: EquipmentDef[];
// Blueprints
blueprints: Record<string, BlueprintDef>;
+1 -3
View File
@@ -29,9 +29,7 @@ export function computeMaxMana(
return base;
}
// computeElementMax is now in ../store.ts with support for unlockedManaTypeUpgrades
// This file no longer exports computeElementMax to avoid duplicate export issues
// Import computeElementMax from '../store' instead
// computeElementMax has been removed — element max is computed in manaStore.ts
export function computeRegen(
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'attunements'>,