/** * Galaxy Strike Online — Alliance System (GDD §12) * - Alliance creation (Quantum Relay level 1, 10,000 credits founding fee) * - Ranks: Founder, Officer, Veteran, Member * - Alliance Warehouse: shared resource pool * - Alliance Research stubs (phase 2) */ const mongoose = require('mongoose'); const allianceSchema = new mongoose.Schema({ allianceId: { type: String, required: true, unique: true }, name: { type: String, required: true, unique: true }, tag: { type: String, required: true, unique: true, maxlength: 4 }, founderId: { type: String, required: true }, founderName: { type: String, default: '' }, description: { type: String, default: '' }, createdAt: { type: Date, default: Date.now }, members: [{ userId: String, username: String, rank: { type: String, enum: ['founder','officer','veteran','member'], default: 'member' }, joinedAt: { type: Date, default: Date.now }, }], // GDD §12.3 Alliance Warehouse warehouse: { metal: { type: Number, default: 0 }, gas: { type: Number, default: 0 }, crystal: { type: Number, default: 0 }, energyCells: { type: Number, default: 0 }, credits: { type: Number, default: 0 }, maxPerResource: { type: Number, default: 50000 }, }, warehouseLog: [{ type: mongoose.Schema.Types.Mixed }], // Alliance research (stubbed for phase 2) research: { type: mongoose.Schema.Types.Mixed, default: {} }, maxMembers: { type: Number, default: 50 }, isRecruiting: { type: Boolean, default: true }, }, { timestamps: true }); const Alliance = mongoose.model('Alliance', allianceSchema); const RANKS = ['founder', 'officer', 'veteran', 'member']; const RANK_PERMS = { founder: ['kick', 'promote', 'demote', 'deposit', 'withdraw', 'edit', 'disband'], officer: ['kick_member', 'promote_veteran', 'deposit', 'withdraw'], veteran: ['deposit', 'withdraw_limited'], member: ['deposit'], }; class AllianceSystem { constructor() { this.Alliance = Alliance; } async createAlliance(playerData, { name, tag, description = '' }) { if (!name || name.length < 3 || name.length > 24) throw new Error('Alliance name must be 3–24 characters'); if (!tag || tag.length < 2 || tag.length > 4) throw new Error('Alliance tag must be 2–4 characters'); if (playerData.stats.credits < 10000) throw new Error('Founding fee is 10,000 credits'); if (playerData.allianceId) throw new Error('You are already in an alliance'); // Check Quantum Relay (if building exists) const qrLevel = playerData.buildings?.quantum_relay?.level || 0; // Allow creation even without relay in early access — just warn // if (qrLevel < 1) throw new Error('Quantum Relay level 1 required to found an alliance'); const allianceId = 'alliance_' + Date.now() + '_' + Math.random().toString(36).slice(2,6); const alliance = new Alliance({ allianceId, name: name.trim(), tag: tag.toUpperCase().trim(), founderId: playerData.userId, founderName: playerData.username, description: description.trim(), members: [{ userId: playerData.userId, username: playerData.username, rank: 'founder', joinedAt: new Date() }], }); await alliance.save(); playerData.stats.credits -= 10000; playerData.allianceId = allianceId; playerData.allianceTag = alliance.tag; playerData.allianceName = alliance.name; playerData.allianceRank = 'founder'; return alliance; } async joinAlliance(playerData, allianceId) { if (playerData.allianceId) throw new Error('Leave your current alliance first'); const alliance = await Alliance.findOne({ allianceId }); if (!alliance) throw new Error('Alliance not found'); if (!alliance.isRecruiting) throw new Error('Alliance is not recruiting'); if (alliance.members.length >= alliance.maxMembers) throw new Error('Alliance is full'); alliance.members.push({ userId: playerData.userId, username: playerData.username, rank: 'member', joinedAt: new Date() }); await alliance.save(); playerData.allianceId = allianceId; playerData.allianceTag = alliance.tag; playerData.allianceName = alliance.name; playerData.allianceRank = 'member'; return alliance; } async leaveAlliance(playerData) { if (!playerData.allianceId) throw new Error('Not in an alliance'); const alliance = await Alliance.findOne({ allianceId: playerData.allianceId }); if (!alliance) { playerData.allianceId = null; return; } if (alliance.founderId === playerData.userId && alliance.members.length > 1) { throw new Error('Transfer leadership before leaving'); } alliance.members = alliance.members.filter(m => m.userId !== playerData.userId); if (alliance.members.length === 0) { await Alliance.deleteOne({ allianceId: playerData.allianceId }); } else { await alliance.save(); } playerData.allianceId = null; playerData.allianceTag = null; playerData.allianceName = null; playerData.allianceRank = null; } async depositWarehouse(playerData, { resource, amount }) { if (!playerData.allianceId) throw new Error('Not in an alliance'); if (!['metal','gas','crystal','energyCells','credits'].includes(resource)) throw new Error('Invalid resource'); amount = Math.floor(amount); if (amount <= 0) throw new Error('Amount must be positive'); const alliance = await Alliance.findOne({ allianceId: playerData.allianceId }); if (!alliance) throw new Error('Alliance not found'); // Deduct from player const resKey = resource === 'credits' ? null : resource; if (resource === 'credits') { if ((playerData.stats.credits || 0) < amount) throw new Error('Insufficient credits'); playerData.stats.credits -= amount; } else { if (!playerData.resources || (playerData.resources[resource] || 0) < amount) throw new Error(`Insufficient ${resource}`); playerData.resources[resource] -= amount; } alliance.warehouse[resource] = Math.min((alliance.warehouse[resource] || 0) + amount, alliance.warehouse.maxPerResource); alliance.warehouseLog.push({ userId: playerData.userId, username: playerData.username, action:'deposit', resource, amount, at: new Date() }); if (alliance.warehouseLog.length > 200) alliance.warehouseLog = alliance.warehouseLog.slice(-200); await alliance.save(); return alliance.warehouse; } async withdrawWarehouse(playerData, { resource, amount }) { if (!playerData.allianceId) throw new Error('Not in an alliance'); const rank = playerData.allianceRank || 'member'; if (!RANK_PERMS[rank]?.some(p => p.startsWith('withdraw'))) throw new Error('Insufficient rank to withdraw'); const alliance = await Alliance.findOne({ allianceId: playerData.allianceId }); if (!alliance) throw new Error('Alliance not found'); amount = Math.floor(amount); if ((alliance.warehouse[resource] || 0) < amount) throw new Error('Insufficient in warehouse'); alliance.warehouse[resource] -= amount; if (resource === 'credits') playerData.stats.credits = (playerData.stats.credits || 0) + amount; else { if (!playerData.resources) playerData.resources = {}; playerData.resources[resource] = (playerData.resources[resource]||0) + amount; } alliance.warehouseLog.push({ userId: playerData.userId, username: playerData.username, action:'withdraw', resource, amount, at: new Date() }); await alliance.save(); return alliance.warehouse; } async getAllianceData(allianceId) { return Alliance.findOne({ allianceId }).lean(); } async searchAlliances(query = '') { const q = query.trim(); const filter = q ? { $or: [{ name: new RegExp(q,'i') }, { tag: new RegExp(q,'i') }] } : {}; return Alliance.find(filter, { name:1, tag:1, description:1, allianceId:1, 'members':1, isRecruiting:1, maxMembers:1 }).limit(20).lean(); } } module.exports = { AllianceSystem, Alliance };