/** * Galaxy Strike Online โ€” Season System (GDD ยง20.3) * 90-day seasons with themed content, seasonal leaderboards, and exclusive rewards. * Season 1: "Dawn of the Void" */ const SEASONS = { 1: { id: 1, name: 'Dawn of the Void', theme: 'void_cult', icon: '๐ŸŒ‘', color: '#78909c', description: 'The Void Cult stirs. Ancient relics emerge from uncharted sectors. Are you ready?', durationDays: 90, rewards: { top1: { title: 'Void Champion', skin: 'void_champion_skin', credits: 50000 }, top10: { title: 'Void Knight', skin: 'void_knight_skin', credits: 20000 }, top100:{ title: 'Void Initiate', skin: null, credits: 5000 }, participation: { title: 'Season 1 Veteran', credits: 1000 }, }, bonuses: { darkMatterBonus: 0.25, // +25% dark matter production voidCultRepBonus: 0.5, // +50% Void Cult reputation gain }, startDate: new Date('2026-01-01'), endDate: new Date('2026-04-01'), } }; const CURRENT_SEASON_ID = 1; class SeasonSystem { constructor() { this.currentSeason = SEASONS[CURRENT_SEASON_ID]; } getCurrentSeason() { const now = new Date(); const s = this.currentSeason; if (!s) return { active: false }; const daysLeft = Math.max(0, Math.ceil((s.endDate - now) / 86400000)); const daysTotal = Math.ceil((s.endDate - s.startDate) / 86400000); const daysDone = Math.max(0, Math.ceil((now - s.startDate) / 86400000)); return { active: now >= s.startDate && now <= s.endDate, season: { ...s, daysLeft, daysTotal, daysDone, progress: Math.round(daysDone/daysTotal*100) }, }; } getSeasonBonuses() { return this.currentSeason?.bonuses || {}; } recordSeasonActivity(playerData, activityType, score = 1) { if (!playerData.seasonProgress) playerData.seasonProgress = {}; playerData.seasonProgress[CURRENT_SEASON_ID] = playerData.seasonProgress[CURRENT_SEASON_ID] || { score: 0, activities: {} }; const sp = playerData.seasonProgress[CURRENT_SEASON_ID]; sp.score += score; sp.activities[activityType] = (sp.activities[activityType] || 0) + 1; return sp.score; } getSeasonScore(playerData) { return playerData.seasonProgress?.[CURRENT_SEASON_ID]?.score || 0; } /** Check if the season just ended (active last tick, ended now) */ isActive() { const now = Date.now(); const s = this.currentSeason; return s && now >= s.startDate && now <= s.endDate; } /** * Distribute end-of-season rewards to all players. * Called once when season transitions from active -> ended. * @param {Array} allPlayerDataArray โ€” array of playerData objects */ distributeSeasonRewards(allPlayerDataArray) { const TIER_REWARDS = [ { minRank: 1, maxRank: 1, credits: 100000, title: 'Void Champion', skin: 'season1_champion' }, { minRank: 2, maxRank: 3, credits: 50000, title: 'Void Master', skin: 'season1_master' }, { minRank: 4, maxRank: 10, credits: 25000, title: 'Void Elite', skin: null }, { minRank: 11, maxRank: 50, credits: 10000, title: 'Void Veteran', skin: null }, { minRank: 51, maxRank: Infinity, credits: 2500, title: 'Void Recruit', skin: null }, ]; // Sort players by season score descending const ranked = allPlayerDataArray .filter(pd => (pd.seasonProgress?.[CURRENT_SEASON_ID]?.score || 0) > 0) .sort((a, b) => this.getSeasonScore(b) - this.getSeasonScore(a)); const distributed = []; ranked.forEach((pd, idx) => { const rank = idx + 1; const tier = TIER_REWARDS.find(t => rank >= t.minRank && rank <= t.maxRank); if (!tier) return; // Apply rewards pd.resources = pd.resources || {}; pd.resources.credits = (pd.resources.credits || 0) + tier.credits; pd.stats = pd.stats || {}; pd.stats.title = tier.title; if (tier.skin && pd.cosmetics) { pd.cosmetics.unlockedSkins = pd.cosmetics.unlockedSkins || []; if (!pd.cosmetics.unlockedSkins.includes(tier.skin)) { pd.cosmetics.unlockedSkins.push(tier.skin); } } // Mark season as rewarded if (pd.seasonProgress?.[CURRENT_SEASON_ID]) { pd.seasonProgress[CURRENT_SEASON_ID].rewarded = true; pd.seasonProgress[CURRENT_SEASON_ID].rank = rank; } distributed.push({ userId: pd.userId || pd._id, rank, credits: tier.credits, title: tier.title }); }); console.log(`[SEASON] Distributed rewards to ${distributed.length} players for season ${CURRENT_SEASON_ID}`); return distributed; } }