/** * AllianceWarSystem — GDD Phase 3 Launch Prep * * 72-hour scheduled alliance wars. Alliances declare war on each other. * War score = fleet battles won. Territory bonuses for winning alliance. * Each war cycles through: declaration (24h) → active (72h) → resolution. */ const mongoose = require('mongoose'); const warSchema = new mongoose.Schema({ warId: { type: String, required: true, unique: true }, attackerId: { type: String, required: true }, attackerName: { type: String, required: true }, attackerTag: { type: String, required: true }, defenderId: { type: String, required: true }, defenderName: { type: String, required: true }, defenderTag: { type: String, required: true }, status: { type: String, default: 'declaration', enum: ['declaration','active','ended'] }, attackerScore: { type: Number, default: 0 }, defenderScore: { type: Number, default: 0 }, battles: [{ type: mongoose.Schema.Types.Mixed }], declaredAt: { type: Date, default: Date.now }, startedAt: { type: Date, default: null }, endsAt: { type: Date, default: null }, winnerId: { type: String, default: null }, winnerName: { type: String, default: null }, territory: { type: mongoose.Schema.Types.Mixed, default: null }, }, { timestamps: true }); let AllianceWar; try { AllianceWar = mongoose.model('AllianceWar'); } catch(e) { AllianceWar = mongoose.model('AllianceWar', warSchema); } class AllianceWarSystem { constructor() { this.io = null; // Territory bonus applied to winning alliance members for 7 days this.TERRITORY_BONUSES = { resource_production: 0.10, // +10% all resource production market_fee_reduction: 0.005, // −0.5% market fee xp_bonus: 0.05, // +5% XP gain }; this.DECLARATION_HOURS = 24; this.WAR_HOURS = 72; } setIO(io) { this.io = io; } // ── Declare War ─────────────────────────────────────────────────── async declareWar(attackerAlliance, defenderAlliance) { if (!attackerAlliance || !defenderAlliance) throw new Error('Invalid alliances'); if (attackerAlliance._id.toString() === defenderAlliance._id.toString()) { throw new Error('Cannot declare war on your own alliance'); } // Check no existing active war between these two const existing = await AllianceWar.findOne({ status: { $in: ['declaration','active'] }, $or: [ { attackerId: attackerAlliance._id.toString(), defenderId: defenderAlliance._id.toString() }, { attackerId: defenderAlliance._id.toString(), defenderId: attackerAlliance._id.toString() }, ] }); if (existing) throw new Error('A war between these alliances is already in progress'); const warId = `war_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`; const declaredAt = new Date(); const startedAt = new Date(declaredAt.getTime() + this.DECLARATION_HOURS * 3600 * 1000); const endsAt = new Date(startedAt.getTime() + this.WAR_HOURS * 3600 * 1000); const war = await AllianceWar.create({ warId, attackerId: attackerAlliance._id.toString(), attackerName: attackerAlliance.name, attackerTag: attackerAlliance.tag, defenderId: defenderAlliance._id.toString(), defenderName: defenderAlliance.name, defenderTag: defenderAlliance.tag, status: 'declaration', declaredAt, startedAt, endsAt, }); // Schedule auto-start and auto-end const toStart = startedAt.getTime() - Date.now(); const toEnd = endsAt.getTime() - Date.now(); if (toStart > 0) setTimeout(() => this._activateWar(warId), toStart); if (toEnd > 0) setTimeout(() => this._resolveWar(warId), toEnd); return war; } async _activateWar(warId) { const war = await AllianceWar.findOne({ warId }); if (!war || war.status !== 'declaration') return; war.status = 'active'; await war.save(); if (this.io) this.io.emit('alliance_war_started', { warId, attackerTag: war.attackerTag, defenderTag: war.defenderTag }); } async _resolveWar(warId) { const war = await AllianceWar.findOne({ warId }); if (!war || war.status === 'ended') return; war.status = 'ended'; if (war.attackerScore > war.defenderScore) { war.winnerId = war.attackerId; war.winnerName = war.attackerName; } else if (war.defenderScore > war.attackerScore) { war.winnerId = war.defenderId; war.winnerName = war.defenderName; } else { war.winnerName = 'Draw'; } // Territory bonus for winning alliance (stored as metadata on war doc) if (war.winnerId) { war.territory = { allianceId: war.winnerId, bonuses: this.TERRITORY_BONUSES, expiresAt: new Date(Date.now() + 7 * 24 * 3600 * 1000), }; } await war.save(); if (this.io) { this.io.emit('alliance_war_ended', { warId, attackerTag: war.attackerTag, attackerScore: war.attackerScore, defenderTag: war.defenderTag, defenderScore: war.defenderScore, winnerName: war.winnerName, territoryBonus: war.territory ? this.TERRITORY_BONUSES : null, }); } } // ── Battle Submission ────────────────────────────────────────────── async submitBattle(warId, attackerUserId, defenderUserId, attackerWon, battleDetails = {}) { const war = await AllianceWar.findOne({ warId, status: 'active' }); if (!war) throw new Error('War not found or not active'); const battle = { attackerUserId, defenderUserId, attackerWon, at: new Date(), ...battleDetails, }; war.battles.push(battle); // Score: 1 point per win for the winning side if (attackerWon) war.attackerScore += 1; else war.defenderScore += 1; await war.save(); if (this.io) { this.io.emit('alliance_war_battle', { warId, attackerTag: war.attackerTag, attackerScore: war.attackerScore, defenderTag: war.defenderTag, defenderScore: war.defenderScore, battle, }); } return war; } // ── Queries ──────────────────────────────────────────────────────── async getActiveWars() { return AllianceWar.find({ status: { $in: ['declaration','active'] } }).sort({ declaredAt: -1 }).limit(20); } async getWarsByAlliance(allianceId) { return AllianceWar.find({ $or: [{ attackerId: allianceId }, { defenderId: allianceId }] }).sort({ declaredAt: -1 }).limit(10); } async getWar(warId) { return AllianceWar.findOne({ warId }); } async getTerritoryBonus(allianceId) { const now = new Date(); const war = await AllianceWar.findOne({ status: 'ended', winnerId: allianceId, 'territory.expiresAt': { $gt: now }, }); return war ? war.territory.bonuses : null; } // Resume timers on server restart async resumeTimers() { const wars = await AllianceWar.find({ status: { $in: ['declaration','active'] } }); const now = Date.now(); for (const war of wars) { if (war.status === 'declaration' && war.startedAt > new Date()) { const ms = war.startedAt.getTime() - now; if (ms > 0) setTimeout(() => this._activateWar(war.warId), ms); } if (war.endsAt > new Date()) { const ms = war.endsAt.getTime() - now; if (ms > 0) setTimeout(() => this._resolveWar(war.warId), ms); } } } } module.exports = AllianceWarSystem;