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.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();
};