159 lines
6.2 KiB
JavaScript
159 lines
6.2 KiB
JavaScript
/**
|
||
* 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;
|