173 lines
5.5 KiB
JavaScript
173 lines
5.5 KiB
JavaScript
import React, { useEffect, useState, useMemo } from "react";
|
|
import Card from "../../../components/ui/Card";
|
|
import { useSocket } from "../../../hooks/useSocket";
|
|
import gameDataManager from "../../../services/GameDataManager";
|
|
import "./styles/QuestsTab.css";
|
|
|
|
const QuestsTab = () => {
|
|
const { socket } = useSocket();
|
|
const [quests, setQuests] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [activeTab, setActiveTab] = useState("ACTIVE");
|
|
|
|
useEffect(() => {
|
|
if (!socket) return;
|
|
|
|
socket.emit("quest:get_list");
|
|
|
|
const localize = (q) => {
|
|
const staticData = gameDataManager.getQuest(q.id);
|
|
return {
|
|
...q,
|
|
displayName: staticData?.displayName || q.id,
|
|
description: staticData?.description || "",
|
|
objectives: q.objectives.map((obj, idx) => ({
|
|
...obj,
|
|
description: staticData?.objectives[idx]?.description || obj.type,
|
|
})),
|
|
};
|
|
};
|
|
|
|
const handleQuestData = (data) => {
|
|
const uniqueQuests = new Map();
|
|
data.forEach((q) => {
|
|
uniqueQuests.set(q.id, localize(q));
|
|
});
|
|
|
|
setQuests(Array.from(uniqueQuests.values()));
|
|
setLoading(false);
|
|
};
|
|
|
|
const handleQuestUpdate = (updatedQuest) => {
|
|
setQuests((prev) => {
|
|
const localized = localize(updatedQuest);
|
|
const questMap = new Map(prev.map((q) => [q.id, q]));
|
|
questMap.set(localized.id, localized);
|
|
return Array.from(questMap.values());
|
|
});
|
|
};
|
|
|
|
socket.on("quest:list_data", handleQuestData);
|
|
socket.on("quest:update", handleQuestUpdate);
|
|
|
|
return () => {
|
|
socket.off("quest:list_data", handleQuestData);
|
|
socket.off("quest:update", handleQuestUpdate);
|
|
};
|
|
}, [socket]);
|
|
|
|
const filteredQuests = useMemo(() => {
|
|
return quests.filter((q) =>
|
|
activeTab === "ACTIVE"
|
|
? q.status === "active" || q.status === "ready"
|
|
: q.status === "completed",
|
|
);
|
|
}, [quests, activeTab]);
|
|
|
|
const renderObjective = (obj, index) => {
|
|
const progress = Math.min(
|
|
(obj.currentAmount / obj.requiredAmount) * 100,
|
|
100,
|
|
);
|
|
return (
|
|
<div key={index} className="objective-item">
|
|
<div className="objective-info">
|
|
<span className="objective-desc">{obj.description || obj.type}</span>
|
|
<span className="objective-count">
|
|
{obj.currentAmount} / {obj.requiredAmount}
|
|
</span>
|
|
</div>
|
|
<div className="objective-progress-track">
|
|
<div
|
|
className="objective-progress-fill"
|
|
style={{ width: `${progress}%` }}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="quests-container">
|
|
<div className="dash-scanline"></div>
|
|
<div className="quests-header">
|
|
<h2 className="glitch-text" data-text="MISSION_LOG">
|
|
MISSION_LOG
|
|
</h2>
|
|
<div className="quest-tabs-nav">
|
|
<button
|
|
className={`nav-btn ${activeTab === "ACTIVE" ? "active" : ""}`}
|
|
onClick={() => setActiveTab("ACTIVE")}
|
|
>
|
|
ACTIVE_OPERATIONS
|
|
</button>
|
|
<button
|
|
className={`nav-btn ${activeTab === "COMPLETED" ? "active" : ""}`}
|
|
onClick={() => setActiveTab("COMPLETED")}
|
|
>
|
|
ARCHIVED_DATA
|
|
</button>
|
|
</div>
|
|
<div className="header-line"></div>
|
|
</div>
|
|
{loading ? (
|
|
<div className="loading-status">SCANNING_NEURAL_NETWORK...</div>
|
|
) : (
|
|
<div className="quests-grid">
|
|
{filteredQuests.length > 0 ? (
|
|
filteredQuests.map((quest) => (
|
|
<Card key={quest.id} className={`quest-card ${quest.status}`}>
|
|
<div className="card-tag">{quest.category || "MISSION"}</div>
|
|
<div className="quest-main">
|
|
<h3 className="quest-title">{quest.displayName}</h3>
|
|
<p className="quest-description">{quest.description}</p>
|
|
</div>
|
|
<div className="quest-objectives">
|
|
<div className="section-label">OBJECTIVES</div>
|
|
{quest.objectives.map((obj, idx) =>
|
|
renderObjective(obj, idx),
|
|
)}
|
|
</div>
|
|
<div className="quest-rewards">
|
|
<div className="section-label">REWARDS</div>
|
|
<div className="rewards-row">
|
|
{quest.rewards?.credits > 0 && (
|
|
<span className="reward-pill credits">
|
|
+{quest.rewards.credits} CR
|
|
</span>
|
|
)}
|
|
{quest.rewards?.xp > 0 && (
|
|
<span className="reward-pill xp">
|
|
+{quest.rewards.xp} XP
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{quest.status === "ready" && (
|
|
<button
|
|
className="claim-btn"
|
|
onClick={() =>
|
|
socket.emit("quest:claim_reward", { questId: quest.id })
|
|
}
|
|
>
|
|
COMPLETE_MISSION
|
|
</button>
|
|
)}
|
|
{quest.status === "completed" && (
|
|
<div className="completed-stamp">MISSION_ACCOMPLISHED</div>
|
|
)}
|
|
</Card>
|
|
))
|
|
) : (
|
|
<div className="no-quests">
|
|
<p>NO_{activeTab}_SIGNALS_FOUND</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default QuestsTab;
|