This repository has been archived on 2026-05-04. You can view files and clone it, but cannot push or open issues or pull requests.
Galaxy-Strike-Online/GameServer/systems/RaidSystem.js
2026-03-11 00:32:45 -03:00

268 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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); // 50100 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;