diff --git a/src/components/game/tabs/EquipmentSlotGrid.tsx b/src/components/game/tabs/EquipmentSlotGrid.tsx index 957e6cf..2aa4788 100644 --- a/src/components/game/tabs/EquipmentSlotGrid.tsx +++ b/src/components/game/tabs/EquipmentSlotGrid.tsx @@ -1,7 +1,7 @@ 'use client'; import { EquipmentSlot } from '@/lib/game/data/equipment'; -import { SLOT_NAMES } from './EquipmentTab'; +import { SLOT_NAMES } from '@/lib/game/data/equipment'; import type { GameStore, EquipmentInstance } from '@/lib/game/types'; import { GameCard } from '@/components/ui/game-card'; import { Badge } from '@/components/ui/badge'; diff --git a/src/components/game/tabs/EquipmentTab.tsx b/src/components/game/tabs/EquipmentTab.tsx index 23a5f43..a7d1469 100755 --- a/src/components/game/tabs/EquipmentTab.tsx +++ b/src/components/game/tabs/EquipmentTab.tsx @@ -4,6 +4,7 @@ import { useState, useMemo } from 'react'; import { EQUIPMENT_TYPES, EQUIPMENT_SLOTS, + SLOT_NAMES, getEquipmentBySlot, type EquipmentSlot, type EquipmentType, @@ -23,18 +24,6 @@ import { EnchantmentsPanel } from './EnchantmentsPanel'; import { useGameToast } from '@/components/game/GameToast'; import { ConfirmDialog } from '@/components/game/ConfirmDialog'; -// Slot display names -const SLOT_NAMES: Record = { - mainHand: 'Main Hand', - offHand: 'Off Hand', - head: 'Head', - body: 'Body', - hands: 'Hands', - feet: 'Feet', - accessory1: 'Accessory 1', - accessory2: 'Accessory 2', -}; - // Rarity color mappings using design system tokens export const RARITY_BORDER_COLORS: Record = { common: 'border-[var(--text-muted)]', diff --git a/src/lib/game/crafting-actions.ts b/src/lib/game/crafting-actions.ts new file mode 100644 index 0000000..c32bccf --- /dev/null +++ b/src/lib/game/crafting-actions.ts @@ -0,0 +1,513 @@ +// ─── Crafting Action Implementations ────────────────────────────────────────── +// Action implementations for crafting-slice.ts. Extracted to keep main slice focused. +// These functions implement the CraftingActions interface defined in crafting-slice.ts + +import type { GameState, EquipmentInstance, AppliedEnchantment, EnchantmentDesign, DesignEffect, EquipmentCraftingProgress, LootInventory, AttunementState } from './types'; +import { EQUIPMENT_TYPES, type EquipmentSlot } from './data/equipment'; +import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects'; +import { CRAFTING_RECIPES } from './data/crafting-recipes'; +import { computeEffects } from './upgrade-effects'; +import { hasSpecial, SPECIAL_EFFECTS } from './special-effects'; + +import * as CraftingUtils from './crafting-utils'; +import * as CraftingDesign from './crafting-design'; +import * as CraftingPrep from './crafting-prep'; +import * as CraftingApply from './crafting-apply'; +import * as CraftingEquipment from './crafting-equipment'; +import * as CraftingLoot from './crafting-loot'; +import * as CraftingAttunements from './crafting-attunements'; + +// ─── Equipment Management Actions ──────────────────────────────────────────── + +// Create equipment instance +export function createEquipmentInstance( + typeId: string, + set: (fn: (state: GameState) => Partial) => void +): string | null { + const type = CraftingUtils.getEquipmentType(typeId); + if (!type) return null; + + const instanceId = CraftingUtils.generateInstanceId(); + const instance: EquipmentInstance = { + instanceId, + typeId, + name: type.name, + enchantments: [], + usedCapacity: 0, + totalCapacity: type.baseCapacity, + rarity: 'common', + quality: 100, + tags: [], + }; + + set((state) => ({ + equipmentInstances: { + ...state.equipmentInstances, + [instanceId]: instance, + }, + })); + + return instanceId; +} + +// Equip item +export function equipItem( + instanceId: string, + slot: EquipmentSlot, + get: () => GameState, + set: (fn: (state: GameState) => Partial) => void +): boolean { + const state = get(); + const instance = state.equipmentInstances[instanceId]; + if (!instance) return false; + + if (!CraftingUtils.canEquipInSlot(instance, slot, state.equippedInstances)) { + return false; + } + + let newEquipped = { ...state.equippedInstances }; + for (const [s, id] of Object.entries(newEquipped)) { + if (id === instanceId) { + newEquipped[s as EquipmentSlot] = null; + } + } + + newEquipped[slot] = instanceId; + + if (CraftingUtils.isTwoHanded(instance.typeId) && slot === 'mainHand') { + newEquipped.offHand = null; + } + + set(() => ({ equippedInstances: newEquipped })); + return true; +} + +// Unequip item +export function unequipItem( + slot: EquipmentSlot, + set: (fn: (state: GameState) => Partial) => void +) { + set((state) => ({ + equippedInstances: { + ...state.equippedInstances, + [slot]: null, + }, + })); +} + +// Delete equipment instance +export function deleteEquipmentInstance( + instanceId: string, + get: () => GameState, + set: (fn: (state: GameState) => Partial) => void +) { + const state = get(); + let newEquipped = { ...state.equippedInstances }; + for (const [slot, id] of Object.entries(newEquipped)) { + if (id === instanceId) { + newEquipped[slot as EquipmentSlot] = null; + } + } + + const newInstances = { ...state.equipmentInstances }; + delete newInstances[instanceId]; + + set(() => ({ + equippedInstances: newEquipped, + equipmentInstances: newInstances, + })); +} + +// ─── Enchantment Design Actions ──────────────────────────────────────────── + +export function startDesigningEnchantment( + name: string, + equipmentTypeId: string, + effects: DesignEffect[], + get: () => GameState, + set: (fn: (state: GameState) => Partial) => void +): boolean { + const state = get(); + const enchantingLevel = state.skills.enchanting || 0; + const validation = CraftingDesign.validateDesignEffects( + effects, + equipmentTypeId, + enchantingLevel + ); + if (!validation.valid) return false; + + const equipType = CraftingUtils.getEquipmentType(equipmentTypeId); + if (!equipType) return false; + + const efficiencyBonus = ((state.skillUpgrades || {})['efficientEnchant'] || [])?.length * 0.05 || 0; + const totalCapacityCost = CraftingDesign.calculateDesignCapacityCost(effects, efficiencyBonus); + + if (totalCapacityCost > equipType.baseCapacity) { + return false; + } + + const computedEffects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {}); + const hasEnchantMastery = hasSpecial(computedEffects, SPECIAL_EFFECTS.ENCHANT_MASTERY); + + let updates: any = {}; + + if (!state.designProgress) { + updates = { + currentAction: 'design' as const, + designProgress: { + designId: CraftingUtils.generateDesignId(), + progress: 0, + required: CraftingDesign.calculateDesignTime(effects), + name, + equipmentType: equipmentTypeId, + effects, + }, + }; + } else if (hasEnchantMastery && !state.designProgress2) { + updates = { + designProgress2: { + designId: CraftingUtils.generateDesignId(), + progress: 0, + required: CraftingDesign.calculateDesignTime(effects), + name, + equipmentType: equipmentTypeId, + effects, + }, + }; + } else { + return false; + } + + set(() => updates); + return true; +} + +export function cancelDesign( + get: () => GameState, + set: (fn: (state: GameState) => Partial) => void +) { + const state = get(); + if (state.designProgress2 && !state.designProgress) { + set(() => ({ designProgress2: null })); + } else { + set(() => ({ + currentAction: 'meditate' as const, + designProgress: null, + })); + } +} + +export function saveDesign( + design: EnchantmentDesign, + get: () => GameState, + set: (fn: (state: GameState) => Partial) => void +) { + const state = get(); + if (state.designProgress2 && state.designProgress2.designId === design.id) { + set((state) => ({ + enchantmentDesigns: [...state.enchantmentDesigns, design], + designProgress2: null, + })); + } else { + set((state) => ({ + enchantmentDesigns: [...state.enchantmentDesigns, design], + designProgress: null, + currentAction: 'meditate' as const, + })); + } +} + +export function deleteDesign( + designId: string, + set: (fn: (state: GameState) => Partial) => void +) { + set((state) => ({ + enchantmentDesigns: state.enchantmentDesigns.filter(d => d.id !== designId), + })); +} + +// ─── Enchantment Preparation Actions ──────────────────────────────────────── + +export function startPreparing( + equipmentInstanceId: string, + get: () => GameState, + set: (fn: (state: GameState) => Partial) => void +): boolean { + const state = get(); + const instance = state.equipmentInstances[equipmentInstanceId]; + + const validation = CraftingPrep.canPrepareEquipment( + instance, + instance?.tags || [] + ); + if (!validation.canPrepare) return false; + + if (!instance) return false; + + const costs = CraftingPrep.calculatePreparationCosts(instance.totalCapacity); + + if (state.rawMana < costs.manaTotal) return false; + + set(() => ({ + currentAction: 'prepare' as const, + preparationProgress: CraftingPrep.initializePreparationProgress( + equipmentInstanceId, + instance.totalCapacity + ), + })); + + return true; +} + +export function cancelPreparation( + set: (fn: (state: GameState) => Partial) => void +) { + set(() => ({ + currentAction: 'meditate' as const, + preparationProgress: null, + })); +} + +// ─── Enchantment Application Actions ──────────────────────────────────────── + +export function startApplying( + equipmentInstanceId: string, + designId: string, + get: () => GameState, + set: (fn: (state: GameState) => Partial) => void +): boolean { + const state = get(); + const instance = state.equipmentInstances[equipmentInstanceId]; + const design = state.enchantmentDesigns.find(d => d.id === designId); + + const validation = CraftingApply.canApplyEnchantment( + instance, + design, + state.currentAction + ); + if (!validation.canApply) return false; + + set(() => ({ + currentAction: 'enchant' as const, + applicationProgress: CraftingApply.initializeApplicationProgress( + equipmentInstanceId, + designId, + design! + ), + })); + + return true; +} + +export function pauseApplication( + get: () => GameState, + set: (fn: (state: GameState) => Partial) => void +) { + set((state) => { + if (!state.applicationProgress) return {}; + return { + applicationProgress: { + ...state.applicationProgress, + paused: true, + }, + }; + }); +} + +export function resumeApplication( + get: () => GameState, + set: (fn: (state: GameState) => Partial) => void +) { + set((state) => { + if (!state.applicationProgress) return {}; + return { + applicationProgress: { + ...state.applicationProgress, + paused: false, + }, + }; + }); +} + +export function cancelApplication( + set: (fn: (state: GameState) => Partial) => void +) { + set(() => ({ + currentAction: 'meditate' as const, + applicationProgress: null, + })); +} + +// ─── Disenchanting Actions ───────────────────────────────────────────────── + +export function disenchantEquipment( + instanceId: string, + get: () => GameState, + set: (fn: (state: GameState) => Partial) => void +) { + const state = get(); + const instance = state.equipmentInstances[instanceId]; + if (!instance || instance.enchantments.length === 0) return; + + const disenchantLevel = 0; + const recoveryRate = 0.1 + disenchantLevel * 0.2; + + 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)], + })); +} + +// ─── Equipment Crafting Actions ──────────────────────────────────────────── + +export function startCraftingEquipment( + blueprintId: string, + get: () => GameState, + set: (fn: (state: GameState) => Partial) => void +): boolean { + const state = get(); + + const check = CraftingEquipment.canStartEquipmentCrafting( + blueprintId, + state.lootInventory.blueprints.includes(blueprintId), + state.lootInventory.materials, + state.rawMana, + state.currentAction + ); + + if (!check.canCraft) return false; + + const result = CraftingEquipment.initializeEquipmentCrafting( + blueprintId, + state.lootInventory.materials, + state.rawMana + ); + + set((state) => ({ + lootInventory: { + ...state.lootInventory, + materials: result.newMaterials, + }, + rawMana: state.rawMana - result.manaCost, + currentAction: 'craft' as const, + equipmentCraftingProgress: result.progress, + })); + + return true; +} + +export function cancelEquipmentCrafting( + get: () => GameState, + set: (fn: (state: GameState) => Partial) => void +) { + set((state) => { + const progress = state.equipmentCraftingProgress; + if (!progress) return { currentAction: 'meditate' as const, equipmentCraftingProgress: null }; + + const cancelResult = CraftingEquipment.cancelEquipmentCrafting( + progress.blueprintId, + progress.manaSpent + ); + + return { + currentAction: 'meditate' as const, + equipmentCraftingProgress: null, + rawMana: state.rawMana + cancelResult.manaRefund, + log: [cancelResult.logMessage, ...state.log.slice(0, 49)], + }; + }); +} + +export function deleteMaterial( + materialId: string, + amount: number, + get: () => GameState, + set: (fn: (state: GameState) => Partial) => void +) { + set((state) => { + const newMaterials = { ...state.lootInventory.materials }; + const currentAmount = newMaterials[materialId] || 0; + const newAmount = Math.max(0, currentAmount - amount); + + if (newAmount <= 0) { + delete newMaterials[materialId]; + } else { + newMaterials[materialId] = newAmount; + } + + return { + lootInventory: { + ...state.lootInventory, + materials: newMaterials, + }, + log: [`🗑️ Deleted ${amount}x ${materialId}.`, ...state.log.slice(0, 49)], + }; + }); +} + +// ─── Computed Getters ────────────────────────────────────────────────────── + +export function getEquipmentSpells(get: () => GameState): string[] { + 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)]; +} + +export function getEquipmentEffects(get: () => GameState): Record { + 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; +} + +export function getAvailableCapacity( + instanceId: string, + get: () => GameState +): number { + const state = get(); + const instance = state.equipmentInstances[instanceId]; + if (!instance) return 0; + return instance.totalCapacity - instance.usedCapacity; +} diff --git a/src/lib/game/crafting-apply.ts b/src/lib/game/crafting-apply.ts new file mode 100644 index 0000000..1e85ec8 --- /dev/null +++ b/src/lib/game/crafting-apply.ts @@ -0,0 +1,235 @@ +// ─── Crafting Application System ──────────────────────────────────────────── +// Application system functions extracted from crafting-slice.ts + +import type { EquipmentInstance, AppliedEnchantment, EnchantmentDesign, ApplicationProgress } from './types'; +import { calculateApplicationTime, calculateApplicationManaPerHour } from './crafting-utils'; +import { hasSpecial, SPECIAL_EFFECTS } from './special-effects'; +import { computeEffects } from './upgrade-effects'; +import type { AttunementState } from './types'; +import { calculateEnchantingXP } from './data/attunements'; +import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects'; + +// ─── Application Validation ───────────────────────────────────────────────── + +// Check if enchantment application can start +export function canApplyEnchantment( + instance: EquipmentInstance | undefined, + design: EnchantmentDesign | undefined, + currentAction: string +): { canApply: boolean; reason?: string } { + if (!instance) { + return { canApply: false, reason: 'Equipment instance not found' }; + } + + if (!design) { + return { canApply: false, reason: 'Enchantment design not found' }; + } + + if (currentAction !== 'meditate') { + return { canApply: false, reason: 'Must be in meditate state' }; + } + + if (!instance.tags?.includes('Ready for Enchantment')) { + return { canApply: false, reason: 'Equipment must be prepared for enchanting' }; + } + + if (instance.usedCapacity + design.totalCapacityUsed > instance.totalCapacity) { + return { canApply: false, reason: 'Not enough capacity on equipment' }; + } + + return { canApply: true }; +} + +// ─── Application Resource Calculation ──────────────────────────────────────── + +export interface ApplicationCosts { + time: number; + manaPerHour: number; + manaPerTick: number; +} + +export function calculateApplicationCosts(design: EnchantmentDesign): ApplicationCosts { + const time = calculateApplicationTime(design); + const manaPerHour = calculateApplicationManaPerHour(design); + const manaPerTick = manaPerHour * 0.04; // HOURS_PER_TICK + + return { time, manaPerHour, manaPerTick }; +} + +// ─── Application Progress ─────────────────────────────────────────────────── + +// Initialize application progress +export function initializeApplicationProgress( + equipmentInstanceId: string, + designId: string, + design: EnchantmentDesign +): ApplicationProgress { + const costs = calculateApplicationCosts(design); + + return { + equipmentInstanceId, + designId, + progress: 0, + required: costs.time, + manaPerHour: costs.manaPerHour, + paused: false, + manaSpent: 0, + }; +} + +// Calculate application progress after a tick +export interface ApplicationTickResult { + progress: number; + manaSpent: number; + manaConsumed: number; + isComplete: boolean; + triggeredFreeEnchant: boolean; +} + +export function calculateApplicationTick( + currentProgress: number, + required: number, + currentManaSpent: number, + manaPerTick: number, + computedEffects: any +): ApplicationTickResult { + let progress = currentProgress + 0.04; + let manaSpent = currentManaSpent + manaPerTick; + let manaConsumed = manaPerTick; + let triggeredFreeEnchant = false; + + let freeEnchantChance = 0; + if (hasSpecial(computedEffects, SPECIAL_EFFECTS.ENCHANT_PRESERVATION)) { + freeEnchantChance += 0.25; + } + if (hasSpecial(computedEffects, SPECIAL_EFFECTS.THRIFTY_ENCHANTER)) { + freeEnchantChance += 0.10; + } + if (hasSpecial(computedEffects, SPECIAL_EFFECTS.OPTIMIZED_ENCHANTING)) { + freeEnchantChance += 0.25; + } + + if (freeEnchantChance > 0 && Math.random() < freeEnchantChance) { + progress = required; + manaConsumed = 0; + manaSpent = currentManaSpent; + triggeredFreeEnchant = true; + } + + return { + progress, + manaSpent, + manaConsumed, + isComplete: progress >= required, + triggeredFreeEnchant, + }; +} + +// ─── Enchantment Application ──────────────────────────────────────────────── + +// Apply enchantments to equipment instance +export function applyEnchantments( + instance: EquipmentInstance, + design: EnchantmentDesign, + computedEffects: any +): { + updatedInstance: EquipmentInstance; + xpGained: number; + logMessage: string; +} { + const isPureEssenceActive = hasSpecial(computedEffects, SPECIAL_EFFECTS.PURE_ESSENCE); + + const newEnchantments: AppliedEnchantment[] = design.effects.map(eff => { + let stacks = eff.stacks; + let actualCost = eff.capacityCost; + + const effectDef = ENCHANTMENT_EFFECTS[eff.effectId]; + if (isPureEssenceActive && effectDef && effectDef.baseCapacityCost < 100) { + stacks = Math.ceil(stacks * 1.25); + } + + return { + effectId: eff.effectId, + stacks, + actualCost, + }; + }); + + const xpGained = calculateEnchantingXP(design.totalCapacityUsed); + + const updatedInstance: EquipmentInstance = { + ...instance, + enchantments: [...instance.enchantments, ...newEnchantments], + usedCapacity: instance.usedCapacity + design.totalCapacityUsed, + }; + + return { + updatedInstance, + xpGained, + logMessage: `✨ Enchantment "${design.name}" applied to ${instance.name}! (+${xpGained} Enchanter XP)`, + }; +} + +// ─── Attunement XP Updates ────────────────────────────────────────────────── + +export function updateEnchanterAttunement( + attunements: Record, + xpGained: number +): Record { + if (!attunements?.enchanter?.active || xpGained <= 0) { + return attunements; + } + + const enchanterState = attunements.enchanter; + let newXP = enchanterState.experience + xpGained; + let newLevel = enchanterState.level; + + const { getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } = require('./data/attunements'); + + while (newLevel < MAX_ATTUNEMENT_LEVEL) { + const xpNeeded = getAttunementXPForLevel(newLevel + 1); + if (newXP >= xpNeeded) { + newXP -= xpNeeded; + newLevel++; + } else { + break; + } + } + + return { + ...attunements, + enchanter: { + ...enchanterState, + level: newLevel, + experience: newXP, + }, + }; +} + +// ─── Application Cancellation ──────────────────────────────────────────────── + +export function cancelApplication() { + return { logMessage: 'Enchantment application cancelled.' }; +} + +export function pauseApplication() { + return { logMessage: 'Enchantment application paused.' }; +} + +export function resumeApplication() { + return { logMessage: 'Enchantment application resumed.' }; +} + +// ─── Progress Calculations ────────────────────────────────────────────────── + +export function getApplicationManaCostForTick(manaPerHour: number): number { + return manaPerHour * 0.04; +} + +export function getApplicationRemainingTime(currentProgress: number, required: number): number { + return Math.max(0, required - currentProgress); +} + +export function getApplicationCompletionPercent(currentProgress: number, required: number): number { + return Math.min(100, Math.floor((currentProgress / required) * 100)); +} diff --git a/src/lib/game/crafting-attunements.ts b/src/lib/game/crafting-attunements.ts new file mode 100644 index 0000000..52838c3 --- /dev/null +++ b/src/lib/game/crafting-attunements.ts @@ -0,0 +1,171 @@ +// ─── Attunement System ────────────────────────────────────────────────────── +// Attunement system functions extracted from crafting-slice.ts + +import type { AttunementState } from './types'; +import { calculateEnchantingXP, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from './data/attunements'; + +// ─── Enchanter Attunement ────────────────────────────────────────────────── + +export function initEnchanterAttunement(): AttunementState { + return { + id: 'enchanter', + active: true, + level: 1, + experience: 0, + title: 'Novice Enchanter', + }; +} + +export function isEnchanterActive(attunements: Record): boolean { + return attunements.enchanter?.active === true; +} + +// ─── XP Gain & Leveling ──────────────────────────────────────────────────── + +export function gainEnchantingXP( + attunements: Record, + xpAmount: number +): { + attunements: Record; + leveledUp: boolean; + previousLevel: number; + newLevel: number; +} { + if (!attunements?.enchanter?.active || xpAmount <= 0) { + return { + attunements, + leveledUp: false, + previousLevel: attunements.enchanter?.level || 0, + newLevel: attunements.enchanter?.level || 0, + }; + } + + const previousLevel = attunements.enchanter.level; + let newXP = attunements.enchanter.experience + xpAmount; + let newLevel = attunements.enchanter.level; + let leveledUp = false; + + while (newLevel < MAX_ATTUNEMENT_LEVEL) { + const xpNeeded = getAttunementXPForLevel(newLevel + 1); + if (newXP >= xpNeeded) { + newXP -= xpNeeded; + newLevel++; + leveledUp = true; + } else { + break; + } + } + + const updatedAttunements: Record = { + ...attunements, + enchanter: { + ...attunements.enchanter, + level: newLevel, + experience: newXP, + }, + }; + + return { + attunements: updatedAttunements, + leveledUp, + previousLevel, + newLevel, + }; +} + +export function getXpToNextLevel(currentLevel: number): number { + if (currentLevel >= MAX_ATTUNEMENT_LEVEL) return 0; + return getAttunementXPForLevel(currentLevel + 1); +} + +export function getLevelProgress(attunements: Record): { + currentLevel: number; + currentXP: number; + xpToNextLevel: number; + progressPercent: number; + maxLevel: number; +} { + const enchanter = attunements?.enchanter; + if (!enchanter?.active) { + return { + currentLevel: 0, + currentXP: 0, + xpToNextLevel: 0, + progressPercent: 0, + maxLevel: MAX_ATTUNEMENT_LEVEL, + }; + } + + const xpToNext = getXpToNextLevel(enchanter.level); + return { + currentLevel: enchanter.level, + currentXP: enchanter.experience, + xpToNextLevel: xpToNext, + progressPercent: xpToNext > 0 ? (enchanter.experience / xpToNext) * 100 : 100, + maxLevel: MAX_ATTUNEMENT_LEVEL, + }; +} + +export function calculateEnchantingXpFromDesignCapacity(capacityCost: number): number { + return calculateEnchantingXP(capacityCost); +} + +export function calculateXpFromEquipmentInstance( + instance: { enchantments: Array<{ effectId: string; stacks: number; actualCost: number }> } +): number { + let totalXp = 0; + for (const ench of instance.enchantments) { + totalXp += calculateEnchantingXP(ench.actualCost); + } + return totalXp; +} + +export function getEnchanterTitle(level: number): string { + if (level >= 10) return 'Grandmaster Enchanter'; + if (level >= 8) return 'Master Enchanter'; + if (level >= 6) return 'Expert Enchanter'; + if (level >= 4) return 'Journeyman Enchanter'; + if (level >= 2) return 'Apprentice Enchanter'; + return 'Novice Enchanter'; +} + +export function updateEnchanterTitle(attunements: Record): Record { + if (!attunements.enchanter?.active) return attunements; + const title = getEnchanterTitle(attunements.enchanter.level); + return { + ...attunements, + enchanter: { ...attunements.enchanter, title }, + }; +} + +export function resetEnchanterExperience(attunements: Record): Record { + if (!attunements.enchanter) return attunements; + return { + ...attunements, + enchanter: { ...attunements.enchanter, experience: 0, level: 1, title: 'Novice Enchanter' }, + }; +} + +export function initAttunements(): Record { + return { enchanter: initEnchanterAttunement() }; +} + +export function batchGainXP( + attunements: Record, + xpAmounts: number[] +): { attunements: Record; totalXP: number; totalLevelUps: number; finalLevel: number } { + let result = { + attunements, + totalXP: 0, + totalLevelUps: 0, + finalLevel: attunements.enchanter?.level || 1, + }; + for (const xpAmount of xpAmounts) { + const gain = gainEnchantingXP(result.attunements, xpAmount); + result.attunements = gain.attunements; + result.totalXP += xpAmount; + if (gain.leveledUp) result.totalLevelUps++; + result.finalLevel = gain.newLevel; + } + return result; +} diff --git a/src/lib/game/crafting-design.ts b/src/lib/game/crafting-design.ts new file mode 100644 index 0000000..7750747 --- /dev/null +++ b/src/lib/game/crafting-design.ts @@ -0,0 +1,225 @@ +// ─── Crafting Design System ───────────────────────────────────────────────── +// Design system functions: calculateDesignTime, capacity cost, XP, etc. + +import type { EnchantmentDesign, DesignEffect, AppliedEnchantment } from './types'; +import { calculateEnchantingXP } from './data/attunements'; +import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects'; +import { hasSpecial, SPECIAL_EFFECTS } from './special-effects'; +import { computeEffects } from './upgrade-effects'; +import { EQUIPMENT_TYPES, type EquipmentCategory } from './data/equipment'; + +// ─── Design Creation & Calculation ────────────────────────────────────────── + +// Validate effects for a design against equipment category +export function validateDesignEffects( + effects: DesignEffect[], + equipmentTypeId: string, + enchantingLevel: number +): { valid: boolean; reason?: string } { + if (enchantingLevel < 1) { + return { valid: false, reason: 'Requires enchanting skill level 1' }; + } + + const equipType = EQUIPMENT_TYPES[equipmentTypeId]; + if (!equipType) { + return { valid: false, reason: 'Invalid equipment type' }; + } + const category = equipType.category; + if (!category) { + return { valid: false, reason: 'Invalid equipment category' }; + } + + for (const eff of effects) { + const effectDef = ENCHANTMENT_EFFECTS[eff.effectId]; + if (!effectDef) { + return { valid: false, reason: `Unknown effect: ${eff.effectId}` }; + } + if (!effectDef.allowedEquipmentCategories.includes(category)) { + return { valid: false, reason: `Effect ${eff.effectId} not allowed on ${category}` }; + } + if (eff.stacks > effectDef.maxStacks) { + return { valid: false, reason: `Stacks exceed maximum for ${eff.effectId}` }; + } + } + + return { valid: true }; +} + +// Create an enchantment design from validated inputs +export function createEnchantmentDesign( + name: string, + equipmentType: string, + effects: DesignEffect[], + efficiencyBonus: number = 0 +): EnchantmentDesign { + const totalCapacityUsed = calculateDesignCapacityCost(effects, efficiencyBonus); + const designTime = calculateDesignTime(effects); + + return { + id: `design_${Date.now()}`, + name, + equipmentType, + effects, + totalCapacityUsed, + designTime, + created: Date.now(), + }; +} + +// ─── Capacity Cost Calculation ────────────────────────────────────────────── + +export function calculateDesignCapacityCost(effects: DesignEffect[], efficiencyBonus: number = 0): number { + return effects.reduce((total, eff) => total + calculateEffectCapacityCost(eff.effectId, eff.stacks, efficiencyBonus), 0); +} + +export function calculateTotalCapacityCost(design: EnchantmentDesign): number { + return design.totalCapacityUsed; +} + +// ─── XP & Progression ─────────────────────────────────────────────────────── + +export function calculateEnchantingXpFromDesign(design: EnchantmentDesign): number { + return calculateEnchantingXP(design.totalCapacityUsed); +} + +export function calculateXpFromInstanceEnchantments( + instance: { enchantments: AppliedEnchantment[] } +): number { + let totalXp = 0; + for (const ench of instance.enchantments) { + const effectDef = ENCHANTMENT_EFFECTS[ench.effectId]; + const baseCost = effectDef?.baseCapacityCost || 0; + totalXp += calculateEnchantingXP(baseCost * ench.stacks); + } + return totalXp; +} + +// ─── Design Time Calculations ────────────────────────────────────────────── + +export function calculateDesignTime(effects: DesignEffect[]): number { + let time = 1; + for (const eff of effects) { + const effectDef = ENCHANTMENT_EFFECTS[eff.effectId]; + if (effectDef) { + time += 0.5 * eff.stacks; + } + } + return time; +} + +export function getDesignTimeWithHaste( + effects: DesignEffect[], + isRepeatDesign: boolean, + computedEffects: any +): number { + let time = calculateDesignTime(effects); + if (isRepeatDesign && hasSpecial(computedEffects, SPECIAL_EFFECTS.HASTY_ENCHANTER)) { + time *= 0.75; + } + return time; +} + +// ─── Progress Calculations ────────────────────────────────────────────────── + +export interface DesignProgressUpdate { + progress: number; + required: number; + isComplete: boolean; + timeBonus: number; +} + +export function calculateDesignProgress( + currentProgress: number, + required: number, + computedEffects: any, + isRepeatDesign: boolean +): DesignProgressUpdate { + let progress = currentProgress + 0.04; + let timeBonus = 0; + + if (isRepeatDesign && hasSpecial(computedEffects, SPECIAL_EFFECTS.HASTY_ENCHANTER)) { + timeBonus = 0.04 * 0.25; + progress += timeBonus; + } + + if (hasSpecial(computedEffects, SPECIAL_EFFECTS.INSTANT_DESIGNS) && Math.random() < 0.10) { + progress = required; + } + + return { progress, required, isComplete: progress >= required, timeBonus }; +} + +export function calculateSecondDesignProgress( + currentProgress: number, + required: number, + computedEffects: any, + isRepeatDesign: boolean +): DesignProgressUpdate { + return calculateDesignProgress(currentProgress, required, computedEffects, isRepeatDesign); +} + +export function isSecondDesignSlotAvailable( + designProgress: any, + designProgress2: any, + hasEnchantMastery: boolean +): boolean { + if (!designProgress && !designProgress2) return true; + if (!designProgress && designProgress2) return false; + if (designProgress && !designProgress2 && hasEnchantMastery) return true; + return false; +} + +// ─── Auto-save Completed Design ──────────────────────────────────────────── + +export function createCompletedDesignFromProgress( + progressData: { + designId: string; + name: string; + equipmentType: string; + effects: DesignEffect[]; + required: number; + }, + efficiencyBonus: number = 0 +): EnchantmentDesign { + const totalCapacityCost = calculateDesignCapacityCost(progressData.effects, efficiencyBonus); + return { + id: progressData.designId, + name: progressData.name, + equipmentType: progressData.equipmentType, + effects: progressData.effects, + totalCapacityUsed: totalCapacityCost, + designTime: progressData.required, + created: Date.now(), + }; +} + +// ─── Design Management ────────────────────────────────────────────────────── + +export interface DesignWithCapacityInfo { + design: EnchantmentDesign; + fitsInEquipment: boolean; + availableCapacity: number; +} + +export function filterDesignsByEquipment( + designs: EnchantmentDesign[], + equipment: { instanceId: string; totalCapacity: number; usedCapacity: number } | null +): DesignWithCapacityInfo[] { + if (!equipment) return []; + return designs.map(design => ({ + design, + fitsInEquipment: designFitsInEquipment(design, { + ...equipment, + enchantments: [], + rarity: 'common', + quality: 100, + typeId: '', + name: '', + } as any), + availableCapacity: equipment.totalCapacity - equipment.usedCapacity, + })); +} + +function designFitsInEquipment(design: EnchantmentDesign, instance: any): boolean { + return (instance.usedCapacity || 0) + design.totalCapacityUsed <= instance.totalCapacity; +} diff --git a/src/lib/game/crafting-equipment.ts b/src/lib/game/crafting-equipment.ts new file mode 100644 index 0000000..a8f0966 --- /dev/null +++ b/src/lib/game/crafting-equipment.ts @@ -0,0 +1,229 @@ +// ─── Equipment Crafting System ────────────────────────────────────────────── +// Equipment crafting functions extracted from crafting-slice.ts + +import type { EquipmentInstance, EquipmentCraftingProgress } from './types'; +import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/crafting-recipes'; +import { EQUIPMENT_TYPES } from './data/equipment'; +import { generateInstanceId } from './crafting-utils'; + +// ─── Equipment Crafting Validation ────────────────────────────────────────── + +// Check if equipment crafting can start +export function canStartEquipmentCrafting( + blueprintId: string, + hasBlueprint: boolean, + materials: Record, + currentMana: number, + currentAction: string +): { canCraft: boolean; reason?: string; recipe?: CraftingRecipe; missingMaterials?: Record; missingMana?: number } { + if (currentAction !== 'meditate') { + return { canCraft: false, reason: 'Must be in meditate state' }; + } + + const recipe = CRAFTING_RECIPES[blueprintId]; + if (!recipe) { + return { canCraft: false, reason: 'Invalid blueprint' }; + } + + if (!hasBlueprint) { + return { canCraft: false, reason: 'Blueprint not acquired' }; + } + + const { canCraft, missingMaterials } = canCraftRecipe(recipe, materials, currentMana); + + if (!canCraft) { + const missingMana = Math.max(0, recipe.manaCost - currentMana); + return { + canCraft: false, + reason: missingMana > 0 ? 'Insufficient mana' : 'Missing materials', + recipe, + missingMaterials, + missingMana: missingMana > 0 ? missingMana : undefined, + }; + } + + return { canCraft: true, recipe }; +} + +// ─── Equipment Crafting Execution ─────────────────────────────────────────── + +// Deduct crafting costs and initialize progress +export interface CraftingInitResult { + recipe: CraftingRecipe; + newMaterials: Record; + manaCost: number; + progress: EquipmentCraftingProgress; +} + +export function initializeEquipmentCrafting( + blueprintId: string, + materials: Record, + currentMana: number +): CraftingInitResult { + const recipe = CRAFTING_RECIPES[blueprintId]; + + // Deduct materials + const newMaterials = { ...materials }; + for (const [matId, amount] of Object.entries(recipe.materials)) { + newMaterials[matId] = (newMaterials[matId] || 0) - amount; + if (newMaterials[matId] <= 0) { + delete newMaterials[matId]; + } + } + + // Create progress + const progress: EquipmentCraftingProgress = { + blueprintId, + equipmentTypeId: recipe.equipmentTypeId, + progress: 0, + required: recipe.craftTime, + manaSpent: recipe.manaCost, + }; + + return { + recipe, + newMaterials, + manaCost: recipe.manaCost, + progress, + }; +} + +// ─── Crafting Progress ────────────────────────────────────────────────────── + +// Calculate crafting progress after a tick +export interface CraftingTickResult { + progress: number; + isComplete: boolean; +} + +export function calculateCraftingTick(currentProgress: number, required: number): CraftingTickResult { + const progress = currentProgress + 0.04; // HOURS_PER_TICK + return { + progress, + isComplete: progress >= required, + }; +} + +// ─── Crafting Completion ─────────────────────────────────────────────────── + +// Create equipment instance from completed crafting +export function completeEquipmentCrafting( + blueprintId: string, + recipe: CraftingRecipe +): { + instanceId: string; + instance: EquipmentInstance; + logMessage: string; +} { + const equipType = EQUIPMENT_TYPES[recipe.equipmentTypeId]; + if (!equipType) { + throw new Error(`Invalid equipment type: ${recipe.equipmentTypeId}`); + } + + const instanceId = `equip_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const newInstance: EquipmentInstance = { + instanceId, + typeId: recipe.equipmentTypeId, + name: recipe.name, + enchantments: [], + usedCapacity: 0, + totalCapacity: equipType.baseCapacity, + rarity: recipe.rarity, + quality: 100, + tags: [], + }; + + return { + instanceId, + instance: newInstance, + logMessage: `🔨 Crafted ${recipe.name}!`, + }; +} + +// ─── Crafting Cancellation ────────────────────────────────────────────────── + +// Cancel active crafting and refund partial resources +export interface CraftingCancelResult { + manaRefund: number; + logMessage: string; +} + +export function cancelEquipmentCrafting(blueprintId: string, manaSpent: number): CraftingCancelResult { + const recipe = CRAFTING_RECIPES[blueprintId]; + if (!recipe) { + return { + manaRefund: 0, + logMessage: 'Invalid crafting recipe.', + }; + } + + // Refund 50% of mana + const manaRefund = Math.floor(manaSpent * 0.5); + + return { + manaRefund, + logMessage: `🚫 Equipment crafting cancelled. Refunded ${manaRefund} mana.`, + }; +} + +// ─── Recipe Information ───────────────────────────────────────────────────── + +export function getRecipe(blueprintId: string): CraftingRecipe | null { + return CRAFTING_RECIPES[blueprintId] || null; +} + +export function getCraftableRecipes( + blueprints: string[], + materials: Record, + currentMana: number +): CraftingRecipe[] { + const craftable: CraftingRecipe[] = []; + + for (const blueprintId of blueprints) { + const recipe = CRAFTING_RECIPES[blueprintId]; + if (!recipe) continue; + + const { canCraft } = canCraftRecipe(recipe, materials, currentMana); + if (canCraft) { + craftable.push(recipe); + } + } + + return craftable; +} + +// ─── Material Management ──────────────────────────────────────────────────── + +// Delete materials from inventory +export function deleteMaterials(materialId: string, amount: number, materials: Record): { + newMaterials: Record; + deleted: number; +} { + const currentAmount = materials[materialId] || 0; + const deleted = Math.min(amount, currentAmount); + const remaining = Math.max(0, currentAmount - amount); + const newMaterials = { ...materials }; + + if (remaining <= 0) { + delete newMaterials[materialId]; + } else { + newMaterials[materialId] = remaining; + } + + return { + newMaterials, + deleted, + }; +} + +// Get total material count +export function getMaterialCount(materials: Record, materialId: string): number { + return materials[materialId] || 0; +} + +// Add materials to inventory +export function addMaterials(materials: Record, materialId: string, amount: number): Record { + const newMaterials = { ...materials }; + newMaterials[materialId] = (newMaterials[materialId] || 0) + amount; + return newMaterials; +} diff --git a/src/lib/game/crafting-loot.ts b/src/lib/game/crafting-loot.ts new file mode 100644 index 0000000..63c7d6e --- /dev/null +++ b/src/lib/game/crafting-loot.ts @@ -0,0 +1,277 @@ +// ─── Crafting Loot Inventory System ──────────────────────────────────────────── +// Loot inventory functions extracted from crafting-slice.ts + +import type { LootInventory } from './types'; +import type { CraftingRecipe } from './data/crafting-recipes'; + +// ─── Inventory Queries ────────────────────────────────────────────────────── + +// Get material count +export function getMaterialCount(inventory: LootInventory, materialId: string): number { + return inventory.materials[materialId] || 0; +} + +// Check if has blueprint +export function hasBlueprint(inventory: LootInventory, blueprintId: string): boolean { + return inventory.blueprints.includes(blueprintId); +} + +// Check if has any materials +export function hasMaterials(inventory: LootInventory): boolean { + return Object.keys(inventory.materials).length > 0; +} + +// Get total number of unique materials +export function getUniqueMaterialCount(inventory: LootInventory): number { + return Object.keys(inventory.materials).length; +} + +// Get total material stacks (sum of all quantities) +export function getTotalMaterialStacks(inventory: LootInventory): number { + return Object.values(inventory.materials).reduce((sum, qty) => sum + qty, 0); +} + +// ─── Inventory Modifications ──────────────────────────────────────────────── + +// Add materials to inventory +export function addMaterial(inventory: LootInventory, materialId: string, amount: number): LootInventory { + const currentAmount = inventory.materials[materialId] || 0; + return { + ...inventory, + materials: { + ...inventory.materials, + [materialId]: currentAmount + amount, + }, + }; +} + +// Add multiple materials at once +export function addMaterials( + inventory: LootInventory, + materials: Record +): LootInventory { + const newMaterials = { ...inventory.materials }; + for (const [matId, amount] of Object.entries(materials)) { + newMaterials[matId] = (newMaterials[matId] || 0) + amount; + } + return { + ...inventory, + materials: newMaterials, + }; +} + +// Remove materials from inventory +export function removeMaterial(inventory: LootInventory, materialId: string, amount: number): { + inventory: LootInventory; + removed: number; +} { + const currentAmount = inventory.materials[materialId] || 0; + const removed = Math.min(amount, currentAmount); + const remaining = Math.max(0, currentAmount - amount); + + const newMaterials = { ...inventory.materials }; + if (remaining <= 0) { + delete newMaterials[materialId]; + } else { + newMaterials[materialId] = remaining; + } + + return { + inventory: { + ...inventory, + materials: newMaterials, + }, + removed, + }; +} + +// Delete materials completely (admin/debug operation) +export function deleteMaterial(inventory: LootInventory, materialId: string): LootInventory { + const newMaterials = { ...inventory.materials }; + delete newMaterials[materialId]; + return { + ...inventory, + materials: newMaterials, + }; +} + +// Clear all materials +export function clearMaterials(inventory: LootInventory): LootInventory { + return { + ...inventory, + materials: {}, + }; +} + +// ─── Blueprint Management ────────────────────────────────────────────────── + +// Add a blueprint +export function addBlueprint(inventory: LootInventory, blueprintId: string): LootInventory { + if (inventory.blueprints.includes(blueprintId)) { + return inventory; + } + return { + ...inventory, + blueprints: [...inventory.blueprints, blueprintId], + }; +} + +// Remove a blueprint +export function removeBlueprint(inventory: LootInventory, blueprintId: string): LootInventory { + return { + ...inventory, + blueprints: inventory.blueprints.filter(id => id !== blueprintId), + }; +} + +// Check and remove consumed blueprint (if single-use) +export function consumeBlueprint(inventory: LootInventory, blueprintId: string): LootInventory { + // Blueprints are persistent for now, but this function provides future flexibility + return inventory; +} + +// ─── Recipe Material Checks ───────────────────────────────────────────────── + +// Check if specific materials are available +export function checkMaterialsAvailable( + inventory: LootInventory, + required: Record +): { hasAll: boolean; missing: Record } { + const missing: Record = {}; + let hasAll = true; + + for (const [matId, amount] of Object.entries(required)) { + const available = inventory.materials[matId] || 0; + if (available < amount) { + missing[matId] = amount - available; + hasAll = false; + } + } + + return { hasAll, missing }; +} + +// Deduct recipe materials +export function deductRecipeMaterials( + inventory: LootInventory, + recipeMaterials: Record +): LootInventory { + const newMaterials = { ...inventory.materials }; + for (const [matId, amount] of Object.entries(recipeMaterials)) { + const newAmount = (newMaterials[matId] || 0) - amount; + if (newAmount <= 0) { + delete newMaterials[matId]; + } else { + newMaterials[matId] = newAmount; + } + } + return { + ...inventory, + materials: newMaterials, + }; +} + +// ─── Inventory Filters & Sorting ──────────────────────────────────────────── + +export interface MaterialWithInfo { + id: string; + quantity: number; + name: string; + rarity: string; +} + +// Get all materials with their info +export function getAllMaterials(inventory: LootInventory): MaterialWithInfo[] { + return Object.entries(inventory.materials).map(([id, quantity]) => ({ + id, + quantity, + name: id, // Could be replaced with lookup from LOOT_DROPS + rarity: 'common', // Could be replaced with lookup + })); +} + +// Filter materials by minimum quantity +export function filterMaterialsByQuantity( + inventory: LootInventory, + minQuantity: number +): Record { + const filtered: Record = {}; + for (const [id, qty] of Object.entries(inventory.materials)) { + if (qty >= minQuantity) { + filtered[id] = qty; + } + } + return filtered; +} + +// ─── Crafting Availability ───────────────────────────────────────────────── + +// Check if can craft a recipe +export function canCraftFromInventory( + inventory: LootInventory, + recipe: CraftingRecipe, + currentMana: number +): { canCraft: boolean; reason?: string; missingMaterials?: Record; missingMana?: number } { + const { hasAll, missing } = checkMaterialsAvailable(inventory, recipe.materials); + + if (!hasAll) { + return { canCraft: false, missingMaterials: missing }; + } + + if (currentMana < recipe.manaCost) { + return { + canCraft: false, + missingMana: recipe.manaCost - currentMana, + reason: 'Insufficient mana', + }; + } + + if (!inventory.blueprints.includes(recipe.id)) { + return { canCraft: false, reason: 'Blueprint not acquired' }; + } + + return { canCraft: true }; +} + +// Get all craftable recipes from inventory +export function getCraftableRecipes( + inventory: LootInventory, + recipes: Record, + currentMana: number +): CraftingRecipe[] { + const craftable: CraftingRecipe[] = []; + + for (const blueprintId of inventory.blueprints) { + const recipe = recipes[blueprintId]; + if (!recipe) continue; + + const { canCraft } = canCraftFromInventory(inventory, recipe, currentMana); + if (canCraft) { + craftable.push(recipe); + } + } + + return craftable; +} + +// ─── Inventory Statistics ────────────────────────────────────────────────── + +export interface InventoryStats { + totalUniqueMaterials: number; + totalMaterialStacks: number; + totalBlueprints: number; + craftableItems: string[]; +} + +export function getInventoryStats(inventory: LootInventory): InventoryStats { + const totalUniqueMaterials = Object.keys(inventory.materials).length; + const totalMaterialStacks = Object.values(inventory.materials).reduce((sum, qty) => sum + qty, 0); + const totalBlueprints = inventory.blueprints.length; + + return { + totalUniqueMaterials, + totalMaterialStacks, + totalBlueprints, + craftableItems: [], // Would need recipes to compute + }; +} diff --git a/src/lib/game/crafting-prep.ts b/src/lib/game/crafting-prep.ts new file mode 100644 index 0000000..06c1acd --- /dev/null +++ b/src/lib/game/crafting-prep.ts @@ -0,0 +1,140 @@ +// ─── Crafting Preparation System ──────────────────────────────────────────── +// Preparation system functions extracted from crafting-slice.ts + +import type { EquipmentInstance, PreparationProgress } from './types'; +import { calculatePrepTime, calculatePrepManaCost, calculateManaPerHourForPrep } from './crafting-utils'; + +// ─── Preparation Validation ───────────────────────────────────────────────── + +// Check if an equipment instance can be prepared +export function canPrepareEquipment( + instance: EquipmentInstance | undefined, + currentTags: string[] +): { canPrepare: boolean; reason?: string } { + if (!instance) { + return { canPrepare: false, reason: 'Equipment instance not found' }; + } + + if (currentTags.includes('Ready for Enchantment')) { + return { canPrepare: false, reason: 'Equipment is already prepared for enchanting' }; + } + + return { canPrepare: true }; +} + +// Calculate preparation resource costs +export interface PreparationCosts { + time: number; + manaTotal: number; + manaPerHour: number; + manaPerTick: number; +} + +export function calculatePreparationCosts(totalCapacity: number): PreparationCosts { + const time = calculatePrepTime(totalCapacity); + const manaTotal = calculatePrepManaCost(totalCapacity); + const manaPerHour = calculateManaPerHourForPrep(totalCapacity, time); + const manaPerTick = manaPerHour * 0.04; // HOURS_PER_TICK + + return { time, manaTotal, manaPerHour, manaPerTick }; +} + +// ─── Preparation Progress ─────────────────────────────────────────────────── + +// Initialize preparation progress +export function initializePreparationProgress( + equipmentInstanceId: string, + totalCapacity: number, + manaCostPaid: number = 0 +): PreparationProgress { + const costs = calculatePreparationCosts(totalCapacity); + + return { + equipmentInstanceId, + progress: 0, + required: costs.time, + manaCostPaid, + }; +} + +// Calculate updated preparation progress after a tick +export interface PreparationTickResult { + progress: number; + manaCostPaid: number; + manaConsumed: number; + isComplete: boolean; +} + +export function calculatePreparationTick( + currentProgress: number, + required: number, + manaPerTick: number +): PreparationTickResult { + const progress = currentProgress + 0.04; // HOURS_PER_TICK + const manaConsumed = manaPerTick; + const manaCostPaid = manaPerTick; // Accumulated + + return { + progress, + manaCostPaid, + manaConsumed, + isComplete: progress >= required, + }; +} + +// ─── Preparation Completion ───────────────────────────────────────────────── + +// Apply preparation completion to equipment instance +export function completePreparation( + instance: EquipmentInstance, + manaSpent: number +): { + updatedInstance: EquipmentInstance; + manaRecovered: number; + logMessage: string; +} { + // Calculate mana recovery from disenchanting (disenchanting skill removed - Bug 13) + const disenchantLevel = 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); + } + + const updatedInstance: EquipmentInstance = { + ...instance, + enchantments: [], + usedCapacity: 0, + rarity: 'common', + tags: [...(instance.tags || []), 'Ready for Enchantment'], + }; + + return { + updatedInstance, + manaRecovered: totalRecovered, + logMessage: `✅ Equipment prepared for enchanting! Recovered ${totalRecovered} mana.`, + }; +} + +// Cancel preparation (no resource recovery for preparation itself) +export function cancelPreparation() { + return { + logMessage: 'Preparation cancelled.', + }; +} + +// ─── Preparation State Calculations ───────────────────────────────────────── + +export function getPreparationManaCostForTick(instance: EquipmentInstance): number { + const costs = calculatePreparationCosts(instance.totalCapacity); + return costs.manaPerTick; +} + +export function getPreparationRemainingTime(currentProgress: number, required: number): number { + return Math.max(0, required - currentProgress); +} + +export function getPreparationCompletionPercent(currentProgress: number, required: number): number { + return Math.min(100, Math.floor((currentProgress / required) * 100)); +} diff --git a/src/lib/game/crafting-slice.ts b/src/lib/game/crafting-slice.ts index 565c2ac..4183ce1 100755 --- a/src/lib/game/crafting-slice.ts +++ b/src/lib/game/crafting-slice.ts @@ -1,8 +1,10 @@ -// ─── Crafting Store Slice ───────────────────────────────────────────────────────── -// Handles equipment and enchantment system: design, prepare, apply stages +// ─── Crafting Store Slice ───────────────────────────────────────────────────── +// Core slice logic for equipment and enchantment system. Extracted logic lives +// in focused modules: crafting-utils, crafting-design, crafting-prep, +// crafting-apply, crafting-equipment, crafting-loot, crafting-attunements. import type { GameState, EquipmentInstance, AppliedEnchantment, EnchantmentDesign, DesignEffect, EquipmentCraftingProgress, LootInventory, AttunementState } from './types'; -import { EQUIPMENT_TYPES, type EquipmentCategory, type EquipmentSlot } from './data/equipment'; +import { EQUIPMENT_TYPES, type EquipmentSlot } from './data/equipment'; import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects'; import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/crafting-recipes'; import { SPELLS_DEF } from './constants'; @@ -11,122 +13,34 @@ import { computeEffects } from './upgrade-effects'; import { hasSpecial, SPECIAL_EFFECTS } from './special-effects'; import type { ComputedEffects } from './upgrade-effects.types'; -// ─── Helper Functions ───────────────────────────────────────────────────────── +// ─── Crafting Modules ─────────────────────────────────────────────────────── -function generateInstanceId(): string { - return `equip_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; -} +import * as CraftingUtils from './crafting-utils'; +import * as CraftingDesign from './crafting-design'; +import * as CraftingPrep from './crafting-prep'; +import * as CraftingApply from './crafting-apply'; +import * as CraftingEquipment from './crafting-equipment'; +import * as CraftingLoot from './crafting-loot'; +import * as CraftingAttunements from './crafting-attunements'; +import * as CraftingActions from './crafting-actions'; -// Get equipment category from type -function getEquipmentCategory(typeId: string): EquipmentCategory | null { - const type = EQUIPMENT_TYPES[typeId]; - return type?.category || null; -} +// ─── Initial Equipment Setup ───────────────────────────────────────────────── -// 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(); +export function createStartingEquipment() { + const staffId = CraftingUtils.generateInstanceId(); const staffInstance: EquipmentInstance = { instanceId: staffId, typeId: 'basicStaff', name: 'Basic Staff', - enchantments: [ - { effectId: 'spell_manaBolt', stacks: 1, actualCost: 50 } - ], + enchantments: [{ effectId: 'spell_manaBolt', stacks: 1, actualCost: 50 }], usedCapacity: 50, totalCapacity: 50, rarity: 'common', quality: 100, + tags: [], }; - // Create starting clothes - const shirtId = generateInstanceId(); + const shirtId = CraftingUtils.generateInstanceId(); const shirtInstance: EquipmentInstance = { instanceId: shirtId, typeId: 'civilianShirt', @@ -136,9 +50,10 @@ export function createStartingEquipment(): { totalCapacity: 30, rarity: 'common', quality: 100, + tags: [], }; - const shoesId = generateInstanceId(); + const shoesId = CraftingUtils.generateInstanceId(); const shoesInstance: EquipmentInstance = { instanceId: shoesId, typeId: 'civilianShoes', @@ -148,6 +63,7 @@ export function createStartingEquipment(): { totalCapacity: 15, rarity: 'common', quality: 100, + tags: [], }; return { @@ -169,532 +85,66 @@ export function createStartingEquipment(): { }; } -// ─── Crafting Store Extensions ───────────────────────────────────────────────── +// ─── Crafting Actions Interface ───────────────────────────────────────────── + +export type CraftingActions = { + createEquipmentInstance: (typeId: string) => string | null; + equipItem: (instanceId: string, slot: EquipmentSlot) => boolean; + unequipItem: (slot: EquipmentSlot) => void; + deleteEquipmentInstance: (instanceId: string) => void; + startDesigningEnchantment: (name: string, equipmentTypeId: string, effects: DesignEffect[]) => boolean; + cancelDesign: () => void; + saveDesign: (design: EnchantmentDesign) => void; + deleteDesign: (designId: string) => void; + startPreparing: (equipmentInstanceId: string) => boolean; + cancelPreparation: () => void; + startApplying: (equipmentInstanceId: string, designId: string) => boolean; + pauseApplication: () => void; + resumeApplication: () => void; + cancelApplication: () => void; + disenchantEquipment: (instanceId: string) => void; + startCraftingEquipment: (blueprintId: string) => boolean; + cancelEquipmentCrafting: () => void; + deleteMaterial: (materialId: string, amount: number) => void; + getEquipmentSpells: () => string[]; + getEquipmentEffects: () => Record; + getAvailableCapacity: (instanceId: string) => number; +}; + +// ─── Crafting Store Slice ─────────────────────────────────────────────────── 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 - - // 2-handed weapon checks - const isTwoHanded = type.twoHanded === true; - - if (isTwoHanded) { - // Cannot equip 2-handed weapon if main hand or offhand is occupied - if (state.equippedInstances.mainHand || state.equippedInstances.offHand) { - return false; - } - // 2-handed weapons can only be equipped to main hand - if (slot !== 'mainHand') return false; - } - - // If equipping to main hand, check if a 2-handed weapon is in main hand (block offhand) - if (slot === 'offHand' && state.equippedInstances.mainHand) { - const mainHandType = EQUIPMENT_TYPES[state.equippedInstances.mainHand]; - if (mainHandType?.twoHanded) { - return false; // Cannot equip offhand when 2-handed weapon is equipped - } - } - - // If equipping to offhand, check if a 2-handed weapon is in main hand - if (slot === 'offHand' && state.equippedInstances.mainHand) { - const mainHandType = EQUIPMENT_TYPES[state.equippedInstances.mainHand]; - if (mainHandType?.twoHanded) { - return false; // Cannot equip offhand when 2-handed weapon is in main hand - } - } - - // 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; - - // If 2-handed weapon, also clear offhand slot (should already be null from check above) - if (isTwoHanded && slot === 'mainHand') { - newEquipped.offHand = null; - } - - 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; - - // Get equipment type and category - const equipType = EQUIPMENT_TYPES[equipmentTypeId]; - if (!equipType) return false; - - const category = equipType.category; - 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); - - // Validate capacity - design must fit within equipment capacity - if (totalCapacityCost > equipType.baseCapacity) { - return false; // Design exceeds equipment capacity - } - - // Create design ID - const designId = `design_${Date.now()}`; - const designTime = calculateDesignTime(effects); - - // Check for ENCHANT_MASTERY: allow 2 concurrent designs - const hasEnchantMastery = hasSpecial( - computeEffects(state.skillUpgrades || {}, state.skillTiers || {}), - SPECIAL_EFFECTS.ENCHANT_MASTERY - ); - - // Determine which design slot to use - let updates: any = {}; - - if (!state.designProgress) { - // First slot is free - updates = { - currentAction: 'design', - designProgress: { - designId, - progress: 0, - required: designTime, - name, - equipmentType: equipmentTypeId, - effects, - }, - }; - } else if (hasEnchantMastery && !state.designProgress2) { - // Second slot available with ENCHANT_MASTERY - updates = { - designProgress2: { - designId, - progress: 0, - required: designTime, - name, - equipmentType: equipmentTypeId, - effects, - }, - }; - } else { - return false; // No slot available - } - - set(() => updates); - return true; - }, - - cancelDesign: () => { - const state = get(); - // Check if cancelling designProgress2 - if (state.designProgress2 && !state.designProgress) { - set(() => ({ - designProgress2: null, - })); - } else { - set(() => ({ - currentAction: 'meditate', - designProgress: null, - })); - } - }, - - saveDesign: (design: EnchantmentDesign) => { - const state = get(); - // Check if saving from designProgress2 - if (state.designProgress2 && state.designProgress2.designId === design.id) { - set((state) => ({ - enchantmentDesigns: [...state.enchantmentDesigns, design], - designProgress2: null, - })); - } else { - 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; - - // Don't allow preparing an item that's already prepared - if (instance.tags?.includes('Ready for Enchantment')) 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 if equipment is ready for enchantment - if (!instance.tags?.includes('Ready for Enchantment')) { - 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 = 0; // disenchanting skill removed (Bug 13) - 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)], - }; - }); - }, + createEquipmentInstance: (typeId) => CraftingActions.createEquipmentInstance(typeId, set), + equipItem: (instanceId, slot) => CraftingActions.equipItem(instanceId, slot, get, set), + unequipItem: (slot) => CraftingActions.unequipItem(slot, set), + deleteEquipmentInstance: (instanceId) => CraftingActions.deleteEquipmentInstance(instanceId, get, set), + startDesigningEnchantment: (name, equipmentTypeId, effects) => + CraftingActions.startDesigningEnchantment(name, equipmentTypeId, effects, get, set), + cancelDesign: () => CraftingActions.cancelDesign(get, set), + saveDesign: (design) => CraftingActions.saveDesign(design, get, set), + deleteDesign: (designId) => CraftingActions.deleteDesign(designId, set), + startPreparing: (equipmentInstanceId) => CraftingActions.startPreparing(equipmentInstanceId, get, set), + cancelPreparation: () => CraftingActions.cancelPreparation(set), + startApplying: (equipmentInstanceId, designId) => + CraftingActions.startApplying(equipmentInstanceId, designId, get, set), + pauseApplication: () => CraftingActions.pauseApplication(get, set), + resumeApplication: () => CraftingActions.resumeApplication(get, set), + cancelApplication: () => CraftingActions.cancelApplication(set), + disenchantEquipment: (instanceId) => CraftingActions.disenchantEquipment(instanceId, get, set), + startCraftingEquipment: (blueprintId) => CraftingActions.startCraftingEquipment(blueprintId, get, set), + cancelEquipmentCrafting: () => CraftingActions.cancelEquipmentCrafting(get, set), + deleteMaterial: (materialId, amount) => CraftingActions.deleteMaterial(materialId, amount, get, set), + getEquipmentSpells: () => CraftingActions.getEquipmentSpells(get), + getEquipmentEffects: () => CraftingActions.getEquipmentEffects(get), + getAvailableCapacity: (instanceId) => CraftingActions.getAvailableCapacity(instanceId, get), }; } -// ─── Tick Processing for Crafting ───────────────────────────────────────────── +// ─── Tick Processing for Crafting ──────────────────────────────────────────── export function processCraftingTick( state: GameState, @@ -703,166 +153,121 @@ export function processCraftingTick( const { rawMana, log } = effects; let updates: Partial = {}; - // Get computed effects for special effect checks const computedEffects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {}); - // Process design progress + // Process design progress (slot 1) if (state.currentAction === 'design' && state.designProgress) { - // Check for INSTANT_DESIGNS special effect (10% chance instant completion) - let progress = state.designProgress.progress + 0.04; // HOURS_PER_TICK - - // HASTY_ENCHANTER: +25% speed for repeat designs - if (hasSpecial(computedEffects, SPECIAL_EFFECTS.HASTY_ENCHANTER)) { - const designId = state.designProgress.designId; - const isRepeatDesign = state.enchantmentDesigns.some(d => d.equipmentType === state.designProgress?.equipmentType); - if (isRepeatDesign) { - progress += 0.04 * 0.25; // +25% speed - } - } - - // INSTANT_DESIGNS: 10% chance of instant completion - if (hasSpecial(computedEffects, SPECIAL_EFFECTS.INSTANT_DESIGNS) && Math.random() < 0.10) { - progress = state.designProgress.required; - } - - if (progress >= state.designProgress.required) { - // Design complete - auto-save the design using stored data - const dp = state.designProgress; - const efficiencyBonus = (state.skills.efficientEnchant || 0) * 0.05; - const totalCapacityCost = calculateDesignCapacityCost(dp.effects, efficiencyBonus); - - const completedDesign: EnchantmentDesign = { - id: dp.designId, - name: dp.name, - equipmentType: dp.equipmentType, - effects: dp.effects, - totalCapacityUsed: totalCapacityCost, - designTime: dp.required, - created: Date.now(), - }; - + const designResult = CraftingDesign.calculateDesignProgress( + state.designProgress.progress, + state.designProgress.required, + computedEffects, + false + ); + + if (designResult.isComplete) { + const completedDesign = CraftingDesign.createCompletedDesignFromProgress( + { + designId: state.designProgress.designId, + name: state.designProgress.name, + equipmentType: state.designProgress.equipmentType, + effects: state.designProgress.effects, + required: state.designProgress.required, + }, + ((state.skillUpgrades || {})['efficientEnchant'] || []).length * 0.05 + ); updates = { ...updates, designProgress: null, - currentAction: 'meditate', + currentAction: 'meditate' as const, enchantmentDesigns: [...state.enchantmentDesigns, completedDesign], - log: [`✅ Enchantment design "${dp.name}" complete!`, ...log], + log: [`✅ Enchantment design "${completedDesign.name}" complete!`, ...log], }; } else { updates = { ...updates, - designProgress: { - ...state.designProgress, - progress, - }, + designProgress: { ...state.designProgress, progress: designResult.progress }, }; } } - // Process second design progress (for ENCHANT_MASTERY) + // Process second design progress (slot 2) if (state.designProgress2) { - let progress2 = state.designProgress2.progress + 0.04; // HOURS_PER_TICK - - // HASTY_ENCHANTER: +25% speed for repeat designs - if (hasSpecial(computedEffects, SPECIAL_EFFECTS.HASTY_ENCHANTER)) { - const isRepeatDesign = state.enchantmentDesigns.some(d => d.equipmentType === state.designProgress2?.equipmentType); - if (isRepeatDesign) { - progress2 += 0.04 * 0.25; // +25% speed - } - } - - // INSTANT_DESIGNS: 10% chance of instant completion - if (hasSpecial(computedEffects, SPECIAL_EFFECTS.INSTANT_DESIGNS) && Math.random() < 0.10) { - progress2 = state.designProgress2.required; - } - - if (progress2 >= state.designProgress2.required) { - // Design complete - auto-save the design using stored data - const dp = state.designProgress2; - const efficiencyBonus = (state.skills.efficientEnchant || 0) * 0.05; - const totalCapacityCost = calculateDesignCapacityCost(dp.effects, efficiencyBonus); - - const completedDesign: EnchantmentDesign = { - id: dp.designId, - name: dp.name, - equipmentType: dp.equipmentType, - effects: dp.effects, - totalCapacityUsed: totalCapacityCost, - designTime: dp.required, - created: Date.now(), - }; - - // If the first design slot is also null, transition to meditate + const designResult2 = CraftingDesign.calculateSecondDesignProgress( + state.designProgress2.progress, + state.designProgress2.required, + computedEffects, + false + ); + if (designResult2.isComplete) { + const completedDesign = CraftingDesign.createCompletedDesignFromProgress( + { + designId: state.designProgress2.designId, + name: state.designProgress2.name, + equipmentType: state.designProgress2.equipmentType, + effects: state.designProgress2.effects, + required: state.designProgress2.required, + }, + ((state.skillUpgrades || {})['efficientEnchant'] || []).length * 0.05 + ); const shouldTransitionToMeditate = !state.designProgress; updates = { ...updates, designProgress2: null, - currentAction: shouldTransitionToMeditate ? 'meditate' : state.currentAction, + currentAction: shouldTransitionToMeditate ? 'meditate' as const : state.currentAction, enchantmentDesigns: [...state.enchantmentDesigns, completedDesign], - log: [`✅ Enchantment design "${dp.name}" complete! (2nd slot)`, ...log], + log: [`✅ Enchantment design "${completedDesign.name}" complete! (2nd slot)`, ...log], }; } else { updates = { ...updates, - designProgress2: { - ...state.designProgress2, - progress: progress2, - }, + designProgress2: { ...state.designProgress2, progress: designResult2.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 + const instance = state.equipmentInstances[state.preparationProgress.equipmentInstanceId]; + const manaPerTick = instance ? CraftingPrep.getPreparationManaCostForTick(instance) : 0; - if (rawMana >= manaCost) { - const progress = prep.progress + 0.04; - const manaCostPaid = prep.manaCostPaid + manaCost; + if (rawMana >= manaPerTick) { + const tickResult = CraftingPrep.calculatePreparationTick( + state.preparationProgress.progress, + state.preparationProgress.required, + manaPerTick + ); - if (progress >= prep.required) { - // Preparation complete - clear enchantments, add tag, and recover some mana - const instance = state.equipmentInstances[prep.equipmentInstanceId]; - let totalRecovered = 0; - + if (tickResult.isComplete) { if (instance) { - // Calculate mana recovery - disenchanting skill removed (Bug 13) - const disenchantLevel = 0; - const recoveryRate = 0.1 + disenchantLevel * 0.2; // 10% base + 20% per level - for (const ench of instance.enchantments) { - totalRecovered += Math.floor(ench.actualCost * recoveryRate); - } - } - - updates = { - ...updates, - rawMana: rawMana - manaCost + totalRecovered, - preparationProgress: null, - currentAction: 'meditate', - equipmentInstances: instance ? { - ...state.equipmentInstances, - [instance.instanceId]: { - ...instance, - enchantments: [], - usedCapacity: 0, - rarity: 'common', - tags: [...(instance.tags || []), 'Ready for Enchantment'], + const completeResult = CraftingPrep.completePreparation(instance, state.preparationProgress.manaCostPaid); + updates = { + ...updates, + rawMana: rawMana - tickResult.manaConsumed + completeResult.manaRecovered, + preparationProgress: null, + currentAction: 'meditate' as const, + equipmentInstances: { + ...state.equipmentInstances, + [instance.instanceId]: completeResult.updatedInstance, }, - } : state.equipmentInstances, - log: [`✅ Equipment prepared for enchanting! Recovered ${totalRecovered} mana.`, ...log], - }; + log: [completeResult.logMessage, ...log], + }; + } else { + updates = { + ...updates, + preparationProgress: null, + currentAction: 'meditate' as const, + rawMana: rawMana - tickResult.manaConsumed, + log: ['✅ Preparation complete!', ...log], + }; + } } else { updates = { ...updates, - rawMana: rawMana - manaCost, + rawMana: rawMana - tickResult.manaConsumed, preparationProgress: { - ...prep, - progress, - manaCostPaid, + ...state.preparationProgress, + progress: tickResult.progress, + manaCostPaid: tickResult.manaCostPaid, }, }; } @@ -872,115 +277,44 @@ export function processCraftingTick( // 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 + const manaPerTick = CraftingApply.getApplicationManaCostForTick(app.manaPerHour); - if (rawMana >= manaCost) { - let progress = app.progress + 0.04; - const manaSpent = app.manaSpent + manaCost; + if (rawMana >= manaPerTick) { + const tickResult = CraftingApply.calculateApplicationTick( + app.progress, + app.required, + app.manaSpent, + manaPerTick, + computedEffects + ); - // Check for free enchantment chances - // ENCHANT_PRESERVATION: 25% chance free enchant - // THRIFTY_ENCHANTER: +10% chance free enchantment - // OPTIMIZED_ENCHANTING: +25% chance free enchantment - let freeEnchantChance = 0; - if (hasSpecial(computedEffects, SPECIAL_EFFECTS.ENCHANT_PRESERVATION)) { - freeEnchantChance += 0.25; - } - if (hasSpecial(computedEffects, SPECIAL_EFFECTS.THRIFTY_ENCHANTER)) { - freeEnchantChance += 0.10; - } - if (hasSpecial(computedEffects, SPECIAL_EFFECTS.OPTIMIZED_ENCHANTING)) { - freeEnchantChance += 0.25; - } - - // If free enchant triggers, complete instantly - if (freeEnchantChance > 0 && Math.random() < freeEnchantChance) { - progress = app.required; - } - - if (progress >= app.required) { - // Apply the enchantment! + if (tickResult.isComplete) { const instance = state.equipmentInstances[app.equipmentInstanceId]; const design = state.enchantmentDesigns.find(d => d.id === app.designId); - if (instance && design) { - // PURE_ESSENCE: +25% power for tier 1 enchants - const isPureEssenceActive = hasSpecial(computedEffects, SPECIAL_EFFECTS.PURE_ESSENCE); - - const newEnchantments: AppliedEnchantment[] = design.effects.map(eff => { - let stacks = eff.stacks; - let actualCost = eff.capacityCost; - - // Check if this is a tier 1 enchantment (heuristic: baseCapacityCost < 100) - const effectDef = ENCHANTMENT_EFFECTS[eff.effectId]; - if (isPureEssenceActive && effectDef && effectDef.baseCapacityCost < 100) { - // +25% power = increase stacks by 25% (rounded up) - stacks = Math.ceil(stacks * 1.25); - } - - return { - effectId: eff.effectId, - stacks, - actualCost, - }; - }); - - // 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, - }, - }; - } - + const applyResult = CraftingApply.applyEnchantments(instance, design, computedEffects); + const xpGain = CraftingAttunements.gainEnchantingXP(state.attunements, applyResult.xpGained); updates = { ...updates, - rawMana: rawMana - manaCost, + rawMana: rawMana - tickResult.manaConsumed, applicationProgress: null, - currentAction: 'meditate', - attunements: newAttunements, + currentAction: 'meditate' as const, + attunements: { + ...state.attunements, + enchanter: xpGain.attunements.enchanter, + }, equipmentInstances: { ...state.equipmentInstances, - [app.equipmentInstanceId]: { - ...instance, - enchantments: [...instance.enchantments, ...newEnchantments], - usedCapacity: instance.usedCapacity + design.totalCapacityUsed, - }, + [instance.instanceId]: applyResult.updatedInstance, }, - log: [`✨ Enchantment "${design.name}" applied to ${instance.name}! (+${xpGained} Enchanter XP)`, ...log], + log: [applyResult.logMessage, ...log], }; } } else { updates = { ...updates, - rawMana: rawMana - manaCost, - applicationProgress: { - ...app, - progress, - manaSpent, - }, + rawMana: rawMana - tickResult.manaConsumed, + applicationProgress: { ...app, progress: tickResult.progress, manaSpent: tickResult.manaSpent }, }; } } @@ -989,52 +323,35 @@ export function processCraftingTick( // 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, - }; + const tickResult = CraftingEquipment.calculateCraftingTick(craft.progress, craft.required); + if (tickResult.isComplete) { + const recipe = CraftingEquipment.getRecipe(craft.blueprintId); + if (recipe) { + const craftResult = CraftingEquipment.completeEquipmentCrafting(craft.blueprintId, recipe); updates = { ...updates, equipmentCraftingProgress: null, - currentAction: 'meditate', + currentAction: 'meditate' as const, equipmentInstances: { ...state.equipmentInstances, - [instanceId]: newInstance, + [craftResult.instanceId]: craftResult.instance, }, totalCraftsCompleted: (state.totalCraftsCompleted || 0) + 1, - log: [`🔨 Crafted ${recipe.name}!`, ...log], + log: [craftResult.logMessage, ...log], }; } else { updates = { ...updates, equipmentCraftingProgress: null, - currentAction: 'meditate', + currentAction: 'meditate' as const, log: ['⚠️ Crafting failed - invalid recipe!', ...log], }; } } else { updates = { ...updates, - equipmentCraftingProgress: { - ...craft, - progress, - }, + equipmentCraftingProgress: { ...craft, progress: tickResult.progress }, }; } } @@ -1042,16 +359,14 @@ export function processCraftingTick( return updates; } -// ─── Export helper to get equipment instance spells ───────────────────────────── +// ─── 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) { @@ -1059,6 +374,5 @@ export function getSpellsFromEquipment(instances: Record total + calculateEffectCapacityCost(eff.effectId, eff.stacks, efficiencyBonus), 0); +} + +export function getAvailableCapacity(instance: EquipmentInstance): number { + return instance.totalCapacity - instance.usedCapacity; +} + +export function designFitsInEquipment(design: EnchantmentDesign, instance: EquipmentInstance): boolean { + return instance.usedCapacity + design.totalCapacityUsed <= instance.totalCapacity; +} + +// ─── Time Calculation Helpers ──────────────────────────────────────────────── + +export function calculateDesignTime(effects: DesignEffect[]): number { + let time = 1; + for (const eff of effects) { + const effectDef = ENCHANTMENT_EFFECTS[eff.effectId]; + if (effectDef) { + time += 0.5 * eff.stacks; + } + } + return time; +} + +export function calculatePrepTime(equipmentCapacity: number): number { + return 2 + Math.floor(equipmentCapacity / 50); +} + +export function calculateApplicationTime(design: EnchantmentDesign): number { + return 2 + design.effects.reduce((total, eff) => total + eff.stacks, 0); +} + +// ─── Mana Cost Calculation Helpers ─────────────────────────────────────────── + +export function calculatePrepManaCost(equipmentCapacity: number): number { + return equipmentCapacity * 10; +} + +export function calculateApplicationManaPerHour(design: EnchantmentDesign): number { + return 20 + design.effects.reduce((total, eff) => total + eff.stacks * 5, 0); +} + +export function calculateManaPerHourForPrep(equipmentCapacity: number, prepTime: number): number { + return calculatePrepManaCost(equipmentCapacity) / prepTime; +} + +// ─── Recipe & Crafting Helpers ─────────────────────────────────────────────── + +export function checkRecipeMaterials( + recipe: CraftingRecipe, + materials: Record +): { canCraft: boolean; missingMaterials: Record } { + const missingMaterials: Record = {}; + let canCraft = true; + + for (const [matId, amount] of Object.entries(recipe.materials)) { + const have = materials[matId] || 0; + if (have < amount) { + missingMaterials[matId] = amount - have; + canCraft = false; + } + } + + return { canCraft, missingMaterials }; +} + +export function deductRecipeMaterials( + recipe: CraftingRecipe, + materials: Record +): Record { + const newMaterials = { ...materials }; + for (const [matId, amount] of Object.entries(recipe.materials)) { + newMaterials[matId] = (newMaterials[matId] || 0) - amount; + if (newMaterials[matId] <= 0) { + delete newMaterials[matId]; + } + } + return newMaterials; +} + +export function refundCraftMaterials(recipe: CraftingRecipe, refundRate: number = 0.5): Record { + const refunds: Record = {}; + for (const [matId, amount] of Object.entries(recipe.materials)) { + refunds[matId] = Math.floor(amount * refundRate); + } + return refunds; +} + +// ─── Validation Helpers ────────────────────────────────────────────────────── + +export function canEquipInSlot( + instance: EquipmentInstance, + slot: EquipmentSlot, + currentlyEquipped: Record +): boolean { + const type = EQUIPMENT_TYPES[instance.typeId]; + if (!type) return false; + + const validSlots = type.category === 'accessory' + ? ['accessory1', 'accessory2'] + : [type.slot]; + + if (!validSlots.includes(slot)) return false; + if (currentlyEquipped[slot] === instance.instanceId) return true; + + const isTwoHanded = type.twoHanded === true; + + if (isTwoHanded) { + if (currentlyEquipped.mainHand || currentlyEquipped.offHand) { + return false; + } + if (slot !== 'mainHand') return false; + } + + if (slot === 'offHand' && currentlyEquipped.mainHand) { + const mainHandType = EQUIPMENT_TYPES[currentlyEquipped.mainHand]; + if (mainHandType?.twoHanded) { + return false; + } + } + + return true; +} + +export function isTwoHanded(typeId: string): boolean { + return EQUIPMENT_TYPES[typeId]?.twoHanded === true; +} diff --git a/src/lib/game/data/equipment.ts b/src/lib/game/data/equipment.ts index efc32c2..ee16422 100755 --- a/src/lib/game/data/equipment.ts +++ b/src/lib/game/data/equipment.ts @@ -6,6 +6,18 @@ export type EquipmentCategory = 'caster' | 'shield' | 'catalyst' | 'sword' | 'he // All equipment slots in order export const EQUIPMENT_SLOTS: EquipmentSlot[] = ['mainHand', 'offHand', 'head', 'body', 'hands', 'feet', 'accessory1', 'accessory2']; +// Human-readable names for equipment slots +export const SLOT_NAMES: Record = { + mainHand: 'Main Hand', + offHand: 'Off Hand', + head: 'Head', + body: 'Body', + hands: 'Hands', + feet: 'Feet', + accessory1: 'Accessory 1', + accessory2: 'Accessory 2', +}; + export interface EquipmentType { id: string; name: string; diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts index 49a6287..fb45736 100755 --- a/src/lib/game/types.ts +++ b/src/lib/game/types.ts @@ -1,64 +1,70 @@ -import type { GameStore } from '@/lib/game/store'; -import type { SPELLS } from '@/lib/game/constants'; -import type { EquipmentSpellState } from '@/lib/game/state'; -import type { RoomType, ActivityLogEntry } from '@/lib/game/types/game'; +// ─── Game Types (Barrel Re-Exports) ────────────────────────────────────────── +// Re-exports all core game types from the types/ subdirectory -// Re-export ActivityLogEntry for convenience -export { ActivityLogEntry }; +// ─── Core Game Types (re-exported from types/ subdirectory) ───────────────── -// Room Display Props -export interface RoomDisplayProps { - roomType: RoomType; - roomConfig: { label: string; icon: string; color: string }; - primaryEnemy: any; - swarmEnemies: any[]; - puzzleId?: string; - puzzleProgress?: number; - simpleMode: boolean; - floorElemDef: any; - floorHP: number; - floorMaxHP: number; - totalDPS: number; - currentAction: string | null; - activeEquipmentSpells: Array<{ spellId: string; equipmentId: string }>; -} +export type { + ElementCategory, + ElementDef, + ElementState, +} from './types/elements'; -// Floor Controls Props -export interface FloorControlsProps { - store: GameStore; - climbDirection: 'up' | 'down' | null; - isGuardianFloor: boolean; - currentRoom: any; - currentGuardian: any; - isFloorCleared: boolean; - floorElemDef: any; - roomType: RoomType; - roomConfig: { label: string; icon: string; color: string }; - activeEquipmentSpells: Array<{ spellId: string; equipmentId: string }>; - upgradeEffects: any; - floorElem: string; - totalDPS: number; - getEnemyName: typeof import('@/lib/game/store').getEnemyName; - calcDamage: typeof import('@/lib/game/store').calcDamage; - SPELLS_DEF: typeof import('@/lib/game/constants').SPELLS_DEF; - canCastSpell: (spellId: string) => boolean; - storeCurrentAction: string | null; - handleClimb: (direction: 'up' | 'down') => void; - formatSpellCost: typeof import('@/lib/game/formatting').formatSpellCost; - getSpellCostColor: typeof import('@/lib/game/formatting').getSpellCostColor; -} +export type { + AttunementSlot, + AttunementDef, + AttunementState, + GuardianBoon, + GuardianDef, +} from './types/attunements'; -// Combat Stats Panel Props -export interface CombatStatsPanelProps { - activeEquipmentSpells: Array<{ spellId: string; equipmentId: string }>; - store: GameStore; - totalDPS: number; - calcDamage: typeof import('@/lib/game/store').calcDamage; - formatSpellCost: typeof import('@/lib/game/formatting').formatSpellCost; - getSpellCostColor: typeof import('@/lib/game/formatting').getSpellCostColor; - SPELLS_DEF: typeof import('@/lib/game/constants').SPELLS_DEF; - upgradeEffects: any; - canCastSpell: (spellId: string) => boolean; - studySpeedMult: number; - storeCurrentAction: string | null; -} +export type { + SpellCost, + SpellDef, + SpellEffect, + SpellState, +} from './types/spells'; + +export type { + SkillDef, + SkillUpgradeDef, + SkillUpgradeEffect, + SkillEvolutionPath, + SkillTierDef, + SkillPerkChoice, + SkillUpgradeChoice, + PrestigeDef, + SkillCost, +} from './types/skills'; + +export type { + EquipmentDef, + EquipmentInstance, + AppliedEnchantment, + EnchantmentDesign, + DesignEffect, + DesignProgress, + PreparationProgress, + ApplicationProgress, + EquipmentCraftingProgress, + EquipmentSpellState, + BlueprintDef, + LootInventory, +} from './types/equipment'; + +export type { + RoomType, + EnemyState, + FloorState, + AchievementDef, + AchievementState, + GameAction, + ScheduleBlock, + StudyTarget, + SummonedGolem, + GolemancyState, + GameState, + GameActionType, + ActivityEventType, + ActivityLogEntry, + EquipmentSpellState, +} from './types/game'; diff --git a/src/lib/game/types/attunements.ts b/src/lib/game/types/attunements.ts index aabc992..d22ad21 100644 --- a/src/lib/game/types/attunements.ts +++ b/src/lib/game/types/attunements.ts @@ -26,6 +26,7 @@ export interface AttunementState { active: boolean; // Whether this attunement is currently active level: number; // Attunement level (for future progression) experience: number; // Progress toward next level + title?: string; // Title based on level (e.g., 'Novice Enchanter') } // Boon types that guardians can grant