/** * RaidSystem — GDD Phase 3 Launch Prep * * Weekly guild raids (5-player co-op dungeons) and monthly world boss events. * Raid-exclusive loot tables with rare drops not available elsewhere. * * Raid types: * weekly — resets every Monday 00:00 UTC, requires alliance, max 5 players * boss — resets 1st of each month 00:00 UTC, any players, single-phase boss */ class RaidSystem { constructor(contentLoader) { this.loader = contentLoader; this.io = null; this.activeRaids = new Map(); // raidId -> raid instance this.raidQueue = new Map(); // allianceId -> pending party } setIO(io) { this.io = io; } // ── Raid Definitions ───────────────────────────────────────────── getRaidDefinitions() { return { weekly_nebula_assault: { id: 'weekly_nebula_assault', name: 'Nebula Assault', type: 'weekly', description: 'Alliance operation to clear a heavily fortified nebula sector.', minPlayers: 2, maxPlayers: 5, minLevel: 5, phases: 3, energyCost: 30, cooldownHours: 168, // 1 week lootTable: [ { itemId: 'raid_nebula_core', weight: 5, qtyMin: 1, qtyMax: 1 }, { itemId: 'voidsteel_ingot', weight: 20, qtyMin: 2, qtyMax: 5 }, { itemId: 'flux_crystal', weight: 25, qtyMin: 3, qtyMax: 8 }, { itemId: 'energy_boost_large', weight: 30, qtyMin: 1, qtyMax: 2 }, { itemId: 'credit_multiplier', weight: 20, qtyMin: 1, qtyMax: 1 }, ], rewards: { creditsMin: 2000, creditsMax: 5000, xpMin: 800, xpMax: 2000, gems: 5 }, ui: { icon: 'fa-radiation', color: '#8e44ad' } }, weekly_pirate_fortress: { id: 'weekly_pirate_fortress', name: 'Pirate Fortress Siege', type: 'weekly', description: 'Assault a fortified pirate stronghold for rare weapons and credits.', minPlayers: 2, maxPlayers: 5, minLevel: 3, phases: 4, energyCost: 25, cooldownHours: 168, lootTable: [ { itemId: 'raid_pirate_flag', weight: 5, qtyMin: 1, qtyMax: 1 }, { itemId: 'assault_rifle_rare', weight: 15, qtyMin: 1, qtyMax: 1 }, { itemId: 'iron_ore', weight: 30, qtyMin: 5, qtyMax: 15 }, { itemId: 'bandage', weight: 25, qtyMin: 2, qtyMax: 5 }, { itemId: 'health_kit_large', weight: 25, qtyMin: 1, qtyMax: 3 }, ], rewards: { creditsMin: 3000, creditsMax: 8000, xpMin: 600, xpMax: 1500, gems: 3 }, ui: { icon: 'fa-skull-crossbones', color: '#e67e22' } }, monthly_void_titan: { id: 'monthly_void_titan', name: 'Void Titan — World Boss', type: 'monthly', description: 'A colossal entity tears through the sector. All pilots must unite to bring it down.', minPlayers: 1, maxPlayers: 20, minLevel: 8, phases: 1, energyCost: 50, cooldownHours: 720, // 30 days bossId: 'void_titan', bossStats: { health: 500000, attack: 80, defense: 40, phase_thresholds: [0.75, 0.5, 0.25] }, lootTable: [ { itemId: 'raid_titan_shard', weight: 10, qtyMin: 1, qtyMax: 2 }, { itemId: 'void_crystal_rare', weight: 15, qtyMin: 1, qtyMax: 3 }, { itemId: 'helmet_tactical_epic', weight: 8, qtyMin: 1, qtyMax: 1 }, { itemId: 'body_exosuit_epic', weight: 8, qtyMin: 1, qtyMax: 1 }, { itemId: 'xp_booster', weight: 35, qtyMin: 2, qtyMax: 4 }, { itemId: 'credit_multiplier', weight: 24, qtyMin: 1, qtyMax: 2 }, ], rewards: { creditsMin: 10000, creditsMax: 25000, xpMin: 3000, xpMax: 8000, gems: 15 }, ui: { icon: 'fa-dragon', color: '#ff4488' } } }; } getRaid(id) { return this.getRaidDefinitions()[id] || null; } getAllRaids() { return Object.values(this.getRaidDefinitions()); } getWeeklyRaids() { return this.getAllRaids().filter(r => r.type === 'weekly'); } getMonthlyRaids() { return this.getAllRaids().filter(r => r.type === 'monthly'); } // ── Reset Windows ───────────────────────────────────────────────── getWeeklyResetTime() { const now = new Date(); const day = now.getUTCDay(); // 0=Sun,1=Mon const daysUntilMonday = day === 0 ? 1 : (8 - day) % 7 || 7; const reset = new Date(now); reset.setUTCDate(now.getUTCDate() + daysUntilMonday); reset.setUTCHours(0, 0, 0, 0); return reset; } getMonthlyResetTime() { const now = new Date(); const reset = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1)); return reset; } // ── Party Queue ─────────────────────────────────────────────────── queueForRaid(raidId, userId, username, allianceId, playerData) { const def = this.getRaid(raidId); if (!def) return { success: false, error: 'Unknown raid' }; if (def.type === 'weekly' && !allianceId) { return { success: false, error: 'Must be in an alliance for weekly raids' }; } if ((playerData.stats?.level || 1) < def.minLevel) { return { success: false, error: `Requires level ${def.minLevel}` }; } if ((playerData.resources?.energyCells || 0) < def.energyCost) { return { success: false, error: `Requires ${def.energyCost} Energy Cells` }; } const queueKey = def.type === 'weekly' ? allianceId : 'global'; if (!this.raidQueue.has(queueKey)) this.raidQueue.set(queueKey, {}); const q = this.raidQueue.get(queueKey); if (!q[raidId]) q[raidId] = []; // Prevent duplicate queue if (q[raidId].find(p => p.userId === userId)) { return { success: false, error: 'Already queued for this raid' }; } q[raidId].push({ userId, username, allianceId, joinedAt: Date.now() }); const party = q[raidId]; if (party.length >= def.minPlayers) { const launched = this._launchRaid(raidId, def, party.slice(0, def.maxPlayers), queueKey); q[raidId] = party.slice(def.maxPlayers); // leftover back in queue return { success: true, launched: true, raidId: launched.raidId, party: launched.party }; } return { success: true, launched: false, queued: true, partySize: party.length, waiting: def.minPlayers - party.length }; } _launchRaid(defId, def, party, queueKey) { const raidId = `raid_${defId}_${Date.now()}`; const instance = { raidId, defId, type: def.type, name: def.name, party: party.map(p => ({ ...p, alive: true, damage: 0 })), phase: 1, totalPhases: def.phases, bossHp: def.bossStats?.health || (def.phases * 1000), maxBossHp: def.bossStats?.health || (def.phases * 1000), startedAt: Date.now(), completed: false, failed: false, lootTable: def.lootTable, rewards: def.rewards, log: [] }; this.activeRaids.set(raidId, instance); // Notify all party members if (this.io) { party.forEach(p => { const sockets = [...this.io.sockets.sockets.values()].filter(s => s.userId === p.userId); sockets.forEach(s => s.emit('raid_started', { raidId, name: def.name, phase: 1, totalPhases: def.phases, party: party.map(m => m.username), bossHp: instance.bossHp })); }); } return instance; } // ── Encounter Processing ────────────────────────────────────────── processRaidAction(raidId, userId, action = 'attack') { const raid = this.activeRaids.get(raidId); if (!raid) return { success: false, error: 'Raid not found' }; if (raid.completed || raid.failed) return { success: false, error: 'Raid already ended' }; const member = raid.party.find(p => p.userId === userId); if (!member) return { success: false, error: 'Not in this raid party' }; if (!member.alive) return { success: false, error: 'You have been defeated' }; // Simple authoritative tick: each action = one attack round for this player const baseDmg = 50 + Math.floor(Math.random() * 50); // 50–100 per action const partyMult = 1 + (raid.party.filter(p => p.alive).length - 1) * 0.15; // +15% per extra live member const dealt = Math.round(baseDmg * partyMult); member.damage += dealt; raid.bossHp = Math.max(0, raid.bossHp - dealt); raid.log.push({ userId, username: member.username, action, dealt, bossHp: raid.bossHp, at: Date.now() }); // Check phase advance const phasePct = 1 - (raid.bossHp / raid.maxBossHp); const nextPhase = Math.min(raid.totalPhases, Math.floor(phasePct * raid.totalPhases) + 1); if (nextPhase > raid.phase) { raid.phase = nextPhase; if (this.io) { raid.party.forEach(p => { const sockets = [...this.io.sockets.sockets.values()].filter(s => s.userId === p.userId); sockets.forEach(s => s.emit('raid_phase_change', { raidId, phase: raid.phase, bossHp: raid.bossHp })); }); } } // Boss defeated if (raid.bossHp <= 0) { raid.completed = true; const loot = this._rollLoot(raid.lootTable); return { success: true, bossDefeated: true, raidId, loot, rewards: raid.rewards, damage: member.damage }; } return { success: true, bossDefeated: false, dealt, bossHp: raid.bossHp, bossMaxHp: raid.maxBossHp, phase: raid.phase }; } _rollLoot(table) { const totalWeight = table.reduce((s, e) => s + e.weight, 0); const results = []; // Each party member gets ~2 loot rolls for (let i = 0; i < 2; i++) { let roll = Math.random() * totalWeight; for (const entry of table) { roll -= entry.weight; if (roll <= 0) { const qty = entry.qtyMin + Math.floor(Math.random() * (entry.qtyMax - entry.qtyMin + 1)); results.push({ itemId: entry.itemId, qty }); break; } } } return results; } getRaidInstance(raidId) { return this.activeRaids.get(raidId) || null; } getPlayerRaid(userId) { for (const [, raid] of this.activeRaids) { if (!raid.completed && !raid.failed && raid.party.find(p => p.userId === userId)) return raid; } return null; } cleanup() { // Remove completed/failed raids older than 2h const cutoff = Date.now() - 2 * 60 * 60 * 1000; for (const [id, raid] of this.activeRaids) { if ((raid.completed || raid.failed) && raid.startedAt < cutoff) { this.activeRaids.delete(id); } } } } module.exports = RaidSystem;