Game-Server/Client/js/systems/CraftingSystem.js

404 lines
15 KiB
JavaScript

/**
* 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 = '<div style="text-align:center;padding:2rem;color:var(--text-secondary)"><i class="fas fa-spinner fa-spin"></i><p>Loading recipes from server...</p></div>';
return;
}
const recipes = this.getRecipesByCategory(this.currentCategory);
listEl.innerHTML = '';
if (recipes.length === 0) {
listEl.innerHTML = '<p class="no-recipes">No recipes available in this category</p>';
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 `<div class="${cls}">
<span class="material-name">${mat.id}</span>
<span class="material-quantity">${cur}/${mat.quantity}</span>
</div>`;
}).join('');
el.innerHTML = `
<div class="recipe-header">
<h4>${recipe.name}</h4>
<span class="recipe-level">Level ${reqText}</span>
</div>
<div class="recipe-description">${recipe.description || ''}</div>
<div class="recipe-materials">${matsHtml}</div>
${missingMats.length > 0 ? `
<div class="missing-materials-text">
<i class="fas fa-exclamation-triangle"></i>
Missing: ${missingMats.map(m => `${m.missing}x ${m.id}`).join(', ')}
</div>` : ''}
<div class="recipe-time">
<i class="fas fa-clock"></i>
<span>${(recipe.craftingTime || 0) / 1000}s</span>
</div>
`;
el.addEventListener('click', () => {
this.selectRecipe(recipe.id);
this.updateCraftingDetails();
});
listEl.appendChild(el);
});
}
updateCraftingDetails() {
const detailsEl = document.getElementById('craftingDetails');
if (!detailsEl) return;
if (!this.selectedRecipe) {
detailsEl.innerHTML = `
<div class="selected-recipe">
<h3>Select a Recipe</h3>
<p>Choose a recipe from the list to see details and craft items.</p>
</div>`;
return;
}
const recipe = this.selectedRecipe;
const canCraft = this.canCraftRecipe(recipe.id);
detailsEl.innerHTML = `
<div class="selected-recipe">
<h3>${recipe.name}</h3>
<p class="recipe-description">${recipe.description || ''}</p>
<div class="recipe-requirements">
<h4>Requirements:</h4>
${recipe.requirements
? Object.entries(recipe.requirements).map(([s, l]) =>
`<div class="requirement-item">
<span class="skill-name">${s}</span>
<span class="skill-level">Level ${l}</span>
</div>`).join('')
: '<p>No special requirements</p>'}
</div>
<div class="recipe-materials-needed">
<h4>Materials Needed:</h4>
${recipe.materials.map(mat =>
`<div class="material-needed">
<span class="material-name">${mat.id}</span>
<span class="material-needed">x${mat.quantity}</span>
<span class="material-have">Have: ${this.game.systems.inventory?.getItemCount(mat.id) || 0}</span>
</div>`).join('')}
</div>
<div class="recipe-results">
<h4>Results:</h4>
${recipe.results.map(r =>
`<div class="result-item">
<span class="result-name">${r.id}</span>
<span class="result-quantity">x${r.quantity}</span>
</div>`).join('')}
</div>
<div class="recipe-info">
<div class="experience-reward">
<i class="fas fa-star"></i>
<span>${recipe.experience || 0} XP</span>
</div>
<div class="crafting-time">
<i class="fas fa-clock"></i>
<span>${(recipe.craftingTime || 0) / 1000} seconds</span>
</div>
</div>
<button class="btn btn-primary craft-btn ${canCraft ? '' : 'disabled'}"
${canCraft ? `onclick="window.game.systems.crafting.craftRecipe('${recipe.id}')"` : 'disabled'}>
${canCraft ? 'Craft Item' : 'Cannot Craft'}
</button>
</div>`;
}
updateCraftingInfo() {
const skillSystem = this.game.systems.skillSystem;
if (!skillSystem) return;
const craftingLevel = skillSystem.getSkillLevel('crafting');
const craftingExp = skillSystem.getSkillExperience('crafting');
const expNeeded = skillSystem.getExperienceNeeded('crafting');
const levelEl = document.getElementById('craftingLevel');
const expEl = document.getElementById('craftingExp');
if (levelEl) levelEl.textContent = craftingLevel;
if (expEl) expEl.textContent = `${craftingExp}/${expNeeded}`;
}
switchCategory(category) {
this.currentCategory = category;
this.selectedRecipe = null;
if (window.smartSaveManager?.isMultiplayer || this.game?.isRunning) {
this.updateUI();
}
}
}
// Export for use in GameEngine
if (typeof module !== 'undefined' && module.exports) {
module.exports = CraftingSystem;
}