This repository has been archived on 2026-05-04. You can view files and clone it, but cannot push or open issues or pull requests.
Galaxy-Strike-Online/GameServer/systems/SocialSystem.js
2026-03-11 00:32:45 -03:00

159 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Galaxy Strike Online — Social System (GDD §17.2)
* Friends list, online status, sector tracking, gift system (≤5k credits/day)
*/
const mongoose = require('mongoose');
const socialSchema = new mongoose.Schema({
userId: { type: String, required: true, unique: true },
username: { type: String, default: '' },
friends: [{ userId: String, username: String, addedAt: Date }],
friendRequests: [{ // pending incoming
fromId: String, fromName: String, sentAt: { type: Date, default: Date.now }
}],
giftsGivenToday: { type: Number, default: 0 },
lastGiftReset: { type: Date, default: Date.now },
// Combat log entries (GDD §9.5) — last 50
combatLog: [{ type: mongoose.Schema.Types.Mixed }],
// Notification feed — last 50 in-app activity events
notificationFeed: [{ type: mongoose.Schema.Types.Mixed }],
unreadNotifications: { type: Number, default: 0 },
}, { timestamps: true });
const SocialData = mongoose.model('SocialData', socialSchema);
const DAILY_GIFT_LIMIT = 5000;
class SocialSystem {
constructor() { this.SocialData = SocialData; }
async getOrCreate(userId, username) {
let doc = await SocialData.findOne({ userId });
if (!doc) { doc = new SocialData({ userId, username }); await doc.save(); }
return doc;
}
async sendFriendRequest(senderId, senderName, targetUsername, onlinePlayers) {
// Find target in online players first, then DB
let targetId = null, targetName = null;
for (const [, cd] of onlinePlayers) {
if (cd.username?.toLowerCase() === targetUsername.toLowerCase()) {
targetId = cd.userId; targetName = cd.username; break;
}
}
if (!targetId) {
const pd = await SocialData.findOne({ username: new RegExp('^' + targetUsername + '$', 'i') });
if (pd) { targetId = pd.userId; targetName = pd.username; }
}
if (!targetId) throw new Error(`Player "${targetUsername}" not found`);
if (targetId === senderId) throw new Error('Cannot add yourself');
const targetDoc = await this.getOrCreate(targetId, targetName);
if (targetDoc.friends.some(f => f.userId === senderId)) throw new Error('Already friends');
if (targetDoc.friendRequests.some(r => r.fromId === senderId)) throw new Error('Request already sent');
targetDoc.friendRequests.push({ fromId: senderId, fromName: senderName });
await targetDoc.save();
return { targetId, targetName };
}
async acceptFriendRequest(userId, username, fromId, fromName) {
const myDoc = await this.getOrCreate(userId, username);
const theirDoc = await this.getOrCreate(fromId, fromName);
const idx = myDoc.friendRequests.findIndex(r => r.fromId === fromId);
if (idx < 0) throw new Error('No pending request from that player');
myDoc.friendRequests.splice(idx, 1);
if (!myDoc.friends.some(f => f.userId === fromId)) myDoc.friends.push({ userId: fromId, username: fromName, addedAt: new Date() });
if (!theirDoc.friends.some(f => f.userId === userId)) theirDoc.friends.push({ userId, username, addedAt: new Date() });
await myDoc.save(); await theirDoc.save();
}
async removeFriend(userId, friendId) {
await SocialData.updateOne({ userId }, { $pull: { friends: { userId: friendId } } });
await SocialData.updateOne({ userId: friendId }, { $pull: { friends: { userId } } });
}
async sendGift(senderData, targetId, amount) {
if (amount > DAILY_GIFT_LIMIT || amount <= 0) throw new Error(`Gift must be 1${DAILY_GIFT_LIMIT} credits`);
const myDoc = await this.getOrCreate(senderData.userId, senderData.username);
// Reset daily counter if needed
const today = new Date(); today.setHours(0,0,0,0);
if (myDoc.lastGiftReset < today) { myDoc.giftsGivenToday = 0; myDoc.lastGiftReset = today; }
if (myDoc.giftsGivenToday + amount > DAILY_GIFT_LIMIT) throw new Error(`Daily gift limit is ${DAILY_GIFT_LIMIT} credits`);
if ((senderData.stats?.credits || 0) < amount) throw new Error('Insufficient credits');
senderData.stats.credits -= amount;
myDoc.giftsGivenToday += amount;
await myDoc.save();
return { targetId, amount };
}
async getFriendsList(userId, username, onlinePlayers) {
const doc = await this.getOrCreate(userId, username);
const onlineIds = new Set();
for (const [, cd] of onlinePlayers) if (cd.userId) onlineIds.add(cd.userId);
return {
friends: doc.friends.map(f => ({ ...f.toObject?.() || f, online: onlineIds.has(f.userId) })),
requests: doc.friendRequests,
};
}
async addCombatLogEntry(userId, entry) {
await SocialData.findOneAndUpdate(
{ userId },
{ $push: { combatLog: { $each: [entry], $slice: -50 } } },
{ upsert: true }
);
}
async getCombatLog(userId) {
const doc = await SocialData.findOne({ userId });
return (doc?.combatLog || []).reverse();
}
// ── Notification Feed ──────────────────────────────────────────────────────
/**
* Push an event into a player's notification feed.
* type: 'pvp_win'|'pvp_loss'|'friend_request'|'gift_received'|'level_up'|
* 'quest_complete'|'dungeon_complete'|'raid_reward'|'alliance_event'|'system'
*/
async pushNotification(userId, { type, title, body, meta = {} }) {
const entry = { type, title, body, meta, read: false, timestamp: Date.now() };
await SocialData.findOneAndUpdate(
{ userId },
{
$push: { notificationFeed: { $each: [entry], $slice: -50 } },
$inc: { unreadNotifications: 1 },
},
{ upsert: true }
);
}
async markNotificationsRead(userId) {
await SocialData.updateOne(
{ userId },
{
$set: { 'notificationFeed.$[].read': true },
$set2: { unreadNotifications: 0 },
}
).catch(async () => {
// Fallback: set all read manually
const doc = await SocialData.findOne({ userId });
if (doc) {
doc.notificationFeed = (doc.notificationFeed || []).map(n => ({ ...n, read: true }));
doc.unreadNotifications = 0;
await doc.save();
}
});
}
async getUnreadCount(userId) {
const doc = await SocialData.findOne({ userId });
return doc?.unreadNotifications || 0;
}
}
module.exports = SocialSystem;