208 lines
7.7 KiB
JavaScript
208 lines
7.7 KiB
JavaScript
/**
|
||
* 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;
|