161 lines
5.3 KiB
JavaScript
161 lines
5.3 KiB
JavaScript
/**
|
|
* 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;
|