/** * 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 }], }, { 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(); } } module.exports = SocialSystem;