feat: add material crafting recipes to Fabricator
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 4m25s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 4m25s
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Circular Dependencies
|
||||
Generated: 2026-05-27T10:40:00.800Z
|
||||
Generated: 2026-05-27T10:56:11.451Z
|
||||
|
||||
No circular dependencies found. ✅
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_meta": {
|
||||
"generated": "2026-05-27T10:39:58.930Z",
|
||||
"generated": "2026-05-27T10:56:09.647Z",
|
||||
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||
},
|
||||
|
||||
@@ -91,7 +91,8 @@ Mana-Loop/
|
||||
│ │ │ ├── tabs/
|
||||
│ │ │ │ ├── CraftingTab/
|
||||
│ │ │ │ │ ├── EnchanterSubTab.tsx
|
||||
│ │ │ │ │ └── FabricatorSubTab.tsx
|
||||
│ │ │ │ │ ├── FabricatorSubTab.tsx
|
||||
│ │ │ │ │ └── MaterialRecipeCard.tsx
|
||||
│ │ │ │ ├── DebugTab/
|
||||
│ │ │ │ │ ├── AchievementDebugSection.tsx
|
||||
│ │ │ │ │ ├── AttunementDebugSection.tsx
|
||||
@@ -248,6 +249,7 @@ Mana-Loop/
|
||||
│ │ │ ├── application-actions.ts
|
||||
│ │ │ ├── computed-getters.ts
|
||||
│ │ │ ├── crafting-equipment-actions.ts
|
||||
│ │ │ ├── crafting-material-actions.ts
|
||||
│ │ │ ├── design-actions.ts
|
||||
│ │ │ ├── disenchant-actions.ts
|
||||
│ │ │ ├── equipment-actions.ts
|
||||
@@ -314,7 +316,8 @@ Mana-Loop/
|
||||
│ │ │ ├── fabricator-recipes.ts
|
||||
│ │ │ ├── guardian-data.ts
|
||||
│ │ │ ├── guardian-encounters.ts
|
||||
│ │ │ └── loot-drops.ts
|
||||
│ │ │ ├── loot-drops.ts
|
||||
│ │ │ └── material-recipes.ts
|
||||
│ │ ├── effects/
|
||||
│ │ │ ├── discipline-effects.ts
|
||||
│ │ │ ├── dynamic-compute.ts
|
||||
|
||||
@@ -7,23 +7,19 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Anvil, Hammer, Package } from 'lucide-react';
|
||||
import { Anvil, FlaskConical, Hammer, Package } from 'lucide-react';
|
||||
import { MaterialRecipeCard } from './MaterialRecipeCard';
|
||||
import {
|
||||
FABRICATOR_RECIPES,
|
||||
MATERIAL_RECIPES,
|
||||
getRecipesByManaType,
|
||||
canCraftRecipe,
|
||||
MANA_TYPE_LABELS,
|
||||
} from '@/lib/game/data/fabricator-recipes';
|
||||
import { LOOT_DROPS, LOOT_RARITY_COLORS } from '@/lib/game/data/loot-drops';
|
||||
import { useCraftingStore, useManaStore } from '@/lib/game/stores';
|
||||
import type { FabricatorRecipe } from '@/lib/game/data/fabricator-recipes';
|
||||
|
||||
const MANA_TYPE_LABELS: Record<string, string> = {
|
||||
earth: '⛰️ Earth',
|
||||
metal: '🔩 Metal',
|
||||
crystal: '💎 Crystal',
|
||||
sand: '🏜️ Sand',
|
||||
};
|
||||
|
||||
function RecipeCard({
|
||||
recipe,
|
||||
materials,
|
||||
@@ -117,12 +113,14 @@ function RecipeCard({
|
||||
|
||||
export function FabricatorSubTab() {
|
||||
const [selectedManaType, setSelectedManaType] = useState<string>('earth');
|
||||
const [activeSection, setActiveSection] = useState<'equipment' | 'materials'>('equipment');
|
||||
|
||||
const lootInventory = useCraftingStore((s) => s.lootInventory);
|
||||
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
const startFabricatorCrafting = useCraftingStore((s) => s.startFabricatorCrafting);
|
||||
const craftMaterial = useCraftingStore((s) => s.craftMaterial);
|
||||
const cancelEquipmentCrafting = useCraftingStore((s) => s.cancelEquipmentCrafting);
|
||||
|
||||
const availableManaTypes = useMemo(() => {
|
||||
@@ -136,13 +134,40 @@ export function FabricatorSubTab() {
|
||||
|
||||
const isCrafting = equipmentCraftingProgress !== null;
|
||||
|
||||
const materialRecipes = useMemo(() => MATERIAL_RECIPES, []);
|
||||
|
||||
const handleCraft = (recipe: FabricatorRecipe) => {
|
||||
if (recipe.recipeType === 'material') {
|
||||
craftMaterial(recipe.id);
|
||||
} else {
|
||||
startFabricatorCrafting(recipe.id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Mana type filter */}
|
||||
{/* Section toggle: Equipment vs Materials */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={activeSection === 'equipment' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setActiveSection('equipment')}
|
||||
>
|
||||
<Hammer className="w-3 h-3 mr-1" />
|
||||
Equipment
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeSection === 'materials' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setActiveSection('materials')}
|
||||
>
|
||||
<FlaskConical className="w-3 h-3 mr-1" />
|
||||
Materials
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Mana type filter — only for equipment */}
|
||||
{activeSection === 'equipment' && (
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{availableManaTypes.map((mt) => (
|
||||
<Button
|
||||
@@ -155,14 +180,18 @@ export function FabricatorSubTab() {
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Recipe list */}
|
||||
<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">
|
||||
<Hammer className="w-4 h-4" />
|
||||
{MANA_TYPE_LABELS[selectedManaType] ?? selectedManaType} Recipes
|
||||
{activeSection === 'equipment' ? (
|
||||
<><Hammer className="w-4 h-4" />{MANA_TYPE_LABELS[selectedManaType] ?? selectedManaType} Recipes</>
|
||||
) : (
|
||||
<><FlaskConical className="w-4 h-4" />Material Recipes</>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -193,7 +222,8 @@ export function FabricatorSubTab() {
|
||||
) : (
|
||||
<ScrollArea className="h-80">
|
||||
<div className="space-y-2">
|
||||
{filteredRecipes.length === 0 ? (
|
||||
{activeSection === 'equipment' ? (
|
||||
filteredRecipes.length === 0 ? (
|
||||
<div className="text-center text-gray-400 py-4">
|
||||
<Anvil className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p>No recipes for this mana type yet.</p>
|
||||
@@ -210,6 +240,25 @@ export function FabricatorSubTab() {
|
||||
isCrafting={isCrafting}
|
||||
/>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
materialRecipes.length === 0 ? (
|
||||
<div className="text-center text-gray-400 py-4">
|
||||
<FlaskConical className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p>No material recipes available.</p>
|
||||
</div>
|
||||
) : (
|
||||
materialRecipes.map((recipe) => (
|
||||
<MaterialRecipeCard
|
||||
key={recipe.id}
|
||||
recipe={recipe}
|
||||
materials={lootInventory.materials}
|
||||
rawMana={rawMana}
|
||||
elementalMana={elements}
|
||||
onCraft={handleCraft}
|
||||
/>
|
||||
))
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { LOOT_DROPS, LOOT_RARITY_COLORS } from '@/lib/game/data/loot-drops';
|
||||
import { canCraftRecipe, MANA_TYPE_LABELS } from '@/lib/game/data/fabricator-recipes';
|
||||
import type { FabricatorRecipe } from '@/lib/game/data/fabricator-recipes';
|
||||
|
||||
interface MaterialRecipeCardProps {
|
||||
recipe: FabricatorRecipe;
|
||||
materials: Record<string, number>;
|
||||
rawMana: number;
|
||||
elementalMana: Record<string, { current: number; max: number; unlocked: boolean }>;
|
||||
onCraft: (recipe: FabricatorRecipe) => void;
|
||||
}
|
||||
|
||||
export function MaterialRecipeCard({
|
||||
recipe,
|
||||
materials,
|
||||
rawMana,
|
||||
elementalMana,
|
||||
onCraft,
|
||||
}: MaterialRecipeCardProps) {
|
||||
const pool = recipe.manaType === 'raw'
|
||||
? rawMana
|
||||
: (elementalMana[recipe.manaType]?.current ?? 0);
|
||||
const { canCraft } = canCraftRecipe(recipe, materials, pool, recipe.manaType);
|
||||
const resultDrop = recipe.resultMaterial ? LOOT_DROPS[recipe.resultMaterial] : null;
|
||||
const resultRarity = resultDrop ? LOOT_RARITY_COLORS[resultDrop.rarity] : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="p-3 rounded border bg-gray-800/50"
|
||||
style={{ borderColor: resultRarity?.color ?? '#6B7280' }}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<div className="font-semibold" style={{ color: resultRarity?.color ?? '#D1D5DB' }}>
|
||||
{recipe.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 capitalize">{recipe.rarity}</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{recipe.resultAmount}x Output
|
||||
</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">
|
||||
{Object.keys(recipe.materials).length > 0 && (
|
||||
<>
|
||||
<div className="text-gray-500">Input Materials:</div>
|
||||
{Object.entries(recipe.materials).map(([matId, amount]) => {
|
||||
const available = 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>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{recipe.manaCost > 0 && (
|
||||
<div className="flex justify-between mt-2">
|
||||
<span>{MANA_TYPE_LABELS[recipe.manaType]?.split(' ')[1] ?? recipe.manaType} Mana:</span>
|
||||
<span className={pool >= recipe.manaCost ? 'text-green-400' : 'text-red-400'}>
|
||||
{pool} / {recipe.manaCost}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between">
|
||||
<span>Produces:</span>
|
||||
<span className="text-amber-400">
|
||||
{recipe.resultAmount}x {resultDrop?.name ?? recipe.resultMaterial}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full mt-3"
|
||||
size="sm"
|
||||
disabled={!canCraft}
|
||||
onClick={() => onCraft(recipe)}
|
||||
>
|
||||
{canCraft ? 'Craft' : 'Missing Resources'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// ─── Material Crafting Actions ─────────────────────────────────────────────────
|
||||
// Separate file to avoid exceeding the 400-line limit in craftingStore.ts.
|
||||
|
||||
import { useManaStore } from '../stores/manaStore';
|
||||
import { useCombatStore } from '../stores/combatStore';
|
||||
import { useUIStore } from '../stores/uiStore';
|
||||
import {
|
||||
getFabricatorRecipe,
|
||||
deductFabricatorMana,
|
||||
deductMaterials,
|
||||
} from '../crafting-fabricator';
|
||||
|
||||
export interface CraftMaterialResult {
|
||||
success: boolean;
|
||||
newMaterials?: Record<string, number>;
|
||||
}
|
||||
|
||||
export function craftMaterial(
|
||||
recipeId: string,
|
||||
lootInventoryMaterials: Record<string, number>,
|
||||
): CraftMaterialResult {
|
||||
const currentAction = useCombatStore.getState().currentAction;
|
||||
if (currentAction !== 'meditate') return { success: false };
|
||||
|
||||
const recipe = getFabricatorRecipe(recipeId);
|
||||
if (!recipe || recipe.recipeType !== 'material') return { success: false };
|
||||
if (!recipe.resultMaterial || !recipe.resultAmount) return { success: false };
|
||||
|
||||
const rawMana = useManaStore.getState().rawMana;
|
||||
const elements = useManaStore.getState().elements;
|
||||
|
||||
const deducted = deductFabricatorMana(recipe, rawMana, elements);
|
||||
if (!deducted) return { success: false };
|
||||
|
||||
const newMaterials = deductMaterials(recipe, lootInventoryMaterials);
|
||||
newMaterials[recipe.resultMaterial] = (newMaterials[recipe.resultMaterial] || 0) + recipe.resultAmount;
|
||||
|
||||
useManaStore.setState({ rawMana: deducted.rawMana, elements: deducted.elements });
|
||||
useUIStore.getState().addLog(`✨ Crafted ${recipe.resultAmount}x ${recipe.name}!`);
|
||||
|
||||
return { success: true, newMaterials };
|
||||
}
|
||||
@@ -4,6 +4,9 @@
|
||||
import type { EquipmentCraftingProgress } from './types';
|
||||
import type { FabricatorRecipe } from './data/fabricator-recipes';
|
||||
import { FABRICATOR_RECIPES } from './data/fabricator-recipes';
|
||||
import { useManaStore } from './stores/manaStore';
|
||||
import { useCombatStore } from './stores/combatStore';
|
||||
import { useUIStore } from './stores/uiStore';
|
||||
|
||||
// ─── Lookup ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -139,3 +142,37 @@ export function makeFabricatorProgress(
|
||||
manaSpent: manaCost,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Material Crafting ──────────────────────────────────────────────────────
|
||||
|
||||
export interface CraftMaterialResult {
|
||||
success: boolean;
|
||||
newMaterials: Record<string, number>;
|
||||
newRawMana: number;
|
||||
newElements: Record<string, { current: number; max: number; unlocked: boolean }>;
|
||||
logMessage: string;
|
||||
}
|
||||
|
||||
export function executeMaterialCraft(
|
||||
recipe: FabricatorRecipe,
|
||||
materials: Record<string, number>,
|
||||
): CraftMaterialResult | null {
|
||||
if (recipe.recipeType !== 'material' || !recipe.resultMaterial || !recipe.resultAmount) return null;
|
||||
|
||||
const rawMana = useManaStore.getState().rawMana;
|
||||
const elements = useManaStore.getState().elements;
|
||||
|
||||
const deducted = deductFabricatorMana(recipe, rawMana, elements);
|
||||
if (!deducted) return null;
|
||||
|
||||
const newMaterials = deductMaterials(recipe, materials);
|
||||
newMaterials[recipe.resultMaterial] = (newMaterials[recipe.resultMaterial] || 0) + recipe.resultAmount;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
newMaterials,
|
||||
newRawMana: deducted.rawMana,
|
||||
newElements: deducted.elements,
|
||||
logMessage: `✨ Crafted ${recipe.resultAmount}x ${recipe.name}!`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,8 +24,18 @@ export interface FabricatorRecipe {
|
||||
rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary';
|
||||
/** Flavor text describing the gear's properties */
|
||||
gearTrait: string;
|
||||
/** Recipe type: 'equipment' (default) or 'material' */
|
||||
recipeType?: 'equipment' | 'material';
|
||||
/** For material recipes: the material ID produced */
|
||||
resultMaterial?: string;
|
||||
/** For material recipes: how many are produced */
|
||||
resultAmount?: number;
|
||||
}
|
||||
|
||||
import { MATERIAL_RECIPES } from './material-recipes';
|
||||
|
||||
export { MATERIAL_RECIPES };
|
||||
|
||||
export const FABRICATOR_RECIPES: FabricatorRecipe[] = [
|
||||
// ─── Earth Gear (Compacted Earth — high defense) ──────────────────────
|
||||
{
|
||||
@@ -190,16 +200,36 @@ export const FABRICATOR_RECIPES: FabricatorRecipe[] = [
|
||||
rarity: 'rare',
|
||||
gearTrait: '+20% cast speed, +5% evasion',
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
// ─── Mana Type Labels ────────────────────────────────────────────────────────
|
||||
|
||||
export const MANA_TYPE_LABELS: Record<string, string> = {
|
||||
raw: '⚪ Raw',
|
||||
fire: '🔥 Fire',
|
||||
water: '💧 Water',
|
||||
air: '🌬️ Air',
|
||||
earth: '⛰️ Earth',
|
||||
light: '☀️ Light',
|
||||
dark: '🌑 Dark',
|
||||
metal: '🔩 Metal',
|
||||
crystal: '💎 Crystal',
|
||||
sand: '🏜️ Sand',
|
||||
};
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export function getRecipesByManaType(manaType: string): FabricatorRecipe[] {
|
||||
return FABRICATOR_RECIPES.filter(r => r.manaType === manaType);
|
||||
return FABRICATOR_RECIPES.filter(r => r.manaType === manaType && r.recipeType !== 'material');
|
||||
}
|
||||
|
||||
export function getMaterialRecipes(): FabricatorRecipe[] {
|
||||
return FABRICATOR_RECIPES.filter(r => r.recipeType === 'material');
|
||||
}
|
||||
|
||||
export function getRecipeById(id: string): FabricatorRecipe | undefined {
|
||||
return FABRICATOR_RECIPES.find(r => r.id === id);
|
||||
return FABRICATOR_RECIPES.find(r => r.id === id) ?? MATERIAL_RECIPES.find(r => r.id === id);
|
||||
}
|
||||
|
||||
export function canCraftRecipe(
|
||||
|
||||
@@ -52,6 +52,78 @@ export const LOOT_DROPS: Record<string, LootDrop> = {
|
||||
minFloor: 15,
|
||||
dropChance: 0.07,
|
||||
},
|
||||
manaCrystal: {
|
||||
id: 'manaCrystal',
|
||||
name: 'Mana Crystal',
|
||||
rarity: 'uncommon',
|
||||
type: 'material',
|
||||
minFloor: 5,
|
||||
dropChance: 0.10,
|
||||
},
|
||||
fireCrystal: {
|
||||
id: 'fireCrystal',
|
||||
name: 'Fire Attuned Crystal',
|
||||
rarity: 'rare',
|
||||
type: 'material',
|
||||
minFloor: 15,
|
||||
dropChance: 0.06,
|
||||
},
|
||||
waterCrystal: {
|
||||
id: 'waterCrystal',
|
||||
name: 'Water Attuned Crystal',
|
||||
rarity: 'rare',
|
||||
type: 'material',
|
||||
minFloor: 15,
|
||||
dropChance: 0.06,
|
||||
},
|
||||
airCrystal: {
|
||||
id: 'airCrystal',
|
||||
name: 'Air Attuned Crystal',
|
||||
rarity: 'rare',
|
||||
type: 'material',
|
||||
minFloor: 15,
|
||||
dropChance: 0.06,
|
||||
},
|
||||
earthCrystal: {
|
||||
id: 'earthCrystal',
|
||||
name: 'Earth Attuned Crystal',
|
||||
rarity: 'rare',
|
||||
type: 'material',
|
||||
minFloor: 15,
|
||||
dropChance: 0.06,
|
||||
},
|
||||
lightCrystal: {
|
||||
id: 'lightCrystal',
|
||||
name: 'Light Attuned Crystal',
|
||||
rarity: 'rare',
|
||||
type: 'material',
|
||||
minFloor: 25,
|
||||
dropChance: 0.05,
|
||||
},
|
||||
darkCrystal: {
|
||||
id: 'darkCrystal',
|
||||
name: 'Dark Attuned Crystal',
|
||||
rarity: 'rare',
|
||||
type: 'material',
|
||||
minFloor: 25,
|
||||
dropChance: 0.05,
|
||||
},
|
||||
metalCrystal: {
|
||||
id: 'metalCrystal',
|
||||
name: 'Metal Attuned Crystal',
|
||||
rarity: 'rare',
|
||||
type: 'material',
|
||||
minFloor: 30,
|
||||
dropChance: 0.05,
|
||||
},
|
||||
crystalCrystal: {
|
||||
id: 'crystalCrystal',
|
||||
name: 'Crystal Attuned Crystal',
|
||||
rarity: 'epic',
|
||||
type: 'material',
|
||||
minFloor: 40,
|
||||
dropChance: 0.03,
|
||||
},
|
||||
elementalCore: {
|
||||
id: 'elementalCore',
|
||||
name: 'Elemental Core',
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
// ─── Material Crafting Recipes ────────────────────────────────────────────────
|
||||
// Recipes that produce materials rather than equipment.
|
||||
// All recipes use the FabricatorRecipe interface with recipeType: 'material'.
|
||||
|
||||
import type { FabricatorRecipe } from './fabricator-recipes';
|
||||
|
||||
export const MATERIAL_RECIPES: FabricatorRecipe[] = [
|
||||
{
|
||||
id: 'manaCrystal',
|
||||
name: 'Mana Crystal',
|
||||
description: 'Condense raw mana into a stable crystal. Used in all advanced crafting.',
|
||||
manaType: 'raw',
|
||||
equipmentTypeId: 'basicStaff',
|
||||
slot: 'mainHand',
|
||||
materials: {},
|
||||
manaCost: 500,
|
||||
craftTime: 1,
|
||||
rarity: 'uncommon',
|
||||
gearTrait: 'Produces 1 Mana Crystal',
|
||||
recipeType: 'material',
|
||||
resultMaterial: 'manaCrystal',
|
||||
resultAmount: 1,
|
||||
},
|
||||
{
|
||||
id: 'manaCrystalDustCraft',
|
||||
name: 'Mana Crystal Dust',
|
||||
description: 'Grind a Mana Crystal into dust. Used as a base material.',
|
||||
manaType: 'raw',
|
||||
equipmentTypeId: 'basicStaff',
|
||||
slot: 'mainHand',
|
||||
materials: { manaCrystal: 1 },
|
||||
manaCost: 10,
|
||||
craftTime: 1,
|
||||
rarity: 'common',
|
||||
gearTrait: 'Produces 2 Mana Crystal Dust',
|
||||
recipeType: 'material',
|
||||
resultMaterial: 'manaCrystalDust',
|
||||
resultAmount: 2,
|
||||
},
|
||||
{
|
||||
id: 'fireCrystal',
|
||||
name: 'Fire Attuned Crystal',
|
||||
description: 'Infuse a Mana Crystal with fire mana to attune it to the flame element.',
|
||||
manaType: 'fire',
|
||||
equipmentTypeId: 'basicStaff',
|
||||
slot: 'mainHand',
|
||||
materials: { manaCrystal: 1 },
|
||||
manaCost: 100,
|
||||
craftTime: 1,
|
||||
rarity: 'rare',
|
||||
gearTrait: 'Produces 1 Fire Attuned Crystal',
|
||||
recipeType: 'material',
|
||||
resultMaterial: 'fireCrystal',
|
||||
resultAmount: 1,
|
||||
},
|
||||
{
|
||||
id: 'waterCrystal',
|
||||
name: 'Water Attuned Crystal',
|
||||
description: 'Infuse a Mana Crystal with water mana to attune it to the flow element.',
|
||||
manaType: 'water',
|
||||
equipmentTypeId: 'basicStaff',
|
||||
slot: 'mainHand',
|
||||
materials: { manaCrystal: 1 },
|
||||
manaCost: 100,
|
||||
craftTime: 1,
|
||||
rarity: 'rare',
|
||||
gearTrait: 'Produces 1 Water Attuned Crystal',
|
||||
recipeType: 'material',
|
||||
resultMaterial: 'waterCrystal',
|
||||
resultAmount: 1,
|
||||
},
|
||||
{
|
||||
id: 'airCrystal',
|
||||
name: 'Air Attuned Crystal',
|
||||
description: 'Infuse a Mana Crystal with air mana to attune it to the wind element.',
|
||||
manaType: 'air',
|
||||
equipmentTypeId: 'basicStaff',
|
||||
slot: 'mainHand',
|
||||
materials: { manaCrystal: 1 },
|
||||
manaCost: 100,
|
||||
craftTime: 1,
|
||||
rarity: 'rare',
|
||||
gearTrait: 'Produces 1 Air Attuned Crystal',
|
||||
recipeType: 'material',
|
||||
resultMaterial: 'airCrystal',
|
||||
resultAmount: 1,
|
||||
},
|
||||
{
|
||||
id: 'earthCrystal',
|
||||
name: 'Earth Attuned Crystal',
|
||||
description: 'Infuse a Mana Crystal with earth mana to attune it to the stone element.',
|
||||
manaType: 'earth',
|
||||
equipmentTypeId: 'basicStaff',
|
||||
slot: 'mainHand',
|
||||
materials: { manaCrystal: 1 },
|
||||
manaCost: 100,
|
||||
craftTime: 1,
|
||||
rarity: 'rare',
|
||||
gearTrait: 'Produces 1 Earth Attuned Crystal',
|
||||
recipeType: 'material',
|
||||
resultMaterial: 'earthCrystal',
|
||||
resultAmount: 1,
|
||||
},
|
||||
{
|
||||
id: 'lightCrystal',
|
||||
name: 'Light Attuned Crystal',
|
||||
description: 'Infuse a Mana Crystal with light mana to attune it to the radiant element.',
|
||||
manaType: 'light',
|
||||
equipmentTypeId: 'basicStaff',
|
||||
slot: 'mainHand',
|
||||
materials: { manaCrystal: 1 },
|
||||
manaCost: 100,
|
||||
craftTime: 1,
|
||||
rarity: 'rare',
|
||||
gearTrait: 'Produces 1 Light Attuned Crystal',
|
||||
recipeType: 'material',
|
||||
resultMaterial: 'lightCrystal',
|
||||
resultAmount: 1,
|
||||
},
|
||||
{
|
||||
id: 'darkCrystal',
|
||||
name: 'Dark Attuned Crystal',
|
||||
description: 'Infuse a Mana Crystal with dark mana to attune it to the shadow element.',
|
||||
manaType: 'dark',
|
||||
equipmentTypeId: 'basicStaff',
|
||||
slot: 'mainHand',
|
||||
materials: { manaCrystal: 1 },
|
||||
manaCost: 100,
|
||||
craftTime: 1,
|
||||
rarity: 'rare',
|
||||
gearTrait: 'Produces 1 Dark Attuned Crystal',
|
||||
recipeType: 'material',
|
||||
resultMaterial: 'darkCrystal',
|
||||
resultAmount: 1,
|
||||
},
|
||||
{
|
||||
id: 'metalCrystal',
|
||||
name: 'Metal Attuned Crystal',
|
||||
description: 'Infuse a Mana Crystal with metal mana to attune it to the alloy element.',
|
||||
manaType: 'metal',
|
||||
equipmentTypeId: 'basicStaff',
|
||||
slot: 'mainHand',
|
||||
materials: { manaCrystal: 1 },
|
||||
manaCost: 100,
|
||||
craftTime: 1,
|
||||
rarity: 'rare',
|
||||
gearTrait: 'Produces 1 Metal Attuned Crystal',
|
||||
recipeType: 'material',
|
||||
resultMaterial: 'metalCrystal',
|
||||
resultAmount: 1,
|
||||
},
|
||||
{
|
||||
id: 'crystalCrystal',
|
||||
name: 'Crystal Attuned Crystal',
|
||||
description: 'Infuse a Mana Crystal with crystal mana to attune it to the prismatic element.',
|
||||
manaType: 'crystal',
|
||||
equipmentTypeId: 'basicStaff',
|
||||
slot: 'mainHand',
|
||||
materials: { manaCrystal: 1 },
|
||||
manaCost: 100,
|
||||
craftTime: 1,
|
||||
rarity: 'epic',
|
||||
gearTrait: 'Produces 1 Crystal Attuned Crystal',
|
||||
recipeType: 'material',
|
||||
resultMaterial: 'crystalCrystal',
|
||||
resultAmount: 1,
|
||||
},
|
||||
{
|
||||
id: 'elementalCore',
|
||||
name: 'Elemental Core',
|
||||
description: 'Combine mana crystals and all four base elements into a powerful core.',
|
||||
manaType: 'raw',
|
||||
equipmentTypeId: 'basicStaff',
|
||||
slot: 'mainHand',
|
||||
materials: { manaCrystal: 10 },
|
||||
manaCost: 100,
|
||||
craftTime: 10,
|
||||
rarity: 'epic',
|
||||
gearTrait: 'Produces 1 Elemental Core',
|
||||
recipeType: 'material',
|
||||
resultMaterial: 'elementalCore',
|
||||
resultAmount: 1,
|
||||
},
|
||||
];
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
deductMaterials,
|
||||
makeFabricatorProgress,
|
||||
} from '../crafting-fabricator';
|
||||
import { craftMaterial as craftMaterialAction } from '../crafting-actions/crafting-material-actions';
|
||||
|
||||
export const useCraftingStore = create<CraftingStore>()(
|
||||
persist(
|
||||
@@ -198,45 +199,29 @@ export const useCraftingStore = create<CraftingStore>()(
|
||||
useCombatStore.setState({ currentAction: 'meditate' });
|
||||
},
|
||||
|
||||
// Equipment crafting actions
|
||||
startCraftingEquipment: (blueprintId: string) => {
|
||||
const state = get();
|
||||
const rawMana = useManaStore.getState().rawMana;
|
||||
const currentAction = useCombatStore.getState().currentAction;
|
||||
|
||||
// Check if we can start crafting
|
||||
const check = CraftingEquipment.canStartEquipmentCrafting(
|
||||
blueprintId,
|
||||
state.lootInventory.blueprints.includes(blueprintId),
|
||||
state.lootInventory.materials,
|
||||
rawMana,
|
||||
currentAction
|
||||
currentAction,
|
||||
);
|
||||
|
||||
if (!check.canCraft) return false;
|
||||
|
||||
// Initialize crafting
|
||||
const result = CraftingEquipment.initializeEquipmentCrafting(
|
||||
blueprintId,
|
||||
state.lootInventory.materials,
|
||||
rawMana
|
||||
rawMana,
|
||||
);
|
||||
|
||||
// Update crafting store state
|
||||
set((s) => ({
|
||||
lootInventory: {
|
||||
...s.lootInventory,
|
||||
materials: result.newMaterials,
|
||||
},
|
||||
lootInventory: { ...s.lootInventory, materials: result.newMaterials },
|
||||
equipmentCraftingProgress: result.progress,
|
||||
}));
|
||||
|
||||
// Update mana store (deduct mana)
|
||||
useManaStore.setState((s) => ({ rawMana: s.rawMana - result.manaCost }));
|
||||
|
||||
// Update combat store (set current action)
|
||||
useCombatStore.setState({ currentAction: 'craft' });
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
@@ -274,6 +259,17 @@ export const useCraftingStore = create<CraftingStore>()(
|
||||
return true;
|
||||
},
|
||||
|
||||
// Material crafting — instant crafting of materials
|
||||
craftMaterial: (recipeId: string) => {
|
||||
const state = get();
|
||||
const result = craftMaterialAction(recipeId, state.lootInventory.materials);
|
||||
if (!result.success) return false;
|
||||
if (result.newMaterials) {
|
||||
set((s) => ({ lootInventory: { ...s.lootInventory, materials: result.newMaterials! } }));
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
// Enchantment selection actions
|
||||
setSelectedEquipmentType: (type) => {
|
||||
set((s) => ({ enchantmentSelection: { ...s.enchantmentSelection, selectedEquipmentType: type }}));
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface CraftingActions {
|
||||
unequipItem: (slot: EquipmentSlot) => void;
|
||||
startCraftingEquipment: (blueprintId: string) => boolean;
|
||||
startFabricatorCrafting: (recipeId: string) => boolean;
|
||||
craftMaterial: (recipeId: string) => boolean;
|
||||
cancelEquipmentCrafting: () => void;
|
||||
setSelectedEquipmentType: (type: string | null) => void;
|
||||
setSelectedEffects: (effects: DesignEffect[]) => void;
|
||||
|
||||
Reference in New Issue
Block a user