From 1d2dce75cc86e63aa10585401692cfffab137368 Mon Sep 17 00:00:00 2001 From: zhipu Date: Thu, 26 Mar 2026 11:04:50 +0000 Subject: [PATCH] Add equipment crafting system - Add crafting-recipes.ts with blueprint definitions and material requirements - Update crafting-slice.ts with equipment crafting functions - Add EquipmentCraftingProgress type and state - Update CraftingTab.tsx with new Craft tab for equipment crafting - Add material deletion functionality - Update store.ts with equipment crafting methods - Update page.tsx to pass new props to CraftingTab Features: - Players can craft equipment from discovered blueprints - Crafting requires materials and mana - Materials are obtained from loot drops - New Craft tab in the crafting interface - Shows blueprint details and material requirements --- src/app/page.tsx | 5 + src/components/game/tabs/CraftingTab.tsx | 217 ++++++++++++++++++- src/lib/game/crafting-slice.ts | 152 +++++++++++++- src/lib/game/data/crafting-recipes.ts | 257 +++++++++++++++++++++++ src/lib/game/store.ts | 98 ++++++++- src/lib/game/types.ts | 10 + 6 files changed, 734 insertions(+), 5 deletions(-) create mode 100644 src/lib/game/data/crafting-recipes.ts diff --git a/src/app/page.tsx b/src/app/page.tsx index cddd501..cb75679 100755 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2341,10 +2341,12 @@ export default function ManaLoopGame() { designProgress={store.designProgress} preparationProgress={store.preparationProgress} applicationProgress={store.applicationProgress} + equipmentCraftingProgress={store.equipmentCraftingProgress} rawMana={store.rawMana} skills={store.skills} currentAction={store.currentAction} unlockedEffects={store.unlockedEffects || ['spell_manaBolt']} + lootInventory={store.lootInventory} startDesigningEnchantment={store.startDesigningEnchantment} cancelDesign={store.cancelDesign} saveDesign={store.saveDesign} @@ -2357,6 +2359,9 @@ export default function ManaLoopGame() { cancelApplication={store.cancelApplication} disenchantEquipment={store.disenchantEquipment} getAvailableCapacity={store.getAvailableCapacity} + startCraftingEquipment={store.startCraftingEquipment} + cancelEquipmentCrafting={store.cancelEquipmentCrafting} + deleteMaterial={store.deleteMaterial} /> diff --git a/src/components/game/tabs/CraftingTab.tsx b/src/components/game/tabs/CraftingTab.tsx index bd1b36a..fff40dc 100755 --- a/src/components/game/tabs/CraftingTab.tsx +++ b/src/components/game/tabs/CraftingTab.tsx @@ -10,11 +10,13 @@ import { Separator } from '@/components/ui/separator'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Wand2, Scroll, Hammer, Sparkles, Trash2, Plus, Minus, - Package, Zap, Clock, ChevronRight, Circle + Package, Zap, Clock, ChevronRight, Circle, Anvil } from 'lucide-react'; import { EQUIPMENT_TYPES, type EquipmentType, type EquipmentSlot } from '@/lib/game/data/equipment'; import { ENCHANTMENT_EFFECTS, type EnchantmentEffectDef, calculateEffectCapacityCost } from '@/lib/game/data/enchantment-effects'; -import type { EquipmentInstance, EnchantmentDesign, DesignEffect, AppliedEnchantment } from '@/lib/game/types'; +import { CRAFTING_RECIPES, canCraftRecipe } from '@/lib/game/data/crafting-recipes'; +import { LOOT_DROPS, RARITY_COLORS } from '@/lib/game/data/loot-drops'; +import type { EquipmentInstance, EnchantmentDesign, DesignEffect, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types'; import { fmt } from '@/lib/game/store'; // Slot display names @@ -39,6 +41,7 @@ interface CraftingTabProps { designProgress: { designId: string; progress: number; required: number } | null; preparationProgress: { equipmentInstanceId: string; progress: number; required: number; manaCostPaid: number } | null; applicationProgress: { equipmentInstanceId: string; designId: string; progress: number; required: number; manaPerHour: number; paused: boolean; manaSpent: number } | null; + equipmentCraftingProgress: EquipmentCraftingProgress | null; // Player state rawMana: number; @@ -46,6 +49,9 @@ interface CraftingTabProps { currentAction: string; unlockedEffects: string[]; // Effect IDs that have been researched + // Loot inventory + lootInventory: LootInventory; + // Actions startDesigningEnchantment: (name: string, equipmentTypeId: string, effects: DesignEffect[]) => boolean; cancelDesign: () => void; @@ -59,6 +65,11 @@ interface CraftingTabProps { cancelApplication: () => void; disenchantEquipment: (instanceId: string) => void; getAvailableCapacity: (instanceId: string) => number; + + // Equipment crafting actions + startCraftingEquipment: (blueprintId: string) => boolean; + cancelEquipmentCrafting: () => void; + deleteMaterial: (materialId: string, amount: number) => void; } export function CraftingTab({ @@ -68,10 +79,12 @@ export function CraftingTab({ designProgress, preparationProgress, applicationProgress, + equipmentCraftingProgress, rawMana, skills, currentAction, unlockedEffects, + lootInventory, startDesigningEnchantment, cancelDesign, saveDesign, @@ -84,8 +97,11 @@ export function CraftingTab({ cancelApplication, disenchantEquipment, getAvailableCapacity, + startCraftingEquipment, + cancelEquipmentCrafting, + deleteMaterial, }: CraftingTabProps) { - const [craftingStage, setCraftingStage] = useState<'design' | 'prepare' | 'apply'>('design'); + const [craftingStage, setCraftingStage] = useState<'design' | 'prepare' | 'apply' | 'craft'>('craft'); const [selectedEquipmentType, setSelectedEquipmentType] = useState(null); const [selectedEquipmentInstance, setSelectedEquipmentInstance] = useState(null); const [selectedDesign, setSelectedDesign] = useState(null); @@ -718,11 +734,189 @@ export function CraftingTab({ ); + // Render equipment crafting stage + const renderCraftStage = () => ( +
+ {/* Blueprint Selection */} + + + + + Available Blueprints + + + + {equipmentCraftingProgress ? ( +
+
+ Crafting: {CRAFTING_RECIPES[equipmentCraftingProgress.blueprintId]?.name} +
+ +
+ {equipmentCraftingProgress.progress.toFixed(1)}h / {equipmentCraftingProgress.required.toFixed(1)}h + Mana spent: {fmt(equipmentCraftingProgress.manaSpent)} +
+ +
+ ) : ( + +
+ {lootInventory.blueprints.length === 0 ? ( +
+ +

No blueprints discovered yet.

+

Defeat guardians to find blueprints!

+
+ ) : ( + lootInventory.blueprints.map(bpId => { + const recipe = CRAFTING_RECIPES[bpId]; + if (!recipe) return null; + + const { canCraft, missingMaterials, missingMana } = canCraftRecipe( + recipe, + lootInventory.materials, + rawMana + ); + + const rarityStyle = RARITY_COLORS[recipe.rarity]; + + return ( +
+
+
+
+ {recipe.name} +
+
{recipe.rarity}
+
+ + {EQUIPMENT_TYPES[recipe.equipmentTypeId]?.category} + +
+ +
{recipe.description}
+ + + +
+
Materials:
+ {Object.entries(recipe.materials).map(([matId, amount]) => { + const available = lootInventory.materials[matId] || 0; + const matDrop = LOOT_DROPS[matId]; + const hasEnough = available >= amount; + + return ( +
+ {matDrop?.name || matId} + + {available} / {amount} + +
+ ); + })} + +
+ Mana Cost: + = recipe.manaCost ? 'text-green-400' : 'text-red-400'}> + {fmt(recipe.manaCost)} + +
+ +
+ Craft Time: + {recipe.craftTime}h +
+
+ + +
+ ); + }) + )} +
+
+ )} +
+
+ + {/* Materials Inventory */} + + + + + Materials ({Object.values(lootInventory.materials).reduce((a, b) => a + b, 0)}) + + + + + {Object.keys(lootInventory.materials).length === 0 ? ( +
+ +

No materials collected yet.

+

Defeat floors to gather materials!

+
+ ) : ( +
+ {Object.entries(lootInventory.materials).map(([matId, count]) => { + if (count <= 0) return null; + const drop = LOOT_DROPS[matId]; + if (!drop) return null; + + const rarityStyle = RARITY_COLORS[drop.rarity]; + + return ( +
+
+
+
+ {drop.name} +
+
x{count}
+
+ +
+
+ ); + })} +
+ )} +
+
+
+
+ ); + return (
{/* Stage Tabs */} setCraftingStage(v as typeof craftingStage)}> + + + Craft + Design @@ -737,6 +931,9 @@ export function CraftingTab({ + + {renderCraftStage()} + {renderDesignStage()} @@ -749,6 +946,20 @@ export function CraftingTab({ {/* Current Activity Indicator */} + {currentAction === 'craft' && equipmentCraftingProgress && ( + + +
+ + Crafting equipment... +
+
+ {((equipmentCraftingProgress.progress / equipmentCraftingProgress.required) * 100).toFixed(0)}% +
+
+
+ )} + {currentAction === 'design' && designProgress && ( diff --git a/src/lib/game/crafting-slice.ts b/src/lib/game/crafting-slice.ts index ee581f4..4cb2a22 100755 --- a/src/lib/game/crafting-slice.ts +++ b/src/lib/game/crafting-slice.ts @@ -1,9 +1,10 @@ // ─── Crafting Store Slice ───────────────────────────────────────────────────────── // Handles equipment and enchantment system: design, prepare, apply stages -import type { GameState, EquipmentInstance, AppliedEnchantment, EnchantmentDesign, DesignEffect, EquipmentSlot } from './types'; +import type { GameState, EquipmentInstance, AppliedEnchantment, EnchantmentDesign, DesignEffect, EquipmentSlot, EquipmentCraftingProgress, LootInventory } from './types'; import { EQUIPMENT_TYPES, type EquipmentCategory } from './data/equipment'; import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects'; +import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/crafting-recipes'; import { SPELLS_DEF } from './constants'; // ─── Helper Functions ───────────────────────────────────────────────────────── @@ -88,6 +89,11 @@ export interface CraftingActions { // 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; @@ -502,6 +508,97 @@ export function createCraftingSlice( 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)], + }; + }); + }, }; } @@ -620,6 +717,59 @@ 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, + }; + + updates = { + ...updates, + equipmentCraftingProgress: null, + currentAction: 'meditate', + equipmentInstances: { + ...state.equipmentInstances, + [instanceId]: newInstance, + }, + totalCraftsCompleted: (state.totalCraftsCompleted || 0) + 1, + log: [`🔨 Crafted ${recipe.name}!`, ...log], + }; + } else { + updates = { + ...updates, + equipmentCraftingProgress: null, + currentAction: 'meditate', + log: ['⚠️ Crafting failed - invalid recipe!', ...log], + }; + } + } else { + updates = { + ...updates, + equipmentCraftingProgress: { + ...craft, + progress, + }, + }; + } + } + return updates; } diff --git a/src/lib/game/data/crafting-recipes.ts b/src/lib/game/data/crafting-recipes.ts new file mode 100644 index 0000000..f9f2a0b --- /dev/null +++ b/src/lib/game/data/crafting-recipes.ts @@ -0,0 +1,257 @@ +// ─── Crafting Recipes ───────────────────────────────────────────────────────── +// Defines what materials are needed to craft equipment from blueprints + +import type { EquipmentSlot } from '../types'; + +export interface CraftingRecipe { + id: string; // Blueprint ID (matches loot drop) + equipmentTypeId: string; // Resulting equipment type ID + name: string; // Display name + description: string; + rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary'; + materials: Record; // materialId -> count required + manaCost: number; // Raw mana cost to craft + craftTime: number; // Hours to craft + minFloor: number; // Minimum floor where blueprint drops + unlocked: boolean; // Whether the player has discovered this +} + +export const CRAFTING_RECIPES: Record = { + // ─── Staff Blueprints ─── + staffBlueprint: { + id: 'staffBlueprint', + equipmentTypeId: 'oakStaff', + name: 'Oak Staff', + description: 'A sturdy oak staff with decent mana capacity.', + rarity: 'uncommon', + materials: { + manaCrystalDust: 5, + arcaneShard: 2, + }, + manaCost: 200, + craftTime: 4, + minFloor: 10, + unlocked: false, + }, + + wandBlueprint: { + id: 'wandBlueprint', + equipmentTypeId: 'crystalWand', + name: 'Crystal Wand', + description: 'A wand tipped with a small crystal. Excellent for elemental enchantments.', + rarity: 'rare', + materials: { + manaCrystalDust: 8, + arcaneShard: 4, + elementalCore: 1, + }, + manaCost: 500, + craftTime: 6, + minFloor: 20, + unlocked: false, + }, + + robeBlueprint: { + id: 'robeBlueprint', + equipmentTypeId: 'scholarRobe', + name: 'Scholar Robe', + description: 'A robe worn by scholars and researchers.', + rarity: 'rare', + materials: { + manaCrystalDust: 6, + arcaneShard: 3, + elementalCore: 1, + }, + manaCost: 400, + craftTime: 5, + minFloor: 25, + unlocked: false, + }, + + artifactBlueprint: { + id: 'artifactBlueprint', + equipmentTypeId: 'arcanistStaff', + name: 'Arcanist Staff', + description: 'A staff designed for advanced spellcasters. High capacity for complex enchantments.', + rarity: 'legendary', + materials: { + manaCrystalDust: 20, + arcaneShard: 10, + elementalCore: 5, + voidEssence: 2, + celestialFragment: 1, + }, + manaCost: 2000, + craftTime: 12, + minFloor: 60, + unlocked: false, + }, + + // ─── Additional Blueprints ─── + battlestaffBlueprint: { + id: 'battlestaffBlueprint', + equipmentTypeId: 'battlestaff', + name: 'Battlestaff', + description: 'A reinforced staff suitable for both casting and combat.', + rarity: 'rare', + materials: { + manaCrystalDust: 10, + arcaneShard: 5, + elementalCore: 2, + }, + manaCost: 600, + craftTime: 6, + minFloor: 30, + unlocked: false, + }, + + catalystBlueprint: { + id: 'catalystBlueprint', + equipmentTypeId: 'fireCatalyst', + name: 'Fire Catalyst', + description: 'A catalyst attuned to fire magic. Enhances fire enchantments.', + rarity: 'rare', + materials: { + manaCrystalDust: 8, + arcaneShard: 4, + elementalCore: 3, + }, + manaCost: 500, + craftTime: 5, + minFloor: 25, + unlocked: false, + }, + + shieldBlueprint: { + id: 'shieldBlueprint', + equipmentTypeId: 'runicShield', + name: 'Runic Shield', + description: 'A shield engraved with protective runes.', + rarity: 'rare', + materials: { + manaCrystalDust: 10, + arcaneShard: 6, + elementalCore: 2, + }, + manaCost: 450, + craftTime: 5, + minFloor: 28, + unlocked: false, + }, + + hatBlueprint: { + id: 'hatBlueprint', + equipmentTypeId: 'wizardHat', + name: 'Wizard Hat', + description: 'A classic pointed wizard hat. Decent capacity for headwear.', + rarity: 'uncommon', + materials: { + manaCrystalDust: 4, + arcaneShard: 2, + }, + manaCost: 150, + craftTime: 3, + minFloor: 12, + unlocked: false, + }, + + glovesBlueprint: { + id: 'glovesBlueprint', + equipmentTypeId: 'spellweaveGloves', + name: 'Spellweave Gloves', + description: 'Gloves woven with mana-conductive threads.', + rarity: 'uncommon', + materials: { + manaCrystalDust: 3, + arcaneShard: 2, + }, + manaCost: 120, + craftTime: 3, + minFloor: 15, + unlocked: false, + }, + + bootsBlueprint: { + id: 'bootsBlueprint', + equipmentTypeId: 'travelerBoots', + name: 'Traveler Boots', + description: 'Comfortable boots for long journeys.', + rarity: 'uncommon', + materials: { + manaCrystalDust: 3, + arcaneShard: 1, + }, + manaCost: 100, + craftTime: 2, + minFloor: 8, + unlocked: false, + }, + + ringBlueprint: { + id: 'ringBlueprint', + equipmentTypeId: 'silverRing', + name: 'Silver Ring', + description: 'A silver ring with decent magical conductivity.', + rarity: 'uncommon', + materials: { + manaCrystalDust: 2, + arcaneShard: 1, + }, + manaCost: 80, + craftTime: 2, + minFloor: 10, + unlocked: false, + }, + + amuletBlueprint: { + id: 'amuletBlueprint', + equipmentTypeId: 'silverAmulet', + name: 'Silver Amulet', + description: 'A silver amulet with a small gem.', + rarity: 'uncommon', + materials: { + manaCrystalDust: 3, + arcaneShard: 2, + }, + manaCost: 100, + craftTime: 3, + minFloor: 12, + unlocked: false, + }, +}; + +// Helper functions +export function getRecipeByBlueprint(blueprintId: string): CraftingRecipe | undefined { + return CRAFTING_RECIPES[blueprintId]; +} + +export function canCraftRecipe( + recipe: CraftingRecipe, + materials: Record, + rawMana: number +): { canCraft: boolean; missingMaterials: Record; missingMana: number } { + const missingMaterials: Record = {}; + let canCraft = true; + + for (const [matId, required] of Object.entries(recipe.materials)) { + const available = materials[matId] || 0; + if (available < required) { + missingMaterials[matId] = required - available; + canCraft = false; + } + } + + const missingMana = Math.max(0, recipe.manaCost - rawMana); + if (missingMana > 0) { + canCraft = false; + } + + return { canCraft, missingMaterials, missingMana }; +} + +// Get all recipes available based on unlocked blueprints +export function getAvailableRecipes(unlockedBlueprints: string[]): CraftingRecipe[] { + return unlockedBlueprints + .map(bpId => CRAFTING_RECIPES[bpId]) + .filter(Boolean); +} diff --git a/src/lib/game/store.ts b/src/lib/game/store.ts index 131be55..11bc0ee 100755 --- a/src/lib/game/store.ts +++ b/src/lib/game/store.ts @@ -47,7 +47,8 @@ import { type FamiliarBonuses, DEFAULT_FAMILIAR_BONUSES, } from './familiar-slice'; -import { rollLootDrops } from './data/loot-drops'; +import { rollLootDrops, LOOT_DROPS } from './data/loot-drops'; +import { CRAFTING_RECIPES, canCraftRecipe } from './data/crafting-recipes'; // Default empty effects for when effects aren't provided const DEFAULT_EFFECTS: ComputedEffects = { @@ -560,6 +561,9 @@ function makeInitial(overrides: Partial = {}): GameState { blueprints: [], }, lootDropsToday: 0, + + // Equipment Crafting Progress + equipmentCraftingProgress: null, // Achievements achievements: { @@ -1912,6 +1916,98 @@ export const useGameStore = create()( 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 and mana + const { canCraft } = 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, + }, + log: [`🔨 Started crafting ${recipe.name}...`, ...state.log.slice(0, 49)], + })); + + 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: [`🚫 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 = LOOT_DROPS[materialId]?.name || materialId; + return { + lootInventory: { + ...state.lootInventory, + materials: newMaterials, + }, + log: [`🗑️ Deleted ${amount}x ${dropName}.`, ...state.log.slice(0, 49)], + }; + }); + }, + // ─── Floor Navigation ──────────────────────────────────────────────────────── setClimbDirection: (direction: 'up' | 'down') => { diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts index 381467b..a61a6d2 100755 --- a/src/lib/game/types.ts +++ b/src/lib/game/types.ts @@ -206,6 +206,15 @@ export interface ApplicationProgress { manaSpent: number; // Total mana spent so far } +// Equipment Crafting Progress (crafting from blueprints) +export interface EquipmentCraftingProgress { + blueprintId: string; // Blueprint being crafted + equipmentTypeId: string; // Resulting equipment type + progress: number; // Hours spent crafting + required: number; // Total hours needed + manaSpent: number; // Mana spent so far +} + // Equipment spell state (for multi-spell casting) export interface EquipmentSpellState { spellId: string; @@ -436,6 +445,7 @@ export interface GameState { designProgress: DesignProgress | null; preparationProgress: PreparationProgress | null; applicationProgress: ApplicationProgress | null; + equipmentCraftingProgress: EquipmentCraftingProgress | null; // Unlocked enchantment effects for designing unlockedEffects: string[]; // Effect IDs that have been researched