diff --git a/api/src/models/GameServer.js b/api/src/models/GameServer.js index 3a2dcc1..719e4d2 100644 --- a/api/src/models/GameServer.js +++ b/api/src/models/GameServer.js @@ -8,5 +8,9 @@ const gameServerSchema = new mongoose.Schema({ description: { type: String, default: "Description..." }, isModded: { type: Boolean, default: false }, region: { type: String, default: "Null" }, + playersOnline: { + type: Number, + default: 0, + }, }); module.exports = mongoose.model("GameServer", gameServerSchema); diff --git a/api/src/models/User.js b/api/src/models/User.js index 060ee33..46c5327 100644 --- a/api/src/models/User.js +++ b/api/src/models/User.js @@ -22,10 +22,7 @@ const userSchema = new mongoose.Schema( required: [true, "Password is required"], minlength: [6, "Password must be at least 6 characters long"], }, - playersOnline: { - type: Number, - default: 0, - }, + role: { type: String, enum: ["user", "admin"], diff --git a/client/index.html b/client/index.html index 7c0c5fa..7b685b1 100644 --- a/client/index.html +++ b/client/index.html @@ -8,7 +8,7 @@ name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content" /> - client + Galaxy Strike Online { const [inputValue, setInputValue] = useState(""); const [showSidebar, setShowSidebar] = useState(true); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); + const [confirmUnfriend, setConfirmUnfriend] = useState(null); const messagesEndRef = useRef(null); @@ -28,7 +29,6 @@ const ChatTab = () => { useEffect(() => { if (!socket) return; - // Початкові запити socket.emit("friend:get_list"); if (activeChat === "global") { socket.emit("chat:get_global_history"); @@ -81,6 +81,14 @@ const ChatTab = () => { setSearchResults([]); }; + const removeFriend = () => { + if (confirmUnfriend) { + socket.emit("friend:remove", { friendId: confirmUnfriend.id }); + if (activeChat === confirmUnfriend.id) setActiveChat("global"); + setConfirmUnfriend(null); + } + }; + const sendMessage = () => { if (!inputValue.trim() || !socket) return; socket.emit("chat:send_message", { @@ -104,6 +112,29 @@ const ChatTab = () => { return (
+ {confirmUnfriend && ( +
+
+

TERMINATE_CONTACT

+

+ Are you sure you want to remove {confirmUnfriend.username} from + your contacts? +

+
+ + +
+
+
+ )} +
@@ -155,12 +188,25 @@ const ChatTab = () => {
selectChat(friend.id)} >
- {friend.username} + className="chat-item-main" + onClick={() => selectChat(friend.id)} + > +
+ {friend.username} +
+
))} diff --git a/client/src/views/GameInterface/tabs/NotificationTab.jsx b/client/src/views/GameInterface/tabs/NotificationTab.jsx index c29b5b1..891a5e0 100644 --- a/client/src/views/GameInterface/tabs/NotificationTab.jsx +++ b/client/src/views/GameInterface/tabs/NotificationTab.jsx @@ -12,7 +12,6 @@ const NotificationsTab = () => { socket.emit("notifications:get_all"); const handleNewNotify = (notify) => { - console.log(notify); setNotifications((prev) => [notify, ...prev]); }; @@ -31,9 +30,14 @@ const NotificationsTab = () => { const handleAction = (id, action, data) => { if (action === "accept_friend") { - socket.emit("friend:add", { friendId: data.fromId }); + socket.emit("friend:accept", { id, friendId: data.fromId }); + socket.emit("notification:read", { id }); + } else if (action === "dismiss") { + socket.emit("notification:dismiss", { id }); + } else { + socket.emit("notification:read", { id }); } - socket.emit("notification:read", { id }); + setNotifications((prev) => prev.filter((n) => n.id !== id)); }; @@ -45,6 +49,10 @@ const NotificationsTab = () => { return "fas fa-hammer"; case "system": return "fas fa-robot"; + case "item_received": + return "fas fa-box-open"; + case "inventory_clear": + return "fas fa-trash-alt"; default: return "fas fa-bell"; } @@ -66,7 +74,7 @@ const NotificationsTab = () => { )} {notifications.map((n) => ( -
+
diff --git a/client/src/views/GameInterface/tabs/styles/ChatTab.css b/client/src/views/GameInterface/tabs/styles/ChatTab.css index 702639a..cea5596 100644 --- a/client/src/views/GameInterface/tabs/styles/ChatTab.css +++ b/client/src/views/GameInterface/tabs/styles/ChatTab.css @@ -14,6 +14,7 @@ flex-direction: column; transition: all 0.3s ease; z-index: 2; + background: rgba(10, 15, 24, 0.6); } .chat-main { @@ -21,9 +22,303 @@ display: flex; flex-direction: column; background: rgba(0, 0, 0, 0.2); + position: relative; } -/* МОБІЛЬНА ВЕРСІЯ */ +/* SEARCH SECTION */ +.search-section { + padding: 20px 15px 15px; + border-bottom: 1px solid var(--border-color); + position: relative; + z-index: 10; +} + +.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; + font-family: "Geologica", sans-serif; +} + +.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.8); +} + +.search-result-item { + padding: 10px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + font-size: 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.search-result-item:hover { + background: rgba(0, 212, 255, 0.1); +} + +/* CHAT LIST & ITEMS */ +.chats-list { + flex: 1; + overflow-y: auto; +} + +.friends-section-label { + padding: 20px 15px 10px; + display: flex; + align-items: center; + gap: 10px; + opacity: 0.4; +} + +.label-text { + font-size: 10px; + letter-spacing: 2px; + font-weight: bold; +} + +.label-line { + flex: 1; + height: 1px; + background: var(--border-color); +} + +.chat-item { + padding: 12px 15px; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + border-bottom: 1px solid rgba(255, 255, 255, 0.02); + transition: all 0.2s; +} + +.chat-item:hover { + background: rgba(255, 255, 255, 0.03); +} + +.chat-item.active { + background: rgba(0, 212, 255, 0.1); + box-shadow: inset 4px 0 0 var(--primary-color); +} + +.chat-item-main { + display: flex; + align-items: center; + gap: 12px; + flex: 1; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.status-dot.online { + background: #00ff88; + box-shadow: 0 0 5px #00ff88; +} + +.status-dot.offline { + background: #444; +} + +.unfriend-btn { + background: transparent; + border: none; + color: rgba(255, 255, 255, 0.2); + padding: 8px; + cursor: pointer; + transition: all 0.2s; + opacity: 0; +} + +.chat-item:hover .unfriend-btn { + opacity: 1; +} + +.unfriend-btn:hover { + color: #ff3e3e; + text-shadow: 0 0 8px rgba(255, 62, 62, 0.4); +} + +/* CHAT MAIN AREA */ +.chat-header { + padding: 15px; + display: flex; + align-items: center; + border-bottom: 1px solid var(--border-color); + background: rgba(10, 15, 24, 0.8); +} + +.active-chat-info { + display: flex; + align-items: center; + gap: 10px; +} + +.active-chat-info i { + color: var(--primary-color); +} + +.chat-messages { + flex: 1; + padding: 15px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 8px; +} + +.message { + font-size: 13px; + line-height: 1.4; +} + +.msg-time { + color: rgba(255, 255, 255, 0.3); + margin-right: 8px; + font-family: monospace; +} + +.msg-author { + color: var(--primary-color); + font-weight: bold; + margin-right: 8px; +} + +.message.system .msg-author { + color: #ff3e3e; +} + +.message.system .msg-text { + color: #888; + font-style: italic; +} + +.chat-input-area { + padding: 15px; + background: #05080c; + display: flex; + gap: 10px; + border-top: 1px solid var(--border-color); +} + +.chat-input-area input { + flex: 1; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border-color); + color: #fff; + padding: 10px 15px; + outline: none; +} + +.send-btn { + background: transparent; + border: 1px solid var(--primary-color); + color: var(--primary-color); + padding: 0 20px; + cursor: pointer; + transition: all 0.2s; +} + +.send-btn:hover { + background: var(--primary-color); + color: #000; +} + +/* MODAL STYLES */ +.modal-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + backdrop-filter: blur(4px); +} + +.modal-content { + background: #0a0f18; + border: 1px solid #ff3e3e; + padding: 25px; + width: 90%; + max-width: 350px; + text-align: center; + box-shadow: 0 0 30px rgba(255, 62, 62, 0.15); +} + +.modal-content h3 { + color: #ff3e3e; + margin-bottom: 15px; + font-size: 14px; + letter-spacing: 2px; +} + +.modal-content p { + color: #aaa; + font-size: 13px; + margin-bottom: 25px; +} + +.modal-actions { + display: flex; + gap: 10px; +} + +.confirm-btn, +.cancel-btn { + flex: 1; + padding: 10px; + font-size: 11px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s; +} + +.confirm-btn { + background: rgba(255, 62, 62, 0.1); + border: 1px solid #ff3e3e; + color: #ff3e3e; +} + +.confirm-btn:hover { + background: #ff3e3e; + color: #fff; +} + +.cancel-btn { + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border-color); + color: #fff; +} + +/* MOBILE RESPONSIVENESS */ @media (max-width: 768px) { .chat-sidebar { width: 100%; @@ -57,41 +352,19 @@ } } -/* Стилі заголовків та вводу */ -.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; + padding: 12px 15px; display: flex; align-items: center; - gap: 12px; + justify-content: space-between; cursor: pointer; border-bottom: 1px solid rgba(255, 255, 255, 0.02); + transition: all 0.2s; +} + +.chat-item:hover { + background: rgba(255, 255, 255, 0.03); } .chat-item.active { @@ -99,84 +372,42 @@ box-shadow: inset 4px 0 0 var(--primary-color); } -.chat-header { - padding: 15px; +.chat-item-main { display: flex; align-items: center; - border-bottom: 1px solid var(--border-color); - background: rgba(10, 15, 24, 0.8); -} - -.chat-messages { + gap: 12px; 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 { +.unfriend-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; + border: none; + color: rgba(255, 255, 255, 0.3); + padding: 8px; cursor: pointer; - font-size: 12px; - color: #fff; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); + transition: all 0.2s; + opacity: 0; } -.search-result-item:hover { - background: rgba(0, 212, 255, 0.1); +@media (min-width: 769px) { + .chat-item:hover .unfriend-btn { + opacity: 1; + } } -.search-result-item i { - color: var(--primary-color); +@media (max-width: 768px) { + .unfriend-btn { + opacity: 1; + color: rgba(255, 255, 255, 0.5); + padding: 12px; + } + + .chat-item { + padding: 15px; + } } -.message.system .msg-author { +.unfriend-btn:hover { color: #ff3e3e; -} - -.message.system .msg-text { - color: #aaa; - font-style: italic; + text-shadow: 0 0 8px rgba(255, 62, 62, 0.4); } diff --git a/game-server/datapacks/original/assets/textures/materials/alloy/chronotanium.png b/game-server/datapacks/original/assets/textures/materials/alloy/chronotanium.png new file mode 100644 index 0000000..8948848 Binary files /dev/null and b/game-server/datapacks/original/assets/textures/materials/alloy/chronotanium.png differ diff --git a/game-server/datapacks/original/assets/textures/materials/alloy/titaniumweave.png b/game-server/datapacks/original/assets/textures/materials/alloy/titaniumweave.png new file mode 100644 index 0000000..710fc3f Binary files /dev/null and b/game-server/datapacks/original/assets/textures/materials/alloy/titaniumweave.png differ diff --git a/game-server/datapacks/original/assets/textures/materials/alloy/voidsteel.png b/game-server/datapacks/original/assets/textures/materials/alloy/voidsteel.png new file mode 100644 index 0000000..7b590b5 Binary files /dev/null and b/game-server/datapacks/original/assets/textures/materials/alloy/voidsteel.png differ diff --git a/game-server/datapacks/original/assets/textures/materials/crystal/dimentionalcrystal.gif b/game-server/datapacks/original/assets/textures/materials/crystal/dimentionalcrystal.gif new file mode 100644 index 0000000..9dfcbb5 Binary files /dev/null and b/game-server/datapacks/original/assets/textures/materials/crystal/dimentionalcrystal.gif differ diff --git a/game-server/datapacks/original/assets/textures/materials/crystal/fluxcrystal.png b/game-server/datapacks/original/assets/textures/materials/crystal/fluxcrystal.png new file mode 100644 index 0000000..4a4cf9a Binary files /dev/null and b/game-server/datapacks/original/assets/textures/materials/crystal/fluxcrystal.png differ diff --git a/game-server/datapacks/original/assets/textures/materials/crystal/voidcrystal.png b/game-server/datapacks/original/assets/textures/materials/crystal/voidcrystal.png new file mode 100644 index 0000000..274be5b Binary files /dev/null and b/game-server/datapacks/original/assets/textures/materials/crystal/voidcrystal.png differ diff --git a/game-server/datapacks/original/data/enemies/hostiles/pirates/black_mark_cruiser.json b/game-server/datapacks/original/data/enemies/hostiles/pirates/black_mark_cruiser.json index 20ea9f6..7b94bf4 100644 --- a/game-server/datapacks/original/data/enemies/hostiles/pirates/black_mark_cruiser.json +++ b/game-server/datapacks/original/data/enemies/hostiles/pirates/black_mark_cruiser.json @@ -2,12 +2,12 @@ "hostile": { "id": "original:pirates/black_mark_cruiser", "displayName": "Black Mark Heavy Cruiser", - "meta": { + "stats": { "health": 850, "defense": 5.0, "damage": 18.0, - "criticalChance": 0.15, - "attackRate": 0.6 + "critical.chance": 0.15, + "attack.rate": 0.6 } } } diff --git a/game-server/datapacks/original/data/enemies/hostiles/pirates/raider_frigate.json b/game-server/datapacks/original/data/enemies/hostiles/pirates/raider_frigate.json index 6f24464..63553bb 100644 --- a/game-server/datapacks/original/data/enemies/hostiles/pirates/raider_frigate.json +++ b/game-server/datapacks/original/data/enemies/hostiles/pirates/raider_frigate.json @@ -2,12 +2,12 @@ "hostile": { "id": "original:pirates/raider_frigate", "displayName": "Raider Frigate", - "meta": { + "stats": { "health": 110, "defense": 1.5, "damage": 6.5, - "criticalChance": 0.2, - "attackRate": 1.0 + "critical.chance": 0.2, + "attack.rate": 1.0 } } } diff --git a/game-server/datapacks/original/data/enemies/hostiles/pirates/scout_drone.json b/game-server/datapacks/original/data/enemies/hostiles/pirates/scout_drone.json index 4c51b7c..57c3c69 100644 --- a/game-server/datapacks/original/data/enemies/hostiles/pirates/scout_drone.json +++ b/game-server/datapacks/original/data/enemies/hostiles/pirates/scout_drone.json @@ -2,12 +2,12 @@ "hostile": { "id": "original:pirates/scout_drone", "displayName": "Pirate Scout Drone", - "meta": { + "stats": { "health": 25, "defense": 0.2, "damage": 1.8, - "criticalChance": 0.1, - "attackRate": 3.0 + "critical.chance": 0.1, + "attack.rate": 3.0 } } } diff --git a/game-server/datapacks/original/data/enemies/hostiles/tutorial/tutorial_boss_hostile.json b/game-server/datapacks/original/data/enemies/hostiles/tutorial/tutorial_boss_hostile.json index c659d33..1e76af4 100644 --- a/game-server/datapacks/original/data/enemies/hostiles/tutorial/tutorial_boss_hostile.json +++ b/game-server/datapacks/original/data/enemies/hostiles/tutorial/tutorial_boss_hostile.json @@ -2,12 +2,12 @@ "hostile": { "id": "original:tutorial/tutorial_boss_hostile", "displayName": "enemies.original.tutorial.tutorial_boss_hostile", - "meta": { + "stats": { "health": 90, "defense": 1.3, "damage": 4.1, - "criticalChance": 0.3, - "attackRate": 2 + "critical.chance": 0.3, + "attack.rate": 2 } } } diff --git a/game-server/datapacks/original/data/enemies/hostiles/tutorial/tutorial_hostile.json b/game-server/datapacks/original/data/enemies/hostiles/tutorial/tutorial_hostile.json index fc718a0..8b57d26 100644 --- a/game-server/datapacks/original/data/enemies/hostiles/tutorial/tutorial_hostile.json +++ b/game-server/datapacks/original/data/enemies/hostiles/tutorial/tutorial_hostile.json @@ -2,12 +2,12 @@ "hostile": { "id": "original:tutorial/tutorial_hostile", "displayName": "enemies.original.tutorial.tutorial_hostile", - "meta": { + "stats": { "health": 30, "defense": 0.3, "damage": 2.1, - "criticalChance": 0.3, - "attackRate": 2 + "critical,chance": 0.3, + "attack.rate": 2 } } } diff --git a/game-server/datapacks/original/data/items/materials/alloys/titanium_weave.json b/game-server/datapacks/original/data/items/materials/alloys/titanium_weave.json index cc88028..7584d8f 100644 --- a/game-server/datapacks/original/data/items/materials/alloys/titanium_weave.json +++ b/game-server/datapacks/original/data/items/materials/alloys/titanium_weave.json @@ -1,7 +1,7 @@ { "materials": { "id": "original:alloy_titanium_weave", - "texture": "original/assets/textures/materials/alloy/titanium_weave.png", + "texture": "original/assets/textures/materials/alloy/titaniumweave.png", "displayName": "items.materials.original.alloys.titanium_weave", "description": "items.materials.original.alloys.titanium_weave.desc", "meta": { diff --git a/game-server/datapacks/original/data/items/materials/alloys/void_steel.json b/game-server/datapacks/original/data/items/materials/alloys/void_steel.json index 5f56fd2..b277384 100644 --- a/game-server/datapacks/original/data/items/materials/alloys/void_steel.json +++ b/game-server/datapacks/original/data/items/materials/alloys/void_steel.json @@ -1,7 +1,7 @@ { "materials": { "id": "original:alloy_void_steel", - "texture": "original/assets/textures/materials/alloy/void_steel.png", + "texture": "original/assets/textures/materials/alloy/voidsteel.png", "displayName": "items.materials.original.alloys.void_steel", "description": "items.materials.original.alloys.void_steel.desc", "meta": { diff --git a/game-server/datapacks/original/data/items/materials/crystals/flux_core.json b/game-server/datapacks/original/data/items/materials/crystals/core_flux.json similarity index 100% rename from game-server/datapacks/original/data/items/materials/crystals/flux_core.json rename to game-server/datapacks/original/data/items/materials/crystals/core_flux.json diff --git a/game-server/datapacks/original/data/items/materials/crystals/void crystal.json b/game-server/datapacks/original/data/items/materials/crystals/crystal._void.json similarity index 76% rename from game-server/datapacks/original/data/items/materials/crystals/void crystal.json rename to game-server/datapacks/original/data/items/materials/crystals/crystal._void.json index 0c40fd8..3f96e78 100644 --- a/game-server/datapacks/original/data/items/materials/crystals/void crystal.json +++ b/game-server/datapacks/original/data/items/materials/crystals/crystal._void.json @@ -1,7 +1,7 @@ { "materials": { "id": "original:crystal_void", - "texture": "original/assets/textures/materials/ingot/crystal_void.png", + "texture": "original/assets/textures/materials/ingot/voidcrystal.png", "displayName": "items.materials.original.crystal.void", "description": "items.materials.original.crystal.void.desc", "meta": { diff --git a/game-server/datapacks/original/data/items/materials/crystals/dimentional_crystal.json b/game-server/datapacks/original/data/items/materials/crystals/crystal_dimentional.json similarity index 76% rename from game-server/datapacks/original/data/items/materials/crystals/dimentional_crystal.json rename to game-server/datapacks/original/data/items/materials/crystals/crystal_dimentional.json index eaf76d3..7a0833e 100644 --- a/game-server/datapacks/original/data/items/materials/crystals/dimentional_crystal.json +++ b/game-server/datapacks/original/data/items/materials/crystals/crystal_dimentional.json @@ -1,7 +1,7 @@ { "materials": { "id": "original:crystal_dimentional", - "texture": "original/assets/textures/materials/ingot/crystal_dimentional.png", + "texture": "original/assets/textures/materials/ingot/dimentionalcrystal.png", "displayName": "items.materials.original.crystal.dimentional", "description": "items.materials.original.crystal.dimentional.desc", "meta": { diff --git a/game-server/datapacks/original/data/items/materials/crystals/flux_crystal.json b/game-server/datapacks/original/data/items/materials/crystals/crystal_flux.json similarity index 76% rename from game-server/datapacks/original/data/items/materials/crystals/flux_crystal.json rename to game-server/datapacks/original/data/items/materials/crystals/crystal_flux.json index 13778c6..2b9a920 100644 --- a/game-server/datapacks/original/data/items/materials/crystals/flux_crystal.json +++ b/game-server/datapacks/original/data/items/materials/crystals/crystal_flux.json @@ -1,7 +1,7 @@ { "materials": { "id": "original:crystal_flux", - "texture": "original/assets/textures/materials/ingot/crystal_flux.png", + "texture": "original/assets/textures/materials/ingot/fluxcrystal.png", "displayName": "items.materials.original.crystal.flux", "description": "items.materials.original.crystal.flux.desc", "meta": { diff --git a/game-server/src/game/AdminManager.js b/game-server/src/game/AdminManager.js new file mode 100644 index 0000000..452112d --- /dev/null +++ b/game-server/src/game/AdminManager.js @@ -0,0 +1,91 @@ +const { Player, Inventory } = require("../models"); +const DatapackLoader = require("../game/DatapackLoader"); +const notificationManager = require("./NotificationManager"); + +class AdminManager { + constructor() { + this.io = null; + } + + init(io) { + this.io = io; + } + + async giveItem(targetName, itemId, amount) { + const targetPlayer = await Player.findOne({ + where: { username: targetName }, + }); + if (!targetPlayer) throw new Error(`Player '${targetName}' not found.`); + + const itemData = DatapackLoader.getItem(itemId); + if (!itemData) throw new Error(`Item ID '${itemId}' does not exist.`); + + const [inventoryItem, created] = await Inventory.findOrCreate({ + where: { playerId: targetPlayer.id, itemId: itemId }, + defaults: { quantity: amount }, + }); + + if (!created) { + await inventoryItem.increment("quantity", { by: amount }); + } + + await notificationManager.send({ + playerId: targetPlayer.id, + type: "item_received", + title: "Items Received", + message: `Admin granted you ${amount}x ${itemData.name || itemId}.`, + data: { itemId, amount }, + priority: "normal", + }); + + this._updatePlayerInventory(targetPlayer.id); + return { targetName, itemId, amount }; + } + + async clearInventory(targetName) { + const targetPlayer = await Player.findOne({ + where: { username: targetName }, + }); + if (!targetPlayer) throw new Error(`Player '${targetName}' not found.`); + + await Inventory.destroy({ where: { playerId: targetPlayer.id } }); + + await notificationManager.send({ + playerId: targetPlayer.id, + type: "inventory_clear", + title: "Inventory Wiped", + message: "Your inventory has been cleared by an administrator.", + priority: "high", + }); + + this._updatePlayerInventory(targetPlayer.id, true); + return targetName; + } + + async reloadData() { + DatapackLoader.loadAll(); + if (this.io) { + this.io.emit("admin:log", "System: Datapacks reloaded by admin."); + } + } + + async _updatePlayerInventory(playerId, isEmpty = false) { + if (!this.io) return; + + const targetSocket = [...this.io.sockets.sockets.values()].find( + (s) => s.user?.id === playerId, + ); + + if (targetSocket) { + const items = isEmpty + ? [] + : await Inventory.findAll({ + where: { playerId }, + attributes: ["itemId", "quantity"], + }); + targetSocket.emit("player:inventory_data", items); + } + } +} + +module.exports = new AdminManager(); diff --git a/game-server/src/game/NotificationManager.js b/game-server/src/game/NotificationManager.js index 62e3da4..9a97e09 100644 --- a/game-server/src/game/NotificationManager.js +++ b/game-server/src/game/NotificationManager.js @@ -1,33 +1,93 @@ const Notification = require("../models/Notification"); class NotificationManager { - async createNotification({ playerId, type, title, message, data = {} }) { + constructor() { + this.io = null; + } + + init(io) { + this.io = io; + console.log("[NotificationManager] Initialized with Socket.io"); + } + + async send({ + playerId, + type = "info", + title, + message, + data = {}, + priority = "normal", + }) { try { - return await Notification.create({ + const notification = await Notification.create({ playerId, type, title, message, data, + priority, + isRead: false, }); + + const targetSocket = this._getSocketByPlayerId(playerId); + + if (targetSocket) { + targetSocket.emit("notification:new", notification); + + const unreadCount = await this.getUnreadCount(playerId); + targetSocket.emit("notifications:unread_count", unreadCount); + } + + return notification; } catch (error) { - console.error("Notify Error:", error); + console.error( + `[NotificationManager] Error sending to ${playerId}:`, + error, + ); } } - async getPlayerNotifications(playerId) { + _getSocketByPlayerId(playerId) { + if (!this.io) return null; + return [...this.io.sockets.sockets.values()].find( + (s) => s.user?.id === playerId, + ); + } + async getPlayerHistory(playerId, limit = 50) { return await Notification.findAll({ where: { playerId }, order: [["createdAt", "DESC"]], - limit: 20, + limit, }); } - async markAsRead(notificationId) { - return await Notification.update( + async getUnreadCount(playerId) { + return await Notification.count({ + where: { playerId, isRead: false }, + }); + } + + async markAsRead(notificationId, playerId) { + await Notification.update( { isRead: true }, - { where: { id: notificationId } }, + { where: { id: notificationId, playerId } }, ); + return await this.getUnreadCount(playerId); + } + + async markAllAsRead(playerId) { + await Notification.update( + { isRead: true }, + { where: { playerId, isRead: false } }, + ); + return 0; + } + + async delete(notificationId, playerId) { + await Notification.destroy({ + where: { id: notificationId, playerId }, + }); + return await this.getUnreadCount(playerId); } } diff --git a/game-server/src/game/SocialManager.js b/game-server/src/game/SocialManager.js new file mode 100644 index 0000000..a5e718a --- /dev/null +++ b/game-server/src/game/SocialManager.js @@ -0,0 +1,95 @@ +const { Player, Friend } = require("../models"); +const notificationManager = require("./NotificationManager"); +const { Op } = require("sequelize"); + +class SocialManager { + constructor() { + this.io = null; + } + + init(io) { + this.io = io; + } + + async searchPlayers(query, excludeId) { + return await Player.findAll({ + where: { + username: { [Op.like]: `%${query}%` }, + id: { [Op.ne]: excludeId }, + }, + limit: 5, + attributes: ["id", "username", "level"], + }); + } + async removeFriend(myId, friendId) { + await Friend.destroy({ + where: { + [Op.or]: [ + { playerId: myId, friendId: friendId }, + { playerId: friendId, friendId: myId }, + ], + }, + }); + + await this.broadcastFriendListUpdate(myId); + await this.broadcastFriendListUpdate(friendId); + } + async sendFriendRequest(sender, targetId) { + await notificationManager.send({ + playerId: targetId, + type: "friend_request", + title: "NEW FRIEND REQUEST", + message: `${sender.username} wants to add you as a friend.`, + data: { fromId: sender.id }, + priority: "normal", + }); + } + + async acceptFriendRequest(myId, friendId, notificationId) { + const exists = await Friend.findOne({ + where: { playerId: myId, friendId: friendId }, + }); + + if (!exists) { + await Friend.bulkCreate([ + { playerId: myId, friendId: friendId }, + { playerId: friendId, friendId: myId }, + ]); + await notificationManager.delete(notificationId, myId); + + await this.broadcastFriendListUpdate(myId); + await this.broadcastFriendListUpdate(friendId); + + return true; + } + return false; + } + + async getFriendList(playerId) { + const player = await Player.findByPk(playerId, { + include: [ + { + model: Player, + as: "Friends", + attributes: ["id", "username", "level"], + }, + ], + }); + return player?.Friends || []; + } + + async broadcastFriendListUpdate(playerId) { + if (!this.io) return; + + const list = await this.getFriendList(playerId); + const targetSocket = [...this.io.sockets.sockets.values()].find( + (s) => s.user?.id === playerId, + ); + + if (targetSocket) { + targetSocket.emit("friend:list", list); + } + } +} + +module.exports = new SocialManager(); diff --git a/game-server/src/index.js b/game-server/src/index.js index 36759da..da54b17 100644 --- a/game-server/src/index.js +++ b/game-server/src/index.js @@ -10,6 +10,9 @@ const DatapackLoader = require("./game/DatapackLoader.js"); const path = require("path"); const app = express(); const economyService = require("./game/EconomyService.js"); +const NotificationManager = require("./game/NotificationManager.js"); +const AdminManager = require("./game/AdminManager.js"); +const SocialManager = require("./game/SocialManager.js"); app.use( cors({ @@ -80,7 +83,10 @@ server.listen(config.port, async () => { DatapackLoader.init(datapacksPath, io); await sequelize.initDatabase(); initSockets(io); + NotificationManager.init(io); + AdminManager.init(io); economyService.init(io); + SocialManager.init(io); await registerInApi(); setInterval(sendHeartbeat, HEARTBEAT_INTERVAL); console.log(`Server running on ${config.host}. PORT: ${config.port}`); diff --git a/game-server/src/models/index.js b/game-server/src/models/index.js index cc0076f..e733245 100644 --- a/game-server/src/models/index.js +++ b/game-server/src/models/index.js @@ -3,6 +3,7 @@ const Player = require("./Player"); const Inventory = require("./Inventory"); const setupAssociations = require("./associations"); const Notification = require("./Notification"); +const Friend = require("./Friend.js"); Player.hasMany(Inventory, { foreignKey: "playerId", as: "inventory" }); Inventory.belongsTo(Player, { foreignKey: "playerId" }); @@ -13,4 +14,5 @@ module.exports = { Player, Inventory, Notification, + Friend, }; diff --git a/game-server/src/sockets/handlers/adminHandler.js b/game-server/src/sockets/handlers/adminHandler.js index 716199a..9ab1fbe 100644 --- a/game-server/src/sockets/handlers/adminHandler.js +++ b/game-server/src/sockets/handlers/adminHandler.js @@ -1,7 +1,8 @@ -const { Player, Inventory } = require("../../models"); -const DatapackLoader = require("../../game/DatapackLoader"); +const adminManager = require("../../game/AdminManager"); module.exports = (io, socket) => { + if (!adminManager.io) adminManager.init(io); + const handleAdminCommand = async ({ command }) => { const args = command.trim().split(/\s+/); const cmd = args[0].toLowerCase(); @@ -13,76 +14,33 @@ module.exports = (io, socket) => { const amount = parseInt(amountStr) || 1; if (!targetName || !itemId) { - throw new Error("Usage: /give [player_name] [item_id] [amount]"); - } - - const targetPlayer = await Player.findOne({ - where: { username: targetName }, - }); - if (!targetPlayer) - throw new Error(`Player '${targetName}' not found in database.`); - - const itemData = DatapackLoader.getItem(itemId); - if (!itemData) - throw new Error(`Item ID '${itemId}' does not exist in datapacks.`); - - const [inventoryItem, created] = await Inventory.findOrCreate({ - where: { playerId: targetPlayer.id, itemId: itemId }, - defaults: { quantity: amount }, - }); - - if (!created) { - await inventoryItem.increment("quantity", { by: amount }); + throw new Error("Usage: /give [player] [item] [amount]"); } + const result = await adminManager.giveItem( + targetName, + itemId, + amount, + ); socket.emit( "admin:log", - `Successfully gave ${amount}x [${itemId}] to ${targetName}.`, + `Successfully gave ${result.amount}x [${result.itemId}] to ${result.targetName}.`, ); - - const targetSocket = [...io.sockets.sockets.values()].find( - (s) => s.user?.id === targetPlayer.id, - ); - - if (targetSocket) { - const updatedItems = await Inventory.findAll({ - where: { playerId: targetPlayer.id }, - attributes: ["itemId", "quantity"], - }); - targetSocket.emit("player:inventory_data", updatedItems); - targetSocket.emit( - "admin:log", - `Admin gave you ${amount}x ${itemId}`, - ); - } break; } case "/clear": { const [_, targetName] = args; - const targetPlayer = await Player.findOne({ - where: { username: targetName }, - }); - if (!targetPlayer) throw new Error("Player not found."); + if (!targetName) throw new Error("Usage: /clear [player]"); - await Inventory.destroy({ where: { playerId: targetPlayer.id } }); - - socket.emit( - "admin:log", - `Inventory for ${targetName} has been wiped.`, - ); - - const targetSocket = [...io.sockets.sockets.values()].find( - (s) => s.user?.id === targetPlayer.id, - ); - if (targetSocket) targetSocket.emit("player:inventory_data", []); + const target = await adminManager.clearInventory(targetName); + socket.emit("admin:log", `Inventory for ${target} has been wiped.`); break; } case "/reload_data": { socket.emit("admin:log", "Reloading all datapacks..."); - DatapackLoader.loadAll(); - io.emit("admin:log", "System: Datapacks reloaded by admin."); + await adminManager.reloadData(); break; } @@ -90,7 +48,6 @@ module.exports = (io, socket) => { socket.emit("admin:log", `Unknown admin command: ${cmd}`); } } catch (err) { - console.error("Admin Command Error:", err.message); socket.emit("admin:log", `Error: ${err.message}`); } }; diff --git a/game-server/src/sockets/handlers/chatHandler.js b/game-server/src/sockets/handlers/chatHandler.js index d9cb627..1cdc2b4 100644 --- a/game-server/src/sockets/handlers/chatHandler.js +++ b/game-server/src/sockets/handlers/chatHandler.js @@ -42,6 +42,7 @@ module.exports = (io, socket) => { if (messageData.type === "global") { io.emit("chat:new_message", messageData); } else { + console.log(payload.receiverId); socket .to(`user_${payload.receiverId}`) .emit("chat:new_message", messageData); diff --git a/game-server/src/sockets/handlers/friendHandler.js b/game-server/src/sockets/handlers/friendHandler.js deleted file mode 100644 index 6065315..0000000 --- a/game-server/src/sockets/handlers/friendHandler.js +++ /dev/null @@ -1,114 +0,0 @@ -const Player = require("../../models/Player"); -const Friend = require("../../models/Friend"); -const NotificationManager = require("../../game/NotificationManager"); -const Notification = require("../../models/Notification"); - -module.exports = (io, socket) => { - socket.on("player:search", async ({ query }) => { - try { - const players = await Player.findAll({ - where: { - username: { [require("sequelize").Op.like]: `%${query}%` }, - id: { [require("sequelize").Op.ne]: socket.user.id }, - }, - limit: 5, - attributes: ["id", "username", "level"], - }); - socket.emit("player:search_results", players); - } catch (e) { - console.error(e); - } - }); - - socket.on("friend:add", async ({ friendId }) => { - try { - const player = socket.user; - - await NotificationManager.createNotification({ - playerId: friendId, - type: "friend_request", - title: "NEW FRIEND REQUEST", - message: `${player.username} wants to add you as a friend.`, - data: { fromId: player.id }, - }); - - io.to(friendId).emit("notification:new", { - type: "friend_request", - title: "NEW FRIEND REQUEST", - message: `${player.username} wants to add you as a friend.`, - data: { fromId: player.id }, - createdAt: new Date(), - }); - } catch (e) { - socket.emit("error", { message: "FAILED_TO_SEND_REQUEST" }); - } - }); - - socket.on("friend:accept", async ({ friendId, notificationId }) => { - try { - const myId = socket.user.id; - - const exists = await Friend.findOne({ - where: { playerId: myId, friendId: friendId }, - }); - - if (!exists) { - await Friend.bulkCreate([ - { playerId: myId, friendId: friendId }, - { playerId: friendId, friendId: myId }, - ]); - - await Notification.destroy({ - where: { id: notificationId, playerId: myId }, - }); - - const myUpdated = await Player.findByPk(myId, { - include: [ - { - model: Player, - as: "Friends", - attributes: ["id", "username", "level"], - }, - ], - }); - socket.emit("friend:list", myUpdated.Friends || []); - - const friendUpdated = await Player.findByPk(friendId, { - include: [ - { - model: Player, - as: "Friends", - attributes: ["id", "username", "level"], - }, - ], - }); - io.to(friendId).emit("friend:list", friendUpdated.Friends || []); - - const unreadCount = await Notification.count({ - where: { playerId: myId, isRead: false }, - }); - socket.emit("notifications:unread_count", unreadCount); - } - } catch (e) { - console.error(e); - socket.emit("error", { message: "FAILED_TO_ACCEPT_FRIEND" }); - } - }); - - socket.on("friend:get_list", async () => { - try { - const player = await Player.findByPk(socket.user.id, { - include: [ - { - model: Player, - as: "Friends", - attributes: ["id", "username", "level"], - }, - ], - }); - socket.emit("friend:list", player.Friends || []); - } catch (e) { - console.error(e); - } - }); -}; diff --git a/game-server/src/sockets/handlers/notificationHandler.js b/game-server/src/sockets/handlers/notificationHandler.js index 0f0e1e0..d1d190f 100644 --- a/game-server/src/sockets/handlers/notificationHandler.js +++ b/game-server/src/sockets/handlers/notificationHandler.js @@ -37,11 +37,11 @@ module.exports = (io, socket) => { socket.on("notification:dismiss", async ({ id }) => { try { await Notification.destroy({ - where: { id, playerId: socket.player.id }, + where: { id, playerId: socket.user.id }, }); const unreadCount = await Notification.count({ - where: { playerId: socket.player.id, isRead: false }, + where: { playerId: socket.user.id, isRead: false }, }); socket.emit("notifications:unread_count", unreadCount); } catch (e) { diff --git a/game-server/src/sockets/handlers/socialHandler.js b/game-server/src/sockets/handlers/socialHandler.js new file mode 100644 index 0000000..f0eca54 --- /dev/null +++ b/game-server/src/sockets/handlers/socialHandler.js @@ -0,0 +1,48 @@ +const socialManager = require("../../game/SocialManager"); + +module.exports = (io, socket) => { + if (!socialManager.io) socialManager.init(io); + + socket.on("player:search", async ({ query }) => { + try { + const players = await socialManager.searchPlayers(query, socket.user.id); + socket.emit("player:search_results", players); + } catch (e) { + console.error(e); + } + }); + + socket.on("friend:add", async ({ friendId }) => { + try { + await socialManager.sendFriendRequest(socket.user, friendId); + } catch (e) { + socket.emit("error", { message: "FAILED_TO_SEND_REQUEST" }); + } + }); + + socket.on("friend:remove", async ({ friendId }) => { + try { + await socialManager.removeFriend(socket.user.id, friendId); + } catch (e) { + console.error(e); + socket.emit("error", { message: "FAILED_TO_REMOVE_FRIEND" }); + } + }); + socket.on("friend:accept", async ({ friendId, id }) => { + try { + await socialManager.acceptFriendRequest(socket.user.id, friendId, id); + } catch (e) { + console.error(e); + socket.emit("error", { message: "FAILED_TO_ACCEPT_FRIEND" }); + } + }); + + socket.on("friend:get_list", async () => { + try { + const list = await socialManager.getFriendList(socket.user.id); + socket.emit("friend:list", list); + } catch (e) { + console.error(e); + } + }); +}; diff --git a/game-server/src/sockets/socket.js b/game-server/src/sockets/socket.js index 1f2435a..43494bf 100644 --- a/game-server/src/sockets/socket.js +++ b/game-server/src/sockets/socket.js @@ -5,8 +5,8 @@ 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 socialHandler = require("./handlers/socialHandler"); const initSockets = (io) => { io.use(socketAuth); @@ -18,7 +18,7 @@ const initSockets = (io) => { adminHandler(io, socket); dungeonHandler(io, socket); chatHandler(io, socket); - friendHandler(io, socket); + socialHandler(io, socket); notificationHandler(io, socket); }); };