/** * Galaxy Strike Online - Client Crafting System * Recipe definitions are loaded from the server; this file handles * local crafting logic, requirement checking, and UI rendering. */ class CraftingSystem extends BaseSystem { constructor(gameEngine) { super(gameEngine); this.recipes = new Map(); // recipeId -> recipe object this.currentCategory = 'weapons'; this.selectedRecipe = null; this._loaded = false; this._loading = false; } // ------------------------------------------------------------------ // // Initialisation — request recipes from the server // ------------------------------------------------------------------ // async initialize() { if (this._loaded || this._loading) return; this._loading = true; console.log('[CRAFTING SYSTEM] Requesting recipes from server'); if (!window.game?.socket) { console.warn('[CRAFTING SYSTEM] No socket — recipes will load when connected'); this._loading = false; return; } try { const recipes = await this._fetchRecipesFromServer(); this._applyServerRecipes(recipes); this._loaded = true; console.log(`[CRAFTING SYSTEM] Loaded ${this.recipes.size} recipes from server`); } catch (err) { console.error('[CRAFTING SYSTEM] Failed to load recipes from server:', err); } finally { this._loading = false; } } _fetchRecipesFromServer() { return new Promise((resolve, reject) => { const socket = window.game.socket; const timeout = setTimeout(() => { socket.off('recipes_data', handler); reject(new Error('Recipe data request timed out')); }, 10000); const handler = (data) => { clearTimeout(timeout); socket.off('recipes_data', handler); if (data && (Array.isArray(data) || typeof data === 'object')) { resolve(data); } else { reject(new Error('Invalid recipe data from server')); } }; socket.on('recipes_data', handler); socket.emit('get_recipes'); }); } _applyServerRecipes(serverRecipes) { this.recipes.clear(); // Server may return array or object keyed by id const asList = Array.isArray(serverRecipes) ? serverRecipes : Object.values(serverRecipes); for (const recipe of asList) { if (!recipe.id) continue; // Normalise materials: server uses { itemId: qty } objects, client expects array let materials = recipe.materials; if (materials && !Array.isArray(materials)) { materials = Object.entries(materials).map(([id, quantity]) => ({ id, quantity })); } // Normalise results similarly let results = recipe.results; if (results && !Array.isArray(results)) { results = Object.entries(results) .filter(([k]) => k !== 'experience') .map(([id, quantity]) => ({ id, quantity })); } this.recipes.set(recipe.id, { ...recipe, materials: materials || [], results: results || [], category: recipe.type || recipe.category || 'items', unlocked: false // will be resolved by checkRecipeUnlocks() }); } } // ------------------------------------------------------------------ // // Runtime // ------------------------------------------------------------------ // addRecipe(id, recipe) { recipe.id = id; recipe.unlocked = false; this.recipes.set(id, recipe); } update(deltaTime) { this.checkRecipeUnlocks(); } checkRecipeUnlocks() { const skillSystem = this.game.systems.skillSystem; if (!skillSystem) return; for (const [id, recipe] of this.recipes) { if (recipe.unlocked) continue; let canUnlock = true; if (recipe.requirements) { for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) { if (skillSystem.getSkillLevel(skillName) < requiredLevel) { canUnlock = false; break; } } } if (canUnlock) { recipe.unlocked = true; console.log(`[CRAFTING] Recipe unlocked: ${recipe.name}`); } } } getRecipesByCategory(category) { return Array.from(this.recipes.values()) .filter(r => r.category === category || r.type === category); } canCraftRecipe(recipeId) { const recipe = this.recipes.get(recipeId); const skillSystem = this.game.systems.skillSystem; const inventory = this.game.systems.inventory; if (!recipe) return false; if (recipe.requirements && skillSystem) { for (const [skillName, requiredLevel] of Object.entries(recipe.requirements)) { if (skillSystem.getSkillLevel(skillName) < requiredLevel) return false; } } if (recipe.materials && inventory) { for (const mat of recipe.materials) { if (!inventory.hasItem(mat.id, mat.quantity)) return false; } } return true; } getMissingMaterials(recipeId) { const recipe = this.recipes.get(recipeId); const inventory = this.game.systems.inventory; if (!recipe?.materials) return []; const missing = []; for (const mat of recipe.materials) { let current = 0; if (inventory?.getItemCount) { try { current = inventory.getItemCount(mat.id) || 0; } catch (_) {} } const required = mat.quantity || 0; if (current < required) { missing.push({ id: mat.id, required, current, missing: required - current }); } } return missing; } async craftRecipe(recipeId) { const recipe = this.recipes.get(recipeId); if (!recipe || !this.canCraftRecipe(recipeId)) return false; console.log(`[CRAFTING] Crafting: ${recipe.name}`); if (recipe.materials) { for (const mat of recipe.materials) { this.game.systems.inventory.removeItem(mat.id, mat.quantity); } } if (recipe.experience && this.game.systems.skillSystem) { this.game.systems.skillSystem.awardCraftingExperience(recipe.experience); } await new Promise(resolve => setTimeout(resolve, recipe.craftingTime || 3000)); if (recipe.results) { for (const result of recipe.results) { this.game.systems.inventory.addItem(result.id, result.quantity); } } if (this.game.systems.questSystem) { this.game.systems.questSystem.onItemCrafted?.(); } console.log(`[CRAFTING] Done: ${recipe.name}`); return true; } selectRecipe(recipeId) { this.selectedRecipe = this.recipes.get(recipeId); return this.selectedRecipe; } getSelectedRecipe() { return this.selectedRecipe; } // ------------------------------------------------------------------ // // UI // ------------------------------------------------------------------ // updateUI() { this.updateRecipeList(); this.updateCraftingDetails(); this.updateCraftingInfo(); } updateRecipeList() { const listEl = document.getElementById('recipeList'); if (!listEl) return; if (!this._loaded) { listEl.innerHTML = '
Loading recipes from server...
No recipes available in this category
'; return; } recipes.forEach(recipe => { const el = document.createElement('div'); el.className = 'recipe-item'; el.dataset.recipeId = recipe.id; const canCraft = this.canCraftRecipe(recipe.id); const missingMats = this.getMissingMaterials(recipe.id); const skillSystem = this.game.systems.skillSystem; let skillsMet = true; if (recipe.requirements && skillSystem) { for (const [skill, level] of Object.entries(recipe.requirements)) { if (skillSystem.getSkillLevel(skill) < level) { skillsMet = false; break; } } } if (!skillsMet) el.classList.add('locked'); else if (!canCraft) el.classList.add('missing-materials'); else el.classList.add('can-craft'); const reqText = recipe.requirements ? Object.entries(recipe.requirements).map(([s, l]) => `${s}: ${l}`).join(', ') : 'None'; const matsHtml = recipe.materials.map(mat => { const mis = missingMats.find(m => m.id === mat.id); const cur = mis ? mis.current : (this.game.systems.inventory?.getItemCount(mat.id) || 0); const cls = mis ? 'material-item missing' : 'material-item'; return `Choose a recipe from the list to see details and craft items.
${recipe.description || ''}
No special requirements
'}