Merge branch 'main' of https://github.com/Korvarix/Galaxy-Strike-Online
105
Admin Panel.md
Normal file
@ -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)
|
||||
@ -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("");
|
||||
};
|
||||
|
||||
20
client/src/config/api.js
Normal file
@ -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();
|
||||
},
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
46
client/src/services/PlayerManager.js
Normal file
@ -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();
|
||||
@ -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: <ShopTab />,
|
||||
crafting: <CraftingTab />,
|
||||
itemlist: <ItemListTab />,
|
||||
chat: <ChatTab />,
|
||||
notifications: <Notification />,
|
||||
};
|
||||
|
||||
return (
|
||||
<div id="gameInterface" className="game-interface">
|
||||
<GameHeader onReturn={onExit} />
|
||||
|
||||
<Navigation activeTab={activeTab} onTabChange={setActiveTab} />
|
||||
|
||||
<main className="main-content">
|
||||
{tabs[activeTab] || <DashboardTab />}
|
||||
</main>
|
||||
|
||||
<Console players={onlinePlayers} />
|
||||
<Console />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -25,7 +25,6 @@ const GameHeader = ({ onReturn }) => {
|
||||
<span className="player-level">Lv. 1</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="header-center">
|
||||
<div className="resources">
|
||||
<div className="resource">
|
||||
@ -34,17 +33,18 @@ const GameHeader = ({ onReturn }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="header-right">
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
className="header-icon-btn"
|
||||
onClick={() => setIsSettingsOpen(true)}
|
||||
style={{ marginRight: "10px" }}
|
||||
>
|
||||
<i className="fas fa-cog"></i>
|
||||
</button>
|
||||
|
||||
<button className="btn btn-warning" onClick={handleHomeClick}>
|
||||
<button
|
||||
className="header-icon-btn exit-btn"
|
||||
onClick={handleHomeClick}
|
||||
>
|
||||
<i className="fas fa-home"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`nav-btn ${activeTab === tab.id ? "active" : ""}`}
|
||||
onClick={() => onTabChange(tab.id)}
|
||||
className={`nav-btn ${activeTab === tab.id ? "active" : ""} ${tab.id}`}
|
||||
onClick={() => handleTabClick(tab.id)}
|
||||
>
|
||||
<div className="nav-btn-content">
|
||||
<i className={`fas ${tab.icon}`}></i>
|
||||
<div className="icon-wrapper">
|
||||
<i className={`fas ${tab.icon}`}></i>
|
||||
{tab.id === "notifications" && unreadCount > 0 && (
|
||||
<span className="nav-badge">{unreadCount}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="nav-label">{getLabel(tab.id)}</span>
|
||||
</div>
|
||||
<div className="nav-active-indicator"></div>
|
||||
|
||||
232
client/src/views/GameInterface/tabs/ChatTab.jsx
Normal file
@ -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 (
|
||||
<div className={`chat-container ${isMobile ? "mobile" : ""}`}>
|
||||
<aside
|
||||
className={`chat-sidebar ${isMobile && !showSidebar ? "hidden" : ""}`}
|
||||
>
|
||||
<div className="search-section">
|
||||
<div className="card-tag">USER_SEARCH</div>
|
||||
<div className="search-input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="SEARCH_PILOTS..."
|
||||
value={searchQuery}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{searchResults.length > 0 && (
|
||||
<div className="search-results-dropdown">
|
||||
{searchResults.map((player) => (
|
||||
<div
|
||||
key={player.id}
|
||||
className="search-result-item"
|
||||
onClick={() => addFriend(player)}
|
||||
>
|
||||
<span>
|
||||
{player.username} (LVL {player.level})
|
||||
</span>
|
||||
<i className="fas fa-plus"></i>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="chats-list">
|
||||
<div
|
||||
className={`chat-item ${activeChat === "global" ? "active" : ""}`}
|
||||
onClick={() => selectChat("global")}
|
||||
>
|
||||
<i className="fas fa-globe"></i>
|
||||
<span>GLOBAL_CHANNEL</span>
|
||||
</div>
|
||||
|
||||
<div className="friends-section-label">
|
||||
<span className="label-text">CONTACTS</span>
|
||||
<span className="label-line"></span>
|
||||
</div>
|
||||
|
||||
<div className="friends-list">
|
||||
{friends.map((friend) => (
|
||||
<div
|
||||
key={friend.id}
|
||||
className={`chat-item ${activeChat === friend.id ? "active" : ""}`}
|
||||
onClick={() => selectChat(friend.id)}
|
||||
>
|
||||
<div
|
||||
className={`status-dot ${friend.online ? "online" : "offline"}`}
|
||||
></div>
|
||||
<span>{friend.username}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className={`chat-main ${isMobile && showSidebar ? "hidden" : ""}`}>
|
||||
<div className="chat-header">
|
||||
{isMobile && (
|
||||
<button className="back-btn" onClick={() => setShowSidebar(true)}>
|
||||
<i className="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
)}
|
||||
<div className="active-chat-info">
|
||||
<i
|
||||
className={
|
||||
activeChat === "global" ? "fas fa-globe" : "fas fa-user"
|
||||
}
|
||||
></i>
|
||||
<h3>{getChatName()}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="chat-messages">
|
||||
{messages.length === 0 && (
|
||||
<div className="message system">
|
||||
<span className="msg-text">
|
||||
NO_LOGS_FOUND. SECURE_LINE_READY...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{messages.map((msg, index) => (
|
||||
<div
|
||||
key={msg.id || index}
|
||||
className={`message ${msg.senderName === "System" ? "system" : ""}`}
|
||||
>
|
||||
<span className="msg-time">
|
||||
[
|
||||
{new Date(msg.createdAt).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
]
|
||||
</span>
|
||||
<span className="msg-author">{msg.senderName}:</span>
|
||||
<span className="msg-text">{msg.content}</span>
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
<div className="chat-input-area">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="TYPE_MESSAGE..."
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
|
||||
/>
|
||||
<button className="send-btn" onClick={sendMessage}>
|
||||
<i className="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatTab;
|
||||
@ -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 = () => {
|
||||
<div
|
||||
className="progress-bar-fill"
|
||||
style={{
|
||||
width: `${((activeCraft.totalTime - activeCraft.timeLeft) / activeCraft.totalTime) * 100}%`,
|
||||
width: `${Math.min(100, ((activeCraft.totalTime - activeCraft.timeLeft) / activeCraft.totalTime) * 100)}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
@ -122,6 +145,7 @@ const CraftingTab = () => {
|
||||
|
||||
<div className="crafting-grid">
|
||||
{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 (
|
||||
<div
|
||||
key={recipe.id}
|
||||
className={`recipe-card ${!canCraft ? "insufficient-resources" : ""}`}
|
||||
className={`recipe-card ${!canCraft && !isThisRecipeCrafting ? "insufficient-resources" : ""} ${isThisRecipeCrafting ? "crafting-active" : ""}`}
|
||||
onClick={() => setSelectedRecipe(recipe)}
|
||||
>
|
||||
<div className="recipe-icon">
|
||||
{recipe.texture ? (
|
||||
<img src={recipe.texture} alt={recipe.displayName} />
|
||||
<img
|
||||
width={64}
|
||||
src={`${config.serverUrl}/static/${recipe.texture}`}
|
||||
alt={recipe.displayName}
|
||||
/>
|
||||
) : (
|
||||
<div className="fallback-icon">
|
||||
{recipe.id[0].toUpperCase()}
|
||||
</div>
|
||||
<div className="fallback-icon">{recipe.displayName[0]}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="recipe-info-main">
|
||||
<span className="recipe-name">{recipe.displayName}</span>
|
||||
<div className="recipe-badges">
|
||||
<span className="badge-time">
|
||||
<i className="fas fa-clock"></i> {recipe.constructionTime}
|
||||
s
|
||||
<i className="fas fa-clock"></i> {recipe.time_seconds}s
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!canCraft && (
|
||||
<div className="lock-overlay">
|
||||
<i className="fas fa-lock"></i>
|
||||
{isThisRecipeCrafting && (
|
||||
<div className="craft-overlay-mini">
|
||||
<i className="fas fa-sync fa-spin"></i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{recipes.length === 0 && (
|
||||
<div className="empty-category">
|
||||
No blueprints available in this sector.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CraftModal
|
||||
recipe={selectedRecipe}
|
||||
onClose={() => setSelectedRecipe(null)}
|
||||
onClose={() => !activeCraft && setSelectedRecipe(null)}
|
||||
onStartCraft={handleStartCrafting}
|
||||
isBusy={!!activeCraft}
|
||||
activeCraft={activeCraft}
|
||||
getOwnedAmount={getOwnedAmount}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -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 = () => {
|
||||
<span
|
||||
className={`diag-status ${isConnected ? "online" : "offline"}`}
|
||||
>
|
||||
{isConnected ? "CONNECTED" : "DISCONNECTED"}
|
||||
{isConnected ? (
|
||||
<span className="online-info">
|
||||
<span className="online-dot"></span>
|
||||
LIVE: {onlineCount}
|
||||
</span>
|
||||
) : (
|
||||
"DISCONNECTED"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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 = () => (
|
||||
<div
|
||||
className={`item-inspector ${isMobile && selectedItem ? "mobile-modal" : ""}`}
|
||||
onClick={() => isMobile && setSelectedItem(null)}
|
||||
>
|
||||
{selectedItem && (
|
||||
<div className="inspector-content" onClick={(e) => e.stopPropagation()}>
|
||||
{isMobile && (
|
||||
<button
|
||||
className="close-inspector"
|
||||
onClick={() => setSelectedItem(null)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className={`inspector-header ${selectedItem.meta?.rarity}`}>
|
||||
<div className="header-visual">
|
||||
<img src={getFullTextureUrl(selectedItem.texture)} alt="" />
|
||||
</div>
|
||||
<div className="header-info">
|
||||
<div className="id-tag">{selectedItem.id}</div>
|
||||
<h1>{selectedItem.displayName}</h1>
|
||||
<span className="rarity-label">
|
||||
{selectedItem.meta?.rarity?.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="inspector-grid">
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">DATA_DESCRIPTION</div>
|
||||
<p>{selectedItem.description}</p>
|
||||
</div>
|
||||
|
||||
{selectedItem.stats &&
|
||||
Object.keys(selectedItem.stats).length > 0 && (
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">PARAMETER_READOUT</div>
|
||||
<div className="stat-pills">
|
||||
{Object.entries(selectedItem.stats).map(([k, v]) => (
|
||||
<div key={k} className="stat-pill">
|
||||
<span>{GameDataManager.getStatName(k)}</span>
|
||||
<span>+{v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="item-list-container">
|
||||
<div className="item-list-sidebar">
|
||||
<div className="search-box">
|
||||
<i className="fas fa-search"></i>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="FILTER_BY_DATABASE_ID..."
|
||||
placeholder="SEARCH_ID_OR_NAME..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="category-filters">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
@ -74,7 +131,6 @@ const ItemListTab = () => {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="items-list-scroll">
|
||||
{filteredItems.map((item) => (
|
||||
<div
|
||||
@ -89,80 +145,11 @@ const ItemListTab = () => {
|
||||
<div className="item-row-title">{item.displayName}</div>
|
||||
<div className="item-row-subtitle">{item.id}</div>
|
||||
</div>
|
||||
<div className="item-row-rarity-indicator"></div>
|
||||
</div>
|
||||
))}
|
||||
{filteredItems.length === 0 && (
|
||||
<div className="no-results">NO_RECORDS_FOUND</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="item-inspector">
|
||||
{selectedItem ? (
|
||||
<div className="inspector-content">
|
||||
<div className={`inspector-header ${selectedItem.meta?.rarity}`}>
|
||||
<div className="header-visual">
|
||||
<img src={getFullTextureUrl(selectedItem.texture)} alt="" />
|
||||
</div>
|
||||
<div className="header-info">
|
||||
<div className="id-tag">{selectedItem.id}</div>
|
||||
<h1>{selectedItem.displayName}</h1>
|
||||
<div className="rarity-label">
|
||||
{selectedItem.meta?.rarity?.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="inspector-grid">
|
||||
<div className="inspector-section main-desc">
|
||||
<div className="section-title">DATA_DESCRIPTION</div>
|
||||
<p>{selectedItem.description}</p>
|
||||
</div>
|
||||
|
||||
{selectedItem.stats &&
|
||||
Object.keys(selectedItem.stats).length > 0 && (
|
||||
<div className="inspector-section stats">
|
||||
<div className="section-title">PARAMETER_READOUT</div>
|
||||
<div className="stat-pills">
|
||||
{Object.entries(selectedItem.stats).map(([k, v]) => (
|
||||
<div key={k} className="stat-pill">
|
||||
<span className="p-label">
|
||||
{GameDataManager.getStatName(k)}
|
||||
</span>
|
||||
<span className="p-value">+{v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="inspector-section meta">
|
||||
<div className="section-title">OBJECT_METADATA</div>
|
||||
<div className="meta-list">
|
||||
<div className="meta-item">
|
||||
<span>CATEGORY</span>
|
||||
<span>{selectedItem.meta?.category || "general"}</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<span>EQUIP_SLOT</span>
|
||||
<span>{selectedItem.meta?.equipmentSlot || "none"}</span>
|
||||
</div>
|
||||
<div className="meta-item">
|
||||
<span>STACK_LIMIT</span>
|
||||
<span>{selectedItem.meta?.stackable ? "64" : "1"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="inspector-placeholder">
|
||||
<div className="scanner-line"></div>
|
||||
<p>AWAITING_OBJECT_SELECTION...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(!isMobile || (isMobile && selectedItem)) && renderInspector()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
108
client/src/views/GameInterface/tabs/NotificationTab.jsx
Normal file
@ -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 (
|
||||
<div className="notifications-container">
|
||||
<div className="notifications-header">
|
||||
<div className="card-tag">SYSTEM_ALERTS</div>
|
||||
<h2>NOTIFICATIONS</h2>
|
||||
</div>
|
||||
|
||||
<div className="notifications-list">
|
||||
{notifications.length === 0 && (
|
||||
<div className="no-notifications">
|
||||
<i className="fas fa-satellite-dish"></i>
|
||||
<span>NO_ACTIVE_ALERTS_FOUND</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notifications.map((n) => (
|
||||
<div key={n.id} className={`notify-card ${n.type}`}>
|
||||
<div className="notify-icon">
|
||||
<i className={getIcon(n.type)}></i>
|
||||
</div>
|
||||
<div className="notify-content">
|
||||
<div className="notify-title-row">
|
||||
<h4>{n.title}</h4>
|
||||
<span className="notify-time">
|
||||
{new Date(n.createdAt).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<p>{n.message}</p>
|
||||
</div>
|
||||
<div className="notify-actions">
|
||||
{n.type === "friend_request" && (
|
||||
<button
|
||||
className="action-btn accept"
|
||||
onClick={() => handleAction(n.id, "accept_friend", n.data)}
|
||||
>
|
||||
ACCEPT
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="action-btn dismiss"
|
||||
onClick={() => handleAction(n.id, "dismiss")}
|
||||
>
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationsTab;
|
||||
@ -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 = ({
|
||||
<div className="craft-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h3>
|
||||
<i className="fas fa-tools"></i> Construction: {displayName}
|
||||
<i className="fas fa-tools"></i> Construction: {recipe.displayName}
|
||||
</h3>
|
||||
<button className="close-x" onClick={onClose}>
|
||||
×
|
||||
@ -36,35 +34,32 @@ const CraftModal = ({
|
||||
<i className="fas fa-list-ul"></i> Required Resources
|
||||
</h4>
|
||||
<div className="res-grid">
|
||||
{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 (
|
||||
<div
|
||||
key={ing.itemId}
|
||||
className={`res-item ${hasEnough ? "enough" : "not-enough"}`}
|
||||
>
|
||||
<div className="res-main-info">
|
||||
<i
|
||||
className={`fas fa-cube ${hasEnough ? "icon-green" : "icon-red"}`}
|
||||
></i>
|
||||
<span className="res-name">
|
||||
{ing.name || ing.itemId.replace("_", " ")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="res-quantity-info">
|
||||
<span
|
||||
className={`owned-val ${hasEnough ? "val-green" : "val-red"}`}
|
||||
>
|
||||
{owned}
|
||||
</span>
|
||||
<span className="required-val"> / {ing.quantity}</span>
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
key={ing.itemId}
|
||||
className={`res-item ${hasEnough ? "enough" : "not-enough"}`}
|
||||
>
|
||||
<div className="res-main-info">
|
||||
<i
|
||||
className={`fas fa-cube ${hasEnough ? "icon-green" : "icon-red"}`}
|
||||
></i>
|
||||
<span className="res-name">{ing.displayName}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="res-quantity-info">
|
||||
<span
|
||||
className={`owned-val ${hasEnough ? "val-green" : "val-red"}`}
|
||||
>
|
||||
{owned}
|
||||
</span>
|
||||
<span className="required-val"> / {ing.quantity}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -76,32 +71,50 @@ const CraftModal = ({
|
||||
<div className="outcome-row">
|
||||
<span>Result:</span>
|
||||
<strong>
|
||||
{displayName} x{recipe.result?.quantity || 1}
|
||||
{recipe.displayName} x{outputQty}
|
||||
</strong>
|
||||
</div>
|
||||
<div className="outcome-row">
|
||||
<span>Time:</span>
|
||||
<strong>{craftTime}s</strong>
|
||||
<strong>{recipe.time_seconds}s</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button
|
||||
className={`btn-start-craft ${!canAfford || isBusy ? "disabled" : ""}`}
|
||||
onClick={() => canAfford && !isBusy && onStartCraft(recipe)}
|
||||
disabled={!canAfford || isBusy}
|
||||
>
|
||||
{isBusy
|
||||
? "System Busy..."
|
||||
: !canAfford
|
||||
? "Insufficient Resources"
|
||||
: `Start Construction (${craftTime}s)`}
|
||||
</button>
|
||||
<button className="btn-cancel" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
{activeCraft && activeCraft.recipeId === recipe.id ? (
|
||||
<div className="modal-progress-container">
|
||||
<div className="progress-text">
|
||||
Processing... {activeCraft.timeLeft}s
|
||||
</div>
|
||||
<div className="progress-bar-bg">
|
||||
<div
|
||||
className="progress-bar-fill"
|
||||
style={{
|
||||
width: `${((activeCraft.totalTime - activeCraft.timeLeft) / activeCraft.totalTime) * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className={`btn-start-craft ${!canAfford || isBusy ? "disabled" : ""}`}
|
||||
onClick={() => canAfford && !isBusy && onStartCraft(recipe)}
|
||||
disabled={!canAfford || isBusy}
|
||||
>
|
||||
{isBusy
|
||||
? "System Busy..."
|
||||
: !canAfford
|
||||
? "Insufficient Resources"
|
||||
: `Start Construction (${recipe.time_seconds}s)`}
|
||||
</button>
|
||||
<button className="btn-cancel" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
182
client/src/views/GameInterface/tabs/styles/ChatTab.css
Normal file
@ -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;
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
148
client/src/views/GameInterface/tabs/styles/NotificationsTab.css
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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": "Підземелля",
|
||||
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
@ -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": [
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
70
game-server/src/game/ChatManager.js
Normal file
@ -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();
|
||||
71
game-server/src/game/CraftManager.js
Normal file
@ -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();
|
||||
@ -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"));
|
||||
|
||||
34
game-server/src/game/NotificationManager.js
Normal file
@ -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();
|
||||
@ -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,
|
||||
|
||||
16
game-server/src/models/Friend.js
Normal file
@ -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;
|
||||
23
game-server/src/models/Message.js
Normal file
@ -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;
|
||||
29
game-server/src/models/Notification.js
Normal file
@ -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;
|
||||
27
game-server/src/models/associations.js
Normal file
@ -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;
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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.");
|
||||
|
||||
|
||||
63
game-server/src/sockets/handlers/chatHandler.js
Normal file
@ -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);
|
||||
});
|
||||
};
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
114
game-server/src/sockets/handlers/friendHandler.js
Normal file
@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
51
game-server/src/sockets/handlers/notificationHandler.js
Normal file
@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
19
package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
21
readme.md
Normal file
@ -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
|
||||
```
|
||||