diff --git a/test/content/armoire.test.js b/test/content/armoire.test.js index 788dc05bee..6462939782 100644 --- a/test/content/armoire.test.js +++ b/test/content/armoire.test.js @@ -64,6 +64,6 @@ describe('armoire', () => { delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')]; clock = sinon.useFakeTimers(new Date('2024-02-07T09:00:00.000Z')); const febuaryItems = makeArmoireIitemList(); - expect(febuaryItems.length).to.equal(381); + expect(febuaryItems.length).to.equal(384); }); }); diff --git a/test/content/eggs.test.js b/test/content/eggs.test.js index cb0e173579..69dcb47798 100644 --- a/test/content/eggs.test.js +++ b/test/content/eggs.test.js @@ -5,29 +5,51 @@ import { expectValidTranslationString, } from '../helpers/content.helper'; -import * as eggs from '../../website/common/script/content/eggs'; +import eggs from '../../website/common/script/content/eggs'; describe('eggs', () => { - describe('all', () => { - it('is a combination of drop and quest eggs', () => { - const dropNumber = Object.keys(eggs.drops).length; - const questNumber = Object.keys(eggs.quests).length; - const allNumber = Object.keys(eggs.all).length; + let clock; - expect(allNumber).to.be.greaterThan(0); - expect(allNumber).to.equal(dropNumber + questNumber); - }); + afterEach(() => { + if (clock) { + clock.restore(); + } + }); - it('contains basic information about each egg', () => { - each(eggs.all, (egg, key) => { - expectValidTranslationString(egg.text); - expectValidTranslationString(egg.adjective); - expectValidTranslationString(egg.mountText); - expectValidTranslationString(egg.notes); - expect(egg.canBuy).to.be.a('function'); - expect(egg.value).to.be.a('number'); - expect(egg.key).to.equal(key); + const eggTypes = [ + 'drops', + 'quests', + ]; + + eggTypes.forEach(eggType => { + describe(eggType, () => { + it('contains basic information about each egg', () => { + each(eggs[eggType], (egg, key) => { + expectValidTranslationString(egg.text); + expectValidTranslationString(egg.adjective); + expectValidTranslationString(egg.mountText); + expectValidTranslationString(egg.notes); + expect(egg.canBuy).to.be.a('function'); + expect(egg.value).to.be.a('number'); + expect(egg.key).to.equal(key); + }); }); }); }); + + it('does not contain unreleased eggs', () => { + clock = sinon.useFakeTimers(new Date('2024-05-20')); + const questEggs = eggs.quests; + expect(questEggs.Giraffe).to.not.exist; + }); + + it('Releases eggs when appropriate without needing restarting', () => { + clock = sinon.useFakeTimers(new Date('2024-05-20')); + const mayEggs = eggs.quests; + clock.restore(); + clock = sinon.useFakeTimers(new Date('2024-06-20')); + const juneEggs = eggs.quests; + expect(juneEggs.Giraffe).to.exist; + expect(Object.keys(mayEggs).length).to.equal(Object.keys(juneEggs).length - 1); + }); }); diff --git a/test/content/stable.test.js b/test/content/stable.test.js index 3266cb7f47..ca1475d631 100644 --- a/test/content/stable.test.js +++ b/test/content/stable.test.js @@ -7,11 +7,20 @@ import { import t from '../../website/common/script/content/translation'; import * as stable from '../../website/common/script/content/stable'; -import * as eggs from '../../website/common/script/content/eggs'; +import eggs from '../../website/common/script/content/eggs'; import * as potions from '../../website/common/script/content/hatching-potions'; describe('stable', () => { describe('dropPets', () => { + let clock; + beforeEach(() => { + clock = sinon.useFakeTimers(new Date('2020-05-20')); + }); + + afterEach(() => { + clock.restore(); + }); + it('contains a pet for each drop potion * each drop egg', () => { const numberOfDropPotions = Object.keys(potions.drops).length; const numberOfDropEggs = Object.keys(eggs.drops).length; diff --git a/website/common/script/content/constants/release_dates.js b/website/common/script/content/constants/release_dates.js new file mode 100644 index 0000000000..e7edd2b9c8 --- /dev/null +++ b/website/common/script/content/constants/release_dates.js @@ -0,0 +1,15 @@ +export const ARMOIRE_RELEASE_DATES = { + somethingSpooky: { year: 2023, month: 10 }, + cookingImplementsTwo: { year: 2023, month: 11 }, + greenTrapper: { year: 2023, month: 12 }, + schoolUniform: { year: 2024, month: 1 }, + whiteLoungeWear: { year: 2024, month: 2 }, + hatterSet: { year: 2024, month: 3 }, + optimistSet: { year: 2024, month: 4 }, + pottersSet: { year: 2024, month: 5 }, + beachsideSet: { year: 2024, month: 6 }, +}; + +export const EGGS_RELEASE_DATES = { + Giraffe: { year: 2024, month: 6, day: 1 }, +}; diff --git a/website/common/script/content/eggs.js b/website/common/script/content/eggs.js index 0a6e438e5b..65e346dd85 100644 --- a/website/common/script/content/eggs.js +++ b/website/common/script/content/eggs.js @@ -1,7 +1,9 @@ -import assign from 'lodash/assign'; import defaults from 'lodash/defaults'; import each from 'lodash/each'; import t from './translation'; +import { filterReleased } from './is_released'; +import { EGGS_RELEASE_DATES } from './constants/release_dates'; +import datedMemoize from '../fns/datedMemoize'; function applyEggDefaults (set, config) { each(set, (egg, key) => { @@ -410,10 +412,17 @@ applyEggDefaults(quests, { }, }); -const all = assign({}, drops, quests); +function filterEggs (eggs) { + return filterReleased(eggs, 'key', EGGS_RELEASE_DATES); +} -export { - drops, - quests, - all, +const memoizedFilter = datedMemoize(filterEggs); + +export default { + get drops () { + return memoizedFilter({ memoizeConfig: true, identifier: 'drops' }, drops); + }, + get quests () { + return memoizedFilter({ memoizeConfig: true, identifier: 'quests' }, quests); + }, }; diff --git a/website/common/script/content/gear/sets/armoire.js b/website/common/script/content/gear/sets/armoire.js index 8c4084ed9d..635054dfd6 100644 --- a/website/common/script/content/gear/sets/armoire.js +++ b/website/common/script/content/gear/sets/armoire.js @@ -2,12 +2,13 @@ import defaults from 'lodash/defaults'; import find from 'lodash/find'; import forEach from 'lodash/forEach'; import moment from 'moment'; -import nconf from 'nconf'; import upperFirst from 'lodash/upperFirst'; import { ownsItem } from '../gear-helper'; import { ATTRIBUTES } from '../../../constants'; import t from '../../translation'; import memoize from '../../../fns/datedMemoize'; +import { ARMOIRE_RELEASE_DATES as releaseDates } from '../../constants/release_dates'; +import { buildReleaseDate } from '../../is_released'; const armor = { lunarArmor: { @@ -1833,19 +1834,7 @@ const weapon = { }, }; -const SWITCHOVER_TIME = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0; const releaseDay = 7; -const releaseDates = { - somethingSpooky: { year: 2023, month: 10 }, - cookingImplementsTwo: { year: 2023, month: 11 }, - greenTrapper: { year: 2023, month: 12 }, - schoolUniform: { year: 2024, month: 1 }, - whiteLoungeWear: { year: 2024, month: 2 }, - hatterSet: { year: 2024, month: 3 }, - optimistSet: { year: 2024, month: 4 }, - pottersSet: { year: 2024, month: 5 }, - beachsideSet: { year: 2024, month: 6 }, -}; forEach({ armor, @@ -1890,12 +1879,12 @@ forEach({ function updateReleased (type) { const today = moment(); - const releaseDateEndPart = `${String(releaseDay).padStart(2, '0')}T${String(SWITCHOVER_TIME).padStart(2, '0')}:00-0500`; const returnType = {}; forEach(type, (gearItem, gearKey) => { let released; if (releaseDates[gearItem.set]) { - const releaseDateString = `${releaseDates[gearItem.set].year}-${String(releaseDates[gearItem.set].month).padStart(2, '0')}-${releaseDateEndPart}`; + const components = releaseDates[gearItem.set]; + const releaseDateString = buildReleaseDate(components.year, components.month, releaseDay); released = today.isAfter(releaseDateString); } else { released = true; diff --git a/website/common/script/content/index.js b/website/common/script/content/index.js index 0de5f5d5f0..f6ba549919 100644 --- a/website/common/script/content/index.js +++ b/website/common/script/content/index.js @@ -1,5 +1,6 @@ import defaults from 'lodash/defaults'; import each from 'lodash/each'; +import assign from 'lodash/assign'; import moment from 'moment'; import t from './translation'; import { tasksByCategory } from './tasks'; @@ -18,7 +19,7 @@ import { import achievements from './achievements'; -import * as eggs from './eggs'; +import eggs from './eggs'; import * as hatchingPotions from './hatching-potions'; import * as stable from './stable'; import gear from './gear'; @@ -167,7 +168,7 @@ api.special = api.spells.special; api.dropEggs = eggs.drops; api.questEggs = eggs.quests; -api.eggs = eggs.all; +api.eggs = assign({}, eggs.drops, eggs.quests); api.timeTravelStable = { pets: { diff --git a/website/common/script/content/is_released.js b/website/common/script/content/is_released.js new file mode 100644 index 0000000000..e1b4f69cef --- /dev/null +++ b/website/common/script/content/is_released.js @@ -0,0 +1,30 @@ +import moment from 'moment'; +import filter from 'lodash/filter'; +import { pickBy } from 'lodash'; +import nconf from 'nconf'; + +const SWITCHOVER_TIME = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0; + +const releaseDateEndPart = `T${String(SWITCHOVER_TIME).padStart(2, '0')}:00-0000`; + +export function buildReleaseDate (year, month, day = 1) { + return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}${releaseDateEndPart}`; +} + +function isReleased (item, fieldName, releaseDateMap, releaseByDefault) { + if (releaseDateMap[item[fieldName]]) { + const release = releaseDateMap[item[fieldName]]; + if (release.day) { + return moment().isAfter(moment(buildReleaseDate(release.year, release.month, release.day))); + } + return moment().isAfter(releaseDateMap[item[fieldName]]); + } + return releaseByDefault; +} + +export function filterReleased (items, fieldName, releaseDateMap, releaseByDefault = true) { + if (typeof items === 'object') { + return pickBy(items, item => isReleased(item, fieldName, releaseDateMap, releaseByDefault)); + } + return filter(items, item => isReleased(item, fieldName, releaseDateMap, releaseByDefault)); +} diff --git a/website/common/script/content/stable.js b/website/common/script/content/stable.js index e6015f6e77..93d72d482f 100644 --- a/website/common/script/content/stable.js +++ b/website/common/script/content/stable.js @@ -1,10 +1,7 @@ import each from 'lodash/each'; import moment from 'moment'; import { EVENTS } from './constants/events'; -import { - drops as dropEggs, - quests as questEggs, -} from './eggs'; +import allEggs from './eggs'; import { drops as dropPotions, premium as premiumPotions, @@ -12,10 +9,14 @@ import { } from './hatching-potions'; import t from './translation'; +const STABLE_RELEASE_DATES = { + +}; + const petInfo = {}; const mountInfo = {}; -function constructSet (type, eggs, potions) { +function constructSet (type, eggs, potions, hasMounts = true) { const pets = {}; const mounts = {}; @@ -37,52 +38,24 @@ function constructSet (type, eggs, potions) { potion: potion.text, egg: egg.text, })); - mountInfo[key] = getAnimalData(t('mountName', { - potion: potion.text, - mount: egg.mountText, - })); - pets[key] = true; - mounts[key] = true; - }); - }); - return [pets, mounts]; -} - -function constructPetOnlySet (type, eggs, potions) { - const pets = {}; - - each(eggs, egg => { - each(potions, potion => { - const key = `${egg.key}-${potion.key}`; - - function getAnimalData (text) { - return { - key, - type, - potion: potion.key, - egg: egg.key, - text, - }; + if (hasMounts) { + mountInfo[key] = getAnimalData(t('mountName', { + potion: potion.text, + mount: egg.mountText, + })); + mounts[key] = true; } - - petInfo[key] = getAnimalData(t('petName', { - potion: potion.text, - egg: egg.text, - })); - pets[key] = true; }); }); + if (hasMounts) { + return [pets, mounts]; + } return pets; } -const [dropPets, dropMounts] = constructSet('drop', dropEggs, dropPotions); -const [premiumPets, premiumMounts] = constructSet('premium', dropEggs, premiumPotions); -const [questPets, questMounts] = constructSet('quest', questEggs, dropPotions); -const wackyPets = constructPetOnlySet('wacky', dropEggs, wackyPotions); - const canFindSpecial = { pets: { // Veteran Pet Ladder - awarded on major updates @@ -158,6 +131,11 @@ const canFindSpecial = { }, }; +const [dropPets, dropMounts] = constructSet('drop', allEggs.drops, dropPotions); +const [premiumPets, premiumMounts] = constructSet('premium', allEggs.drops, premiumPotions); +const [questPets, questMounts] = constructSet('quest', allEggs.quests, dropPotions); +const wackyPets = constructSet('wacky', allEggs.drops, wackyPotions, false); + const specialPets = { 'Wolf-Veteran': 'veteranWolf', 'Wolf-Cerberus': 'cerberusPup', diff --git a/website/common/script/fns/datedMemoize.js b/website/common/script/fns/datedMemoize.js index 6e91fbc52c..c71ab59248 100644 --- a/website/common/script/fns/datedMemoize.js +++ b/website/common/script/fns/datedMemoize.js @@ -33,6 +33,10 @@ const memoize = fn => { identifier = config.identifier; } } + + if (identifier.length === 0) { + identifier = args.filter(arg => typeof arg === 'string').join('-'); + } } if (!checkedDate) { checkedDate = new Date(); diff --git a/website/common/script/fns/firstDrops.js b/website/common/script/fns/firstDrops.js index cfcd5c933a..7a2413cb97 100644 --- a/website/common/script/fns/firstDrops.js +++ b/website/common/script/fns/firstDrops.js @@ -1,9 +1,9 @@ -import { drops as eggs } from '../content/eggs'; +import allEggs from '../content/eggs'; import { drops as hatchingPotions } from '../content/hatching-potions'; import randomVal from '../libs/randomVal'; export default function firstDrops (user) { - const eggDrop = randomVal(eggs); + const eggDrop = randomVal(allEggs.drops); const potionDrop = randomVal(hatchingPotions); user.items.eggs = {