Refactor large files into modular components
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:
Refactoring Agent
2026-05-02 17:35:03 +02:00
parent c9ae2576f4
commit d2d28887b1
194 changed files with 16862 additions and 15729 deletions
@@ -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;
}
+26 -643
View File
@@ -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';