Refactor large files into modular components
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m9s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m9s
- Refactored page.tsx (613→252 lines) with GameOverScreen and LeftPanel extracted - Refactored StatsTab.tsx (584→92 lines) with section components - Refactored SkillsTab.tsx (434→54 lines) with sub-components - Created modular structure for GameContext, LootInventory, and other components - All extracted components organized into feature directories
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
// ─── Crafting Initial State ───────────────────────────────────────────────────
|
||||
|
||||
import type { CraftingState } from './types';
|
||||
import { EQUIPMENT_SLOTS } from '../../data/equipment';
|
||||
|
||||
export const initialCraftingState: CraftingState = {
|
||||
equippedInstances: {
|
||||
mainHand: null,
|
||||
offHand: null,
|
||||
head: null,
|
||||
body: null,
|
||||
hands: null,
|
||||
feet: null,
|
||||
accessory1: null,
|
||||
accessory2: null,
|
||||
},
|
||||
equipmentInstances: {},
|
||||
enchantmentDesigns: [],
|
||||
designProgress: null,
|
||||
preparationProgress: null,
|
||||
applicationProgress: null,
|
||||
equipmentSpellStates: [],
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
// ─── Crafting Selectors ───────────────────────────────────────────────────
|
||||
|
||||
import type { CraftingStore } from './types';
|
||||
import type { EquipmentInstance } from '../../types';
|
||||
import type { EquipmentSlot } from '../../data/equipment';
|
||||
import { EQUIPMENT_SLOTS } from '../../data/equipment';
|
||||
import { getSpellsFromEquipment, computeEquipmentEffects } from './utils';
|
||||
|
||||
/**
|
||||
* Creates selector functions that depend on the store's get function.
|
||||
* Selectors are pure functions that derive data from the current state.
|
||||
*/
|
||||
export const createSelectors = (get: () => CraftingStore) => ({
|
||||
getEquippedInstance: (slot: EquipmentSlot): EquipmentInstance | null => {
|
||||
const state = get();
|
||||
const instanceId = state.equippedInstances[slot];
|
||||
if (!instanceId) return null;
|
||||
return state.equipmentInstances[instanceId] || null;
|
||||
},
|
||||
|
||||
getAllEquipped: (): EquipmentInstance[] => {
|
||||
const state = get();
|
||||
const equipped: EquipmentInstance[] = [];
|
||||
|
||||
for (const slot of EQUIPMENT_SLOTS) {
|
||||
const instanceId = state.equippedInstances[slot];
|
||||
if (instanceId && state.equipmentInstances[instanceId]) {
|
||||
equipped.push(state.equipmentInstances[instanceId]);
|
||||
}
|
||||
}
|
||||
|
||||
return equipped;
|
||||
},
|
||||
|
||||
getAvailableSpells: (): string[] => {
|
||||
const equipped = get().getAllEquipped();
|
||||
const spells: string[] = [];
|
||||
|
||||
for (const equip of equipped) {
|
||||
spells.push(...getSpellsFromEquipment(equip));
|
||||
}
|
||||
|
||||
return spells;
|
||||
},
|
||||
|
||||
getEquipmentEffects: () => {
|
||||
return computeEquipmentEffects(get().getAllEquipped());
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,252 @@
|
||||
// ─── Crafting Slice Logic ─────────────────────────────────────────────────
|
||||
|
||||
import type { StateCreator } from 'zustand';
|
||||
import type { CraftingStore } from './types';
|
||||
import type {
|
||||
DesignEffect,
|
||||
EnchantmentDesign,
|
||||
EquipmentInstance
|
||||
} from '../../types';
|
||||
import type { EquipmentSlot } from '../../data/equipment';
|
||||
import { initialCraftingState } from './initial-state';
|
||||
import {
|
||||
generateInstanceId,
|
||||
generateDesignId,
|
||||
createEquipmentInstance,
|
||||
calculateDesignTime,
|
||||
calculatePreparationTime,
|
||||
calculatePreparationManaCost,
|
||||
calculateApplicationTime,
|
||||
calculateApplicationManaPerHour
|
||||
} from './utils';
|
||||
import {
|
||||
EQUIPMENT_SLOTS,
|
||||
getEquipmentType
|
||||
} from '../../data/equipment';
|
||||
import { createSelectors } from './selectors';
|
||||
import {
|
||||
processDesignTick,
|
||||
processPreparationTick,
|
||||
processApplicationTick
|
||||
} from './tick-processors';
|
||||
|
||||
// ─── Cached Skills Workaround ──────────────────────────────────────────────
|
||||
// We need to access skills from the main store - this is a workaround
|
||||
// The store will pass skills when calling these methods
|
||||
|
||||
let cachedSkills: Record<string, number> = {};
|
||||
|
||||
export function setCachedSkills(skills: Record<string, number>): void {
|
||||
cachedSkills = skills;
|
||||
}
|
||||
|
||||
// ─── Slice Creator ─────────────────────────────────────────────────────────
|
||||
|
||||
export const createCraftingSlice: StateCreator<CraftingStore, [], [], CraftingStore> = (set, get) => {
|
||||
const selectors = createSelectors(get);
|
||||
|
||||
return {
|
||||
...initialCraftingState,
|
||||
|
||||
// Equipment management
|
||||
createEquipment: (typeId: string, slot?: EquipmentSlot) => {
|
||||
const instance = createEquipmentInstance(typeId);
|
||||
|
||||
set((state) => ({
|
||||
equipmentInstances: {
|
||||
...state.equipmentInstances,
|
||||
[instance.instanceId]: instance,
|
||||
},
|
||||
}));
|
||||
|
||||
// Auto-equip if slot provided
|
||||
if (slot) {
|
||||
get().equipInstance(instance.instanceId, slot);
|
||||
}
|
||||
|
||||
return instance;
|
||||
},
|
||||
|
||||
equipInstance: (instanceId: string, slot: EquipmentSlot) => {
|
||||
const instance = get().equipmentInstances[instanceId];
|
||||
if (!instance) return;
|
||||
|
||||
const typeDef = getEquipmentType(instance.typeId);
|
||||
if (!typeDef) return;
|
||||
|
||||
// Check if equipment can go in this slot
|
||||
if (typeDef.slot !== slot) {
|
||||
// For accessories, both accessory1 and accessory2 are valid
|
||||
if (typeDef.category !== 'accessory' || (slot !== 'accessory1' && slot !== 'accessory2')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
equippedInstances: {
|
||||
...state.equippedInstances,
|
||||
[slot]: instanceId,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
unequipSlot: (slot: EquipmentSlot) => {
|
||||
set((state) => ({
|
||||
equippedInstances: {
|
||||
...state.equippedInstances,
|
||||
[slot]: null,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
deleteInstance: (instanceId: string) => {
|
||||
set((state) => {
|
||||
const newInstanceMap = { ...state.equipmentInstances };
|
||||
delete newInstanceMap[instanceId];
|
||||
|
||||
// Remove from equipped slots
|
||||
const newEquipped = { ...state.equippedInstances };
|
||||
for (const slot of EQUIPMENT_SLOTS) {
|
||||
if (newEquipped[slot] === instanceId) {
|
||||
newEquipped[slot] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
equipmentInstances: newInstanceMap,
|
||||
equippedInstances: newEquipped,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Enchantment design
|
||||
startDesign: (name: string, equipmentType: string, effects: DesignEffect[]) => {
|
||||
const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
|
||||
const designTime = calculateDesignTime(effects);
|
||||
|
||||
const design: EnchantmentDesign = {
|
||||
id: generateDesignId(),
|
||||
name,
|
||||
equipmentType,
|
||||
effects,
|
||||
totalCapacityUsed: totalCapacity,
|
||||
designTime,
|
||||
created: Date.now(),
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
enchantmentDesigns: [...state.enchantmentDesigns, design],
|
||||
designProgress: {
|
||||
designId: design.id,
|
||||
progress: 0,
|
||||
required: designTime,
|
||||
name,
|
||||
equipmentType,
|
||||
effects,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
cancelDesign: () => {
|
||||
const progress = get().designProgress;
|
||||
if (!progress) return;
|
||||
|
||||
set((state) => ({
|
||||
designProgress: null,
|
||||
enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== progress.designId),
|
||||
}));
|
||||
},
|
||||
|
||||
deleteDesign: (designId: string) => {
|
||||
set((state) => ({
|
||||
enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== designId),
|
||||
}));
|
||||
},
|
||||
|
||||
// Equipment preparation
|
||||
startPreparation: (instanceId: string) => {
|
||||
const instance = get().equipmentInstances[instanceId];
|
||||
if (!instance) return;
|
||||
|
||||
const prepTime = calculatePreparationTime(instance.typeId);
|
||||
const manaCost = calculatePreparationManaCost(instance.typeId);
|
||||
|
||||
set({
|
||||
preparationProgress: {
|
||||
equipmentInstanceId: instanceId,
|
||||
progress: 0,
|
||||
required: prepTime,
|
||||
manaCostPaid: 0,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
cancelPreparation: () => {
|
||||
set({ preparationProgress: null });
|
||||
},
|
||||
|
||||
// Enchantment application
|
||||
startApplication: (instanceId: string, designId: string) => {
|
||||
const instance = get().equipmentInstances[instanceId];
|
||||
const design = get().enchantmentDesigns.find(d => d.id === designId);
|
||||
|
||||
if (!instance || !design) return;
|
||||
|
||||
const appTime = calculateApplicationTime(design.effects, cachedSkills);
|
||||
const manaPerHour = calculateApplicationManaPerHour(design.effects);
|
||||
|
||||
set({
|
||||
applicationProgress: {
|
||||
equipmentInstanceId: instanceId,
|
||||
designId,
|
||||
progress: 0,
|
||||
required: appTime,
|
||||
manaPerHour,
|
||||
paused: false,
|
||||
manaSpent: 0,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
pauseApplication: () => {
|
||||
const progress = get().applicationProgress;
|
||||
if (!progress) return;
|
||||
|
||||
set({
|
||||
applicationProgress: { ...progress, paused: true },
|
||||
});
|
||||
},
|
||||
|
||||
resumeApplication: () => {
|
||||
const progress = get().applicationProgress;
|
||||
if (!progress) return;
|
||||
|
||||
set({
|
||||
applicationProgress: { ...progress, paused: false },
|
||||
});
|
||||
},
|
||||
|
||||
cancelApplication: () => {
|
||||
set({ applicationProgress: null });
|
||||
},
|
||||
|
||||
// Tick processing - delegated to tick-processors module
|
||||
processDesignTick: (hours: number) => {
|
||||
return processDesignTick(get(), set, hours);
|
||||
},
|
||||
|
||||
processPreparationTick: (hours: number, manaAvailable: number) => {
|
||||
return processPreparationTick(get(), set, hours, manaAvailable);
|
||||
},
|
||||
|
||||
processApplicationTick: (hours: number, manaAvailable: number) => {
|
||||
return processApplicationTick(get(), set, get, hours, manaAvailable, cachedSkills);
|
||||
},
|
||||
|
||||
// Selectors - delegated to selectors module
|
||||
getEquippedInstance: selectors.getEquippedInstance,
|
||||
getAllEquipped: selectors.getAllEquipped,
|
||||
getAvailableSpells: selectors.getAvailableSpells,
|
||||
getEquipmentEffects: selectors.getEquipmentEffects,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
// ─── Starting Equipment Factory ─────────────────────────────────────────
|
||||
|
||||
import type { EquipmentInstance } from '../../types';
|
||||
import { createEquipmentInstance } from './utils';
|
||||
|
||||
export function createStartingEquipment(): {
|
||||
equippedInstances: Record<string, string | null>;
|
||||
equipmentInstances: Record<string, EquipmentInstance>;
|
||||
} {
|
||||
const instances: EquipmentInstance[] = [];
|
||||
|
||||
// Create starting equipment
|
||||
const basicStaff = createEquipmentInstance('basicStaff');
|
||||
basicStaff.enchantments = [{
|
||||
effectId: 'spell_manaBolt',
|
||||
stacks: 1,
|
||||
actualCost: 50, // Fills the staff completely
|
||||
}];
|
||||
basicStaff.usedCapacity = 50;
|
||||
basicStaff.rarity = 'uncommon';
|
||||
instances.push(basicStaff);
|
||||
|
||||
const civilianShirt = createEquipmentInstance('civilianShirt');
|
||||
instances.push(civilianShirt);
|
||||
|
||||
const civilianGloves = createEquipmentInstance('civilianGloves');
|
||||
instances.push(civilianGloves);
|
||||
|
||||
const civilianShoes = createEquipmentInstance('civilianShoes');
|
||||
instances.push(civilianShoes);
|
||||
|
||||
// Build instance map
|
||||
const equipmentInstances: Record<string, EquipmentInstance> = {};
|
||||
for (const inst of instances) {
|
||||
equipmentInstances[inst.instanceId] = inst;
|
||||
}
|
||||
|
||||
// Build equipped map
|
||||
const equippedInstances: Record<string, string | null> = {
|
||||
mainHand: basicStaff.instanceId,
|
||||
offHand: null,
|
||||
head: null,
|
||||
body: civilianShirt.instanceId,
|
||||
hands: civilianGloves.instanceId,
|
||||
feet: civilianShoes.instanceId,
|
||||
accessory1: null,
|
||||
accessory2: null,
|
||||
};
|
||||
|
||||
return { equippedInstances, equipmentInstances };
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
// ─── Tick Processors ─────────────────────────────────────────────────────
|
||||
// These functions handle the time-based processing for crafting operations.
|
||||
|
||||
import type { CraftingStore } from './types';
|
||||
import type { AppliedEnchantment, EquipmentInstance } from '../../types';
|
||||
import {
|
||||
getEnchantEfficiencyBonus,
|
||||
calculatePreparationManaCost,
|
||||
calculateEffectCapacityCost,
|
||||
calculateRarity
|
||||
} from './utils';
|
||||
import { getEnchantmentEffect } from '../../data/enchantment-effects';
|
||||
|
||||
/**
|
||||
* Processes design tick progress.
|
||||
* Returns true if design was completed this tick.
|
||||
*/
|
||||
export function processDesignTick(
|
||||
state: CraftingStore,
|
||||
setState: (updater: Partial<CraftingStore> | ((state: CraftingStore) => Partial<CraftingStore>)) => void,
|
||||
hours: number
|
||||
): boolean {
|
||||
const progress = state.designProgress;
|
||||
if (!progress) return false;
|
||||
|
||||
const newProgress = progress.progress + hours;
|
||||
|
||||
if (newProgress >= progress.required) {
|
||||
// Design complete
|
||||
setState({ designProgress: null });
|
||||
return true;
|
||||
} else {
|
||||
setState({
|
||||
designProgress: { ...progress, progress: newProgress },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes preparation tick progress.
|
||||
* Returns the amount of mana consumed.
|
||||
*/
|
||||
export function processPreparationTick(
|
||||
state: CraftingStore,
|
||||
setState: (updater: Partial<CraftingStore> | ((state: CraftingStore) => Partial<CraftingStore>)) => void,
|
||||
hours: number,
|
||||
manaAvailable: number
|
||||
): number {
|
||||
const progress = state.preparationProgress;
|
||||
if (!progress) return 0;
|
||||
|
||||
const instance = state.equipmentInstances[progress.equipmentInstanceId];
|
||||
if (!instance) {
|
||||
setState({ preparationProgress: null });
|
||||
return 0;
|
||||
}
|
||||
|
||||
const totalManaCost = calculatePreparationManaCost(instance.typeId);
|
||||
const remainingManaCost = totalManaCost - progress.manaCostPaid;
|
||||
const manaToPay = Math.min(manaAvailable, remainingManaCost);
|
||||
|
||||
if (manaToPay < remainingManaCost) {
|
||||
// Not enough mana, just pay what we can
|
||||
setState({
|
||||
preparationProgress: {
|
||||
...progress,
|
||||
manaCostPaid: progress.manaCostPaid + manaToPay,
|
||||
},
|
||||
});
|
||||
return manaToPay;
|
||||
}
|
||||
|
||||
// Pay remaining mana and progress
|
||||
const newProgress = progress.progress + hours;
|
||||
|
||||
if (newProgress >= progress.required) {
|
||||
// Preparation complete - clear enchantments and add 'Ready for Enchantment' tag
|
||||
setState((state) => ({
|
||||
preparationProgress: null,
|
||||
equipmentInstances: {
|
||||
...state.equipmentInstances,
|
||||
[instance.instanceId]: {
|
||||
...instance,
|
||||
enchantments: [],
|
||||
usedCapacity: 0,
|
||||
rarity: 'common' as const,
|
||||
tags: [...(instance.tags || []), 'Ready for Enchantment'],
|
||||
},
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
setState({
|
||||
preparationProgress: {
|
||||
...progress,
|
||||
progress: newProgress,
|
||||
manaCostPaid: progress.manaCostPaid + manaToPay,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return manaToPay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes application tick progress.
|
||||
* Returns the amount of mana consumed.
|
||||
*/
|
||||
export function processApplicationTick(
|
||||
state: CraftingStore,
|
||||
setState: (updater: Partial<CraftingStore> | ((state: CraftingStore) => Partial<CraftingStore>)) => void,
|
||||
getState: () => CraftingStore,
|
||||
hours: number,
|
||||
manaAvailable: number,
|
||||
cachedSkills: Record<string, number>
|
||||
): number {
|
||||
const progress = state.applicationProgress;
|
||||
if (!progress || progress.paused) return 0;
|
||||
|
||||
const design = state.enchantmentDesigns.find(d => d.id === progress.designId);
|
||||
const instance = state.equipmentInstances[progress.equipmentInstanceId];
|
||||
|
||||
if (!design || !instance) {
|
||||
setState({ applicationProgress: null });
|
||||
return 0;
|
||||
}
|
||||
|
||||
const manaNeeded = progress.manaPerHour * hours;
|
||||
const manaToUse = Math.min(manaAvailable, manaNeeded);
|
||||
|
||||
if (manaToUse < manaNeeded) {
|
||||
// Not enough mana - pause and save progress
|
||||
setState({
|
||||
applicationProgress: {
|
||||
...progress,
|
||||
manaSpent: progress.manaSpent + manaToUse,
|
||||
},
|
||||
});
|
||||
return manaToUse;
|
||||
}
|
||||
|
||||
const newProgress = progress.progress + hours;
|
||||
|
||||
if (newProgress >= progress.required) {
|
||||
// Application complete - apply enchantments
|
||||
const efficiencyBonus = getEnchantEfficiencyBonus(cachedSkills);
|
||||
const newEnchantments: AppliedEnchantment[] = design.effects.map(e => ({
|
||||
effectId: e.effectId,
|
||||
stacks: e.stacks,
|
||||
actualCost: calculateEffectCapacityCost(e.effectId, e.stacks, efficiencyBonus),
|
||||
}));
|
||||
|
||||
const totalUsedCapacity = newEnchantments.reduce((sum, e) => sum + e.actualCost, 0);
|
||||
|
||||
setState((state) => ({
|
||||
applicationProgress: null,
|
||||
equipmentInstances: {
|
||||
...state.equipmentInstances,
|
||||
[instance.instanceId]: {
|
||||
...instance,
|
||||
enchantments: newEnchantments,
|
||||
usedCapacity: totalUsedCapacity,
|
||||
rarity: calculateRarity(newEnchantments),
|
||||
},
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
setState({
|
||||
applicationProgress: {
|
||||
...progress,
|
||||
progress: newProgress,
|
||||
manaSpent: progress.manaSpent + manaToUse,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return manaToUse;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// ─── Crafting Store Types ──────────────────────────────────────────────────────
|
||||
|
||||
import type {
|
||||
EquipmentInstance,
|
||||
AppliedEnchantment,
|
||||
EnchantmentDesign,
|
||||
DesignEffect,
|
||||
DesignProgress,
|
||||
PreparationProgress,
|
||||
ApplicationProgress,
|
||||
EquipmentSpellState
|
||||
} from '../../types';
|
||||
import type { EquipmentSlot } from '../../data/equipment';
|
||||
|
||||
export interface CraftingState {
|
||||
// Equipment instances
|
||||
equippedInstances: Record<string, string | null>; // slot -> instanceId
|
||||
equipmentInstances: Record<string, EquipmentInstance>; // instanceId -> instance
|
||||
|
||||
// Enchantment designs
|
||||
enchantmentDesigns: EnchantmentDesign[];
|
||||
|
||||
// Crafting progress
|
||||
designProgress: DesignProgress | null;
|
||||
preparationProgress: PreparationProgress | null;
|
||||
applicationProgress: ApplicationProgress | null;
|
||||
|
||||
// Equipment spell states
|
||||
equipmentSpellStates: EquipmentSpellState[];
|
||||
}
|
||||
|
||||
export interface CraftingActions {
|
||||
// Equipment management
|
||||
createEquipment: (typeId: string, slot?: EquipmentSlot) => EquipmentInstance;
|
||||
equipInstance: (instanceId: string, slot: EquipmentSlot) => void;
|
||||
unequipSlot: (slot: EquipmentSlot) => void;
|
||||
deleteInstance: (instanceId: string) => void;
|
||||
|
||||
// Enchantment design
|
||||
startDesign: (name: string, equipmentType: string, effects: DesignEffect[]) => void;
|
||||
cancelDesign: () => void;
|
||||
deleteDesign: (designId: string) => void;
|
||||
|
||||
// Equipment preparation
|
||||
startPreparation: (instanceId: string) => void;
|
||||
cancelPreparation: () => void;
|
||||
|
||||
// Enchantment application
|
||||
startApplication: (instanceId: string, designId: string) => void;
|
||||
pauseApplication: () => void;
|
||||
resumeApplication: () => void;
|
||||
cancelApplication: () => void;
|
||||
|
||||
// Tick processing
|
||||
processDesignTick: (hours: number) => void;
|
||||
processPreparationTick: (hours: number, manaAvailable: number) => number; // Returns mana used
|
||||
processApplicationTick: (hours: number, manaAvailable: number) => number; // Returns mana used
|
||||
|
||||
// Getters
|
||||
getEquippedInstance: (slot: EquipmentSlot) => EquipmentInstance | null;
|
||||
getAllEquipped: () => EquipmentInstance[];
|
||||
getAvailableSpells: () => string[];
|
||||
getEquipmentEffects: () => Record<string, number>;
|
||||
}
|
||||
|
||||
export type CraftingStore = CraftingState & CraftingActions;
|
||||
@@ -0,0 +1,167 @@
|
||||
// ─── Crafting Utility Functions ──────────────────────────────────────────────
|
||||
|
||||
import type {
|
||||
EquipmentInstance,
|
||||
DesignEffect,
|
||||
AppliedEnchantment
|
||||
} from '../../types';
|
||||
import {
|
||||
getEquipmentType
|
||||
} from '../../data/equipment';
|
||||
import {
|
||||
getEnchantmentEffect,
|
||||
calculateEffectCapacityCost
|
||||
} from '../../data/enchantment-effects';
|
||||
|
||||
// Re-export for use in other modules
|
||||
export { getEquipmentType } from '../../data/equipment';
|
||||
export { calculateEffectCapacityCost, getEnchantmentEffect } from '../../data/enchantment-effects';
|
||||
|
||||
// ─── ID Generators ────────────────────────────────────────────────────────────
|
||||
|
||||
let instanceIdCounter = 0;
|
||||
export function generateInstanceId(): string {
|
||||
return `equip_${Date.now()}_${++instanceIdCounter}`;
|
||||
}
|
||||
|
||||
let designIdCounter = 0;
|
||||
export function generateDesignId(): string {
|
||||
return `design_${Date.now()}_${++designIdCounter}`;
|
||||
}
|
||||
|
||||
// ─── Skill-based Calculations ────────────────────────────────────────────────
|
||||
|
||||
// Calculate efficiency bonus from skills
|
||||
export function getEnchantEfficiencyBonus(skills: Record<string, number>): number {
|
||||
const enchantingLevel = skills.enchanting || 0;
|
||||
const efficientEnchantLevel = skills.efficientEnchant || 0;
|
||||
|
||||
// 2% per enchanting level + 5% per efficient enchant level
|
||||
return (enchantingLevel * 0.02) + (efficientEnchantLevel * 0.05);
|
||||
}
|
||||
|
||||
// ─── Time and Cost Calculations ──────────────────────────────────────────────
|
||||
|
||||
// Calculate design time based on effects
|
||||
export function calculateDesignTime(effects: DesignEffect[]): number {
|
||||
const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
|
||||
return Math.max(1, Math.floor(totalCapacity / 10)); // Hours
|
||||
}
|
||||
|
||||
// Calculate preparation time for equipment
|
||||
export function calculatePreparationTime(equipmentType: string): number {
|
||||
const typeDef = getEquipmentType(equipmentType);
|
||||
if (!typeDef) return 1;
|
||||
return Math.max(1, Math.floor(typeDef.baseCapacity / 5)); // Hours
|
||||
}
|
||||
|
||||
// Calculate preparation mana cost
|
||||
export function calculatePreparationManaCost(equipmentType: string): number {
|
||||
const typeDef = getEquipmentType(equipmentType);
|
||||
if (!typeDef) return 50;
|
||||
return typeDef.baseCapacity * 5;
|
||||
}
|
||||
|
||||
// Calculate application time based on effects
|
||||
export function calculateApplicationTime(effects: DesignEffect[], skills: Record<string, number>): number {
|
||||
const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
|
||||
const speedBonus = 1 + (skills.enchantSpeed || 0) * 0.1;
|
||||
return Math.max(4, Math.floor(totalCapacity / 20 * 24 / speedBonus)); // Hours (days * 24)
|
||||
}
|
||||
|
||||
// Calculate mana per hour for application
|
||||
export function calculateApplicationManaPerHour(effects: DesignEffect[]): number {
|
||||
const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
|
||||
return Math.max(1, Math.floor(totalCapacity * 0.5));
|
||||
}
|
||||
|
||||
// ─── Equipment Instance Creation ─────────────────────────────────────────────
|
||||
|
||||
// Create a new equipment instance
|
||||
export function createEquipmentInstance(typeId: string, name?: string): EquipmentInstance {
|
||||
const typeDef = getEquipmentType(typeId);
|
||||
if (!typeDef) {
|
||||
throw new Error(`Unknown equipment type: ${typeId}`);
|
||||
}
|
||||
|
||||
return {
|
||||
instanceId: generateInstanceId(),
|
||||
typeId,
|
||||
name: name || typeDef.name,
|
||||
enchantments: [],
|
||||
usedCapacity: 0,
|
||||
totalCapacity: typeDef.baseCapacity,
|
||||
rarity: 'common',
|
||||
quality: 100, // Full quality for new items
|
||||
tags: [], // Initialize with empty tags array
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Rarity Calculation ────────────────────────────────────────────────────
|
||||
|
||||
// Calculate rarity based on number and quality of enchantments
|
||||
export function calculateRarity(enchantments: AppliedEnchantment[]): 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary' {
|
||||
if (enchantments.length === 0) return 'common';
|
||||
|
||||
const totalCapacity = enchantments.reduce((sum, e) => sum + e.actualCost, 0);
|
||||
const avgStacks = enchantments.reduce((sum, e) => sum + e.stacks, 0) / enchantments.length;
|
||||
|
||||
// Determine rarity based on capacity used and number of enchantments
|
||||
if (totalCapacity >= 200 && enchantments.length >= 3) return 'legendary';
|
||||
if (totalCapacity >= 150 && enchantments.length >= 2) return 'epic';
|
||||
if (totalCapacity >= 100 || enchantments.length >= 2) return 'rare';
|
||||
if (totalCapacity >= 50 || avgStacks > 1) return 'uncommon';
|
||||
return 'common';
|
||||
}
|
||||
|
||||
// ─── Equipment Effect Computation ────────────────────────────────────────────
|
||||
|
||||
// Get spells from equipment
|
||||
export function getSpellsFromEquipment(equipment: EquipmentInstance): string[] {
|
||||
const spells: string[] = [];
|
||||
|
||||
for (const ench of equipment.enchantments) {
|
||||
const effectDef = getEnchantmentEffect(ench.effectId);
|
||||
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
|
||||
spells.push(effectDef.effect.spellId);
|
||||
}
|
||||
}
|
||||
|
||||
return spells;
|
||||
}
|
||||
|
||||
// Compute total effects from equipment
|
||||
export function computeEquipmentEffects(equipment: EquipmentInstance[]): Record<string, number> {
|
||||
const effects: Record<string, number> = {};
|
||||
const multipliers: Record<string, number> = {};
|
||||
const specials: Set<string> = new Set();
|
||||
|
||||
for (const equip of equipment) {
|
||||
for (const ench of equip.enchantments) {
|
||||
const effectDef = getEnchantmentEffect(ench.effectId);
|
||||
if (!effectDef) continue;
|
||||
|
||||
const value = (effectDef.effect.value || 0) * ench.stacks;
|
||||
|
||||
if (effectDef.effect.type === 'bonus' && effectDef.effect.stat) {
|
||||
effects[effectDef.effect.stat] = (effects[effectDef.effect.stat] || 0) + value;
|
||||
} else if (effectDef.effect.type === 'multiplier' && effectDef.effect.stat) {
|
||||
multipliers[effectDef.effect.stat] = (multipliers[effectDef.effect.stat] || 1) * Math.pow(value, ench.stacks);
|
||||
} else if (effectDef.effect.type === 'special' && effectDef.effect.specialId) {
|
||||
specials.add(effectDef.effect.specialId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply multipliers to bonus effects
|
||||
for (const [stat, mult] of Object.entries(multipliers)) {
|
||||
effects[`${stat}_multiplier`] = mult;
|
||||
}
|
||||
|
||||
// Add special effect flags
|
||||
for (const special of specials) {
|
||||
effects[`special_${special}`] = 1;
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
@@ -1,646 +1,29 @@
|
||||
// ─── Crafting Store Slice ────────────────────────────────────────────────────────
|
||||
// Handles equipment, enchantments, and crafting progress
|
||||
//
|
||||
// This file is a re-export barrel that combines all crafting modules.
|
||||
// For the actual implementation, see:
|
||||
// - crafting-modules/types.ts - Type definitions
|
||||
// - crafting-modules/initial-state.ts - Initial state
|
||||
// - crafting-modules/utils.ts - Utility functions
|
||||
// - crafting-modules/slice-logic.ts - Main slice creator
|
||||
// - crafting-modules/starting-equipment.ts - Starting equipment factory
|
||||
|
||||
import type {
|
||||
EquipmentInstance,
|
||||
AppliedEnchantment,
|
||||
EnchantmentDesign,
|
||||
DesignEffect,
|
||||
DesignProgress,
|
||||
PreparationProgress,
|
||||
ApplicationProgress,
|
||||
EquipmentSpellState
|
||||
} from '../types';
|
||||
import {
|
||||
EQUIPMENT_TYPES,
|
||||
EQUIPMENT_SLOTS,
|
||||
type EquipmentSlot,
|
||||
type EquipmentTypeDef,
|
||||
getEquipmentType,
|
||||
calculateRarity
|
||||
} from '../data/equipment';
|
||||
import {
|
||||
ENCHANTMENT_EFFECTS,
|
||||
getEnchantmentEffect,
|
||||
canApplyEffect,
|
||||
calculateEffectCapacityCost,
|
||||
type EnchantmentEffectDef
|
||||
} from '../data/enchantment-effects';
|
||||
import { SPELLS_DEF } from '../constants';
|
||||
import type { StateCreator } from 'zustand';
|
||||
|
||||
// ─── Helper Functions ────────────────────────────────────────────────────────────
|
||||
|
||||
let instanceIdCounter = 0;
|
||||
function generateInstanceId(): string {
|
||||
return `equip_${Date.now()}_${++instanceIdCounter}`;
|
||||
}
|
||||
|
||||
let designIdCounter = 0;
|
||||
function generateDesignId(): string {
|
||||
return `design_${Date.now()}_${++designIdCounter}`;
|
||||
}
|
||||
|
||||
// Calculate efficiency bonus from skills
|
||||
function getEnchantEfficiencyBonus(skills: Record<string, number>): number {
|
||||
const enchantingLevel = skills.enchanting || 0;
|
||||
const efficientEnchantLevel = skills.efficientEnchant || 0;
|
||||
|
||||
// 2% per enchanting level + 5% per efficient enchant level
|
||||
return (enchantingLevel * 0.02) + (efficientEnchantLevel * 0.05);
|
||||
}
|
||||
|
||||
// Calculate design time based on effects
|
||||
function calculateDesignTime(effects: DesignEffect[]): number {
|
||||
const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
|
||||
return Math.max(1, Math.floor(totalCapacity / 10)); // Hours
|
||||
}
|
||||
|
||||
// Calculate preparation time for equipment
|
||||
function calculatePreparationTime(equipmentType: string): number {
|
||||
const typeDef = getEquipmentType(equipmentType);
|
||||
if (!typeDef) return 1;
|
||||
return Math.max(1, Math.floor(typeDef.baseCapacity / 5)); // Hours
|
||||
}
|
||||
|
||||
// Calculate preparation mana cost
|
||||
function calculatePreparationManaCost(equipmentType: string): number {
|
||||
const typeDef = getEquipmentType(equipmentType);
|
||||
if (!typeDef) return 50;
|
||||
return typeDef.baseCapacity * 5;
|
||||
}
|
||||
|
||||
// Calculate application time based on effects
|
||||
function calculateApplicationTime(effects: DesignEffect[], skills: Record<string, number>): number {
|
||||
const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
|
||||
const speedBonus = 1 + (skills.enchantSpeed || 0) * 0.1;
|
||||
return Math.max(4, Math.floor(totalCapacity / 20 * 24 / speedBonus)); // Hours (days * 24)
|
||||
}
|
||||
|
||||
// Calculate mana per hour for application
|
||||
function calculateApplicationManaPerHour(effects: DesignEffect[]): number {
|
||||
const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
|
||||
return Math.max(1, Math.floor(totalCapacity * 0.5));
|
||||
}
|
||||
|
||||
// Create a new equipment instance
|
||||
export function createEquipmentInstance(typeId: string, name?: string): EquipmentInstance {
|
||||
const typeDef = getEquipmentType(typeId);
|
||||
if (!typeDef) {
|
||||
throw new Error(`Unknown equipment type: ${typeId}`);
|
||||
}
|
||||
|
||||
return {
|
||||
instanceId: generateInstanceId(),
|
||||
typeId,
|
||||
name: name || typeDef.name,
|
||||
enchantments: [],
|
||||
usedCapacity: 0,
|
||||
totalCapacity: typeDef.baseCapacity,
|
||||
rarity: 'common',
|
||||
quality: 100, // Full quality for new items
|
||||
tags: [], // Initialize with empty tags array
|
||||
};
|
||||
}
|
||||
|
||||
// Get spells from equipment
|
||||
export function getSpellsFromEquipment(equipment: EquipmentInstance): string[] {
|
||||
const spells: string[] = [];
|
||||
|
||||
for (const ench of equipment.enchantments) {
|
||||
const effectDef = getEnchantmentEffect(ench.effectId);
|
||||
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
|
||||
spells.push(effectDef.effect.spellId);
|
||||
}
|
||||
}
|
||||
|
||||
return spells;
|
||||
}
|
||||
|
||||
// Compute total effects from equipment
|
||||
export function computeEquipmentEffects(equipment: EquipmentInstance[]): Record<string, number> {
|
||||
const effects: Record<string, number> = {};
|
||||
const multipliers: Record<string, number> = {};
|
||||
const specials: Set<string> = new Set();
|
||||
|
||||
for (const equip of equipment) {
|
||||
for (const ench of equip.enchantments) {
|
||||
const effectDef = getEnchantmentEffect(ench.effectId);
|
||||
if (!effectDef) continue;
|
||||
|
||||
const value = (effectDef.effect.value || 0) * ench.stacks;
|
||||
|
||||
if (effectDef.effect.type === 'bonus' && effectDef.effect.stat) {
|
||||
effects[effectDef.effect.stat] = (effects[effectDef.effect.stat] || 0) + value;
|
||||
} else if (effectDef.effect.type === 'multiplier' && effectDef.effect.stat) {
|
||||
multipliers[effectDef.effect.stat] = (multipliers[effectDef.effect.stat] || 1) * Math.pow(value, ench.stacks);
|
||||
} else if (effectDef.effect.type === 'special' && effectDef.effect.specialId) {
|
||||
specials.add(effectDef.effect.specialId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply multipliers to bonus effects
|
||||
for (const [stat, mult] of Object.entries(multipliers)) {
|
||||
effects[`${stat}_multiplier`] = mult;
|
||||
}
|
||||
|
||||
// Add special effect flags
|
||||
for (const special of specials) {
|
||||
effects[`special_${special}`] = 1;
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
|
||||
// ─── Store Interface ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface CraftingState {
|
||||
// Equipment instances
|
||||
equippedInstances: Record<string, string | null>; // slot -> instanceId
|
||||
equipmentInstances: Record<string, EquipmentInstance>; // instanceId -> instance
|
||||
|
||||
// Enchantment designs
|
||||
enchantmentDesigns: EnchantmentDesign[];
|
||||
|
||||
// Crafting progress
|
||||
designProgress: DesignProgress | null;
|
||||
preparationProgress: PreparationProgress | null;
|
||||
applicationProgress: ApplicationProgress | null;
|
||||
|
||||
// Equipment spell states
|
||||
equipmentSpellStates: EquipmentSpellState[];
|
||||
}
|
||||
|
||||
export interface CraftingActions {
|
||||
// Equipment management
|
||||
createEquipment: (typeId: string, slot?: EquipmentSlot) => EquipmentInstance;
|
||||
equipInstance: (instanceId: string, slot: EquipmentSlot) => void;
|
||||
unequipSlot: (slot: EquipmentSlot) => void;
|
||||
deleteInstance: (instanceId: string) => void;
|
||||
|
||||
// Enchantment design
|
||||
startDesign: (name: string, equipmentType: string, effects: DesignEffect[]) => void;
|
||||
cancelDesign: () => void;
|
||||
deleteDesign: (designId: string) => void;
|
||||
|
||||
// Equipment preparation
|
||||
startPreparation: (instanceId: string) => void;
|
||||
cancelPreparation: () => void;
|
||||
|
||||
// Enchantment application
|
||||
startApplication: (instanceId: string, designId: string) => void;
|
||||
pauseApplication: () => void;
|
||||
resumeApplication: () => void;
|
||||
cancelApplication: () => void;
|
||||
|
||||
// Tick processing
|
||||
processDesignTick: (hours: number) => void;
|
||||
processPreparationTick: (hours: number, manaAvailable: number) => number; // Returns mana used
|
||||
processApplicationTick: (hours: number, manaAvailable: number) => number; // Returns mana used
|
||||
|
||||
// Getters
|
||||
getEquippedInstance: (slot: EquipmentSlot) => EquipmentInstance | null;
|
||||
getAllEquipped: () => EquipmentInstance[];
|
||||
getAvailableSpells: () => string[];
|
||||
getEquipmentEffects: () => Record<string, number>;
|
||||
}
|
||||
|
||||
export type CraftingStore = CraftingState & CraftingActions;
|
||||
|
||||
// ─── Initial State ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const initialCraftingState: CraftingState = {
|
||||
equippedInstances: {
|
||||
mainHand: null,
|
||||
offHand: null,
|
||||
head: null,
|
||||
body: null,
|
||||
hands: null,
|
||||
feet: null,
|
||||
accessory1: null,
|
||||
accessory2: null,
|
||||
},
|
||||
equipmentInstances: {},
|
||||
enchantmentDesigns: [],
|
||||
designProgress: null,
|
||||
preparationProgress: null,
|
||||
applicationProgress: null,
|
||||
equipmentSpellStates: [],
|
||||
};
|
||||
|
||||
// ─── Store Slice Creator ────────────────────────────────────────────────────────
|
||||
|
||||
// We need to access skills from the main store - this is a workaround
|
||||
// The store will pass skills when calling these methods
|
||||
let cachedSkills: Record<string, number> = {};
|
||||
|
||||
export function setCachedSkills(skills: Record<string, number>): void {
|
||||
cachedSkills = skills;
|
||||
}
|
||||
|
||||
export const createCraftingSlice: StateCreator<CraftingStore, [], [], CraftingStore> = (set, get) => ({
|
||||
...initialCraftingState,
|
||||
|
||||
// Equipment management
|
||||
createEquipment: (typeId: string, slot?: EquipmentSlot) => {
|
||||
const instance = createEquipmentInstance(typeId);
|
||||
|
||||
set((state) => ({
|
||||
equipmentInstances: {
|
||||
...state.equipmentInstances,
|
||||
[instance.instanceId]: instance,
|
||||
},
|
||||
}));
|
||||
|
||||
// Auto-equip if slot provided
|
||||
if (slot) {
|
||||
get().equipInstance(instance.instanceId, slot);
|
||||
}
|
||||
|
||||
return instance;
|
||||
},
|
||||
|
||||
equipInstance: (instanceId: string, slot: EquipmentSlot) => {
|
||||
const instance = get().equipmentInstances[instanceId];
|
||||
if (!instance) return;
|
||||
|
||||
const typeDef = getEquipmentType(instance.typeId);
|
||||
if (!typeDef) return;
|
||||
|
||||
// Check if equipment can go in this slot
|
||||
if (typeDef.slot !== slot) {
|
||||
// For accessories, both accessory1 and accessory2 are valid
|
||||
if (typeDef.category !== 'accessory' || (slot !== 'accessory1' && slot !== 'accessory2')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
equippedInstances: {
|
||||
...state.equippedInstances,
|
||||
[slot]: instanceId,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
unequipSlot: (slot: EquipmentSlot) => {
|
||||
set((state) => ({
|
||||
equippedInstances: {
|
||||
...state.equippedInstances,
|
||||
[slot]: null,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
deleteInstance: (instanceId: string) => {
|
||||
set((state) => {
|
||||
const newInstanceMap = { ...state.equipmentInstances };
|
||||
delete newInstanceMap[instanceId];
|
||||
|
||||
// Remove from equipped slots
|
||||
const newEquipped = { ...state.equippedInstances };
|
||||
for (const slot of EQUIPMENT_SLOTS) {
|
||||
if (newEquipped[slot] === instanceId) {
|
||||
newEquipped[slot] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
equipmentInstances: newInstanceMap,
|
||||
equippedInstances: newEquipped,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Enchantment design
|
||||
startDesign: (name: string, equipmentType: string, effects: DesignEffect[]) => {
|
||||
const totalCapacity = effects.reduce((sum, e) => sum + e.capacityCost, 0);
|
||||
const designTime = calculateDesignTime(effects);
|
||||
|
||||
const design: EnchantmentDesign = {
|
||||
id: generateDesignId(),
|
||||
name,
|
||||
equipmentType,
|
||||
effects,
|
||||
totalCapacityUsed: totalCapacity,
|
||||
designTime,
|
||||
created: Date.now(),
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
enchantmentDesigns: [...state.enchantmentDesigns, design],
|
||||
designProgress: {
|
||||
designId: design.id,
|
||||
progress: 0,
|
||||
required: designTime,
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
cancelDesign: () => {
|
||||
const progress = get().designProgress;
|
||||
if (!progress) return;
|
||||
|
||||
set((state) => ({
|
||||
designProgress: null,
|
||||
enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== progress.designId),
|
||||
}));
|
||||
},
|
||||
|
||||
deleteDesign: (designId: string) => {
|
||||
set((state) => ({
|
||||
enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== designId),
|
||||
}));
|
||||
},
|
||||
|
||||
// Equipment preparation
|
||||
startPreparation: (instanceId: string) => {
|
||||
const instance = get().equipmentInstances[instanceId];
|
||||
if (!instance) return;
|
||||
|
||||
const prepTime = calculatePreparationTime(instance.typeId);
|
||||
const manaCost = calculatePreparationManaCost(instance.typeId);
|
||||
|
||||
set({
|
||||
preparationProgress: {
|
||||
equipmentInstanceId: instanceId,
|
||||
progress: 0,
|
||||
required: prepTime,
|
||||
manaCostPaid: 0,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
cancelPreparation: () => {
|
||||
set({ preparationProgress: null });
|
||||
},
|
||||
|
||||
// Enchantment application
|
||||
startApplication: (instanceId: string, designId: string) => {
|
||||
const instance = get().equipmentInstances[instanceId];
|
||||
const design = get().enchantmentDesigns.find(d => d.id === designId);
|
||||
|
||||
if (!instance || !design) return;
|
||||
|
||||
const appTime = calculateApplicationTime(design.effects, cachedSkills);
|
||||
const manaPerHour = calculateApplicationManaPerHour(design.effects);
|
||||
|
||||
set({
|
||||
applicationProgress: {
|
||||
equipmentInstanceId: instanceId,
|
||||
designId,
|
||||
progress: 0,
|
||||
required: appTime,
|
||||
manaPerHour,
|
||||
paused: false,
|
||||
manaSpent: 0,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
pauseApplication: () => {
|
||||
const progress = get().applicationProgress;
|
||||
if (!progress) return;
|
||||
|
||||
set({
|
||||
applicationProgress: { ...progress, paused: true },
|
||||
});
|
||||
},
|
||||
|
||||
resumeApplication: () => {
|
||||
const progress = get().applicationProgress;
|
||||
if (!progress) return;
|
||||
|
||||
set({
|
||||
applicationProgress: { ...progress, paused: false },
|
||||
});
|
||||
},
|
||||
|
||||
cancelApplication: () => {
|
||||
set({ applicationProgress: null });
|
||||
},
|
||||
|
||||
// Tick processing
|
||||
processDesignTick: (hours: number) => {
|
||||
const progress = get().designProgress;
|
||||
if (!progress) return;
|
||||
|
||||
const newProgress = progress.progress + hours;
|
||||
|
||||
if (newProgress >= progress.required) {
|
||||
// Design complete
|
||||
set({ designProgress: null });
|
||||
} else {
|
||||
set({
|
||||
designProgress: { ...progress, progress: newProgress },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
processPreparationTick: (hours: number, manaAvailable: number) => {
|
||||
const progress = get().preparationProgress;
|
||||
if (!progress) return 0;
|
||||
|
||||
const instance = get().equipmentInstances[progress.equipmentInstanceId];
|
||||
if (!instance) {
|
||||
set({ preparationProgress: null });
|
||||
return 0;
|
||||
}
|
||||
|
||||
const totalManaCost = calculatePreparationManaCost(instance.typeId);
|
||||
const remainingManaCost = totalManaCost - progress.manaCostPaid;
|
||||
const manaToPay = Math.min(manaAvailable, remainingManaCost);
|
||||
|
||||
if (manaToPay < remainingManaCost) {
|
||||
// Not enough mana, just pay what we can
|
||||
set({
|
||||
preparationProgress: {
|
||||
...progress,
|
||||
manaCostPaid: progress.manaCostPaid + manaToPay,
|
||||
},
|
||||
});
|
||||
return manaToPay;
|
||||
}
|
||||
|
||||
// Pay remaining mana and progress
|
||||
const newProgress = progress.progress + hours;
|
||||
|
||||
if (newProgress >= progress.required) {
|
||||
// Preparation complete - clear enchantments and add 'Ready for Enchantment' tag
|
||||
set((state) => ({
|
||||
preparationProgress: null,
|
||||
equipmentInstances: {
|
||||
...state.equipmentInstances,
|
||||
[instance.instanceId]: {
|
||||
...instance,
|
||||
enchantments: [],
|
||||
usedCapacity: 0,
|
||||
rarity: 'common',
|
||||
tags: [...(instance.tags || []), 'Ready for Enchantment'],
|
||||
},
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
set({
|
||||
preparationProgress: {
|
||||
...progress,
|
||||
progress: newProgress,
|
||||
manaCostPaid: progress.manaCostPaid + manaToPay,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return manaToPay;
|
||||
},
|
||||
|
||||
processApplicationTick: (hours: number, manaAvailable: number) => {
|
||||
const progress = get().applicationProgress;
|
||||
if (!progress || progress.paused) return 0;
|
||||
|
||||
const design = get().enchantmentDesigns.find(d => d.id === progress.designId);
|
||||
const instance = get().equipmentInstances[progress.equipmentInstanceId];
|
||||
|
||||
if (!design || !instance) {
|
||||
set({ applicationProgress: null });
|
||||
return 0;
|
||||
}
|
||||
|
||||
const manaNeeded = progress.manaPerHour * hours;
|
||||
const manaToUse = Math.min(manaAvailable, manaNeeded);
|
||||
|
||||
if (manaToUse < manaNeeded) {
|
||||
// Not enough mana - pause and save progress
|
||||
set({
|
||||
applicationProgress: {
|
||||
...progress,
|
||||
manaSpent: progress.manaSpent + manaToUse,
|
||||
},
|
||||
});
|
||||
return manaToUse;
|
||||
}
|
||||
|
||||
const newProgress = progress.progress + hours;
|
||||
|
||||
if (newProgress >= progress.required) {
|
||||
// Application complete - apply enchantments
|
||||
const efficiencyBonus = getEnchantEfficiencyBonus(cachedSkills);
|
||||
const newEnchantments: AppliedEnchantment[] = design.effects.map(e => ({
|
||||
effectId: e.effectId,
|
||||
stacks: e.stacks,
|
||||
actualCost: calculateEffectCapacityCost(e.effectId, e.stacks, efficiencyBonus),
|
||||
}));
|
||||
|
||||
const totalUsedCapacity = newEnchantments.reduce((sum, e) => sum + e.actualCost, 0);
|
||||
|
||||
set((state) => ({
|
||||
applicationProgress: null,
|
||||
equipmentInstances: {
|
||||
...state.equipmentInstances,
|
||||
[instance.instanceId]: {
|
||||
...instance,
|
||||
enchantments: newEnchantments,
|
||||
usedCapacity: totalUsedCapacity,
|
||||
rarity: calculateRarity(newEnchantments),
|
||||
},
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
set({
|
||||
applicationProgress: {
|
||||
...progress,
|
||||
progress: newProgress,
|
||||
manaSpent: progress.manaSpent + manaToUse,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return manaToUse;
|
||||
},
|
||||
|
||||
// Getters
|
||||
getEquippedInstance: (slot: EquipmentSlot) => {
|
||||
const state = get();
|
||||
const instanceId = state.equippedInstances[slot];
|
||||
if (!instanceId) return null;
|
||||
return state.equipmentInstances[instanceId] || null;
|
||||
},
|
||||
|
||||
getAllEquipped: () => {
|
||||
const state = get();
|
||||
const equipped: EquipmentInstance[] = [];
|
||||
|
||||
for (const slot of EQUIPMENT_SLOTS) {
|
||||
const instanceId = state.equippedInstances[slot];
|
||||
if (instanceId && state.equipmentInstances[instanceId]) {
|
||||
equipped.push(state.equipmentInstances[instanceId]);
|
||||
}
|
||||
}
|
||||
|
||||
return equipped;
|
||||
},
|
||||
|
||||
getAvailableSpells: () => {
|
||||
const equipped = get().getAllEquipped();
|
||||
const spells: string[] = [];
|
||||
|
||||
for (const equip of equipped) {
|
||||
spells.push(...getSpellsFromEquipment(equip));
|
||||
}
|
||||
|
||||
return spells;
|
||||
},
|
||||
|
||||
getEquipmentEffects: () => {
|
||||
return computeEquipmentEffects(get().getAllEquipped());
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Starting Equipment Factory ────────────────────────────────────────────────
|
||||
|
||||
export function createStartingEquipment(): {
|
||||
equippedInstances: Record<string, string | null>;
|
||||
equipmentInstances: Record<string, EquipmentInstance>;
|
||||
} {
|
||||
const instances: EquipmentInstance[] = [];
|
||||
|
||||
// Create starting equipment
|
||||
const basicStaff = createEquipmentInstance('basicStaff');
|
||||
basicStaff.enchantments = [{
|
||||
effectId: 'spell_manaBolt',
|
||||
stacks: 1,
|
||||
actualCost: 50, // Fills the staff completely
|
||||
}];
|
||||
basicStaff.usedCapacity = 50;
|
||||
basicStaff.rarity = 'uncommon';
|
||||
instances.push(basicStaff);
|
||||
|
||||
const civilianShirt = createEquipmentInstance('civilianShirt');
|
||||
instances.push(civilianShirt);
|
||||
|
||||
const civilianGloves = createEquipmentInstance('civilianGloves');
|
||||
instances.push(civilianGloves);
|
||||
|
||||
const civilianShoes = createEquipmentInstance('civilianShoes');
|
||||
instances.push(civilianShoes);
|
||||
|
||||
// Build instance map
|
||||
const equipmentInstances: Record<string, EquipmentInstance> = {};
|
||||
for (const inst of instances) {
|
||||
equipmentInstances[inst.instanceId] = inst;
|
||||
}
|
||||
|
||||
// Build equipped map
|
||||
const equippedInstances: Record<string, string | null> = {
|
||||
mainHand: basicStaff.instanceId,
|
||||
offHand: null,
|
||||
head: null,
|
||||
body: civilianShirt.instanceId,
|
||||
hands: civilianGloves.instanceId,
|
||||
feet: civilianShoes.instanceId,
|
||||
accessory1: null,
|
||||
accessory2: null,
|
||||
};
|
||||
|
||||
return { equippedInstances, equipmentInstances };
|
||||
}
|
||||
// Re-export everything from the modules
|
||||
export type { CraftingState, CraftingActions, CraftingStore } from './crafting-modules/types';
|
||||
export { initialCraftingState } from './crafting-modules/initial-state';
|
||||
export {
|
||||
generateInstanceId,
|
||||
generateDesignId,
|
||||
getEnchantEfficiencyBonus,
|
||||
calculateDesignTime,
|
||||
calculatePreparationTime,
|
||||
calculatePreparationManaCost,
|
||||
calculateApplicationTime,
|
||||
calculateApplicationManaPerHour,
|
||||
createEquipmentInstance,
|
||||
getSpellsFromEquipment,
|
||||
computeEquipmentEffects
|
||||
} from './crafting-modules/utils';
|
||||
export { createCraftingSlice, setCachedSkills } from './crafting-modules/slice-logic';
|
||||
export { createStartingEquipment } from './crafting-modules/starting-equipment';
|
||||
|
||||
Reference in New Issue
Block a user