196 lines
6.2 KiB
JavaScript
196 lines
6.2 KiB
JavaScript
import React, { useState, useEffect } from "react";
|
|
import { useSocket } from "../../../hooks/useSocket";
|
|
import GameDataManager from "../../../services/GameDataManager";
|
|
import "./styles/CraftingTab.css";
|
|
import CategorySelector from "../components/CategorySelector";
|
|
import CraftModal from "./components/CraftModal";
|
|
|
|
const CraftingTab = () => {
|
|
const { socket } = useSocket();
|
|
|
|
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);
|
|
|
|
useEffect(() => {
|
|
const manifestCategories = GameDataManager.getRecipeCategories();
|
|
setCategories(manifestCategories);
|
|
if (manifestCategories.length > 0 && !activeCategory) {
|
|
setActiveCategory(manifestCategories[0].id);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (activeCategory) {
|
|
const filteredRecipes =
|
|
GameDataManager.getRecipesByCategory(activeCategory);
|
|
setRecipes(filteredRecipes);
|
|
}
|
|
}, [activeCategory]);
|
|
|
|
useEffect(() => {
|
|
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(() => {
|
|
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?.finishAt]);
|
|
|
|
const getOwnedAmount = (itemId) => {
|
|
const item = userInventory.find((i) => (i.itemId || i.id) === itemId);
|
|
return item ? item.quantity : 0;
|
|
};
|
|
|
|
const handleStartCrafting = (recipe) => {
|
|
if (activeCraft) return;
|
|
socket.emit("player:craft_item", { recipeId: recipe.id });
|
|
};
|
|
|
|
return (
|
|
<div className="tab-content active">
|
|
<div className="crafting-container">
|
|
{activeCraft && (
|
|
<div className="active-craft-panel">
|
|
<div className="craft-info">
|
|
<span>
|
|
<i className="fas fa-hammer fa-spin"></i> Assembling:{" "}
|
|
{activeCraft.name}
|
|
</span>
|
|
<span className="time-left">{activeCraft.timeLeft}s</span>
|
|
</div>
|
|
<div className="progress-bar-bg">
|
|
<div
|
|
className="progress-bar-fill"
|
|
style={{
|
|
width: `${Math.min(100, ((activeCraft.totalTime - activeCraft.timeLeft) / activeCraft.totalTime) * 100)}%`,
|
|
}}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="crafting-header">
|
|
<h2>
|
|
<i className="fas fa-microchip"></i> Fabrication Unit
|
|
</h2>
|
|
</div>
|
|
|
|
<CategorySelector
|
|
categories={categories}
|
|
activeCategory={activeCategory}
|
|
onCategoryChange={setActiveCategory}
|
|
/>
|
|
|
|
<div className="crafting-grid">
|
|
{recipes.map((recipe) => {
|
|
const isThisRecipeCrafting = activeCraft?.recipeId === recipe.id;
|
|
const canCraft = recipe.ingredients.every(
|
|
(ing) => getOwnedAmount(ing.itemId) >= ing.quantity,
|
|
);
|
|
|
|
return (
|
|
<div
|
|
key={recipe.id}
|
|
className={`recipe-card ${!canCraft && !isThisRecipeCrafting ? "insufficient-resources" : ""} ${isThisRecipeCrafting ? "crafting-active" : ""}`}
|
|
onClick={() => setSelectedRecipe(recipe)}
|
|
>
|
|
<div className="recipe-icon">
|
|
{recipe.texture ? (
|
|
<img src={recipe.texture} alt={recipe.displayName} />
|
|
) : (
|
|
<div className="fallback-icon">{recipe.displayName[0]}</div>
|
|
)}
|
|
</div>
|
|
<div className="recipe-info-main">
|
|
<span className="recipe-name">{recipe.displayName}</span>
|
|
<div className="recipe-badges">
|
|
<span className="badge-time">
|
|
<i className="fas fa-clock"></i> {recipe.time_seconds}s
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{isThisRecipeCrafting && (
|
|
<div className="craft-overlay-mini">
|
|
<i className="fas fa-sync fa-spin"></i>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<CraftModal
|
|
recipe={selectedRecipe}
|
|
onClose={() => !activeCraft && setSelectedRecipe(null)}
|
|
onStartCraft={handleStartCrafting}
|
|
activeCraft={activeCraft}
|
|
getOwnedAmount={getOwnedAmount}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CraftingTab;
|