import moment from 'moment'; import SEASONAL_SETS from './seasonalSets'; import { getRepeatingEvents } from './events'; function isAfterNewSchedule (year, month) { if (year >= 2025) { return true; } if (year === 2024) { return month >= 6; } return false; } function backgroundMatcher (month1, month2, oddYear) { return function call (key, date) { const keyLength = key.length; const month = parseInt(key.substring(keyLength - 6, keyLength - 4), 10); const year = parseInt(key.substring(keyLength - 4, keyLength), 10); if (isAfterNewSchedule(year, month)) { if (date.getMonth() === 0 && month1 === 12) { return month === month1 && (date.getFullYear() - 1) >= year; } return month === month1 && date.getFullYear() >= year; } return (month === month1 || month === month2) && year % 2 === (oddYear ? 1 : 0); }; } function timeTravelersMatcher (month1, month2) { return function call (item, date) { const month = parseInt(item.substring(4, 6), 10); const year = parseInt(item.substring(0, 4), 10); if (date.getFullYear() < year || (date.getFullYear() === year && (date.getMonth() + 1) === month)) { return false; } return month === month1 || month === month2; }; } function inListMatcher (list) { return function call (item) { return list.indexOf(item) !== -1; }; } export const ALWAYS_AVAILABLE_CUSTOMIZATIONS = [ 'animalSkins', 'rainbowSkins', 'rainbowHairColors', 'specialShirts', 'facialHair', 'baseHair1', 'baseHair2', 'baseHair3', ]; function customizationMatcher (list) { return function call (item) { if (ALWAYS_AVAILABLE_CUSTOMIZATIONS.indexOf(item) !== -1) { return true; } return list.indexOf(item) !== -1; }; } export const FIRST_RELEASE_DAY = 1; export const SECOND_RELEASE_DAY = 7; export const THIRD_RELEASE_DAY = 14; export const FOURTH_RELEASE_DAY = 21; export const MONTHLY_SCHEDULE = { // January 0: { [FIRST_RELEASE_DAY]: [ { type: 'timeTravelers', matcher: timeTravelersMatcher(1, 7), }, ], [SECOND_RELEASE_DAY]: [ { type: 'backgrounds', matcher: backgroundMatcher(1, 7, false), }, ], [THIRD_RELEASE_DAY]: [ { type: 'petQuests', items: [ 'nudibranch', 'seaserpent', 'gryphon', 'yarn', 'axolotl', ], }, { type: 'hatchingPotionQuests', items: [ 'silver', ], }, { type: 'bundles', items: [ 'winterQuests', ], }, ], [FOURTH_RELEASE_DAY]: [ { type: 'premiumHatchingPotions', items: [ 'Aurora', 'Cupid', 'IcySnow', ], }, ], }, // February 1: { [FIRST_RELEASE_DAY]: [ { type: 'timeTravelers', matcher: timeTravelersMatcher(2, 8), }, ], [SECOND_RELEASE_DAY]: [ { type: 'backgrounds', matcher: backgroundMatcher(2, 8, false), }, ], [THIRD_RELEASE_DAY]: [ { type: 'petQuests', items: [ 'rooster', 'slime', 'peacock', 'bunny', 'cat', ], }, { type: 'hatchingPotionQuests', items: [ 'pinkMarble', ], }, { type: 'bundles', items: [ 'cuddleBuddies', 'mythicalMarvels', ], }, ], [FOURTH_RELEASE_DAY]: [ { type: 'premiumHatchingPotions', items: [ 'PolkaDot', 'Celestial', 'RoseGold', ], }, ], }, // March 2: { [FIRST_RELEASE_DAY]: [ { type: 'timeTravelers', matcher: timeTravelersMatcher(3, 9), }, ], [SECOND_RELEASE_DAY]: [ { type: 'backgrounds', matcher: backgroundMatcher(3, 9, false), }, ], [THIRD_RELEASE_DAY]: [ { type: 'petQuests', items: [ 'frog', 'spider', 'cow', 'pterodactyl', 'otter', ], }, { type: 'hatchingPotionQuests', items: [ 'jade', ], }, { type: 'bundles', items: [ 'birdBuddies', ], }, ], [FOURTH_RELEASE_DAY]: [ { type: 'premiumHatchingPotions', items: [ 'StainedGlass', 'Porcelain', 'BirchBark', ], }, ], }, // April 3: { [FIRST_RELEASE_DAY]: [ { type: 'timeTravelers', matcher: timeTravelersMatcher(4, 10), }, ], [SECOND_RELEASE_DAY]: [ { type: 'backgrounds', matcher: backgroundMatcher(4, 10, false), }, ], [THIRD_RELEASE_DAY]: [ { type: 'petQuests', items: [ 'snake', 'monkey', 'falcon', 'alligator', 'alpaca', ], }, { type: 'hatchingPotionQuests', items: [ 'stone', ], }, { type: 'bundles', items: [ 'hugabug', ], }, ], [FOURTH_RELEASE_DAY]: [ { type: 'premiumHatchingPotions', items: [ 'Shimmer', 'Glass', 'Balloon', ], }, ], }, // May 4: { [FIRST_RELEASE_DAY]: [ { type: 'timeTravelers', matcher: timeTravelersMatcher(5, 11), }, ], [SECOND_RELEASE_DAY]: [ { type: 'backgrounds', matcher: backgroundMatcher(5, 11, false), }, ], [THIRD_RELEASE_DAY]: [ { type: 'petQuests', items: [ 'octopus', 'horse', 'kraken', 'sloth', 'platypus', ], }, { type: 'hatchingPotionQuests', items: [ 'opal', ], }, { type: 'bundles', items: [ 'splashyPals', ], }, ], [FOURTH_RELEASE_DAY]: [ { type: 'premiumHatchingPotions', items: [ 'Floral', 'Fairy', 'RoseQuartz', ], }, ], }, // June 5: { [FIRST_RELEASE_DAY]: [ { type: 'timeTravelers', matcher: timeTravelersMatcher(6, 12), }, ], [SECOND_RELEASE_DAY]: [ { type: 'backgrounds', matcher: backgroundMatcher(6, 12, false), }, ], [THIRD_RELEASE_DAY]: [ { type: 'petQuests', items: [ 'trex', 'unicorn', 'velociraptor', 'hippo', 'giraffe', ], }, { type: 'hatchingPotionQuests', items: [ 'turquoise', ], }, { type: 'bundles', items: [ 'delightfulDinos', 'sandySidekicks', ], }, ], [FOURTH_RELEASE_DAY]: [ { type: 'premiumHatchingPotions', items: [ 'Rainbow', 'Sunshine', 'Koi', ], }, ], }, // July 6: { [FIRST_RELEASE_DAY]: [ { type: 'timeTravelers', matcher: timeTravelersMatcher(7, 1), }, ], [SECOND_RELEASE_DAY]: [ { type: 'backgrounds', matcher: backgroundMatcher(7, 1, true), }, ], [THIRD_RELEASE_DAY]: [ { type: 'petQuests', items: [ 'whale', 'dilatory_derby', 'armadillo', 'guineapig', 'chameleon', ], }, { type: 'hatchingPotionQuests', items: [ 'fluorite', ], }, { type: 'bundles', items: [ 'aquaticAmigos', 'jungleBuddies', ], }, ], [FOURTH_RELEASE_DAY]: [ { type: 'premiumHatchingPotions', items: [ 'Moonglow', 'Watery', 'SandSculpture', ], }, ], }, // August 7: { [FIRST_RELEASE_DAY]: [ { type: 'timeTravelers', matcher: timeTravelersMatcher(8, 2), }, ], [SECOND_RELEASE_DAY]: [ { type: 'backgrounds', matcher: backgroundMatcher(8, 2, true), }, ], [THIRD_RELEASE_DAY]: [ { type: 'petQuests', items: [ 'turtle', 'penguin', 'butterfly', 'cheetah', 'crab', ], }, { type: 'hatchingPotionQuests', items: [ 'blackPearl', ], }, { type: 'bundles', items: [ 'featheredFriends', ], }, ], [FOURTH_RELEASE_DAY]: [ { type: 'premiumHatchingPotions', items: [ 'Aquatic', 'StarryNight', 'Sunset', ], }, ], }, // September 8: { [FIRST_RELEASE_DAY]: [ { type: 'timeTravelers', matcher: timeTravelersMatcher(9, 3), }, ], [SECOND_RELEASE_DAY]: [ { type: 'backgrounds', matcher: backgroundMatcher(9, 3, true), }, ], [THIRD_RELEASE_DAY]: [ { type: 'petQuests', items: [ 'squirrel', 'triceratops', 'treeling', 'beetle', 'raccoon', ], }, { type: 'hatchingPotionQuests', items: [ 'bronze', ], }, { type: 'bundles', items: [ 'farmFriends', ], }, ], [FOURTH_RELEASE_DAY]: [ { type: 'premiumHatchingPotions', items: [ 'Glow', 'AutumnLeaf', 'Shadow', ], }, ], }, // October 9: { [FIRST_RELEASE_DAY]: [ { type: 'timeTravelers', matcher: timeTravelersMatcher(10, 4), }, ], [SECOND_RELEASE_DAY]: [ { type: 'backgrounds', matcher: backgroundMatcher(10, 4, true), }, ], [THIRD_RELEASE_DAY]: [ { type: 'petQuests', items: [ 'snail', 'rock', 'ferret', 'hedgehog', 'dog', ], }, { type: 'hatchingPotionQuests', items: [ 'onyx', ], }, { type: 'bundles', items: [ 'witchyFamiliars', ], }, ], [FOURTH_RELEASE_DAY]: [ { type: 'premiumHatchingPotions', items: [ 'Vampire', 'Ghost', 'Spooky', ], }, ], }, // November 10: { [FIRST_RELEASE_DAY]: [ { type: 'timeTravelers', matcher: timeTravelersMatcher(11, 5), }, ], [SECOND_RELEASE_DAY]: [ { type: 'backgrounds', matcher: backgroundMatcher(11, 5, true), }, ], [THIRD_RELEASE_DAY]: [ { type: 'petQuests', items: [ 'sheep', 'kangaroo', 'owl', 'rat', 'badger', ], }, { type: 'hatchingPotionQuests', items: [ 'amber', ], }, { type: 'bundles', items: [ 'forestFriends', 'oddballs', ], }, ], [FOURTH_RELEASE_DAY]: [ { type: 'premiumHatchingPotions', items: [ 'Ember', 'Frost', 'Thunderstorm', ], }, ], }, // December 11: { [FIRST_RELEASE_DAY]: [ { type: 'timeTravelers', matcher: timeTravelersMatcher(12, 6), }, ], [SECOND_RELEASE_DAY]: [ { type: 'backgrounds', matcher: backgroundMatcher(12, 6, true), }, ], [THIRD_RELEASE_DAY]: [ { type: 'petQuests', items: [ 'ghost_stag', 'trex_undead', 'harpy', 'sabretooth', 'dolphin', ], }, { type: 'hatchingPotionQuests', items: [ 'ruby', ], }, { type: 'bundles', items: [ 'rockingReptiles', ], }, ], [FOURTH_RELEASE_DAY]: [ { type: 'premiumHatchingPotions', items: [ 'Peppermint', 'Holly', 'Gingerbread', ], }, ], }, }; export const GALA_SWITCHOVER_DAY = 1; export const GALA_KEYS = [ 'winter', 'spring', 'summer', 'fall', ]; export const GALA_SCHEDULE = { // Winter 0: { startMonth: 11, endMonth: 2, matchers: [ { type: 'seasonalGear', items: SEASONAL_SETS.winter, }, { type: 'seasonalSpells', items: [ 'snowball', ], }, { type: 'seasonalQuests', items: [ 'evilsanta', 'evilsanta2', ], }, { type: 'customizations', matcher: customizationMatcher([ 'winteryHairColors', 'winterySkins', ]), }, ], }, // Spring 1: { startMonth: 2, endMonth: 5, matchers: [ { type: 'seasonalGear', items: SEASONAL_SETS.spring, }, { type: 'seasonalSpells', items: [ 'shinySeed', ], }, { type: 'seasonalQuests', items: [ 'egg', ], }, { type: 'customizations', matcher: customizationMatcher([ 'shimmerHairColors', 'pastelSkins', ]), }, ], }, // Summer 2: { startMonth: 5, endMonth: 8, matchers: [ { type: 'seasonalGear', items: SEASONAL_SETS.summer, }, { type: 'seasonalSpells', items: [ 'seafoam', ], }, { type: 'seasonalQuests', items: [ ], }, { type: 'customizations', matcher: customizationMatcher([ 'summerHairColors', 'splashySkins', ]), }, ], }, // Fall 3: { startMonth: 8, endMonth: 11, matchers: [ { type: 'seasonalGear', items: SEASONAL_SETS.fall, }, { type: 'seasonalSpells', items: [ 'spookySparkles', ], }, { type: 'seasonalQuests', items: [ ], }, { type: 'customizations', matcher: customizationMatcher([ 'hauntedHairColors', 'supernaturalSkins', ]), }, ], }, }; const SWITCHOVER_TIME = process.env.CONTENT_SWITCHOVER_TIME_OFFSET || 0; export const TYPE_SCHEDULE = { timeTravelers: FIRST_RELEASE_DAY, backgrounds: SECOND_RELEASE_DAY, petQuests: THIRD_RELEASE_DAY, hatchingPotionQuests: THIRD_RELEASE_DAY, bundles: THIRD_RELEASE_DAY, premiumHatchingPotions: FOURTH_RELEASE_DAY, seasonalGear: GALA_SWITCHOVER_DAY, seasonalSpells: GALA_SWITCHOVER_DAY, seasonalQuests: GALA_SWITCHOVER_DAY, customizations: GALA_SWITCHOVER_DAY, }; function adjustDateForUTCAndSwitch (date) { const checkDate = new Date(date.getTime() + date.getTimezoneOffset() * 60000); checkDate.setHours(checkDate.getHours() - SWITCHOVER_TIME); return checkDate; } function getDay (date) { if (date === undefined) { return 0; } return adjustDateForUTCAndSwitch(date).getDate(); } function getMonth (date) { if (date === undefined) { return 0; } return adjustDateForUTCAndSwitch(date).getMonth(); } function getGalaIndex (date) { const month = getMonth(date); const todayDay = getDay(date); let galaMonth = month; const galaCount = Object.keys(GALA_SCHEDULE).length; if (todayDay >= GALA_SWITCHOVER_DAY) { galaMonth += 1; } if (galaMonth >= 12) { return 0; } return parseInt((galaCount / 12) * galaMonth, 10); } function makeEndDate (checkedDate, matcher) { let end = moment.utc(checkedDate); end.hour(SWITCHOVER_TIME); end.minute(0); end.second(0); if (matcher.end !== undefined) { end.date(matcher.end.getDate()); end.month(matcher.end.getMonth()); } else { end.date(TYPE_SCHEDULE[matcher.type]); if (matcher.endMonth !== undefined) { if (matcher.startMonth && matcher.startMonth > matcher.endMonth && checkedDate.getMonth() > matcher.endMonth) { end.year(checkedDate.getFullYear() + 1); } end.month(matcher.endMonth); } else if (end.valueOf() <= checkedDate.getTime()) { end = moment(end).add(1, 'months'); } } return end.toDate(); } export function assembleScheduledMatchers (date) { const items = []; const month = getMonth(date); const todayDay = getDay(date); const previousMonth = month === 0 ? 11 : month - 1; for (const [day, value] of Object.entries(MONTHLY_SCHEDULE[previousMonth])) { if (day > todayDay) { items.push(...value); } } for (const [day, value] of Object.entries(MONTHLY_SCHEDULE[month])) { if (day <= todayDay) { items.push(...value); } } const gala = GALA_SCHEDULE[getGalaIndex(date)]; const galaMatchers = gala.matchers; galaMatchers.forEach(matcher => { matcher.startMonth = gala.startMonth; matcher.endMonth = gala.endMonth; }); items.push(...galaMatchers); getRepeatingEvents(date).forEach(event => { if (event.content) { const { content } = event; const end = makeEndDate(date, event); const m = content.map(matcher => { const newMatcher = { ...matcher }; newMatcher.end = end; return newMatcher; }); items.push(...m); } }); return items; } let cachedScheduleMatchers = null; let cacheDate = null; function makeMatcherClass (date) { return { matchers: [], end: new Date(), specialEnds: {}, items: [], matchingDate: date, getEnd (key) { if (this.specialEnds[key]) { return this.specialEnds[key]; } return this.end; }, match (key) { if (this.matchers.length === 0) { if (this.items.length > 0) { return inListMatcher(this.items)(key, this.matchingDate); } } else { if (this.items.length > 0 && !inListMatcher(this.items)(key, this.matchingDate)) { return false; } return this.matchers.every(m => m(key, this.matchingDate)); } return false; }, }; } export function clearCachedMatchers () { cacheDate = null; cachedScheduleMatchers = null; } export function getAllScheduleMatchingGroups (date) { const checkedDate = date || moment.utc().toDate(); if (cacheDate !== null && (getDay(checkedDate) !== getDay(cacheDate) || getMonth(checkedDate) !== getMonth(cacheDate))) { // Clear cached matchers, since they are old clearCachedMatchers(); } if (!cachedScheduleMatchers) { // No matchers exist, make new ones cacheDate = new Date(); cachedScheduleMatchers = {}; // subtract switchover time for the matcher classes, but // NOT to decide which matchers to assemble. // assembly uses getDate and getMonth which already adjust for switchover time const adjustedDate = adjustDateForUTCAndSwitch(checkedDate); assembleScheduledMatchers(checkedDate).forEach(matcher => { if (!cachedScheduleMatchers[matcher.type]) { cachedScheduleMatchers[matcher.type] = makeMatcherClass(adjustedDate); } if (matcher.end === undefined) { // we want the default end date to be for matcher type cachedScheduleMatchers[matcher.type].end = makeEndDate(checkedDate, matcher); } if (matcher.matcher instanceof Function) { cachedScheduleMatchers[matcher.type].matchers.push(matcher.matcher); } else if (matcher.items instanceof Array) { cachedScheduleMatchers[matcher.type].items.push(...matcher.items); if (matcher.end !== undefined) { matcher.items.forEach(item => { cachedScheduleMatchers[matcher.type].specialEnds[item] = matcher.end; }); } } }); } return cachedScheduleMatchers; } export function getScheduleMatchingGroup (type, date) { const matchingGroups = getAllScheduleMatchingGroups(date); if (!matchingGroups[type]) { // No matchers exist for this type return { items: [], match () { return true; }, }; } return matchingGroups[type]; } export function getCurrentGalaKey (date) { return GALA_KEYS[getGalaIndex(date || new Date())]; } export function getCurrentGalaEvent (date) { const checkedDate = date || new Date(); const index = getGalaIndex(checkedDate); const key = GALA_KEYS[index]; const gala = GALA_SCHEDULE[index]; const today = new Date(); return { event: key, npcImageSuffix: `_${key}`, season: key, gear: true, start: `${today.getFullYear()}.${gala.startMonth + 1}.${GALA_SWITCHOVER_DAY}`, end: `${today.getFullYear()}.${gala.endMonth + 1}.${GALA_SWITCHOVER_DAY}`, }; }