Squashed commit of the following:

commit 28193f86fb
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Jun 21 11:12:18 2024 +0200

    Fix serving memoized content

commit 877fe48225
Author: Phillip Thelen <phillip@habitica.com>
Date:   Thu Jun 20 12:23:24 2024 +0200

    correctly memoize conent api

commit e0f6f79c5b
Author: Phillip Thelen <phillip@habitica.com>
Date:   Thu Jun 20 10:11:27 2024 +0200

    don’t build multiple times on heroku

commit f62254d68e
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 19:40:20 2024 +0200

    fix client command

commit d054e6fc16
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 19:36:57 2024 +0200

    correct build call

commit 7231f699c1
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 19:32:32 2024 +0200

    try setting up with heroku buildpack

commit 1dae0793fd
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:50:32 2024 +0200

    call gulp build:prod

commit f18fbe86b6
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:40:53 2024 +0200

    build client

commit 61a61724ca
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:33:18 2024 +0200

    testing

commit 93cf30eb18
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:20:25 2024 +0200

    integration fix

commit cff08adcd0
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:13:20 2024 +0200

    specify dev docker file

commit 4da2ed4a1f
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:10:07 2024 +0200

    initialize stub

commit 11c5b26c59
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:08:45 2024 +0200

    test heroku file

commit ac85bb2e2d
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:03:15 2024 +0200

    fix stub reference

commit 74dfb2710f
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:01:27 2024 +0200

    test fixes

commit 8dbd3c3db1
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:37:04 2024 +0200

    fix canOwn test

commit 74b3b348ff
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:32:31 2024 +0200

    fix buy test

commit 3386d61fde
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:30:37 2024 +0200

    fix debug tests

commit 19da14531c
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:05:25 2024 +0200

    add chameleon to featured quests

commit 254dd80f24
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:05:14 2024 +0200

    fix import

commit 0bc3f16b4b
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:33:22 2024 +0200

    add new content to new release file

commit 5184973bd5
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:33:11 2024 +0200

    fix release date tests

commit b6accca5ca
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:33:06 2024 +0200

    fix armoire tests

commit fec68e6211
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:02:03 2024 +0200

    fix tests

commit fc63c906dd
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 10 14:44:21 2024 +0200

    Improve test coverage

commit 3333f8f0f5
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 10 14:24:59 2024 +0200

    allow hatching potions to have a release date

commit 89a3ac3dde
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 10 14:11:38 2024 +0200

    allow eggs to have a release date

    # Conflicts:
    #	test/content/armoire.test.js

commit 16551ec83f
Merge: f5f4974a73 2645bf6023
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 15:03:12 2024 -0400

    Merge branch '2024-07-content-prebuild' into subs-private

commit 2645bf6023
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 15:02:47 2024 -0400

    update habitica images

commit f5f4974a73
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 14:58:13 2024 -0400

    update habitica-images

commit 162e337d14
Merge: f2506c3231 21a7d36b7b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:46:03 2024 -0400

    Merge branch '2024-07-content-prebuild' into subs-private

commit 21a7d36b7b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:45:09 2024 -0400

    update sprites

commit f2506c3231
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:24:21 2024 -0400

    updated sprites css

commit d47641e25a
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 12:46:59 2024 -0400

    typo fix

commit fb8479ad1e
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 17 13:44:36 2024 -0400

    finish July prebuild

commit 3810cf3ef3
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Jun 14 10:42:47 2024 -0400

    add chameleon quest

commit d05da3722c
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 17:12:43 2024 -0400

    add June background notes

commit b8a3440ef2
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 16:40:04 2024 -0400

    fix mystery item and background description

commit 44d63032d8
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 15:38:23 2024 -0400

    add subscriber gear, enchanted armoire, and background

commit 9d7da91ec6
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 14:44:59 2024 -0400

    add sprites
This commit is contained in:
Sabe Jones
2024-06-28 09:49:08 -05:00
parent b46e2da61b
commit 287014518d
31 changed files with 976 additions and 468 deletions

View File

@@ -3,10 +3,13 @@ FROM node:20
# Install global packages # Install global packages
RUN npm install -g gulp-cli mocha RUN npm install -g gulp-cli mocha
# Copy package.json and package-lock.json into image, then install # Copy package.json and package-lock.json into image
# dependencies.
WORKDIR /usr/src/habitica WORKDIR /usr/src/habitica
COPY ["package.json", "package-lock.json", "./"] COPY ["package.json", "package-lock.json", "./"]
# Copy the remaining source files in. # Copy the remaining source files in.
COPY . /usr/src/habitica COPY . /usr/src/habitica
# Install dependencies
RUN npm install RUN npm install
RUN npm run postinstall
RUN npm run client:build
RUN gulp build:prod

View File

@@ -107,7 +107,9 @@
"debug": "gulp nodemon --inspect", "debug": "gulp nodemon --inspect",
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet", "mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install", "postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
"apidoc": "gulp apidoc" "apidoc": "gulp apidoc",
"heroku-postbuild": "npm run client:build"
}, },
"devDependencies": { "devDependencies": {
"axios": "^1.4.0", "axios": "^1.4.0",

View File

@@ -55,7 +55,7 @@ describe('contentLib', () => {
beforeEach(() => { beforeEach(() => {
resSpy = generateRes(); resSpy = generateRes();
if (fs.existsSync(contentLib.CONTENT_CACHE_PATH)) { if (fs.existsSync(contentLib.CONTENT_CACHE_PATH)) {
fs.rmdirSync(contentLib.CONTENT_CACHE_PATH, { recursive: true }); fs.rmSync(contentLib.CONTENT_CACHE_PATH, { recursive: true });
} }
fs.mkdirSync(contentLib.CONTENT_CACHE_PATH); fs.mkdirSync(contentLib.CONTENT_CACHE_PATH);
}); });

View File

@@ -3,6 +3,7 @@ import armoireSet from '../../../website/common/script/content/gear/sets/armoire
describe('armoireSet items', () => { describe('armoireSet items', () => {
it('checks if canOwn has the same id', () => { it('checks if canOwn has the same id', () => {
Object.keys(armoireSet).forEach(type => { Object.keys(armoireSet).forEach(type => {
if (type === 'all') return;
Object.keys(armoireSet[type]).forEach(itemKey => { Object.keys(armoireSet[type]).forEach(itemKey => {
const ownedKey = `${type}_armoire_${itemKey}`; const ownedKey = `${type}_armoire_${itemKey}`;
expect(armoireSet[type][itemKey].canOwn({ expect(armoireSet[type][itemKey].canOwn({

View File

@@ -3,38 +3,26 @@ import forEach from 'lodash/forEach';
import { import {
expectValidTranslationString, expectValidTranslationString,
} from '../helpers/content.helper'; } from '../helpers/content.helper';
import armoire from '../../website/common/script/content/gear/sets/armoire';
function makeArmoireIitemList () {
const armoire = require('../../website/common/script/content/gear/sets/armoire').default;
const items = [];
items.push(...Object.values(armoire.armor));
items.push(...Object.values(armoire.body));
items.push(...Object.values(armoire.eyewear));
items.push(...Object.values(armoire.head));
items.push(...Object.values(armoire.headAccessory));
items.push(...Object.values(armoire.shield));
items.push(...Object.values(armoire.weapon));
return items;
}
describe('armoire', () => { describe('armoire', () => {
let clock; let clock;
beforeEach(() => {
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
});
afterEach(() => { afterEach(() => {
clock.restore(); if (clock) {
clock.restore();
}
}); });
it('does not return unreleased gear', async () => { it('does not return unreleased gear', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-02')); clock = sinon.useFakeTimers(new Date('2024-01-02'));
const items = makeArmoireIitemList(); const items = armoire.all;
expect(items.length).to.equal(377); expect(items.length).to.equal(377);
expect(items.filter(item => item.set === 'pottersSet' || item.set === 'optimistSet' || item.set === 'schoolUniform')).to.be.an('array').that.is.empty; expect(items.filter(item => item.set === 'pottersSet' || item.set === 'optimistSet' || item.set === 'schoolUniform')).to.be.an('array').that.is.empty;
}); });
it('released gear has all required properties', async () => { it('released gear has all required properties', async () => {
clock = sinon.useFakeTimers(new Date('2024-05-08')); clock = sinon.useFakeTimers(new Date('2024-05-08'));
const items = makeArmoireIitemList(); const items = armoire.all;
expect(items.length).to.equal(396); expect(items.length).to.equal(396);
forEach(items, item => { forEach(items, item => {
if (item.set !== undefined) { if (item.set !== undefined) {
@@ -48,29 +36,30 @@ describe('armoire', () => {
it('releases gear when appropriate', async () => { it('releases gear when appropriate', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-01T00:00:00.000Z')); clock = sinon.useFakeTimers(new Date('2024-01-01T00:00:00.000Z'));
const items = makeArmoireIitemList(); const items = armoire.all;
expect(items.length).to.equal(377); expect(items.length).to.equal(377);
clock.restore(); clock.restore();
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')]; delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
clock = sinon.useFakeTimers(new Date('2024-01-08')); clock = sinon.useFakeTimers(new Date('2024-01-08'));
const januaryItems = makeArmoireIitemList(); const januaryItems = armoire.all;
expect(januaryItems.length).to.equal(381); expect(januaryItems.length).to.equal(381);
clock.restore(); clock.restore();
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')]; delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
clock = sinon.useFakeTimers(new Date('2024-02-07')); clock = sinon.useFakeTimers(new Date('2024-02-07'));
const januaryItems2 = makeArmoireIitemList(); const januaryItems2 = armoire.all;
expect(januaryItems2.length).to.equal(381); expect(januaryItems2.length).to.equal(381);
clock.restore(); clock.restore();
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')]; delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
clock = sinon.useFakeTimers(new Date('2024-02-07T16:00:00.000Z')); clock = sinon.useFakeTimers(new Date('2024-02-07T09:00:00.000Z'));
const febuaryItems = makeArmoireIitemList(); const febuaryItems = armoire.all;
expect(febuaryItems.length).to.equal(384); expect(febuaryItems.length).to.equal(384);
}); });
it('sets have at least 2 items', () => { it('sets have at least 2 items', () => {
const armoire = makeArmoireIitemList();
const setMap = {}; const setMap = {};
forEach(armoire, item => { forEach(armoire.all, item => {
// Gotta have one outlier
if (!item.set || item.set.startsWith('armoire-')) return;
if (setMap[item.set] === undefined) { if (setMap[item.set] === undefined) {
setMap[item.set] = 0; setMap[item.set] = 0;
} }

View File

@@ -5,29 +5,51 @@ import {
expectValidTranslationString, expectValidTranslationString,
} from '../helpers/content.helper'; } from '../helpers/content.helper';
import * as eggs from '../../website/common/script/content/eggs'; import eggs from '../../website/common/script/content/eggs';
describe('eggs', () => { describe('eggs', () => {
describe('all', () => { let clock;
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;
expect(allNumber).to.be.greaterThan(0); afterEach(() => {
expect(allNumber).to.equal(dropNumber + questNumber); if (clock) {
}); clock.restore();
}
});
it('contains basic information about each egg', () => { const eggTypes = [
each(eggs.all, (egg, key) => { 'drops',
expectValidTranslationString(egg.text); 'quests',
expectValidTranslationString(egg.adjective); ];
expectValidTranslationString(egg.mountText);
expectValidTranslationString(egg.notes); eggTypes.forEach(eggType => {
expect(egg.canBuy).to.be.a('function'); describe(eggType, () => {
expect(egg.value).to.be.a('number'); it('contains basic information about each egg', () => {
expect(egg.key).to.equal(key); 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);
});
}); });

154
test/content/index.test.js Normal file
View File

@@ -0,0 +1,154 @@
import content from '../../website/common/script/content';
describe('content index', () => {
let clock;
afterEach(() => {
if (clock) {
clock.restore();
}
});
it('Releases eggs when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const mayEggs = content.eggs;
expect(mayEggs.Chameleon).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-07-20'));
const juneEggs = content.eggs;
expect(juneEggs.Chameleon).to.exist;
expect(Object.keys(mayEggs).length, '').to.equal(Object.keys(juneEggs).length - 1);
});
it('Releases hatching potions when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-05-20'));
const mayHatchingPotions = content.hatchingPotions;
expect(mayHatchingPotions.Koi).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const juneHatchingPotions = content.hatchingPotions;
expect(juneHatchingPotions.Koi).to.exist;
expect(Object.keys(mayHatchingPotions).length, '').to.equal(Object.keys(juneHatchingPotions).length - 1);
});
it('Releases armoire gear when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const juneGear = content.gear.flat;
expect(juneGear.armor_armoire_corsairsCoatAndCape).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-07-10'));
const julyGear = content.gear.flat;
expect(julyGear.armor_armoire_corsairsCoatAndCape).to.exist;
expect(Object.keys(juneGear).length, '').to.equal(Object.keys(julyGear).length - 3);
});
it('Releases pets gear when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const junePets = content.petInfo;
expect(junePets['Chameleon-Base']).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-07-10'));
const julyPets = content.petInfo;
expect(julyPets['Chameleon-Base']).to.exist;
expect(Object.keys(junePets).length, '').to.equal(Object.keys(julyPets).length - 10);
});
it('Releases mounts gear when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const juneMounts = content.mountInfo;
expect(juneMounts['Chameleon-Base']).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-07-10'));
const julyMounts = content.mountInfo;
expect(julyMounts['Chameleon-Base']).to.exist;
expect(Object.keys(juneMounts).length, '').to.equal(Object.keys(julyMounts).length - 10);
});
it('marks regular food as buyable and droppable without any events', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const { food } = content;
Object.keys(food).forEach(key => {
if (key === 'Saddle') {
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
return;
}
let expected = true;
if (key.startsWith('Cake_')) {
expected = false;
} else if (key.startsWith('Candy_')) {
expected = false;
} else if (key.startsWith('Pie_')) {
expected = false;
}
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
});
});
it('marks candy as buyable and droppable during habitoween', () => {
clock = sinon.useFakeTimers(new Date('2024-10-31'));
const { food } = content;
Object.keys(food).forEach(key => {
if (key === 'Saddle') {
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
return;
}
let expected = false;
if (key.startsWith('Cake_')) {
expected = false;
} else if (key.startsWith('Candy_')) {
expected = true;
} else if (key.startsWith('Pie_')) {
expected = false;
}
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
});
});
it('marks cake as buyable and droppable during birthday', () => {
clock = sinon.useFakeTimers(new Date('2024-01-31'));
const { food } = content;
Object.keys(food).forEach(key => {
if (key === 'Saddle') {
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
return;
}
let expected = false;
if (key.startsWith('Cake_')) {
expected = true;
} else if (key.startsWith('Candy_')) {
expected = false;
} else if (key.startsWith('Pie_')) {
expected = false;
}
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
});
});
it('marks pie as buyable and droppable during pi day', () => {
clock = sinon.useFakeTimers(new Date('2024-03-14'));
const { food } = content;
Object.keys(food).forEach(key => {
if (key === 'Saddle') {
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
return;
}
let expected = false;
if (key.startsWith('Cake_')) {
expected = false;
} else if (key.startsWith('Candy_')) {
expected = false;
} else if (key.startsWith('Pie_')) {
expected = true;
}
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
});
});
});

View File

@@ -0,0 +1,82 @@
import find from 'lodash/find';
import maxBy from 'lodash/maxBy';
import {
ARMOIRE_RELEASE_DATES,
EGGS_RELEASE_DATES,
HATCHING_POTIONS_RELEASE_DATES,
} from '../../website/common/script/content/constants/releaseDates';
import armoire from '../../website/common/script/content/gear/sets/armoire';
import eggs from '../../website/common/script/content/eggs';
import hatchingPotions from '../../website/common/script/content/hatching-potions';
describe('releaseDates', () => {
let clock;
afterEach(() => {
if (clock) {
clock.restore();
}
});
describe('armoire', () => {
it('should only contain valid armoire names', () => {
const lastReleaseDate = maxBy(Object.values(ARMOIRE_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-20`));
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-20`));
Object.keys(ARMOIRE_RELEASE_DATES).forEach(key => {
expect(find(armoire.all, { set: key }), `${key} is not a valid armoire set`).to.exist;
});
});
it('should contain a valid year and month', () => {
Object.keys(ARMOIRE_RELEASE_DATES).forEach(key => {
const date = ARMOIRE_RELEASE_DATES[key];
expect(date.year, `${key} year is not a valid year`).to.be.a('number');
expect(date.year).to.be.at.least(2023);
expect(date.month, `${key} month is not a valid month`).to.be.a('number');
expect(date.month).to.be.within(1, 12);
expect(date.day).to.not.exist;
});
});
});
describe('eggs', () => {
it('should only contain valid egg names', () => {
const lastReleaseDate = maxBy(Object.values(EGGS_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-${value.day}`));
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-${lastReleaseDate.day}`));
Object.keys(EGGS_RELEASE_DATES).forEach(key => {
expect(eggs.all[key], `${key} is not a valid egg name`).to.exist;
});
});
it('should contain a valid year, month and date', () => {
Object.keys(EGGS_RELEASE_DATES).forEach(key => {
const date = EGGS_RELEASE_DATES[key];
expect(date.year, `${key} year is not a valid year`).to.be.a('number');
expect(date.year).to.be.at.least(2024);
expect(date.month, `${key} month is not a valid month`).to.be.a('number');
expect(date.month).to.be.within(1, 12);
expect(date.day, `${key} day is not a valid day`).to.be.a('number');
});
});
});
describe('hatchingPotions', () => {
it('should only contain valid potion names', () => {
const lastReleaseDate = maxBy(Object.values(HATCHING_POTIONS_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-${value.day}`));
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-${lastReleaseDate.day}`));
Object.keys(HATCHING_POTIONS_RELEASE_DATES).forEach(key => {
expect(hatchingPotions.all[key], `${key} is not a valid potion name`).to.exist;
});
});
it('should contain a valid year, month and date', () => {
Object.keys(HATCHING_POTIONS_RELEASE_DATES).forEach(key => {
const date = HATCHING_POTIONS_RELEASE_DATES[key];
expect(date.year, `${key} year is not a valid year`).to.be.a('number');
expect(date.year).to.be.at.least(2024);
expect(date.month, `${key} month is not a valid month`).to.be.a('number');
expect(date.month).to.be.within(1, 12);
expect(date.day, `${key} day is not a valid day`).to.be.a('number');
});
});
});
});

View File

@@ -6,12 +6,21 @@ import {
} from '../helpers/content.helper'; } from '../helpers/content.helper';
import t from '../../website/common/script/content/translation'; import t from '../../website/common/script/content/translation';
import * as stable from '../../website/common/script/content/stable'; import 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'; import potions from '../../website/common/script/content/hatching-potions';
describe('stable', () => { describe('stable', () => {
describe('dropPets', () => { 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', () => { it('contains a pet for each drop potion * each drop egg', () => {
const numberOfDropPotions = Object.keys(potions.drops).length; const numberOfDropPotions = Object.keys(potions.drops).length;
const numberOfDropEggs = Object.keys(eggs.drops).length; const numberOfDropEggs = Object.keys(eggs.drops).length;

View File

@@ -320,7 +320,7 @@
<script> <script>
import each from 'lodash/each'; import each from 'lodash/each';
import * as quests from '@/../../common/script/content/quests'; import * as quests from '@/../../common/script/content/quests';
import { mountInfo, petInfo } from '@/../../common/script/content/stable'; import stable from '@/../../common/script/content/stable';
import content from '@/../../common/script/content'; import content from '@/../../common/script/content';
import gear from '@/../../common/script/content/gear'; import gear from '@/../../common/script/content/gear';
import styleHelper from '@/mixins/styleHelper'; import styleHelper from '@/mixins/styleHelper';
@@ -330,6 +330,8 @@ import userLink from '../userLink';
import PurchaseHistoryTable from '../ui/purchaseHistoryTable.vue'; import PurchaseHistoryTable from '../ui/purchaseHistoryTable.vue';
import { userStateMixin } from '../../mixins/userState'; import { userStateMixin } from '../../mixins/userState';
const { mountInfo, petInfo } = stable;
export default { export default {
components: { components: {
userLink, userLink,

View File

@@ -80,7 +80,7 @@
</style> </style>
<script> <script>
import { mountInfo } from '@/../../common/script/content/stable'; import stable from '@/../../common/script/content/stable';
import markdownDirective from '@/directives/markdown'; import markdownDirective from '@/directives/markdown';
export default { export default {
@@ -105,7 +105,7 @@ export default {
}, },
methods: { methods: {
openDialog (mountKey) { openDialog (mountKey) {
this.mount = mountInfo[mountKey]; this.mount = stable.mountInfo[mountKey];
this.$root.$emit('bv::show::modal', 'mount-raised-modal'); this.$root.$emit('bv::show::modal', 'mount-raised-modal');
}, },
close () { close () {

View File

@@ -586,8 +586,8 @@ import reduce from 'lodash/reduce';
import moment from 'moment'; import moment from 'moment';
import planGemLimits from '@/../../common/script/libs/planGemLimits'; import planGemLimits from '@/../../common/script/libs/planGemLimits';
import { drops as dropEggs } from '@/../../common/script/content/eggs'; import eggs from '@/../../common/script/content/eggs';
import { drops as dropPotions } from '@/../../common/script/content/hatching-potions'; import hatchingPotions from '@/../../common/script/content/hatching-potions';
import { avatarEditorUtilities } from '@/mixins/avatarEditUtilities'; import { avatarEditorUtilities } from '@/mixins/avatarEditUtilities';
import numberInvalid from '@/mixins/numberInvalid'; import numberInvalid from '@/mixins/numberInvalid';
import spellsMixin from '@/mixins/spells'; import spellsMixin from '@/mixins/spells';
@@ -617,6 +617,9 @@ import EquipmentAttributesGrid from '../inventory/equipment/attributesGrid.vue';
import Item from '@/components/inventory/item'; import Item from '@/components/inventory/item';
import Avatar from '@/components/avatar'; import Avatar from '@/components/avatar';
const dropEggs = eggs.drops;
const dropPotions = hatchingPotions.drops;
const dropEggKeys = keys(dropEggs); const dropEggKeys = keys(dropEggs);
const amountOfDropEggs = size(dropEggs); const amountOfDropEggs = size(dropEggs);

View File

@@ -983,6 +983,10 @@
"backgroundShellGateText": "Shell Gate", "backgroundShellGateText": "Shell Gate",
"backgroundShellGateNotes": "March through the decorated coral of a Shell Gate.", "backgroundShellGateNotes": "March through the decorated coral of a Shell Gate.",
"backgrounds072024": "SET 122: Released July 2024",
"backgroundRiverBottomText": "River Bottom",
"backgroundRiverBottomNotes": "Explore a River Bottom.",
"timeTravelBackgrounds": "Steampunk Backgrounds", "timeTravelBackgrounds": "Steampunk Backgrounds",
"backgroundAirshipText": "Airship", "backgroundAirshipText": "Airship",
"backgroundAirshipNotes": "Become a sky sailor on board your very own Airship.", "backgroundAirshipNotes": "Become a sky sailor on board your very own Airship.",

View File

@@ -255,6 +255,10 @@
"questEggGiraffeMountText": "Giraffe", "questEggGiraffeMountText": "Giraffe",
"questEggGiraffeAdjective": "a towering", "questEggGiraffeAdjective": "a towering",
"questEggChameleonText": "Chameleon",
"questEggChameleonMountText": "Chameleon",
"questEggChameleonAdjective": "a chaotic",
"eggNotes": "Find a hatching potion to pour on this egg, and it will hatch into <%= eggAdjective(locale) %> <%= eggText(locale) %>.", "eggNotes": "Find a hatching potion to pour on this egg, and it will hatch into <%= eggAdjective(locale) %> <%= eggText(locale) %>.",
"hatchingPotionBase": "Base", "hatchingPotionBase": "Base",

View File

@@ -770,6 +770,8 @@
"weaponArmoirePottersWheelNotes": "Throw some clay on this wheel and make a bowl or a mug or a vase or a slightly different bowl. If you're lucky, a ghost might visit while you create! Increases Perception by <%= per %>. Enchanted Armoire: Potter Set (Item 4 of 4).", "weaponArmoirePottersWheelNotes": "Throw some clay on this wheel and make a bowl or a mug or a vase or a slightly different bowl. If you're lucky, a ghost might visit while you create! Increases Perception by <%= per %>. Enchanted Armoire: Potter Set (Item 4 of 4).",
"weaponArmoireShadyBeachUmbrellaText": "Beach Umbrella", "weaponArmoireShadyBeachUmbrellaText": "Beach Umbrella",
"weaponArmoireShadyBeachUmbrellaNotes": "The shade of this rainbow-colored umbrella conceals you briefly from the day star and any unwanted bothers. Increases Perception by <%= per %>. Enchanted Armoire: Beachside Set (Item 3 of 4).", "weaponArmoireShadyBeachUmbrellaNotes": "The shade of this rainbow-colored umbrella conceals you briefly from the day star and any unwanted bothers. Increases Perception by <%= per %>. Enchanted Armoire: Beachside Set (Item 3 of 4).",
"weaponArmoireCorsairsBladeText": "Corsairs Blade",
"weaponArmoireCorsairsBladeNotes": "Whether you wield it to plunder or to protect, you can be glad you brought this fierce blade to sea with you. Just be sure to stow it safely when not in use. Increases Strength by <%= str %>. Enchanted Armoire: Corsair Set (Item 3 of 3)",
"armor": "armor", "armor": "armor",
"armorCapitalized": "Armor", "armorCapitalized": "Armor",
@@ -1400,6 +1402,8 @@
"armorMystery202401Notes": "These robes appear as delicate as crystal snowflakes, but will keep you plenty warm as you work your wintry magic. Confers no benefit. January 2024 Subscriber Item.", "armorMystery202401Notes": "These robes appear as delicate as crystal snowflakes, but will keep you plenty warm as you work your wintry magic. Confers no benefit. January 2024 Subscriber Item.",
"armorMystery202406Text": "Phantom Buccaneers Attire", "armorMystery202406Text": "Phantom Buccaneers Attire",
"armorMystery202406Notes": "Haunt your enemies with style and flair! Confers no benefit. June 2024 Subscriber Item.", "armorMystery202406Notes": "Haunt your enemies with style and flair! Confers no benefit. June 2024 Subscriber Item.",
"armorMystery202407Text": "Amiable Axolotl Suit",
"armorMystery202407Notes": "Glide through lakes and canals with your sweeping pink tail!",
"armorMystery301404Text": "Steampunk Suit", "armorMystery301404Text": "Steampunk Suit",
"armorMystery301404Notes": "Dapper and dashing, wot! Confers no benefit. February 3015 Subscriber Item.", "armorMystery301404Notes": "Dapper and dashing, wot! Confers no benefit. February 3015 Subscriber Item.",
@@ -1616,6 +1620,8 @@
"armorArmoireYellowStripedSwimsuitNotes": "What could be more delightful than battling sea monsters on the beach? Increases Constitution by <%= con %>. Enchanted Armoire: Beachside Set (Item 1 of 4).", "armorArmoireYellowStripedSwimsuitNotes": "What could be more delightful than battling sea monsters on the beach? Increases Constitution by <%= con %>. Enchanted Armoire: Beachside Set (Item 1 of 4).",
"armorArmoireBlueStripedSwimsuitText": "Blue Striped Swimsuit", "armorArmoireBlueStripedSwimsuitText": "Blue Striped Swimsuit",
"armorArmoireBlueStripedSwimsuitNotes": "What could be more exciting than battling sea monsters on the beach? Increases Constitution by <%= con %>. Enchanted Armoire: Beachside Set (Item 2 of 4).", "armorArmoireBlueStripedSwimsuitNotes": "What could be more exciting than battling sea monsters on the beach? Increases Constitution by <%= con %>. Enchanted Armoire: Beachside Set (Item 2 of 4).",
"armorArmoireCorsairsCoatAndCapeText": "Corsairs Coat and Cape",
"armorArmoireCorsairsCoatAndCapeNotes": "Whether youre biding your time on the docks or watching for danger on the open seas, these will surely keep you feeling dry and looking dramatic. Just keep your balance on deck. Increases Constitution by <%= con %>. Enchanted Armoire: Corsair Set (Item 1 of 3)",
"headgear": "helm", "headgear": "helm",
"headgearCapitalized": "Headgear", "headgearCapitalized": "Headgear",
@@ -2279,6 +2285,8 @@
"headMystery202404Notes": "This hat will connect you with the earth and allow you to hear secret wishes from many creatures. Confers no benefit. April 2024 Subscriber Item.", "headMystery202404Notes": "This hat will connect you with the earth and allow you to hear secret wishes from many creatures. Confers no benefit. April 2024 Subscriber Item.",
"headMystery202406Text": "Phantom Buccaneers Hat", "headMystery202406Text": "Phantom Buccaneers Hat",
"headMystery202406Notes": "The ghostly feathers that adorn this hat glow faintly, like the waves of a spectral sea. Confers no benefit. June 2024 Subscriber Item.", "headMystery202406Notes": "The ghostly feathers that adorn this hat glow faintly, like the waves of a spectral sea. Confers no benefit. June 2024 Subscriber Item.",
"headMystery202407Text": "Amiable Axolotl Hood",
"headMystery202407Notes": "These magical gills will let you breathe underwater!",
"headMystery301404Text": "Fancy Top Hat", "headMystery301404Text": "Fancy Top Hat",
"headMystery301404Notes": "A fancy top hat for the finest of gentlefolk! January 3015 Subscriber Item. Confers no benefit.", "headMystery301404Notes": "A fancy top hat for the finest of gentlefolk! January 3015 Subscriber Item. Confers no benefit.",
@@ -2475,6 +2483,8 @@
"headArmoireHattersTopHatNotes": "Our hats are off to you, and yours is on! Whats hidden in your hat is anybodys guess (but were hoping its a bunny). Increases Perception by <%= per %>. Enchanted Armoire: Hatter Set (Item 1 of 4).", "headArmoireHattersTopHatNotes": "Our hats are off to you, and yours is on! Whats hidden in your hat is anybodys guess (but were hoping its a bunny). Increases Perception by <%= per %>. Enchanted Armoire: Hatter Set (Item 1 of 4).",
"headArmoirePottersBandanaText": "Bandana", "headArmoirePottersBandanaText": "Bandana",
"headArmoirePottersBandanaNotes": "Look the part and keep your hair out of your face while you work. Its a win-win! Increases Intelligence by <%= int %>. Enchanted Armoire: Potter Set (Item 2 of 4).", "headArmoirePottersBandanaNotes": "Look the part and keep your hair out of your face while you work. Its a win-win! Increases Intelligence by <%= int %>. Enchanted Armoire: Potter Set (Item 2 of 4).",
"headArmoireCorsairsBandanaText": "Corsairs Bandana",
"headArmoireCorsairsBandanaNotes": "Whether youre keeping your head covered in case a seagull flies overhead or making sure your foes never see you sweat, this bandana is essential. Just add a decorative bead for every adventure you complete. Increases Intelligence by <%= int %>. Enchanted Armoire: Corsair Set (Item 2 of 3)",
"offhand": "off-hand item", "offhand": "off-hand item",
"offHandCapitalized": "Off-Hand Item", "offHandCapitalized": "Off-Hand Item",

View File

@@ -895,7 +895,13 @@
"questGiraffeBoss": "Gear-affe", "questGiraffeBoss": "Gear-affe",
"questGiraffeDropGiraffeEgg": "Giraffe (Egg)", "questGiraffeDropGiraffeEgg": "Giraffe (Egg)",
"QuestGiraffeUnlockText": "Unlocks Giraffe Eggs for purchase in the Market.", "QuestGiraffeUnlockText": "Unlocks Giraffe Eggs for purchase in the Market.",
"questPinkMarbleUnlockText": "Unlocks Pink Marble Hatching Potions for purchase in the Market.",
"questChameleonText": "The Chaotic Chameleon",
"questChameleonNotes": "Its a beautiful day in a warm, rainy corner of the Taskwoods. Youre on the hunt for new additions to your leaf collection when a branch in front of you changes color without warning! Then it moves!<br><br>Stumbling backwards, you realize this is not a branch at all, but a huge chameleon! Each part of his body keeps changing colors as his eyes dart in different directions.<br><br>“Are you all right?” you ask the chameleon.<br><br>“Ahhh, well,” he says, looking a little flustered. “Ive been trying to blend in… but its so overwhelming… the colors keep coming and going! Its hard to focus on just one....”<br><br>“Aha,” you say, “I think I can help. Well sharpen your focus with a little challenge! Get your colors ready!”<br><br>“Youre on!” replied the chameleon.",
"questChameleonCompletion": "After a few lively turns the Chameleon went through every color of the rainbow, perfectly matching each color you requested.<br><br>“Wow,” he says, “working together and making it into a game really helped me concentrate! Please take these as a reward, youre earned them! Teach these little guys how to change all the colors of the rainbow when they hatch.”",
"questChameleonBoss": "Chaotic Chameleon",
"questChameleonDropChameleonEgg": "Chameleon (Egg)",
"QuestChameleonUnlockText": "Unlocks Chameleon Eggs for purchase in the Market",
"questFungiText": "The Moody Mushroom", "questFungiText": "The Moody Mushroom",
"questFungiNotes": "Its been a rainy spring in Habitica and the ground around the stables is spongy and damp. You notice quite a few mushrooms have appeared along the wooden stable walls and fences. Theres a fog hanging about, not quite letting the sun peek through, and its a bit dispiriting.<br><br>Out of the mist you see the outline of the April Fool, not at all his usual bouncy self.<br><br>”Id hoped to bring you all some delightful Fungi Magic Hatching Potions so that you can keep your mushroom friends from my special day forever,” he says, his expression alarmingly unsmiling. “But this cold fog is really getting to me, its making me feel too tired and dismal to work my usual magic.”<br><br>“Oh no, sorry to hear that,” you say, noticing your own increasingly somber mood. “This fog is really making the day gloomy. I wonder where it came from…”<br><br>A low rumble sounds across the fields, and you see an outline emerging from the mist. Youre alarmed to see a gigantic and unhappy looking mushroom creature, and the mist appears to be emanating from it.<br><br>“Aha,” says the Fool, “I think this fungal fellow may be the source of our blues. Lets see if we can summon a little cheer for our friend here and ourselves.”", "questFungiNotes": "Its been a rainy spring in Habitica and the ground around the stables is spongy and damp. You notice quite a few mushrooms have appeared along the wooden stable walls and fences. Theres a fog hanging about, not quite letting the sun peek through, and its a bit dispiriting.<br><br>Out of the mist you see the outline of the April Fool, not at all his usual bouncy self.<br><br>”Id hoped to bring you all some delightful Fungi Magic Hatching Potions so that you can keep your mushroom friends from my special day forever,” he says, his expression alarmingly unsmiling. “But this cold fog is really getting to me, its making me feel too tired and dismal to work my usual magic.”<br><br>“Oh no, sorry to hear that,” you say, noticing your own increasingly somber mood. “This fog is really making the day gloomy. I wonder where it came from…”<br><br>A low rumble sounds across the fields, and you see an outline emerging from the mist. Youre alarmed to see a gigantic and unhappy looking mushroom creature, and the mist appears to be emanating from it.<br><br>“Aha,” says the Fool, “I think this fungal fellow may be the source of our blues. Lets see if we can summon a little cheer for our friend here and ourselves.”",

View File

@@ -163,6 +163,7 @@
"mysterySet202404": "Mycelial Magus Set", "mysterySet202404": "Mycelial Magus Set",
"mysterySet202405": "Gilded Dragon Set", "mysterySet202405": "Gilded Dragon Set",
"mysterySet202406": "Phantom Buccaneer Set", "mysterySet202406": "Phantom Buccaneer Set",
"mysterySet202407": "Amiable Axolotl Set",
"mysterySet301404": "Steampunk Standard Set", "mysterySet301404": "Steampunk Standard Set",
"mysterySet301405": "Steampunk Accessories Set", "mysterySet301405": "Steampunk Accessories Set",
"mysterySet301703": "Peacock Steampunk Set", "mysterySet301703": "Peacock Steampunk Set",

View File

@@ -623,6 +623,9 @@ const backgrounds = {
backgrounds062024: { backgrounds062024: {
shell_gate: { }, shell_gate: { },
}, },
backgrounds072024: {
river_bottom: { },
},
eventBackgrounds: { eventBackgrounds: {
birthday_bash: { birthday_bash: {
price: 0, price: 0,

View File

@@ -0,0 +1,21 @@
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 },
corsairSet: { year: 2024, month: 7 },
};
export const EGGS_RELEASE_DATES = {
Giraffe: { year: 2024, month: 6, day: 1 },
Chameleon: { year: 2024, month: 7, day: 1 },
};
export const HATCHING_POTIONS_RELEASE_DATES = {
Koi: { year: 2024, month: 6, day: 1 },
};

View File

@@ -377,6 +377,7 @@ export const MONTHLY_SCHEDULE = {
'dilatory_derby', 'dilatory_derby',
'armadillo', 'armadillo',
'guineapig', 'guineapig',
'chameleon',
], ],
}, },
{ {

View File

@@ -1,7 +1,10 @@
import assign from 'lodash/assign';
import defaults from 'lodash/defaults'; import defaults from 'lodash/defaults';
import each from 'lodash/each'; import each from 'lodash/each';
import assign from 'lodash/assign';
import t from './translation'; import t from './translation';
import { filterReleased } from './is_released';
import { EGGS_RELEASE_DATES } from './constants/releaseDates';
import datedMemoize from '../fns/datedMemoize';
function applyEggDefaults (set, config) { function applyEggDefaults (set, config) {
each(set, (egg, key) => { each(set, (egg, key) => {
@@ -396,6 +399,12 @@ const quests = {
adjective: t('questEggGiraffeAdjective'), adjective: t('questEggGiraffeAdjective'),
canBuy: hasQuestAchievementFunction('giraffe'), canBuy: hasQuestAchievementFunction('giraffe'),
}, },
Chameleon: {
text: t('questEggChameleonText'),
mountText: t('questEggChameleonMountText'),
adjective: t('questEggChameleonAdjective'),
canBuy: hasQuestAchievementFunction('chameleon'),
},
}; };
applyEggDefaults(drops, { applyEggDefaults(drops, {
@@ -410,10 +419,20 @@ applyEggDefaults(quests, {
}, },
}); });
const all = assign({}, drops, quests); function filterEggs (eggs) {
return filterReleased(eggs, 'key', EGGS_RELEASE_DATES);
}
export { const memoizedFilter = datedMemoize(filterEggs);
drops,
quests, export default {
all, get drops () {
return memoizedFilter({ memoizeConfig: true, identifier: 'drops' }, drops);
},
get quests () {
return memoizedFilter({ memoizeConfig: true, identifier: 'quests' }, quests);
},
get all () {
return assign({}, this.drops, this.quests);
},
}; };

View File

@@ -2,12 +2,13 @@ import defaults from 'lodash/defaults';
import find from 'lodash/find'; import find from 'lodash/find';
import forEach from 'lodash/forEach'; import forEach from 'lodash/forEach';
import moment from 'moment'; import moment from 'moment';
import nconf from 'nconf';
import upperFirst from 'lodash/upperFirst'; import upperFirst from 'lodash/upperFirst';
import { ownsItem } from '../gear-helper'; import { ownsItem } from '../gear-helper';
import { ATTRIBUTES } from '../../../constants'; import { ATTRIBUTES } from '../../../constants';
import t from '../../translation'; import t from '../../translation';
import memoize from '../../../fns/datedMemoize'; import memoize from '../../../fns/datedMemoize';
import { ARMOIRE_RELEASE_DATES as releaseDates } from '../../constants/releaseDates';
import { buildReleaseDate } from '../../is_released';
const armor = { const armor = {
lunarArmor: { lunarArmor: {
@@ -485,6 +486,10 @@ const armor = {
con: 13, con: 13,
set: 'beachsideSet', set: 'beachsideSet',
}, },
corsairsCoatAndCape: {
con: 14,
set: 'corsairSet',
},
}; };
const body = { const body = {
@@ -994,6 +999,10 @@ const head = {
int: 8, int: 8,
set: 'pottersSet', set: 'pottersSet',
}, },
corsairsBandana: {
int: 7,
set: 'corsairSet',
},
}; };
const shield = { const shield = {
@@ -1831,21 +1840,13 @@ const weapon = {
per: 12, per: 12,
set: 'beachsideSet', set: 'beachsideSet',
}, },
corsairsBlade: {
str: 7,
set: 'corsairSet',
},
}; };
const SWITCHOVER_TIME = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0;
const releaseDay = 7; 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({ forEach({
armor, armor,
@@ -1890,12 +1891,12 @@ forEach({
function updateReleased (type) { function updateReleased (type) {
const today = moment(); const today = moment();
const releaseDateEndPart = `${String(releaseDay).padStart(2, '0')}T${String(SWITCHOVER_TIME).padStart(2, '0')}:00-0500`;
const returnType = {}; const returnType = {};
forEach(type, (gearItem, gearKey) => { forEach(type, (gearItem, gearKey) => {
let released; let released;
if (releaseDates[gearItem.set]) { 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); released = today.isAfter(releaseDateString);
} else { } else {
released = true; released = true;
@@ -1931,4 +1932,16 @@ export default {
get weapon () { get weapon () {
return memoizedUpdatReleased({ identifier: 'weapon', memoizeConfig: true }, weapon); return memoizedUpdatReleased({ identifier: 'weapon', memoizeConfig: true }, weapon);
}, },
// convenience method for tests mostly. Not used in the app
get all () {
const items = [];
items.push(...Object.values(this.armor));
items.push(...Object.values(this.body));
items.push(...Object.values(this.eyewear));
items.push(...Object.values(this.head));
items.push(...Object.values(this.headAccessory));
items.push(...Object.values(this.shield));
items.push(...Object.values(this.weapon));
return items;
},
}; };

View File

@@ -66,6 +66,7 @@ const armor = {
202310: { }, 202310: { },
202401: { }, 202401: { },
202406: { }, 202406: { },
202407: { },
301404: { }, 301404: { },
301703: { }, 301703: { },
301704: { }, 301704: { },
@@ -226,6 +227,7 @@ const head = {
202403: { }, 202403: { },
202404: { }, 202404: { },
202406: { }, 202406: { },
202407: { },
301404: { }, 301404: { },
301405: { }, 301405: { },
301703: { }, 301703: { },

View File

@@ -2,6 +2,9 @@ import defaults from 'lodash/defaults';
import each from 'lodash/each'; import each from 'lodash/each';
import { assign } from 'lodash'; import { assign } from 'lodash';
import t from './translation'; import t from './translation';
import datedMemoize from '../fns/datedMemoize';
import { filterReleased } from './is_released';
import { HATCHING_POTIONS_RELEASE_DATES } from './constants/releaseDates';
function hasQuestAchievementFunction (key) { function hasQuestAchievementFunction (key) {
return user => user.achievements.quests && user.achievements.quests[key] > 0; return user => user.achievements.quests && user.achievements.quests[key] > 0;
@@ -194,8 +197,23 @@ each(wacky, (pot, key) => {
}); });
}); });
const all = assign({}, drops, premium, wacky); function filterEggs (eggs) {
return filterReleased(eggs, 'key', HATCHING_POTIONS_RELEASE_DATES);
}
export { const memoizedFilter = datedMemoize(filterEggs);
drops, premium, wacky, all,
export default {
get drops () {
return memoizedFilter({ memoizeConfig: true, identifier: 'drops' }, drops);
},
get premium () {
return memoizedFilter({ memoizeConfig: true, identifier: 'premium' }, premium);
},
get wacky () {
return memoizedFilter({ memoizeConfig: true, identifier: 'wacky' }, wacky);
},
get all () {
return assign({}, this.drops, this.premium, this.wacky);
},
}; };

View File

@@ -18,9 +18,9 @@ import {
import achievements from './achievements'; import achievements from './achievements';
import * as eggs from './eggs'; import eggs from './eggs';
import * as hatchingPotions from './hatching-potions'; import hatchingPotions from './hatching-potions';
import * as stable from './stable'; import stable from './stable';
import gear from './gear'; import gear from './gear';
import { quests, questsByLevel, userCanOwnQuestCategories } from './quests'; import { quests, questsByLevel, userCanOwnQuestCategories } from './quests';
@@ -38,6 +38,7 @@ import { REPEATING_EVENTS, getRepeatingEvents } from './constants/events';
import loginIncentives from './loginIncentives'; import loginIncentives from './loginIncentives';
import officialPinnedItems from './officialPinnedItems'; import officialPinnedItems from './officialPinnedItems';
import memoize from '../fns/datedMemoize';
const api = {}; const api = {};
@@ -165,9 +166,18 @@ api.cardTypes = {
api.special = api.spells.special; api.special = api.spells.special;
api.dropEggs = eggs.drops; Object.defineProperty(api, 'dropEggs', {
api.questEggs = eggs.quests; get () { return eggs.drops; },
api.eggs = eggs.all; enumerable: true,
});
Object.defineProperty(api, 'questEggs', {
get () { return eggs.quests; },
enumerable: true,
});
Object.defineProperty(api, 'eggs', {
get () { return eggs.all; },
enumerable: true,
});
api.timeTravelStable = { api.timeTravelStable = {
pets: { pets: {
@@ -186,298 +196,354 @@ api.timeTravelStable = {
}, },
}; };
api.dropHatchingPotions = hatchingPotions.drops; Object.defineProperty(api, 'dropHatchingPotions', {
api.premiumHatchingPotions = hatchingPotions.premium; get () { return hatchingPotions.drops; },
api.wackyHatchingPotions = hatchingPotions.wacky; enumerable: true,
api.hatchingPotions = hatchingPotions.all;
api.pets = stable.dropPets;
api.premiumPets = stable.premiumPets;
api.questPets = stable.questPets;
api.specialPets = stable.specialPets;
api.wackyPets = stable.wackyPets;
api.petInfo = stable.petInfo;
api.mounts = stable.dropMounts;
api.questMounts = stable.questMounts;
api.premiumMounts = stable.premiumMounts;
api.specialMounts = stable.specialMounts;
api.mountInfo = stable.mountInfo;
api.food = {
Meat: {
text: t('foodMeat'),
textA: t('foodMeatA'),
textThe: t('foodMeatThe'),
target: 'Base',
},
Milk: {
text: t('foodMilk'),
textA: t('foodMilkA'),
textThe: t('foodMilkThe'),
target: 'White',
},
Potatoe: {
text: t('foodPotatoe'),
textA: t('foodPotatoeA'),
textThe: t('foodPotatoeThe'),
target: 'Desert',
},
Strawberry: {
text: t('foodStrawberry'),
textA: t('foodStrawberryA'),
textThe: t('foodStrawberryThe'),
target: 'Red',
},
Chocolate: {
text: t('foodChocolate'),
textA: t('foodChocolateA'),
textThe: t('foodChocolateThe'),
target: 'Shade',
},
Fish: {
text: t('foodFish'),
textA: t('foodFishA'),
textThe: t('foodFishThe'),
target: 'Skeleton',
},
RottenMeat: {
text: t('foodRottenMeat'),
textA: t('foodRottenMeatA'),
textThe: t('foodRottenMeatThe'),
target: 'Zombie',
},
CottonCandyPink: {
text: t('foodCottonCandyPink'),
textA: t('foodCottonCandyPinkA'),
textThe: t('foodCottonCandyPinkThe'),
target: 'CottonCandyPink',
},
CottonCandyBlue: {
text: t('foodCottonCandyBlue'),
textA: t('foodCottonCandyBlueA'),
textThe: t('foodCottonCandyBlueThe'),
target: 'CottonCandyBlue',
},
Honey: {
text: t('foodHoney'),
textA: t('foodHoneyA'),
textThe: t('foodHoneyThe'),
target: 'Golden',
},
Saddle: {
sellWarningNote: t('foodSaddleSellWarningNote'),
text: t('foodSaddleText'),
value: 5,
notes: t('foodSaddleNotes'),
canDrop: false,
},
/* eslint-disable camelcase */
Cake_Skeleton: {
text: t('foodCakeSkeleton'),
textA: t('foodCakeSkeletonA'),
textThe: t('foodCakeSkeletonThe'),
target: 'Skeleton',
},
Cake_Base: {
text: t('foodCakeBase'),
textA: t('foodCakeBaseA'),
textThe: t('foodCakeBaseThe'),
target: 'Base',
},
Cake_CottonCandyBlue: {
text: t('foodCakeCottonCandyBlue'),
textA: t('foodCakeCottonCandyBlueA'),
textThe: t('foodCakeCottonCandyBlueThe'),
target: 'CottonCandyBlue',
},
Cake_CottonCandyPink: {
text: t('foodCakeCottonCandyPink'),
textA: t('foodCakeCottonCandyPinkA'),
textThe: t('foodCakeCottonCandyPinkThe'),
target: 'CottonCandyPink',
},
Cake_Shade: {
text: t('foodCakeShade'),
textA: t('foodCakeShadeA'),
textThe: t('foodCakeShadeThe'),
target: 'Shade',
},
Cake_White: {
text: t('foodCakeWhite'),
textA: t('foodCakeWhiteA'),
textThe: t('foodCakeWhiteThe'),
target: 'White',
},
Cake_Golden: {
text: t('foodCakeGolden'),
textA: t('foodCakeGoldenA'),
textThe: t('foodCakeGoldenThe'),
target: 'Golden',
},
Cake_Zombie: {
text: t('foodCakeZombie'),
textA: t('foodCakeZombieA'),
textThe: t('foodCakeZombieThe'),
target: 'Zombie',
},
Cake_Desert: {
text: t('foodCakeDesert'),
textA: t('foodCakeDesertA'),
textThe: t('foodCakeDesertThe'),
target: 'Desert',
},
Cake_Red: {
text: t('foodCakeRed'),
textA: t('foodCakeRedA'),
textThe: t('foodCakeRedThe'),
target: 'Red',
},
Candy_Skeleton: {
text: t('foodCandySkeleton'),
textA: t('foodCandySkeletonA'),
textThe: t('foodCandySkeletonThe'),
target: 'Skeleton',
},
Candy_Base: {
text: t('foodCandyBase'),
textA: t('foodCandyBaseA'),
textThe: t('foodCandyBaseThe'),
target: 'Base',
},
Candy_CottonCandyBlue: {
text: t('foodCandyCottonCandyBlue'),
textA: t('foodCandyCottonCandyBlueA'),
textThe: t('foodCandyCottonCandyBlueThe'),
target: 'CottonCandyBlue',
},
Candy_CottonCandyPink: {
text: t('foodCandyCottonCandyPink'),
textA: t('foodCandyCottonCandyPinkA'),
textThe: t('foodCandyCottonCandyPinkThe'),
target: 'CottonCandyPink',
},
Candy_Shade: {
text: t('foodCandyShade'),
textA: t('foodCandyShadeA'),
textThe: t('foodCandyShadeThe'),
target: 'Shade',
},
Candy_White: {
text: t('foodCandyWhite'),
textA: t('foodCandyWhiteA'),
textThe: t('foodCandyWhiteThe'),
target: 'White',
},
Candy_Golden: {
text: t('foodCandyGolden'),
textA: t('foodCandyGoldenA'),
textThe: t('foodCandyGoldenThe'),
target: 'Golden',
},
Candy_Zombie: {
text: t('foodCandyZombie'),
textA: t('foodCandyZombieA'),
textThe: t('foodCandyZombieThe'),
target: 'Zombie',
},
Candy_Desert: {
text: t('foodCandyDesert'),
textA: t('foodCandyDesertA'),
textThe: t('foodCandyDesertThe'),
target: 'Desert',
},
Candy_Red: {
text: t('foodCandyRed'),
textA: t('foodCandyRedA'),
textThe: t('foodCandyRedThe'),
target: 'Red',
},
Pie_Skeleton: {
text: t('foodPieSkeleton'),
textA: t('foodPieSkeletonA'),
textThe: t('foodPieSkeletonThe'),
target: 'Skeleton',
},
Pie_Base: {
text: t('foodPieBase'),
textA: t('foodPieBaseA'),
textThe: t('foodPieBaseThe'),
target: 'Base',
},
Pie_CottonCandyBlue: {
text: t('foodPieCottonCandyBlue'),
textA: t('foodPieCottonCandyBlueA'),
textThe: t('foodPieCottonCandyBlueThe'),
target: 'CottonCandyBlue',
},
Pie_CottonCandyPink: {
text: t('foodPieCottonCandyPink'),
textA: t('foodPieCottonCandyPinkA'),
textThe: t('foodPieCottonCandyPinkThe'),
target: 'CottonCandyPink',
},
Pie_Shade: {
text: t('foodPieShade'),
textA: t('foodPieShadeA'),
textThe: t('foodPieShadeThe'),
target: 'Shade',
},
Pie_White: {
text: t('foodPieWhite'),
textA: t('foodPieWhiteA'),
textThe: t('foodPieWhiteThe'),
target: 'White',
},
Pie_Golden: {
text: t('foodPieGolden'),
textA: t('foodPieGoldenA'),
textThe: t('foodPieGoldenThe'),
target: 'Golden',
},
Pie_Zombie: {
text: t('foodPieZombie'),
textA: t('foodPieZombieA'),
textThe: t('foodPieZombieThe'),
target: 'Zombie',
},
Pie_Desert: {
text: t('foodPieDesert'),
textA: t('foodPieDesertA'),
textThe: t('foodPieDesertThe'),
target: 'Desert',
},
Pie_Red: {
text: t('foodPieRed'),
textA: t('foodPieRedA'),
textThe: t('foodPieRedThe'),
target: 'Red',
},
/* eslint-enable camelcase */
};
let FOOD_SEASON = 'Normal';
getRepeatingEvents(moment()).forEach(event => {
if (event.foodSeason) {
FOOD_SEASON = event.foodSeason;
}
}); });
each(api.food, (food, key) => { Object.defineProperty(api, 'premiumHatchingPotions', {
let foodType = 'Normal'; get () { return hatchingPotions.premium; },
if (key.startsWith('Cake_')) { enumerable: true,
foodType = 'Cake'; });
} else if (key.startsWith('Candy_')) { Object.defineProperty(api, 'wackyHatchingPotions', {
foodType = 'Candy'; get () { return hatchingPotions.wacky; },
} else if (key.startsWith('Pie_')) { enumerable: true,
foodType = 'Pie'; });
} Object.defineProperty(api, 'hatchingPotions', {
defaults(food, { get () { return hatchingPotions.all; },
value: 1, enumerable: true,
key, });
notes: t('foodNotes'),
canBuy: () => FOOD_SEASON === foodType, Object.defineProperty(api, 'dropPets', {
canDrop: FOOD_SEASON === foodType, get () { return stable.dropPets; },
enumerable: true,
});
Object.defineProperty(api, 'premiumPets', {
get () { return stable.premiumPets; },
enumerable: true,
});
Object.defineProperty(api, 'questPets', {
get () { return stable.questPets; },
enumerable: true,
});
Object.defineProperty(api, 'specialPets', {
get () { return stable.specialPets; },
enumerable: true,
});
Object.defineProperty(api, 'wackyPets', {
get () { return stable.wackyPets; },
enumerable: true,
});
Object.defineProperty(api, 'petInfo', {
get () { return stable.petInfo; },
enumerable: true,
});
Object.defineProperty(api, 'dropMounts', {
get () { return stable.dropMounts; },
enumerable: true,
});
Object.defineProperty(api, 'premiumMounts', {
get () { return stable.premiumMounts; },
enumerable: true,
});
Object.defineProperty(api, 'questMounts', {
get () { return stable.questMounts; },
enumerable: true,
});
Object.defineProperty(api, 'specialMounts', {
get () { return stable.specialMounts; },
enumerable: true,
});
Object.defineProperty(api, 'mountInfo', {
get () { return stable.mountInfo; },
enumerable: true,
});
function buildFood () {
const food = {
Meat: {
text: t('foodMeat'),
textA: t('foodMeatA'),
textThe: t('foodMeatThe'),
target: 'Base',
},
Milk: {
text: t('foodMilk'),
textA: t('foodMilkA'),
textThe: t('foodMilkThe'),
target: 'White',
},
Potatoe: {
text: t('foodPotatoe'),
textA: t('foodPotatoeA'),
textThe: t('foodPotatoeThe'),
target: 'Desert',
},
Strawberry: {
text: t('foodStrawberry'),
textA: t('foodStrawberryA'),
textThe: t('foodStrawberryThe'),
target: 'Red',
},
Chocolate: {
text: t('foodChocolate'),
textA: t('foodChocolateA'),
textThe: t('foodChocolateThe'),
target: 'Shade',
},
Fish: {
text: t('foodFish'),
textA: t('foodFishA'),
textThe: t('foodFishThe'),
target: 'Skeleton',
},
RottenMeat: {
text: t('foodRottenMeat'),
textA: t('foodRottenMeatA'),
textThe: t('foodRottenMeatThe'),
target: 'Zombie',
},
CottonCandyPink: {
text: t('foodCottonCandyPink'),
textA: t('foodCottonCandyPinkA'),
textThe: t('foodCottonCandyPinkThe'),
target: 'CottonCandyPink',
},
CottonCandyBlue: {
text: t('foodCottonCandyBlue'),
textA: t('foodCottonCandyBlueA'),
textThe: t('foodCottonCandyBlueThe'),
target: 'CottonCandyBlue',
},
Honey: {
text: t('foodHoney'),
textA: t('foodHoneyA'),
textThe: t('foodHoneyThe'),
target: 'Golden',
},
Saddle: {
sellWarningNote: t('foodSaddleSellWarningNote'),
text: t('foodSaddleText'),
value: 5,
notes: t('foodSaddleNotes'),
canBuy: () => true,
canDrop: false,
},
/* eslint-disable camelcase */
Cake_Skeleton: {
text: t('foodCakeSkeleton'),
textA: t('foodCakeSkeletonA'),
textThe: t('foodCakeSkeletonThe'),
target: 'Skeleton',
},
Cake_Base: {
text: t('foodCakeBase'),
textA: t('foodCakeBaseA'),
textThe: t('foodCakeBaseThe'),
target: 'Base',
},
Cake_CottonCandyBlue: {
text: t('foodCakeCottonCandyBlue'),
textA: t('foodCakeCottonCandyBlueA'),
textThe: t('foodCakeCottonCandyBlueThe'),
target: 'CottonCandyBlue',
},
Cake_CottonCandyPink: {
text: t('foodCakeCottonCandyPink'),
textA: t('foodCakeCottonCandyPinkA'),
textThe: t('foodCakeCottonCandyPinkThe'),
target: 'CottonCandyPink',
},
Cake_Shade: {
text: t('foodCakeShade'),
textA: t('foodCakeShadeA'),
textThe: t('foodCakeShadeThe'),
target: 'Shade',
},
Cake_White: {
text: t('foodCakeWhite'),
textA: t('foodCakeWhiteA'),
textThe: t('foodCakeWhiteThe'),
target: 'White',
},
Cake_Golden: {
text: t('foodCakeGolden'),
textA: t('foodCakeGoldenA'),
textThe: t('foodCakeGoldenThe'),
target: 'Golden',
},
Cake_Zombie: {
text: t('foodCakeZombie'),
textA: t('foodCakeZombieA'),
textThe: t('foodCakeZombieThe'),
target: 'Zombie',
},
Cake_Desert: {
text: t('foodCakeDesert'),
textA: t('foodCakeDesertA'),
textThe: t('foodCakeDesertThe'),
target: 'Desert',
},
Cake_Red: {
text: t('foodCakeRed'),
textA: t('foodCakeRedA'),
textThe: t('foodCakeRedThe'),
target: 'Red',
},
Candy_Skeleton: {
text: t('foodCandySkeleton'),
textA: t('foodCandySkeletonA'),
textThe: t('foodCandySkeletonThe'),
target: 'Skeleton',
},
Candy_Base: {
text: t('foodCandyBase'),
textA: t('foodCandyBaseA'),
textThe: t('foodCandyBaseThe'),
target: 'Base',
},
Candy_CottonCandyBlue: {
text: t('foodCandyCottonCandyBlue'),
textA: t('foodCandyCottonCandyBlueA'),
textThe: t('foodCandyCottonCandyBlueThe'),
target: 'CottonCandyBlue',
},
Candy_CottonCandyPink: {
text: t('foodCandyCottonCandyPink'),
textA: t('foodCandyCottonCandyPinkA'),
textThe: t('foodCandyCottonCandyPinkThe'),
target: 'CottonCandyPink',
},
Candy_Shade: {
text: t('foodCandyShade'),
textA: t('foodCandyShadeA'),
textThe: t('foodCandyShadeThe'),
target: 'Shade',
},
Candy_White: {
text: t('foodCandyWhite'),
textA: t('foodCandyWhiteA'),
textThe: t('foodCandyWhiteThe'),
target: 'White',
},
Candy_Golden: {
text: t('foodCandyGolden'),
textA: t('foodCandyGoldenA'),
textThe: t('foodCandyGoldenThe'),
target: 'Golden',
},
Candy_Zombie: {
text: t('foodCandyZombie'),
textA: t('foodCandyZombieA'),
textThe: t('foodCandyZombieThe'),
target: 'Zombie',
},
Candy_Desert: {
text: t('foodCandyDesert'),
textA: t('foodCandyDesertA'),
textThe: t('foodCandyDesertThe'),
target: 'Desert',
},
Candy_Red: {
text: t('foodCandyRed'),
textA: t('foodCandyRedA'),
textThe: t('foodCandyRedThe'),
target: 'Red',
},
Pie_Skeleton: {
text: t('foodPieSkeleton'),
textA: t('foodPieSkeletonA'),
textThe: t('foodPieSkeletonThe'),
target: 'Skeleton',
},
Pie_Base: {
text: t('foodPieBase'),
textA: t('foodPieBaseA'),
textThe: t('foodPieBaseThe'),
target: 'Base',
},
Pie_CottonCandyBlue: {
text: t('foodPieCottonCandyBlue'),
textA: t('foodPieCottonCandyBlueA'),
textThe: t('foodPieCottonCandyBlueThe'),
target: 'CottonCandyBlue',
},
Pie_CottonCandyPink: {
text: t('foodPieCottonCandyPink'),
textA: t('foodPieCottonCandyPinkA'),
textThe: t('foodPieCottonCandyPinkThe'),
target: 'CottonCandyPink',
},
Pie_Shade: {
text: t('foodPieShade'),
textA: t('foodPieShadeA'),
textThe: t('foodPieShadeThe'),
target: 'Shade',
},
Pie_White: {
text: t('foodPieWhite'),
textA: t('foodPieWhiteA'),
textThe: t('foodPieWhiteThe'),
target: 'White',
},
Pie_Golden: {
text: t('foodPieGolden'),
textA: t('foodPieGoldenA'),
textThe: t('foodPieGoldenThe'),
target: 'Golden',
},
Pie_Zombie: {
text: t('foodPieZombie'),
textA: t('foodPieZombieA'),
textThe: t('foodPieZombieThe'),
target: 'Zombie',
},
Pie_Desert: {
text: t('foodPieDesert'),
textA: t('foodPieDesertA'),
textThe: t('foodPieDesertThe'),
target: 'Desert',
},
Pie_Red: {
text: t('foodPieRed'),
textA: t('foodPieRedA'),
textThe: t('foodPieRedThe'),
target: 'Red',
},
/* eslint-enable camelcase */
};
let FOOD_SEASON = 'Normal';
getRepeatingEvents(moment()).forEach(event => {
if (event.foodSeason) {
FOOD_SEASON = event.foodSeason;
}
}); });
each(food, (foodItem, key) => {
let foodType = 'Normal';
if (key.startsWith('Cake_')) {
foodType = 'Cake';
} else if (key.startsWith('Candy_')) {
foodType = 'Candy';
} else if (key.startsWith('Pie_')) {
foodType = 'Pie';
}
defaults(foodItem, {
value: 1,
key,
notes: t('foodNotes'),
canBuy: () => FOOD_SEASON === foodType,
canDrop: FOOD_SEASON === foodType,
});
});
return food;
}
const memoizedBuildFood = memoize(buildFood);
Object.defineProperty(api, 'food', {
get () { return memoizedBuildFood(); },
}); });
api.appearances = appearances; api.appearances = appearances;

View File

@@ -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));
}

View File

@@ -232,6 +232,38 @@ const QUEST_PETS = {
unlock: t('questButterflyUnlockText'), unlock: t('questButterflyUnlockText'),
}, },
}, },
chameleon: {
text: t('questChameleonText'),
notes: t('questChameleonNotes'),
completion: t('questChameleonCompletion'),
value: 4,
category: 'pet',
boss: {
name: t('questChameleonBoss'),
hp: 400,
str: 1.5,
},
drop: {
items: [
{
type: 'eggs',
key: 'Chameleon',
text: t('questChameleonDropChameleonEgg'),
}, {
type: 'eggs',
key: 'Chameleon',
text: t('questChameleonDropChameleonEgg'),
}, {
type: 'eggs',
key: 'Chameleon',
text: t('questChameleonDropChameleonEgg'),
},
],
gp: 35,
exp: 250,
unlock: t('questChameleonUnlockText'),
},
},
cheetah: { cheetah: {
text: t('questCheetahText'), text: t('questCheetahText'),
notes: t('questCheetahNotes'), notes: t('questCheetahNotes'),

View File

@@ -20,6 +20,7 @@ const potentialFeaturedPetQuests = [
'giraffe', 'giraffe',
'guineapig', 'guineapig',
'chameleon',
'cheetah', 'cheetah',
@@ -34,7 +35,6 @@ const potentialFeaturedPetQuests = [
'sabretooth', 'sabretooth',
]; ];
// hatching potions and food names should be capitalized lest you break the market
const featuredItems = { const featuredItems = {
market () { market () {
const featured = [{ const featured = [{

View File

@@ -1,21 +1,12 @@
import each from 'lodash/each'; import each from 'lodash/each';
import moment from 'moment'; import moment from 'moment';
import { EVENTS } from './constants/events'; import { EVENTS } from './constants/events';
import { import allEggs from './eggs';
drops as dropEggs, import allPotions from './hatching-potions';
quests as questEggs,
} from './eggs';
import {
drops as dropPotions,
premium as premiumPotions,
wacky as wackyPotions,
} from './hatching-potions';
import t from './translation'; import t from './translation';
import memoize from '../fns/datedMemoize';
const petInfo = {}; function constructSet (type, eggs, potions, petInfo, mountInfo, hasMounts = true) {
const mountInfo = {};
function constructSet (type, eggs, potions) {
const pets = {}; const pets = {};
const mounts = {}; const mounts = {};
@@ -37,52 +28,24 @@ function constructSet (type, eggs, potions) {
potion: potion.text, potion: potion.text,
egg: egg.text, egg: egg.text,
})); }));
mountInfo[key] = getAnimalData(t('mountName', {
potion: potion.text,
mount: egg.mountText,
}));
pets[key] = true; pets[key] = true;
mounts[key] = true;
});
});
return [pets, mounts]; if (hasMounts) {
} mountInfo[key] = getAnimalData(t('mountName', {
potion: potion.text,
function constructPetOnlySet (type, eggs, potions) { mount: egg.mountText,
const pets = {}; }));
mounts[key] = true;
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,
};
} }
petInfo[key] = getAnimalData(t('petName', {
potion: potion.text,
egg: egg.text,
}));
pets[key] = true;
}); });
}); });
if (hasMounts) {
return [pets, mounts];
}
return pets; 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 = { const canFindSpecial = {
pets: { pets: {
// Veteran Pet Ladder - awarded on major updates // Veteran Pet Ladder - awarded on major updates
@@ -208,44 +171,88 @@ const specialMounts = {
'JackOLantern-RoyalPurple': 'royalPurpleJackolantern', 'JackOLantern-RoyalPurple': 'royalPurpleJackolantern',
}; };
each(specialPets, (translationString, key) => { function buildInfo () {
petInfo[key] = { const petInfo = {};
key, const mountInfo = {};
type: 'special',
text: t(translationString),
canFind: canFindSpecial.pets[key],
};
});
Object.assign(petInfo['Gryphatrice-Jubilant'], { const [dropPets, dropMounts] = constructSet('drop', allEggs.drops, allPotions.drops, petInfo, mountInfo);
canBuy () { const [premiumPets, premiumMounts] = constructSet('premium', allEggs.drops, allPotions.premium, petInfo, mountInfo);
return moment().isBetween(EVENTS.birthday10.start, EVENTS.birthday10.end); const [questPets, questMounts] = constructSet('quest', allEggs.quests, allPotions.drops, petInfo, mountInfo);
const wackyPets = constructSet('wacky', allEggs.drops, allPotions.wacky, petInfo, mountInfo, false);
each(specialPets, (translationString, key) => {
petInfo[key] = {
key,
type: 'special',
text: t(translationString),
canFind: canFindSpecial.pets[key],
};
});
Object.assign(petInfo['Gryphatrice-Jubilant'], {
canBuy () {
return moment().isBetween(EVENTS.birthday10.start, EVENTS.birthday10.end);
},
currency: 'gems',
event: 'birthday10',
value: 60,
purchaseType: 'pets',
});
each(specialMounts, (translationString, key) => {
mountInfo[key] = {
key,
type: 'special',
text: t(translationString),
canFind: canFindSpecial.mounts[key],
};
});
return {
dropPets,
premiumPets,
questPets,
wackyPets,
dropMounts,
questMounts,
premiumMounts,
specialPets,
specialMounts,
petInfo,
mountInfo,
};
}
const memoizedBuildInfo = memoize(buildInfo);
export default {
get dropPets () {
return memoizedBuildInfo().dropPets;
},
get premiumPets () {
return memoizedBuildInfo().premiumPets;
},
get questPets () {
return memoizedBuildInfo().questPets;
},
get wackyPets () {
return memoizedBuildInfo().wackyPets;
},
get dropMounts () {
return memoizedBuildInfo().dropMounts;
},
get questMounts () {
return memoizedBuildInfo().questMounts;
},
get premiumMounts () {
return memoizedBuildInfo().premiumMounts;
},
get petInfo () {
return memoizedBuildInfo().petInfo;
},
get mountInfo () {
return memoizedBuildInfo().mountInfo;
}, },
currency: 'gems',
event: 'birthday10',
value: 60,
purchaseType: 'pets',
});
each(specialMounts, (translationString, key) => {
mountInfo[key] = {
key,
type: 'special',
text: t(translationString),
canFind: canFindSpecial.mounts[key],
};
});
export {
dropPets,
premiumPets,
questPets,
wackyPets,
dropMounts,
questMounts,
premiumMounts,
specialPets, specialPets,
specialMounts, specialMounts,
petInfo,
mountInfo,
}; };

View File

@@ -33,6 +33,10 @@ const memoize = fn => {
identifier = config.identifier; identifier = config.identifier;
} }
} }
if (identifier.length === 0) {
identifier = args.filter(arg => typeof arg === 'string').join('-');
}
} }
if (!checkedDate) { if (!checkedDate) {
checkedDate = new Date(); checkedDate = new Date();

View File

@@ -1,10 +1,10 @@
import { drops as eggs } from '../content/eggs'; import allEggs from '../content/eggs';
import { drops as hatchingPotions } from '../content/hatching-potions'; import allPotions from '../content/hatching-potions';
import randomVal from '../libs/randomVal'; import randomVal from '../libs/randomVal';
export default function firstDrops (user) { export default function firstDrops (user) {
const eggDrop = randomVal(eggs); const eggDrop = randomVal(allEggs.drops);
const potionDrop = randomVal(hatchingPotions); const potionDrop = randomVal(allPotions.drops);
user.items.eggs = { user.items.eggs = {
...user.items.eggs, ...user.items.eggs,