/** * CraftingSystem — recipe lookup + craft execution with inventory checks. * GDD §11: Crafting */ class CraftingSystem { constructor(contentLoader) { this.loader = contentLoader; this.playerCrafting = new Map(); } getAllRecipes() { return this.loader.getAllRecipes(); } getRecipe(id) { return this.loader.getRecipe(id); } getRecipesByType(type) { return this.loader.getRecipesByType(type); } getCraftingTabs() { return this.loader.getCraftingTabs(); } initializePlayerData(userId) { if (this.playerCrafting.has(userId)) return this.playerCrafting.get(userId); const data = { skill: 1, experience: 0, knownRecipes: new Set(), totalCrafted: 0 }; this.playerCrafting.set(userId, data); return data; } getPlayerCrafting(userId) { return this.initializePlayerData(userId); } /** * Check if player has all required materials in inventory. * Returns { canCraft: bool, missing: [{itemId, need, have}] } */ checkMaterials(recipeId, playerInventory) { const recipe = this.getRecipe(recipeId); if (!recipe) return { canCraft: false, missing: [{ itemId: 'unknown', need: 1, have: 0 }] }; const inputs = recipe.recipe?.inputs || recipe.inputs || {}; const missing = []; for (const [itemId, qty] of Object.entries(inputs)) { const have = this._countItem(playerInventory, itemId); if (have < qty) { missing.push({ itemId, need: qty, have }); } } return { canCraft: missing.length === 0, missing }; } /** * Execute a craft: consume materials, add output item, return result. * Returns { success, output, xpGained, error? } */ executeCraft(recipeId, playerData, userId) { const recipe = this.getRecipe(recipeId); if (!recipe) return { success: false, error: 'Recipe not found' }; const inputs = recipe.recipe?.inputs || recipe.inputs || {}; const outputs = recipe.recipe?.output || recipe.output || {}; const xpGain = recipe.craft?.xp || recipe.xp || 10; const inventory = playerData.inventory || { items: [] }; // Re-check materials server-side const check = this.checkMaterials(recipeId, inventory); if (!check.canCraft) { return { success: false, error: 'Missing materials', missing: check.missing }; } // Consume inputs for (const [itemId, qty] of Object.entries(inputs)) { this._removeItems(inventory, itemId, qty); } // Add outputs const outputItems = []; for (const [itemId, qty] of Object.entries(outputs)) { const item = this._makeItem(itemId, qty); inventory.items.push(item); outputItems.push(item); } // Update crafting stats const pd = this.initializePlayerData(userId); pd.totalCrafted++; pd.experience += xpGain; const newSkill = this._calcSkillLevel(pd.experience); if (newSkill > pd.skill) pd.skill = newSkill; pd.knownRecipes.add(recipeId); // Persist crafting data back into playerData playerData.crafting = { ...(playerData.crafting || {}), skill: pd.skill, experience: pd.experience, totalCrafted: pd.totalCrafted, }; this.recordCraft(userId, recipeId); return { success: true, output: outputItems, xpGained: xpGain, craftingLevel: pd.skill, craftingXp: pd.experience, }; } recordCraft(userId, recipeId) { const pd = this.initializePlayerData(userId); pd.totalCrafted++; pd.knownRecipes.add(recipeId); } // ── Private helpers ────────────────────────────────────────────────── _countItem(inventory, itemId) { const items = (inventory && inventory.items) ? inventory.items : inventory || []; let total = 0; for (const it of items) { if (it && (it.id === itemId || it.itemId === itemId)) { total += (it.quantity || it.qty || 1); } } return total; } _removeItems(inventory, itemId, qty) { let remaining = qty; const items = inventory.items || []; for (let i = items.length - 1; i >= 0 && remaining > 0; i--) { const it = items[i]; if (!it) continue; if (it.id === itemId || it.itemId === itemId) { const have = it.quantity || it.qty || 1; if (have <= remaining) { remaining -= have; items.splice(i, 1); } else { it.quantity = have - remaining; remaining = 0; } } } } _makeItem(itemId, qty) { return { id: itemId, itemId, quantity: qty, obtainedAt: Date.now(), source: 'crafting', }; } _calcSkillLevel(xp) { // Every 200 XP = 1 crafting level, cap at 50 return Math.min(50, Math.floor(xp / 200) + 1); } } module.exports = CraftingSystem;