// ─── Crafting Store Slice ───────────────────────────────────────────────────────── // Handles equipment and enchantment system: design, prepare, apply stages import type { GameState, EquipmentInstance, AppliedEnchantment, EnchantmentDesign, DesignEffect, EquipmentSlot, EquipmentCraftingProgress, LootInventory, AttunementState } from './types'; import { EQUIPMENT_TYPES, type EquipmentCategory } 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'; // ─── Helper Functions ───────────────────────────────────────────────────────── function generateInstanceId(): string { return `equip_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } // Get equipment category from type function getEquipmentCategory(typeId: string): EquipmentCategory | null { const type = EQUIPMENT_TYPES[typeId]; return type?.category || null; } // Calculate total capacity cost for a design function calculateDesignCapacityCost(effects: DesignEffect[], efficiencyBonus: number = 0): number { return effects.reduce((total, eff) => total + calculateEffectCapacityCost(eff.effectId, eff.stacks, efficiencyBonus), 0); } // Calculate design time based on number and complexity of effects function calculateDesignTime(effects: DesignEffect[]): number { // Base 1 hour + 0.5 hours per effect stack let time = 1; for (const eff of effects) { const effectDef = ENCHANTMENT_EFFECTS[eff.effectId]; if (effectDef) { time += 0.5 * eff.stacks; } } return time; } // Calculate preparation time based on equipment capacity function calculatePrepTime(equipmentCapacity: number): number { // Base 2 hours + 1 hour per 50 capacity return 2 + Math.floor(equipmentCapacity / 50); } // Calculate preparation mana cost function calculatePrepManaCost(equipmentCapacity: number): number { // 10 mana per capacity point return equipmentCapacity * 10; } // Calculate application time based on design complexity function calculateApplicationTime(design: EnchantmentDesign): number { // 2 hours base + 1 hour per effect stack return 2 + design.effects.reduce((total, eff) => total + eff.stacks, 0); } // Calculate application mana cost per hour function calculateApplicationManaPerHour(design: EnchantmentDesign): number { // 20 mana per hour base + 5 per effect stack return 20 + design.effects.reduce((total, eff) => total + eff.stacks * 5, 0); } // ─── Crafting Actions Interface ─────────────────────────────────────────────── export interface CraftingActions { // Equipment management createEquipmentInstance: (typeId: string) => string | null; equipItem: (instanceId: string, slot: EquipmentSlot) => boolean; unequipItem: (slot: EquipmentSlot) => void; deleteEquipmentInstance: (instanceId: string) => void; // Enchantment design startDesigningEnchantment: (name: string, equipmentTypeId: string, effects: DesignEffect[]) => boolean; cancelDesign: () => void; saveDesign: (design: EnchantmentDesign) => void; deleteDesign: (designId: string) => void; // Enchantment preparation startPreparing: (equipmentInstanceId: string) => boolean; cancelPreparation: () => void; // Enchantment application startApplying: (equipmentInstanceId: string, designId: string) => boolean; pauseApplication: () => void; resumeApplication: () => void; cancelApplication: () => void; // Disenchanting disenchantEquipment: (instanceId: string) => void; // Equipment Crafting (from blueprints) startCraftingEquipment: (blueprintId: string) => boolean; cancelEquipmentCrafting: () => void; deleteMaterial: (materialId: string, amount: number) => void; // Computed getters getEquipmentSpells: () => string[]; getEquipmentEffects: () => Record; getAvailableCapacity: (instanceId: string) => number; } // ─── Initial Equipment Setup ─────────────────────────────────────────────────── export function createStartingEquipment(): { equippedInstances: Record; equipmentInstances: Record; } { // Create starting staff with Mana Bolt enchantment const staffId = 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, }; // Create starting clothes const shirtId = generateInstanceId(); const shirtInstance: EquipmentInstance = { instanceId: shirtId, typeId: 'civilianShirt', name: 'Civilian Shirt', enchantments: [], usedCapacity: 0, totalCapacity: 30, rarity: 'common', quality: 100, }; const shoesId = generateInstanceId(); const shoesInstance: EquipmentInstance = { instanceId: shoesId, typeId: 'civilianShoes', name: 'Civilian Shoes', enchantments: [], usedCapacity: 0, totalCapacity: 15, rarity: 'common', quality: 100, }; 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 Store Extensions ───────────────────────────────────────────────── export function createCraftingSlice( set: (fn: (state: GameState) => Partial) => void, get: () => GameState & CraftingActions ): CraftingActions { return { // ─── Equipment Management ───────────────────────────────────────────────── createEquipmentInstance: (typeId: string) => { const type = EQUIPMENT_TYPES[typeId]; if (!type) return null; const instanceId = generateInstanceId(); const instance: EquipmentInstance = { instanceId, typeId, name: type.name, enchantments: [], usedCapacity: 0, totalCapacity: type.baseCapacity, rarity: 'common', quality: 100, }; set((state) => ({ equipmentInstances: { ...state.equipmentInstances, [instanceId]: instance, }, })); return instanceId; }, equipItem: (instanceId: string, slot: EquipmentSlot) => { const state = get(); const instance = state.equipmentInstances[instanceId]; if (!instance) return false; const type = EQUIPMENT_TYPES[instance.typeId]; if (!type) return false; // Check if equipment can go in this slot const validSlots = type.category === 'accessory' ? ['accessory1', 'accessory2'] : [type.slot]; if (!validSlots.includes(slot)) return false; // Check if slot is occupied const currentEquipped = state.equippedInstances[slot]; if (currentEquipped === instanceId) return true; // Already equipped here // If this item is equipped elsewhere, unequip it first let newEquipped = { ...state.equippedInstances }; for (const [s, id] of Object.entries(newEquipped)) { if (id === instanceId) { newEquipped[s as EquipmentSlot] = null; } } // Equip to new slot newEquipped[slot] = instanceId; set(() => ({ equippedInstances: newEquipped })); return true; }, unequipItem: (slot: EquipmentSlot) => { set((state) => ({ equippedInstances: { ...state.equippedInstances, [slot]: null, }, })); }, deleteEquipmentInstance: (instanceId: string) => { set((state) => { // First unequip if equipped let newEquipped = { ...state.equippedInstances }; for (const [slot, id] of Object.entries(newEquipped)) { if (id === instanceId) { newEquipped[slot as EquipmentSlot] = null; } } // Remove from instances const newInstances = { ...state.equipmentInstances }; delete newInstances[instanceId]; return { equippedInstances: newEquipped, equipmentInstances: newInstances, }; }); }, // ─── Enchantment Design ───────────────────────────────────────────────── startDesigningEnchantment: (name: string, equipmentTypeId: string, effects: DesignEffect[]) => { const state = get(); // Check if player has enchanting skill const enchantingLevel = state.skills.enchanting || 0; if (enchantingLevel < 1) return false; // Validate effects for equipment category const category = getEquipmentCategory(equipmentTypeId); if (!category) return false; for (const eff of effects) { const effectDef = ENCHANTMENT_EFFECTS[eff.effectId]; if (!effectDef) return false; if (!effectDef.allowedEquipmentCategories.includes(category)) return false; if (eff.stacks > effectDef.maxStacks) return false; } // Calculate capacity cost const efficiencyBonus = (state.skills.efficientEnchant || 0) * 0.05; const totalCapacityCost = calculateDesignCapacityCost(effects, efficiencyBonus); // Create design const designTime = calculateDesignTime(effects); const design: EnchantmentDesign = { id: `design_${Date.now()}`, name, equipmentType: equipmentTypeId, effects, totalCapacityUsed: totalCapacityCost, designTime, created: Date.now(), }; set(() => ({ currentAction: 'design', designProgress: { designId: design.id, progress: 0, required: designTime, }, })); return true; }, cancelDesign: () => { set(() => ({ currentAction: 'meditate', designProgress: null, })); }, saveDesign: (design: EnchantmentDesign) => { set((state) => ({ enchantmentDesigns: [...state.enchantmentDesigns, design], designProgress: null, currentAction: 'meditate', })); }, deleteDesign: (designId: string) => { set((state) => ({ enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== designId), })); }, // ─── Enchantment Preparation ───────────────────────────────────────────── startPreparing: (equipmentInstanceId: string) => { const state = get(); const instance = state.equipmentInstances[equipmentInstanceId]; if (!instance) return false; const prepTime = calculatePrepTime(instance.totalCapacity); const manaCost = calculatePrepManaCost(instance.totalCapacity); if (state.rawMana < manaCost) return false; set(() => ({ currentAction: 'prepare', preparationProgress: { equipmentInstanceId, progress: 0, required: prepTime, manaCostPaid: 0, }, })); return true; }, cancelPreparation: () => { set(() => ({ currentAction: 'meditate', preparationProgress: null, })); }, // ─── Enchantment Application ───────────────────────────────────────────── startApplying: (equipmentInstanceId: string, designId: string) => { const state = get(); const instance = state.equipmentInstances[equipmentInstanceId]; const design = state.enchantmentDesigns.find(d => d.id === designId); if (!instance || !design) return false; // Check capacity if (instance.usedCapacity + design.totalCapacityUsed > instance.totalCapacity) { return false; } const applicationTime = calculateApplicationTime(design); const manaPerHour = calculateApplicationManaPerHour(design); set(() => ({ currentAction: 'enchant', applicationProgress: { equipmentInstanceId, designId, progress: 0, required: applicationTime, manaPerHour, paused: false, manaSpent: 0, }, })); return true; }, pauseApplication: () => { set((state) => { if (!state.applicationProgress) return {}; return { applicationProgress: { ...state.applicationProgress, paused: true, }, }; }); }, resumeApplication: () => { set((state) => { if (!state.applicationProgress) return {}; return { applicationProgress: { ...state.applicationProgress, paused: false, }, }; }); }, cancelApplication: () => { set(() => ({ currentAction: 'meditate', applicationProgress: null, })); }, // ─── Disenchanting ───────────────────────────────────────────────────────── disenchantEquipment: (instanceId: string) => { const state = get(); const instance = state.equipmentInstances[instanceId]; if (!instance || instance.enchantments.length === 0) return; const disenchantLevel = state.skills.disenchanting || 0; const recoveryRate = 0.1 + disenchantLevel * 0.2; // 10% base + 20% per level let totalRecovered = 0; for (const ench of instance.enchantments) { totalRecovered += Math.floor(ench.actualCost * recoveryRate); } set((state) => ({ rawMana: state.rawMana + totalRecovered, equipmentInstances: { ...state.equipmentInstances, [instanceId]: { ...instance, enchantments: [], usedCapacity: 0, }, }, log: [`✨ Disenchanted ${instance.name}, recovered ${totalRecovered} mana.`, ...state.log.slice(0, 49)], })); }, // ─── Computed Getters ───────────────────────────────────────────────────── getEquipmentSpells: () => { const state = get(); const spells: string[] = []; for (const instanceId of Object.values(state.equippedInstances)) { if (!instanceId) continue; const instance = state.equipmentInstances[instanceId]; 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)]; // Remove duplicates }, getEquipmentEffects: () => { const state = get(); const effects: Record = {}; for (const instanceId of Object.values(state.equippedInstances)) { if (!instanceId) continue; const instance = state.equipmentInstances[instanceId]; if (!instance) continue; for (const ench of instance.enchantments) { const effectDef = ENCHANTMENT_EFFECTS[ench.effectId]; if (!effectDef) continue; if (effectDef.effect.type === 'bonus' && effectDef.effect.stat && effectDef.effect.value) { effects[effectDef.effect.stat] = (effects[effectDef.effect.stat] || 0) + effectDef.effect.value * ench.stacks; } } } return effects; }, getAvailableCapacity: (instanceId: string) => { const state = get(); const instance = state.equipmentInstances[instanceId]; if (!instance) return 0; return instance.totalCapacity - instance.usedCapacity; }, // ─── Equipment Crafting (from Blueprints) ─────────────────────────────────── startCraftingEquipment: (blueprintId: string) => { const state = get(); const recipe = CRAFTING_RECIPES[blueprintId]; if (!recipe) return false; // Check if player has the blueprint if (!state.lootInventory.blueprints.includes(blueprintId)) return false; // Check materials const { canCraft, missingMaterials, missingMana } = canCraftRecipe( recipe, state.lootInventory.materials, state.rawMana ); if (!canCraft) return false; // Deduct materials const newMaterials = { ...state.lootInventory.materials }; for (const [matId, amount] of Object.entries(recipe.materials)) { newMaterials[matId] = (newMaterials[matId] || 0) - amount; if (newMaterials[matId] <= 0) { delete newMaterials[matId]; } } // Start crafting progress set((state) => ({ lootInventory: { ...state.lootInventory, materials: newMaterials, }, rawMana: state.rawMana - recipe.manaCost, currentAction: 'craft', equipmentCraftingProgress: { blueprintId, equipmentTypeId: recipe.equipmentTypeId, progress: 0, required: recipe.craftTime, manaSpent: recipe.manaCost, }, })); return true; }, cancelEquipmentCrafting: () => { set((state) => { const progress = state.equipmentCraftingProgress; if (!progress) return {}; const recipe = CRAFTING_RECIPES[progress.blueprintId]; if (!recipe) return { currentAction: 'meditate', equipmentCraftingProgress: null }; // Refund 50% of mana const manaRefund = Math.floor(progress.manaSpent * 0.5); return { currentAction: 'meditate', equipmentCraftingProgress: null, rawMana: state.rawMana + manaRefund, log: [`🚫 Equipment crafting cancelled. Refunded ${manaRefund} mana.`, ...state.log.slice(0, 49)], }; }); }, deleteMaterial: (materialId: string, amount: number) => { set((state) => { const currentAmount = state.lootInventory.materials[materialId] || 0; const newAmount = Math.max(0, currentAmount - amount); const newMaterials = { ...state.lootInventory.materials }; if (newAmount <= 0) { delete newMaterials[materialId]; } else { newMaterials[materialId] = newAmount; } const dropName = materialId; // Could look up in LOOT_DROPS for proper name return { lootInventory: { ...state.lootInventory, materials: newMaterials, }, log: [`🗑️ Deleted ${amount}x ${dropName}.`, ...state.log.slice(0, 49)], }; }); }, }; } // ─── Tick Processing for Crafting ───────────────────────────────────────────── export function processCraftingTick( state: GameState, effects: { rawMana: number; log: string[] } ): Partial { const { rawMana, log } = effects; let updates: Partial = {}; // Process design progress if (state.currentAction === 'design' && state.designProgress) { const progress = state.designProgress.progress + 0.04; // HOURS_PER_TICK if (progress >= state.designProgress.required) { // Design complete - but we need the design data to save it // This will be handled by the UI calling saveDesign updates = { ...updates, log: ['✅ Enchantment design complete!', ...log], }; } else { updates = { ...updates, designProgress: { ...state.designProgress, progress, }, }; } } // Process preparation progress if (state.currentAction === 'prepare' && state.preparationProgress) { const prep = state.preparationProgress; const manaPerHour = calculatePrepManaCost( state.equipmentInstances[prep.equipmentInstanceId]?.totalCapacity || 50 ) / prep.required; const manaCost = manaPerHour * 0.04; // HOURS_PER_TICK if (rawMana >= manaCost) { const progress = prep.progress + 0.04; const manaCostPaid = prep.manaCostPaid + manaCost; if (progress >= prep.required) { updates = { ...updates, rawMana: rawMana - manaCost, preparationProgress: null, currentAction: 'meditate', log: ['✅ Equipment prepared for enchanting!', ...log], }; } else { updates = { ...updates, rawMana: rawMana - manaCost, preparationProgress: { ...prep, progress, manaCostPaid, }, }; } } } // Process application progress if (state.currentAction === 'enchant' && state.applicationProgress && !state.applicationProgress.paused) { const app = state.applicationProgress; const manaCost = app.manaPerHour * 0.04; // HOURS_PER_TICK if (rawMana >= manaCost) { const progress = app.progress + 0.04; const manaSpent = app.manaSpent + manaCost; if (progress >= app.required) { // Apply the enchantment! const instance = state.equipmentInstances[app.equipmentInstanceId]; const design = state.enchantmentDesigns.find(d => d.id === app.designId); if (instance && design) { const newEnchantments: AppliedEnchantment[] = design.effects.map(eff => ({ effectId: eff.effectId, stacks: eff.stacks, actualCost: eff.capacityCost, })); // Calculate and grant attunement XP to enchanter const xpGained = calculateEnchantingXP(design.totalCapacityUsed); let newAttunements = state.attunements; if (state.attunements.enchanter?.active && xpGained > 0) { const enchanterState = state.attunements.enchanter; let newXP = enchanterState.experience + xpGained; let newLevel = enchanterState.level; // Check for level ups while (newLevel < MAX_ATTUNEMENT_LEVEL) { const xpNeeded = getAttunementXPForLevel(newLevel + 1); if (newXP >= xpNeeded) { newXP -= xpNeeded; newLevel++; } else { break; } } newAttunements = { ...state.attunements, enchanter: { ...enchanterState, level: newLevel, experience: newXP, }, }; } updates = { ...updates, rawMana: rawMana - manaCost, applicationProgress: null, currentAction: 'meditate', attunements: newAttunements, equipmentInstances: { ...state.equipmentInstances, [app.equipmentInstanceId]: { ...instance, enchantments: [...instance.enchantments, ...newEnchantments], usedCapacity: instance.usedCapacity + design.totalCapacityUsed, }, }, log: [`✨ Enchantment "${design.name}" applied to ${instance.name}! (+${xpGained} Enchanter XP)`, ...log], }; } } else { updates = { ...updates, rawMana: rawMana - manaCost, applicationProgress: { ...app, progress, manaSpent, }, }; } } } // Process equipment crafting progress if (state.currentAction === 'craft' && state.equipmentCraftingProgress) { const craft = state.equipmentCraftingProgress; const progress = craft.progress + 0.04; // HOURS_PER_TICK if (progress >= craft.required) { // Crafting complete - create the equipment! const recipe = CRAFTING_RECIPES[craft.blueprintId]; const equipType = recipe ? EQUIPMENT_TYPES[recipe.equipmentTypeId] : null; if (recipe && equipType) { const instanceId = generateInstanceId(); const newInstance: EquipmentInstance = { instanceId, typeId: recipe.equipmentTypeId, name: recipe.name, enchantments: [], usedCapacity: 0, totalCapacity: equipType.baseCapacity, rarity: recipe.rarity, quality: 100, }; updates = { ...updates, equipmentCraftingProgress: null, currentAction: 'meditate', equipmentInstances: { ...state.equipmentInstances, [instanceId]: newInstance, }, totalCraftsCompleted: (state.totalCraftsCompleted || 0) + 1, log: [`🔨 Crafted ${recipe.name}!`, ...log], }; } else { updates = { ...updates, equipmentCraftingProgress: null, currentAction: 'meditate', log: ['⚠️ Crafting failed - invalid recipe!', ...log], }; } } else { updates = { ...updates, equipmentCraftingProgress: { ...craft, progress, }, }; } } return updates; } // ─── Export helper to get equipment instance spells ───────────────────────────── export function getSpellsFromEquipment(instances: Record, 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)]; }