404 lines
15 KiB
JavaScript
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;
|
|
}
|