feat: add material crafting recipes to Fabricator
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 4m25s

This commit is contained in:
2026-05-27 14:13:46 +02:00
parent cbeb0b50ad
commit 3f20991d2d
12 changed files with 579 additions and 64 deletions
@@ -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 };
}
+37
View File
@@ -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}!`,
};
}
+32 -2
View File
@@ -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(
+72
View File
@@ -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',
+184
View File
@@ -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,
},
];
+15 -19
View File
@@ -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;