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
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [selectedEquipmentInstance, setSelectedEquipmentInstance] = useState<string | null>(null);
|
||||
const [selectedDesign, setSelectedDesign] = useState<string | null>(null);
|
||||
@@ -718,11 +734,189 @@ export function CraftingTab({
|
||||
</div>
|
||||
);
|
||||
|
||||
// Render equipment crafting stage
|
||||
const renderCraftStage = () => (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Blueprint Selection */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
|
||||
<Anvil className="w-4 h-4" />
|
||||
Available Blueprints
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{equipmentCraftingProgress ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm text-gray-400">
|
||||
Crafting: {CRAFTING_RECIPES[equipmentCraftingProgress.blueprintId]?.name}
|
||||
</div>
|
||||
<Progress value={(equipmentCraftingProgress.progress / equipmentCraftingProgress.required) * 100} className="h-3" />
|
||||
<div className="flex justify-between text-xs text-gray-400">
|
||||
<span>{equipmentCraftingProgress.progress.toFixed(1)}h / {equipmentCraftingProgress.required.toFixed(1)}h</span>
|
||||
<span>Mana spent: {fmt(equipmentCraftingProgress.manaSpent)}</span>
|
||||
</div>
|
||||
<Button size="sm" variant="outline" onClick={cancelEquipmentCrafting}>Cancel</Button>
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="h-64">
|
||||
<div className="space-y-2">
|
||||
{lootInventory.blueprints.length === 0 ? (
|
||||
<div className="text-center text-gray-400 py-4">
|
||||
<Package className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p>No blueprints discovered yet.</p>
|
||||
<p className="text-xs mt-1">Defeat guardians to find blueprints!</p>
|
||||
</div>
|
||||
) : (
|
||||
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 (
|
||||
<div
|
||||
key={bpId}
|
||||
className="p-3 rounded border bg-gray-800/50"
|
||||
style={{ borderColor: rarityStyle?.color }}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<div className="font-semibold" style={{ color: rarityStyle?.color }}>
|
||||
{recipe.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 capitalize">{recipe.rarity}</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{EQUIPMENT_TYPES[recipe.equipmentTypeId]?.category}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-400 mb-2">{recipe.description}</div>
|
||||
|
||||
<Separator className="bg-gray-700 my-2" />
|
||||
|
||||
<div className="text-xs space-y-1">
|
||||
<div className="text-gray-500">Materials:</div>
|
||||
{Object.entries(recipe.materials).map(([matId, amount]) => {
|
||||
const available = lootInventory.materials[matId] || 0;
|
||||
const matDrop = LOOT_DROPS[matId];
|
||||
const hasEnough = available >= amount;
|
||||
|
||||
return (
|
||||
<div key={matId} className="flex justify-between">
|
||||
<span>{matDrop?.name || matId}</span>
|
||||
<span className={hasEnough ? 'text-green-400' : 'text-red-400'}>
|
||||
{available} / {amount}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="flex justify-between mt-2">
|
||||
<span>Mana Cost:</span>
|
||||
<span className={rawMana >= recipe.manaCost ? 'text-green-400' : 'text-red-400'}>
|
||||
{fmt(recipe.manaCost)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<span>Craft Time:</span>
|
||||
<span>{recipe.craftTime}h</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full mt-3"
|
||||
size="sm"
|
||||
disabled={!canCraft || currentAction === 'craft'}
|
||||
onClick={() => startCraftingEquipment(bpId)}
|
||||
>
|
||||
{canCraft ? 'Craft Equipment' : 'Missing Resources'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Materials Inventory */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
|
||||
<Package className="w-4 h-4" />
|
||||
Materials ({Object.values(lootInventory.materials).reduce((a, b) => a + b, 0)})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-64">
|
||||
{Object.keys(lootInventory.materials).length === 0 ? (
|
||||
<div className="text-center text-gray-400 py-4">
|
||||
<Sparkles className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p>No materials collected yet.</p>
|
||||
<p className="text-xs mt-1">Defeat floors to gather materials!</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{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 (
|
||||
<div
|
||||
key={matId}
|
||||
className="p-2 rounded border bg-gray-800/50 group relative"
|
||||
style={{ borderColor: rarityStyle?.color }}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-semibold" style={{ color: rarityStyle?.color }}>
|
||||
{drop.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">x{count}</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 text-red-400 hover:text-red-300 hover:bg-red-900/20"
|
||||
onClick={() => deleteMaterial(matId, count)}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Stage Tabs */}
|
||||
<Tabs value={craftingStage} onValueChange={(v) => setCraftingStage(v as typeof craftingStage)}>
|
||||
<TabsList className="bg-gray-800/50">
|
||||
<TabsTrigger value="craft" className="data-[state=active]:bg-cyan-600">
|
||||
<Anvil className="w-4 h-4 mr-1" />
|
||||
Craft
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="design" className="data-[state=active]:bg-amber-600">
|
||||
<Scroll className="w-4 h-4 mr-1" />
|
||||
Design
|
||||
@@ -737,6 +931,9 @@ export function CraftingTab({
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="craft" className="mt-4">
|
||||
{renderCraftStage()}
|
||||
</TabsContent>
|
||||
<TabsContent value="design" className="mt-4">
|
||||
{renderDesignStage()}
|
||||
</TabsContent>
|
||||
@@ -749,6 +946,20 @@ export function CraftingTab({
|
||||
</Tabs>
|
||||
|
||||
{/* Current Activity Indicator */}
|
||||
{currentAction === 'craft' && equipmentCraftingProgress && (
|
||||
<Card className="bg-cyan-900/30 border-cyan-600">
|
||||
<CardContent className="py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Anvil className="w-5 h-5 text-cyan-400" />
|
||||
<span>Crafting equipment...</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{((equipmentCraftingProgress.progress / equipmentCraftingProgress.required) * 100).toFixed(0)}%
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{currentAction === 'design' && designProgress && (
|
||||
<Card className="bg-purple-900/30 border-purple-600">
|
||||
<CardContent className="py-3 flex items-center justify-between">
|
||||
|
||||
@@ -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<string, number>;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
257
src/lib/game/data/crafting-recipes.ts
Normal file
257
src/lib/game/data/crafting-recipes.ts
Normal file
@@ -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<string, number>; // 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<string, CraftingRecipe> = {
|
||||
// ─── 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<string, number>,
|
||||
rawMana: number
|
||||
): { canCraft: boolean; missingMaterials: Record<string, number>; missingMana: number } {
|
||||
const missingMaterials: Record<string, number> = {};
|
||||
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);
|
||||
}
|
||||
@@ -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> = {}): GameState {
|
||||
blueprints: [],
|
||||
},
|
||||
lootDropsToday: 0,
|
||||
|
||||
// Equipment Crafting Progress
|
||||
equipmentCraftingProgress: null,
|
||||
|
||||
// Achievements
|
||||
achievements: {
|
||||
@@ -1912,6 +1916,98 @@ export const useGameStore = create<GameStore>()(
|
||||
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') => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user