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) => (
onTabChange(tab.id)}
+ className={`nav-btn ${activeTab === tab.id ? "active" : ""} ${tab.id}`}
+ onClick={() => handleTabClick(tab.id)}
>
-
+
+
+ {tab.id === "notifications" && unreadCount > 0 && (
+ {unreadCount}
+ )}
+
{getLabel(tab.id)}
diff --git a/client/src/views/GameInterface/tabs/ChatTab.jsx b/client/src/views/GameInterface/tabs/ChatTab.jsx
new file mode 100644
index 0000000..75ada32
--- /dev/null
+++ b/client/src/views/GameInterface/tabs/ChatTab.jsx
@@ -0,0 +1,232 @@
+import React, { useState, useEffect, useRef } from "react";
+import { useSocket } from "../../../hooks/useSocket";
+import "./styles/ChatTab.css";
+
+const ChatTab = () => {
+ const { socket } = useSocket();
+ const [activeChat, setActiveChat] = useState("global");
+ const [searchQuery, setSearchQuery] = useState("");
+ const [searchResults, setSearchResults] = useState([]);
+ const [messages, setMessages] = useState([]);
+ const [friends, setFriends] = useState([]);
+ const [inputValue, setInputValue] = useState("");
+ const [showSidebar, setShowSidebar] = useState(true);
+ const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
+
+ const messagesEndRef = useRef(null);
+
+ useEffect(() => {
+ const handleResize = () => {
+ const mobile = window.innerWidth <= 768;
+ setIsMobile(mobile);
+ if (!mobile) setShowSidebar(true);
+ };
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ useEffect(() => {
+ if (!socket) return;
+
+ // Початкові запити
+ socket.emit("friend:get_list");
+ if (activeChat === "global") {
+ socket.emit("chat:get_global_history");
+ }
+
+ const handleNewMessage = (msg) => {
+ if (
+ (activeChat === "global" && msg.type === "global") ||
+ (activeChat !== "global" &&
+ (msg.senderId === activeChat || msg.receiverId === activeChat))
+ ) {
+ setMessages((prev) => [...prev, msg]);
+ }
+ };
+
+ const handleHistory = (history) => setMessages(history);
+ const handleSearchResults = (results) => setSearchResults(results);
+ const handleFriendList = (list) => setFriends(list);
+
+ socket.on("chat:new_message", handleNewMessage);
+ socket.on("chat:global_history", handleHistory);
+ socket.on("player:search_results", handleSearchResults);
+ socket.on("friend:list", handleFriendList);
+
+ return () => {
+ socket.off("chat:new_message", handleNewMessage);
+ socket.off("chat:global_history", handleHistory);
+ socket.off("player:search_results", handleSearchResults);
+ socket.off("friend:list", handleFriendList);
+ };
+ }, [socket, activeChat]);
+
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
+ }, [messages]);
+
+ const handleSearch = (e) => {
+ const query = e.target.value;
+ setSearchQuery(query);
+ if (query.length > 1) {
+ socket.emit("player:search", { query });
+ } else {
+ setSearchResults([]);
+ }
+ };
+
+ const addFriend = (player) => {
+ socket.emit("friend:add", { friendId: player.id });
+ setSearchQuery("");
+ setSearchResults([]);
+ };
+
+ const sendMessage = () => {
+ if (!inputValue.trim() || !socket) return;
+ socket.emit("chat:send_message", {
+ content: inputValue,
+ type: activeChat === "global" ? "global" : "private",
+ receiverId: activeChat === "global" ? null : activeChat,
+ });
+ setInputValue("");
+ };
+
+ const selectChat = (id) => {
+ setActiveChat(id);
+ if (isMobile) setShowSidebar(false);
+ };
+
+ const getChatName = () => {
+ if (activeChat === "global") return "GLOBAL_SYSTEM_CHAT";
+ const friend = friends.find((f) => f.id === activeChat);
+ return friend ? friend.username : "UNKNOWN_PILOT";
+ };
+
+ return (
+
+
+
+
+
+ {isMobile && (
+
setShowSidebar(true)}>
+
+
+ )}
+
+
+
{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.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 && (
+
setSelectedItem(null)}
+ >
+ ×
+
+ )}
+
+
+
+
})
+
+
+
{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" && (
+ handleAction(n.id, "accept_friend", n.data)}
+ >
+ ACCEPT
+
+ )}
+ handleAction(n.id, "dismiss")}
+ >
+
+
+
+
+ ))}
+
+
+ );
+};
+
+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}
×
@@ -36,35 +34,32 @@ const CraftModal = ({
Required Resources
- {recipe.ingredients &&
- recipe.ingredients.map((ing) => {
- const owned = getOwnedAmount(ing.itemId);
- const hasEnough = owned >= ing.quantity;
+ {recipe.ingredients?.map((ing) => {
+ const owned = getOwnedAmount(ing.itemId);
+ const hasEnough = owned >= ing.quantity;
- return (
-
-
-
-
- {ing.name || ing.itemId.replace("_", " ")}
-
-
-
-
- {owned}
-
- / {ing.quantity}
-
+ return (
+
+
+
+ {ing.displayName}
- );
- })}
+
+
+ {owned}
+
+ / {ing.quantity}
+
+
+ );
+ })}
@@ -76,32 +71,50 @@ const CraftModal = ({
Result:
- {displayName} x{recipe.result?.quantity || 1}
+ {recipe.displayName} x{outputQty}
Time:
- {craftTime}s
+ {recipe.time_seconds}s
-
canAfford && !isBusy && onStartCraft(recipe)}
- disabled={!canAfford || isBusy}
- >
- {isBusy
- ? "System Busy..."
- : !canAfford
- ? "Insufficient Resources"
- : `Start Construction (${craftTime}s)`}
-
-
- Close
-
+ {activeCraft && activeCraft.recipeId === recipe.id ? (
+
+
+ Processing... {activeCraft.timeLeft}s
+
+
+
+ ) : (
+ <>
+
canAfford && !isBusy && onStartCraft(recipe)}
+ disabled={!canAfford || isBusy}
+ >
+ {isBusy
+ ? "System Busy..."
+ : !canAfford
+ ? "Insufficient Resources"
+ : `Start Construction (${recipe.time_seconds}s)`}
+
+
+ Close
+
+ >
+ )}
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
+```