diff --git a/Admin Panel.md b/Admin Panel.md new file mode 100644 index 0000000..31b703c --- /dev/null +++ b/Admin Panel.md @@ -0,0 +1,105 @@ +# Admin Panel +## - Items List +### Panel Setup +- Tags (Dynamic) + - Default (all) + - Alloys + - Circuits + - Customizables + - Ingots + - Materials + - Ores + - Personal + - Shop + - Space Ships + - Shields + - Weapons +## - Hostiles List +### Panel Setup +- Tags (Dynamic) + - Default (all) + - Ground Units + - Space Ships +## - Player List +- Tags (Dynamic) + - Default (all) + - Members + - Moderators + - Admins +## - Permissions +### Panel hierarchy +- Roles (Static) + - Edit + - Role Tag + - Role Name + - Permission Nodes + - Chat Font Color + - New + - Role Tag + - Role Name + - Permission Nodes + - Chat Font Color +- Users (Static) + - Edit + - Permission Nodes +### Default Roles +- Members Permission Nodes + - permission.node.player.text.chat + - permission.node.player.text.chat.message + - permission.node.player.text.chat.message.alliance + - permission.node.player.text.chat.message.direct + - permission.node.player.text.chat.message.fleet + - permission.node.player.text.color +- Moderator Permission Nodes + - roles.members.* + - permission.node.player.server.ban + - permission.node.player.server.timeout + - permission.node.player.server.unban + - permission.node.player.text.ban + - permission.node.player.text.timeout.["time_in_seconds"] + - permission.node.player.text.unban +- Admin Permission Nodes + - roles.moderators.* + - permission.node.player.clean.database.all + - permission.node.player.clean.database.item + - permission.node.player.clean.inventory.all + - permission.node.player.clean.inventory.item + - permission.node.player.console + - permission.node.player.give.exp + - permission.node.player.give.item + - permission.node.player.give.skills + - permission.node.player.permission.add.["permission.node.*"] + - permission.node.player.permission.edit.["permission.node.*"] + - permission.node.player.permission.remove.["permission.node.*"] + - permission.node.player.role.add.["permission.node.*"] + - permission.node.player.role.edit + - permission.node.player.role.give + - permission.node.player.role.hierarchy + - permission.node.player.role.new + - permission.node.player.role.remove + - permission.node.player.role.remove.["permission.node.*"] +- Super Admin Permission Nodes (First Person On Server) + - permission.node.player.bypass +### Extra Nodes +- permission.node.tab.*.(allow/deny) +- permission.node.tab.dashboard.(allow/deny) +- permission.node.tab.dungeons.* + - ["permission.node.tab.dungeons.{datapackId}.{dungeonId}.(allow/deny)"] +- permission.node.tab.skills.* + - ["permission.node.tab.skills.{datapackId}.{skillsId}.(allow/deny)"] +- permission.node.tab.inventory.* + - ["permission.node.tab.inventory.{datapackId}.{core_systemId}.(allow/deny)"] +- permission.node.tab.shop.* + - ["permission.node.tab.shop.{datapackId}.{shopId}.(allow/deny)"] +- permission.node.tab.crafting.* + - ["permission.node.tab.crafting.{datapackId}.{craftingId}.(allow/deny)"] +- permission.node.tab.admin.* + - ["permission.node.tab.admin.{datapackId}.{adminId}.(allow/deny)"] +- permission.node.tab.chat.* + - ["permission.node.tab.chat.{datapackId}.{chatId}.(allow/deny)"] +- permission.node.tab.alerts.(allow/deny) +- permission.node.generate.credits.online.*.(allow/deny) +- permission.node.generate.credits.offline.*.(allow/deny) +- permission.node.generate.datacores.online.*.(allow/deny) +- permission.node.generate.datacores.offline.*.(allow/deny) +- permission.node.player.nickname.(allow/deny) \ No newline at end of file diff --git a/client/src/components/Console/Console.jsx b/client/src/components/Console/Console.jsx index 08e7123..7b01929 100644 --- a/client/src/components/Console/Console.jsx +++ b/client/src/components/Console/Console.jsx @@ -2,8 +2,9 @@ import React, { useState, useEffect, useRef } from "react"; import "./Console.css"; import { useSocket } from "../../hooks/useSocket"; import ConsoleManager from "../../services/ConsoleManager"; +import PlayerManager from "../../services/PlayerManager"; // Імпортуємо менеджер гравців -const Console = ({ players = [] }) => { +const Console = () => { const { socket } = useSocket(); const [isOpen, setIsOpen] = useState(false); const [input, setInput] = useState(""); @@ -11,6 +12,8 @@ const Console = ({ players = [] }) => { const [selectedIndex, setSelectedIndex] = useState(0); const [logs, setLogs] = useState(ConsoleManager.logs); + const [playerNames, setPlayerNames] = useState([]); + const inputRef = useRef(null); const scrollRef = useRef(null); @@ -18,6 +21,21 @@ const Console = ({ players = [] }) => { ConsoleManager.init(socket, setLogs); }, [socket]); + useEffect(() => { + const updatePlayers = (data) => { + setPlayerNames([...data.online, ...data.offline]); + }; + + const unsubscribe = PlayerManager.subscribe(updatePlayers); + + setPlayerNames([ + ...PlayerManager.onlinePlayers, + ...PlayerManager.offlinePlayers, + ]); + + return () => unsubscribe(); + }, []); + useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; @@ -36,12 +54,11 @@ const Console = ({ players = [] }) => { }, []); useEffect(() => { - setSuggestions(ConsoleManager.getSuggestions(input, players)); + setSuggestions(ConsoleManager.getSuggestions(input, playerNames)); setSelectedIndex(0); - }, [input, players]); + }, [input, playerNames]); const handleKeyDown = (e) => { - // 1. Tab для автозаповнення if (e.key === "Tab" && suggestions.length > 0) { e.preventDefault(); const parts = input.split(" "); @@ -74,6 +91,7 @@ const Console = ({ players = [] }) => { const handleSubmit = (e) => { e.preventDefault(); + if (!input.trim()) return; ConsoleManager.execute(input); setInput(""); }; diff --git a/client/src/config/api.js b/client/src/config/api.js new file mode 100644 index 0000000..5d3a7ad --- /dev/null +++ b/client/src/config/api.js @@ -0,0 +1,20 @@ +const getActiveServer = () => { + const saved = localStorage.getItem("activeServer"); + if (!saved) return null; + try { + return JSON.parse(saved); + } catch (e) { + return null; + } +}; + +export const getServerUrl = () => { + const server = getActiveServer(); + return server ? server.connectUrl : null; +}; + +export const config = { + get serverUrl() { + return getServerUrl(); + }, +}; diff --git a/client/src/context/SocketContext.jsx b/client/src/context/SocketContext.jsx index 049f8af..47b5df8 100644 --- a/client/src/context/SocketContext.jsx +++ b/client/src/context/SocketContext.jsx @@ -6,6 +6,7 @@ import React, { useEffect, } from "react"; import { io } from "socket.io-client"; +import PlayerManager from "../services/PlayerManager"; // Імпортуємо менеджер export const SocketContext = createContext(null); @@ -19,12 +20,12 @@ export const SocketProvider = ({ children }) => { if (socketRef.current?.connected) { return socketRef.current; } + const userInfo = JSON.parse(localStorage.getItem("user")); + const newSocket = io(url, { - auth: { token, username: userInfo.username }, + auth: { token, username: userInfo?.username }, transports: ["websocket"], - upgrade: true, - withCredentials: true, reconnectionAttempts: 5, }); @@ -33,8 +34,19 @@ export const SocketProvider = ({ children }) => { setIsConnected(true); }); + newSocket.on("session:ready", (data) => { + PlayerManager.setInitialState(data.onlinePlayers, data.offlinePlayers); + }); + + newSocket.on("player:joined", (data) => { + PlayerManager.handlePlayerJoined(data.username); + }); + + newSocket.on("player:left", (data) => { + PlayerManager.handlePlayerLeft(data.username); + }); + newSocket.on("disconnect", (reason) => { - console.log("❌ Disconnected:", reason); setIsConnected(false); }); diff --git a/client/src/services/ConsoleManager.js b/client/src/services/ConsoleManager.js index 6131296..51cd7c4 100644 --- a/client/src/services/ConsoleManager.js +++ b/client/src/services/ConsoleManager.js @@ -2,7 +2,6 @@ import GameDataManager from "./GameDataManager"; class ConsoleManager { constructor() { - // Залишили тільки вказані команди this.commands = ["/clear", "/give", "/set_exp"]; this.logs = ["[System] Console Manager Ready. Press F9 to hide."]; this.history = []; @@ -27,46 +26,45 @@ class ConsoleManager { } getSuggestions(input, players = []) { + const safePlayers = (players || []).filter((p) => typeof p === "string"); + const parts = input.split(" "); if (parts.length === 0) return []; const cmd = parts[0].toLowerCase(); - // 1. Пропозиції команд if (parts.length === 1 && input.startsWith("/")) { return this.commands.filter((c) => c.startsWith(cmd)); } - // 2. Логіка для /give (Гравець -> Предмет) if (cmd === "/give") { if (parts.length === 2) { const search = parts[1].toLowerCase(); - return players.filter((p) => p.toLowerCase().startsWith(search)); + return safePlayers.filter((p) => p.toLowerCase().startsWith(search)); } if (parts.length === 3) { const search = parts[2].toLowerCase(); const itemIds = Array.from(GameDataManager.items.keys()); - return itemIds.filter((id) => id.includes(search)).slice(0, 15); + return itemIds + .filter((id) => id.toLowerCase().includes(search)) + .slice(0, 15); } } - // 3. Логіка для /set_exp (Гравець -> Кількість) if (cmd === "/set_exp") { if (parts.length === 2) { const search = parts[1].toLowerCase(); - return players.filter((p) => p.toLowerCase().startsWith(search)); + return safePlayers.filter((p) => p.toLowerCase().startsWith(search)); } } - // 4. Логіка для /clear (Гравець) if (cmd === "/clear" && parts.length === 2) { const search = parts[1].toLowerCase(); - return players.filter((p) => p.toLowerCase().startsWith(search)); + return safePlayers.filter((p) => p.toLowerCase().startsWith(search)); } return []; } - execute(input) { const trimmed = input.trim(); if (!trimmed) return; diff --git a/client/src/services/GameDataManager.js b/client/src/services/GameDataManager.js index e83e565..81d07b5 100644 --- a/client/src/services/GameDataManager.js +++ b/client/src/services/GameDataManager.js @@ -9,7 +9,7 @@ class GameDataManager { this.enemies = new Map(); this.translations = {}; this.manifest = {}; - this.currentLang = localStorage.getItem("selected_lang") || "en_us"; + this.currentLang = localStorage.getItem("selected_lang") || "en_US"; this.isLoaded = false; } diff --git a/client/src/services/PlayerManager.js b/client/src/services/PlayerManager.js new file mode 100644 index 0000000..3f431d9 --- /dev/null +++ b/client/src/services/PlayerManager.js @@ -0,0 +1,46 @@ +class PlayerManager { + constructor() { + this.onlinePlayers = []; + this.offlinePlayers = []; + this.listeners = new Set(); + } + + setInitialState(online, offline) { + this.onlinePlayers = online || []; + this.offlinePlayers = offline || []; + + this.notify(); + } + + handlePlayerJoined(username) { + if (!this.onlinePlayers.includes(username)) { + this.onlinePlayers.push(username); + this.offlinePlayers = this.offlinePlayers.filter((u) => u !== username); + this.notify(); + } + } + + handlePlayerLeft(username) { + this.onlinePlayers = this.onlinePlayers.filter((u) => u !== username); + if (!this.offlinePlayers.includes(username)) { + this.offlinePlayers.push(username); + } + this.notify(); + } + + subscribe(listener) { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + notify() { + this.listeners.forEach((listener) => + listener({ + online: this.onlinePlayers, + offline: this.offlinePlayers, + }), + ); + } +} + +export default new PlayerManager(); diff --git a/client/src/views/GameInterface/GameInterface.jsx b/client/src/views/GameInterface/GameInterface.jsx index 39222a8..a13f492 100644 --- a/client/src/views/GameInterface/GameInterface.jsx +++ b/client/src/views/GameInterface/GameInterface.jsx @@ -12,27 +12,25 @@ import BaseTab from "./tabs/BaseTab"; import QuestsTab from "./tabs/QuestsTab"; import ShopTab from "./tabs/ShopTab"; import CraftingTab from "./tabs/CraftingTab"; - +import ChatTab from "./tabs/ChatTab"; +import ItemListTab from "./tabs/ItemListTab.jsx"; +import Notification from "./tabs/NotificationTab.jsx"; import DungeonScreen from "./components/DungeonScreen"; import { useSocket } from "../../hooks/useSocket.js"; -import ItemListTab from "./tabs/ItemListTab.jsx"; const GameInterface = ({ onExit }) => { const [activeTab, setActiveTab] = useState("dashboard"); const [activeDungeonSession, setActiveDungeonSession] = useState(null); - const [onlinePlayers, setOnlinePlayers] = useState([]); const { socket } = useSocket(); useEffect(() => { if (!socket) return; socket.on("dungeon:started", (sessionData) => { - console.log("Deployment initiated:", sessionData); setActiveDungeonSession(sessionData); }); socket.on("dungeon:completed", (results) => { - console.log("Mission accomplished:", results); setActiveDungeonSession(null); }); @@ -81,19 +79,18 @@ const GameInterface = ({ onExit }) => { shop: , crafting: , itemlist: , + chat: , + notifications: , }; return (
- -
{tabs[activeTab] || }
- - +
); }; diff --git a/client/src/views/GameInterface/components/GameHeader.css b/client/src/views/GameInterface/components/GameHeader.css index 3fc8d5e..a16cb76 100644 --- a/client/src/views/GameInterface/components/GameHeader.css +++ b/client/src/views/GameInterface/components/GameHeader.css @@ -1,76 +1,110 @@ .game-header { - height: 60px; - background: var(--bg-secondary); - border-bottom: 1px solid var(--border-color); - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 1rem; - backdrop-filter: blur(10px); + height: 60px; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1rem; + backdrop-filter: blur(10px); } .header-left { - display: flex; - align-items: center; - gap: 1rem; + display: flex; + align-items: center; + gap: 1rem; } .logo { - font-family: 'Orbitron', sans-serif; - font-size: 1.5rem; - font-weight: 900; - color: var(--primary-color); - text-shadow: 0 0 10px rgba(0, 212, 255, 0.5); + font-family: "Orbitron", sans-serif; + font-size: 1.5rem; + font-weight: 900; + color: var(--primary-color); + text-shadow: 0 0 10px rgba(0, 212, 255, 0.5); } .player-info { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; } .player-name { - font-weight: 700; - color: var(--text-primary); + font-weight: 700; + color: var(--text-primary); } .player-level { - font-size: 0.8rem; - color: var(--primary-color); + font-size: 0.8rem; + color: var(--primary-color); } .header-center { - flex: 1; - display: flex; - justify-content: center; + flex: 1; + display: flex; + justify-content: center; } .resources { - display: flex; - gap: 1.5rem; + display: flex; + gap: 1.5rem; } .resource { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - background: var(--bg-tertiary); - border-radius: 20px; - border: 1px solid var(--border-color); - transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: var(--bg-tertiary); + border-radius: 20px; + border: 1px solid var(--border-color); + transition: all 0.3s ease; } .resource:hover { - border-color: var(--primary-color); - box-shadow: 0 0 10px rgba(0, 212, 255, 0.3); + border-color: var(--primary-color); + box-shadow: 0 0 10px rgba(0, 212, 255, 0.3); } .resource i { - color: var(--primary-color); + color: var(--primary-color); } .header-right { - display: flex; - gap: 0.5rem; + display: flex; + gap: 0.5rem; } +.header-right { + display: flex; + gap: 0.75rem; + align-items: center; +} + +.header-icon-btn { + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border-color); + color: var(--text-secondary); + width: 40px; + height: 40px; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + font-size: 1.1rem; +} + +.header-icon-btn:hover { + background: rgba(0, 212, 255, 0.1); + border-color: var(--primary-color); + color: var(--primary-color); + box-shadow: 0 0 15px rgba(0, 212, 255, 0.2); +} + +.header-icon-btn.exit-btn:hover { + background: rgba(255, 87, 34, 0.1); + border-color: #ff5722; + color: #ff5722; + box-shadow: 0 0 15px rgba(255, 87, 34, 0.2); +} diff --git a/client/src/views/GameInterface/components/GameHeader.jsx b/client/src/views/GameInterface/components/GameHeader.jsx index a4dcb9e..4a2b141 100644 --- a/client/src/views/GameInterface/components/GameHeader.jsx +++ b/client/src/views/GameInterface/components/GameHeader.jsx @@ -25,7 +25,6 @@ const GameHeader = ({ onReturn }) => { Lv. 1 -
@@ -34,17 +33,18 @@ const GameHeader = ({ onReturn }) => {
-
-
diff --git a/client/src/views/GameInterface/components/Navigation.css b/client/src/views/GameInterface/components/Navigation.css index c0067e6..c3517eb 100644 --- a/client/src/views/GameInterface/components/Navigation.css +++ b/client/src/views/GameInterface/components/Navigation.css @@ -1,5 +1,4 @@ .main-nav { - /* Зменшуємо загальну висоту з 60px до 45px */ height: 45px; background: #0a0f18; border-bottom: 1px solid #1a2638; @@ -19,7 +18,7 @@ .nav-container { display: flex; padding: 0 5px; - gap: 2px; /* Мінімальний зазор між кнопками */ + gap: 2px; } .nav-btn { @@ -27,7 +26,6 @@ flex-direction: column; justify-content: center; align-items: center; - /* Зменшуємо горизонтальні відступи */ padding: 0 12px; background: transparent; border: none; @@ -42,21 +40,20 @@ .nav-btn-content { display: flex; align-items: center; - gap: 6px; /* Менша відстань між іконкою та текстом */ + gap: 6px; } .nav-btn i { - font-size: 0.9rem; /* Зменшили розмір іконок */ + font-size: 0.9rem; } .nav-label { - font-size: 9px; /* Ультра-компактний шрифт */ + font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; } -/* Активний стан */ .nav-btn.active { color: #00d4ff; background: linear-gradient(to bottom, rgba(0, 212, 255, 0.08), transparent); @@ -67,7 +64,7 @@ bottom: 0; left: 0; right: 0; - height: 2px; /* Тонша лінія */ + height: 2px; background: #00d4ff; box-shadow: 0 0 8px #00d4ff; transform: scaleX(0); @@ -78,21 +75,66 @@ transform: scaleX(1); } -/* Адаптивність для мобілок (ще компактніше) */ @media (max-width: 768px) { .main-nav { - height: 42px; /* Мінімум для зручного натискання пальцем */ + height: 42px; } .nav-label { - display: none; /* Тільки іконки на мобілках */ + display: none; } .nav-btn { - padding: 0 18px; /* Більше місця для пальця, але без тексту */ + padding: 0 18px; } .nav-btn i { font-size: 1.1rem; } } + +.icon-wrapper { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.nav-badge { + position: absolute; + top: -8px; + right: -8px; + background: #ff3e3e; + color: white; + font-size: 10px; + font-weight: bold; + min-width: 16px; + height: 16px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + padding: 2px; + box-shadow: 0 0 10px rgba(255, 62, 62, 0.5); + border: 1px solid #1a1a1a; + animation: pulse-red 2s infinite; +} + +@keyframes pulse-red { + 0% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(255, 62, 62, 0.7); + } + 70% { + transform: scale(1.1); + box-shadow: 0 0 0 5px rgba(255, 62, 62, 0); + } + 100% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(255, 62, 62, 0); + } +} + +.nav-btn.notifications.active i { + color: #ffd700; +} diff --git a/client/src/views/GameInterface/components/Navigation.jsx b/client/src/views/GameInterface/components/Navigation.jsx index e319d13..8a1863b 100644 --- a/client/src/views/GameInterface/components/Navigation.jsx +++ b/client/src/views/GameInterface/components/Navigation.jsx @@ -1,8 +1,12 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import GameDataManager from "../../../services/GameDataManager.js"; +import { useSocket } from "../../../hooks/useSocket"; import "./Navigation.css"; const Navigation = ({ activeTab, onTabChange }) => { + const { socket } = useSocket(); + const [unreadCount, setUnreadCount] = useState(0); + const tabs = [ { id: "dashboard", icon: "fa-tachometer-alt" }, { id: "dungeons", icon: "fa-dungeon" }, @@ -11,10 +15,40 @@ const Navigation = ({ activeTab, onTabChange }) => { { id: "shop", icon: "fa-store" }, { id: "crafting", icon: "fa-hammer" }, { id: "itemlist", icon: "fa-list-ul" }, + { id: "chat", icon: "fa-comments" }, + { id: "notifications", icon: "fa-bell" }, ]; + useEffect(() => { + if (!socket) return; + + const handleNotifyUpdate = (count) => { + setUnreadCount(count); + }; + + socket.on("notifications:unread_count", handleNotifyUpdate); + + socket.on("notification:new", () => { + if (activeTab !== "notifications") { + setUnreadCount((prev) => prev + 1); + } + }); + + return () => { + socket.off("notifications:unread_count", handleNotifyUpdate); + socket.off("notification:new"); + }; + }, [socket, activeTab]); + + const handleTabClick = (id) => { + if (id === "notifications") setUnreadCount(0); + onTabChange(id); + }; + const getLabel = (id) => { if (id === "itemlist") return "ITEM_LIST"; + if (id === "chat") return "CHAT"; + if (id === "notifications") return "ALERTS"; return GameDataManager.t(`category.tabs.original.${id}`); }; @@ -24,11 +58,16 @@ const Navigation = ({ activeTab, onTabChange }) => { {tabs.map((tab) => ( + )} +
+ +

{getChatName()}

+
+ + +
+ {messages.length === 0 && ( +
+ + NO_LOGS_FOUND. SECURE_LINE_READY... + +
+ )} + {messages.map((msg, index) => ( +
+ + [ + {new Date(msg.createdAt).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} + ] + + {msg.senderName}: + {msg.content} +
+ ))} +
+
+ +
+ setInputValue(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && sendMessage()} + /> + +
+ +
+ ); +}; + +export default ChatTab; diff --git a/client/src/views/GameInterface/tabs/CraftingTab.jsx b/client/src/views/GameInterface/tabs/CraftingTab.jsx index 1318b4d..a4f985a 100644 --- a/client/src/views/GameInterface/tabs/CraftingTab.jsx +++ b/client/src/views/GameInterface/tabs/CraftingTab.jsx @@ -4,6 +4,7 @@ import GameDataManager from "../../../services/GameDataManager"; import "./styles/CraftingTab.css"; import CategorySelector from "../components/CategorySelector"; import CraftModal from "./components/CraftModal"; +import { config } from "../../../config/api"; const CraftingTab = () => { const { socket } = useSocket(); @@ -11,7 +12,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 +19,6 @@ const CraftingTab = () => { useEffect(() => { const manifestCategories = GameDataManager.getRecipeCategories(); setCategories(manifestCategories); - if (manifestCategories.length > 0 && !activeCategory) { setActiveCategory(manifestCategories[0].id); } @@ -29,7 +28,6 @@ const CraftingTab = () => { if (activeCategory) { const filteredRecipes = GameDataManager.getRecipesByCategory(activeCategory); - console.log(filteredRecipes); setRecipes(filteredRecipes); } }, [activeCategory]); @@ -38,30 +36,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 +105,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 +124,7 @@ const CraftingTab = () => {
@@ -122,6 +145,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 +153,44 @@ const CraftingTab = () => { return (
setSelectedRecipe(recipe)} >
{recipe.texture ? ( - {recipe.displayName} + {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/DashboardTab.jsx b/client/src/views/GameInterface/tabs/DashboardTab.jsx index 9def855..235a7d5 100644 --- a/client/src/views/GameInterface/tabs/DashboardTab.jsx +++ b/client/src/views/GameInterface/tabs/DashboardTab.jsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from "react"; import Card from "../../../components/ui/Card"; import { useSocket } from "../../../hooks/useSocket"; +import playerManager from "../../../services/PlayerManager"; import "./styles/DashboardTab.css"; const DashboardTab = () => { @@ -8,10 +9,18 @@ const DashboardTab = () => { const [playerData, setPlayerData] = useState(null); const [earnedPopup, setEarnedPopup] = useState(false); const [credits, setCredits] = useState(0); + const [onlineCount, setOnlineCount] = useState( + playerManager.onlinePlayers.length, + ); + const savedUser = JSON.parse(localStorage.getItem("user")); const localUsername = savedUser?.username || "Unknown Pilot"; useEffect(() => { + const unsubscribe = playerManager.subscribe(({ online }) => { + setOnlineCount(online.length); + }); + if (!socket) return; socket.emit("player:get_dashboard"); @@ -36,6 +45,7 @@ const DashboardTab = () => { socket.on("player:offline_report", handleOfflineReport); return () => { + unsubscribe(); socket.off("player:dashboard_data", handleData); socket.off("player:credits_update", handleCreditsUpdate); socket.off("player:offline_report", handleOfflineReport); @@ -124,7 +134,14 @@ const DashboardTab = () => { - {isConnected ? "CONNECTED" : "DISCONNECTED"} + {isConnected ? ( + + + LIVE: {onlineCount} + + ) : ( + "DISCONNECTED" + )} diff --git a/client/src/views/GameInterface/tabs/ItemListTab.jsx b/client/src/views/GameInterface/tabs/ItemListTab.jsx index b7d5883..b018290 100644 --- a/client/src/views/GameInterface/tabs/ItemListTab.jsx +++ b/client/src/views/GameInterface/tabs/ItemListTab.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from "react"; import GameDataManager from "../../../services/GameDataManager.js"; import "./styles/ItemListTab.css"; +import { config } from "../../../config/api.js"; const ItemListTab = () => { const [allItems, setAllItems] = useState([]); @@ -8,26 +9,30 @@ const ItemListTab = () => { const [searchQuery, setSearchQuery] = useState(""); const [selectedCategory, setSelectedCategory] = useState("all"); const [selectedItem, setSelectedItem] = useState(null); + const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); - const ASSET_BASE_URL = "http://localhost:5003/static/"; + const ASSET_BASE_URL = `${config.serverUrl}/static/`; useEffect(() => { + const handleResize = () => setIsMobile(window.innerWidth <= 768); + window.addEventListener("resize", handleResize); + const itemsArray = Array.from(GameDataManager.items.keys()).map((id) => GameDataManager.getItem(id), ); setAllItems(itemsArray); setFilteredItems(itemsArray); + + return () => window.removeEventListener("resize", handleResize); }, []); useEffect(() => { let result = allItems; - if (selectedCategory !== "all") { result = result.filter( (item) => item.meta?.category === selectedCategory, ); } - if (searchQuery) { const q = searchQuery.toLowerCase(); result = result.filter( @@ -36,7 +41,6 @@ const ItemListTab = () => { item.id.toLowerCase().includes(q), ); } - setFilteredItems(result); }, [searchQuery, selectedCategory, allItems]); @@ -50,19 +54,72 @@ const ItemListTab = () => { ...new Set(allItems.map((i) => i.meta?.category).filter(Boolean)), ]; + const renderInspector = () => ( +
isMobile && setSelectedItem(null)} + > + {selectedItem && ( +
e.stopPropagation()}> + {isMobile && ( + + )} + +
+
+ +
+
+
{selectedItem.id}
+

{selectedItem.displayName}

+ + {selectedItem.meta?.rarity?.toUpperCase()} + +
+
+ +
+
+
DATA_DESCRIPTION
+

{selectedItem.description}

+
+ + {selectedItem.stats && + Object.keys(selectedItem.stats).length > 0 && ( +
+
PARAMETER_READOUT
+
+ {Object.entries(selectedItem.stats).map(([k, v]) => ( +
+ {GameDataManager.getStatName(k)} + +{v} +
+ ))} +
+
+ )} +
+
+ )} +
+ ); + return (
- setSearchQuery(e.target.value)} />
-
{categories.map((cat) => ( ))}
-
{filteredItems.map((item) => (
{
{item.displayName}
{item.id}
-
))} - {filteredItems.length === 0 && ( -
NO_RECORDS_FOUND
- )}
- -
- {selectedItem ? ( -
-
-
- -
-
-
{selectedItem.id}
-

{selectedItem.displayName}

-
- {selectedItem.meta?.rarity?.toUpperCase()} -
-
-
- -
-
-
DATA_DESCRIPTION
-

{selectedItem.description}

-
- - {selectedItem.stats && - Object.keys(selectedItem.stats).length > 0 && ( -
-
PARAMETER_READOUT
-
- {Object.entries(selectedItem.stats).map(([k, v]) => ( -
- - {GameDataManager.getStatName(k)} - - +{v} -
- ))} -
-
- )} - -
-
OBJECT_METADATA
-
-
- CATEGORY - {selectedItem.meta?.category || "general"} -
-
- EQUIP_SLOT - {selectedItem.meta?.equipmentSlot || "none"} -
-
- STACK_LIMIT - {selectedItem.meta?.stackable ? "64" : "1"} -
-
-
-
-
- ) : ( -
-
-

AWAITING_OBJECT_SELECTION...

-
- )} -
+ {(!isMobile || (isMobile && selectedItem)) && renderInspector()} ); }; diff --git a/client/src/views/GameInterface/tabs/NotificationTab.jsx b/client/src/views/GameInterface/tabs/NotificationTab.jsx new file mode 100644 index 0000000..c29b5b1 --- /dev/null +++ b/client/src/views/GameInterface/tabs/NotificationTab.jsx @@ -0,0 +1,108 @@ +import React, { useState, useEffect } from "react"; +import { useSocket } from "../../../hooks/useSocket"; +import "./styles/NotificationsTab.css"; + +const NotificationsTab = () => { + const { socket } = useSocket(); + const [notifications, setNotifications] = useState([]); + + useEffect(() => { + if (!socket) return; + + socket.emit("notifications:get_all"); + + const handleNewNotify = (notify) => { + console.log(notify); + setNotifications((prev) => [notify, ...prev]); + }; + + const handleInitialNotify = (data) => { + setNotifications(data); + }; + + socket.on("notification:new", handleNewNotify); + socket.on("notifications:list", handleInitialNotify); + + return () => { + socket.off("notification:new", handleNewNotify); + socket.off("notifications:list", handleInitialNotify); + }; + }, [socket]); + + const handleAction = (id, action, data) => { + if (action === "accept_friend") { + socket.emit("friend:add", { friendId: data.fromId }); + } + socket.emit("notification:read", { id }); + setNotifications((prev) => prev.filter((n) => n.id !== id)); + }; + + const getIcon = (type) => { + switch (type) { + case "friend_request": + return "fas fa-user-plus"; + case "crafting": + return "fas fa-hammer"; + case "system": + return "fas fa-robot"; + default: + return "fas fa-bell"; + } + }; + + return ( +
+
+
SYSTEM_ALERTS
+

NOTIFICATIONS

+
+ +
+ {notifications.length === 0 && ( +
+ + NO_ACTIVE_ALERTS_FOUND +
+ )} + + {notifications.map((n) => ( +
+
+ +
+
+
+

{n.title}

+ + {new Date(n.createdAt).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })} + +
+

{n.message}

+
+
+ {n.type === "friend_request" && ( + + )} + +
+
+ ))} +
+
+ ); +}; + +export default NotificationsTab; 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/client/src/views/GameInterface/tabs/styles/ChatTab.css b/client/src/views/GameInterface/tabs/styles/ChatTab.css new file mode 100644 index 0000000..702639a --- /dev/null +++ b/client/src/views/GameInterface/tabs/styles/ChatTab.css @@ -0,0 +1,182 @@ +.chat-container { + display: flex; + height: 100%; + background: rgba(5, 8, 12, 0.9); + border: 1px solid var(--border-color); + overflow: hidden; + position: relative; +} + +.chat-sidebar { + width: 300px; + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; + transition: all 0.3s ease; + z-index: 2; +} + +.chat-main { + flex: 1; + display: flex; + flex-direction: column; + background: rgba(0, 0, 0, 0.2); +} + +/* МОБІЛЬНА ВЕРСІЯ */ +@media (max-width: 768px) { + .chat-sidebar { + width: 100%; + position: absolute; + height: 100%; + background: #0a0f18; + } + + .chat-sidebar.hidden { + transform: translateX(-100%); + } + + .chat-main { + width: 100%; + position: absolute; + height: 100%; + z-index: 1; + } + + .chat-main.hidden { + transform: translateX(100%); + } + + .back-btn { + background: transparent; + border: none; + color: var(--primary-color); + font-size: 1.2rem; + padding: 0 15px 0 0; + cursor: pointer; + } +} + +/* Стилі заголовків та вводу */ +.search-section { + padding: 20px 15px 15px; + border-bottom: 1px solid var(--border-color); +} + +.search-input-wrapper { + display: flex; + gap: 5px; + margin-top: 10px; +} + +.search-input-wrapper input { + flex: 1; + background: #05080c; + border: 1px solid var(--border-color); + color: #fff; + padding: 8px; + font-size: 12px; +} + +.add-friend-btn { + background: var(--primary-color); + border: none; + width: 35px; + cursor: pointer; +} + +.chat-item { + padding: 15px 20px; + display: flex; + align-items: center; + gap: 12px; + cursor: pointer; + border-bottom: 1px solid rgba(255, 255, 255, 0.02); +} + +.chat-item.active { + background: rgba(0, 212, 255, 0.1); + box-shadow: inset 4px 0 0 var(--primary-color); +} + +.chat-header { + padding: 15px; + display: flex; + align-items: center; + border-bottom: 1px solid var(--border-color); + background: rgba(10, 15, 24, 0.8); +} + +.chat-messages { + flex: 1; + padding: 15px; + overflow-y: auto; +} + +.chat-input-area { + padding: 15px; + background: #05080c; + display: flex; + gap: 10px; +} + +.chat-input-area input { + flex: 1; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border-color); + color: #fff; + padding: 10px; +} + +.send-btn { + background: transparent; + border: 1px solid var(--primary-color); + color: var(--primary-color); + padding: 0 15px; +} + +.search-section { + position: relative; + z-index: 10; +} + +.search-results-dropdown { + position: absolute; + top: 100%; + left: 15px; + right: 15px; + background: #0a0f18; + border: 1px solid var(--primary-color); + border-top: none; + max-height: 200px; + overflow-y: auto; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); +} + +.search-result-item { + padding: 10px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + font-size: 12px; + color: #fff; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.search-result-item:hover { + background: rgba(0, 212, 255, 0.1); +} + +.search-result-item i { + color: var(--primary-color); +} + +.message.system .msg-author { + color: #ff3e3e; +} + +.message.system .msg-text { + color: #aaa; + font-style: italic; +} diff --git a/client/src/views/GameInterface/tabs/styles/DashboardTab.css b/client/src/views/GameInterface/tabs/styles/DashboardTab.css index 98ae759..39c7916 100644 --- a/client/src/views/GameInterface/tabs/styles/DashboardTab.css +++ b/client/src/views/GameInterface/tabs/styles/DashboardTab.css @@ -216,7 +216,40 @@ .diag-status.online { color: #00ff88; - text-shadow: 0 0 5px #00ff88; + display: flex; + align-items: center; + gap: 8px; + text-shadow: 0 0 8px rgba(0, 255, 136, 0.4); +} + +.online-info { + display: flex; + align-items: center; + gap: 8px; +} + +.online-dot { + width: 6px; + height: 6px; + background-color: #00ff88; + border-radius: 50%; + display: inline-block; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.4; + transform: scale(1.2); + } + 100% { + opacity: 1; + transform: scale(1); + } } @media (min-width: 600px) { diff --git a/client/src/views/GameInterface/tabs/styles/ItemListTab.css b/client/src/views/GameInterface/tabs/styles/ItemListTab.css index 18133b4..4858264 100644 --- a/client/src/views/GameInterface/tabs/styles/ItemListTab.css +++ b/client/src/views/GameInterface/tabs/styles/ItemListTab.css @@ -6,9 +6,9 @@ font-family: "Rajdhani", "Segoe UI", sans-serif; border-top: 1px solid rgba(0, 255, 255, 0.1); overflow: hidden; + position: relative; } -/* SIDEBAR & SEARCH */ .item-list-sidebar { width: 380px; border-right: 1px solid rgba(0, 255, 255, 0.1); @@ -19,7 +19,6 @@ .search-box { padding: 20px; - position: relative; } .search-box input { @@ -31,16 +30,8 @@ text-transform: uppercase; letter-spacing: 1px; font-size: 14px; - transition: all 0.3s; } -.search-box input:focus { - outline: none; - border-color: #00ffff; - box-shadow: 0 0 10px rgba(0, 255, 255, 0.2); -} - -/* CATEGORY FILTERS */ .category-filters { display: flex; flex-wrap: wrap; @@ -55,17 +46,14 @@ padding: 4px 10px; font-size: 11px; cursor: pointer; - transition: 0.2s; } -.filter-btn.active, -.filter-btn:hover { +.filter-btn.active { background: rgba(0, 255, 255, 0.1); color: #00ffff; border-color: #00ffff; } -/* ITEM LIST ROWS */ .items-list-scroll { flex: 1; overflow-y: auto; @@ -78,60 +66,108 @@ padding: 10px; margin-bottom: 8px; background: rgba(255, 255, 255, 0.02); - border-left: 3px solid transparent; + border-left: 3px solid #9d9d9d; cursor: pointer; - transition: all 0.2s; - position: relative; -} - -.item-row:hover { - background: rgba(255, 255, 255, 0.05); - transform: translateX(5px); } .item-row.selected { background: rgba(0, 255, 255, 0.08); - border-left-color: #00ffff; } .item-row-icon { width: 44px; height: 44px; background: rgba(0, 0, 0, 0.5); - border: 1px solid rgba(255, 255, 255, 0.1); + margin-right: 15px; display: flex; align-items: center; justify-content: center; - margin-right: 15px; } .item-row-icon img { width: 32px; height: 32px; - object-fit: contain; - image-rendering: pixelated; /* Щоб іконки були чіткими */ + image-rendering: pixelated; } -.item-row-content { +/* Inspector Base */ +.item-inspector { flex: 1; + padding: 40px; + overflow-y: auto; +} + +.inspector-header { display: flex; - flex-direction: column; + gap: 25px; + margin-bottom: 30px; } -.item-row-title { - font-size: 15px; - font-weight: 600; - color: #fff; - text-transform: uppercase; +.header-visual { + width: 100px; + height: 100px; + background: rgba(0, 0, 0, 0.4); + border: 1px solid rgba(0, 255, 255, 0.2); + display: flex; + align-items: center; + justify-content: center; } -.item-row-subtitle { - font-size: 11px; - color: #666; - font-family: "Courier New", monospace; +.header-visual img { + width: 64px; + height: 64px; } -/* RARITY COLORS */ +/* Mobile Modal Logic */ +@media (max-width: 768px) { + .item-list-sidebar { + width: 100%; + border: none; + } + + .item-inspector.mobile-modal { + display: flex; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 3000; + background: rgba(0, 0, 0, 0.85); + backdrop-filter: blur(6px); + align-items: center; + justify-content: center; + padding: 20px; + } + + .inspector-content { + background: #0d121d; + border: 1px solid #00ffff44; + width: 100%; + max-height: 120vh; + overflow-y: auto; + position: relative; + padding: 20px; + box-shadow: 0 0 30px rgba(0, 0, 0, 0.5); + } + + .close-inspector { + position: absolute; + top: 10px; + right: 15px; + background: none; + border: none; + color: #00ffff; + font-size: 30px; + cursor: pointer; + } + + .item-inspector:not(.mobile-modal) { + display: none; + } +} + +/* Rarity */ .item-row.legendary { border-left-color: #ff8000; } @@ -141,67 +177,11 @@ .item-row.rare { border-left-color: #0070dd; } -.item-row.common { - border-left-color: #9d9d9d; -} -/* INSPECTOR PANEL */ -.item-inspector { - flex: 1; - padding: 40px; - background: radial-gradient( - circle at top right, - rgba(0, 255, 255, 0.03), - transparent - ); - overflow-y: auto; -} - -.inspector-header { - display: flex; - gap: 30px; - margin-bottom: 40px; - align-items: center; -} - -.header-visual { - width: 120px; - height: 120px; - background: rgba(0, 0, 0, 0.3); - border: 2px solid rgba(0, 255, 255, 0.2); - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); -} - -.header-visual img { - width: 80px; - height: 80px; -} - -.header-info h1 { - font-size: 32px; - margin: 5px 0; - text-transform: uppercase; - letter-spacing: 2px; -} - -.id-tag { - font-family: monospace; - color: #00ffff; - opacity: 0.6; - font-size: 14px; -} - -/* SCROLLBAR CUSTOMIZATION */ -.items-list-scroll::-webkit-scrollbar { - width: 4px; -} -.items-list-scroll::-webkit-scrollbar-thumb { - background: rgba(0, 255, 255, 0.2); -} - -.section-title { - font-size: 16px; +@media screen and (max-width: 600px) { + .section-title { + font-size: 16px; + padding: 0; + margin: 10px; + } } diff --git a/client/src/views/GameInterface/tabs/styles/NotificationsTab.css b/client/src/views/GameInterface/tabs/styles/NotificationsTab.css new file mode 100644 index 0000000..dbb4c99 --- /dev/null +++ b/client/src/views/GameInterface/tabs/styles/NotificationsTab.css @@ -0,0 +1,148 @@ +.notifications-container { + padding: 20px; + height: 100%; + display: flex; + flex-direction: column; + background: rgba(10, 15, 24, 0.6); +} + +.notifications-header { + margin-bottom: 20px; +} + +.notifications-header h2 { + font-family: "Orbitron", sans-serif; + color: #fff; + letter-spacing: 2px; + margin-top: 5px; +} + +.notifications-list { + display: flex; + flex-direction: column; + gap: 10px; + overflow-y: auto; + padding-right: 5px; +} + +.no-notifications { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 15px; + padding: 100px 0; + color: #4a5d75; + font-family: "Orbitron", sans-serif; + font-size: 12px; +} + +.no-notifications i { + font-size: 2rem; + opacity: 0.3; +} + +.notify-card { + display: flex; + align-items: center; + background: rgba(26, 38, 56, 0.5); + border-left: 3px solid #00d4ff; + padding: 15px; + gap: 15px; + backdrop-filter: blur(5px); + border-radius: 0 4px 4px 0; + animation: notify-slide-in 0.3s ease-out; +} + +.notify-card.friend_request { + border-left-color: #00ff88; +} +.notify-card.crafting { + border-left-color: #ffd700; +} +.notify-card.system { + border-left-color: #ff3e3e; +} + +.notify-icon { + width: 35px; + height: 35px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.3); + border-radius: 50%; + color: #fff; +} + +.notify-content { + flex: 1; +} + +.notify-title-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 4px; +} + +.notify-content h4 { + margin: 0; + font-family: "Orbitron", sans-serif; + font-size: 12px; + color: #fff; +} + +.notify-time { + font-size: 10px; + color: #4a5d75; +} + +.notify-content p { + margin: 0; + font-size: 13px; + color: #a0aec0; +} + +.notify-actions { + display: flex; + gap: 8px; +} + +.action-btn { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + color: #fff; + padding: 6px 12px; + cursor: pointer; + font-family: "Orbitron", sans-serif; + font-size: 10px; + transition: all 0.2s; +} + +.action-btn.accept { + background: #00ff88; + color: #000; + border: none; + font-weight: bold; +} + +.action-btn.dismiss { + width: 28px; + padding: 6px 0; +} + +.action-btn:hover { + filter: brightness(1.2); +} + +@keyframes notify-slide-in { + from { + opacity: 0; + transform: translateX(30px); + } + to { + opacity: 1; + transform: translateX(0); + } +} diff --git a/game-server/datapacks/original/assets/languages/en_US.json b/game-server/datapacks/original/assets/languages/en_US.json index 5e06c12..049085f 100644 --- a/game-server/datapacks/original/assets/languages/en_US.json +++ b/game-server/datapacks/original/assets/languages/en_US.json @@ -1,110 +1,157 @@ { - "_comment_Dungeons": "", - "dungeons.original.pirate.pirates_outpost": "Pirate Outpost", - "dungeons.original.pirate.pirates_outpost.desc": "A hidden supply station belonging to the Black Mark syndicate.", - "dungeons.original.tutorial.pirates_outpost": "Tutorial", - "dungeons.original.tutorial.pirates_outpost.desc": "A one time dungeon.", - "_comment_Enemies": "", - "enemies.original.pirate.black_mark_heavy_cruiser": "Black Mark Heavy Cruiser", - "enemies.original.pirate.raider_frigate": "Raider Frigate", - "enemies.original.pirate.scout_drone": "Scout Drone", - "enemies.original.tutorial.tutorial_hostile": "Tutorial hostile", - "enemies.original.tutorial.tutorial_boss_hostile": "Tutorial Boss", - "_comment_Equipment": "", - "items.materials.original.backpack_basic": "Basic Backpack", - "items.materials.original.backpack_basic.desc": "Basics of the suits.", - "items.materials.original.backpack_advanced": "Advanced Backpack", - "items.materials.original.backpack_advanced.desc": "Trying out a better suit.", - "items.materials.original.backpack_elite": "Elite Backpack", - "items.materials.original.backpack_elite.desc": "Now the best the world has to offer.", - "_comment_Materials": "", - "items.materials.original.bio.bio_pulp": "Bio Pulp", - "items.materials.original.bio.bio_pulp.desc": "A pile of biological material.", - "items.materials.original.alloys.steel": "Steel Ingot", - "items.materials.original.alloys.steel.desc": "A steel ingot.", - "items.materials.original.ingots.aluminum": "Aluminum Ingot", - "items.materials.original.ingots.aluminum.desc": "An aluminum ingot.", - "items.materials.original.ingots.chronite": "Chronite Ingot", - "items.materials.original.ingots.chronite.desc": "A chronite ingot.", - "items.materials.original.ingots.copper": "Copper Ingot", - "items.materials.original.ingots.copper.desc": "A copper ingot.", - "items.materials.original.ingots.gold": "Gold Ingot", - "items.materials.original.ingots.gold.desc": "A gold ingot.", - "items.materials.original.ingots.iron": "Iron Ingot", - "items.materials.original.ingots.iron.desc": "A iron ingot.", - "items.materials.original.ingots.titanium": "Titanium Ingot", - "items.materials.original.ingots.titanium.desc": "A titanium ingot.", - "items.materials.original.ingots.tungsten": "Tungsten Ingot", - "items.materials.original.ingots.tungsten.desc": "A tungsten ingot.", - "items.materials.original.ores.bauxite": "Bauxite Ore", - "items.materials.original.ores.bauxite.desc": "A pile of bauxite ore.", - "items.materials.original.ores.chronite": "Chronium Ore", - "items.materials.original.ores.chronite.desc": "A pile of chronium ore.", - "items.materials.original.ores.coal": "Coal Ore", - "items.materials.original.ores.coal.desc": "A pile of coal ore.", - "items.materials.original.ores.copper": "Copper Ore", - "items.materials.original.ores.copper.desc": "A pile of copper ore.", - "items.materials.original.ores.gold": "Gold Ore", - "items.materials.original.ores.gold.desc": "A pile of gold ore.", - "items.materials.original.ores.ilmenite": "Ilmenite Ore", - "items.materials.original.ores.ilmenite.desc": "A pile of ilmenite ore.", - "items.materials.original.ores.iron": "Iron Ore", - "items.materials.original.ores.iron.desc": "A pile of iron ore.", - "items.materials.original.ores.wolfrinite": "Wolfrinite Ore", - "items.materials.original.ores.wolfrinite.desc": "A pile of wolfrinite ore.", - "items.materials.original.plating.basic_ship_plating": "Ship Plating", - "items.materials.original.plating.basic_ship_plating.desc": "Just basic ship plating.", - "_comment_Recipes": "", - "recipes.category.original.alloys": "Alloys", - "recipes.category.original.circuits": "Circuits", - "recipes.category.original.food": "Food", - "recipes.category.original.forging": "Forging", - "recipes.category.original.hull_sections": "Hull Sections", - "recipes.category.original.hulls": "Hulls", - "recipes.category.original.organics": "Organics", - "recipes.category.original.spacesuit_parts": "Spacesuit Parts", - "_comment_Shop": "", - "shop.category.original.materials": "Materials", - "_comment_Skills": "", - "skills.category.original.combat": "Combat", - "skills.category.original.combat.weapon_effiency": "Weapon Effiency", - "skills.category.original.combat.weapon_effiency.desc": "Let's get those weapons better!", - "skills.category.original.crafting": "Crafting", - "skills.category.original.crafting.blacksmithing": "Blacksmithing", - "skills.category.original.crafting.blacksmithing.desc": "To forge the basics.", - "skills.category.original.crafting.alloying": "Alloying", - "skills.category.original.crafting.alloying.desc": "Let's start alloy making.", - "skills.category.original.science": "Science", - "skills.category.original.science.alien_technology": "Alien Technology", - "skills.category.original.science.alien_technology.desc": "Unknown Mysterious Tech", - "skills.category.original.science.biology_engineering": "Biology Engineering", - "skills.category.original.science.biology_engineering.desc": "Maybe we will unlock bio-tech?", - "_comment_Stats": "", - "stats.category.original.attack.base": "Attack", - "stats.category.original.attack.chance": "Attack Chance", - "stats.category.original.attack.rating": "Attack Rating", - "stats.category.original.defence.base": "Defense", - "stats.category.original.defence.chance": "Defense Chance", - "stats.category.original.defence.rating": "Defense Rating", - "stats.category.original.health": "Health", - "stats.category.original.penetration.base": "Penetration", - "stats.category.original.penetration.chance": "Penetration Chance", - "stats.category.original.penetration.rating": "Penetration Rating", - "stats.category.original.reflect.base": "Reflect", - "stats.category.original.reflect.chance": "Reflection Chance", - "stats.category.original.reflect.rating": "Reflection Rating", - "stats.category.original.resistance.base": "Resistance", - "stats.category.original.resistance.cold": "Cold Resistance", - "stats.category.original.resistance.gamma": "Gamma Resistance", - "stats.category.original.resistance.heat": "Heat Resistance", - "stats.category.original.resistance.ion": "Ion Resistance", - "stats.category.original.resistance.physical": "Physical Resistance", - "stats.category.original.resistance.plasma": "Plasma Resistance", - "_comment_Tabs": "", - "category.tabs.original.crafting": "Crafting", - "category.tabs.original.dashboard": "Dashboard", - "category.tabs.original.dungeons": "Dungeons", - "category.tabs.original.inventory": "Inventory", - "category.tabs.original.shop": "Shop", - "category.tabs.original.skills": "Skills" + "_comment_Admin" : "", + "admin.category.original.hostile_list" : "Hostile List", + "admin.category.original.item_list" : "Item List", + "admin.category.original.player_list" : "Player List", + "admin.category.original.item_list.all" : "All", + "admin.category.original.item_list.alloys" : "Alloys", + "admin.category.original.item_list.circuits" : "Circuits", + "admin.category.original.item_list.customizables" : "Customizables", + "admin.category.original.item_list.ingots" : "Ingots", + "admin.category.original.item_list.maerials" : "Materials", + "admin.category.original.item_list.ores" : "Ores", + "admin.category.original.item_list.personal" : "Personal", + "admin.category.original.item_list.ships" : "Ships", + "admin.category.original.item_list.shields" : "Shields", + "admin.category.original.item_list.weapons" : "Weapons", + "admin.category.original.hostile_list.all" : "All", + "admin.category.original.hostile_list.ground" : "Ground Units", + "admin.category.original.hostile_list.ships" : "Ships", + "admin.category.original.player_list.all" : "All", + "admin.category.original.player_list.members" : "Members", + "admin.category.original.player_list.moderators" : "Moderators", + "admin.category.original.player_list.admins" : "Admins", + "_comment_Core_Systems" : "", + "core_systems.category.original.person.helmet" : "Personal Helmet", + "core_systems.category.original.person.suit" : "Personal Suit", + "core_systems.category.original.person.gloves" : "Personal Gloves", + "core_systems.category.original.person.boots" : "Personal Boots", + "core_systems.category.original.person.accessory_1" : "Personal Accessory 1", + "core_systems.category.original.person.accessory_2" : "Personal Accessory 2", + "core_systems.category.original.person.accessory_3" : "Personal Accessory 3", + "core_systems.category.original.person.accessory_4" : "Personal Accessory 4", + "core_systems.category.original.person.weapon" : "Personal Weapon", + "core_systems.category.original.ship.hull" : "Ship Hull", + "core_systems.category.original.ship.shields" : "Ship Shield", + "core_systems.category.original.ship.engines" : "Ship Engine", + "core_systems.category.original.ship.weapon_1" : "Ship Weapon 1", + "core_systems.category.original.ship.weapon_2" : "Ship Weapon 2", + "core_systems.category.original.ship.thruster_1" : "Ship Thruster 1", + "core_systems.category.original.ship.thruster_2" : "Ship Thruster 2", + "core_systems.category.original.ship.thruster_3" : "Ship Thruster 3", + "core_systems.category.original.ship.thruster_4" : "Ship Thruster 4", + "_comment_Dungeons" : "", + "dungeons.original.pirate.pirates_outpost" : "Pirate Outpost", + "dungeons.original.pirate.pirates_outpost.desc" : "A hidden supply station belonging to the Black Mark syndicate.", + "dungeons.original.tutorial.tutorial" : "Tutorial", + "dungeons.original.tutorial.tutorial.desc" : "A one time dungeon.", + "_comment_Enemies" : "", + "enemies.original.pirate.black_mark_heavy_cruiser" : "Black Mark Heavy Cruiser", + "enemies.original.pirate.raider_frigate" : "Raider Frigate", + "enemies.original.pirate.scout_drone" : "Scout Drone", + "enemies.original.tutorial.tutorial_hostile" : "Tutorial hostile", + "enemies.original.tutorial.tutorial_boss_hostile" : "Tutorial Boss", + "_comment_Equipment" : "", + "items.materials.original.backpack_basic" : "Basic Backpack", + "items.materials.original.backpack_basic.desc" : "Basics of the suits.", + "items.materials.original.backpack_advanced" : "Advanced Backpack", + "items.materials.original.backpack_advanced.desc" : "Trying out a better suit.", + "items.materials.original.backpack_elite" : "Elite Backpack", + "items.materials.original.backpack_elite.desc" : "Now the best the world has to offer.", + "_comment_Materials" : "", + "items.materials.original.bio.bio_pulp" : "Bio Pulp", + "items.materials.original.bio.bio_pulp.desc" : "A pile of biological material.", + "items.materials.original.alloys.steel" : "Steel Ingot", + "items.materials.original.alloys.steel.desc" : "A steel ingot.", + "items.materials.original.ingots.aluminum" : "Aluminum Ingot", + "items.materials.original.ingots.aluminum.desc" : "An aluminum ingot.", + "items.materials.original.ingots.chronite" : "Chronite Ingot", + "items.materials.original.ingots.chronite.desc" : "A chronite ingot.", + "items.materials.original.ingots.copper" : "Copper Ingot", + "items.materials.original.ingots.copper.desc" : "A copper ingot.", + "items.materials.original.ingots.gold" : "Gold Ingot", + "items.materials.original.ingots.gold.desc" : "A gold ingot.", + "items.materials.original.ingots.iron" : "Iron Ingot", + "items.materials.original.ingots.iron.desc" : "A iron ingot.", + "items.materials.original.ingots.titanium" : "Titanium Ingot", + "items.materials.original.ingots.titanium.desc" : "A titanium ingot.", + "items.materials.original.ingots.tungsten" : "Tungsten Ingot", + "items.materials.original.ingots.tungsten.desc" : "A tungsten ingot.", + "items.materials.original.ores.bauxite" : "Bauxite Ore", + "items.materials.original.ores.bauxite.desc" : "A pile of bauxite ore.", + "items.materials.original.ores.chronite" : "Chronium Ore", + "items.materials.original.ores.chronite.desc" : "A pile of chronium ore.", + "items.materials.original.ores.coal" : "Coal Ore", + "items.materials.original.ores.coal.desc" : "A pile of coal ore.", + "items.materials.original.ores.copper" : "Copper Ore", + "items.materials.original.ores.copper.desc" : "A pile of copper ore.", + "items.materials.original.ores.gold" : "Gold Ore", + "items.materials.original.ores.gold.desc" : "A pile of gold ore.", + "items.materials.original.ores.ilunite" : "Ilunite Ore", + "items.materials.original.ores.ilunite.desc" : "A pile of ilunite ore.", + "items.materials.original.ores.iron" : "Iron Ore", + "items.materials.original.ores.iron.desc" : "A pile of iron ore.", + "items.materials.original.ores.wolfrinite" : "Wolfrinite Ore", + "items.materials.original.ores.wolfrinite.desc" : "A pile of wolfrinite ore.", + "items.materials.original.plating.basic_ship_plating" : "Ship Plating", + "items.materials.original.plating.basic_ship_plating.desc" : "Just basic ship plating.", + "_comment_Recipes" : "", + "recipes.category.original.alloys" : "Alloys", + "recipes.category.original.circuits" : "Circuits", + "recipes.category.original.food" : "Food", + "recipes.category.original.forging" : "Forging", + "recipes.category.original.hull_sections" : "Hull Sections", + "recipes.category.original.hulls" : "Hulls", + "recipes.category.original.organics" : "Organics", + "recipes.category.original.spacesuit_parts" : "Spacesuit Parts", + "_comment_Shop" : "", + "shop.category.original.consumables" : "Consumables", + "shop.category.original.defence" : "Defence", + "shop.category.original.featured" : "Featured", + "shop.category.original.materials" : "Materials", + "shop.category.original.premium" : "Premium", + "shop.category.original.ships" : "Ships", + "shop.category.original.weapons" : "Weapons", + "_comment_Skills" : "", + "skills.category.original.combat" : "Combat", + "skills.category.original.combat.weapon_effiency" : "Weapon Effiency", + "skills.category.original.combat.weapon_effiency.desc" : "Let's get those weapons better!", + "skills.category.original.crafting" : "Crafting", + "skills.category.original.crafting.blacksmithing" : "Blacksmithing", + "skills.category.original.crafting.blacksmithing.desc" : "To forge the basics.", + "skills.category.original.crafting.alloying" : "Alloying", + "skills.category.original.crafting.alloying.desc" : "Lets start alloy making.", + "skills.category.original.science" : "Science", + "skills.category.original.science.alien_technology" : "Alien Technology", + "skills.category.original.science.alien_technology.desc" : "Unknown Mysterious Tech", + "skills.category.original.science.biology_engineering" : "Biology Engineering", + "skills.category.original.science.biology_engineering.desc" : "Maybe we will unlock bio-tech?", + "_comment_Stats" : "", + "stats.category.original.attack.base" : "Attack", + "stats.category.original.attack.chance" : "Attack Chance", + "stats.category.original.attack.rating" : "Attack Rating", + "stats.category.original.defence.base" : "Defence", + "stats.category.original.defence.chance" : "Defence Chance", + "stats.category.original.defence.rating" : "Defence Rating", + "stats.category.original.health" : "Health", + "stats.category.original.penetration.base" : "Penetration", + "stats.category.original.penetration.chance" : "Penetration Chance", + "stats.category.original.penetration.rating" : "Penetration Rating", + "stats.category.original.reflect.base" : "Reflect", + "stats.category.original.reflect.chance" : "Reflection Chance", + "stats.category.original.reflect.rating" : "Reflection Rating", + "stats.category.original.resistance.base" : "Resistance", + "stats.category.original.resistance.cold" : "Cold Resistance", + "stats.category.original.resistance.gamma" : "Gamma Resistance", + "stats.category.original.resistance.heat" : "Heat Resistance", + "stats.category.original.resistance.ion" : "Ion Resistance", + "stats.category.original.resistance.physical" : "Physical Resistance", + "stats.category.original.resistance.plasma" : "Plasma Resistance", + "_comment_Tabs" : "", + "category.tabs.original.crafting" : "Crafting", + "category.tabs.original.dashboard" : "Dashboard", + "category.tabs.original.dungeons" : "Dungeons", + "category.tabs.original.inventory" : "Inventory", + "category.tabs.original.shop" : "Shop", + "category.tabs.original.skills" : "Skills" } \ No newline at end of file diff --git a/game-server/datapacks/original/assets/languages/fr_FR.json b/game-server/datapacks/original/assets/languages/fr_FR.json index d7b8365..8e5a633 100644 --- a/game-server/datapacks/original/assets/languages/fr_FR.json +++ b/game-server/datapacks/original/assets/languages/fr_FR.json @@ -1,9 +1,50 @@ { - "_comment_Dungeons": "", + "_comment_Admin" : "", + "admin.category.original.hostile_list" : "Hostile List", + "admin.category.original.item_list" : "Item List", + "admin.category.original.player_list" : "Player List", + "admin.category.original.item_list.all" : "All", + "admin.category.original.item_list.alloys" : "Alloys", + "admin.category.original.item_list.circuits" : "Circuits", + "admin.category.original.item_list.customizables" : "Customizables", + "admin.category.original.item_list.ingots" : "Ingots", + "admin.category.original.item_list.maerials" : "Materials", + "admin.category.original.item_list.ores" : "Ores", + "admin.category.original.item_list.personal" : "Personal", + "admin.category.original.item_list.ships" : "Ships", + "admin.category.original.item_list.shields" : "Shields", + "admin.category.original.item_list.weapons" : "Weapons", + "admin.category.original.hostile_list.all" : "All", + "admin.category.original.hostile_list.ground" : "Ground Units", + "admin.category.original.hostile_list.ships" : "Ships", + "admin.category.original.player_list.all" : "All", + "admin.category.original.player_list.members" : "Members", + "admin.category.original.player_list.moderators" : "Moderators", + "admin.category.original.player_list.admins" : "Admins", + "_comment_Core_Systems" : "", + "core_systems.category.original.person.helmet" : "Personal Helmet", + "core_systems.category.original.person.suit" : "Personal Suit", + "core_systems.category.original.person.gloves" : "Personal Gloves", + "core_systems.category.original.person.boots" : "Personal Boots", + "core_systems.category.original.person.accessory_1" : "Personal Accessory 1", + "core_systems.category.original.person.accessory_2" : "Personal Accessory 2", + "core_systems.category.original.person.accessory_3" : "Personal Accessory 3", + "core_systems.category.original.person.accessory_4" : "Personal Accessory 4", + "core_systems.category.original.person.weapon" : "Personal Weapon", + "core_systems.category.original.ship.hull" : "Ship Hull", + "core_systems.category.original.ship.shields" : "Ship Shield", + "core_systems.category.original.ship.engines" : "Ship Engine", + "core_systems.category.original.ship.weapon_1" : "Ship Weapon 1", + "core_systems.category.original.ship.weapon_2" : "Ship Weapon 2", + "core_systems.category.original.ship.thruster_1" : "Ship Thruster 1", + "core_systems.category.original.ship.thruster_2" : "Ship Thruster 2", + "core_systems.category.original.ship.thruster_3" : "Ship Thruster 3", + "core_systems.category.original.ship.thruster_4" : "Ship Thruster 4", + "_comment_Dungeons": "", "dungeons.original.pirate.pirates_outpost": "Avant-Poste Pirate", "dungeons.original.pirate.pirates_outpost.desc": "Une station de ravitaillement caché appartenant au syndicat de la Marque Noire.", - "dungeons.original.tutorial.pirates_outpost": "Tutoriel", - "dungeons.original.tutorial.pirates_outpost.desc": "Un donjon unique.", + "dungeons.original.tutorial.tutorial": "Tutoriel", + "dungeons.original.tutorial.tutorial.desc": "Un donjon unique.", "_comment_Enemies": "", "enemies.original.pirate.black_mark_heavy_cruiser": "Croiseur Lourd Marque Noire", "enemies.original.pirate.raider_frigate": "Frégate d'Assaillant", @@ -46,8 +87,8 @@ "items.materials.original.ores.copper.desc": "Une pile de minerai de cuivre.", "items.materials.original.ores.gold": "Minerai d'or", "items.materials.original.ores.gold.desc": "Une pile de minerai d'or.", - "items.materials.original.ores.ilmenite": "Minerai d'Ilménite", - "items.materials.original.ores.ilmenite.desc": "Une pile de minerai d'ilménite.", + "items.materials.original.ores.ilunite": "Minerai d'Ilménite", + "items.materials.original.ores.ilunite.desc": "Une pile de minerai d'ilménite.", "items.materials.original.ores.iron": "Minerai de fer", "items.materials.original.ores.iron.desc": "Une pile de minerai de fer.", "items.materials.original.ores.wolfrinite": "Minerai de Loufrinite", @@ -63,8 +104,14 @@ "recipes.category.original.hulls": "Coques", "recipes.category.original.organics": "Organismes", "recipes.category.original.spacesuit_parts": "Pièces spatiales", - "_comment_Shop": "", - "shop.category.original.materials": "Matériaux", + "_comment_Shop" : "", + "shop.category.original.consumables" : "Consumables", + "shop.category.original.defence" : "Defence", + "shop.category.original.featured" : "Featured", + "shop.category.original.materials" : "Materials", + "shop.category.original.premium" : "Premium", + "shop.category.original.ships" : "Ships", + "shop.category.original.weapons" : "Weapons", "_comment_Skills": "", "skills.category.original.combat": "Guerre", "skills.category.original.combat.weapon_effiency": "Efficacité de l'arme", @@ -101,6 +148,7 @@ "stats.category.original.resistance.physical": "Résistance physique", "stats.category.original.resistance.plasma": "Résistance au plasma", "_comment_Tabs": "", + "category.tabs.original.admin_panel" : "Admin", "category.tabs.original.crafting": "Fabriquer", "category.tabs.original.dashboard": "Tableaux de bord", "category.tabs.original.dungeons": "Les donjons", diff --git a/game-server/datapacks/original/assets/languages/uk_UA.json b/game-server/datapacks/original/assets/languages/uk_UA.json index 69114f6..829003b 100644 --- a/game-server/datapacks/original/assets/languages/uk_UA.json +++ b/game-server/datapacks/original/assets/languages/uk_UA.json @@ -1,9 +1,50 @@ { + "_comment_Admin" : "", + "admin.category.original.hostile_list" : "Hostile List", + "admin.category.original.item_list" : "Item List", + "admin.category.original.player_list" : "Player List", + "admin.category.original.item_list.all" : "All", + "admin.category.original.item_list.alloys" : "Alloys", + "admin.category.original.item_list.circuits" : "Circuits", + "admin.category.original.item_list.customizables" : "Customizables", + "admin.category.original.item_list.ingots" : "Ingots", + "admin.category.original.item_list.maerials" : "Materials", + "admin.category.original.item_list.ores" : "Ores", + "admin.category.original.item_list.personal" : "Personal", + "admin.category.original.item_list.ships" : "Ships", + "admin.category.original.item_list.shields" : "Shields", + "admin.category.original.item_list.weapons" : "Weapons", + "admin.category.original.hostile_list.all" : "All", + "admin.category.original.hostile_list.ground" : "Ground Units", + "admin.category.original.hostile_list.ships" : "Ships", + "admin.category.original.player_list.all" : "All", + "admin.category.original.player_list.members" : "Members", + "admin.category.original.player_list.moderators" : "Moderators", + "admin.category.original.player_list.admins" : "Admins", + "_comment_Core_Systems" : "", + "core_systems.category.original.person.helmet" : "Personal Helmet", + "core_systems.category.original.person.suit" : "Personal Suit", + "core_systems.category.original.person.gloves" : "Personal Gloves", + "core_systems.category.original.person.boots" : "Personal Boots", + "core_systems.category.original.person.accessory_1" : "Personal Accessory 1", + "core_systems.category.original.person.accessory_2" : "Personal Accessory 2", + "core_systems.category.original.person.accessory_3" : "Personal Accessory 3", + "core_systems.category.original.person.accessory_4" : "Personal Accessory 4", + "core_systems.category.original.person.weapon" : "Personal Weapon", + "core_systems.category.original.ship.hull" : "Ship Hull", + "core_systems.category.original.ship.shields" : "Ship Shield", + "core_systems.category.original.ship.engines" : "Ship Engine", + "core_systems.category.original.ship.weapon_1" : "Ship Weapon 1", + "core_systems.category.original.ship.weapon_2" : "Ship Weapon 2", + "core_systems.category.original.ship.thruster_1" : "Ship Thruster 1", + "core_systems.category.original.ship.thruster_2" : "Ship Thruster 2", + "core_systems.category.original.ship.thruster_3" : "Ship Thruster 3", + "core_systems.category.original.ship.thruster_4" : "Ship Thruster 4", "_comment_Dungeons": "", "dungeons.original.pirate.pirates_outpost": "Аванпост піратів", "dungeons.original.pirate.pirates_outpost.desc": "Прихована станція постачання, що належить синдикату «Чорна Мітка».", - "dungeons.original.tutorial.pirates_outpost": "Навчання", - "dungeons.original.tutorial.pirates_outpost.desc": "Одноразове підземелля для освоєння азів.", + "dungeons.original.tutorial.tutorial": "Навчання", + "dungeons.original.tutorial.tutorial.desc": "Одноразове підземелля для освоєння азів.", "_comment_Enemies": "", "enemies.original.pirate.black_mark_heavy_cruiser": "Важкий крейсер «Чорної Мітки»", "enemies.original.pirate.raider_frigate": "Фрегат рейдерів", @@ -46,8 +87,8 @@ "items.materials.original.ores.copper.desc": "Купа мідної руди.", "items.materials.original.ores.gold": "Золота руда", "items.materials.original.ores.gold.desc": "Купа золотоносної руди.", - "items.materials.original.ores.ilmenite": "Ільменітова руда", - "items.materials.original.ores.ilmenite.desc": "Купа ільменітової руди.", + "items.materials.original.ores.ilunite": "Ільменітова руда", + "items.materials.original.ores.ilunite.desc": "Купа ільменітової руди.", "items.materials.original.ores.iron": "Залізна руда", "items.materials.original.ores.iron.desc": "Купа залізної руди.", "items.materials.original.ores.wolfrinite": "Вольфрамова руда", @@ -64,7 +105,13 @@ "recipes.category.original.organics": "Органіка", "recipes.category.original.spacesuit_parts": "Деталі скафандра", "_comment_Shop": "", + "shop.category.original.consumables" : "Consumables", + "shop.category.original.defence" : "Defence", + "shop.category.original.featured" : "Featured", "shop.category.original.materials": "Матеріали", + "shop.category.original.premium" : "Premium", + "shop.category.original.ships" : "Ships", + "shop.category.original.weapons" : "Weapons", "_comment_Skills": "", "skills.category.original.combat": "Бойові навички", "skills.category.original.combat.weapon_effiency": "Ефективність зброї", @@ -101,6 +148,7 @@ "stats.category.original.resistance.physical": "Фізичний опір", "stats.category.original.resistance.plasma": "Плазмовий опір", "_comment_Tabs": "", + "category.tabs.original.admin_panel" : "Admin", "category.tabs.original.crafting": "Крафт", "category.tabs.original.dashboard": "Головна", "category.tabs.original.dungeons": "Підземелля", diff --git a/game-server/datapacks/original/assets/textures/materials/alloy/steel.png b/game-server/datapacks/original/assets/textures/materials/alloy/steel.png new file mode 100644 index 0000000..58ac20f Binary files /dev/null and b/game-server/datapacks/original/assets/textures/materials/alloy/steel.png differ diff --git a/game-server/datapacks/original/assets/textures/materials/ingot/aluminum.png b/game-server/datapacks/original/assets/textures/materials/ingot/aluminum.png new file mode 100644 index 0000000..318aa19 Binary files /dev/null and b/game-server/datapacks/original/assets/textures/materials/ingot/aluminum.png differ diff --git a/game-server/datapacks/original/assets/textures/materials/ingot/carbon.png b/game-server/datapacks/original/assets/textures/materials/ingot/carbon.png new file mode 100644 index 0000000..d4f0ada Binary files /dev/null and b/game-server/datapacks/original/assets/textures/materials/ingot/carbon.png differ diff --git a/game-server/datapacks/original/assets/textures/materials/ingot/chronite.png b/game-server/datapacks/original/assets/textures/materials/ingot/chronite.png new file mode 100644 index 0000000..ef66bcc Binary files /dev/null and b/game-server/datapacks/original/assets/textures/materials/ingot/chronite.png differ diff --git a/game-server/datapacks/original/assets/textures/materials/ingot/copper.png b/game-server/datapacks/original/assets/textures/materials/ingot/copper.png new file mode 100644 index 0000000..34ca6a5 Binary files /dev/null and b/game-server/datapacks/original/assets/textures/materials/ingot/copper.png differ diff --git a/game-server/datapacks/original/assets/textures/materials/ingot/gold.png b/game-server/datapacks/original/assets/textures/materials/ingot/gold.png new file mode 100644 index 0000000..111e72a Binary files /dev/null and b/game-server/datapacks/original/assets/textures/materials/ingot/gold.png differ diff --git a/game-server/datapacks/original/assets/textures/materials/ingot/iron.png b/game-server/datapacks/original/assets/textures/materials/ingot/iron.png new file mode 100644 index 0000000..8b39dc6 Binary files /dev/null and b/game-server/datapacks/original/assets/textures/materials/ingot/iron.png differ diff --git a/game-server/datapacks/original/assets/textures/materials/ingot/neutronium.gif b/game-server/datapacks/original/assets/textures/materials/ingot/neutronium.gif new file mode 100644 index 0000000..5289f58 Binary files /dev/null and b/game-server/datapacks/original/assets/textures/materials/ingot/neutronium.gif differ diff --git a/game-server/datapacks/original/assets/textures/materials/ingot/titanium.png b/game-server/datapacks/original/assets/textures/materials/ingot/titanium.png new file mode 100644 index 0000000..8ad579c Binary files /dev/null and b/game-server/datapacks/original/assets/textures/materials/ingot/titanium.png differ diff --git a/game-server/datapacks/original/assets/textures/materials/ingot/tungsten.png b/game-server/datapacks/original/assets/textures/materials/ingot/tungsten.png new file mode 100644 index 0000000..e42d6aa Binary files /dev/null and b/game-server/datapacks/original/assets/textures/materials/ingot/tungsten.png differ diff --git a/game-server/datapacks/original/data/dungeons/tutorial/tutorial.json b/game-server/datapacks/original/data/dungeons/tutorial/tutorial.json index 8038440..d15d112 100644 --- a/game-server/datapacks/original/data/dungeons/tutorial/tutorial.json +++ b/game-server/datapacks/original/data/dungeons/tutorial/tutorial.json @@ -1,8 +1,8 @@ { "dungeon": { "id": "original:tutorial/tutorial_dungeon", - "displayName": "dungeons.original.tutorial.pirates_outpost", - "description": "dungeons.original.tutorial.pirates_outpost.desc", + "displayName": "dungeons.original.tutorial.tutorial", + "description": "dungeons.original.tutorial.tutorial.desc", "energyCost": 0, "repeatable": false, "rooms": [ diff --git a/game-server/datapacks/original/data/items/materials/ores/ilmenite.json b/game-server/datapacks/original/data/items/materials/ores/ilmenite.json deleted file mode 100644 index e190e91..0000000 --- a/game-server/datapacks/original/data/items/materials/ores/ilmenite.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "materials": { - "id": "original:ore_ilmenite", - "texture": "original/assets/textures/materials/ore/ilmenite.png", - "displayName": "items.materials.original.ores.ilmenite", - "description": "items.materials.original.ores.ilmenite.desc", - "meta": { - "storeCategory": "original:materials" - } - } -} diff --git a/game-server/datapacks/original/data/items/materials/ores/ilunite.json b/game-server/datapacks/original/data/items/materials/ores/ilunite.json new file mode 100644 index 0000000..5a2ae44 --- /dev/null +++ b/game-server/datapacks/original/data/items/materials/ores/ilunite.json @@ -0,0 +1,11 @@ +{ + "materials": { + "id": "original:ore_ilunite", + "texture": "original/assets/textures/materials/ore/ilunite.png", + "displayName": "items.materials.original.ores.ilunite", + "description": "items.materials.original.ores.ilunite.desc", + "meta": { + "storeCategory": "original:materials" + } + } +} 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/datapacks/original/manifest.json b/game-server/datapacks/original/manifest.json index d6a9e57..d1994cc 100644 --- a/game-server/datapacks/original/manifest.json +++ b/game-server/datapacks/original/manifest.json @@ -1,6 +1,159 @@ { "name": "original", "version": "0.0.1", + "admin_item_list": { + "categories": { + "original:all": { + "displayName": "admin.category.original.item_list.all" + }, + "original:alloys": { + "displayName": "admin.category.original.item_list.alloys" + }, + "original:circuits": { + "displayName": "admin.category.original.item_list.circuits" + }, + "original:customizables": { + "displayName": "admin.category.original.item_list.customizables" + }, + "original:ingots": { + "displayName": "admin.category.original.item_list.ingots" + }, + "original:materials": { + "displayName": "admin.category.original.item_list.materials" + }, + "original:ores": { + "displayName": "admin.category.original.item_list.ores" + }, + "original:personal": { + "displayName": "admin.category.original.item_list.personal" + }, + "original:ships": { + "displayName": "admin.category.original.item_list.ships" + }, + "original:shields": { + "displayName": "admin.category.original.item_list.shields" + }, + "original:weapons": { + "displayName": "admin.category.original.item_list.weapons" + } + } + }, + "admin_hostiles_list": { + "categories": { + "original:all": { + "displayName": "admin.category.original.hostile_list.all" + }, + "original:ground": { + "displayName": "admin.category.original.hostile_list.ground" + }, + "original:ships": { + "displayName": "admin.category.original.hostile_list.ship" + } + } + }, + "admin_panel": { + "categories": { + "original:admin_hostiles_list": { + "displayName": "admin.category.original.hostile_list" + }, + "original:admin_item_list": { + "displayName": "admin.category.original.item_list" + }, + "original:admin_player_list": { + "displayName": "admin.category.original.player_list" + } + } + }, + "admin_player_list": { + "categories": { + "original:all": { + "displayName": "admin.category.original.player_list.all" + }, + "original:members": { + "displayName": "admin.category.original.player_list.members" + }, + "original:moderators": { + "displayName": "admin.category.original.player_list.moderators" + }, + "original:admins": { + "displayName": "admin.category.original.player_list.admins" + } + } + }, + "admin_role": { + "categories": { + "original:all": { + "displayName": "admin.category.original.player_list.all" + }, + "original:members": { + "displayName": "admin.category.original.player_list.members" + }, + "original:moderators": { + "displayName": "admin.category.original.player_list.moderators" + }, + "original:admins": { + "displayName": "admin.category.original.player_list.admins" + } + } + }, + "core_systems": { + "categories": { + "original:person_helmet": { + "displayName": "core_systems.category.original.person.helmet" + }, + "original:person_suit": { + "displayName": "core_systems.category.original.person.suit" + }, + "original:person_gloves": { + "displayName": "core_systems.category.original.person.gloves" + }, + "original:person_boots": { + "displayName": "core_systems.category.original.person.boots" + }, + "original:person_accessory_1": { + "displayName": "core_systems.category.original.person.accessory_1" + }, + "original:person_accessory_2": { + "displayName": "core_systems.category.original.person.accessory_2" + }, + "original:person_accessory_3": { + "displayName": "core_systems.category.original.person.accessory_3" + }, + "original:person_accessory_4": { + "displayName": "core_systems.category.original.person.accessory_4" + }, + "original:person_weapons": { + "displayName": "core_systems.category.original.person.weapon" + }, + "original:ship_hull": { + "displayName": "core_systems.category.original.ship.hull" + }, + "original:ship_shields": { + "displayName": "core_systems.category.original.ship.shields" + }, + "original:ship_engines": { + "displayName": "core_systems.category.original.ship.engines" + }, + "original:ship_weapon_1": { + "displayName": "core_systems.category.original.ship.weapon_1" + }, + "original:ship_weapon_2": { + "displayName": "core_systems.category.original.ship.weapon_2" + }, + "original:ship_thruster_1": { + "displayName": "core_systems.category.original.ship.thruster_1" + }, + "original:ship_thruster_2": { + "displayName": "core_systems.category.original.ship.thruster_2" + }, + "original:ship_thruster_3": { + "displayName": "core_systems.category.original.ship.thruster_3" + }, + "original:ship_thruster_4": { + "displayName": "core_systems.category.original.ship.thruster_4" + } + } + }, "recipes": { "categories": { "original:alloys": { @@ -31,8 +184,26 @@ }, "shop": { "categories": { + "original:featured": { + "displayName": "shop.category.original.featured" + }, + "original:ships": { + "displayName": "shop.category.original.ships" + }, + "original:weapons": { + "displayName": "shop.category.original.weapons" + }, + "original:defence": { + "displayName": "shop.category.original.defence" + }, + "original:consumables": { + "displayName": "shop.category.original.consumables" + }, "original:materials": { "displayName": "shop.category.original.materials" + }, + "original:premium": { + "displayName": "shop.category.original.premium" } } }, diff --git a/game-server/package.json b/game-server/package.json index e5b6a44..0b55fcd 100644 --- a/game-server/package.json +++ b/game-server/package.json @@ -18,6 +18,7 @@ "dotenv": "^17.3.1", "express": "^5.2.1", "jsonwebtoken": "^9.0.3", + "pg": "^8.20.0", "sequelize": "^6.37.8", "socket.io": "^4.8.3", "sqlite3": "^6.0.1" diff --git a/game-server/src/config/config.js b/game-server/src/config/config.js index ff63738..2e6146e 100644 --- a/game-server/src/config/config.js +++ b/game-server/src/config/config.js @@ -7,6 +7,7 @@ const config = { serverSecret: process.env.SERVER_SECRET, serverDescription: process.env.DESCRIPTION, serverRegion: process.env.REGION, + dbUri: process.env.DB_URI || "local", }; module.exports = config; diff --git a/game-server/src/config/db.js b/game-server/src/config/db.js index 822c30d..5fadd0e 100644 --- a/game-server/src/config/db.js +++ b/game-server/src/config/db.js @@ -1,30 +1,44 @@ const { Sequelize } = require("sequelize"); +const config = require("./config"); // Шлях до твого файлу з конфігом -const sequelize = new Sequelize({ - dialect: "sqlite", - storage: "./database.sqlite", - logging: false, - dialectOptions: { - timeout: 20000, - }, - pool: { - max: 1, - min: 1, - idle: 10000, - acquire: 30000, - }, -}); +const isLocal = config.dbUri === "local"; + +const sequelize = isLocal + ? new Sequelize({ + dialect: "sqlite", + storage: "./database.sqlite", + logging: false, + dialectOptions: { timeout: 20000 }, + pool: { max: 1, min: 1, idle: 10000, acquire: 30000 }, + }) + : new Sequelize(config.dbUri, { + dialect: "postgres", + logging: false, + dialectOptions: { + ssl: { + require: true, + rejectUnauthorized: false, + }, + }, + pool: { max: 5, min: 0, idle: 10000, acquire: 30000 }, + }); sequelize.initDatabase = async () => { try { await sequelize.authenticate(); - await sequelize.query("PRAGMA journal_mode=WAL;"); - await sequelize.query("PRAGMA foreign_keys = OFF;"); + + if (isLocal) { + await sequelize.query("PRAGMA journal_mode=WAL;"); + await sequelize.query("PRAGMA foreign_keys = OFF;"); + } await sequelize.sync({ alter: true }); - await sequelize.query("PRAGMA foreign_keys = ON;"); + if (isLocal) { + await sequelize.query("PRAGMA foreign_keys = ON;"); + } + console.log(`✅ Database connected (${isLocal ? "SQLite" : "Postgres"})`); return true; } catch (error) { console.error("❌ Database Init Error:", error); diff --git a/game-server/src/game/ChatManager.js b/game-server/src/game/ChatManager.js new file mode 100644 index 0000000..0e79a4f --- /dev/null +++ b/game-server/src/game/ChatManager.js @@ -0,0 +1,70 @@ +const Player = require("../models/Player"); +const { Op } = require("sequelize"); +const Message = require("../models/Message.js"); + +class ChatManager { + async getGlobalHistory(limit = 50) { + try { + return await Message.findAll({ + where: { type: "global" }, + limit, + order: [["createdAt", "ASC"]], + include: [ + { + model: Player, + as: "sender", + attributes: ["username"], + }, + ], + }); + } catch (error) { + console.error("History Error:", error); + return []; + } + } + + async saveMessage({ content, type, senderId, receiverId = null }) { + try { + const newMessage = await Message.create({ + content, + type, + senderId, + receiverId, + }); + const sender = await Player.findByPk(senderId, { + attributes: ["username"], + }); + + return { + id: newMessage.id, + content: newMessage.content, + type: newMessage.type, + senderId: newMessage.senderId, + senderName: sender ? sender.username : "Unknown", + receiverId: newMessage.receiverId, + createdAt: newMessage.createdAt, + }; + } catch (error) { + console.error("Save Message Error:", error); + throw error; + } + } + + async searchPlayers(query, excludeId) { + try { + return await Player.findAll({ + where: { + username: { [Op.like]: `%${query}%` }, + id: { [Op.ne]: excludeId }, + }, + attributes: ["id", "username", "level"], + limit: 10, + }); + } catch (error) { + console.error("Search Error:", error); + return []; + } + } +} + +module.exports = new ChatManager(); 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/game/NotificationManager.js b/game-server/src/game/NotificationManager.js new file mode 100644 index 0000000..62e3da4 --- /dev/null +++ b/game-server/src/game/NotificationManager.js @@ -0,0 +1,34 @@ +const Notification = require("../models/Notification"); + +class NotificationManager { + async createNotification({ playerId, type, title, message, data = {} }) { + try { + return await Notification.create({ + playerId, + type, + title, + message, + data, + }); + } catch (error) { + console.error("Notify Error:", error); + } + } + + async getPlayerNotifications(playerId) { + return await Notification.findAll({ + where: { playerId }, + order: [["createdAt", "DESC"]], + limit: 20, + }); + } + + async markAsRead(notificationId) { + return await Notification.update( + { isRead: true }, + { where: { id: notificationId } }, + ); + } +} + +module.exports = new NotificationManager(); diff --git a/game-server/src/game/SessionManager.js b/game-server/src/game/SessionManager.js index b845081..cd938ee 100644 --- a/game-server/src/game/SessionManager.js +++ b/game-server/src/game/SessionManager.js @@ -6,7 +6,7 @@ class SessionManager { addPlayer(socketId, playerRaw) { this.sessions.set(socketId, { id: playerRaw.id, - nickname: playerRaw.username, + username: playerRaw.username, level: playerRaw.level, energy: playerRaw.energy, maxEnergy: playerRaw.maxEnergy, diff --git a/game-server/src/models/Friend.js b/game-server/src/models/Friend.js new file mode 100644 index 0000000..c4a698b --- /dev/null +++ b/game-server/src/models/Friend.js @@ -0,0 +1,16 @@ +const { DataTypes } = require("sequelize"); +const sequelize = require("../config/db"); + +const Friend = sequelize.define("Friend", { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + status: { + type: DataTypes.ENUM("pending", "accepted"), + defaultValue: "pending", + }, +}); + +module.exports = Friend; diff --git a/game-server/src/models/Message.js b/game-server/src/models/Message.js new file mode 100644 index 0000000..e205621 --- /dev/null +++ b/game-server/src/models/Message.js @@ -0,0 +1,23 @@ +const { DataTypes } = require("sequelize"); +const sequelize = require("../config/db"); + +const Message = sequelize.define("Message", { + content: { + type: DataTypes.TEXT, + allowNull: false, + }, + type: { + type: DataTypes.ENUM("global", "private"), + defaultValue: "global", + }, + senderId: { + type: DataTypes.STRING, + allowNull: false, + }, + receiverId: { + type: DataTypes.STRING, + allowNull: true, + }, +}); + +module.exports = Message; diff --git a/game-server/src/models/Notification.js b/game-server/src/models/Notification.js new file mode 100644 index 0000000..47e1ba9 --- /dev/null +++ b/game-server/src/models/Notification.js @@ -0,0 +1,29 @@ +const { DataTypes } = require("sequelize"); +const sequelize = require("../config/db"); + +const Notification = sequelize.define("Notification", { + playerId: { + type: DataTypes.STRING, + allowNull: false, + }, + type: { + type: DataTypes.ENUM("friend_request", "crafting", "system", "trade"), + allowNull: false, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + message: { + type: DataTypes.TEXT, + }, + data: { + type: DataTypes.JSON, + }, + isRead: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, +}); + +module.exports = Notification; diff --git a/game-server/src/models/associations.js b/game-server/src/models/associations.js new file mode 100644 index 0000000..a5addd5 --- /dev/null +++ b/game-server/src/models/associations.js @@ -0,0 +1,27 @@ +const Player = require("./Player"); +const Message = require("./Message.js"); +const Friend = require("./Friend"); +const setupAssociations = () => { + Message.belongsTo(Player, { + as: "sender", + foreignKey: "senderId", + }); + + Message.belongsTo(Player, { + as: "receiver", + foreignKey: "receiverId", + }); + + Player.hasMany(Message, { + foreignKey: "senderId", + }); + + Player.belongsToMany(Player, { + through: Friend, + as: "Friends", + foreignKey: "playerId", + otherKey: "friendId", + }); +}; + +module.exports = setupAssociations; diff --git a/game-server/src/models/index.js b/game-server/src/models/index.js index 2f7b6cb..cc0076f 100644 --- a/game-server/src/models/index.js +++ b/game-server/src/models/index.js @@ -1,12 +1,16 @@ const sequelize = require("../config/db"); const Player = require("./Player"); const Inventory = require("./Inventory"); +const setupAssociations = require("./associations"); +const Notification = require("./Notification"); Player.hasMany(Inventory, { foreignKey: "playerId", as: "inventory" }); Inventory.belongsTo(Player, { foreignKey: "playerId" }); +setupAssociations(); module.exports = { sequelize, Player, Inventory, + Notification, }; diff --git a/game-server/src/sockets/handlers/adminHandler.js b/game-server/src/sockets/handlers/adminHandler.js index 490dc54..716199a 100644 --- a/game-server/src/sockets/handlers/adminHandler.js +++ b/game-server/src/sockets/handlers/adminHandler.js @@ -61,7 +61,7 @@ module.exports = (io, socket) => { case "/clear": { const [_, targetName] = args; const targetPlayer = await Player.findOne({ - where: { name: targetName }, + where: { username: targetName }, }); if (!targetPlayer) throw new Error("Player not found."); diff --git a/game-server/src/sockets/handlers/chatHandler.js b/game-server/src/sockets/handlers/chatHandler.js new file mode 100644 index 0000000..d9cb627 --- /dev/null +++ b/game-server/src/sockets/handlers/chatHandler.js @@ -0,0 +1,63 @@ +const chatManager = require("../../game/ChatManager"); + +module.exports = (io, socket) => { + socket.on("chat:get_global_history", async () => { + const history = await chatManager.getGlobalHistory(); + const formattedHistory = history.map((m) => ({ + id: m.id, + content: m.content, + senderName: m.sender?.username || "Unknown", + senderId: m.senderId, + createdAt: m.createdAt, + type: m.type, + })); + socket.emit("chat:global_history", formattedHistory); + }); + socket.on("chat:get_global_history", async () => { + console.log(`Player ${socket.player?.id} requested chat history`); + const history = await chatManager.getGlobalHistory(); + + const formattedHistory = history.map((m) => ({ + id: m.id, + content: m.content, + senderName: m.sender?.username || "Unknown", + senderId: m.senderId, + createdAt: m.createdAt, + type: m.type, + })); + + socket.emit("chat:global_history", formattedHistory); + }); + socket.on("chat:send_message", async (payload) => { + try { + const senderId = socket.user?.id; + if (!senderId) return; + const messageData = await chatManager.saveMessage({ + content: payload.content, + type: payload.type || "global", + senderId: senderId, + receiverId: payload.receiverId, + }); + + if (messageData.type === "global") { + io.emit("chat:new_message", messageData); + } else { + socket + .to(`user_${payload.receiverId}`) + .emit("chat:new_message", messageData); + socket.emit("chat:new_message", messageData); + } + } catch (err) { + console.log(err); + socket.emit("error", { message: "CHAT_SEND_ERROR" }); + } + }); + + socket.on("player:search", async ({ query }) => { + const senderId = socket.player?.id; + if (!query || !senderId) return; + + const results = await chatManager.searchPlayers(query, senderId); + socket.emit("player:search_results", results); + }); +}; diff --git a/game-server/src/sockets/handlers/connectionHandler.js b/game-server/src/sockets/handlers/connectionHandler.js index 42a1a68..e103c08 100644 --- a/game-server/src/sockets/handlers/connectionHandler.js +++ b/game-server/src/sockets/handlers/connectionHandler.js @@ -1,3 +1,4 @@ +const { Op } = require("sequelize"); const Player = require("../../models/Player"); const sessionManager = require("../../game/SessionManager"); const economyService = require("../../game/EconomyService.js"); @@ -8,7 +9,6 @@ module.exports = async (io, socket) => { const sid = socket.id.substring(0, 5); if (!userId) { - console.log(`⚠️ [${sid}] Anonymous connection rejected`); return socket.disconnect(); } @@ -21,7 +21,6 @@ module.exports = async (io, socket) => { id: userId, username: username, }); - console.log(`🆕 [${sid}] New player registered: ${username}`); } catch (createErr) { player = await Player.findByPk(userId); } @@ -35,6 +34,20 @@ module.exports = async (io, socket) => { const playerRaw = player.get({ plain: true }); sessionManager.addPlayer(socket.id, playerRaw); + const onlinePlayersData = sessionManager.getAllOnline(); + const onlineUsernames = onlinePlayersData.map((p) => p.username); + + const onlineIds = onlinePlayersData.map((p) => p.id); + + const offlinePlayersModels = await Player.findAll({ + where: { + id: { [Op.notIn]: onlineIds }, + }, + attributes: ["username"], + raw: true, + }); + + const offlineUsernames = offlinePlayersModels.map((p) => p.username); socket.emit("session:ready", { player: { @@ -45,10 +58,12 @@ module.exports = async (io, socket) => { experience: playerRaw.experience, }, offlineEarned: offlineCredits, - onlineCount: sessionManager.getAllOnline().length, + onlinePlayers: onlineUsernames, + offlinePlayers: offlineUsernames, }); socket.broadcast.emit("player:joined", { username: playerRaw.username }); + socket.on("player:get_dashboard", async () => { try { const p = await Player.findByPk(userId); @@ -57,6 +72,7 @@ module.exports = async (io, socket) => { console.error(`❌ [${sid}] Dashboard error:`, err.message); } }); + socket.on("disconnect", async () => { try { await Player.update( @@ -67,7 +83,7 @@ module.exports = async (io, socket) => { economyService.removePlayer(userId); sessionManager.removePlayer(socket.id); - console.log(`🔌 [${sid}] Player disconnected: ${userId}`); + socket.broadcast.emit("player:left", { username: playerRaw.username }); }); } catch (err) { console.error(`❌ [${sid}] Connection Error:`, err.message); 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(); }; diff --git a/game-server/src/sockets/handlers/friendHandler.js b/game-server/src/sockets/handlers/friendHandler.js new file mode 100644 index 0000000..6065315 --- /dev/null +++ b/game-server/src/sockets/handlers/friendHandler.js @@ -0,0 +1,114 @@ +const Player = require("../../models/Player"); +const Friend = require("../../models/Friend"); +const NotificationManager = require("../../game/NotificationManager"); +const Notification = require("../../models/Notification"); + +module.exports = (io, socket) => { + socket.on("player:search", async ({ query }) => { + try { + const players = await Player.findAll({ + where: { + username: { [require("sequelize").Op.like]: `%${query}%` }, + id: { [require("sequelize").Op.ne]: socket.user.id }, + }, + limit: 5, + attributes: ["id", "username", "level"], + }); + socket.emit("player:search_results", players); + } catch (e) { + console.error(e); + } + }); + + socket.on("friend:add", async ({ friendId }) => { + try { + const player = socket.user; + + await NotificationManager.createNotification({ + playerId: friendId, + type: "friend_request", + title: "NEW FRIEND REQUEST", + message: `${player.username} wants to add you as a friend.`, + data: { fromId: player.id }, + }); + + io.to(friendId).emit("notification:new", { + type: "friend_request", + title: "NEW FRIEND REQUEST", + message: `${player.username} wants to add you as a friend.`, + data: { fromId: player.id }, + createdAt: new Date(), + }); + } catch (e) { + socket.emit("error", { message: "FAILED_TO_SEND_REQUEST" }); + } + }); + + socket.on("friend:accept", async ({ friendId, notificationId }) => { + try { + const myId = socket.user.id; + + const exists = await Friend.findOne({ + where: { playerId: myId, friendId: friendId }, + }); + + if (!exists) { + await Friend.bulkCreate([ + { playerId: myId, friendId: friendId }, + { playerId: friendId, friendId: myId }, + ]); + + await Notification.destroy({ + where: { id: notificationId, playerId: myId }, + }); + + const myUpdated = await Player.findByPk(myId, { + include: [ + { + model: Player, + as: "Friends", + attributes: ["id", "username", "level"], + }, + ], + }); + socket.emit("friend:list", myUpdated.Friends || []); + + const friendUpdated = await Player.findByPk(friendId, { + include: [ + { + model: Player, + as: "Friends", + attributes: ["id", "username", "level"], + }, + ], + }); + io.to(friendId).emit("friend:list", friendUpdated.Friends || []); + + const unreadCount = await Notification.count({ + where: { playerId: myId, isRead: false }, + }); + socket.emit("notifications:unread_count", unreadCount); + } + } catch (e) { + console.error(e); + socket.emit("error", { message: "FAILED_TO_ACCEPT_FRIEND" }); + } + }); + + socket.on("friend:get_list", async () => { + try { + const player = await Player.findByPk(socket.user.id, { + include: [ + { + model: Player, + as: "Friends", + attributes: ["id", "username", "level"], + }, + ], + }); + socket.emit("friend:list", player.Friends || []); + } catch (e) { + console.error(e); + } + }); +}; diff --git a/game-server/src/sockets/handlers/notificationHandler.js b/game-server/src/sockets/handlers/notificationHandler.js new file mode 100644 index 0000000..0f0e1e0 --- /dev/null +++ b/game-server/src/sockets/handlers/notificationHandler.js @@ -0,0 +1,51 @@ +const Notification = require("../../models/Notification"); + +module.exports = (io, socket) => { + socket.on("notifications:get_all", async () => { + try { + const list = await Notification.findAll({ + where: { playerId: socket.user.id }, + order: [["createdAt", "DESC"]], + limit: 50, + }); + + socket.emit("notifications:list", list); + + const unreadCount = list.filter((n) => !n.isRead).length; + socket.emit("notifications:unread_count", unreadCount); + } catch (e) { + console.error("Fetch notifications error:", e); + } + }); + + socket.on("notification:read", async ({ id }) => { + try { + await Notification.update( + { isRead: true }, + { where: { id, playerId: socket.user.id } }, + ); + + const unreadCount = await Notification.count({ + where: { playerId: socket.user.id, isRead: false }, + }); + socket.emit("notifications:unread_count", unreadCount); + } catch (e) { + console.error("Read notification error:", e); + } + }); + + socket.on("notification:dismiss", async ({ id }) => { + try { + await Notification.destroy({ + where: { id, playerId: socket.player.id }, + }); + + const unreadCount = await Notification.count({ + where: { playerId: socket.player.id, isRead: false }, + }); + socket.emit("notifications:unread_count", unreadCount); + } catch (e) { + console.error("Dismiss notification error:", e); + } + }); +}; diff --git a/game-server/src/sockets/socket.js b/game-server/src/sockets/socket.js index 6bfb90d..1f2435a 100644 --- a/game-server/src/sockets/socket.js +++ b/game-server/src/sockets/socket.js @@ -4,6 +4,9 @@ const inventoryHandler = require("./handlers/inventoryHandler"); const craftingHandler = require("./handlers/craftingHandler"); const adminHandler = require("./handlers/adminHandler"); const dungeonHandler = require("./handlers/dungeonHandler"); +const chatHandler = require("./handlers/chatHandler"); +const friendHandler = require("./handlers/friendHandler"); +const notificationHandler = require("./handlers/notificationHandler"); const initSockets = (io) => { io.use(socketAuth); @@ -14,6 +17,9 @@ const initSockets = (io) => { craftingHandler(io, socket); adminHandler(io, socket); dungeonHandler(io, socket); + chatHandler(io, socket); + friendHandler(io, socket); + notificationHandler(io, socket); }); }; diff --git a/package.json b/package.json new file mode 100644 index 0000000..f075989 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "gso-project-manager", + "version": "1.0.0", + "scripts": { + "install-all": "cd api && npm install && cd ../game-server && npm install && cd ../client && npm install", + "dev": "npx concurrently \"npm run dev-api\" \"npm run dev-server\" \"npm run dev-client\"", + "dev-api": "cd api && npm run start", + "dev-server": "cd game-server && npm run start", + "dev-server-extras": "npx concurrently \"npm run dev-server-extra-proxima-centauri\" \"npm run dev-server-extra-ran\" \"npm run dev-server-extra-sol\" \"npm run dev-server-extra-tau-ceti\"", + "dev-server-extra-proxima-centauri": "cd game-server-proxima-centauri && npm run start", + "dev-server-extra-ran": "cd game-server-ran && npm run start", + "dev-server-extra-sol": "cd game-server-sol && npm run start", + "dev-server-extra-tau-ceti": "cd game-server-tau-ceti && npm run start", + "dev-client": "cd client && npm run dev" + }, + "dependencies": { + "concurrently": "^8.2.2" + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..4c38d8d --- /dev/null +++ b/readme.md @@ -0,0 +1,21 @@ +# GSO Project + +Multiplayer space-themed game built with React, Socket.io. + +## ⚠️ Requirements + +- **Node.js**: Version **22.x** or higher is strictly required (recommended for ESM support and optimal performance). +- **npm**: Included with Node.js. + +> **Note**: You can check your currently installed version by running `node -v` in your terminal. + +## 🚀 Getting Started + +### 1. Unified Dependencies Installation + +To automatically install all dependencies in the correct sub-projects (`api`, `game-server`, `client`) at once, run: + +```bash +npm run install-all +npm run dev +```