diff --git a/client/src/views/GameInterface/tabs/CraftingTab.jsx b/client/src/views/GameInterface/tabs/CraftingTab.jsx index 1318b4d..2bd1045 100644 --- a/client/src/views/GameInterface/tabs/CraftingTab.jsx +++ b/client/src/views/GameInterface/tabs/CraftingTab.jsx @@ -11,7 +11,6 @@ const CraftingTab = () => { const [categories, setCategories] = useState([]); const [recipes, setRecipes] = useState([]); const [userInventory, setUserInventory] = useState([]); - const [activeCategory, setActiveCategory] = useState(""); const [selectedRecipe, setSelectedRecipe] = useState(null); const [activeCraft, setActiveCraft] = useState(null); @@ -19,7 +18,6 @@ const CraftingTab = () => { useEffect(() => { const manifestCategories = GameDataManager.getRecipeCategories(); setCategories(manifestCategories); - if (manifestCategories.length > 0 && !activeCategory) { setActiveCategory(manifestCategories[0].id); } @@ -29,7 +27,6 @@ const CraftingTab = () => { if (activeCategory) { const filteredRecipes = GameDataManager.getRecipesByCategory(activeCategory); - console.log(filteredRecipes); setRecipes(filteredRecipes); } }, [activeCategory]); @@ -38,30 +35,67 @@ const CraftingTab = () => { if (!socket) return; socket.emit("player:get_inventory"); + socket.emit("player:check_active_craft"); const handleInventory = (data) => setUserInventory(data); + + const handleCraftStarted = (data) => { + const recipeData = GameDataManager.getRecipe(data.recipeId); + const now = Date.now(); + const diff = (data.finishAt - now) / 1000; + + if (diff <= 0) { + setActiveCraft(null); + return; + } + + setActiveCraft({ + recipeId: data.recipeId, + name: recipeData?.displayName || data.recipeId, + finishAt: data.finishAt, + totalTime: data.totalTime || recipeData?.time_seconds || diff, + timeLeft: Math.max(0, Math.ceil(diff)), + }); + + if (recipeData) { + setSelectedRecipe(recipeData); + } + }; + + const handleCraftSuccess = () => { + setActiveCraft(null); + setSelectedRecipe(null); + socket.emit("player:get_inventory"); + }; + socket.on("player:inventory_data", handleInventory); + socket.on("player:craft_started", handleCraftStarted); + socket.on("player:craft_success", handleCraftSuccess); return () => { socket.off("player:inventory_data", handleInventory); + socket.off("player:craft_started", handleCraftStarted); + socket.off("player:craft_success", handleCraftSuccess); }; }, [socket]); useEffect(() => { - let timer; - if (activeCraft && activeCraft.timeLeft > 0) { - timer = setInterval(() => { - setActiveCraft((prev) => ({ - ...prev, - timeLeft: Math.max(0, prev.timeLeft - 1), - })); - }, 1000); - } else if (activeCraft && activeCraft.timeLeft === 0) { - setActiveCraft(null); - socket.emit("player:get_inventory"); - } + if (!activeCraft) return; + + const timer = setInterval(() => { + const now = Date.now(); + const diff = Math.max(0, Math.ceil((activeCraft.finishAt - now) / 1000)); + + if (diff <= 0) { + clearInterval(timer); + setActiveCraft(null); + } else { + setActiveCraft((prev) => (prev ? { ...prev, timeLeft: diff } : null)); + } + }, 1000); + return () => clearInterval(timer); - }, [activeCraft, socket]); + }, [activeCraft?.finishAt]); const getOwnedAmount = (itemId) => { const item = userInventory.find((i) => (i.itemId || i.id) === itemId); @@ -70,19 +104,7 @@ const CraftingTab = () => { const handleStartCrafting = (recipe) => { if (activeCraft) return; - - socket.emit("player:craft_item", { - recipeId: recipe.id, - category: activeCategory, - }); - - setActiveCraft({ - name: recipe.displayName, - timeLeft: recipe.constructionTime, - totalTime: recipe.constructionTime, - }); - - setSelectedRecipe(null); + socket.emit("player:craft_item", { recipeId: recipe.id }); }; return ( @@ -101,7 +123,7 @@ const CraftingTab = () => {
@@ -122,6 +144,7 @@ const CraftingTab = () => {
{recipes.map((recipe) => { + const isThisRecipeCrafting = activeCraft?.recipeId === recipe.id; const canCraft = recipe.ingredients.every( (ing) => getOwnedAmount(ing.itemId) >= ing.quantity, ); @@ -129,51 +152,40 @@ const CraftingTab = () => { return (
setSelectedRecipe(recipe)} >
{recipe.texture ? ( {recipe.displayName} ) : ( -
- {recipe.id[0].toUpperCase()} -
+
{recipe.displayName[0]}
)}
-
{recipe.displayName}
- {recipe.constructionTime} - s + {recipe.time_seconds}s
- - {!canCraft && ( -
- + {isThisRecipeCrafting && ( +
+
)}
); })} - - {recipes.length === 0 && ( -
- No blueprints available in this sector. -
- )}
setSelectedRecipe(null)} + onClose={() => !activeCraft && setSelectedRecipe(null)} onStartCraft={handleStartCrafting} - isBusy={!!activeCraft} + activeCraft={activeCraft} getOwnedAmount={getOwnedAmount} /> diff --git a/client/src/views/GameInterface/tabs/components/CraftModal.jsx b/client/src/views/GameInterface/tabs/components/CraftModal.jsx index adfb828..be5c3a0 100644 --- a/client/src/views/GameInterface/tabs/components/CraftModal.jsx +++ b/client/src/views/GameInterface/tabs/components/CraftModal.jsx @@ -5,15 +5,13 @@ const CraftModal = ({ recipe, onClose, onStartCraft, - isBusy, + activeCraft, getOwnedAmount, }) => { if (!recipe) return null; - const displayName = recipe.resultItem?.name || recipe.name || recipe.id; - const craftTime = recipe.constructionTime || recipe.time || 0; - - // Перевірка, чи вистачає всіх ресурсів для крафту + const isBusy = !!activeCraft; + const outputQty = Object.values(recipe.output || {})[0] || 1; const canAfford = recipe.ingredients?.every( (ing) => getOwnedAmount(ing.itemId) >= ing.quantity, ); @@ -23,7 +21,7 @@ const CraftModal = ({
e.stopPropagation()}>

- Construction: {displayName} + Construction: {recipe.displayName}

- - + {activeCraft && activeCraft.recipeId === recipe.id ? ( +
+
+ Processing... {activeCraft.timeLeft}s +
+
+
+
+
+ ) : ( + <> + + + + )}
diff --git a/game-server/datapacks/original/data/recipes/alloys/steel.json b/game-server/datapacks/original/data/recipes/alloys/steel.json index 3fcc84e..db5e4b6 100644 --- a/game-server/datapacks/original/data/recipes/alloys/steel.json +++ b/game-server/datapacks/original/data/recipes/alloys/steel.json @@ -1,13 +1,10 @@ { "recipe": { - "inputs": [ - { "original:ingot_iron": 1 }, - { "original:ore_coal": 5 } - ], + "inputs": [{ "original:ingot_iron": 1 }, { "original:ore_coal": 5 }], "output": { "original:alloy_steel": 1 }, - "time_seconds": 180, + "time_seconds": 10, "requires": { "original:alloying": 0 } diff --git a/game-server/src/game/CraftManager.js b/game-server/src/game/CraftManager.js new file mode 100644 index 0000000..2364a5c --- /dev/null +++ b/game-server/src/game/CraftManager.js @@ -0,0 +1,71 @@ +const datapackLoader = require("../game/DatapackLoader"); +const { Inventory } = require("../models"); + +class CraftManager { + constructor() { + this.activeCrafts = new Map(); + } + + async startCraft(userId, recipeId, socket) { + if (this.activeCrafts.has(userId)) return { error: "Already crafting" }; + + const recipe = datapackLoader.getRecipe(recipeId); + if (!recipe) return { error: "Recipe not found" }; + + const craftTimeMs = (recipe.time_seconds || 0) * 1000; + const finishAt = Date.now() + craftTimeMs; + + const craftData = { + recipeId, + finishAt, + totalTime: recipe.time_seconds, + timer: setTimeout( + () => this.completeCraft(userId, recipeId, socket), + craftTimeMs, + ), + }; + + this.activeCrafts.set(userId, craftData); + return { recipeId, finishAt, totalTime: recipe.time_seconds }; + } + + async completeCraft(userId, recipeId, socket) { + try { + const recipe = datapackLoader.getRecipe(recipeId); + const outputItemId = Object.keys(recipe.output)[0]; + const outputQuantity = recipe.output[outputItemId]; + + const [newItem, created] = await Inventory.findOrCreate({ + where: { playerId: userId, itemId: outputItemId }, + defaults: { quantity: outputQuantity }, + }); + + if (!created) { + await newItem.increment("quantity", { by: outputQuantity }); + } + + this.activeCrafts.delete(userId); + + if (socket && socket.connected) { + socket.emit("player:craft_success", { recipeId }); + socket.emit("player:get_inventory"); + } + } catch (err) { + console.error("Complete craft error:", err); + } + } + + getExistingCraft(userId) { + const craft = this.activeCrafts.get(userId); + if (craft && craft.finishAt > Date.now()) { + return { + recipeId: craft.recipeId, + finishAt: craft.finishAt, + totalTime: craft.totalTime, + }; + } + return null; + } +} + +module.exports = new CraftManager(); diff --git a/game-server/src/game/DatapackLoader.js b/game-server/src/game/DatapackLoader.js index 3767ae4..0253602 100644 --- a/game-server/src/game/DatapackLoader.js +++ b/game-server/src/game/DatapackLoader.js @@ -73,7 +73,22 @@ class DatapackLoader { `🚀 Registry Ready: ${this.registry.items.size} Items, ${this.registry.dungeons.size} Dungeons, ${this.registry.languages.size} Langs, ${manifestCount} Manifests`, ); } + getRecipe(id) { + return this.registry.recipes.get(id); + } + getRecipesByCategory(category) { + const allRecipes = Array.from(this.registry.recipes.values()); + return allRecipes.filter((r) => r.category === category); + } + + getRecipeCategories() { + const allRecipes = Array.from(this.registry.recipes.values()); + const categories = new Set( + allRecipes.map((r) => r.category).filter(Boolean), + ); + return Array.from(categories); + } loadLanguages(langPath) { try { const files = fs.readdirSync(langPath).filter((f) => f.endsWith(".json")); diff --git a/game-server/src/sockets/handlers/craftingHandler.js b/game-server/src/sockets/handlers/craftingHandler.js index 767e42e..5582ca3 100644 --- a/game-server/src/sockets/handlers/craftingHandler.js +++ b/game-server/src/sockets/handlers/craftingHandler.js @@ -1,80 +1,89 @@ const datapackLoader = require("../../game/DatapackLoader"); const { Inventory } = require("../../models"); +const craftManager = require("../../game/CraftManager"); module.exports = (io, socket) => { const userId = socket.user?.id; + const sendStatus = () => { + const existing = craftManager.getExistingCraft(userId); + if (existing) { + socket.emit("player:craft_started", existing); + } + }; + + socket.on("player:check_active_craft", () => { + sendStatus(); + }); + socket.on("player:get_recipe_categories", () => { try { const categories = datapackLoader.getRecipeCategories(); socket.emit("player:recipe_categories_data", categories); } catch (err) { - console.error("Error getting categories:", err.message); + console.error(err.message); } }); socket.on("player:get_recipes", ({ category }) => { try { if (!category) return; - const rawRecipes = datapackLoader.getRecipesByCategory(category); - const recipeIds = rawRecipes.map((r) => r.id); - - socket.emit("player:recipes_data", { - category, - recipeIds: recipeIds, - }); + socket.emit("player:recipes_data", { category, recipeIds }); } catch (err) { - console.error("Error fetching recipes:", err.message); + console.error(err.message); } }); - socket.on("player:craft_item", async ({ recipeId, category }) => { + socket.on("player:craft_item", async ({ recipeId }) => { try { - const recipe = datapackLoader.getRecipe(category, recipeId); + if (craftManager.getExistingCraft(userId)) { + return socket.emit("error", { message: "Already crafting" }); + } + const recipe = datapackLoader.getRecipe(recipeId); if (!recipe) return socket.emit("error", { message: "Recipe not found" }); - for (const ing of recipe.ingredients) { + for (const ing of recipe.inputs) { + const itemId = Object.keys(ing)[0]; + const quantity = ing[itemId]; const invItem = await Inventory.findOne({ - where: { playerId: userId, itemId: ing.itemId }, + where: { playerId: userId, itemId: itemId }, }); - if (!invItem || invItem.quantity < ing.quantity) { - return socket.emit("error", { - message: `Недостатньо ресурсів для ${recipeId}`, - }); + if (!invItem || invItem.quantity < quantity) { + return socket.emit("error", { message: `Not enough resources` }); } } - for (const ing of recipe.ingredients) { + for (const ing of recipe.inputs) { + const itemId = Object.keys(ing)[0]; + const quantity = ing[itemId]; const invItem = await Inventory.findOne({ - where: { playerId: userId, itemId: ing.itemId }, + where: { playerId: userId, itemId: itemId }, }); - if (invItem.quantity === ing.quantity) { + if (invItem.quantity === quantity) { await invItem.destroy(); } else { - await invItem.decrement("quantity", { by: ing.quantity }); + await invItem.decrement("quantity", { by: quantity }); } } - const [newItem, created] = await Inventory.findOrCreate({ - where: { playerId: userId, itemId: recipe.id }, - defaults: { quantity: 1 }, - }); + const result = await craftManager.startCraft(userId, recipeId, socket); - if (!created) { - await newItem.increment("quantity", { by: 1 }); + if (result.error) { + return socket.emit("error", { message: result.error }); } - socket.emit("player:craft_success", { recipeId }); - + socket.emit("player:craft_started", result); socket.emit("player:get_inventory"); } catch (err) { - console.error("Crafting error:", err.message); + console.error(err.message); socket.emit("error", { message: "Internal Crafting Error" }); } }); + + sendStatus(); };