This commit is contained in:
cowcannon 2026-04-02 18:00:19 -05:00
commit 62944d041c
34 changed files with 748 additions and 316 deletions

View File

@ -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);

View File

@ -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"],

View File

@ -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"
/>
<title>client</title>
<title>Galaxy Strike Online</title>
<link
href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Space+Mono:wght@400;700&display=swap"
rel="stylesheet"

View File

@ -12,6 +12,7 @@ const ChatTab = () => {
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 (
<div className={`chat-container ${isMobile ? "mobile" : ""}`}>
{confirmUnfriend && (
<div className="modal-overlay">
<div className="modal-content">
<h3>TERMINATE_CONTACT</h3>
<p>
Are you sure you want to remove {confirmUnfriend.username} from
your contacts?
</p>
<div className="modal-actions">
<button className="confirm-btn" onClick={removeFriend}>
CONFIRM
</button>
<button
className="cancel-btn"
onClick={() => setConfirmUnfriend(null)}
>
CANCEL
</button>
</div>
</div>
</div>
)}
<aside
className={`chat-sidebar ${isMobile && !showSidebar ? "hidden" : ""}`}
>
@ -141,8 +172,10 @@ const ChatTab = () => {
className={`chat-item ${activeChat === "global" ? "active" : ""}`}
onClick={() => selectChat("global")}
>
<i className="fas fa-globe"></i>
<span>GLOBAL_CHANNEL</span>
<div className="chat-item-main">
<i className="fas fa-globe"></i>
<span>GLOBAL_CHANNEL</span>
</div>
</div>
<div className="friends-section-label">
@ -155,12 +188,25 @@ const ChatTab = () => {
<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>
className="chat-item-main"
onClick={() => selectChat(friend.id)}
>
<div
className={`status-dot ${friend.online ? "online" : "offline"}`}
></div>
<span>{friend.username}</span>
</div>
<button
className="unfriend-btn"
onClick={(e) => {
e.stopPropagation();
setConfirmUnfriend(friend);
}}
>
<i className="fas fa-user-minus"></i>
</button>
</div>
))}
</div>

View File

@ -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) => (
<div key={n.id} className={`notify-card ${n.type}`}>
<div key={n.id} className={`notify-card ${n.type} ${n.priority}`}>
<div className="notify-icon">
<i className={getIcon(n.type)}></i>
</div>

View File

@ -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);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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}`);

View File

@ -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,
};

View File

@ -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}`);
}
};

View File

@ -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);

View File

@ -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);
}
});
};

View File

@ -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) {

View File

@ -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);
}
});
};

View File

@ -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);
});
};