mirror of
https://github.com/sudoxnym/habitica.git
synced 2026-04-14 11:46:23 +00:00
Merge branch 'develop' of github.com:HabitRPG/habitica into develop
This commit is contained in:
commit
b46e2da61b
240 changed files with 10210 additions and 5946 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -8,7 +8,7 @@ i18n_cache
|
||||||
apidoc/html
|
apidoc/html
|
||||||
*.swp
|
*.swp
|
||||||
.idea*
|
.idea*
|
||||||
config.json
|
config*.json
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
lib
|
lib
|
||||||
newrelic_agent.log
|
newrelic_agent.log
|
||||||
|
|
@ -48,3 +48,4 @@ webpack.webstorm.config
|
||||||
# mongodb replica set for local dev
|
# mongodb replica set for local dev
|
||||||
mongodb-*.tgz
|
mongodb-*.tgz
|
||||||
/mongodb-data
|
/mongodb-data
|
||||||
|
/.nyc_output
|
||||||
|
|
|
||||||
|
|
@ -89,5 +89,8 @@
|
||||||
"REDIS_HOST": "aaabbbcccdddeeefff",
|
"REDIS_HOST": "aaabbbcccdddeeefff",
|
||||||
"REDIS_PORT": "1234",
|
"REDIS_PORT": "1234",
|
||||||
"REDIS_PASSWORD": "12345678",
|
"REDIS_PASSWORD": "12345678",
|
||||||
"TRUSTED_DOMAINS": "localhost,https://habitica.com"
|
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
|
||||||
|
"TIME_TRAVEL_ENABLED": "false",
|
||||||
|
"DEBUG_ENABLED": "false",
|
||||||
|
"CONTENT_SWITCHOVER_TIME_OFFSET": 8
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,8 @@ function runInChildProcess (command, options = {}, envVariables = '') {
|
||||||
return done => pipe(exec(testBin(command, envVariables), options, done));
|
return done => pipe(exec(testBin(command, envVariables), options, done));
|
||||||
}
|
}
|
||||||
|
|
||||||
function integrationTestCommand (testDir, coverageDir) {
|
function integrationTestCommand (testDir) {
|
||||||
return `istanbul cover --dir coverage/${coverageDir} --report lcovonly node_modules/mocha/bin/_mocha -- ${testDir} --recursive --require ./test/helpers/start-server`;
|
return `nyc --silent --no-clean mocha ${testDir} --recursive --require ./test/helpers/start-server`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Test task definitions */
|
/* Test task definitions */
|
||||||
|
|
@ -148,7 +148,7 @@ gulp.task('test:content:safe', gulp.series('test:prepare:build', cb => {
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
'test:api:unit:run',
|
'test:api:unit:run',
|
||||||
runInChildProcess(integrationTestCommand('test/api/unit', 'coverage/api-unit')),
|
runInChildProcess(integrationTestCommand('test/api/unit')),
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task('test:api:unit:watch', () => gulp.watch(['website/server/libs/*', 'test/api/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api:unit:run', done => done())));
|
gulp.task('test:api:unit:watch', () => gulp.watch(['website/server/libs/*', 'test/api/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api:unit:run', done => done())));
|
||||||
|
|
@ -156,7 +156,7 @@ gulp.task('test:api:unit:watch', () => gulp.watch(['website/server/libs/*', 'tes
|
||||||
gulp.task('test:api-v3:integration', gulp.series(
|
gulp.task('test:api-v3:integration', gulp.series(
|
||||||
'test:prepare:mongo',
|
'test:prepare:mongo',
|
||||||
runInChildProcess(
|
runInChildProcess(
|
||||||
integrationTestCommand('test/api/v3/integration', 'coverage/api-v3-integration'),
|
integrationTestCommand('test/api/v3/integration'),
|
||||||
LIMIT_MAX_BUFFER_OPTIONS,
|
LIMIT_MAX_BUFFER_OPTIONS,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
@ -175,7 +175,7 @@ gulp.task('test:api-v3:integration:separate-server', runInChildProcess(
|
||||||
gulp.task('test:api-v4:integration', gulp.series(
|
gulp.task('test:api-v4:integration', gulp.series(
|
||||||
'test:prepare:mongo',
|
'test:prepare:mongo',
|
||||||
runInChildProcess(
|
runInChildProcess(
|
||||||
integrationTestCommand('test/api/v4', 'api-v4-integration'),
|
integrationTestCommand('test/api/v4'),
|
||||||
LIMIT_MAX_BUFFER_OPTIONS,
|
LIMIT_MAX_BUFFER_OPTIONS,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit aa723320199d7f03ce749d431b46e8d7f95cc8de
|
Subproject commit 617a3d6e6cd870172690a22b4dd16327f5e9d997
|
||||||
149
migrations/archive/2024/20240621_veteran_pets.js
Normal file
149
migrations/archive/2024/20240621_veteran_pets.js
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
const MIGRATION_NAME = '20240621_veteran_pet_ladder';
|
||||||
|
import { model as User } from '../../../website/server/models/user';
|
||||||
|
|
||||||
|
const progressCount = 1000;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
async function updateUser (user) {
|
||||||
|
count++;
|
||||||
|
|
||||||
|
const set = {};
|
||||||
|
let push = { notifications: { $each: [] }};
|
||||||
|
|
||||||
|
set.migration = MIGRATION_NAME;
|
||||||
|
if (user.items.pets['Dragon-Veteran']) {
|
||||||
|
set['items.pets.Cactus-Veteran'] = 5;
|
||||||
|
push.notifications.$each.push({
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'icon_pet_veteran_cactus',
|
||||||
|
title: 'You’ve received a Veteran Pet!',
|
||||||
|
text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Cactus and 24 Gems!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
} else if (user.items.pets['Fox-Veteran']) {
|
||||||
|
set['items.pets.Dragon-Veteran'] = 5;
|
||||||
|
push.notifications.$each.push({
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'icon_pet_veteran_dragon',
|
||||||
|
title: 'You’ve received a Veteran Pet!',
|
||||||
|
text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Dragon and 24 Gems!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
} else if (user.items.pets['Bear-Veteran']) {
|
||||||
|
set['items.pets.Fox-Veteran'] = 5;
|
||||||
|
push.notifications.$each.push({
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'icon_pet_veteran_fox',
|
||||||
|
title: 'You’ve received a Veteran Pet!',
|
||||||
|
text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Fox and 24 Gems!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
} else if (user.items.pets['Lion-Veteran']) {
|
||||||
|
set['items.pets.Bear-Veteran'] = 5;
|
||||||
|
push.notifications.$each.push({
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'icon_pet_veteran_bear',
|
||||||
|
title: 'You’ve received a Veteran Pet!',
|
||||||
|
text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Bear and 24 Gems!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
} else if (user.items.pets['Tiger-Veteran']) {
|
||||||
|
set['items.pets.Lion-Veteran'] = 5;
|
||||||
|
push.notifications.$each.push({
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'icon_pet_veteran_lion',
|
||||||
|
title: 'You’ve received a Veteran Pet!',
|
||||||
|
text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Lion and 24 Gems!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
} else if (user.items.pets['Wolf-Veteran']) {
|
||||||
|
set['items.pets.Tiger-Veteran'] = 5;
|
||||||
|
push.notifications.$each.push({
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'icon_pet_veteran_tiger',
|
||||||
|
title: 'You’ve received a Veteran Pet!',
|
||||||
|
text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Tiger and 24 Gems!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
set['items.pets.Wolf-Veteran'] = 5;
|
||||||
|
push.notifications.$each.push({
|
||||||
|
type: 'ITEM_RECEIVED',
|
||||||
|
data: {
|
||||||
|
icon: 'icon_pet_veteran_wolf',
|
||||||
|
title: 'You’ve received a Veteran Pet!',
|
||||||
|
text: 'To commemorate being here for a new era of Habitica, we’ve awarded you a Veteran Wolf and 24 Gems!',
|
||||||
|
destination: '/inventory/stable',
|
||||||
|
},
|
||||||
|
seen: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||||
|
|
||||||
|
await user.updateBalance(
|
||||||
|
6,
|
||||||
|
'admin_update_balance',
|
||||||
|
'',
|
||||||
|
'Veteran Ladder award',
|
||||||
|
);
|
||||||
|
|
||||||
|
return await User.updateOne(
|
||||||
|
{ _id: user._id },
|
||||||
|
{ $set: set, $push: push, $inc: { balance: 6 } },
|
||||||
|
).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function processUsers () {
|
||||||
|
let query = {
|
||||||
|
migration: {$ne: MIGRATION_NAME},
|
||||||
|
'auth.timestamps.loggedin': { $gt: new Date('2024-05-21') },
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields = {
|
||||||
|
_id: 1,
|
||||||
|
items: 1,
|
||||||
|
migration: 1,
|
||||||
|
contributor: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
while (true) { // eslint-disable-line no-constant-condition
|
||||||
|
const users = await User // eslint-disable-line no-await-in-loop
|
||||||
|
.find(query)
|
||||||
|
.limit(250)
|
||||||
|
.sort({_id: 1})
|
||||||
|
.select(fields)
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
console.warn('All appropriate users found and modified.');
|
||||||
|
console.warn(`\n${count} users processed\n`);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
query._id = {
|
||||||
|
$gt: users[users.length - 1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||||
|
}
|
||||||
|
};
|
||||||
1422
package-lock.json
generated
1422
package-lock.json
generated
File diff suppressed because it is too large
Load diff
15
package.json
15
package.json
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "habitica",
|
"name": "habitica",
|
||||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||||
"version": "5.25.2",
|
"version": "5.25.8",
|
||||||
"main": "./website/server/index.js",
|
"main": "./website/server/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.22.10",
|
"@babel/core": "^7.22.10",
|
||||||
|
|
@ -67,6 +67,7 @@
|
||||||
"remove-markdown": "^0.5.0",
|
"remove-markdown": "^0.5.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"short-uuid": "^4.2.2",
|
"short-uuid": "^4.2.2",
|
||||||
|
"sinon": "^15.2.0",
|
||||||
"stripe": "^12.18.0",
|
"stripe": "^12.18.0",
|
||||||
"superagent": "^8.1.2",
|
"superagent": "^8.1.2",
|
||||||
"universal-analytics": "^0.5.3",
|
"universal-analytics": "^0.5.3",
|
||||||
|
|
@ -93,11 +94,11 @@
|
||||||
"test:api-v3:integration:separate-server": "NODE_ENV=test gulp test:api-v3:integration:separate-server",
|
"test:api-v3:integration:separate-server": "NODE_ENV=test gulp test:api-v3:integration:separate-server",
|
||||||
"test:api-v4:integration": "gulp test:api-v4:integration",
|
"test:api-v4:integration": "gulp test:api-v4:integration",
|
||||||
"test:api-v4:integration:separate-server": "NODE_ENV=test gulp test:api-v4:integration:separate-server",
|
"test:api-v4:integration:separate-server": "NODE_ENV=test gulp test:api-v4:integration:separate-server",
|
||||||
"test:sanity": "istanbul cover --dir coverage/sanity --report lcovonly node_modules/mocha/bin/_mocha -- test/sanity --recursive",
|
"test:sanity": "nyc --silent --no-clean mocha test/sanity --recursive",
|
||||||
"test:common": "istanbul cover --dir coverage/common --report lcovonly node_modules/mocha/bin/_mocha -- test/common --recursive",
|
"test:common": "nyc --silent --no-clean mocha test/common --recursive",
|
||||||
"test:content": "istanbul cover --dir coverage/content --report lcovonly node_modules/mocha/bin/_mocha -- test/content --recursive",
|
"test:content": "nyc --silent --no-clean mocha test/content --recursive",
|
||||||
"test:nodemon": "gulp test:nodemon",
|
"test:nodemon": "gulp test:nodemon",
|
||||||
"coverage": "COVERAGE=true mocha --require register-handlers.js --reporter html-cov > coverage.html; open coverage.html",
|
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
|
||||||
"sprites": "gulp sprites:compile",
|
"sprites": "gulp sprites:compile",
|
||||||
"client:dev": "cd website/client && npm run serve",
|
"client:dev": "cd website/client && npm run serve",
|
||||||
"client:build": "cd website/client && npm run build",
|
"client:build": "cd website/client && npm run build",
|
||||||
|
|
@ -115,13 +116,11 @@
|
||||||
"chai-moment": "^0.1.0",
|
"chai-moment": "^0.1.0",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
"expect.js": "^0.3.1",
|
|
||||||
"istanbul": "^1.1.0-alpha.1",
|
|
||||||
"mocha": "^5.1.1",
|
"mocha": "^5.1.1",
|
||||||
"monk": "^7.3.4",
|
"monk": "^7.3.4",
|
||||||
|
"nyc": "^15.1.0",
|
||||||
"require-again": "^2.0.0",
|
"require-again": "^2.0.0",
|
||||||
"run-rs": "^0.7.7",
|
"run-rs": "^0.7.7",
|
||||||
"sinon": "^15.2.0",
|
|
||||||
"sinon-chai": "^3.7.0",
|
"sinon-chai": "^3.7.0",
|
||||||
"sinon-stub-promise": "^4.0.0"
|
"sinon-stub-promise": "^4.0.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
|
import fs from 'fs';
|
||||||
import * as contentLib from '../../../../website/server/libs/content';
|
import * as contentLib from '../../../../website/server/libs/content';
|
||||||
import content from '../../../../website/common/script/content';
|
import content from '../../../../website/common/script/content';
|
||||||
|
import {
|
||||||
|
generateRes,
|
||||||
|
} from '../../../helpers/api-unit.helper';
|
||||||
|
|
||||||
describe('contentLib', () => {
|
describe('contentLib', () => {
|
||||||
describe('CONTENT_CACHE_PATH', () => {
|
describe('CONTENT_CACHE_PATH', () => {
|
||||||
|
|
@ -13,5 +17,90 @@ describe('contentLib', () => {
|
||||||
contentLib.getLocalizedContentResponse();
|
contentLib.getLocalizedContentResponse();
|
||||||
expect(typeof content.backgrounds.backgrounds062014.beach.text).to.equal('function');
|
expect(typeof content.backgrounds.backgrounds062014.beach.text).to.equal('function');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('removes keys from the content data', () => {
|
||||||
|
const response = contentLib.localizeContentData(content, 'en', { backgroundsFlat: true, dropHatchingPotions: true });
|
||||||
|
expect(response.backgroundsFlat).to.not.exist;
|
||||||
|
expect(response.backgrounds).to.exist;
|
||||||
|
expect(response.dropHatchingPotions).to.not.exist;
|
||||||
|
expect(response.hatchingPotions).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes nested keys from the content data', () => {
|
||||||
|
const response = contentLib.localizeContentData(content, 'en', { gear: { tree: true } });
|
||||||
|
expect(response.gear.tree).to.not.exist;
|
||||||
|
expect(response.gear.flat).to.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates a hash for a filter', () => {
|
||||||
|
const hash = contentLib.hashForFilter('backgroundsFlat,gear.flat');
|
||||||
|
expect(hash).to.equal('-1791877526');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves content', () => {
|
||||||
|
const resSpy = generateRes();
|
||||||
|
contentLib.serveContent(resSpy, 'en', '', false);
|
||||||
|
expect(resSpy.send).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves filtered content', () => {
|
||||||
|
const resSpy = generateRes();
|
||||||
|
contentLib.serveContent(resSpy, 'en', 'backgroundsFlat,gear.flat', false);
|
||||||
|
expect(resSpy.send).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('caches content', async () => {
|
||||||
|
let resSpy;
|
||||||
|
beforeEach(() => {
|
||||||
|
resSpy = generateRes();
|
||||||
|
if (fs.existsSync(contentLib.CONTENT_CACHE_PATH)) {
|
||||||
|
fs.rmdirSync(contentLib.CONTENT_CACHE_PATH, { recursive: true });
|
||||||
|
}
|
||||||
|
fs.mkdirSync(contentLib.CONTENT_CACHE_PATH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not cache requests in development mode', async () => {
|
||||||
|
contentLib.serveContent(resSpy, 'en', '', false);
|
||||||
|
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('caches unfiltered requests', async () => {
|
||||||
|
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.false;
|
||||||
|
contentLib.serveContent(resSpy, 'en', '', true);
|
||||||
|
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves cached requests', async () => {
|
||||||
|
fs.writeFileSync(
|
||||||
|
`${contentLib.CONTENT_CACHE_PATH}en.json`,
|
||||||
|
'{"success": true, "data": {"all": {}}}',
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
contentLib.serveContent(resSpy, 'en', '', true);
|
||||||
|
expect(resSpy.sendFile).to.have.been.calledOnce;
|
||||||
|
expect(resSpy.sendFile).to.have.been.calledWith(`${contentLib.CONTENT_CACHE_PATH}en.json`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('caches filtered requests', async () => {
|
||||||
|
const filter = 'backgroundsFlat,gear.flat';
|
||||||
|
const hash = contentLib.hashForFilter(filter);
|
||||||
|
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`)).to.be.false;
|
||||||
|
contentLib.serveContent(resSpy, 'en', filter, true);
|
||||||
|
expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`)).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serves filtered cached requests', async () => {
|
||||||
|
const filter = 'backgroundsFlat,gear.flat';
|
||||||
|
const hash = contentLib.hashForFilter(filter);
|
||||||
|
fs.writeFileSync(
|
||||||
|
`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`,
|
||||||
|
'{"success": true, "data": {}}',
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
contentLib.serveContent(resSpy, 'en', filter, true);
|
||||||
|
expect(resSpy.sendFile).to.have.been.calledOnce;
|
||||||
|
expect(resSpy.sendFile).to.have.been.calledWith(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ describe('Items Utils', () => {
|
||||||
it('converts values for owned gear to true/false', () => {
|
it('converts values for owned gear to true/false', () => {
|
||||||
expect(castItemVal('items.gear.owned.shield_warrior_0', 'true')).to.equal(true);
|
expect(castItemVal('items.gear.owned.shield_warrior_0', 'true')).to.equal(true);
|
||||||
expect(castItemVal('items.gear.owned.invalid', 'false')).to.equal(false);
|
expect(castItemVal('items.gear.owned.invalid', 'false')).to.equal(false);
|
||||||
expect(castItemVal('items.gear.owned.invalid', 'null')).to.equal(false);
|
expect(castItemVal('items.gear.owned.invalid', 'null')).to.equal(undefined);
|
||||||
expect(castItemVal('items.gear.owned.invalid', 'truthy')).to.equal(true);
|
expect(castItemVal('items.gear.owned.invalid', 'truthy')).to.equal(true);
|
||||||
expect(castItemVal('items.gear.owned.invalid', 0)).to.equal(false);
|
expect(castItemVal('items.gear.owned.invalid', 0)).to.equal(false);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
51
test/api/unit/middlewares/ensureDevelopmentMode.js
Normal file
51
test/api/unit/middlewares/ensureDevelopmentMode.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
/* eslint-disable global-require */
|
||||||
|
import nconf from 'nconf';
|
||||||
|
import {
|
||||||
|
generateRes,
|
||||||
|
generateReq,
|
||||||
|
generateNext,
|
||||||
|
} from '../../../helpers/api-unit.helper';
|
||||||
|
import ensureDevelopmentMode from '../../../../website/server/middlewares/ensureDevelopmentMode';
|
||||||
|
import { NotFound } from '../../../../website/server/libs/errors';
|
||||||
|
|
||||||
|
describe('developmentMode middleware', () => {
|
||||||
|
let res; let req; let next;
|
||||||
|
let nconfStub;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
res = generateRes();
|
||||||
|
req = generateReq();
|
||||||
|
next = generateNext();
|
||||||
|
nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns not found when on production URL', () => {
|
||||||
|
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
|
||||||
|
|
||||||
|
ensureDevelopmentMode(req, res, next);
|
||||||
|
|
||||||
|
const calledWith = next.getCall(0).args;
|
||||||
|
expect(calledWith[0] instanceof NotFound).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns not found when intentionally disabled', () => {
|
||||||
|
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
|
||||||
|
|
||||||
|
ensureDevelopmentMode(req, res, next);
|
||||||
|
|
||||||
|
const calledWith = next.getCall(0).args;
|
||||||
|
expect(calledWith[0] instanceof NotFound).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes when enabled and on non-production URL', () => {
|
||||||
|
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
|
||||||
|
|
||||||
|
ensureDevelopmentMode(req, res, next);
|
||||||
|
|
||||||
|
expect(next).to.be.calledOnce;
|
||||||
|
expect(next.args[0]).to.be.empty;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
/* eslint-disable global-require */
|
|
||||||
import nconf from 'nconf';
|
|
||||||
import {
|
|
||||||
generateRes,
|
|
||||||
generateReq,
|
|
||||||
generateNext,
|
|
||||||
} from '../../../helpers/api-unit.helper';
|
|
||||||
import ensureDevelpmentMode from '../../../../website/server/middlewares/ensureDevelpmentMode';
|
|
||||||
import { NotFound } from '../../../../website/server/libs/errors';
|
|
||||||
|
|
||||||
describe('developmentMode middleware', () => {
|
|
||||||
let res; let req; let
|
|
||||||
next;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
res = generateRes();
|
|
||||||
req = generateReq();
|
|
||||||
next = generateNext();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns not found when in production mode', () => {
|
|
||||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
|
||||||
|
|
||||||
ensureDevelpmentMode(req, res, next);
|
|
||||||
|
|
||||||
const calledWith = next.getCall(0).args;
|
|
||||||
expect(calledWith[0] instanceof NotFound).to.equal(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passes when not in production', () => {
|
|
||||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
|
||||||
|
|
||||||
ensureDevelpmentMode(req, res, next);
|
|
||||||
|
|
||||||
expect(next).to.be.calledOnce;
|
|
||||||
expect(next.args[0]).to.be.empty;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
51
test/api/unit/middlewares/ensureTimeTravelMode.js
Normal file
51
test/api/unit/middlewares/ensureTimeTravelMode.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
/* eslint-disable global-require */
|
||||||
|
import nconf from 'nconf';
|
||||||
|
import {
|
||||||
|
generateRes,
|
||||||
|
generateReq,
|
||||||
|
generateNext,
|
||||||
|
} from '../../../helpers/api-unit.helper';
|
||||||
|
import { NotFound } from '../../../../website/server/libs/errors';
|
||||||
|
import ensureTimeTravelMode from '../../../../website/server/middlewares/ensureTimeTravelMode';
|
||||||
|
|
||||||
|
describe('timetravelMode middleware', () => {
|
||||||
|
let res; let req; let next;
|
||||||
|
let nconfStub;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
res = generateRes();
|
||||||
|
req = generateReq();
|
||||||
|
next = generateNext();
|
||||||
|
nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns not found when using production URL', () => {
|
||||||
|
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
|
||||||
|
|
||||||
|
ensureTimeTravelMode(req, res, next);
|
||||||
|
|
||||||
|
const calledWith = next.getCall(0).args;
|
||||||
|
expect(calledWith[0] instanceof NotFound).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns not found when not in time travel mode', () => {
|
||||||
|
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
|
||||||
|
|
||||||
|
ensureTimeTravelMode(req, res, next);
|
||||||
|
|
||||||
|
const calledWith = next.getCall(0).args;
|
||||||
|
expect(calledWith[0] instanceof NotFound).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes when in time travel mode', () => {
|
||||||
|
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('http://localhost:3000');
|
||||||
|
|
||||||
|
ensureTimeTravelMode(req, res, next);
|
||||||
|
|
||||||
|
expect(next).to.be.calledOnce;
|
||||||
|
expect(next.args[0]).to.be.empty;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -22,4 +22,38 @@ describe('GET /content', () => {
|
||||||
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
||||||
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
|
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not filter content for regular requests', async () => {
|
||||||
|
const res = await requester().get('/content');
|
||||||
|
expect(res).to.have.nested.property('backgrounds.backgrounds062014');
|
||||||
|
expect(res).to.have.nested.property('gear.tree');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters content automatically for iOS requests', async () => {
|
||||||
|
const res = await requester(null, { 'x-client': 'habitica-ios' }).get('/content');
|
||||||
|
expect(res).to.have.nested.property('appearances.background.beach');
|
||||||
|
expect(res).to.not.have.nested.property('backgrounds.backgrounds062014');
|
||||||
|
expect(res).to.not.have.property('backgroundsFlat');
|
||||||
|
expect(res).to.not.have.nested.property('gear.tree');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters content automatically for Android requests', async () => {
|
||||||
|
const res = await requester(null, { 'x-client': 'habitica-android' }).get('/content');
|
||||||
|
expect(res).to.not.have.nested.property('appearances.background.beach');
|
||||||
|
expect(res).to.have.nested.property('backgrounds.backgrounds062014');
|
||||||
|
expect(res).to.not.have.property('backgroundsFlat');
|
||||||
|
expect(res).to.not.have.nested.property('gear.tree');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters content if the request specifies a filter', async () => {
|
||||||
|
const res = await requester().get('/content?filter=backgroundsFlat,gear.flat');
|
||||||
|
expect(res).to.not.have.property('backgroundsFlat');
|
||||||
|
expect(res).to.have.nested.property('gear.tree');
|
||||||
|
expect(res).to.not.have.nested.property('gear.flat');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters content if the request contains invalid filters', async () => {
|
||||||
|
const res = await requester().get('/content?filter=backgroundsFlat,invalid');
|
||||||
|
expect(res).to.not.have.property('backgroundsFlat');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import nconf from 'nconf';
|
||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
} from '../../../../helpers/api-integration/v3';
|
||||||
|
|
||||||
|
describe('GET /debug/time-travel-time', () => {
|
||||||
|
let user;
|
||||||
|
let nconfStub;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
user = await generateUser({ permissions: { fullAccess: true } });
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
nconfStub.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the shifted time', async () => {
|
||||||
|
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
|
||||||
|
const result = await user.get('/debug/time-travel-time');
|
||||||
|
expect(result.time).to.exist;
|
||||||
|
await user.post('/debug/jump-time', { disable: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns shifted when the user is not an admin', async () => {
|
||||||
|
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
|
||||||
|
const regularUser = await generateUser();
|
||||||
|
const result = await regularUser.get('/debug/time-travel-time');
|
||||||
|
expect(result.time).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when not in time travel mode', async () => {
|
||||||
|
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
|
||||||
|
|
||||||
|
await expect(user.get('/debug/time-travel-time'))
|
||||||
|
.eventually.be.rejected.and.to.deep.equal({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: 'Not found.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -5,16 +5,23 @@ import {
|
||||||
|
|
||||||
describe('POST /debug/add-hourglass', () => {
|
describe('POST /debug/add-hourglass', () => {
|
||||||
let userToGetHourGlass;
|
let userToGetHourGlass;
|
||||||
|
let nconfStub;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
userToGetHourGlass = await generateUser();
|
userToGetHourGlass = await generateUser();
|
||||||
});
|
});
|
||||||
|
|
||||||
after(() => {
|
beforeEach(() => {
|
||||||
nconf.set('IS_PROD', false);
|
nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
nconfStub.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds Hourglass to the current user', async () => {
|
it('adds Hourglass to the current user', async () => {
|
||||||
|
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||||
await userToGetHourGlass.post('/debug/add-hourglass');
|
await userToGetHourGlass.post('/debug/add-hourglass');
|
||||||
|
|
||||||
const userWithHourGlass = await userToGetHourGlass.get('/user');
|
const userWithHourGlass = await userToGetHourGlass.get('/user');
|
||||||
|
|
@ -23,7 +30,7 @@ describe('POST /debug/add-hourglass', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns error when not in production mode', async () => {
|
it('returns error when not in production mode', async () => {
|
||||||
nconf.set('IS_PROD', true);
|
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||||
|
|
||||||
await expect(userToGetHourGlass.post('/debug/add-hourglass'))
|
await expect(userToGetHourGlass.post('/debug/add-hourglass'))
|
||||||
.eventually.be.rejected.and.to.deep.equal({
|
.eventually.be.rejected.and.to.deep.equal({
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,23 @@ import {
|
||||||
|
|
||||||
describe('POST /debug/add-ten-gems', () => {
|
describe('POST /debug/add-ten-gems', () => {
|
||||||
let userToGainTenGems;
|
let userToGainTenGems;
|
||||||
|
let nconfStub;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
userToGainTenGems = await generateUser();
|
userToGainTenGems = await generateUser();
|
||||||
});
|
});
|
||||||
|
|
||||||
after(() => {
|
beforeEach(() => {
|
||||||
nconf.set('IS_PROD', false);
|
nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
nconfStub.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds ten gems to the current user', async () => {
|
it('adds ten gems to the current user', async () => {
|
||||||
|
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||||
await userToGainTenGems.post('/debug/add-ten-gems');
|
await userToGainTenGems.post('/debug/add-ten-gems');
|
||||||
|
|
||||||
const userWithTenGems = await userToGainTenGems.get('/user');
|
const userWithTenGems = await userToGainTenGems.get('/user');
|
||||||
|
|
@ -23,7 +30,7 @@ describe('POST /debug/add-ten-gems', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns error when not in production mode', async () => {
|
it('returns error when not in production mode', async () => {
|
||||||
nconf.set('IS_PROD', true);
|
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||||
|
|
||||||
await expect(userToGainTenGems.post('/debug/add-ten-gems'))
|
await expect(userToGainTenGems.post('/debug/add-ten-gems'))
|
||||||
.eventually.be.rejected.and.to.deep.equal({
|
.eventually.be.rejected.and.to.deep.equal({
|
||||||
|
|
|
||||||
82
test/api/v3/integration/debug/POST-debug_jumpTime.test.js
Normal file
82
test/api/v3/integration/debug/POST-debug_jumpTime.test.js
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import nconf from 'nconf';
|
||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
} from '../../../../helpers/api-integration/v3';
|
||||||
|
|
||||||
|
describe('POST /debug/jump-time', () => {
|
||||||
|
let user;
|
||||||
|
let today;
|
||||||
|
let nconfStub;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
user = await generateUser({ permissions: { fullAccess: true } });
|
||||||
|
today = new Date();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
nconfStub.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
nconf.set('TIME_TRAVEL_ENABLED', true);
|
||||||
|
await user.post('/debug/jump-time', { disable: true });
|
||||||
|
nconf.set('TIME_TRAVEL_ENABLED', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Jumps forward', async () => {
|
||||||
|
const resultDate = new Date((await user.post('/debug/jump-time', { reset: true })).time);
|
||||||
|
expect(resultDate.getDate()).to.eql(today.getDate());
|
||||||
|
expect(resultDate.getMonth()).to.eql(today.getMonth());
|
||||||
|
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
|
||||||
|
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 1 })).time);
|
||||||
|
expect(newResultDate.getDate()).to.eql(today.getDate() + 1);
|
||||||
|
expect(newResultDate.getMonth()).to.eql(today.getMonth());
|
||||||
|
expect(newResultDate.getFullYear()).to.eql(today.getFullYear());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('jumps back', async () => {
|
||||||
|
const resultDate = new Date((await user.post('/debug/jump-time', { reset: true })).time);
|
||||||
|
expect(resultDate.getDate()).to.eql(today.getDate());
|
||||||
|
expect(resultDate.getMonth()).to.eql(today.getMonth());
|
||||||
|
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
|
||||||
|
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: -1 })).time);
|
||||||
|
expect(newResultDate.getDate()).to.eql(today.getDate() - 1);
|
||||||
|
expect(newResultDate.getMonth()).to.eql(today.getMonth());
|
||||||
|
expect(newResultDate.getFullYear()).to.eql(today.getFullYear());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can jump a lot', async () => {
|
||||||
|
const resultDate = new Date((await user.post('/debug/jump-time', { reset: true })).time);
|
||||||
|
expect(resultDate.getDate()).to.eql(today.getDate());
|
||||||
|
expect(resultDate.getMonth()).to.eql(today.getMonth());
|
||||||
|
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
|
||||||
|
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 355 })).time);
|
||||||
|
expect(newResultDate.getFullYear()).to.eql(today.getFullYear() + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when the user is not an admin', async () => {
|
||||||
|
const regularUser = await generateUser();
|
||||||
|
await expect(regularUser.post('/debug/jump-time', { offsetDays: 1 }))
|
||||||
|
.eventually.be.rejected.and.to.deep.equal({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'You do not have permission to time travel.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when not in time travel mode', async () => {
|
||||||
|
nconfStub.withArgs('TIME_TRAVEL_ENABLED').returns(false);
|
||||||
|
|
||||||
|
await expect(user.post('/debug/jump-time', { offsetDays: 1 }))
|
||||||
|
.eventually.be.rejected.and.to.deep.equal({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: 'Not found.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -5,16 +5,23 @@ import {
|
||||||
|
|
||||||
describe('POST /debug/make-admin', () => {
|
describe('POST /debug/make-admin', () => {
|
||||||
let user;
|
let user;
|
||||||
|
let nconfStub;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
user = await generateUser();
|
user = await generateUser();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
nconf.set('IS_PROD', false);
|
nconfStub.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('makes user an admin', async () => {
|
it('makes user an admin', async () => {
|
||||||
|
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||||
await user.post('/debug/make-admin');
|
await user.post('/debug/make-admin');
|
||||||
|
|
||||||
await user.sync();
|
await user.sync();
|
||||||
|
|
@ -23,7 +30,7 @@ describe('POST /debug/make-admin', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns error when not in production mode', async () => {
|
it('returns error when not in production mode', async () => {
|
||||||
nconf.set('IS_PROD', true);
|
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||||
|
|
||||||
await expect(user.post('/debug/make-admin'))
|
await expect(user.post('/debug/make-admin'))
|
||||||
.eventually.be.rejected.and.to.deep.equal({
|
.eventually.be.rejected.and.to.deep.equal({
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
describe('POST /debug/modify-inventory', () => {
|
describe('POST /debug/modify-inventory', () => {
|
||||||
let user; let
|
let user; let
|
||||||
originalItems;
|
originalItems;
|
||||||
|
let nconfStub;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
originalItems = {
|
originalItems = {
|
||||||
|
|
@ -39,8 +40,14 @@ describe('POST /debug/modify-inventory', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
nconf.set('IS_PROD', false);
|
nconfStub.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets equipment', async () => {
|
it('sets equipment', async () => {
|
||||||
|
|
@ -149,7 +156,7 @@ describe('POST /debug/modify-inventory', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns error when not in production mode', async () => {
|
it('returns error when not in production mode', async () => {
|
||||||
nconf.set('IS_PROD', true);
|
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||||
|
|
||||||
await expect(user.post('/debug/modify-inventory'))
|
await expect(user.post('/debug/modify-inventory'))
|
||||||
.eventually.be.rejected.and.to.deep.equal({
|
.eventually.be.rejected.and.to.deep.equal({
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,20 @@ import {
|
||||||
|
|
||||||
describe('POST /debug/quest-progress', () => {
|
describe('POST /debug/quest-progress', () => {
|
||||||
let user;
|
let user;
|
||||||
|
let nconfStub;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
user = await generateUser();
|
user = await generateUser();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
nconf.set('IS_PROD', false);
|
nconfStub.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors if user is not on a quest', async () => {
|
it('errors if user is not on a quest', async () => {
|
||||||
|
|
@ -48,7 +55,7 @@ describe('POST /debug/quest-progress', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns error when not in production mode', async () => {
|
it('returns error when not in production mode', async () => {
|
||||||
nconf.set('IS_PROD', true);
|
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||||
|
|
||||||
await expect(user.post('/debug/quest-progress'))
|
await expect(user.post('/debug/quest-progress'))
|
||||||
.eventually.be.rejected.and.to.deep.equal({
|
.eventually.be.rejected.and.to.deep.equal({
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,20 @@ import {
|
||||||
|
|
||||||
describe('POST /debug/set-cron', () => {
|
describe('POST /debug/set-cron', () => {
|
||||||
let user;
|
let user;
|
||||||
|
let nconfStub;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
user = await generateUser();
|
user = await generateUser();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
nconfStub = sandbox.stub(nconf, 'get');
|
||||||
|
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||||
|
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
nconf.set('IS_PROD', false);
|
nconfStub.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets last cron', async () => {
|
it('sets last cron', async () => {
|
||||||
|
|
@ -27,7 +34,7 @@ describe('POST /debug/set-cron', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns error when not in production mode', async () => {
|
it('returns error when not in production mode', async () => {
|
||||||
nconf.set('IS_PROD', true);
|
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||||
|
|
||||||
await expect(user.post('/debug/set-cron'))
|
await expect(user.post('/debug/set-cron'))
|
||||||
.eventually.be.rejected.and.to.deep.equal({
|
.eventually.be.rejected.and.to.deep.equal({
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,5 @@ describe('GET /shops/backgrounds', () => {
|
||||||
expect(shop.notes).to.eql(t('backgroundShop'));
|
expect(shop.notes).to.eql(t('backgroundShop'));
|
||||||
expect(shop.imageName).to.equal('background_shop');
|
expect(shop.imageName).to.equal('background_shop');
|
||||||
expect(shop.sets).to.be.an('array');
|
expect(shop.sets).to.be.an('array');
|
||||||
|
|
||||||
const sets = shop.sets.map(set => set.identifier);
|
|
||||||
expect(sets).to.include('incentiveBackgrounds');
|
|
||||||
expect(sets).to.include('backgrounds062014');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,15 @@ import {
|
||||||
|
|
||||||
describe('GET /shops/time-travelers', () => {
|
describe('GET /shops/time-travelers', () => {
|
||||||
let user;
|
let user;
|
||||||
|
let clock;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
user = await generateUser();
|
user = await generateUser();
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-06-08'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a valid shop object', async () => {
|
it('returns a valid shop object', async () => {
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,20 @@ describe('POST /user/purchase/:type/:key', () => {
|
||||||
expect(user.items[type][key]).to.equal(1);
|
expect(user.items[type][key]).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('purchases animal ears', async () => {
|
||||||
|
await user.post('/user/purchase/gear/headAccessory_special_tigerEars');
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
expect(user.items.gear.owned.headAccessory_special_tigerEars).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('purchases animal tails', async () => {
|
||||||
|
await user.post('/user/purchase/gear/back_special_pandaTail');
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
expect(user.items.gear.owned.back_special_pandaTail).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('can convert gold to gems if subscribed', async () => {
|
it('can convert gold to gems if subscribed', async () => {
|
||||||
const oldBalance = user.balance;
|
const oldBalance = user.balance;
|
||||||
await user.updateOne({
|
await user.updateOne({
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
|
|
||||||
describe('POST /user/unlock', () => {
|
describe('POST /user/unlock', () => {
|
||||||
let user;
|
let user;
|
||||||
const unlockPath = 'shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
|
const unlockPath = 'shirt.convict,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
|
||||||
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
|
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
|
||||||
const unlockCost = 1.25;
|
const unlockCost = 1.25;
|
||||||
const usersStartingGems = 5;
|
const usersStartingGems = 5;
|
||||||
|
|
|
||||||
|
|
@ -274,6 +274,14 @@ describe('PUT /user', () => {
|
||||||
expect(get(updatedUser.preferences, type)).to.eql(item);
|
expect(get(updatedUser.preferences, type)).to.eql(item);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('updates user when background is unequipped', async () => {
|
||||||
|
expect(get(user.preferences, 'background')).to.not.eql('');
|
||||||
|
|
||||||
|
const updatedUser = await user.put('/user', { 'preferences.background': '' });
|
||||||
|
|
||||||
|
expect(get(updatedUser.preferences, 'background')).to.eql('');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
context('Improvement Categories', () => {
|
context('Improvement Categories', () => {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ const { content } = shared;
|
||||||
|
|
||||||
describe('POST /user/buy/:key', () => {
|
describe('POST /user/buy/:key', () => {
|
||||||
let user;
|
let user;
|
||||||
|
let clock;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
user = await generateUser({
|
user = await generateUser({
|
||||||
|
|
@ -18,6 +19,12 @@ describe('POST /user/buy/:key', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (clock) {
|
||||||
|
clock.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// More tests in common code unit tests
|
// More tests in common code unit tests
|
||||||
|
|
||||||
it('returns an error if the item is not found', async () => {
|
it('returns an error if the item is not found', async () => {
|
||||||
|
|
@ -68,9 +75,9 @@ describe('POST /user/buy/:key', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('buys a special spell', async () => {
|
it('buys a special spell', async () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-10-31T00:00:00Z'));
|
||||||
const key = 'spookySparkles';
|
const key = 'spookySparkles';
|
||||||
const item = content.special[key];
|
const item = content.special[key];
|
||||||
const stub = sinon.stub(item, 'canOwn').returns(true);
|
|
||||||
|
|
||||||
await user.updateOne({ 'stats.gp': 250 });
|
await user.updateOne({ 'stats.gp': 250 });
|
||||||
const res = await user.post(`/user/buy/${key}`);
|
const res = await user.post(`/user/buy/${key}`);
|
||||||
|
|
@ -83,8 +90,6 @@ describe('POST /user/buy/:key', () => {
|
||||||
expect(res.message).to.equal(t('messageBought', {
|
expect(res.message).to.equal(t('messageBought', {
|
||||||
itemText: item.text(),
|
itemText: item.text(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
stub.restore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows for bulk purchases', async () => {
|
it('allows for bulk purchases', async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { TAVERN_ID } from '../../../../../website/server/models/group';
|
|
||||||
import { updateDocument } from '../../../../helpers/mongo';
|
|
||||||
import {
|
import {
|
||||||
requester,
|
requester,
|
||||||
resetHabiticaDB,
|
resetHabiticaDB,
|
||||||
|
|
@ -18,7 +16,9 @@ describe('GET /world-state', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns Tavern quest data when world boss is active', async () => {
|
it('returns Tavern quest data when world boss is active', async () => {
|
||||||
await updateDocument('groups', { _id: TAVERN_ID }, { quest: { active: true, key: 'dysheartener', progress: { hp: 50000, rage: 9999 } } });
|
sinon.stub(worldState, 'getWorldBoss').returns({
|
||||||
|
active: true, extra: {}, key: 'dysheartener', progress: { hp: 50000, rage: 9999, collect: {} },
|
||||||
|
});
|
||||||
|
|
||||||
const res = await requester().get('/world-state');
|
const res = await requester().get('/world-state');
|
||||||
expect(res).to.have.nested.property('worldBoss');
|
expect(res).to.have.nested.property('worldBoss');
|
||||||
|
|
@ -33,15 +33,29 @@ describe('GET /world-state', () => {
|
||||||
rage: 9999,
|
rage: 9999,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
worldState.getWorldBoss.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls getRepeatingEvents for data', async () => {
|
||||||
|
const getRepeatingEventsOnDate = sinon.stub(common.content, 'getRepeatingEventsOnDate').returns([]);
|
||||||
|
const getCurrentGalaEvent = sinon.stub(common.schedule, 'getCurrentGalaEvent').returns({});
|
||||||
|
|
||||||
|
await requester().get('/world-state');
|
||||||
|
|
||||||
|
expect(getRepeatingEventsOnDate).to.have.been.calledOnce;
|
||||||
|
expect(getCurrentGalaEvent).to.have.been.calledOnce;
|
||||||
|
|
||||||
|
getRepeatingEventsOnDate.restore();
|
||||||
|
getCurrentGalaEvent.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
context('no current event', () => {
|
context('no current event', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
sinon.stub(worldState, 'getCurrentEvent').returns(null);
|
sinon.stub(worldState, 'getCurrentEventList').returns([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
worldState.getCurrentEvent.restore();
|
worldState.getCurrentEventList.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns null for the current event when there is none active', async () => {
|
it('returns null for the current event when there is none active', async () => {
|
||||||
|
|
@ -51,18 +65,18 @@ describe('GET /world-state', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
context('no current event', () => {
|
context('active event', () => {
|
||||||
const evt = {
|
const evt = {
|
||||||
...common.content.events.fall2020,
|
...common.content.events.fall2020,
|
||||||
event: 'fall2020',
|
event: 'fall2020',
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
sinon.stub(worldState, 'getCurrentEvent').returns(evt);
|
sinon.stub(worldState, 'getCurrentEventList').returns([evt]);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
worldState.getCurrentEvent.restore();
|
worldState.getCurrentEventList.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the current event when there is an active one', async () => {
|
it('returns the current event when there is an active one', async () => {
|
||||||
|
|
@ -71,4 +85,45 @@ describe('GET /world-state', () => {
|
||||||
expect(res.currentEvent).to.eql(evt);
|
expect(res.currentEvent).to.eql(evt);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('active event with NPC image suffix', () => {
|
||||||
|
const evt = {
|
||||||
|
...common.content.events.fall2020,
|
||||||
|
event: 'fall2020',
|
||||||
|
npcImageSuffix: 'fall',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
sinon.stub(worldState, 'getCurrentEventList').returns([evt]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
worldState.getCurrentEventList.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the NPC image suffix when present', async () => {
|
||||||
|
const res = await requester().get('/world-state');
|
||||||
|
|
||||||
|
expect(res.npcImageSuffix).to.equal('fall');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the NPC image suffix with multiple events present', async () => {
|
||||||
|
const evt2 = {
|
||||||
|
...common.content.events.winter2020,
|
||||||
|
event: 'test',
|
||||||
|
};
|
||||||
|
|
||||||
|
const evt3 = {
|
||||||
|
...common.content.events.winter2020,
|
||||||
|
event: 'winter2020',
|
||||||
|
npcImageSuffix: 'winter',
|
||||||
|
};
|
||||||
|
|
||||||
|
worldState.getCurrentEventList.returns([evt, evt2, evt3]);
|
||||||
|
|
||||||
|
const res = await requester().get('/world-state');
|
||||||
|
|
||||||
|
expect(res.npcImageSuffix).to.equal('winter');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
67
test/common/fns/datedMemoize.test.js
Normal file
67
test/common/fns/datedMemoize.test.js
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
/* eslint-disable global-require */
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import nconf from 'nconf';
|
||||||
|
|
||||||
|
const SWITCHOVER_TIME = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0;
|
||||||
|
|
||||||
|
describe('datedMemoize', () => {
|
||||||
|
it('should return a function that returns a function', () => {
|
||||||
|
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
|
||||||
|
const memoized = datedMemoize(() => {});
|
||||||
|
expect(memoized).to.be.a('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call multiple times', () => {
|
||||||
|
const stub = sandbox.stub().returns({});
|
||||||
|
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
|
||||||
|
const memoized = datedMemoize(stub);
|
||||||
|
memoized(1, 2);
|
||||||
|
memoized(1, 3);
|
||||||
|
expect(stub).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('call multiple times for different identifiers', () => {
|
||||||
|
const stub = sandbox.stub().returns({});
|
||||||
|
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
|
||||||
|
const memoized = datedMemoize(stub);
|
||||||
|
memoized({ identifier: 'a', memoizeConfig: true }, 1, 2);
|
||||||
|
memoized({ identifier: 'b', memoizeConfig: true }, 1, 2);
|
||||||
|
expect(stub).to.have.been.calledTwice;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('call once for the same identifier', () => {
|
||||||
|
const stub = sandbox.stub().returns({});
|
||||||
|
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
|
||||||
|
const memoized = datedMemoize(stub);
|
||||||
|
memoized({ identifier: 'a', memoizeConfig: true }, 1, 2);
|
||||||
|
memoized({ identifier: 'a', memoizeConfig: true }, 1, 2);
|
||||||
|
expect(stub).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('call once on the same day', () => {
|
||||||
|
const stub = sandbox.stub().returns({});
|
||||||
|
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
|
||||||
|
const memoized = datedMemoize(stub);
|
||||||
|
memoized({ date: new Date('2024-01-01'), memoizeConfig: true }, 1, 2);
|
||||||
|
memoized({ date: new Date('2024-01-01'), memoizeConfig: true }, 1, 2);
|
||||||
|
expect(stub).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('call multiple times on different days', () => {
|
||||||
|
const stub = sandbox.stub().returns({});
|
||||||
|
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
|
||||||
|
const memoized = datedMemoize(stub);
|
||||||
|
memoized({ date: new Date('2024-01-01'), memoizeConfig: true }, 1, 2);
|
||||||
|
memoized({ date: new Date('2024-01-02'), memoizeConfig: true }, 1, 2);
|
||||||
|
expect(stub).to.have.been.calledTwice;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects switchover time', () => {
|
||||||
|
const stub = sandbox.stub().returns({});
|
||||||
|
const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default;
|
||||||
|
const memoized = datedMemoize(stub);
|
||||||
|
memoized({ date: new Date('2024-01-01T00:00:00.000Z'), memoizeConfig: true }, 1, 2);
|
||||||
|
memoized({ date: new Date(`2024-01-01T${String(SWITCHOVER_TIME).padStart(2, '0')}`), memoizeConfig: true }, 1, 2);
|
||||||
|
expect(stub).to.have.been.calledTwice;
|
||||||
|
});
|
||||||
|
});
|
||||||
123
test/common/libs/cleanupPinnedItems.test.js
Normal file
123
test/common/libs/cleanupPinnedItems.test.js
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
} from '../../helpers/common.helper';
|
||||||
|
import cleanupPinnedItems from '../../../website/common/script/libs/cleanupPinnedItems';
|
||||||
|
|
||||||
|
describe('cleanupPinnedItems', () => {
|
||||||
|
let user;
|
||||||
|
let testPinnedItems;
|
||||||
|
let clock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
user = generateUser();
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-04-08'));
|
||||||
|
|
||||||
|
testPinnedItems = [
|
||||||
|
{ type: 'armoire', path: 'armoire' },
|
||||||
|
{ type: 'potion', path: 'potion' },
|
||||||
|
{ type: 'background', path: 'backgrounds.backgrounds042020.heather_field' },
|
||||||
|
{ type: 'background', path: 'backgrounds.backgrounds042021.heather_field' },
|
||||||
|
{ type: 'premiumHatchingPotion', path: 'premiumHatchingPotions.Rainbow' },
|
||||||
|
{ type: 'premiumHatchingPotion', path: 'premiumHatchingPotions.StainedGlass' },
|
||||||
|
{ type: 'quests', path: 'quests.rat' },
|
||||||
|
{ type: 'quests', path: 'quests.spider' },
|
||||||
|
{ type: 'quests', path: 'quests.moon1' },
|
||||||
|
{ type: 'quests', path: 'quests.silver' },
|
||||||
|
{ type: 'marketGear', path: 'gear.flat.head_special_nye2021' },
|
||||||
|
{ type: 'gear', path: 'gear.flat.armor_special_spring2019Rogue' },
|
||||||
|
{ type: 'gear', path: 'gear.flat.armor_special_winter2021Rogue' },
|
||||||
|
{ type: 'mystery_set', path: 'mystery.201804' },
|
||||||
|
{ type: 'mystery_set', path: 'mystery.201506' },
|
||||||
|
{ type: 'bundles', path: 'bundles.farmFriends' },
|
||||||
|
{ type: 'bundles', path: 'bundles.birdBuddies' },
|
||||||
|
{ type: 'customization', path: 'skin.birdBuddies' },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clock.restore();
|
||||||
|
});
|
||||||
|
it('always keeps armoire and potion', () => {
|
||||||
|
user.pinnedItems = testPinnedItems;
|
||||||
|
|
||||||
|
const result = cleanupPinnedItems(user);
|
||||||
|
expect(_.find(result, item => item.path === 'armoire')).to.exist;
|
||||||
|
expect(_.find(result, item => item.path === 'potion')).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes simple items that are no longer available', () => {
|
||||||
|
user.pinnedItems = testPinnedItems;
|
||||||
|
|
||||||
|
const result = cleanupPinnedItems(user);
|
||||||
|
expect(_.find(result, item => item.path === 'backgrounds.backgrounds042021.heather_field')).to.not.exist;
|
||||||
|
expect(_.find(result, item => item.path === 'premiumHatchingPotions.Rainbow')).to.not.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps simple items that are still available', () => {
|
||||||
|
user.pinnedItems = testPinnedItems;
|
||||||
|
const result = cleanupPinnedItems(user);
|
||||||
|
expect(_.find(result, item => item.path === 'backgrounds.backgrounds042020.heather_field')).to.exist;
|
||||||
|
expect(_.find(result, item => item.path === 'premiumHatchingPotions.StainedGlass')).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes gear that is no longer available', () => {
|
||||||
|
user.pinnedItems = testPinnedItems;
|
||||||
|
const result = cleanupPinnedItems(user);
|
||||||
|
expect(_.find(result, item => item.path === 'gear.flat.armor_special_winter2021Rogue')).to.not.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps gear that is still available', () => {
|
||||||
|
user.pinnedItems = testPinnedItems;
|
||||||
|
const result = cleanupPinnedItems(user);
|
||||||
|
expect(_.find(result, item => item.path === 'gear.flat.armor_special_spring2019Rogue')).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps gear that is not seasonal', () => {
|
||||||
|
user.pinnedItems = testPinnedItems;
|
||||||
|
const result = cleanupPinnedItems(user);
|
||||||
|
expect(_.find(result, item => item.path === 'gear.flat.head_special_nye2021')).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes time traveler gear that is no longer available', () => {
|
||||||
|
user.pinnedItems = testPinnedItems;
|
||||||
|
const result = cleanupPinnedItems(user);
|
||||||
|
expect(_.find(result, item => item.path === 'mystery.201506')).to.not.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps time traveler gear that is still available', () => {
|
||||||
|
user.pinnedItems = testPinnedItems;
|
||||||
|
const result = cleanupPinnedItems(user);
|
||||||
|
expect(_.find(result, item => item.path === 'mystery.201804')).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes quests that are no longer available', () => {
|
||||||
|
user.pinnedItems = testPinnedItems;
|
||||||
|
const result = cleanupPinnedItems(user);
|
||||||
|
expect(_.find(result, item => item.path === 'quests.rat')).to.not.exist;
|
||||||
|
expect(_.find(result, item => item.path === 'quests.silver')).to.not.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps quests that are still available', () => {
|
||||||
|
user.pinnedItems = testPinnedItems;
|
||||||
|
const result = cleanupPinnedItems(user);
|
||||||
|
expect(_.find(result, item => item.path === 'quests.spider')).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps quests that are not seasonal', () => {
|
||||||
|
user.pinnedItems = testPinnedItems;
|
||||||
|
const result = cleanupPinnedItems(user);
|
||||||
|
expect(_.find(result, item => item.path === 'quests.moon1')).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes bundles that are no longer available', () => {
|
||||||
|
user.pinnedItems = testPinnedItems;
|
||||||
|
const result = cleanupPinnedItems(user);
|
||||||
|
expect(_.find(result, item => item.path === 'bundles.farmFriends')).to.not.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps bundles that are still available', () => {
|
||||||
|
user.pinnedItems = testPinnedItems;
|
||||||
|
const result = cleanupPinnedItems(user);
|
||||||
|
expect(_.find(result, item => item.path === 'bundles.birdBuddies')).to.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,219 +0,0 @@
|
||||||
import shared from '../../../website/common';
|
|
||||||
import {
|
|
||||||
generateUser,
|
|
||||||
} from '../../helpers/common.helper';
|
|
||||||
|
|
||||||
describe('shops', () => {
|
|
||||||
const user = generateUser();
|
|
||||||
|
|
||||||
describe('market', () => {
|
|
||||||
const shopCategories = shared.shops.getMarketCategories(user);
|
|
||||||
|
|
||||||
it('contains at least the 3 default categories', () => {
|
|
||||||
expect(shopCategories.length).to.be.greaterThan(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not contain an empty category', () => {
|
|
||||||
_.each(shopCategories, category => {
|
|
||||||
expect(category.items.length).to.be.greaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not duplicate identifiers', () => {
|
|
||||||
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
|
||||||
|
|
||||||
expect(identifiers.length).to.eql(shopCategories.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('items contain required fields', () => {
|
|
||||||
_.each(shopCategories, category => {
|
|
||||||
_.each(category.items, item => {
|
|
||||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'class'], key => {
|
|
||||||
expect(_.has(item, key)).to.eql(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows relevant non class gear in special category', () => {
|
|
||||||
const contributor = generateUser({
|
|
||||||
contributor: {
|
|
||||||
level: 7,
|
|
||||||
critical: true,
|
|
||||||
},
|
|
||||||
items: {
|
|
||||||
gear: {
|
|
||||||
owned: {
|
|
||||||
weapon_armoire_basicCrossbow: true, // eslint-disable-line camelcase
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const gearCategories = shared.shops.getMarketGearCategories(contributor);
|
|
||||||
const specialCategory = gearCategories.find(o => o.identifier === 'none');
|
|
||||||
expect(specialCategory.items.find(item => item.key === 'weapon_special_1'));
|
|
||||||
expect(specialCategory.items.find(item => item.key === 'armor_special_1'));
|
|
||||||
expect(specialCategory.items.find(item => item.key === 'head_special_1'));
|
|
||||||
expect(specialCategory.items.find(item => item.key === 'shield_special_1'));
|
|
||||||
expect(specialCategory.items.find(item => item.key === 'weapon_special_critical'));
|
|
||||||
expect(specialCategory.items.find(item => item.key === 'weapon_armoire_basicCrossbow'));// eslint-disable-line camelcase
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not show gear when it is all owned', () => {
|
|
||||||
const userWithItems = generateUser({
|
|
||||||
stats: {
|
|
||||||
class: 'wizard',
|
|
||||||
},
|
|
||||||
items: {
|
|
||||||
gear: {
|
|
||||||
owned: {
|
|
||||||
weapon_wizard_0: true, // eslint-disable-line camelcase
|
|
||||||
weapon_wizard_1: true, // eslint-disable-line camelcase
|
|
||||||
weapon_wizard_2: true, // eslint-disable-line camelcase
|
|
||||||
weapon_wizard_3: true, // eslint-disable-line camelcase
|
|
||||||
weapon_wizard_4: true, // eslint-disable-line camelcase
|
|
||||||
weapon_wizard_5: true, // eslint-disable-line camelcase
|
|
||||||
weapon_wizard_6: true, // eslint-disable-line camelcase
|
|
||||||
armor_wizard_1: true, // eslint-disable-line camelcase
|
|
||||||
armor_wizard_2: true, // eslint-disable-line camelcase
|
|
||||||
armor_wizard_3: true, // eslint-disable-line camelcase
|
|
||||||
armor_wizard_4: true, // eslint-disable-line camelcase
|
|
||||||
armor_wizard_5: true, // eslint-disable-line camelcase
|
|
||||||
head_wizard_1: true, // eslint-disable-line camelcase
|
|
||||||
head_wizard_2: true, // eslint-disable-line camelcase
|
|
||||||
head_wizard_3: true, // eslint-disable-line camelcase
|
|
||||||
head_wizard_4: true, // eslint-disable-line camelcase
|
|
||||||
head_wizard_5: true, // eslint-disable-line camelcase
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
|
|
||||||
expect(shopWizardItems.length).to.eql(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows available gear not yet purchased and previously owned', () => {
|
|
||||||
const userWithItems = generateUser({
|
|
||||||
stats: {
|
|
||||||
class: 'wizard',
|
|
||||||
},
|
|
||||||
items: {
|
|
||||||
gear: {
|
|
||||||
owned: {
|
|
||||||
weapon_wizard_0: true, // eslint-disable-line camelcase
|
|
||||||
weapon_wizard_1: true, // eslint-disable-line camelcase
|
|
||||||
weapon_wizard_2: true, // eslint-disable-line camelcase
|
|
||||||
weapon_wizard_3: true, // eslint-disable-line camelcase
|
|
||||||
weapon_wizard_4: true, // eslint-disable-line camelcase
|
|
||||||
armor_wizard_1: true, // eslint-disable-line camelcase
|
|
||||||
armor_wizard_2: true, // eslint-disable-line camelcase
|
|
||||||
armor_wizard_3: false, // eslint-disable-line camelcase
|
|
||||||
armor_wizard_4: false, // eslint-disable-line camelcase
|
|
||||||
head_wizard_1: true, // eslint-disable-line camelcase
|
|
||||||
head_wizard_2: false, // eslint-disable-line camelcase
|
|
||||||
head_wizard_3: true, // eslint-disable-line camelcase
|
|
||||||
head_wizard_4: false, // eslint-disable-line camelcase
|
|
||||||
head_wizard_5: true, // eslint-disable-line camelcase
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
|
|
||||||
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_5').locked).to.eql(false);
|
|
||||||
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_6').locked).to.eql(true);
|
|
||||||
expect(shopWizardItems.find(item => item.key === 'armor_wizard_3').locked).to.eql(false);
|
|
||||||
expect(shopWizardItems.find(item => item.key === 'armor_wizard_4').locked).to.eql(true);
|
|
||||||
expect(shopWizardItems.find(item => item.key === 'head_wizard_2').locked).to.eql(false);
|
|
||||||
expect(shopWizardItems.find(item => item.key === 'head_wizard_4').locked).to.eql(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('questShop', () => {
|
|
||||||
const shopCategories = shared.shops.getQuestShopCategories(user);
|
|
||||||
|
|
||||||
it('does not contain an empty category', () => {
|
|
||||||
_.each(shopCategories, category => {
|
|
||||||
expect(category.items.length).to.be.greaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not duplicate identifiers', () => {
|
|
||||||
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
|
||||||
|
|
||||||
expect(identifiers.length).to.eql(shopCategories.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('items contain required fields', () => {
|
|
||||||
_.each(shopCategories, category => {
|
|
||||||
if (category.identifier === 'bundle') {
|
|
||||||
_.each(category.items, item => {
|
|
||||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'purchaseType', 'class'], key => {
|
|
||||||
expect(_.has(item, key)).to.eql(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_.each(category.items, item => {
|
|
||||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], key => {
|
|
||||||
expect(_.has(item, key)).to.eql(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('timeTravelers', () => {
|
|
||||||
const shopCategories = shared.shops.getTimeTravelersCategories(user);
|
|
||||||
|
|
||||||
it('does not contain an empty category', () => {
|
|
||||||
_.each(shopCategories, category => {
|
|
||||||
expect(category.items.length).to.be.greaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not duplicate identifiers', () => {
|
|
||||||
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
|
||||||
|
|
||||||
expect(identifiers.length).to.eql(shopCategories.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('items contain required fields', () => {
|
|
||||||
_.each(shopCategories, category => {
|
|
||||||
_.each(category.items, item => {
|
|
||||||
_.each(['key', 'text', 'value', 'currency', 'locked', 'purchaseType', 'class', 'notes', 'class'], key => {
|
|
||||||
expect(_.has(item, key)).to.eql(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('seasonalShop', () => {
|
|
||||||
const shopCategories = shared.shops.getSeasonalShopCategories(user);
|
|
||||||
|
|
||||||
it('does not contain an empty category', () => {
|
|
||||||
_.each(shopCategories, category => {
|
|
||||||
expect(category.items.length).to.be.greaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not duplicate identifiers', () => {
|
|
||||||
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
|
||||||
|
|
||||||
expect(identifiers.length).to.eql(shopCategories.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('items contain required fields', () => {
|
|
||||||
_.each(shopCategories, category => {
|
|
||||||
_.each(category.items, item => {
|
|
||||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'type'], key => {
|
|
||||||
expect(_.has(item, key)).to.eql(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
430
test/common/libs/shops.test.js
Normal file
430
test/common/libs/shops.test.js
Normal file
|
|
@ -0,0 +1,430 @@
|
||||||
|
import shared from '../../../website/common';
|
||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
} from '../../helpers/common.helper';
|
||||||
|
|
||||||
|
import seasonalConfig from '../../../website/common/script/libs/shops-seasonal.config';
|
||||||
|
|
||||||
|
describe('shops', () => {
|
||||||
|
const user = generateUser();
|
||||||
|
let clock;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (clock) {
|
||||||
|
clock.restore();
|
||||||
|
}
|
||||||
|
user.achievements.quests = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('market', () => {
|
||||||
|
const shopCategories = shared.shops.getMarketCategories(user);
|
||||||
|
|
||||||
|
it('contains at least the 3 default categories', () => {
|
||||||
|
expect(shopCategories.length).to.be.greaterThan(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not contain an empty category', () => {
|
||||||
|
_.each(shopCategories, category => {
|
||||||
|
expect(category.items.length).to.be.greaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not duplicate identifiers', () => {
|
||||||
|
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
||||||
|
|
||||||
|
expect(identifiers.length).to.eql(shopCategories.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('items contain required fields', () => {
|
||||||
|
_.each(shopCategories, category => {
|
||||||
|
_.each(category.items, item => {
|
||||||
|
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'class'], key => {
|
||||||
|
expect(_.has(item, key)).to.eql(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('premium hatching potions', () => {
|
||||||
|
it('contains current scheduled premium hatching potions', async () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-04-01'));
|
||||||
|
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
|
||||||
|
expect(potions.items.length).to.eql(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not contain past scheduled premium hatching potions', async () => {
|
||||||
|
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
|
||||||
|
expect(potions.items.filter(x => x.key === 'Aquatic' || x.key === 'Celestial').length).to.eql(0);
|
||||||
|
});
|
||||||
|
it('returns end date for scheduled premium potions', async () => {
|
||||||
|
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
|
||||||
|
potions.items.forEach(potion => {
|
||||||
|
expect(potion.end).to.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains unlocked quest premium hatching potions', async () => {
|
||||||
|
user.achievements.quests = {
|
||||||
|
bronze: 1,
|
||||||
|
blackPearl: 1,
|
||||||
|
};
|
||||||
|
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
|
||||||
|
expect(potions.items.filter(x => x.key === 'Bronze' || x.key === 'BlackPearl').length).to.eql(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not contain locked quest premium hatching potions', async () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-04-01'));
|
||||||
|
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
|
||||||
|
expect(potions.items.length).to.eql(2);
|
||||||
|
expect(potions.items.filter(x => x.key === 'Bronze' || x.key === 'BlackPearl').length).to.eql(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not return end date for quest premium potions', async () => {
|
||||||
|
user.achievements.quests = {
|
||||||
|
bronze: 1,
|
||||||
|
blackPearl: 1,
|
||||||
|
};
|
||||||
|
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
|
||||||
|
potions.items.filter(x => x.key === 'Bronze' || x.key === 'BlackPearl').forEach(potion => {
|
||||||
|
expect(potion.end).to.not.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not return items with event data', async () => {
|
||||||
|
shopCategories.forEach(category => {
|
||||||
|
category.items.forEach(item => {
|
||||||
|
expect(item.event).to.not.exist;
|
||||||
|
expect(item.season).to.not.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows relevant non class gear in special category', () => {
|
||||||
|
const contributor = generateUser({
|
||||||
|
contributor: {
|
||||||
|
level: 7,
|
||||||
|
critical: true,
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
gear: {
|
||||||
|
owned: {
|
||||||
|
weapon_armoire_basicCrossbow: true, // eslint-disable-line camelcase
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const gearCategories = shared.shops.getMarketGearCategories(contributor);
|
||||||
|
const specialCategory = gearCategories.find(o => o.identifier === 'none');
|
||||||
|
expect(specialCategory.items.find(item => item.key === 'weapon_special_1'), 'weapon_special_1');
|
||||||
|
expect(specialCategory.items.find(item => item.key === 'armor_special_1'), 'armor_special_1');
|
||||||
|
expect(specialCategory.items.find(item => item.key === 'head_special_1'), 'head_special_1');
|
||||||
|
expect(specialCategory.items.find(item => item.key === 'shield_special_1'), 'shield_special_1');
|
||||||
|
expect(specialCategory.items.find(item => item.key === 'weapon_special_critical'), 'weapon_special_critical');
|
||||||
|
expect(specialCategory.items.find(item => item.key === 'weapon_armoire_basicCrossbow'), 'weapon_armoire_basicCrossbow');// eslint-disable-line camelcase
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handles seasonal gear', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-04-01'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows current seasonal gear for warriors', () => {
|
||||||
|
const warriorItems = shared.shops.getMarketGearCategories(user).find(x => x.identifier === 'warrior').items.filter(x => x.key.indexOf('spring2024') !== -1);
|
||||||
|
expect(warriorItems.length, 'Warrior seasonal gear').to.eql(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows current seasonal gear for mages', () => {
|
||||||
|
const mageItems = shared.shops.getMarketGearCategories(user).find(x => x.identifier === 'wizard').items.filter(x => x.key.indexOf('spring2024') !== -1);
|
||||||
|
expect(mageItems.length, 'Mage seasonal gear').to.eql(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows current seasonal gear for healers', () => {
|
||||||
|
const healerItems = shared.shops.getMarketGearCategories(user).find(x => x.identifier === 'healer').items.filter(x => x.key.indexOf('spring2024') !== -1);
|
||||||
|
expect(healerItems.length, 'Healer seasonal gear').to.eql(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows current seasonal gear for rogues', () => {
|
||||||
|
const rogueItems = shared.shops.getMarketGearCategories(user).find(x => x.identifier === 'rogue').items.filter(x => x.key.indexOf('spring2024') !== -1);
|
||||||
|
expect(rogueItems.length, 'Rogue seasonal gear').to.eql(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('seasonal gear contains end date', () => {
|
||||||
|
const categories = shared.shops.getMarketGearCategories(user);
|
||||||
|
categories.forEach(category => {
|
||||||
|
category.items.filter(x => x.key.indexOf('spring2024') !== -1).forEach(item => {
|
||||||
|
expect(item.end, item.key).to.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only shows gear for the current season', () => {
|
||||||
|
const categories = shared.shops.getMarketGearCategories(user);
|
||||||
|
categories.forEach(category => {
|
||||||
|
const otherSeasons = category.items.filter(item => item.key.indexOf('winter') !== -1 || item.key.indexOf('summer') !== -1 || item.key.indexOf('fall') !== -1);
|
||||||
|
expect(otherSeasons.length).to.eql(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show gear from past seasons', () => {
|
||||||
|
const categories = shared.shops.getMarketGearCategories(user);
|
||||||
|
categories.forEach(category => {
|
||||||
|
const otherYears = category.items.filter(item => item.key.indexOf('spring') !== -1 && item.key.indexOf('2024') === -1);
|
||||||
|
expect(otherYears.length).to.eql(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show gear when it is all owned', () => {
|
||||||
|
const userWithItems = generateUser({
|
||||||
|
stats: {
|
||||||
|
class: 'wizard',
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
gear: {
|
||||||
|
owned: {
|
||||||
|
weapon_wizard_0: true, // eslint-disable-line camelcase
|
||||||
|
weapon_wizard_1: true, // eslint-disable-line camelcase
|
||||||
|
weapon_wizard_2: true, // eslint-disable-line camelcase
|
||||||
|
weapon_wizard_3: true, // eslint-disable-line camelcase
|
||||||
|
weapon_wizard_4: true, // eslint-disable-line camelcase
|
||||||
|
weapon_wizard_5: true, // eslint-disable-line camelcase
|
||||||
|
weapon_wizard_6: true, // eslint-disable-line camelcase
|
||||||
|
armor_wizard_1: true, // eslint-disable-line camelcase
|
||||||
|
armor_wizard_2: true, // eslint-disable-line camelcase
|
||||||
|
armor_wizard_3: true, // eslint-disable-line camelcase
|
||||||
|
armor_wizard_4: true, // eslint-disable-line camelcase
|
||||||
|
armor_wizard_5: true, // eslint-disable-line camelcase
|
||||||
|
head_wizard_1: true, // eslint-disable-line camelcase
|
||||||
|
head_wizard_2: true, // eslint-disable-line camelcase
|
||||||
|
head_wizard_3: true, // eslint-disable-line camelcase
|
||||||
|
head_wizard_4: true, // eslint-disable-line camelcase
|
||||||
|
head_wizard_5: true, // eslint-disable-line camelcase
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
|
||||||
|
expect(shopWizardItems.length).to.eql(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows available gear not yet purchased and previously owned', () => {
|
||||||
|
const userWithItems = generateUser({
|
||||||
|
stats: {
|
||||||
|
class: 'wizard',
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
gear: {
|
||||||
|
owned: {
|
||||||
|
weapon_wizard_0: true, // eslint-disable-line camelcase
|
||||||
|
weapon_wizard_1: true, // eslint-disable-line camelcase
|
||||||
|
weapon_wizard_2: true, // eslint-disable-line camelcase
|
||||||
|
weapon_wizard_3: true, // eslint-disable-line camelcase
|
||||||
|
weapon_wizard_4: true, // eslint-disable-line camelcase
|
||||||
|
armor_wizard_1: true, // eslint-disable-line camelcase
|
||||||
|
armor_wizard_2: true, // eslint-disable-line camelcase
|
||||||
|
armor_wizard_3: false, // eslint-disable-line camelcase
|
||||||
|
armor_wizard_4: false, // eslint-disable-line camelcase
|
||||||
|
head_wizard_1: true, // eslint-disable-line camelcase
|
||||||
|
head_wizard_2: false, // eslint-disable-line camelcase
|
||||||
|
head_wizard_3: true, // eslint-disable-line camelcase
|
||||||
|
head_wizard_4: false, // eslint-disable-line camelcase
|
||||||
|
head_wizard_5: true, // eslint-disable-line camelcase
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const shopWizardItems = shared.shops.getMarketGearCategories(userWithItems).find(x => x.identifier === 'wizard').items.filter(x => x.klass === 'wizard' && (x.owned === false || x.owned === undefined));
|
||||||
|
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_5').locked).to.eql(false);
|
||||||
|
expect(shopWizardItems.find(item => item.key === 'weapon_wizard_6').locked).to.eql(true);
|
||||||
|
expect(shopWizardItems.find(item => item.key === 'armor_wizard_3').locked).to.eql(false);
|
||||||
|
expect(shopWizardItems.find(item => item.key === 'armor_wizard_4').locked).to.eql(true);
|
||||||
|
expect(shopWizardItems.find(item => item.key === 'head_wizard_2').locked).to.eql(false);
|
||||||
|
expect(shopWizardItems.find(item => item.key === 'head_wizard_4').locked).to.eql(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('questShop', () => {
|
||||||
|
const shopCategories = shared.shops.getQuestShopCategories(user);
|
||||||
|
|
||||||
|
it('does not contain an empty category', () => {
|
||||||
|
_.each(shopCategories, category => {
|
||||||
|
expect(category.items.length, category.identifier).to.be.greaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not duplicate identifiers', () => {
|
||||||
|
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
||||||
|
|
||||||
|
expect(identifiers.length).to.eql(shopCategories.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('items contain required fields', () => {
|
||||||
|
_.each(shopCategories, category => {
|
||||||
|
if (category.identifier === 'bundle') {
|
||||||
|
_.each(category.items, item => {
|
||||||
|
_.each(['key', 'text', 'notes', 'value', 'currency', 'purchaseType', 'class'], key => {
|
||||||
|
expect(_.has(item, key)).to.eql(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_.each(category.items, item => {
|
||||||
|
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], key => {
|
||||||
|
expect(_.has(item, key)).to.eql(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not return items with event data', async () => {
|
||||||
|
shopCategories.forEach(category => {
|
||||||
|
category.items.forEach(item => {
|
||||||
|
expect(item.event).to.not.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('timeTravelers', () => {
|
||||||
|
const shopCategories = shared.shops.getTimeTravelersCategories(user);
|
||||||
|
|
||||||
|
it('does not contain an empty category', () => {
|
||||||
|
_.each(shopCategories, category => {
|
||||||
|
expect(category.items.length).to.be.greaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not duplicate identifiers', () => {
|
||||||
|
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
||||||
|
|
||||||
|
expect(identifiers.length).to.eql(shopCategories.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('items contain required fields', () => {
|
||||||
|
_.each(shopCategories, category => {
|
||||||
|
_.each(category.items, item => {
|
||||||
|
_.each(['key', 'text', 'value', 'currency', 'locked', 'purchaseType', 'class', 'notes', 'class'], key => {
|
||||||
|
expect(_.has(item, key)).to.eql(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not return items with event data', async () => {
|
||||||
|
shopCategories.forEach(category => {
|
||||||
|
category.items.forEach(item => {
|
||||||
|
expect(item.event).to.not.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns pets', () => {
|
||||||
|
const pets = shopCategories.find(cat => cat.identifier === 'pets').items;
|
||||||
|
expect(pets.length).to.be.greaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns mounts', () => {
|
||||||
|
const mounts = shopCategories.find(cat => cat.identifier === 'mounts').items;
|
||||||
|
expect(mounts.length).to.be.greaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns quests', () => {
|
||||||
|
const quests = shopCategories.find(cat => cat.identifier === 'quests').items;
|
||||||
|
expect(quests.length).to.be.greaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns backgrounds', () => {
|
||||||
|
const backgrounds = shopCategories.find(cat => cat.identifier === 'backgrounds').items;
|
||||||
|
expect(backgrounds.length).to.be.greaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('customizationShop', () => {
|
||||||
|
const shopCategories = shared.shops.getCustomizationsShopCategories(user, null);
|
||||||
|
|
||||||
|
it('does not return items with event data', async () => {
|
||||||
|
shopCategories.forEach(category => {
|
||||||
|
category.items.forEach(item => {
|
||||||
|
expect(item.event, item.key).to.not.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('backgrounds category contains end date', () => {
|
||||||
|
const backgroundCategory = shopCategories.find(cat => cat.identifier === 'backgrounds');
|
||||||
|
expect(backgroundCategory.end).to.exist;
|
||||||
|
expect(backgroundCategory.end).to.be.greaterThan(new Date());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hair color category contains end date', () => {
|
||||||
|
const colorCategory = shopCategories.find(cat => cat.identifier === 'color');
|
||||||
|
expect(colorCategory.end).to.exist;
|
||||||
|
expect(colorCategory.end).to.be.greaterThan(new Date());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('skin category contains end date', () => {
|
||||||
|
const colorCategory = shopCategories.find(cat => cat.identifier === 'color');
|
||||||
|
expect(colorCategory.end).to.exist;
|
||||||
|
expect(colorCategory.end).to.be.greaterThan(new Date());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('seasonalShop', () => {
|
||||||
|
const shopCategories = shared.shops.getSeasonalShopCategories(user, null, seasonalConfig());
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
it('does not contain an empty category', () => {
|
||||||
|
_.each(shopCategories, category => {
|
||||||
|
expect(category.items.length).to.be.greaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not duplicate identifiers', () => {
|
||||||
|
const identifiers = Array.from(new Set(shopCategories.map(cat => cat.identifier)));
|
||||||
|
|
||||||
|
expect(identifiers.length).to.eql(shopCategories.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not return items with event data', async () => {
|
||||||
|
shopCategories.forEach(category => {
|
||||||
|
category.items.forEach(item => {
|
||||||
|
expect(item.event, item.key).to.not.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('items contain required fields', () => {
|
||||||
|
_.each(shopCategories, category => {
|
||||||
|
_.each(category.items, item => {
|
||||||
|
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'type'], key => {
|
||||||
|
expect(_.has(item, key), item.key).to.eql(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('items have a valid end date', () => {
|
||||||
|
shopCategories.forEach(category => {
|
||||||
|
category.items.forEach(item => {
|
||||||
|
expect(item.end, item.key).to.be.a('date');
|
||||||
|
expect(item.end, item.key).to.be.greaterThan(today);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('items match current season', () => {
|
||||||
|
const currentSeason = seasonalConfig().currentSeason.toLowerCase();
|
||||||
|
shopCategories.forEach(category => {
|
||||||
|
category.items.forEach(item => {
|
||||||
|
if (item.klass === 'special') {
|
||||||
|
expect(item.season, item.key).to.eql(currentSeason);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import * as armoireSet from '../../../website/common/script/content/gear/sets/armoire';
|
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 => {
|
||||||
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({
|
||||||
items: {
|
items: {
|
||||||
gear: {
|
gear: {
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ describe('shared.ops.buy', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('recovers 15 hp', async () => {
|
it('buys health potion', async () => {
|
||||||
user.stats.hp = 30;
|
user.stats.hp = 30;
|
||||||
await buy(user, { params: { key: 'potion' } }, analytics);
|
await buy(user, { params: { key: 'potion' } }, analytics);
|
||||||
expect(user.stats.hp).to.eql(45);
|
expect(user.stats.hp).to.eql(45);
|
||||||
|
|
@ -17,9 +17,7 @@ function getFullArmoire () {
|
||||||
|
|
||||||
_.each(content.gearTypes, type => {
|
_.each(content.gearTypes, type => {
|
||||||
_.each(content.gear.tree[type].armoire, gearObject => {
|
_.each(content.gear.tree[type].armoire, gearObject => {
|
||||||
if (gearObject.released) {
|
fullArmoire[gearObject.key] = true;
|
||||||
fullArmoire[gearObject.key] = true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -22,6 +22,7 @@ async function buyGear (user, req, analytics) {
|
||||||
describe('shared.ops.buyMarketGear', () => {
|
describe('shared.ops.buyMarketGear', () => {
|
||||||
let user;
|
let user;
|
||||||
const analytics = { track () {} };
|
const analytics = { track () {} };
|
||||||
|
let clock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
user = generateUser({
|
user = generateUser({
|
||||||
|
|
@ -54,6 +55,10 @@ describe('shared.ops.buyMarketGear', () => {
|
||||||
shared.fns.predictableRandom.restore();
|
shared.fns.predictableRandom.restore();
|
||||||
shared.onboarding.checkOnboardingStatus.restore();
|
shared.onboarding.checkOnboardingStatus.restore();
|
||||||
analytics.track.restore();
|
analytics.track.restore();
|
||||||
|
|
||||||
|
if (clock) {
|
||||||
|
clock.restore();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
context('Gear', () => {
|
context('Gear', () => {
|
||||||
|
|
@ -184,30 +189,28 @@ describe('shared.ops.buyMarketGear', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO after user.ops.equip is done
|
// TODO after user.ops.equip is done
|
||||||
xit('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', async () => {
|
it('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', async () => {
|
||||||
user.stats.gp = 100;
|
user.stats.gp = 100;
|
||||||
user.preferences.autoEquip = true;
|
user.preferences.autoEquip = true;
|
||||||
await buyGear(user, { params: { key: 'shield_warrior_1' } });
|
user.items.gear.equipped.weapon = 'weapon_warrior_1';
|
||||||
user.ops.equip({ params: { key: 'shield_warrior_1' } });
|
user.items.gear.equipped.shield = 'shield_warrior_1';
|
||||||
await buyGear(user, { params: { key: 'weapon_warrior_1' } });
|
user.stats.class = 'wizard';
|
||||||
user.ops.equip({ params: { key: 'weapon_warrior_1' } });
|
|
||||||
|
|
||||||
await buyGear(user, { params: { key: 'weapon_wizard_1' } });
|
await buyGear(user, { params: { key: 'weapon_wizard_0' } });
|
||||||
|
|
||||||
expect(user.items.gear.equipped).to.have.property('shield', 'shield_base_0');
|
expect(user.items.gear.equipped).to.have.property('shield', 'shield_base_0');
|
||||||
expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_wizard_1');
|
expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_wizard_0');
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO after user.ops.equip is done
|
// TODO after user.ops.equip is done
|
||||||
xit('buyGears two-handed equipment but does not automatically remove sword or shield', async () => {
|
it('buyGears two-handed equipment but does not automatically remove sword or shield', async () => {
|
||||||
user.stats.gp = 100;
|
user.stats.gp = 100;
|
||||||
user.preferences.autoEquip = false;
|
user.preferences.autoEquip = false;
|
||||||
await buyGear(user, { params: { key: 'shield_warrior_1' } });
|
user.items.gear.equipped.weapon = 'weapon_warrior_1';
|
||||||
user.ops.equip({ params: { key: 'shield_warrior_1' } });
|
user.items.gear.equipped.shield = 'shield_warrior_1';
|
||||||
await buyGear(user, { params: { key: 'weapon_warrior_1' } });
|
user.stats.class = 'wizard';
|
||||||
user.ops.equip({ params: { key: 'weapon_warrior_1' } });
|
|
||||||
|
|
||||||
await buyGear(user, { params: { key: 'weapon_wizard_1' } });
|
await buyGear(user, { params: { key: 'weapon_wizard_0' } });
|
||||||
|
|
||||||
expect(user.items.gear.equipped).to.have.property('shield', 'shield_warrior_1');
|
expect(user.items.gear.equipped).to.have.property('shield', 'shield_warrior_1');
|
||||||
expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_warrior_1');
|
expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_warrior_1');
|
||||||
|
|
@ -283,5 +286,40 @@ describe('shared.ops.buyMarketGear', () => {
|
||||||
|
|
||||||
expect(user.items.gear.owned).to.have.property('shield_armoire_ramHornShield', true);
|
expect(user.items.gear.owned).to.have.property('shield_armoire_ramHornShield', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('buys current seasonal gear', async () => {
|
||||||
|
user.stats.gp = 200;
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-01-01'));
|
||||||
|
|
||||||
|
await buyGear(user, { params: { key: 'armor_special_winter2024Warrior' } });
|
||||||
|
|
||||||
|
expect(user.items.gear.owned).to.have.property('armor_special_winter2024Warrior', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors when buying past seasonal gear', async () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-01-01'));
|
||||||
|
user.stats.gp = 200;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await buyGear(user, { params: { key: 'armor_special_winter2023Warrior' } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('notAvailable'));
|
||||||
|
expect(user.items.gear.owned).to.not.have.property('armor_special_winter2023Warrior');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors when buying gear from wrong season', async () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-01-01'));
|
||||||
|
user.stats.gp = 200;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await buyGear(user, { params: { key: 'weapon_special_spring2024Warrior' } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('notAvailable'));
|
||||||
|
expect(user.items.gear.owned).to.not.have.property('weapon_special_spring2024Warrior');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -15,6 +15,7 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||||
describe('shared.ops.buyMysterySet', () => {
|
describe('shared.ops.buyMysterySet', () => {
|
||||||
let user;
|
let user;
|
||||||
const analytics = { track () {} };
|
const analytics = { track () {} };
|
||||||
|
let clock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
user = generateUser({
|
user = generateUser({
|
||||||
|
|
@ -31,6 +32,9 @@ describe('shared.ops.buyMysterySet', () => {
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
analytics.track.restore();
|
analytics.track.restore();
|
||||||
|
if (clock) {
|
||||||
|
clock.restore();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
context('Mystery Sets', () => {
|
context('Mystery Sets', () => {
|
||||||
|
|
@ -41,7 +45,7 @@ describe('shared.ops.buyMysterySet', () => {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
expect(err.message).to.eql(i18n.t('notEnoughHourglasses'));
|
expect(err.message).to.eql(i18n.t('notEnoughHourglasses'));
|
||||||
expect(user.items.gear.owned).to.have.property('weapon_warrior_0', true);
|
expect(user.items.gear.owned).to.not.have.property('armor_mystery_201501');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -72,6 +76,18 @@ describe('shared.ops.buyMysterySet', () => {
|
||||||
expect(err.message).to.equal(errorMessage('missingKeyParam'));
|
expect(err.message).to.equal(errorMessage('missingKeyParam'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns error if the set is not available', async () => {
|
||||||
|
user.purchased.plan.consecutive.trinkets = 1;
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-01-16'));
|
||||||
|
try {
|
||||||
|
await buyMysterySet(user, { params: { key: '201501' } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.eql(i18n.t('notAvailable'));
|
||||||
|
expect(user.items.gear.owned).to.not.have.property('armor_mystery_201501');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
context('successful purchases', () => {
|
context('successful purchases', () => {
|
||||||
|
|
@ -86,6 +102,16 @@ describe('shared.ops.buyMysterySet', () => {
|
||||||
expect(user.items.gear.owned).to.have.property('head_mystery_301404', true);
|
expect(user.items.gear.owned).to.have.property('head_mystery_301404', true);
|
||||||
expect(user.items.gear.owned).to.have.property('eyewear_mystery_301404', true);
|
expect(user.items.gear.owned).to.have.property('eyewear_mystery_301404', true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('buys mystery set if it is available', async () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-01-16'));
|
||||||
|
user.purchased.plan.consecutive.trinkets = 1;
|
||||||
|
await buyMysterySet(user, { params: { key: '201601' } }, analytics);
|
||||||
|
|
||||||
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||||
|
expect(user.items.gear.owned).to.have.property('shield_mystery_201601', true);
|
||||||
|
expect(user.items.gear.owned).to.have.property('head_mystery_201601', true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -10,6 +10,7 @@ import { BuyQuestWithGemOperation } from '../../../../website/common/script/ops/
|
||||||
|
|
||||||
describe('shared.ops.buyQuestGems', () => {
|
describe('shared.ops.buyQuestGems', () => {
|
||||||
let user;
|
let user;
|
||||||
|
let clock;
|
||||||
const goldPoints = 40;
|
const goldPoints = 40;
|
||||||
const analytics = { track () {} };
|
const analytics = { track () {} };
|
||||||
|
|
||||||
|
|
@ -26,11 +27,13 @@ describe('shared.ops.buyQuestGems', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sinon.stub(analytics, 'track');
|
sinon.stub(analytics, 'track');
|
||||||
sinon.spy(pinnedGearUtils, 'removeItemByPath');
|
sinon.spy(pinnedGearUtils, 'removeItemByPath');
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-01-16'));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
analytics.track.restore();
|
analytics.track.restore();
|
||||||
pinnedGearUtils.removeItemByPath.restore();
|
pinnedGearUtils.removeItemByPath.restore();
|
||||||
|
clock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
context('single purchase', () => {
|
context('single purchase', () => {
|
||||||
|
|
@ -44,7 +47,7 @@ describe('shared.ops.buyQuestGems', () => {
|
||||||
user.pinnedItems.push({ type: 'quests', key: 'gryphon' });
|
user.pinnedItems.push({ type: 'quests', key: 'gryphon' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('successfully purchases quest', async () => {
|
it('successfully purchases pet quest', async () => {
|
||||||
const key = 'gryphon';
|
const key = 'gryphon';
|
||||||
|
|
||||||
await buyQuest(user, { params: { key } });
|
await buyQuest(user, { params: { key } });
|
||||||
|
|
@ -52,6 +55,61 @@ describe('shared.ops.buyQuestGems', () => {
|
||||||
expect(user.items.quests[key]).to.equal(1);
|
expect(user.items.quests[key]).to.equal(1);
|
||||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('successfully purchases hatching potion quest', async () => {
|
||||||
|
const key = 'silver';
|
||||||
|
|
||||||
|
await buyQuest(user, { params: { key } });
|
||||||
|
|
||||||
|
expect(user.items.quests[key]).to.equal(1);
|
||||||
|
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('successfully purchases seasonal quest', async () => {
|
||||||
|
const key = 'evilsanta';
|
||||||
|
|
||||||
|
await buyQuest(user, { params: { key } });
|
||||||
|
|
||||||
|
expect(user.items.quests[key]).to.equal(1);
|
||||||
|
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors if the pet quest is not available', async () => {
|
||||||
|
const key = 'sabretooth';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await buyQuest(user, { params: { key } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('notAvailable'));
|
||||||
|
expect(user.items.quests[key]).to.eql(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors if the hatching potion quest is not available', async () => {
|
||||||
|
const key = 'ruby';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await buyQuest(user, { params: { key } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('notAvailable'));
|
||||||
|
expect(user.items.quests[key]).to.eql(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors if the seasonal quest is not available', async () => {
|
||||||
|
const key = 'egg';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await buyQuest(user, { params: { key } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('notAvailable'));
|
||||||
|
expect(user.items.quests[key]).to.eql(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('if a user\'s count of a quest scroll is negative, it will be reset to 0 before incrementing when they buy a new one.', async () => {
|
it('if a user\'s count of a quest scroll is negative, it will be reset to 0 before incrementing when they buy a new one.', async () => {
|
||||||
const key = 'dustbunnies';
|
const key = 'dustbunnies';
|
||||||
user.items.quests[key] = -1;
|
user.items.quests[key] = -1;
|
||||||
|
|
@ -61,6 +119,7 @@ describe('shared.ops.buyQuestGems', () => {
|
||||||
expect(user.items.quests[key]).to.equal(1);
|
expect(user.items.quests[key]).to.equal(1);
|
||||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('errors if the user has not completed prerequisite quests', async () => {
|
it('errors if the user has not completed prerequisite quests', async () => {
|
||||||
const key = 'atom3';
|
const key = 'atom3';
|
||||||
user.achievements.quests.atom1 = 1;
|
user.achievements.quests.atom1 = 1;
|
||||||
|
|
@ -73,6 +132,7 @@ describe('shared.ops.buyQuestGems', () => {
|
||||||
expect(user.items.quests[key]).to.eql(undefined);
|
expect(user.items.quests[key]).to.eql(undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('successfully purchases quest if user has completed all prerequisite quests', async () => {
|
it('successfully purchases quest if user has completed all prerequisite quests', async () => {
|
||||||
const key = 'atom3';
|
const key = 'atom3';
|
||||||
user.achievements.quests.atom1 = 1;
|
user.achievements.quests.atom1 = 1;
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
import { BuySpellOperation } from '../../../../website/common/script/ops/buy/buySpell';
|
|
||||||
import {
|
|
||||||
BadRequest,
|
|
||||||
NotFound,
|
|
||||||
NotAuthorized,
|
|
||||||
} from '../../../../website/common/script/libs/errors';
|
|
||||||
import i18n from '../../../../website/common/script/i18n';
|
|
||||||
import {
|
|
||||||
generateUser,
|
|
||||||
} from '../../../helpers/common.helper';
|
|
||||||
import content from '../../../../website/common/script/content/index';
|
|
||||||
import { errorMessage } from '../../../../website/common/script/libs/errorMessage';
|
|
||||||
|
|
||||||
describe('shared.ops.buySpecialSpell', () => {
|
|
||||||
let user;
|
|
||||||
const analytics = { track () {} };
|
|
||||||
|
|
||||||
async function buySpecialSpell (_user, _req, _analytics) {
|
|
||||||
const buyOp = new BuySpellOperation(_user, _req, _analytics);
|
|
||||||
|
|
||||||
return buyOp.purchase();
|
|
||||||
}
|
|
||||||
beforeEach(() => {
|
|
||||||
user = generateUser();
|
|
||||||
sinon.stub(analytics, 'track');
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
analytics.track.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws an error if params.key is missing', async () => {
|
|
||||||
try {
|
|
||||||
await buySpecialSpell(user);
|
|
||||||
} catch (err) {
|
|
||||||
expect(err).to.be.an.instanceof(BadRequest);
|
|
||||||
expect(err.message).to.equal(errorMessage('missingKeyParam'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws an error if the spell doesn\'t exists', async () => {
|
|
||||||
try {
|
|
||||||
await buySpecialSpell(user, {
|
|
||||||
params: {
|
|
||||||
key: 'notExisting',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
expect(err).to.be.an.instanceof(NotFound);
|
|
||||||
expect(err.message).to.equal(errorMessage('spellNotFound', { spellId: 'notExisting' }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws an error if the user doesn\'t have enough gold', async () => {
|
|
||||||
user.stats.gp = 1;
|
|
||||||
try {
|
|
||||||
await buySpecialSpell(user, {
|
|
||||||
params: {
|
|
||||||
key: 'thankyou',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
|
||||||
expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('buys an item', async () => {
|
|
||||||
user.stats.gp = 11;
|
|
||||||
const item = content.special.thankyou;
|
|
||||||
|
|
||||||
const [data, message] = await buySpecialSpell(user, {
|
|
||||||
params: {
|
|
||||||
key: 'thankyou',
|
|
||||||
},
|
|
||||||
}, analytics);
|
|
||||||
|
|
||||||
expect(user.stats.gp).to.equal(1);
|
|
||||||
expect(user.items.special.thankyou).to.equal(1);
|
|
||||||
expect(data).to.eql({
|
|
||||||
items: user.items,
|
|
||||||
stats: user.stats,
|
|
||||||
});
|
|
||||||
expect(message).to.equal(i18n.t('messageBought', {
|
|
||||||
itemText: item.text(),
|
|
||||||
}));
|
|
||||||
expect(analytics.track).to.be.calledOnce;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
172
test/common/ops/buy/buySpell.test.js
Normal file
172
test/common/ops/buy/buySpell.test.js
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
import { BuySpellOperation } from '../../../../website/common/script/ops/buy/buySpell';
|
||||||
|
import {
|
||||||
|
BadRequest,
|
||||||
|
NotFound,
|
||||||
|
NotAuthorized,
|
||||||
|
} from '../../../../website/common/script/libs/errors';
|
||||||
|
import i18n from '../../../../website/common/script/i18n';
|
||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
} from '../../../helpers/common.helper';
|
||||||
|
import content from '../../../../website/common/script/content/index';
|
||||||
|
import { errorMessage } from '../../../../website/common/script/libs/errorMessage';
|
||||||
|
|
||||||
|
describe('shared.ops.buySpecialSpell', () => {
|
||||||
|
let user;
|
||||||
|
let clock;
|
||||||
|
const analytics = { track () {} };
|
||||||
|
|
||||||
|
async function buySpecialSpell (_user, _req, _analytics) {
|
||||||
|
const buyOp = new BuySpellOperation(_user, _req, _analytics);
|
||||||
|
|
||||||
|
return buyOp.purchase();
|
||||||
|
}
|
||||||
|
beforeEach(() => {
|
||||||
|
user = generateUser();
|
||||||
|
sinon.stub(analytics, 'track');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
analytics.track.restore();
|
||||||
|
if (clock) {
|
||||||
|
clock.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if params.key is missing', async () => {
|
||||||
|
try {
|
||||||
|
await buySpecialSpell(user);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(errorMessage('missingKeyParam'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if the item doesn\'t exists', async () => {
|
||||||
|
try {
|
||||||
|
await buySpecialSpell(user, {
|
||||||
|
params: {
|
||||||
|
key: 'notExisting',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotFound);
|
||||||
|
expect(err.message).to.equal(errorMessage('spellNotFound', { spellId: 'notExisting' }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if the user doesn\'t have enough gold', async () => {
|
||||||
|
user.stats.gp = 1;
|
||||||
|
try {
|
||||||
|
await buySpecialSpell(user, {
|
||||||
|
params: {
|
||||||
|
key: 'thankyou',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buying cards', () => {
|
||||||
|
it('buys a card that is always available', async () => {
|
||||||
|
user.stats.gp = 11;
|
||||||
|
const item = content.special.thankyou;
|
||||||
|
|
||||||
|
const [data, message] = await buySpecialSpell(user, {
|
||||||
|
params: {
|
||||||
|
key: 'thankyou',
|
||||||
|
},
|
||||||
|
}, analytics);
|
||||||
|
|
||||||
|
expect(user.stats.gp).to.equal(1);
|
||||||
|
expect(user.items.special.thankyou).to.equal(1);
|
||||||
|
expect(data).to.eql({
|
||||||
|
items: user.items,
|
||||||
|
stats: user.stats,
|
||||||
|
});
|
||||||
|
expect(message).to.equal(i18n.t('messageBought', {
|
||||||
|
itemText: item.text(),
|
||||||
|
}));
|
||||||
|
expect(analytics.track).to.be.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('buys a limited card when it is available', async () => {
|
||||||
|
user.stats.gp = 11;
|
||||||
|
const item = content.special.nye;
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-01-01'));
|
||||||
|
|
||||||
|
const [data, message] = await buySpecialSpell(user, {
|
||||||
|
params: {
|
||||||
|
key: 'nye',
|
||||||
|
},
|
||||||
|
}, analytics);
|
||||||
|
|
||||||
|
expect(user.stats.gp).to.equal(1);
|
||||||
|
expect(user.items.special.nye).to.equal(1);
|
||||||
|
expect(data).to.eql({
|
||||||
|
items: user.items,
|
||||||
|
stats: user.stats,
|
||||||
|
});
|
||||||
|
expect(message).to.equal(i18n.t('messageBought', {
|
||||||
|
itemText: item.text(),
|
||||||
|
}));
|
||||||
|
expect(analytics.track).to.be.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if the card is not currently available', async () => {
|
||||||
|
user.stats.gp = 11;
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-06-01'));
|
||||||
|
try {
|
||||||
|
await buySpecialSpell(user, {
|
||||||
|
params: {
|
||||||
|
key: 'nye',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('cannotBuyItem'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buying spells', () => {
|
||||||
|
it('buys a spell if it is currently available', async () => {
|
||||||
|
user.stats.gp = 16;
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-06-22'));
|
||||||
|
const item = content.special.seafoam;
|
||||||
|
const [data, message] = await buySpecialSpell(user, {
|
||||||
|
params: {
|
||||||
|
key: 'seafoam',
|
||||||
|
},
|
||||||
|
}, analytics);
|
||||||
|
|
||||||
|
expect(user.stats.gp).to.equal(1);
|
||||||
|
expect(user.items.special.seafoam).to.equal(1);
|
||||||
|
expect(data).to.eql({
|
||||||
|
items: user.items,
|
||||||
|
stats: user.stats,
|
||||||
|
});
|
||||||
|
expect(message).to.equal(i18n.t('messageBought', {
|
||||||
|
itemText: item.text(),
|
||||||
|
}));
|
||||||
|
expect(analytics.track).to.be.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if the spell is not currently available', async () => {
|
||||||
|
user.stats.gp = 50;
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-01-22'));
|
||||||
|
try {
|
||||||
|
await buySpecialSpell(user, {
|
||||||
|
params: {
|
||||||
|
key: 'seafoam',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('cannotBuyItem'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
describe('shared.ops.purchase', () => {
|
describe('shared.ops.purchase', () => {
|
||||||
const SEASONAL_FOOD = moment().isBefore('2021-11-02T20:00-04:00') ? 'Candy_Base' : 'Meat';
|
const SEASONAL_FOOD = moment().isBefore('2021-11-02T20:00-04:00') ? 'Candy_Base' : 'Meat';
|
||||||
let user;
|
let user;
|
||||||
|
let clock;
|
||||||
const goldPoints = 40;
|
const goldPoints = 40;
|
||||||
const analytics = { track () {} };
|
const analytics = { track () {} };
|
||||||
|
|
||||||
|
|
@ -25,11 +26,13 @@ describe('shared.ops.purchase', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sinon.stub(analytics, 'track');
|
sinon.stub(analytics, 'track');
|
||||||
sinon.spy(pinnedGearUtils, 'removeItemByPath');
|
sinon.spy(pinnedGearUtils, 'removeItemByPath');
|
||||||
|
clock = sandbox.useFakeTimers(new Date('2024-01-10'));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
analytics.track.restore();
|
analytics.track.restore();
|
||||||
pinnedGearUtils.removeItemByPath.restore();
|
pinnedGearUtils.removeItemByPath.restore();
|
||||||
|
clock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
context('failure conditions', () => {
|
context('failure conditions', () => {
|
||||||
|
|
@ -82,13 +85,77 @@ describe('shared.ops.purchase', () => {
|
||||||
|
|
||||||
it('returns error when user does not have enough gems to buy an item', async () => {
|
it('returns error when user does not have enough gems to buy an item', async () => {
|
||||||
try {
|
try {
|
||||||
await purchase(user, { params: { type: 'gear', key: 'headAccessory_special_wolfEars' } });
|
await purchase(user, { params: { type: 'gear', key: 'shield_special_winter2019Healer' } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
expect(err.message).to.equal(i18n.t('notEnoughGems'));
|
expect(err.message).to.equal(i18n.t('notEnoughGems'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns error when gear is not available', async () => {
|
||||||
|
try {
|
||||||
|
await purchase(user, { params: { type: 'gear', key: 'shield_special_spring2019Healer' } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when purchasing current seasonal gear', async () => {
|
||||||
|
user.balance = 2;
|
||||||
|
try {
|
||||||
|
await purchase(user, { params: { type: 'gear', key: 'shield_special_winter2024Healer' } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when hatching potion is not available', async () => {
|
||||||
|
try {
|
||||||
|
await purchase(user, { params: { type: 'hatchingPotions', key: 'PolkaDot' } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when quest for hatching potion was not yet completed', async () => {
|
||||||
|
try {
|
||||||
|
await purchase(user, { params: { type: 'hatchingPotions', key: 'BlackPearl' } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when quest for egg was not yet completed', async () => {
|
||||||
|
try {
|
||||||
|
await purchase(user, { params: { type: 'eggs', key: 'Octopus' } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when bundle is not available', async () => {
|
||||||
|
try {
|
||||||
|
await purchase(user, { params: { type: 'bundles', key: 'forestFriends' } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('notAvailable'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when gear is not gem purchasable', async () => {
|
||||||
|
try {
|
||||||
|
await purchase(user, { params: { type: 'gear', key: 'shield_healer_3' } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('messageNotAvailable'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('returns error when item is not found', async () => {
|
it('returns error when item is not found', async () => {
|
||||||
const params = { key: 'notExisting', type: 'food' };
|
const params = { key: 'notExisting', type: 'food' };
|
||||||
|
|
||||||
|
|
@ -99,44 +166,6 @@ describe('shared.ops.purchase', () => {
|
||||||
expect(err.message).to.equal(i18n.t('contentKeyNotFound', params));
|
expect(err.message).to.equal(i18n.t('contentKeyNotFound', params));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns error when user supplies a non-numeric quantity', async () => {
|
|
||||||
const type = 'eggs';
|
|
||||||
const key = 'Wolf';
|
|
||||||
|
|
||||||
try {
|
|
||||||
await purchase(user, { params: { type, key }, quantity: 'jamboree' }, analytics);
|
|
||||||
} catch (err) {
|
|
||||||
expect(err).to.be.an.instanceof(BadRequest);
|
|
||||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns error when user supplies a negative quantity', async () => {
|
|
||||||
const type = 'eggs';
|
|
||||||
const key = 'Wolf';
|
|
||||||
user.balance = 10;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await purchase(user, { params: { type, key }, quantity: -2 }, analytics);
|
|
||||||
} catch (err) {
|
|
||||||
expect(err).to.be.an.instanceof(BadRequest);
|
|
||||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns error when user supplies a decimal quantity', async () => {
|
|
||||||
const type = 'eggs';
|
|
||||||
const key = 'Wolf';
|
|
||||||
user.balance = 10;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await purchase(user, { params: { type, key }, quantity: 2.9 }, analytics);
|
|
||||||
} catch (err) {
|
|
||||||
expect(err).to.be.an.instanceof(BadRequest);
|
|
||||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
context('successful purchase', () => {
|
context('successful purchase', () => {
|
||||||
|
|
@ -150,7 +179,7 @@ describe('shared.ops.purchase', () => {
|
||||||
user.pinnedItems.push({ type: 'eggs', key: 'Wolf' });
|
user.pinnedItems.push({ type: 'eggs', key: 'Wolf' });
|
||||||
user.pinnedItems.push({ type: 'hatchingPotions', key: 'Base' });
|
user.pinnedItems.push({ type: 'hatchingPotions', key: 'Base' });
|
||||||
user.pinnedItems.push({ type: 'food', key: SEASONAL_FOOD });
|
user.pinnedItems.push({ type: 'food', key: SEASONAL_FOOD });
|
||||||
user.pinnedItems.push({ type: 'gear', key: 'headAccessory_special_tigerEars' });
|
user.pinnedItems.push({ type: 'gear', key: 'shield_special_winter2019Healer' });
|
||||||
user.pinnedItems.push({ type: 'bundles', key: 'featheredFriends' });
|
user.pinnedItems.push({ type: 'bundles', key: 'featheredFriends' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -185,9 +214,9 @@ describe('shared.ops.purchase', () => {
|
||||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('purchases gear', async () => {
|
it('purchases past seasonal gear', async () => {
|
||||||
const type = 'gear';
|
const type = 'gear';
|
||||||
const key = 'headAccessory_special_tigerEars';
|
const key = 'shield_special_winter2019Healer';
|
||||||
|
|
||||||
await purchase(user, { params: { type, key } });
|
await purchase(user, { params: { type, key } });
|
||||||
|
|
||||||
|
|
@ -195,9 +224,39 @@ describe('shared.ops.purchase', () => {
|
||||||
expect(pinnedGearUtils.removeItemByPath.calledOnce).to.equal(true);
|
expect(pinnedGearUtils.removeItemByPath.calledOnce).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('purchases hatching potion', async () => {
|
||||||
|
const type = 'hatchingPotions';
|
||||||
|
const key = 'Peppermint';
|
||||||
|
|
||||||
|
await purchase(user, { params: { type, key } });
|
||||||
|
|
||||||
|
expect(user.items.hatchingPotions[key]).to.eql(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('purchases hatching potion if user completed quest', async () => {
|
||||||
|
const type = 'hatchingPotions';
|
||||||
|
const key = 'Bronze';
|
||||||
|
user.achievements.quests.bronze = 1;
|
||||||
|
|
||||||
|
await purchase(user, { params: { type, key } });
|
||||||
|
|
||||||
|
expect(user.items.hatchingPotions[key]).to.eql(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('purchases egg if user completed quest', async () => {
|
||||||
|
const type = 'eggs';
|
||||||
|
const key = 'Deer';
|
||||||
|
user.achievements.quests.ghost_stag = 1;
|
||||||
|
|
||||||
|
await purchase(user, { params: { type, key } });
|
||||||
|
|
||||||
|
expect(user.items.eggs[key]).to.eql(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('purchases quest bundles', async () => {
|
it('purchases quest bundles', async () => {
|
||||||
const startingBalance = user.balance;
|
const startingBalance = user.balance;
|
||||||
const clock = sandbox.useFakeTimers(moment('2024-03-20').valueOf());
|
clock.restore();
|
||||||
|
clock = sandbox.useFakeTimers(moment('2022-03-10').valueOf());
|
||||||
const type = 'bundles';
|
const type = 'bundles';
|
||||||
const key = 'cuddleBuddies';
|
const key = 'cuddleBuddies';
|
||||||
const price = 1.75;
|
const price = 1.75;
|
||||||
|
|
@ -216,7 +275,6 @@ describe('shared.ops.purchase', () => {
|
||||||
expect(user.balance).to.equal(startingBalance - price);
|
expect(user.balance).to.equal(startingBalance - price);
|
||||||
|
|
||||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||||
clock.restore();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -257,5 +315,43 @@ describe('shared.ops.purchase', () => {
|
||||||
|
|
||||||
expect(user.items[type][key]).to.equal(2);
|
expect(user.items[type][key]).to.equal(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns error when user supplies a non-numeric quantity', async () => {
|
||||||
|
const type = 'eggs';
|
||||||
|
const key = 'Wolf';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await purchase(user, { params: { type, key }, quantity: 'jamboree' }, analytics);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when user supplies a negative quantity', async () => {
|
||||||
|
const type = 'eggs';
|
||||||
|
const key = 'Wolf';
|
||||||
|
user.balance = 10;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await purchase(user, { params: { type, key }, quantity: -2 }, analytics);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when user supplies a decimal quantity', async () => {
|
||||||
|
const type = 'eggs';
|
||||||
|
const key = 'Wolf';
|
||||||
|
user.balance = 10;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await purchase(user, { params: { type, key }, quantity: 2.9 }, analytics);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -2,14 +2,17 @@ import get from 'lodash/get';
|
||||||
import unlock from '../../../website/common/script/ops/unlock';
|
import unlock from '../../../website/common/script/ops/unlock';
|
||||||
import i18n from '../../../website/common/script/i18n';
|
import i18n from '../../../website/common/script/i18n';
|
||||||
import { generateUser } from '../../helpers/common.helper';
|
import { generateUser } from '../../helpers/common.helper';
|
||||||
import { NotAuthorized, BadRequest } from '../../../website/common/script/libs/errors';
|
import {
|
||||||
|
NotAuthorized,
|
||||||
|
BadRequest,
|
||||||
|
} from '../../../website/common/script/libs/errors';
|
||||||
|
|
||||||
describe('shared.ops.unlock', () => {
|
describe('shared.ops.unlock', () => {
|
||||||
let user;
|
let user;
|
||||||
const unlockPath = 'shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
|
let clock;
|
||||||
|
const unlockPath = 'shirt.convict,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
|
||||||
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
|
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
|
||||||
const backgroundUnlockPath = 'background.giant_florals';
|
const backgroundUnlockPath = 'background.giant_florals';
|
||||||
const backgroundSetUnlockPath = 'background.archery_range,background.giant_florals,background.rainbows_end';
|
|
||||||
const hairUnlockPath = 'hair.color.rainbow,hair.color.yellow,hair.color.green,hair.color.purple,hair.color.blue,hair.color.TRUred';
|
const hairUnlockPath = 'hair.color.rainbow,hair.color.yellow,hair.color.green,hair.color.purple,hair.color.blue,hair.color.TRUred';
|
||||||
const facialHairUnlockPath = 'hair.mustache.1,hair.mustache.2,hair.beard.1,hair.beard.2,hair.beard.3';
|
const facialHairUnlockPath = 'hair.mustache.1,hair.mustache.2,hair.beard.1,hair.beard.2,hair.beard.3';
|
||||||
const usersStartingGems = 50 / 4;
|
const usersStartingGems = 50 / 4;
|
||||||
|
|
@ -17,6 +20,11 @@ describe('shared.ops.unlock', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
user = generateUser();
|
user = generateUser();
|
||||||
user.balance = usersStartingGems;
|
user.balance = usersStartingGems;
|
||||||
|
clock = sandbox.useFakeTimers(new Date('2024-04-10'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns an error when path is not provided', async () => {
|
it('returns an error when path is not provided', async () => {
|
||||||
|
|
@ -31,7 +39,9 @@ describe('shared.ops.unlock', () => {
|
||||||
it('does not unlock lost gear', async () => {
|
it('does not unlock lost gear', async () => {
|
||||||
user.items.gear.owned.headAccessory_special_bearEars = false;
|
user.items.gear.owned.headAccessory_special_bearEars = false;
|
||||||
|
|
||||||
await unlock(user, { query: { path: 'items.gear.owned.headAccessory_special_bearEars' } });
|
await unlock(user, {
|
||||||
|
query: { path: 'items.gear.owned.headAccessory_special_bearEars' },
|
||||||
|
});
|
||||||
|
|
||||||
expect(user.balance).to.equal(usersStartingGems);
|
expect(user.balance).to.equal(usersStartingGems);
|
||||||
});
|
});
|
||||||
|
|
@ -95,7 +105,9 @@ describe('shared.ops.unlock', () => {
|
||||||
|
|
||||||
it('returns an error if gear is not from the animal set', async () => {
|
it('returns an error if gear is not from the animal set', async () => {
|
||||||
try {
|
try {
|
||||||
await unlock(user, { query: { path: 'items.gear.owned.back_mystery_202004' } });
|
await unlock(user, {
|
||||||
|
query: { path: 'items.gear.owned.back_mystery_202004' },
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err).to.be.an.instanceof(BadRequest);
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
|
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
|
||||||
|
|
@ -153,7 +165,6 @@ describe('shared.ops.unlock', () => {
|
||||||
await unlock(user, { query: { path: partialUnlockPaths[4] } });
|
await unlock(user, { query: { path: partialUnlockPaths[4] } });
|
||||||
await unlock(user, { query: { path: partialUnlockPaths[5] } });
|
await unlock(user, { query: { path: partialUnlockPaths[5] } });
|
||||||
await unlock(user, { query: { path: partialUnlockPaths[6] } });
|
await unlock(user, { query: { path: partialUnlockPaths[6] } });
|
||||||
await unlock(user, { query: { path: partialUnlockPaths[7] } });
|
|
||||||
|
|
||||||
await unlock(user, { query: { path: unlockPath } });
|
await unlock(user, { query: { path: unlockPath } });
|
||||||
});
|
});
|
||||||
|
|
@ -163,7 +174,9 @@ describe('shared.ops.unlock', () => {
|
||||||
|
|
||||||
await unlock(user, { query: { path: backgroundUnlockPath } });
|
await unlock(user, { query: { path: backgroundUnlockPath } });
|
||||||
const afterBalance = user.balance;
|
const afterBalance = user.balance;
|
||||||
const response = await unlock(user, { query: { path: backgroundUnlockPath } });
|
const response = await unlock(user, {
|
||||||
|
query: { path: backgroundUnlockPath },
|
||||||
|
});
|
||||||
expect(user.balance).to.equal(afterBalance); // do not bill twice
|
expect(user.balance).to.equal(afterBalance); // do not bill twice
|
||||||
|
|
||||||
expect(response.message).to.not.exist;
|
expect(response.message).to.not.exist;
|
||||||
|
|
@ -176,7 +189,9 @@ describe('shared.ops.unlock', () => {
|
||||||
await unlock(user, { query: { path: backgroundUnlockPath } }); // unlock
|
await unlock(user, { query: { path: backgroundUnlockPath } }); // unlock
|
||||||
const afterBalance = user.balance;
|
const afterBalance = user.balance;
|
||||||
await unlock(user, { query: { path: backgroundUnlockPath } }); // equip
|
await unlock(user, { query: { path: backgroundUnlockPath } }); // equip
|
||||||
const response = await unlock(user, { query: { path: backgroundUnlockPath } });
|
const response = await unlock(user, {
|
||||||
|
query: { path: backgroundUnlockPath },
|
||||||
|
});
|
||||||
expect(user.balance).to.equal(afterBalance); // do not bill twice
|
expect(user.balance).to.equal(afterBalance); // do not bill twice
|
||||||
|
|
||||||
expect(response.message).to.not.exist;
|
expect(response.message).to.not.exist;
|
||||||
|
|
@ -192,8 +207,9 @@ describe('shared.ops.unlock', () => {
|
||||||
individualPaths.forEach(path => {
|
individualPaths.forEach(path => {
|
||||||
expect(get(user.purchased, path)).to.be.true;
|
expect(get(user.purchased, path)).to.be.true;
|
||||||
});
|
});
|
||||||
expect(Object.keys(user.purchased.shirt).length)
|
expect(Object.keys(user.purchased.shirt).length).to.equal(
|
||||||
.to.equal(initialShirts + individualPaths.length);
|
initialShirts + individualPaths.length,
|
||||||
|
);
|
||||||
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -208,8 +224,9 @@ describe('shared.ops.unlock', () => {
|
||||||
individualPaths.forEach(path => {
|
individualPaths.forEach(path => {
|
||||||
expect(get(user.purchased, path)).to.be.true;
|
expect(get(user.purchased, path)).to.be.true;
|
||||||
});
|
});
|
||||||
expect(Object.keys(user.purchased.hair.color).length)
|
expect(Object.keys(user.purchased.hair.color).length).to.equal(
|
||||||
.to.equal(initialHairColors + individualPaths.length);
|
initialHairColors + individualPaths.length,
|
||||||
|
);
|
||||||
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -219,21 +236,28 @@ describe('shared.ops.unlock', () => {
|
||||||
|
|
||||||
const initialMustache = Object.keys(user.purchased.hair.mustache).length;
|
const initialMustache = Object.keys(user.purchased.hair.mustache).length;
|
||||||
const initialBeard = Object.keys(user.purchased.hair.mustache).length;
|
const initialBeard = Object.keys(user.purchased.hair.mustache).length;
|
||||||
const [, message] = await unlock(user, { query: { path: facialHairUnlockPath } });
|
const [, message] = await unlock(user, {
|
||||||
|
query: { path: facialHairUnlockPath },
|
||||||
|
});
|
||||||
|
|
||||||
expect(message).to.equal(i18n.t('unlocked'));
|
expect(message).to.equal(i18n.t('unlocked'));
|
||||||
const individualPaths = facialHairUnlockPath.split(',');
|
const individualPaths = facialHairUnlockPath.split(',');
|
||||||
individualPaths.forEach(path => {
|
individualPaths.forEach(path => {
|
||||||
expect(get(user.purchased, path)).to.be.true;
|
expect(get(user.purchased, path)).to.be.true;
|
||||||
});
|
});
|
||||||
expect(Object.keys(user.purchased.hair.mustache).length + Object.keys(user.purchased.hair.beard).length) // eslint-disable-line max-len
|
expect(
|
||||||
|
Object.keys(user.purchased.hair.mustache).length
|
||||||
|
+ Object.keys(user.purchased.hair.beard).length,
|
||||||
|
) // eslint-disable-line max-len
|
||||||
.to.equal(initialMustache + initialBeard + individualPaths.length);
|
.to.equal(initialMustache + initialBeard + individualPaths.length);
|
||||||
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unlocks a full set of gear', async () => {
|
it('unlocks a full set of gear', async () => {
|
||||||
const initialGear = Object.keys(user.items.gear.owned).length;
|
const initialGear = Object.keys(user.items.gear.owned).length;
|
||||||
const [, message] = await unlock(user, { query: { path: unlockGearSetPath } });
|
const [, message] = await unlock(user, {
|
||||||
|
query: { path: unlockGearSetPath },
|
||||||
|
});
|
||||||
|
|
||||||
expect(message).to.equal(i18n.t('unlocked'));
|
expect(message).to.equal(i18n.t('unlocked'));
|
||||||
|
|
||||||
|
|
@ -241,32 +265,21 @@ describe('shared.ops.unlock', () => {
|
||||||
individualPaths.forEach(path => {
|
individualPaths.forEach(path => {
|
||||||
expect(get(user, path)).to.be.true;
|
expect(get(user, path)).to.be.true;
|
||||||
});
|
});
|
||||||
expect(Object.keys(user.items.gear.owned).length)
|
expect(Object.keys(user.items.gear.owned).length).to.equal(
|
||||||
.to.equal(initialGear + individualPaths.length);
|
initialGear + individualPaths.length,
|
||||||
|
);
|
||||||
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unlocks a full set of backgrounds', async () => {
|
|
||||||
const initialBackgrounds = Object.keys(user.purchased.background).length;
|
|
||||||
const [, message] = await unlock(user, { query: { path: backgroundSetUnlockPath } });
|
|
||||||
|
|
||||||
expect(message).to.equal(i18n.t('unlocked'));
|
|
||||||
const individualPaths = backgroundSetUnlockPath.split(',');
|
|
||||||
individualPaths.forEach(path => {
|
|
||||||
expect(get(user.purchased, path)).to.be.true;
|
|
||||||
});
|
|
||||||
expect(Object.keys(user.purchased.background).length)
|
|
||||||
.to.equal(initialBackgrounds + individualPaths.length);
|
|
||||||
expect(user.balance).to.equal(usersStartingGems - 3.75);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('unlocks an item (appearance)', async () => {
|
it('unlocks an item (appearance)', async () => {
|
||||||
const path = unlockPath.split(',')[0];
|
const path = unlockPath.split(',')[0];
|
||||||
const initialShirts = Object.keys(user.purchased.shirt).length;
|
const initialShirts = Object.keys(user.purchased.shirt).length;
|
||||||
const [, message] = await unlock(user, { query: { path } });
|
const [, message] = await unlock(user, { query: { path } });
|
||||||
|
|
||||||
expect(message).to.equal(i18n.t('unlocked'));
|
expect(message).to.equal(i18n.t('unlocked'));
|
||||||
expect(Object.keys(user.purchased.shirt).length).to.equal(initialShirts + 1);
|
expect(Object.keys(user.purchased.shirt).length).to.equal(
|
||||||
|
initialShirts + 1,
|
||||||
|
);
|
||||||
expect(get(user.purchased, path)).to.be.true;
|
expect(get(user.purchased, path)).to.be.true;
|
||||||
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
||||||
});
|
});
|
||||||
|
|
@ -279,7 +292,9 @@ describe('shared.ops.unlock', () => {
|
||||||
const [, message] = await unlock(user, { query: { path } });
|
const [, message] = await unlock(user, { query: { path } });
|
||||||
|
|
||||||
expect(message).to.equal(i18n.t('unlocked'));
|
expect(message).to.equal(i18n.t('unlocked'));
|
||||||
expect(Object.keys(user.purchased.hair.color).length).to.equal(initialColorHair + 1);
|
expect(Object.keys(user.purchased.hair.color).length).to.equal(
|
||||||
|
initialColorHair + 1,
|
||||||
|
);
|
||||||
expect(get(user.purchased, path)).to.be.true;
|
expect(get(user.purchased, path)).to.be.true;
|
||||||
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
||||||
});
|
});
|
||||||
|
|
@ -295,8 +310,12 @@ describe('shared.ops.unlock', () => {
|
||||||
|
|
||||||
expect(message).to.equal(i18n.t('unlocked'));
|
expect(message).to.equal(i18n.t('unlocked'));
|
||||||
|
|
||||||
expect(Object.keys(user.purchased.hair.mustache).length).to.equal(initialMustache + 1);
|
expect(Object.keys(user.purchased.hair.mustache).length).to.equal(
|
||||||
expect(Object.keys(user.purchased.hair.beard).length).to.equal(initialBeard);
|
initialMustache + 1,
|
||||||
|
);
|
||||||
|
expect(Object.keys(user.purchased.hair.beard).length).to.equal(
|
||||||
|
initialBeard,
|
||||||
|
);
|
||||||
|
|
||||||
expect(get(user.purchased, path)).to.be.true;
|
expect(get(user.purchased, path)).to.be.true;
|
||||||
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
||||||
|
|
@ -315,11 +334,24 @@ describe('shared.ops.unlock', () => {
|
||||||
|
|
||||||
it('unlocks an item (background)', async () => {
|
it('unlocks an item (background)', async () => {
|
||||||
const initialBackgrounds = Object.keys(user.purchased.background).length;
|
const initialBackgrounds = Object.keys(user.purchased.background).length;
|
||||||
const [, message] = await unlock(user, { query: { path: backgroundUnlockPath } });
|
const [, message] = await unlock(user, {
|
||||||
|
query: { path: backgroundUnlockPath },
|
||||||
|
});
|
||||||
|
|
||||||
expect(message).to.equal(i18n.t('unlocked'));
|
expect(message).to.equal(i18n.t('unlocked'));
|
||||||
expect(Object.keys(user.purchased.background).length).to.equal(initialBackgrounds + 1);
|
expect(Object.keys(user.purchased.background).length).to.equal(
|
||||||
|
initialBackgrounds + 1,
|
||||||
|
);
|
||||||
expect(get(user.purchased, backgroundUnlockPath)).to.be.true;
|
expect(get(user.purchased, backgroundUnlockPath)).to.be.true;
|
||||||
expect(user.balance).to.equal(usersStartingGems - 1.75);
|
expect(user.balance).to.equal(usersStartingGems - 1.75);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles an invalid hair path gracefully', async () => {
|
||||||
|
try {
|
||||||
|
await unlock(user, { query: { path: 'hair.invalid' } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
/* eslint-disable prefer-template, no-shadow, func-names, import/no-commonjs */
|
|
||||||
|
|
||||||
const expect = require('expect.js');
|
|
||||||
|
|
||||||
module.exports.addCustomMatchers = function () {
|
|
||||||
const { Assertion } = expect;
|
|
||||||
Assertion.prototype.toHaveGP = function (gp) {
|
|
||||||
const actual = this.obj.stats.gp;
|
|
||||||
return this.assert(actual === gp, () => 'expected user to have ' + gp + ' gp, but got ' + actual, () => 'expected user to not have ' + gp + ' gp');
|
|
||||||
};
|
|
||||||
Assertion.prototype.toHaveHP = function (hp) {
|
|
||||||
const actual = this.obj.stats.hp;
|
|
||||||
return this.assert(actual === hp, () => 'expected user to have ' + hp + ' hp, but got ' + actual, () => 'expected user to not have ' + hp + ' hp');
|
|
||||||
};
|
|
||||||
Assertion.prototype.toHaveExp = function (exp) {
|
|
||||||
const actual = this.obj.stats.exp;
|
|
||||||
return this.assert(actual === exp, () => 'expected user to have ' + exp + ' experience points, but got ' + actual, () => 'expected user to not have ' + exp + ' experience points');
|
|
||||||
};
|
|
||||||
Assertion.prototype.toHaveLevel = function (lvl) {
|
|
||||||
const actual = this.obj.stats.lvl;
|
|
||||||
return this.assert(actual === lvl, () => 'expected user to be level ' + lvl + ', but got ' + actual, () => 'expected user to not be level ' + lvl);
|
|
||||||
};
|
|
||||||
Assertion.prototype.toHaveMaxMP = function (mp) {
|
|
||||||
const actual = this.obj._statsComputed.maxMP;
|
|
||||||
return this.assert(actual === mp, () => 'expected user to have ' + mp + ' max mp, but got ' + actual, () => 'expected user to not have ' + mp + ' max mp');
|
|
||||||
};
|
|
||||||
};
|
|
||||||
83
test/content/armoire.test.js
Normal file
83
test/content/armoire.test.js
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
/* eslint-disable global-require */
|
||||||
|
import forEach from 'lodash/forEach';
|
||||||
|
import {
|
||||||
|
expectValidTranslationString,
|
||||||
|
} from '../helpers/content.helper';
|
||||||
|
|
||||||
|
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', () => {
|
||||||
|
let clock;
|
||||||
|
beforeEach(() => {
|
||||||
|
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
clock.restore();
|
||||||
|
});
|
||||||
|
it('does not return unreleased gear', async () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-01-02'));
|
||||||
|
const items = makeArmoireIitemList();
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('released gear has all required properties', async () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-05-08'));
|
||||||
|
const items = makeArmoireIitemList();
|
||||||
|
expect(items.length).to.equal(396);
|
||||||
|
forEach(items, item => {
|
||||||
|
if (item.set !== undefined) {
|
||||||
|
expect(item.set, item.key).to.be.a('string');
|
||||||
|
expect(item.set, item.key).to.not.be.empty;
|
||||||
|
}
|
||||||
|
expectValidTranslationString(item.text);
|
||||||
|
expect(item.value, item.key).to.be.a('number');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('releases gear when appropriate', async () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-01-01T00:00:00.000Z'));
|
||||||
|
const items = makeArmoireIitemList();
|
||||||
|
expect(items.length).to.equal(377);
|
||||||
|
clock.restore();
|
||||||
|
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-01-08'));
|
||||||
|
const januaryItems = makeArmoireIitemList();
|
||||||
|
expect(januaryItems.length).to.equal(381);
|
||||||
|
clock.restore();
|
||||||
|
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-02-07'));
|
||||||
|
const januaryItems2 = makeArmoireIitemList();
|
||||||
|
expect(januaryItems2.length).to.equal(381);
|
||||||
|
clock.restore();
|
||||||
|
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-02-07T16:00:00.000Z'));
|
||||||
|
const febuaryItems = makeArmoireIitemList();
|
||||||
|
expect(febuaryItems.length).to.equal(384);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets have at least 2 items', () => {
|
||||||
|
const armoire = makeArmoireIitemList();
|
||||||
|
const setMap = {};
|
||||||
|
forEach(armoire, item => {
|
||||||
|
if (setMap[item.set] === undefined) {
|
||||||
|
setMap[item.set] = 0;
|
||||||
|
}
|
||||||
|
setMap[item.set] += 1;
|
||||||
|
});
|
||||||
|
Object.keys(setMap).forEach(set => {
|
||||||
|
expect(setMap[set], set).to.be.at.least(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
40
test/content/events.test.js
Normal file
40
test/content/events.test.js
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { getRepeatingEvents } from '../../website/common/script/content/constants/events';
|
||||||
|
|
||||||
|
describe('events', () => {
|
||||||
|
let clock;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (clock) {
|
||||||
|
clock.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty array when no events are active', () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-01-06'));
|
||||||
|
const events = getRepeatingEvents();
|
||||||
|
expect(events).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns events when active', () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-01-31'));
|
||||||
|
const events = getRepeatingEvents();
|
||||||
|
expect(events).to.have.length(1);
|
||||||
|
expect(events[0].key).to.equal('birthday');
|
||||||
|
expect(events[0].end).to.be.greaterThan(new Date());
|
||||||
|
expect(events[0].start).to.be.lessThan(new Date());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns nye event at beginning of the year', () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date('2025-01-01'));
|
||||||
|
const events = getRepeatingEvents();
|
||||||
|
expect(events).to.have.length(1);
|
||||||
|
expect(events[0].key).to.equal('nye');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns nye event at end of the year', () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date('2024-12-30'));
|
||||||
|
const events = getRepeatingEvents();
|
||||||
|
expect(events).to.have.length(1);
|
||||||
|
expect(events[0].key).to.equal('nye');
|
||||||
|
});
|
||||||
|
});
|
||||||
94
test/content/food.test.js
Normal file
94
test/content/food.test.js
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
/* eslint-disable global-require */
|
||||||
|
import {
|
||||||
|
each,
|
||||||
|
} from 'lodash';
|
||||||
|
import {
|
||||||
|
expectValidTranslationString,
|
||||||
|
} from '../helpers/content.helper';
|
||||||
|
import content from '../../website/common/script/content';
|
||||||
|
|
||||||
|
describe('food', () => {
|
||||||
|
let clock;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (clock) {
|
||||||
|
clock.restore();
|
||||||
|
}
|
||||||
|
delete require.cache[require.resolve('../../website/common/script/content')];
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('all', () => {
|
||||||
|
it('contains basic information about each food item', () => {
|
||||||
|
each(content.food, (foodItem, key) => {
|
||||||
|
if (foodItem.key === 'Saddle') {
|
||||||
|
expectValidTranslationString(foodItem.sellWarningNote);
|
||||||
|
} else {
|
||||||
|
expectValidTranslationString(foodItem.textA);
|
||||||
|
expectValidTranslationString(foodItem.textThe);
|
||||||
|
expect(foodItem.target).to.be.a('string');
|
||||||
|
}
|
||||||
|
expectValidTranslationString(foodItem.text);
|
||||||
|
expectValidTranslationString(foodItem.notes);
|
||||||
|
expect(foodItem.canBuy).to.be.a('function');
|
||||||
|
expect(foodItem.value).to.be.a('number');
|
||||||
|
expect(foodItem.key).to.equal(key);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets canDrop for normal food if there is no food season', () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date(2024, 5, 8));
|
||||||
|
const datedContent = require('../../website/common/script/content').default;
|
||||||
|
each(datedContent.food, foodItem => {
|
||||||
|
if (foodItem.key.indexOf('Cake') === -1 && foodItem.key.indexOf('Candy_') === -1 && foodItem.key.indexOf('Pie_') === -1 && foodItem.key !== 'Saddle') {
|
||||||
|
expect(foodItem.canDrop).to.equal(true);
|
||||||
|
} else {
|
||||||
|
expect(foodItem.canDrop).to.equal(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets canDrop for candy if it is candy season', () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date(2024, 9, 31));
|
||||||
|
const datedContent = require('../../website/common/script/content').default;
|
||||||
|
each(datedContent.food, foodItem => {
|
||||||
|
if (foodItem.key.indexOf('Candy_') !== -1) {
|
||||||
|
expect(foodItem.canDrop).to.equal(true);
|
||||||
|
} else {
|
||||||
|
expect(foodItem.canDrop).to.equal(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets canDrop for cake if it is cake season', () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date(2024, 0, 31));
|
||||||
|
const datedContent = require('../../website/common/script/content').default;
|
||||||
|
each(datedContent.food, foodItem => {
|
||||||
|
if (foodItem.key.indexOf('Cake_') !== -1) {
|
||||||
|
expect(foodItem.canDrop).to.equal(true);
|
||||||
|
} else {
|
||||||
|
expect(foodItem.canDrop).to.equal(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets canDrop for pie if it is pie season', () => {
|
||||||
|
clock = sinon.useFakeTimers(new Date(2024, 2, 14));
|
||||||
|
const datedContent = require('../../website/common/script/content').default;
|
||||||
|
each(datedContent.food, foodItem => {
|
||||||
|
if (foodItem.key.indexOf('Pie_') !== -1) {
|
||||||
|
expect(foodItem.canDrop).to.equal(true);
|
||||||
|
} else {
|
||||||
|
expect(foodItem.canDrop).to.equal(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets correct values for saddles', () => {
|
||||||
|
const saddle = content.food.Saddle;
|
||||||
|
expect(saddle.canBuy).to.be.a('function');
|
||||||
|
expect(saddle.value).to.equal(5);
|
||||||
|
expect(saddle.key).to.equal('Saddle');
|
||||||
|
expect(saddle.canDrop).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -4,6 +4,8 @@ import {
|
||||||
expectValidTranslationString,
|
expectValidTranslationString,
|
||||||
} from '../helpers/content.helper';
|
} from '../helpers/content.helper';
|
||||||
|
|
||||||
|
import { CLASSES } from '../../website/common/script/content/constants';
|
||||||
|
|
||||||
import gearData from '../../website/common/script/content/gear';
|
import gearData from '../../website/common/script/content/gear';
|
||||||
import * as backerGear from '../../website/common/script/content/gear/sets/special/special-backer';
|
import * as backerGear from '../../website/common/script/content/gear/sets/special/special-backer';
|
||||||
import * as contributorGear from '../../website/common/script/content/gear/sets/special/special-contributor';
|
import * as contributorGear from '../../website/common/script/content/gear/sets/special/special-contributor';
|
||||||
|
|
@ -17,35 +19,48 @@ describe('Gear', () => {
|
||||||
context(`${klass} ${gearType}s`, () => {
|
context(`${klass} ${gearType}s`, () => {
|
||||||
it('have a value of at least 0 for each stat', () => {
|
it('have a value of at least 0 for each stat', () => {
|
||||||
each(items, gear => {
|
each(items, gear => {
|
||||||
expect(gear.con).to.be.at.least(0);
|
expect(gear.con, gear.key).to.be.at.least(0);
|
||||||
expect(gear.int).to.be.at.least(0);
|
expect(gear.int, gear.key).to.be.at.least(0);
|
||||||
expect(gear.per).to.be.at.least(0);
|
expect(gear.per, gear.key).to.be.at.least(0);
|
||||||
expect(gear.str).to.be.at.least(0);
|
expect(gear.str, gear.key).to.be.at.least(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('have a purchase value of at least 0', () => {
|
it('have a purchase value of at least 0', () => {
|
||||||
each(items, gear => {
|
each(items, gear => {
|
||||||
expect(gear.value).to.be.at.least(0);
|
expect(gear.value, gear.key).to.be.at.least(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has a canBuy function', () => {
|
it('has a canBuy function', () => {
|
||||||
each(items, gear => {
|
each(items, gear => {
|
||||||
expect(gear.canBuy).to.be.a('function');
|
expect(gear.canBuy, gear.key).to.be.a('function');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('have valid translation strings for text and notes', () => {
|
it('have valid translation strings for text and notes', () => {
|
||||||
each(items, gear => {
|
each(items, gear => {
|
||||||
expectValidTranslationString(gear.text);
|
expectValidTranslationString(gear.text, gear.key);
|
||||||
expectValidTranslationString(gear.notes);
|
expectValidTranslationString(gear.notes, gear.key);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('has a set attribue', () => {
|
it('has a set attribue', () => {
|
||||||
each(items, gear => {
|
each(items, gear => {
|
||||||
expect(gear.set).to.exist;
|
expect(gear.set, gear.key).to.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a valid value for klass or specialClass', () => {
|
||||||
|
const validClassValues = CLASSES + ['base', 'mystery', 'armoire'];
|
||||||
|
each(items, gear => {
|
||||||
|
const field = gear.klass === 'special' ? gear.specialClass : gear.klass;
|
||||||
|
if (gear.klass === 'special' && field === undefined) {
|
||||||
|
// some special gear doesn't have a klass
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect(field, gear.key).to.exist;
|
||||||
|
expect(validClassValues, gear.key).to.include(field);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -53,6 +68,16 @@ describe('Gear', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('only assigns mage weapons twoHanded', () => {
|
||||||
|
each([allGear.armor.special, allGear.head.special, allGear.shield.special], gearType => {
|
||||||
|
each(gearType, gear => {
|
||||||
|
if (gear.specialClass === 'wizard') {
|
||||||
|
expect(gear.twoHanded, gear.key).to.not.eql(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('backer gear', () => {
|
describe('backer gear', () => {
|
||||||
let user;
|
let user;
|
||||||
|
|
||||||
|
|
@ -5,27 +5,33 @@ import {
|
||||||
expectValidTranslationString,
|
expectValidTranslationString,
|
||||||
} from '../helpers/content.helper';
|
} from '../helpers/content.helper';
|
||||||
|
|
||||||
import * as hatchingPotions from '../../website/common/script/content/hatching-potions';
|
import { all } from '../../website/common/script/content/hatching-potions';
|
||||||
|
|
||||||
describe('hatchingPotions', () => {
|
describe('hatchingPotions', () => {
|
||||||
describe('all', () => {
|
let clock;
|
||||||
it('is a combination of drop, premium, and wacky potions', () => {
|
|
||||||
const dropNumber = Object.keys(hatchingPotions.drops).length;
|
|
||||||
const premiumNumber = Object.keys(hatchingPotions.premium).length;
|
|
||||||
const wackyNumber = Object.keys(hatchingPotions.wacky).length;
|
|
||||||
const allNumber = Object.keys(hatchingPotions.all).length;
|
|
||||||
|
|
||||||
expect(allNumber).to.be.greaterThan(0);
|
afterEach(() => {
|
||||||
expect(allNumber).to.equal(dropNumber + premiumNumber + wackyNumber);
|
if (clock) {
|
||||||
});
|
clock.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('contains basic information about each potion', () => {
|
const potionTypes = [
|
||||||
each(hatchingPotions.all, (potion, key) => {
|
'drops',
|
||||||
expectValidTranslationString(potion.text);
|
'quests',
|
||||||
expectValidTranslationString(potion.notes);
|
'premium',
|
||||||
expect(potion.canBuy).to.be.a('function');
|
'wacky',
|
||||||
expect(potion.value).to.be.a('number');
|
];
|
||||||
expect(potion.key).to.equal(key);
|
potionTypes.forEach(potionType => {
|
||||||
|
describe(potionType, () => {
|
||||||
|
it('contains basic information about each potion', () => {
|
||||||
|
each(all, (potion, key) => {
|
||||||
|
expectValidTranslationString(potion.text);
|
||||||
|
expectValidTranslationString(potion.notes);
|
||||||
|
expect(potion.canBuy).to.be.a('function');
|
||||||
|
expect(potion.value).to.be.a('number');
|
||||||
|
expect(potion.key).to.equal(key);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
271
test/content/schedule.test.js
Normal file
271
test/content/schedule.test.js
Normal file
|
|
@ -0,0 +1,271 @@
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
import moment from 'moment';
|
||||||
|
import nconf from 'nconf';
|
||||||
|
import {
|
||||||
|
getAllScheduleMatchingGroups, clearCachedMatchers, MONTHLY_SCHEDULE, GALA_SCHEDULE,
|
||||||
|
} from '../../website/common/script/content/constants/schedule';
|
||||||
|
import QUEST_PETS from '../../website/common/script/content/quests/pets';
|
||||||
|
import QUEST_HATCHINGPOTIONS from '../../website/common/script/content/quests/potions';
|
||||||
|
import QUEST_BUNDLES from '../../website/common/script/content/bundles';
|
||||||
|
import { premium } from '../../website/common/script/content/hatching-potions';
|
||||||
|
import SPELLS from '../../website/common/script/content/spells';
|
||||||
|
import QUEST_SEASONAL from '../../website/common/script/content/quests/seasonal';
|
||||||
|
|
||||||
|
function validateMatcher (matcher, checkedDate) {
|
||||||
|
expect(matcher.end).to.be.a('date');
|
||||||
|
expect(matcher.end).to.be.greaterThan(checkedDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Content Schedule', () => {
|
||||||
|
let switchoverTime;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
switchoverTime = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0;
|
||||||
|
clearCachedMatchers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('assembles scheduled items on january 15th', () => {
|
||||||
|
const date = new Date('2024-01-15');
|
||||||
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
|
for (const key in matchers) {
|
||||||
|
if (matchers[key]) {
|
||||||
|
validateMatcher(matchers[key], date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('assembles scheduled items on january 31th', () => {
|
||||||
|
const date = new Date('2024-01-31');
|
||||||
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
|
for (const key in matchers) {
|
||||||
|
if (matchers[key]) {
|
||||||
|
validateMatcher(matchers[key], date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('assembles scheduled items on march 2nd', () => {
|
||||||
|
const date = new Date('2024-03-02');
|
||||||
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
|
for (const key in matchers) {
|
||||||
|
if (matchers[key]) {
|
||||||
|
validateMatcher(matchers[key], date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('assembles scheduled items on march 22st', () => {
|
||||||
|
const date = new Date('2024-03-22');
|
||||||
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
|
for (const key in matchers) {
|
||||||
|
if (matchers[key]) {
|
||||||
|
validateMatcher(matchers[key], date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('assembles scheduled items on october 7th', () => {
|
||||||
|
const date = new Date('2024-10-07');
|
||||||
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
|
for (const key in matchers) {
|
||||||
|
if (matchers[key]) {
|
||||||
|
validateMatcher(matchers[key], date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('assembles scheduled items on november 1th', () => {
|
||||||
|
const date = new Date('2024-11-01');
|
||||||
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
|
for (const key in matchers) {
|
||||||
|
if (matchers[key]) {
|
||||||
|
validateMatcher(matchers[key], date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('assembles scheduled items on december 20th', () => {
|
||||||
|
const date = new Date('2024-12-20');
|
||||||
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
|
for (const key in matchers) {
|
||||||
|
if (matchers[key]) {
|
||||||
|
validateMatcher(matchers[key], date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the end date if its in the same month', () => {
|
||||||
|
const date = new Date('2024-04-03');
|
||||||
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
|
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-04-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the end date if its in the next day', () => {
|
||||||
|
const date = new Date('2024-05-06T14:00:00.000Z');
|
||||||
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
|
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-05-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the end date if its on the release day', () => {
|
||||||
|
const date = new Date('2024-05-07T07:00:00.000Z');
|
||||||
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
|
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-06-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the end date if its next month', () => {
|
||||||
|
const date = new Date('2024-05-20T01:00:00.000Z');
|
||||||
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
|
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-06-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the end date for a gala', () => {
|
||||||
|
const date = new Date('2024-05-20');
|
||||||
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
|
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2024-06-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains content for repeating events', () => {
|
||||||
|
const date = new Date('2024-04-15');
|
||||||
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
|
expect(matchers.premiumHatchingPotions).to.exist;
|
||||||
|
expect(matchers.premiumHatchingPotions.items.length).to.equal(4);
|
||||||
|
expect(matchers.premiumHatchingPotions.items.indexOf('Garden')).to.not.equal(-1);
|
||||||
|
expect(matchers.premiumHatchingPotions.items.indexOf('Porcelain')).to.not.equal(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('only contains valid keys for', () => {
|
||||||
|
it('pet quests', () => {
|
||||||
|
const petKeys = Object.keys(QUEST_PETS);
|
||||||
|
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
|
||||||
|
const petQuests = MONTHLY_SCHEDULE[key][14].find(item => item.type === 'petQuests');
|
||||||
|
for (const petQuest of petQuests.items) {
|
||||||
|
expect(petQuest).to.be.a('string');
|
||||||
|
expect(petKeys).to.include(petQuest);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hatchingpotion quests', () => {
|
||||||
|
const potionKeys = Object.keys(QUEST_HATCHINGPOTIONS);
|
||||||
|
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
|
||||||
|
const potionQuests = MONTHLY_SCHEDULE[key][14].find(item => item.type === 'hatchingPotionQuests');
|
||||||
|
for (const potionQuest of potionQuests.items) {
|
||||||
|
expect(potionQuest).to.be.a('string');
|
||||||
|
expect(potionKeys).to.include(potionQuest);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bundles', () => {
|
||||||
|
const bundleKeys = Object.keys(QUEST_BUNDLES);
|
||||||
|
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
|
||||||
|
const bundles = MONTHLY_SCHEDULE[key][14].find(item => item.type === 'bundles');
|
||||||
|
for (const bundle of bundles.items) {
|
||||||
|
expect(bundle).to.be.a('string');
|
||||||
|
expect(bundleKeys).to.include(bundle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('premium hatching potions', () => {
|
||||||
|
const potionKeys = Object.keys(premium);
|
||||||
|
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
|
||||||
|
const monthlyPotions = MONTHLY_SCHEDULE[key][21].find(item => item.type === 'premiumHatchingPotions');
|
||||||
|
for (const potion of monthlyPotions.items) {
|
||||||
|
expect(potion).to.be.a('string');
|
||||||
|
expect(potionKeys).to.include(potion);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('seasonal quests', () => {
|
||||||
|
const questKeys = Object.keys(QUEST_SEASONAL);
|
||||||
|
Object.keys(GALA_SCHEDULE).forEach(key => {
|
||||||
|
const quests = GALA_SCHEDULE[key].matchers.find(item => item.type === 'seasonalQuests');
|
||||||
|
for (const quest of quests.items) {
|
||||||
|
expect(quest).to.be.a('string');
|
||||||
|
expect(questKeys).to.include(quest);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('seasonal spells', () => {
|
||||||
|
const spellKeys = Object.keys(SPELLS.special);
|
||||||
|
Object.keys(GALA_SCHEDULE).forEach(key => {
|
||||||
|
const petQuests = GALA_SCHEDULE[key].matchers.find(item => item.type === 'seasonalSpells');
|
||||||
|
for (const petQuest of petQuests.items) {
|
||||||
|
expect(petQuest).to.be.a('string');
|
||||||
|
expect(spellKeys).to.include(petQuest);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('backgrounds matcher', () => {
|
||||||
|
it('allows background matching the month for new backgrounds', () => {
|
||||||
|
const date = new Date('2024-07-08');
|
||||||
|
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||||
|
expect(matcher.match('backgroundkey072024')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallows background in the future', () => {
|
||||||
|
const date = new Date('2024-07-08');
|
||||||
|
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||||
|
expect(matcher.match('backgroundkey072025')).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallows background for the inverse month for new backgrounds', () => {
|
||||||
|
const date = new Date('2024-07-08');
|
||||||
|
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||||
|
expect(matcher.match('backgroundkey012024')).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows background for the inverse month for old backgrounds', () => {
|
||||||
|
const date = new Date('2024-08-08');
|
||||||
|
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||||
|
expect(matcher.match('backgroundkey022023')).to.be.true;
|
||||||
|
expect(matcher.match('backgroundkey022021')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows background even yeared backgrounds in first half of year', () => {
|
||||||
|
const date = new Date('2025-02-08');
|
||||||
|
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||||
|
expect(matcher.match('backgroundkey022024')).to.be.true;
|
||||||
|
expect(matcher.match('backgroundkey082022')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows background odd yeared backgrounds in second half of year', () => {
|
||||||
|
const date = new Date('2024-08-08');
|
||||||
|
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||||
|
expect(matcher.match('backgroundkey022023')).to.be.true;
|
||||||
|
expect(matcher.match('backgroundkey082021')).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('timeTravelers matcher', () => {
|
||||||
|
it('allows sets matching the month', () => {
|
||||||
|
const date = new Date('2024-07-08');
|
||||||
|
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
|
||||||
|
expect(matcher.match('202307')).to.be.true;
|
||||||
|
expect(matcher.match('202207')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallows sets not matching the month', () => {
|
||||||
|
const date = new Date('2024-07-08');
|
||||||
|
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
|
||||||
|
expect(matcher.match('202306')).to.be.false;
|
||||||
|
expect(matcher.match('202402')).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallows sets from current month', () => {
|
||||||
|
const date = new Date('2024-07-08');
|
||||||
|
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
|
||||||
|
expect(matcher.match('202407')).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disallows sets from the future', () => {
|
||||||
|
const date = new Date('2024-07-08');
|
||||||
|
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||||
|
expect(matcher.match('202507')).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
52
test/content/shop-featuredItems.test.js
Normal file
52
test/content/shop-featuredItems.test.js
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import Sinon from 'sinon';
|
||||||
|
import featuredItems from '../../website/common/script/content/shop-featuredItems';
|
||||||
|
|
||||||
|
describe('Shop Featured Items', () => {
|
||||||
|
let clock;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (clock !== undefined) {
|
||||||
|
clock.restore();
|
||||||
|
clock = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Market', () => {
|
||||||
|
it('contains armoire', () => {
|
||||||
|
const items = featuredItems.market();
|
||||||
|
expect(_.find(items, item => item.path === 'armoire')).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains the current premium hatching potions', () => {
|
||||||
|
clock = Sinon.useFakeTimers(new Date('2024-04-08'));
|
||||||
|
const items = featuredItems.market();
|
||||||
|
expect(_.find(items, item => item.path === 'premiumHatchingPotions.Porcelain')).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is featuring 4 items', () => {
|
||||||
|
clock = Sinon.useFakeTimers(new Date('2024-02-08'));
|
||||||
|
const items = featuredItems.market();
|
||||||
|
expect(items.length).to.eql(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Quest Shop', () => {
|
||||||
|
it('contains bundle', () => {
|
||||||
|
clock = Sinon.useFakeTimers(new Date('2024-03-08'));
|
||||||
|
const items = featuredItems.quests();
|
||||||
|
expect(_.find(items, item => item.path === 'quests.pinkMarble')).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contains pet quests', () => {
|
||||||
|
clock = Sinon.useFakeTimers(new Date('2024-04-08'));
|
||||||
|
const items = featuredItems.quests();
|
||||||
|
expect(_.find(items, item => item.path === 'quests.frog')).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is featuring 3 items', () => {
|
||||||
|
clock = Sinon.useFakeTimers(new Date('2024-02-08'));
|
||||||
|
const items = featuredItems.quests();
|
||||||
|
expect(items.length).to.eql(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
63
test/content/spells.test.js
Normal file
63
test/content/spells.test.js
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
} from '../helpers/common.helper';
|
||||||
|
import spells from '../../website/common/script/content/spells';
|
||||||
|
import {
|
||||||
|
expectValidTranslationString,
|
||||||
|
} from '../helpers/content.helper';
|
||||||
|
import { TRANSFORMATION_DEBUFFS_LIST } from '../../website/common/script/constants';
|
||||||
|
|
||||||
|
// TODO complete the test suite...
|
||||||
|
|
||||||
|
describe('shared.ops.spells', () => {
|
||||||
|
let user;
|
||||||
|
let target;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
user = generateUser();
|
||||||
|
target = generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all spells have required properties', () => {
|
||||||
|
for (const category of Object.values(spells)) {
|
||||||
|
for (const spell of Object.values(category)) {
|
||||||
|
expectValidTranslationString(spell.text, spell.key);
|
||||||
|
expectValidTranslationString(spell.notes);
|
||||||
|
expect(spell.target, spell.key).to.be.oneOf(['self', 'party', 'task', 'tasks', 'user']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all special spells have a working cast method', async () => {
|
||||||
|
for (const s of Object.values(spells.special)) {
|
||||||
|
user.items.special[s.key] = 1;
|
||||||
|
s.cast(user, target, { language: 'en' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all debuff spells cost 5 gold', () => {
|
||||||
|
for (const s of Object.values(spells.special)) {
|
||||||
|
if (s.purchaseType === 'debuffPotion') {
|
||||||
|
user.stats.gp = 5;
|
||||||
|
s.cast(user);
|
||||||
|
expect(user.stats.gp).to.equal(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all debuff spells remove the buff', () => {
|
||||||
|
const debuffMapping = {};
|
||||||
|
Object.keys(TRANSFORMATION_DEBUFFS_LIST).forEach(key => {
|
||||||
|
debuffMapping[TRANSFORMATION_DEBUFFS_LIST[key]] = key;
|
||||||
|
});
|
||||||
|
for (const s of Object.values(spells.special)) {
|
||||||
|
if (s.purchaseType === 'debuffPotion') {
|
||||||
|
user.stats.gp = 5;
|
||||||
|
user.stats.buffs[debuffMapping[s.key]] = true;
|
||||||
|
expect(user.stats.buffs[debuffMapping[s.key]]).to.equal(true);
|
||||||
|
s.cast(user);
|
||||||
|
expect(user.stats.buffs[debuffMapping[s.key]]).to.equal(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -6,23 +6,105 @@ import timeTravelers from '../../website/common/script/content/time-travelers';
|
||||||
|
|
||||||
describe('time-travelers store', () => {
|
describe('time-travelers store', () => {
|
||||||
let user;
|
let user;
|
||||||
|
let date;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
user = generateUser();
|
user = generateUser();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes owned sets from the time travelers store', () => {
|
describe('on january 15th', () => {
|
||||||
user.items.gear.owned.head_mystery_201602 = true; // eslint-disable-line camelcase
|
beforeEach(() => {
|
||||||
expect(timeTravelers.timeTravelerStore(user)['201602']).to.not.exist;
|
date = new Date('2024-01-15');
|
||||||
expect(timeTravelers.timeTravelerStore(user)['201603']).to.exist;
|
});
|
||||||
|
it('returns the correct gear', () => {
|
||||||
|
const items = timeTravelers.timeTravelerStore(user, date);
|
||||||
|
for (const [key] of Object.entries(items)) {
|
||||||
|
if (key.startsWith('20')) {
|
||||||
|
expect(key).to.match(/20[0-9]{2}(01|07)/);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('removes owned sets from the time travelers store', () => {
|
||||||
|
user.items.gear.owned.head_mystery_201601 = true; // eslint-disable-line camelcase
|
||||||
|
const items = timeTravelers.timeTravelerStore(user, date);
|
||||||
|
expect(items['201601']).to.not.exist;
|
||||||
|
expect(items['201801']).to.exist;
|
||||||
|
expect(items['202207']).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes unopened mystery item sets from the time travelers store', () => {
|
||||||
|
user.purchased = {
|
||||||
|
plan: {
|
||||||
|
mysteryItems: ['head_mystery_201601'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const items = timeTravelers.timeTravelerStore(user, date);
|
||||||
|
expect(items['201601']).to.not.exist;
|
||||||
|
expect(items['201607']).to.exist;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes unopened mystery item sets from the time travelers store', () => {
|
describe('on may 1st', () => {
|
||||||
user.purchased = {
|
beforeEach(() => {
|
||||||
plan: {
|
date = new Date('2024-05-01');
|
||||||
mysteryItems: ['head_mystery_201602'],
|
});
|
||||||
},
|
it('returns the correct gear', () => {
|
||||||
};
|
const items = timeTravelers.timeTravelerStore(user, date);
|
||||||
expect(timeTravelers.timeTravelerStore(user)['201602']).to.not.exist;
|
for (const [key] of Object.entries(items)) {
|
||||||
expect(timeTravelers.timeTravelerStore(user)['201603']).to.exist;
|
if (key.startsWith('20')) {
|
||||||
|
expect(key).to.match(/20[0-9]{2}(05|11)/);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('removes owned sets from the time travelers store', () => {
|
||||||
|
user.items.gear.owned.head_mystery_201705 = true; // eslint-disable-line camelcase
|
||||||
|
const items = timeTravelers.timeTravelerStore(user, date);
|
||||||
|
expect(items['201705']).to.not.exist;
|
||||||
|
expect(items['201805']).to.exist;
|
||||||
|
expect(items['202211']).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes unopened mystery item sets from the time travelers store', () => {
|
||||||
|
user.purchased = {
|
||||||
|
plan: {
|
||||||
|
mysteryItems: ['head_mystery_201705'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const items = timeTravelers.timeTravelerStore(user, date);
|
||||||
|
expect(items['201705']).to.not.exist;
|
||||||
|
expect(items['201611']).to.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on october 21st', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
date = new Date('2024-10-21');
|
||||||
|
});
|
||||||
|
it('returns the correct gear', () => {
|
||||||
|
const items = timeTravelers.timeTravelerStore(user, date);
|
||||||
|
for (const [key] of Object.entries(items)) {
|
||||||
|
if (key.startsWith('20')) {
|
||||||
|
expect(key).to.match(/20[0-9]{2}(10|04)/);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('removes owned sets from the time travelers store', () => {
|
||||||
|
user.items.gear.owned.head_mystery_201810 = true; // eslint-disable-line camelcase
|
||||||
|
user.items.gear.owned.armor_mystery_201810 = true; // eslint-disable-line camelcase
|
||||||
|
const items = timeTravelers.timeTravelerStore(user, date);
|
||||||
|
expect(items['201810']).to.not.exist;
|
||||||
|
expect(items['201910']).to.exist;
|
||||||
|
expect(items['202204']).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes unopened mystery item sets from the time travelers store', () => {
|
||||||
|
user.purchased = {
|
||||||
|
plan: {
|
||||||
|
mysteryItems: ['armor_mystery_201710'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const items = timeTravelers.timeTravelerStore(user, date);
|
||||||
|
expect(items['201710']).to.not.exist;
|
||||||
|
expect(items['201604']).to.exist;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ export function generateRes (options = {}) {
|
||||||
redirect: sandbox.stub(),
|
redirect: sandbox.stub(),
|
||||||
render: sandbox.stub(),
|
render: sandbox.stub(),
|
||||||
send: sandbox.stub(),
|
send: sandbox.stub(),
|
||||||
|
sendFile: sandbox.stub(),
|
||||||
sendStatus: sandbox.stub().returnsThis(),
|
sendStatus: sandbox.stub().returnsThis(),
|
||||||
set: sandbox.stub(),
|
set: sandbox.stub(),
|
||||||
status: sandbox.stub().returnsThis(),
|
status: sandbox.stub().returnsThis(),
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,13 @@ i18n.translations = translations;
|
||||||
export const STRING_ERROR_MSG = /^Error processing the string ".*". Please see Help > Report a Bug.$/;
|
export const STRING_ERROR_MSG = /^Error processing the string ".*". Please see Help > Report a Bug.$/;
|
||||||
export const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
|
export const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
|
||||||
|
|
||||||
export function expectValidTranslationString (attribute) {
|
export function expectValidTranslationString (attribute, contextKey) {
|
||||||
expect(attribute).to.be.a('function');
|
expect(attribute, contextKey).to.be.a('function');
|
||||||
|
|
||||||
const translatedString = attribute();
|
const translatedString = attribute();
|
||||||
|
|
||||||
expect(translatedString.trim()).to.not.be.empty;
|
expect(translatedString.trim(), contextKey).to.not.be.empty;
|
||||||
expect(translatedString).to.not.contain('function func(lang)');
|
expect(translatedString, contextKey).to.not.contain('function func(lang)');
|
||||||
expect(translatedString).to.not.eql(STRING_ERROR_MSG);
|
expect(translatedString, contextKey).to.not.eql(STRING_ERROR_MSG);
|
||||||
expect(translatedString).to.not.match(STRING_DOES_NOT_EXIST_MSG);
|
expect(translatedString, contextKey).to.not.match(STRING_DOES_NOT_EXIST_MSG);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
429
website/client/package-lock.json
generated
429
website/client/package-lock.json
generated
|
|
@ -16,12 +16,12 @@
|
||||||
"@vue/cli-service": "^5.0.8",
|
"@vue/cli-service": "^5.0.8",
|
||||||
"@vue/test-utils": "1.0.0-beta.29",
|
"@vue/test-utils": "1.0.0-beta.29",
|
||||||
"amplitude-js": "^8.21.3",
|
"amplitude-js": "^8.21.3",
|
||||||
|
"assert": "^2.1.0",
|
||||||
"axios": "^0.28.0",
|
"axios": "^0.28.0",
|
||||||
"axios-progress-bar": "^1.2.0",
|
"axios-progress-bar": "^1.2.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^4.6.0",
|
||||||
"bootstrap-vue": "^2.23.1",
|
"bootstrap-vue": "^2.23.1",
|
||||||
"chai": "^5.1.0",
|
|
||||||
"core-js": "^3.33.1",
|
"core-js": "^3.33.1",
|
||||||
"dompurify": "^3.0.3",
|
"dompurify": "^3.0.3",
|
||||||
"eslint": "7.32.0",
|
"eslint": "7.32.0",
|
||||||
|
|
@ -30,16 +30,18 @@
|
||||||
"eslint-plugin-vue": "7.20.0",
|
"eslint-plugin-vue": "7.20.0",
|
||||||
"habitica-markdown": "^3.0.0",
|
"habitica-markdown": "^3.0.0",
|
||||||
"hellojs": "^1.20.0",
|
"hellojs": "^1.20.0",
|
||||||
"inspectpack": "^4.7.1",
|
|
||||||
"intro.js": "^7.2.0",
|
"intro.js": "^7.2.0",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
|
"moment-locales-webpack-plugin": "^1.2.0",
|
||||||
"nconf": "^0.12.1",
|
"nconf": "^0.12.1",
|
||||||
"sass": "^1.63.4",
|
"sass": "^1.63.4",
|
||||||
"sass-loader": "^14.1.1",
|
"sass-loader": "^14.1.1",
|
||||||
|
"sinon": "^17.0.1",
|
||||||
"smartbanner.js": "^1.19.3",
|
"smartbanner.js": "^1.19.3",
|
||||||
"stopword": "^2.0.8",
|
"stopword": "^2.0.8",
|
||||||
|
"timers-browserify": "^2.0.12",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"validator": "^13.9.0",
|
"validator": "^13.9.0",
|
||||||
"vue": "^2.7.10",
|
"vue": "^2.7.10",
|
||||||
|
|
@ -49,11 +51,15 @@
|
||||||
"vue-template-babel-compiler": "^2.0.0",
|
"vue-template-babel-compiler": "^2.0.0",
|
||||||
"vue-template-compiler": "^2.7.10",
|
"vue-template-compiler": "^2.7.10",
|
||||||
"vuedraggable": "^2.24.3",
|
"vuedraggable": "^2.24.3",
|
||||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
|
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0"
|
||||||
"webpack": "^5.89.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.21.0"
|
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
|
||||||
|
"babel-plugin-lodash": "^3.3.4",
|
||||||
|
"chai": "^5.1.0",
|
||||||
|
"inspectpack": "^4.7.1",
|
||||||
|
"terser-webpack-plugin": "^5.3.10",
|
||||||
|
"webpack": "^5.89.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aashutoshrathi/word-wrap": {
|
"node_modules/@aashutoshrathi/word-wrap": {
|
||||||
|
|
@ -2120,6 +2126,45 @@
|
||||||
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
|
||||||
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
|
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@sinonjs/commons": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"type-detect": "4.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sinonjs/fake-timers": {
|
||||||
|
"version": "11.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz",
|
||||||
|
"integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@sinonjs/commons": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sinonjs/samsam": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==",
|
||||||
|
"dependencies": {
|
||||||
|
"@sinonjs/commons": "^2.0.0",
|
||||||
|
"lodash.get": "^4.4.2",
|
||||||
|
"type-detect": "^4.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
|
||||||
|
"dependencies": {
|
||||||
|
"type-detect": "4.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sinonjs/text-encoding": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ=="
|
||||||
|
},
|
||||||
"node_modules/@soda/friendly-errors-webpack-plugin": {
|
"node_modules/@soda/friendly-errors-webpack-plugin": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz",
|
||||||
|
|
@ -3601,10 +3646,23 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/assert": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"is-nan": "^1.3.2",
|
||||||
|
"object-is": "^1.1.5",
|
||||||
|
"object.assign": "^4.1.4",
|
||||||
|
"util": "^0.12.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/assertion-error": {
|
"node_modules/assertion-error": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
|
|
@ -3683,9 +3741,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.28.1.tgz",
|
||||||
"integrity": "sha512-Tu7NYoGY4Yoc7I+Npf9HhUMtEEpV7ZiLH9yndTCoNhcpBH0kwcvFbzYN9/u5QKI5A6uefjsNNWaz5olJVYS62Q==",
|
"integrity": "sha512-iUcGA5a7p0mVb4Gm/sy+FSECNkPFT4y7wt6OM/CDpO/OnNCvSs3PoMG8ibrC9jRoGYU0gUK5pXVC4NPXq6lHRQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.0",
|
"follow-redirects": "^1.15.0",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
|
|
@ -3759,6 +3817,19 @@
|
||||||
"object.assign": "^4.1.0"
|
"object.assign": "^4.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/babel-plugin-lodash": {
|
||||||
|
"version": "3.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-plugin-lodash/-/babel-plugin-lodash-3.3.4.tgz",
|
||||||
|
"integrity": "sha512-yDZLjK7TCkWl1gpBeBGmuaDIFhZKmkoL+Cu2MUUjv5VxUZx/z7tBGBCBcQs5RI1Bkz5LLmNdjx7paOyQtMovyg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-module-imports": "^7.0.0-beta.49",
|
||||||
|
"@babel/types": "^7.0.0-beta.49",
|
||||||
|
"glob": "^7.1.1",
|
||||||
|
"lodash": "^4.17.10",
|
||||||
|
"require-package-name": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-plugin-polyfill-corejs2": {
|
"node_modules/babel-plugin-polyfill-corejs2": {
|
||||||
"version": "0.4.7",
|
"version": "0.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.7.tgz",
|
||||||
|
|
@ -4063,13 +4134,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/call-bind": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||||
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
|
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
"function-bind": "^1.1.2",
|
"function-bind": "^1.1.2",
|
||||||
"get-intrinsic": "^1.2.1",
|
"get-intrinsic": "^1.2.4",
|
||||||
"set-function-length": "^1.1.1"
|
"set-function-length": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
|
@ -4142,12 +4218,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chai": {
|
"node_modules/chai": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
|
||||||
"integrity": "sha512-kDZ7MZyM6Q1DhR9jy7dalKohXQ2yrlXkk59CR52aRKxJrobmlBNqnFQxX9xOX8w+4mz8SYlKJa/7D7ddltFXCw==",
|
"integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"assertion-error": "^2.0.1",
|
"assertion-error": "^2.0.1",
|
||||||
"check-error": "^2.0.0",
|
"check-error": "^2.1.1",
|
||||||
"deep-eql": "^5.0.1",
|
"deep-eql": "^5.0.1",
|
||||||
"loupe": "^3.1.0",
|
"loupe": "^3.1.0",
|
||||||
"pathval": "^2.0.0"
|
"pathval": "^2.0.0"
|
||||||
|
|
@ -4170,9 +4247,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/check-error": {
|
"node_modules/check-error": {
|
||||||
"version": "2.0.0",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
||||||
"integrity": "sha512-tjLAOBHKVxtPoHe/SA7kNOMvhCRdCJ3vETdeY0RuAc9popf+hyaSV6ZEg9hr4cpWF7jmo/JSWEnLDrnijS9Tog==",
|
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
}
|
}
|
||||||
|
|
@ -5046,6 +5124,7 @@
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.1.tgz",
|
||||||
"integrity": "sha512-nwQCf6ne2gez3o1MxWifqkciwt0zhl0LO1/UwVu4uMBuPmflWM4oQ70XMqHqnBJA+nhzncaqL9HVL6KkHJ28lw==",
|
"integrity": "sha512-nwQCf6ne2gez3o1MxWifqkciwt0zhl0LO1/UwVu4uMBuPmflWM4oQ70XMqHqnBJA+nhzncaqL9HVL6KkHJ28lw==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
|
|
@ -5141,16 +5220,19 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/define-data-property": {
|
"node_modules/define-data-property": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
|
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"get-intrinsic": "^1.2.1",
|
"es-define-property": "^1.0.0",
|
||||||
"gopd": "^1.0.1",
|
"es-errors": "^1.3.0",
|
||||||
"has-property-descriptors": "^1.0.0"
|
"gopd": "^1.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/define-lazy-prop": {
|
"node_modules/define-lazy-prop": {
|
||||||
|
|
@ -5521,6 +5603,25 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"get-intrinsic": "^1.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-module-lexer": {
|
"node_modules/es-module-lexer": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
|
||||||
|
|
@ -6929,7 +7030,8 @@
|
||||||
"node_modules/fp-ts": {
|
"node_modules/fp-ts": {
|
||||||
"version": "2.16.1",
|
"version": "2.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.16.1.tgz",
|
||||||
"integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA=="
|
"integrity": "sha512-by7U5W8dkIzcvDofUcO42yl9JbnHTEDBrzu3pt5fKT+Z4Oy85I21K80EYJYdjQGC2qum4Vo55Ag57iiIK4FYuA==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
|
|
@ -6975,19 +7077,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||||
},
|
},
|
||||||
"node_modules/fsevents": {
|
|
||||||
"version": "2.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
|
@ -7046,20 +7135,25 @@
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
|
||||||
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
|
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/get-intrinsic": {
|
"node_modules/get-intrinsic": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||||
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
|
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
"function-bind": "^1.1.2",
|
"function-bind": "^1.1.2",
|
||||||
"has-proto": "^1.0.1",
|
"has-proto": "^1.0.1",
|
||||||
"has-symbols": "^1.0.3",
|
"has-symbols": "^1.0.3",
|
||||||
"hasown": "^2.0.0"
|
"hasown": "^2.0.0"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
|
|
@ -7253,11 +7347,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/has-property-descriptors": {
|
"node_modules/has-property-descriptors": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||||
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
|
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"get-intrinsic": "^1.2.2"
|
"es-define-property": "^1.0.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
|
@ -7660,6 +7754,7 @@
|
||||||
"version": "4.7.1",
|
"version": "4.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/inspectpack/-/inspectpack-4.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/inspectpack/-/inspectpack-4.7.1.tgz",
|
||||||
"integrity": "sha512-XoDJbKSM9I2KA+8+OLFJHm8m4NM2pMEgsDD2hze6swVfynEed9ngCx36mRR+otzOsskwnxIZWXjI23FTW1uHqA==",
|
"integrity": "sha512-XoDJbKSM9I2KA+8+OLFJHm8m4NM2pMEgsDD2hze6swVfynEed9ngCx36mRR+otzOsskwnxIZWXjI23FTW1uHqA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"fp-ts": "^2.6.1",
|
"fp-ts": "^2.6.1",
|
||||||
|
|
@ -7680,6 +7775,7 @@
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-convert": "^2.0.1"
|
"color-convert": "^2.0.1"
|
||||||
},
|
},
|
||||||
|
|
@ -7694,6 +7790,7 @@
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^4.1.0",
|
"ansi-styles": "^4.1.0",
|
||||||
"supports-color": "^7.1.0"
|
"supports-color": "^7.1.0"
|
||||||
|
|
@ -7709,6 +7806,7 @@
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-name": "~1.1.4"
|
"color-name": "~1.1.4"
|
||||||
},
|
},
|
||||||
|
|
@ -7719,12 +7817,14 @@
|
||||||
"node_modules/inspectpack/node_modules/color-name": {
|
"node_modules/inspectpack/node_modules/color-name": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/inspectpack/node_modules/has-flag": {
|
"node_modules/inspectpack/node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
|
|
@ -7733,6 +7833,7 @@
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-flag": "^4.0.0"
|
"has-flag": "^4.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -7770,6 +7871,7 @@
|
||||||
"version": "2.2.21",
|
"version": "2.2.21",
|
||||||
"resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.21.tgz",
|
"resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.21.tgz",
|
||||||
"integrity": "sha512-zz2Z69v9ZIC3mMLYWIeoUcwWD6f+O7yP92FMVVaXEOSZH1jnVBmET/urd/uoarD1WGBY4rCj8TAyMPzsGNzMFQ==",
|
"integrity": "sha512-zz2Z69v9ZIC3mMLYWIeoUcwWD6f+O7yP92FMVVaXEOSZH1jnVBmET/urd/uoarD1WGBY4rCj8TAyMPzsGNzMFQ==",
|
||||||
|
"dev": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"fp-ts": "^2.5.0"
|
"fp-ts": "^2.5.0"
|
||||||
}
|
}
|
||||||
|
|
@ -7778,6 +7880,7 @@
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/io-ts-reporters/-/io-ts-reporters-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/io-ts-reporters/-/io-ts-reporters-1.2.2.tgz",
|
||||||
"integrity": "sha512-igASwWWkDY757OutNcM6zTtdJf/eTZYkoe2ymsX2qpm5bKZLo74FJYjsCtMQOEdY7dRHLLEulCyFQwdN69GBCg==",
|
"integrity": "sha512-igASwWWkDY757OutNcM6zTtdJf/eTZYkoe2ymsX2qpm5bKZLo74FJYjsCtMQOEdY7dRHLLEulCyFQwdN69GBCg==",
|
||||||
|
"dev": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"fp-ts": "^2.0.2",
|
"fp-ts": "^2.0.2",
|
||||||
"io-ts": "^2.0.0"
|
"io-ts": "^2.0.0"
|
||||||
|
|
@ -7791,6 +7894,21 @@
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-arguments": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"has-tostringtag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
|
||||||
|
|
@ -7931,6 +8049,20 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-generator-function": {
|
||||||
|
"version": "1.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
|
||||||
|
"integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-tostringtag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-glob": {
|
"node_modules/is-glob": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
|
|
@ -7950,6 +8082,21 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-nan": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.0",
|
||||||
|
"define-properties": "^1.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-negative-zero": {
|
"node_modules/is-negative-zero": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
|
||||||
|
|
@ -8335,6 +8482,11 @@
|
||||||
"graceful-fs": "^4.1.6"
|
"graceful-fs": "^4.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/just-extend": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw=="
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
|
|
@ -8470,6 +8622,16 @@
|
||||||
"resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz",
|
||||||
"integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA=="
|
"integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.difference": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="
|
||||||
|
},
|
||||||
|
"node_modules/lodash.get": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ=="
|
||||||
|
},
|
||||||
"node_modules/lodash.kebabcase": {
|
"node_modules/lodash.kebabcase": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
|
||||||
|
|
@ -8682,9 +8844,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/loupe": {
|
"node_modules/loupe": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
|
||||||
"integrity": "sha512-qKl+FrLXUhFuHUoDJG7f8P8gEMHq9NFS0c6ghXG1J0rldmZFQZoNVv/vyirE9qwCIhWZDsvEFd1sbFu3GvRQFg==",
|
"integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"get-func-name": "^2.0.1"
|
"get-func-name": "^2.0.1"
|
||||||
}
|
}
|
||||||
|
|
@ -9447,6 +9610,18 @@
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/moment-locales-webpack-plugin": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment-locales-webpack-plugin/-/moment-locales-webpack-plugin-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-QAi5v0OlPUP7GXviKMtxnpBAo8WmTHrUNN7iciAhNOEAd9evCOvuN0g1N7ThIg3q11GLCkjY1zQ2saRcf/43nQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.difference": "^4.5.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"moment": "^2.8.0",
|
||||||
|
"webpack": "^1 || ^2 || ^3 || ^4 || ^5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mrmime": {
|
"node_modules/mrmime": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
|
||||||
|
|
@ -9530,6 +9705,23 @@
|
||||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/nise": {
|
||||||
|
"version": "5.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz",
|
||||||
|
"integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==",
|
||||||
|
"dependencies": {
|
||||||
|
"@sinonjs/commons": "^3.0.0",
|
||||||
|
"@sinonjs/fake-timers": "^11.2.2",
|
||||||
|
"@sinonjs/text-encoding": "^0.7.2",
|
||||||
|
"just-extend": "^6.2.0",
|
||||||
|
"path-to-regexp": "^6.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nise/node_modules/path-to-regexp": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
|
||||||
|
},
|
||||||
"node_modules/no-case": {
|
"node_modules/no-case": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
||||||
|
|
@ -9693,6 +9885,21 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-is": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.7",
|
||||||
|
"define-properties": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-keys": {
|
"node_modules/object-keys": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
|
|
@ -10135,6 +10342,7 @@
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
|
||||||
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
|
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.16"
|
"node": ">= 14.16"
|
||||||
}
|
}
|
||||||
|
|
@ -10159,6 +10367,7 @@
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
|
||||||
"integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==",
|
"integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
|
|
@ -11275,6 +11484,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/require-package-name": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/requires-port": {
|
"node_modules/requires-port": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
|
|
@ -11434,9 +11649,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sass-loader": {
|
"node_modules/sass-loader": {
|
||||||
"version": "14.1.1",
|
"version": "14.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.2.1.tgz",
|
||||||
"integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==",
|
"integrity": "sha512-G0VcnMYU18a4N7VoNDegg2OuMjYtxnqzQWARVWCIVSZwJeiL9kg8QMsuIZOplsJgTzZLF6jGxI3AClj8I9nRdQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"neo-async": "^2.6.2"
|
"neo-async": "^2.6.2"
|
||||||
},
|
},
|
||||||
|
|
@ -11533,7 +11748,8 @@
|
||||||
"node_modules/semver-compare": {
|
"node_modules/semver-compare": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||||
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="
|
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/send": {
|
"node_modules/send": {
|
||||||
"version": "0.18.0",
|
"version": "0.18.0",
|
||||||
|
|
@ -11674,15 +11890,16 @@
|
||||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
|
||||||
},
|
},
|
||||||
"node_modules/set-function-length": {
|
"node_modules/set-function-length": {
|
||||||
"version": "1.1.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
|
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"define-data-property": "^1.1.1",
|
"define-data-property": "^1.1.4",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
"function-bind": "^1.1.2",
|
"function-bind": "^1.1.2",
|
||||||
"get-intrinsic": "^1.2.2",
|
"get-intrinsic": "^1.2.4",
|
||||||
"gopd": "^1.0.1",
|
"gopd": "^1.0.1",
|
||||||
"has-property-descriptors": "^1.0.1"
|
"has-property-descriptors": "^1.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
|
@ -11701,6 +11918,11 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/setimmediate": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
|
||||||
|
},
|
||||||
"node_modules/setprototypeof": {
|
"node_modules/setprototypeof": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
|
|
@ -11762,6 +11984,50 @@
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
|
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/sinon": {
|
||||||
|
"version": "17.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
|
||||||
|
"integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@sinonjs/commons": "^3.0.0",
|
||||||
|
"@sinonjs/fake-timers": "^11.2.2",
|
||||||
|
"@sinonjs/samsam": "^8.0.0",
|
||||||
|
"diff": "^5.1.0",
|
||||||
|
"nise": "^5.1.5",
|
||||||
|
"supports-color": "^7.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/sinon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sinon/node_modules/diff": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sinon/node_modules/has-flag": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sinon/node_modules/supports-color": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/sirv": {
|
"node_modules/sirv": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
|
||||||
|
|
@ -12261,15 +12527,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser-webpack-plugin": {
|
"node_modules/terser-webpack-plugin": {
|
||||||
"version": "5.3.9",
|
"version": "5.3.10",
|
||||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
|
||||||
"integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==",
|
"integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/trace-mapping": "^0.3.17",
|
"@jridgewell/trace-mapping": "^0.3.20",
|
||||||
"jest-worker": "^27.4.5",
|
"jest-worker": "^27.4.5",
|
||||||
"schema-utils": "^3.1.1",
|
"schema-utils": "^3.1.1",
|
||||||
"serialize-javascript": "^6.0.1",
|
"serialize-javascript": "^6.0.1",
|
||||||
"terser": "^5.16.8"
|
"terser": "^5.26.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.13.0"
|
"node": ">= 10.13.0"
|
||||||
|
|
@ -12404,6 +12670,17 @@
|
||||||
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
|
||||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="
|
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/timers-browserify": {
|
||||||
|
"version": "2.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz",
|
||||||
|
"integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"setimmediate": "^1.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/to-fast-properties": {
|
"node_modules/to-fast-properties": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||||
|
|
@ -12515,6 +12792,14 @@
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/type-detect": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||||
|
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/type-fest": {
|
"node_modules/type-fest": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
|
||||||
|
|
@ -12718,6 +13003,18 @@
|
||||||
"requires-port": "^1.0.0"
|
"requires-port": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util": {
|
||||||
|
"version": "0.12.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||||
|
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"is-arguments": "^1.0.4",
|
||||||
|
"is-generator-function": "^1.0.7",
|
||||||
|
"is-typed-array": "^1.1.3",
|
||||||
|
"which-typed-array": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,12 @@
|
||||||
"@vue/cli-service": "^5.0.8",
|
"@vue/cli-service": "^5.0.8",
|
||||||
"@vue/test-utils": "1.0.0-beta.29",
|
"@vue/test-utils": "1.0.0-beta.29",
|
||||||
"amplitude-js": "^8.21.3",
|
"amplitude-js": "^8.21.3",
|
||||||
|
"assert": "^2.1.0",
|
||||||
"axios": "^0.28.0",
|
"axios": "^0.28.0",
|
||||||
"axios-progress-bar": "^1.2.0",
|
"axios-progress-bar": "^1.2.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^4.6.0",
|
||||||
"bootstrap-vue": "^2.23.1",
|
"bootstrap-vue": "^2.23.1",
|
||||||
"chai": "^5.1.0",
|
|
||||||
"core-js": "^3.33.1",
|
"core-js": "^3.33.1",
|
||||||
"dompurify": "^3.0.3",
|
"dompurify": "^3.0.3",
|
||||||
"eslint": "7.32.0",
|
"eslint": "7.32.0",
|
||||||
|
|
@ -32,16 +32,18 @@
|
||||||
"eslint-plugin-vue": "7.20.0",
|
"eslint-plugin-vue": "7.20.0",
|
||||||
"habitica-markdown": "^3.0.0",
|
"habitica-markdown": "^3.0.0",
|
||||||
"hellojs": "^1.20.0",
|
"hellojs": "^1.20.0",
|
||||||
"inspectpack": "^4.7.1",
|
|
||||||
"intro.js": "^7.2.0",
|
"intro.js": "^7.2.0",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
|
"moment-locales-webpack-plugin": "^1.2.0",
|
||||||
"nconf": "^0.12.1",
|
"nconf": "^0.12.1",
|
||||||
"sass": "^1.63.4",
|
"sass": "^1.63.4",
|
||||||
"sass-loader": "^14.1.1",
|
"sass-loader": "^14.1.1",
|
||||||
|
"sinon": "^17.0.1",
|
||||||
"smartbanner.js": "^1.19.3",
|
"smartbanner.js": "^1.19.3",
|
||||||
"stopword": "^2.0.8",
|
"stopword": "^2.0.8",
|
||||||
|
"timers-browserify": "^2.0.12",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"validator": "^13.9.0",
|
"validator": "^13.9.0",
|
||||||
"vue": "^2.7.10",
|
"vue": "^2.7.10",
|
||||||
|
|
@ -51,10 +53,14 @@
|
||||||
"vue-template-babel-compiler": "^2.0.0",
|
"vue-template-babel-compiler": "^2.0.0",
|
||||||
"vue-template-compiler": "^2.7.10",
|
"vue-template-compiler": "^2.7.10",
|
||||||
"vuedraggable": "^2.24.3",
|
"vuedraggable": "^2.24.3",
|
||||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
|
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0"
|
||||||
"webpack": "^5.89.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.21.0"
|
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
|
||||||
|
"babel-plugin-lodash": "^3.3.4",
|
||||||
|
"chai": "^5.1.0",
|
||||||
|
"inspectpack": "^4.7.1",
|
||||||
|
"terser-webpack-plugin": "^5.3.10",
|
||||||
|
"webpack": "^5.89.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
BIN
website/client/public/static/npc/normal/customizations_npc.png
Normal file
BIN
website/client/public/static/npc/normal/customizations_npc.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
File diff suppressed because it is too large
Load diff
|
|
@ -217,16 +217,13 @@
|
||||||
|
|
||||||
.btn-show-more {
|
.btn-show-more {
|
||||||
display: block;
|
display: block;
|
||||||
width: 50%;
|
width: 100%;
|
||||||
max-width: 448px;
|
|
||||||
margin: 0 auto;
|
|
||||||
margin-top: 12px;
|
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.43;
|
line-height: 1.43;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: $gray-600;
|
background: $gray-500;
|
||||||
color: $gray-200 !important; // Otherwise it gets ignored when used on an A element
|
color: $gray-200 !important; // Otherwise it gets ignored when used on an A element
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.color {
|
&.color {
|
||||||
svg path {
|
svg path, svg polygon {
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
// TODO move to item component?
|
// TODO move to item component?
|
||||||
|
|
||||||
|
.item, .item-wrapper, .item > div > div {
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.items > div {
|
.items > div {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 24px;
|
margin-right: 24px;
|
||||||
|
|
@ -9,34 +15,22 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid $purple-400;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.items-one-line .item-wrapper {
|
.items-one-line .item-wrapper {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item.pet-slot {
|
|
||||||
// Desktop XL (1440)
|
|
||||||
@media only screen and (min-width: 1440px){
|
|
||||||
margin-right: 1.71em;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Desktop L (1280)
|
|
||||||
@media only screen and (min-width: 1280px) and (max-width: 1439px) {
|
|
||||||
margin-right: 0.43em;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Desktop M (1024)
|
|
||||||
@media only screen and (min-width: 1024px) and (max-width: 1279px) {
|
|
||||||
margin-right: 0.86em;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tablets and mobile
|
|
||||||
@media only screen and (max-width: 1023px) {
|
|
||||||
margin-right: 1.71em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 94px;
|
width: 94px;
|
||||||
|
|
@ -56,11 +50,6 @@
|
||||||
background: $purple-500;
|
background: $purple-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
|
||||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
|
||||||
border-color: $purple-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.highlight {
|
&.highlight {
|
||||||
box-shadow: 0 0 8px 8px rgba($black, 0.16), 0 5px 10px 0 rgba($black, 0.12) !important;
|
box-shadow: 0 0 8px 8px rgba($black, 0.16), 0 5px 10px 0 rgba($black, 0.12) !important;
|
||||||
}
|
}
|
||||||
|
|
@ -70,9 +59,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.flat .item {
|
.flat {
|
||||||
box-shadow: none;
|
.item {
|
||||||
border: none;
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-wrapper:hover {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bordered-item .item {
|
.bordered-item .item {
|
||||||
|
|
|
||||||
90
website/client/src/assets/scss/shops.scss
Normal file
90
website/client/src/assets/scss/shops.scss
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
.featured-label {
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group {
|
||||||
|
display: inline-block;
|
||||||
|
width: 33%;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.items {
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: #edecee;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-wrapper {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items > div:not(:last-of-type) {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeTravelers {
|
||||||
|
.standard-page {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-pin:not(.pinned) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:hover .badge-pin {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
cursor: default;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featuredItems {
|
||||||
|
height: 192px;
|
||||||
|
|
||||||
|
.background {
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.background-open, .background-closed {
|
||||||
|
height: 216px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.npc {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 216px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
|
&.closed {
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -14px;
|
||||||
|
margin: 0;
|
||||||
|
left: 79px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -158,7 +158,6 @@ function collateItemData (self) {
|
||||||
if (
|
if (
|
||||||
// ignore items the user owns because we captured them above:
|
// ignore items the user owns because we captured them above:
|
||||||
!(key in ownedItems)
|
!(key in ownedItems)
|
||||||
&& allItems[key].price > 0
|
|
||||||
) {
|
) {
|
||||||
const item = allItems[key];
|
const item = allItems[key];
|
||||||
itemData.push({
|
itemData.push({
|
||||||
|
|
|
||||||
|
|
@ -282,20 +282,16 @@ export default {
|
||||||
item.modified = true;
|
item.modified = true;
|
||||||
|
|
||||||
// for non-integer items, toggle through the allowed values:
|
// for non-integer items, toggle through the allowed values:
|
||||||
if (item.itemType === 'gear') {
|
if (item.itemType === 'gear' || item.itemType === 'mounts') {
|
||||||
// Allowed starting values are true, false, and '' (never owned)
|
// Allowed starting values are true, false, and undefined (never owned)
|
||||||
// Allowed values to switch to are true and false
|
if (item.value && item.value !== '') {
|
||||||
item.value = !item.value;
|
item.value = false;
|
||||||
} else if (item.itemType === 'mounts') {
|
} else if (typeof item.value === 'boolean') {
|
||||||
// Allowed starting values are true, null, and "never owned"
|
item.value = '';
|
||||||
// Allowed values to switch to are true and null
|
|
||||||
if (item.value === true) {
|
|
||||||
item.value = null;
|
|
||||||
} else {
|
} else {
|
||||||
item.value = true;
|
item.value = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// @TODO add a delete option
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,15 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>About</label>
|
<label>About</label>
|
||||||
<div class="row about-row">
|
<div class="row about-row">
|
||||||
<textarea
|
<textarea
|
||||||
v-model="hero.profile.blurb"
|
v-model="hero.profile.blurb"
|
||||||
class="form-control col"
|
class="form-control col"
|
||||||
rows="10"
|
rows="10"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div
|
<div
|
||||||
v-markdown="hero.profile.blurb"
|
v-markdown="hero.profile.blurb"
|
||||||
class="markdownPreview col"
|
class="markdownPreview col"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,44 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!IS_PRODUCTION && isUserLoaded"
|
v-if="TIME_TRAVEL_ENABLED && user.permissions && user.permissions.fullAccess"
|
||||||
|
:key="lastTimeJump"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="btn btn-secondary mr-1"
|
||||||
|
@click="jumpTime(-1)"
|
||||||
|
>-1 Day</a>
|
||||||
|
<a
|
||||||
|
class="btn btn-secondary mr-1"
|
||||||
|
@click="jumpTime(-7)"
|
||||||
|
>-7 Days</a>
|
||||||
|
<a
|
||||||
|
class="btn btn-secondary mr-1"
|
||||||
|
@click="jumpTime(-30)"
|
||||||
|
>-30 Days</a>
|
||||||
|
<div class="my-2">
|
||||||
|
Time Traveling! It is {{ new Date().toLocaleDateString() }}
|
||||||
|
<a
|
||||||
|
class="btn btn-warning mr-1"
|
||||||
|
@click="resetTime()"
|
||||||
|
>Reset</a>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
class="btn btn-secondary mr-1"
|
||||||
|
@click="jumpTime(1)"
|
||||||
|
>+1 Day</a>
|
||||||
|
<a
|
||||||
|
class="btn btn-secondary mr-1"
|
||||||
|
@click="jumpTime(7)"
|
||||||
|
>+7 Days</a>
|
||||||
|
<a
|
||||||
|
class="btn btn-secondary mr-1"
|
||||||
|
@click="jumpTime(30)"
|
||||||
|
>+30 Days</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="DEBUG_ENABLED && isUserLoaded"
|
||||||
class="debug-toggle"
|
class="debug-toggle"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
|
@ -772,6 +809,7 @@ h3 {
|
||||||
// modules
|
// modules
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
// images
|
// images
|
||||||
import melior from '@/assets/svg/melior.svg';
|
import melior from '@/assets/svg/melior.svg';
|
||||||
|
|
@ -785,13 +823,24 @@ import heart from '@/assets/svg/heart.svg';
|
||||||
import { mapState } from '@/libs/store';
|
import { mapState } from '@/libs/store';
|
||||||
import buyGemsModal from './payments/buyGemsModal.vue';
|
import buyGemsModal from './payments/buyGemsModal.vue';
|
||||||
import reportBug from '@/mixins/reportBug.js';
|
import reportBug from '@/mixins/reportBug.js';
|
||||||
|
import { worldStateMixin } from '@/mixins/worldState';
|
||||||
|
|
||||||
|
const DEBUG_ENABLED = process.env.DEBUG_ENABLED === 'true'; // eslint-disable-line no-process-env
|
||||||
|
const TIME_TRAVEL_ENABLED = process.env.TIME_TRAVEL_ENABLED === 'true'; // eslint-disable-line no-process-env
|
||||||
|
let sinon;
|
||||||
|
if (TIME_TRAVEL_ENABLED) {
|
||||||
|
// eslint-disable-next-line global-require
|
||||||
|
sinon = await import('sinon');
|
||||||
|
}
|
||||||
|
|
||||||
const IS_PRODUCTION = process.env.NODE_ENV === 'production'; // eslint-disable-line no-process-env
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
buyGemsModal,
|
buyGemsModal,
|
||||||
},
|
},
|
||||||
mixins: [reportBug],
|
mixins: [
|
||||||
|
reportBug,
|
||||||
|
worldStateMixin,
|
||||||
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
icons: Object.freeze({
|
icons: Object.freeze({
|
||||||
|
|
@ -803,7 +852,9 @@ export default {
|
||||||
heart,
|
heart,
|
||||||
}),
|
}),
|
||||||
debugMenuShown: false,
|
debugMenuShown: false,
|
||||||
IS_PRODUCTION,
|
DEBUG_ENABLED,
|
||||||
|
TIME_TRAVEL_ENABLED,
|
||||||
|
lastTimeJump: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -865,6 +916,27 @@ export default {
|
||||||
'stats.mp': this.user.stats.mp + 10000,
|
'stats.mp': this.user.stats.mp + 10000,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async jumpTime (amount) {
|
||||||
|
const response = await axios.post('/api/v4/debug/jump-time', { offsetDays: amount });
|
||||||
|
if (amount > 0) {
|
||||||
|
Vue.config.clock.jump(amount * 24 * 60 * 60 * 1000);
|
||||||
|
} else {
|
||||||
|
Vue.config.clock.setSystemTime(moment().add(amount, 'days').toDate());
|
||||||
|
}
|
||||||
|
this.lastTimeJump = response.data.data.time;
|
||||||
|
this.triggerGetWorldState(true);
|
||||||
|
},
|
||||||
|
async resetTime () {
|
||||||
|
const response = await axios.post('/api/v4/debug/jump-time', { reset: true });
|
||||||
|
const time = new Date(response.data.data.time);
|
||||||
|
Vue.config.clock.restore();
|
||||||
|
Vue.config.clock = sinon.useFakeTimers({
|
||||||
|
now: time,
|
||||||
|
shouldAdvanceTime: true,
|
||||||
|
});
|
||||||
|
this.lastTimeJump = response.data.data.time;
|
||||||
|
this.triggerGetWorldState(true);
|
||||||
|
},
|
||||||
addExp () {
|
addExp () {
|
||||||
// @TODO: Name these variables better
|
// @TODO: Name these variables better
|
||||||
let exp = 0;
|
let exp = 0;
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
<span :class="[skinClass, specialMountClass]"></span>
|
<span :class="[skinClass, specialMountClass]"></span>
|
||||||
<!-- eslint-disable max-len-->
|
<!-- eslint-disable max-len-->
|
||||||
<span
|
<span
|
||||||
:class="[member.preferences.size + '_shirt_' + member.preferences.shirt, specialMountClass]"
|
:class="[shirtClass, specialMountClass]"
|
||||||
></span>
|
></span>
|
||||||
<!-- eslint-enable max-len-->
|
<!-- eslint-enable max-len-->
|
||||||
<span :class="['head_0', specialMountClass]"></span>
|
<span :class="['head_0', specialMountClass]"></span>
|
||||||
|
|
@ -46,12 +46,10 @@
|
||||||
<template
|
<template
|
||||||
v-for="type in ['bangs', 'base', 'mustache', 'beard']"
|
v-for="type in ['bangs', 'base', 'mustache', 'beard']"
|
||||||
>
|
>
|
||||||
<!-- eslint-disable max-len-->
|
|
||||||
<span
|
<span
|
||||||
:key="type"
|
:key="type"
|
||||||
:class="['hair_' + type + '_' + member.preferences.hair[type] + '_' + member.preferences.hair.color, specialMountClass]"
|
:class="[hairClass(type), specialMountClass]"
|
||||||
></span>
|
></span>
|
||||||
<!-- eslint-enable max-len-->
|
|
||||||
</template>
|
</template>
|
||||||
<span :class="[getGearClass('body'), specialMountClass]"></span>
|
<span :class="[getGearClass('body'), specialMountClass]"></span>
|
||||||
<span :class="[getGearClass('eyewear'), specialMountClass]"></span>
|
<span :class="[getGearClass('eyewear'), specialMountClass]"></span>
|
||||||
|
|
@ -233,10 +231,20 @@ export default {
|
||||||
},
|
},
|
||||||
skinClass () {
|
skinClass () {
|
||||||
if (!this.member) return '';
|
if (!this.member) return '';
|
||||||
|
if (this.overrideAvatarGear?.skin) {
|
||||||
|
return `skin_${this.overrideAvatarGear.skin}`;
|
||||||
|
}
|
||||||
const baseClass = `skin_${this.member.preferences.skin}`;
|
const baseClass = `skin_${this.member.preferences.skin}`;
|
||||||
|
|
||||||
return `${baseClass}${this.member.preferences.sleep ? '_sleep' : ''}`;
|
return `${baseClass}${this.member.preferences.sleep ? '_sleep' : ''}`;
|
||||||
},
|
},
|
||||||
|
shirtClass () {
|
||||||
|
if (!this.member) return '';
|
||||||
|
if (this.overrideAvatarGear?.shirt) {
|
||||||
|
return `${this.member.preferences.size}_shirt_${this.overrideAvatarGear.shirt}`;
|
||||||
|
}
|
||||||
|
return `${this.member.preferences.size}_shirt_${this.member.preferences.shirt}`;
|
||||||
|
},
|
||||||
costumeClass () {
|
costumeClass () {
|
||||||
return this.member?.preferences.costume ? 'costume' : 'equipped';
|
return this.member?.preferences.costume ? 'costume' : 'equipped';
|
||||||
},
|
},
|
||||||
|
|
@ -269,6 +277,17 @@ export default {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
hairClass (slot) {
|
||||||
|
if (this.overrideAvatarGear?.hair) {
|
||||||
|
if (this.overrideAvatarGear.hair[slot]) {
|
||||||
|
return `hair_${slot}_${this.overrideAvatarGear.hair[slot]}_${this.member.preferences.hair.color}`;
|
||||||
|
}
|
||||||
|
if (this.overrideAvatarGear.hair.color) {
|
||||||
|
return `hair_${slot}_${this.member.preferences.hair[slot]}_${this.overrideAvatarGear.hair.color}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `hair_${slot}_${this.member.preferences.hair[slot]}_${this.member.preferences.hair.color}`;
|
||||||
|
},
|
||||||
hideGear (gearType) {
|
hideGear (gearType) {
|
||||||
if (!this.member) return true;
|
if (!this.member) return true;
|
||||||
if (gearType === 'weapon') {
|
if (gearType === 'weapon') {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
id="body"
|
id="body"
|
||||||
class="section customize-section"
|
class="customize-section d-flex flex-column"
|
||||||
|
:class="{ 'justify-content-between': editing }"
|
||||||
>
|
>
|
||||||
<sub-menu
|
<sub-menu
|
||||||
class="text-center"
|
class="text-center"
|
||||||
|
|
@ -17,17 +18,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-if="activeSubPage === 'shirt'">
|
<div v-if="activeSubPage === 'shirt'">
|
||||||
<customize-options
|
<customize-options
|
||||||
:items="freeShirts"
|
:items="userShirts"
|
||||||
:current-value="user.preferences.shirt"
|
:current-value="user.preferences.shirt"
|
||||||
/>
|
/>
|
||||||
<customize-options
|
|
||||||
v-if="editing"
|
|
||||||
:items="specialShirts"
|
|
||||||
:current-value="user.preferences.shirt"
|
|
||||||
:full-set="!userOwnsSet('shirt', specialShirtKeys)"
|
|
||||||
@unlock="unlock(`shirt.${specialShirtKeys.join(',shirt.')}`)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<customize-banner v-if="editing" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -35,33 +30,27 @@
|
||||||
import appearance from '@/../../common/script/content/appearance';
|
import appearance from '@/../../common/script/content/appearance';
|
||||||
import { subPageMixin } from '../../mixins/subPage';
|
import { subPageMixin } from '../../mixins/subPage';
|
||||||
import { userStateMixin } from '../../mixins/userState';
|
import { userStateMixin } from '../../mixins/userState';
|
||||||
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
|
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||||
import subMenu from './sub-menu';
|
import customizeBanner from './customize-banner.vue';
|
||||||
import customizeOptions from './customize-options';
|
import customizeOptions from './customize-options';
|
||||||
import gem from '@/assets/svg/gem.svg';
|
import subMenu from './sub-menu';
|
||||||
|
|
||||||
const freeShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price === 0);
|
|
||||||
const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
subMenu,
|
subMenu,
|
||||||
|
customizeBanner,
|
||||||
customizeOptions,
|
customizeOptions,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [
|
||||||
subPageMixin,
|
subPageMixin,
|
||||||
userStateMixin,
|
userStateMixin,
|
||||||
avatarEditorUtilies,
|
avatarEditorUtilities,
|
||||||
],
|
],
|
||||||
props: [
|
props: [
|
||||||
'editing',
|
'editing',
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
specialShirtKeys,
|
|
||||||
icons: Object.freeze({
|
|
||||||
gem,
|
|
||||||
}),
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'size',
|
id: 'size',
|
||||||
|
|
@ -78,25 +67,19 @@ export default {
|
||||||
sizes () {
|
sizes () {
|
||||||
return ['slim', 'broad'].map(s => this.mapKeysToFreeOption(s, 'size'));
|
return ['slim', 'broad'].map(s => this.mapKeysToFreeOption(s, 'size'));
|
||||||
},
|
},
|
||||||
freeShirts () {
|
userShirts () {
|
||||||
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
const freeShirts = Object.keys(appearance.shirt)
|
||||||
},
|
.filter(k => appearance.shirt[k].price === 0)
|
||||||
specialShirts () {
|
.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
||||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
const ownedShirts = Object.keys(this.user.purchased.shirt)
|
||||||
const keys = this.specialShirtKeys;
|
.filter(k => this.user.purchased.shirt[k])
|
||||||
const options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
|
.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
||||||
return options;
|
|
||||||
|
return [...freeShirts, ...ownedShirts];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.changeSubPage('size');
|
this.changeSubPage('size');
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
<template>
|
||||||
|
<div class="bottom-banner">
|
||||||
|
<div class="d-flex justify-content-center align-items-center mt-3">
|
||||||
|
<span
|
||||||
|
class="svg svg-icon sparkles"
|
||||||
|
v-html="icons.sparkles"
|
||||||
|
></span>
|
||||||
|
<strong
|
||||||
|
v-once
|
||||||
|
class="mx-2"
|
||||||
|
> {{ $t('lookingForMore') }}
|
||||||
|
</strong>
|
||||||
|
<span
|
||||||
|
v-once
|
||||||
|
class="svg svg-icon sparkles mirror"
|
||||||
|
v-html="icons.sparkles"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="check-link"
|
||||||
|
>
|
||||||
|
<span>Check out the </span>
|
||||||
|
<a href="/shops/customizations">Customizations Shop</a>
|
||||||
|
<span> for even more ways to customize your avatar!</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~@/assets/scss/colors.scss';
|
||||||
|
|
||||||
|
.bottom-banner {
|
||||||
|
background: linear-gradient(114.26deg, $purple-300 0%, $purple-200 100%);
|
||||||
|
border-bottom-left-radius: 8px;
|
||||||
|
border-bottom-right-radius: 8px;
|
||||||
|
color: $white;
|
||||||
|
height: 80px;
|
||||||
|
line-height: 24px;
|
||||||
|
|
||||||
|
.check-link, a {
|
||||||
|
color: $purple-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sparkles {
|
||||||
|
width: 32px;
|
||||||
|
|
||||||
|
&.mirror {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import sparkles from '@/assets/svg/sparkles-left.svg';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
icons: Object.freeze({
|
||||||
|
sparkles,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="customize-options"
|
v-if="items.length > 1"
|
||||||
:class="{'background-set': fullSet}"
|
class="customize-options mb-4"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="option in items"
|
v-for="option in items"
|
||||||
:key="option.key"
|
:key="option.key"
|
||||||
class="outer-option-background"
|
class="outer-option-background"
|
||||||
:class="{
|
:class="{
|
||||||
locked: option.gemLocked || option.goldLocked,
|
|
||||||
premium: Boolean(option.gem),
|
premium: Boolean(option.gem),
|
||||||
active: option.active || currentValue === option.key,
|
active: option.active || currentValue === option.key,
|
||||||
none: option.none,
|
none: option.none,
|
||||||
|
|
@ -28,38 +27,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-if="option.gemLocked"
|
|
||||||
class="gem-lock"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="svg-icon gem"
|
|
||||||
v-html="icons.gem"
|
|
||||||
></div>
|
|
||||||
<span>{{ option.gem }}</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="option.goldLocked"
|
|
||||||
class="gold-lock"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="svg-icon gold"
|
|
||||||
v-html="icons.gold"
|
|
||||||
></div>
|
|
||||||
<span>{{ option.gold }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="fullSet"
|
|
||||||
class="purchase-set"
|
|
||||||
@click="unlock()"
|
|
||||||
>
|
|
||||||
<span class="label">{{ $t('purchaseAll') }}</span>
|
|
||||||
<div
|
|
||||||
class="svg-icon gem"
|
|
||||||
v-html="icons.gem"
|
|
||||||
></div>
|
|
||||||
<span class="price">5</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -67,13 +34,13 @@
|
||||||
<script>
|
<script>
|
||||||
import gem from '@/assets/svg/gem.svg';
|
import gem from '@/assets/svg/gem.svg';
|
||||||
import gold from '@/assets/svg/gold.svg';
|
import gold from '@/assets/svg/gold.svg';
|
||||||
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
|
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [
|
mixins: [
|
||||||
avatarEditorUtilies,
|
avatarEditorUtilities,
|
||||||
],
|
],
|
||||||
props: ['items', 'currentValue', 'fullSet'],
|
props: ['items', 'currentValue'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
icons: Object.freeze({
|
icons: Object.freeze({
|
||||||
|
|
@ -150,7 +117,7 @@ export default {
|
||||||
|
|
||||||
&:not(.locked):not(.active) {
|
&:not(.locked):not(.active) {
|
||||||
.option:hover {
|
.option:hover {
|
||||||
background-color: rgba(213, 200, 255, .32);
|
background-color: rgba($purple-300, .25);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,9 +183,6 @@ export default {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
|
||||||
&.color-bangs {
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
&.skin {
|
&.skin {
|
||||||
margin-top: -4px;
|
margin-top: -4px;
|
||||||
margin-left: -4px;
|
margin-left: -4px;
|
||||||
|
|
@ -237,14 +201,14 @@ export default {
|
||||||
margin-top: -5px;
|
margin-top: -5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.color, &.bangs {
|
&.color, &.bangs, &.beard, &.flower, &.mustache {
|
||||||
margin-top: 4px;
|
background-position-x: -6px;
|
||||||
margin-left: -3px;
|
background-position-y: -12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.hair.base {
|
&.hair.base {
|
||||||
margin-top: 0px;
|
background-position-x: -6px;
|
||||||
margin-left: -5px;
|
background-position-y: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.headAccessory {
|
&.headAccessory {
|
||||||
|
|
@ -258,89 +222,4 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-center {
|
|
||||||
.gem-lock, .gold-lock {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 auto 8px;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.gem-lock, .gold-lock {
|
|
||||||
.svg-icon {
|
|
||||||
width: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-left: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg-icon, span {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.gem-lock span {
|
|
||||||
color: $green-10
|
|
||||||
}
|
|
||||||
|
|
||||||
.purchase-set {
|
|
||||||
background: #fff;
|
|
||||||
padding: 0.5em;
|
|
||||||
border-radius: 0 0 2px 2px;
|
|
||||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span.price {
|
|
||||||
color: #24cc8f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gem, .coin {
|
|
||||||
width: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.single {
|
|
||||||
width: 141px;
|
|
||||||
}
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gem, .coin {
|
|
||||||
width: 20px;
|
|
||||||
margin: 0 .5em;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.background-set {
|
|
||||||
background-color: #edecee;
|
|
||||||
border-radius: 2px;
|
|
||||||
|
|
||||||
padding-top: 12px;
|
|
||||||
margin-left: 12px;
|
|
||||||
margin-right: 12px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
width: calc(100% - 24px);
|
|
||||||
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
|
|
||||||
max-width: unset; // disable col12 styling
|
|
||||||
flex: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
id="extra"
|
id="extra"
|
||||||
class="section container customize-section"
|
class="customize-section d-flex flex-column"
|
||||||
|
:class="{ 'justify-content-between': !showEmptySection}"
|
||||||
>
|
>
|
||||||
<sub-menu
|
<sub-menu
|
||||||
class="text-center"
|
class="text-center"
|
||||||
|
|
@ -20,9 +21,8 @@
|
||||||
id="animal-ears"
|
id="animal-ears"
|
||||||
>
|
>
|
||||||
<customize-options
|
<customize-options
|
||||||
|
v-if="animalItems('back').length > 0"
|
||||||
:items="animalItems('headAccessory')"
|
:items="animalItems('headAccessory')"
|
||||||
:full-set="!animalItemsOwned('headAccessory')"
|
|
||||||
@unlock="unlock(animalItemsUnlockString('headAccessory'))"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -30,9 +30,8 @@
|
||||||
id="animal-tails"
|
id="animal-tails"
|
||||||
>
|
>
|
||||||
<customize-options
|
<customize-options
|
||||||
|
v-if="animalItems('back').length > 0"
|
||||||
:items="animalItems('back')"
|
:items="animalItems('back')"
|
||||||
:full-set="!animalItemsOwned('back')"
|
|
||||||
@unlock="unlock(animalItemsUnlockString('back'))"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -53,6 +52,24 @@
|
||||||
>
|
>
|
||||||
<customize-options :items="flowers" />
|
<customize-options :items="flowers" />
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="showEmptySection"
|
||||||
|
class="my-5"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
v-once
|
||||||
|
>
|
||||||
|
{{ $t('noItemsOwned') }}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
v-once
|
||||||
|
class="w-50 mx-auto"
|
||||||
|
v-html="$t('visitCustomizationsShop')"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
<customize-banner
|
||||||
|
v-else-if="editing"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -60,23 +77,24 @@
|
||||||
import appearance from '@/../../common/script/content/appearance';
|
import appearance from '@/../../common/script/content/appearance';
|
||||||
import { subPageMixin } from '../../mixins/subPage';
|
import { subPageMixin } from '../../mixins/subPage';
|
||||||
import { userStateMixin } from '../../mixins/userState';
|
import { userStateMixin } from '../../mixins/userState';
|
||||||
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
|
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||||
import subMenu from './sub-menu';
|
import customizeBanner from './customize-banner';
|
||||||
import customizeOptions from './customize-options';
|
import customizeOptions from './customize-options';
|
||||||
import gem from '@/assets/svg/gem.svg';
|
import subMenu from './sub-menu';
|
||||||
|
|
||||||
const freeShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price === 0);
|
const freeShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price === 0);
|
||||||
const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
|
const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
subMenu,
|
customizeBanner,
|
||||||
customizeOptions,
|
customizeOptions,
|
||||||
|
subMenu,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [
|
||||||
subPageMixin,
|
subPageMixin,
|
||||||
userStateMixin,
|
userStateMixin,
|
||||||
avatarEditorUtilies,
|
avatarEditorUtilities,
|
||||||
],
|
],
|
||||||
props: [
|
props: [
|
||||||
'editing',
|
'editing',
|
||||||
|
|
@ -89,9 +107,6 @@ export default {
|
||||||
},
|
},
|
||||||
chairKeys: ['none', 'black', 'blue', 'green', 'pink', 'red', 'yellow', 'handleless_black', 'handleless_blue', 'handleless_green', 'handleless_pink', 'handleless_red', 'handleless_yellow'],
|
chairKeys: ['none', 'black', 'blue', 'green', 'pink', 'red', 'yellow', 'handleless_black', 'handleless_blue', 'handleless_green', 'handleless_pink', 'handleless_red', 'handleless_yellow'],
|
||||||
specialShirtKeys,
|
specialShirtKeys,
|
||||||
icons: Object.freeze({
|
|
||||||
gem,
|
|
||||||
}),
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
id: 'size',
|
id: 'size',
|
||||||
|
|
@ -178,7 +193,7 @@ export default {
|
||||||
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
||||||
},
|
},
|
||||||
specialShirts () {
|
specialShirts () {
|
||||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||||
const keys = this.specialShirtKeys;
|
const keys = this.specialShirtKeys;
|
||||||
const options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
|
const options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
|
||||||
return options;
|
return options;
|
||||||
|
|
@ -193,6 +208,11 @@ export default {
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const option = this.createGearItem(key, 'headAccessory', 'special', 'headband');
|
const option = this.createGearItem(key, 'headAccessory', 'special', 'headband');
|
||||||
|
const newKey = `headAccessory_special_${key}`;
|
||||||
|
option.click = () => {
|
||||||
|
const type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||||
|
return this.equip(newKey, type);
|
||||||
|
};
|
||||||
|
|
||||||
options.push(option);
|
options.push(option);
|
||||||
}
|
}
|
||||||
|
|
@ -222,12 +242,22 @@ export default {
|
||||||
option.none = true;
|
option.none = true;
|
||||||
}
|
}
|
||||||
option.active = this.user.preferences.hair.flower === key;
|
option.active = this.user.preferences.hair.flower === key;
|
||||||
option.class = `hair_flower_${key} flower`;
|
option.class = `icon_hair_flower_${key} flower`;
|
||||||
option.click = () => this.set({ 'preferences.hair.flower': key });
|
option.click = () => this.set({ 'preferences.hair.flower': key });
|
||||||
return option;
|
return option;
|
||||||
});
|
});
|
||||||
return options;
|
return options;
|
||||||
},
|
},
|
||||||
|
showEmptySection () {
|
||||||
|
switch (this.activeSubPage) {
|
||||||
|
case 'ears':
|
||||||
|
return this.editing && this.animalItems('headAccessory').length === 1;
|
||||||
|
case 'tails':
|
||||||
|
return this.editing && this.animalItems('back').length === 1;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.changeSubPage(this.extraSubMenuItems[0].id);
|
this.changeSubPage(this.extraSubMenuItems[0].id);
|
||||||
|
|
@ -236,7 +266,7 @@ export default {
|
||||||
animalItems (category) {
|
animalItems (category) {
|
||||||
// @TODO: For some resonse when I use $set on the
|
// @TODO: For some resonse when I use $set on the
|
||||||
// user purchases object, this is not recomputed. Hack for now
|
// user purchases object, this is not recomputed. Hack for now
|
||||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||||
const keys = this.animalItemKeys[category];
|
const keys = this.animalItemKeys[category];
|
||||||
|
|
||||||
const noneOption = this.createGearItem(0, category, 'base', category);
|
const noneOption = this.createGearItem(0, category, 'base', category);
|
||||||
|
|
@ -248,36 +278,22 @@ export default {
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const newKey = `${category}_special_${key}`;
|
const newKey = `${category}_special_${key}`;
|
||||||
const userPurchased = this.user.items.gear.owned[newKey];
|
const userPurchased = this.user.items.gear.owned[newKey];
|
||||||
|
if (userPurchased) {
|
||||||
const option = {};
|
const option = {};
|
||||||
option.key = key;
|
option.key = key;
|
||||||
option.active = this.user.preferences.costume
|
option.active = this.user.preferences.costume
|
||||||
? this.user.items.gear.costume[category] === newKey
|
? this.user.items.gear.costume[category] === newKey
|
||||||
: this.user.items.gear.equipped[category] === newKey;
|
: this.user.items.gear.equipped[category] === newKey;
|
||||||
option.class = `headAccessory_special_${option.key} ${category}`;
|
option.class = `headAccessory_special_${option.key} ${category}`;
|
||||||
if (category === 'back') {
|
if (category === 'back') {
|
||||||
option.class = `icon_back_special_${option.key} back`;
|
option.class = `icon_back_special_${option.key} back`;
|
||||||
}
|
|
||||||
option.gemLocked = userPurchased === undefined;
|
|
||||||
option.goldLocked = userPurchased === false;
|
|
||||||
if (option.goldLocked) {
|
|
||||||
option.gold = 20;
|
|
||||||
}
|
|
||||||
if (option.gemLocked) {
|
|
||||||
option.gem = 2;
|
|
||||||
}
|
|
||||||
option.locked = option.gemLocked || option.goldLocked;
|
|
||||||
option.click = () => {
|
|
||||||
if (option.gemLocked) {
|
|
||||||
return this.unlock(`items.gear.owned.${newKey}`);
|
|
||||||
} if (option.goldLocked) {
|
|
||||||
return this.buy(newKey);
|
|
||||||
}
|
}
|
||||||
const type = this.user.preferences.costume ? 'costume' : 'equipped';
|
option.click = () => {
|
||||||
return this.equip(newKey, type);
|
const type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||||
};
|
return this.equip(newKey, type);
|
||||||
|
};
|
||||||
options.push(option);
|
options.push(option);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
|
|
@ -287,17 +303,6 @@ export default {
|
||||||
|
|
||||||
return keys.join(',');
|
return keys.join(',');
|
||||||
},
|
},
|
||||||
animalItemsOwned (category) {
|
|
||||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
|
||||||
// this is not recomputed. Hack for now
|
|
||||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
|
||||||
|
|
||||||
let own = true;
|
|
||||||
this.animalItemKeys[category].forEach(key => {
|
|
||||||
if (this.user.items.gear.owned[`${category}_special_${key}`] === undefined) own = false;
|
|
||||||
});
|
|
||||||
return own;
|
|
||||||
},
|
|
||||||
createGearItem (key, gearType, subGearType, additionalClass) {
|
createGearItem (key, gearType, subGearType, additionalClass) {
|
||||||
const newKey = `${gearType}_${subGearType ? `${subGearType}_` : ''}${key}`;
|
const newKey = `${gearType}_${subGearType ? `${subGearType}_` : ''}${key}`;
|
||||||
const option = {};
|
const option = {};
|
||||||
|
|
@ -339,7 +344,3 @@ export default {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
id="hair"
|
id="hair"
|
||||||
class="section customize-section"
|
class="customize-section d-flex flex-column"
|
||||||
|
:class="{ 'justify-content-between': editing && !showEmptySection}"
|
||||||
>
|
>
|
||||||
<sub-menu
|
<sub-menu
|
||||||
class="text-center"
|
class="text-center"
|
||||||
|
|
@ -14,37 +15,9 @@
|
||||||
id="hair-color"
|
id="hair-color"
|
||||||
>
|
>
|
||||||
<customize-options
|
<customize-options
|
||||||
:items="freeHairColors"
|
:items="userHairColors"
|
||||||
:current-value="user.preferences.hair.color"
|
:current-value="user.preferences.hair.color"
|
||||||
/>
|
/>
|
||||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
|
||||||
<div
|
|
||||||
v-for="set in seasonalHairColors"
|
|
||||||
v-if="editing && set.key !== 'undefined'"
|
|
||||||
:key="set.key"
|
|
||||||
>
|
|
||||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
|
||||||
<customize-options
|
|
||||||
:items="set.options"
|
|
||||||
:current-value="user.preferences.hair.color"
|
|
||||||
:full-set="!hideSet(set.key) && !userOwnsSet('hair', set.keys, 'color')"
|
|
||||||
@unlock="unlock(`hair.color.${set.keys.join(',hair.color.')}`)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="activeSubPage === 'style'"
|
|
||||||
id="style"
|
|
||||||
>
|
|
||||||
<!-- eslint-disable vue/require-v-for-key NO KEY AVAILABLE HERE -->
|
|
||||||
<div v-for="set in styleSets">
|
|
||||||
<customize-options
|
|
||||||
:items="set.options"
|
|
||||||
:full-set="set.fullSet"
|
|
||||||
@unlock="set.unlock()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<!-- eslint-enable vue/require-v-for-key -->
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="activeSubPage === 'bangs'"
|
v-if="activeSubPage === 'bangs'"
|
||||||
|
|
@ -55,67 +28,73 @@
|
||||||
:current-value="user.preferences.hair.bangs"
|
:current-value="user.preferences.hair.bangs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="activeSubPage === 'style'"
|
||||||
|
id="style"
|
||||||
|
>
|
||||||
|
<customize-options
|
||||||
|
:items="userHairStyles"
|
||||||
|
:current-value="user.preferences.hair.base"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="activeSubPage === 'facialhair'"
|
v-if="activeSubPage === 'facialhair'"
|
||||||
id="facialhair"
|
id="facialhair"
|
||||||
>
|
>
|
||||||
<customize-options
|
<customize-options
|
||||||
v-if="editing"
|
v-if="userMustaches.length > 1"
|
||||||
:items="mustacheList"
|
:items="userMustaches"
|
||||||
/>
|
/>
|
||||||
<!-- eslint-disable max-len -->
|
|
||||||
<customize-options
|
<customize-options
|
||||||
v-if="editing"
|
v-if="userBeards.length > 1"
|
||||||
:items="beardList"
|
:items="userBeards"
|
||||||
:full-set="isPurchaseAllNeeded('hair', ['baseHair5', 'baseHair6'], ['mustache', 'beard'])"
|
|
||||||
@unlock="unlock(`hair.mustache.${baseHair5Keys.join(',hair.mustache.')},hair.beard.${baseHair6Keys.join(',hair.beard.')}`)"
|
|
||||||
/>
|
/>
|
||||||
<!-- eslint-enable max-len -->
|
<div
|
||||||
|
v-if="showEmptySection"
|
||||||
|
class="my-5"
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
v-once
|
||||||
|
>
|
||||||
|
{{ $t('noItemsOwned') }}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
v-once
|
||||||
|
class="w-50 mx-auto"
|
||||||
|
v-html="$t('visitCustomizationsShop')"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<customize-banner
|
||||||
|
v-if="editing && !showEmptySection"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import groupBy from 'lodash/groupBy';
|
import groupBy from 'lodash/groupBy';
|
||||||
import appearance from '@/../../common/script/content/appearance';
|
import appearance from '@/../../common/script/content/appearance';
|
||||||
import appearanceSets from '@/../../common/script/content/appearance/sets';
|
|
||||||
import { subPageMixin } from '../../mixins/subPage';
|
import { subPageMixin } from '../../mixins/subPage';
|
||||||
import { userStateMixin } from '../../mixins/userState';
|
import { userStateMixin } from '../../mixins/userState';
|
||||||
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
|
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||||
import subMenu from './sub-menu';
|
import customizeBanner from './customize-banner';
|
||||||
import customizeOptions from './customize-options';
|
import customizeOptions from './customize-options';
|
||||||
import gem from '@/assets/svg/gem.svg';
|
import subMenu from './sub-menu';
|
||||||
|
|
||||||
const hairColorBySet = groupBy(appearance.hair.color, 'set.key');
|
|
||||||
const freeHairColorKeys = hairColorBySet[undefined].map(s => s.key);
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
subMenu,
|
customizeBanner,
|
||||||
customizeOptions,
|
customizeOptions,
|
||||||
|
subMenu,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [
|
||||||
subPageMixin,
|
subPageMixin,
|
||||||
userStateMixin,
|
userStateMixin,
|
||||||
avatarEditorUtilies,
|
avatarEditorUtilities,
|
||||||
],
|
],
|
||||||
props: [
|
props: [
|
||||||
'editing',
|
'editing',
|
||||||
],
|
],
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
freeHairColorKeys,
|
|
||||||
icons: Object.freeze({
|
|
||||||
gem,
|
|
||||||
}),
|
|
||||||
baseHair1: [1, 3],
|
|
||||||
baseHair2Keys: [2, 4, 5, 6, 7, 8],
|
|
||||||
baseHair3Keys: [9, 10, 11, 12, 13, 14],
|
|
||||||
baseHair4Keys: [15, 16, 17, 18, 19, 20],
|
|
||||||
baseHair5Keys: [1, 2],
|
|
||||||
baseHair6Keys: [1, 2, 3],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
hairSubMenuItems () {
|
hairSubMenuItems () {
|
||||||
const items = [
|
const items = [
|
||||||
|
|
@ -142,91 +121,46 @@ export default {
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
},
|
},
|
||||||
freeHairColors () {
|
userHairColors () {
|
||||||
return freeHairColorKeys.map(s => this.mapKeysToFreeOption(s, 'hair', 'color'));
|
const freeHairColors = groupBy(appearance.hair.color, 'set.key')[undefined]
|
||||||
|
.map(s => s.key).map(s => this.mapKeysToFreeOption(s, 'hair', 'color'));
|
||||||
|
const ownedHairColors = Object.keys(this.user.purchased.hair.color || {})
|
||||||
|
.filter(k => this.user.purchased.hair.color[k])
|
||||||
|
.map(h => this.mapKeysToFreeOption(h, 'hair', 'color'));
|
||||||
|
return [...freeHairColors, ...ownedHairColors];
|
||||||
},
|
},
|
||||||
seasonalHairColors () {
|
userHairStyles () {
|
||||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
const emptyHairStyle = {
|
||||||
// this is not recomputed. Hack for now
|
...this.mapKeysToFreeOption(0, 'hair', 'base'),
|
||||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
none: true,
|
||||||
|
};
|
||||||
|
const freeHairStyles = [1, 3].map(s => this.mapKeysToFreeOption(s, 'hair', 'base'));
|
||||||
|
const ownedHairStyles = Object.keys(this.user.purchased.hair.base || {})
|
||||||
|
.filter(k => this.user.purchased.hair.base[k])
|
||||||
|
.map(h => this.mapKeysToFreeOption(h, 'hair', 'base'));
|
||||||
|
return [emptyHairStyle, ...freeHairStyles, ...ownedHairStyles];
|
||||||
|
},
|
||||||
|
userMustaches () {
|
||||||
|
const emptyMustache = {
|
||||||
|
...this.mapKeysToFreeOption(0, 'hair', 'mustache'),
|
||||||
|
none: true,
|
||||||
|
};
|
||||||
|
const ownedMustaches = Object.keys(this.user.purchased.hair.mustache || {})
|
||||||
|
.filter(k => this.user.purchased.hair.mustache[k])
|
||||||
|
.map(h => this.mapKeysToFreeOption(h, 'hair', 'mustache'));
|
||||||
|
|
||||||
const seasonalHairColors = [];
|
return [emptyMustache, ...ownedMustaches];
|
||||||
for (const key of Object.keys(hairColorBySet)) {
|
},
|
||||||
const set = hairColorBySet[key];
|
userBeards () {
|
||||||
|
const emptyBeard = {
|
||||||
|
...this.mapKeysToFreeOption(0, 'hair', 'beard'),
|
||||||
|
none: true,
|
||||||
|
};
|
||||||
|
const ownedBeards = Object.keys(this.user.purchased.hair.beard || {})
|
||||||
|
.filter(k => this.user.purchased.hair.beard[k])
|
||||||
|
.map(h => this.mapKeysToFreeOption(h, 'hair', 'beard'));
|
||||||
|
|
||||||
const keys = set.map(item => item.key);
|
return [emptyBeard, ...ownedBeards];
|
||||||
|
|
||||||
const options = keys.map(optionKey => {
|
|
||||||
const option = this.mapKeysToOption(optionKey, 'hair', 'color', key);
|
|
||||||
return option;
|
|
||||||
});
|
|
||||||
|
|
||||||
let text = this.$t(key);
|
|
||||||
if (appearanceSets[key] && appearanceSets[key].text) {
|
|
||||||
text = appearanceSets[key].text();
|
|
||||||
}
|
|
||||||
|
|
||||||
const compiledSet = {
|
|
||||||
key,
|
|
||||||
options,
|
|
||||||
keys,
|
|
||||||
text,
|
|
||||||
};
|
|
||||||
seasonalHairColors.push(compiledSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
return seasonalHairColors;
|
|
||||||
},
|
|
||||||
premiumHairColors () {
|
|
||||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
|
||||||
// this is not recomputed. Hack for now
|
|
||||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
|
||||||
const keys = this.premiumHairColorKeys;
|
|
||||||
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'color'));
|
|
||||||
return options;
|
|
||||||
},
|
|
||||||
baseHair2 () {
|
|
||||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
|
||||||
// this is not recomputed. Hack for now
|
|
||||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
|
||||||
const keys = this.baseHair2Keys;
|
|
||||||
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'base'));
|
|
||||||
return options;
|
|
||||||
},
|
|
||||||
baseHair3 () {
|
|
||||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
|
||||||
// this is not recomputed. Hack for now
|
|
||||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
|
||||||
const keys = this.baseHair3Keys;
|
|
||||||
const options = keys.map(key => {
|
|
||||||
const option = this.mapKeysToOption(key, 'hair', 'base');
|
|
||||||
return option;
|
|
||||||
});
|
|
||||||
return options;
|
|
||||||
},
|
|
||||||
baseHair4 () {
|
|
||||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
|
||||||
// this is not recomputed. Hack for now
|
|
||||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
|
||||||
const keys = this.baseHair4Keys;
|
|
||||||
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'base'));
|
|
||||||
return options;
|
|
||||||
},
|
|
||||||
baseHair5 () {
|
|
||||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
|
||||||
// this is not recomputed. Hack for now
|
|
||||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
|
||||||
const keys = this.baseHair5Keys;
|
|
||||||
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'mustache'));
|
|
||||||
return options;
|
|
||||||
},
|
|
||||||
baseHair6 () {
|
|
||||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
|
||||||
// this is not recomputed. Hack for now
|
|
||||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
|
||||||
const keys = this.baseHair6Keys;
|
|
||||||
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'beard'));
|
|
||||||
return options;
|
|
||||||
},
|
},
|
||||||
hairBangs () {
|
hairBangs () {
|
||||||
const none = this.mapKeysToFreeOption(0, 'hair', 'bangs');
|
const none = this.mapKeysToFreeOption(0, 'hair', 'bangs');
|
||||||
|
|
@ -236,136 +170,13 @@ export default {
|
||||||
|
|
||||||
return [none, ...options];
|
return [none, ...options];
|
||||||
},
|
},
|
||||||
mustacheList () {
|
showEmptySection () {
|
||||||
const noneOption = this.mapKeysToFreeOption(0, 'hair', 'mustache');
|
return this.activeSubPage === 'facialhair'
|
||||||
noneOption.none = true;
|
&& this.userMustaches.length === 1 && this.userBeards.length === 1;
|
||||||
|
|
||||||
return [noneOption, ...this.baseHair5];
|
|
||||||
},
|
|
||||||
beardList () {
|
|
||||||
const noneOption = this.mapKeysToFreeOption(0, 'hair', 'beard');
|
|
||||||
noneOption.none = true;
|
|
||||||
|
|
||||||
return [noneOption, ...this.baseHair6];
|
|
||||||
},
|
|
||||||
styleSets () {
|
|
||||||
const sets = [];
|
|
||||||
|
|
||||||
const emptyHairBase = {
|
|
||||||
...this.mapKeysToFreeOption(0, 'hair', 'base'),
|
|
||||||
none: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
sets.push({
|
|
||||||
options: [
|
|
||||||
emptyHairBase,
|
|
||||||
...this.baseHair1.map(key => this.mapKeysToFreeOption(key, 'hair', 'base')),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.editing) {
|
|
||||||
sets.push({
|
|
||||||
fullSet: !this.userOwnsSet('hair', this.baseHair3Keys, 'base'),
|
|
||||||
unlock: () => this.unlock(`hair.base.${this.baseHair3Keys.join(',hair.base.')}`),
|
|
||||||
options: [
|
|
||||||
...this.baseHair3,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
sets.push({
|
|
||||||
fullSet: !this.userOwnsSet('hair', this.baseHair4Keys, 'base'),
|
|
||||||
unlock: () => this.unlock(`hair.base.${this.baseHair4Keys.join(',hair.base.')}`),
|
|
||||||
options: [
|
|
||||||
...this.baseHair4,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.editing) {
|
|
||||||
sets.push({
|
|
||||||
fullSet: !this.userOwnsSet('hair', this.baseHair2Keys, 'base'),
|
|
||||||
unlock: () => this.unlock(`hair.base.${this.baseHair2Keys.join(',hair.base.')}`),
|
|
||||||
options: [
|
|
||||||
...this.baseHair2,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return sets;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.changeSubPage('color');
|
this.changeSubPage('color');
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
/**
|
|
||||||
* Allows you to find out whether you need the "Purchase All" button or not.
|
|
||||||
* If there are more than 2 unpurchased items, returns true, otherwise returns false.
|
|
||||||
* @param {string} category - The selected category.
|
|
||||||
* @param {string[]} keySets - The items keySets.
|
|
||||||
* @param {string[]} [types] - The items types (subcategories). Optional.
|
|
||||||
* @returns {boolean} - Determines whether the "Purchase All" button
|
|
||||||
* is needed (true) or not (false).
|
|
||||||
*/
|
|
||||||
isPurchaseAllNeeded (category, keySets, types) {
|
|
||||||
const purchasedItemsLengths = [];
|
|
||||||
// If item types are specified, count them
|
|
||||||
if (types && types.length > 0) {
|
|
||||||
// Types can be undefined, so we must check them.
|
|
||||||
types.forEach(type => {
|
|
||||||
if (this.user.purchased[category][type]) {
|
|
||||||
purchasedItemsLengths
|
|
||||||
.push(Object.keys(this.user.purchased[category][type]).length);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
let purchasedItemsCounter = 0;
|
|
||||||
|
|
||||||
// If types are not specified, recursively
|
|
||||||
// search for purchased items in the category
|
|
||||||
const findPurchasedItems = item => {
|
|
||||||
if (typeof item === 'object') {
|
|
||||||
Object.values(item)
|
|
||||||
.forEach(innerItem => {
|
|
||||||
if (typeof innerItem === 'boolean' && innerItem === true) {
|
|
||||||
purchasedItemsCounter += 1;
|
|
||||||
}
|
|
||||||
return findPurchasedItems(innerItem);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return purchasedItemsCounter;
|
|
||||||
};
|
|
||||||
|
|
||||||
findPurchasedItems(this.user.purchased[category]);
|
|
||||||
if (purchasedItemsCounter > 0) {
|
|
||||||
purchasedItemsLengths.push(purchasedItemsCounter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't need to count the key sets (below)
|
|
||||||
// if there are no purchased items at all.
|
|
||||||
if (purchasedItemsLengths.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const allItemsLengths = [];
|
|
||||||
// Key sets must be specify correctly.
|
|
||||||
keySets.forEach(keySet => {
|
|
||||||
allItemsLengths.push(Object.keys(this[keySet]).length);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Simply sum all the length values and
|
|
||||||
// write them into variables for the convenience.
|
|
||||||
const allItems = allItemsLengths.reduce((acc, val) => acc + val);
|
|
||||||
const purchasedItems = purchasedItemsLengths.reduce((acc, val) => acc + val);
|
|
||||||
|
|
||||||
const unpurchasedItems = allItems - purchasedItems;
|
|
||||||
return unpurchasedItems > 2;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
id="skin"
|
id="skin"
|
||||||
class="section customize-section"
|
class="customize-section d-flex flex-column"
|
||||||
|
:class="{ 'justify-content-between': editing }"
|
||||||
>
|
>
|
||||||
<sub-menu
|
<sub-menu
|
||||||
class="text-center"
|
class="text-center"
|
||||||
|
|
@ -10,63 +11,39 @@
|
||||||
@changeSubPage="changeSubPage($event)"
|
@changeSubPage="changeSubPage($event)"
|
||||||
/>
|
/>
|
||||||
<customize-options
|
<customize-options
|
||||||
:items="freeSkins"
|
:items="userSkins"
|
||||||
:current-value="user.preferences.skin"
|
:current-value="user.preferences.skin"
|
||||||
/>
|
/>
|
||||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
<customize-banner v-if="editing" />
|
||||||
<div
|
|
||||||
v-for="set in seasonalSkins"
|
|
||||||
v-if="editing && set.key !== 'undefined'"
|
|
||||||
:key="set.key"
|
|
||||||
>
|
|
||||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
|
||||||
<customize-options
|
|
||||||
:items="set.options"
|
|
||||||
:current-value="user.preferences.skin"
|
|
||||||
:full-set="!hideSet(set.key) && !userOwnsSet('skin', set.keys)"
|
|
||||||
@unlock="unlock(`skin.${set.keys.join(',skin.')}`)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import groupBy from 'lodash/groupBy';
|
import groupBy from 'lodash/groupBy';
|
||||||
import appearance from '@/../../common/script/content/appearance';
|
import appearance from '@/../../common/script/content/appearance';
|
||||||
import appearanceSets from '@/../../common/script/content/appearance/sets';
|
|
||||||
import { subPageMixin } from '../../mixins/subPage';
|
import { subPageMixin } from '../../mixins/subPage';
|
||||||
import { userStateMixin } from '../../mixins/userState';
|
import { userStateMixin } from '../../mixins/userState';
|
||||||
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
|
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||||
import subMenu from './sub-menu';
|
import customizeBanner from './customize-banner.vue';
|
||||||
import customizeOptions from './customize-options';
|
import customizeOptions from './customize-options';
|
||||||
import gem from '@/assets/svg/gem.svg';
|
import subMenu from './sub-menu';
|
||||||
|
|
||||||
const skinsBySet = groupBy(appearance.skin, 'set.key');
|
|
||||||
|
|
||||||
const freeSkinKeys = skinsBySet[undefined].map(s => s.key);
|
|
||||||
|
|
||||||
// const specialSkinKeys = Object.keys(appearance.shirt)
|
|
||||||
// .filter(k => appearance.shirt[k].price !== 0);
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
subMenu,
|
subMenu,
|
||||||
|
customizeBanner,
|
||||||
customizeOptions,
|
customizeOptions,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [
|
||||||
subPageMixin,
|
subPageMixin,
|
||||||
userStateMixin,
|
userStateMixin,
|
||||||
avatarEditorUtilies,
|
avatarEditorUtilities,
|
||||||
],
|
],
|
||||||
props: [
|
props: [
|
||||||
'editing',
|
'editing',
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
freeSkinKeys,
|
|
||||||
icons: Object.freeze({
|
|
||||||
gem,
|
|
||||||
}),
|
|
||||||
skinSubMenuItems: [
|
skinSubMenuItems: [
|
||||||
{
|
{
|
||||||
id: 'color',
|
id: 'color',
|
||||||
|
|
@ -76,41 +53,13 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
freeSkins () {
|
userSkins () {
|
||||||
return freeSkinKeys.map(s => this.mapKeysToFreeOption(s, 'skin'));
|
const freeSkins = groupBy(appearance.skin, 'set.key')[undefined]
|
||||||
},
|
.map(s => s.key).map(s => this.mapKeysToFreeOption(s, 'skin'));
|
||||||
seasonalSkins () {
|
const ownedSkins = Object.keys(this.user.purchased.skin)
|
||||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
.filter(k => this.user.purchased.skin[k])
|
||||||
// this is not recomputed. Hack for now
|
.map(s => this.mapKeysToFreeOption(s, 'skin'));
|
||||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
return [...freeSkins, ...ownedSkins];
|
||||||
|
|
||||||
const seasonalSkins = [];
|
|
||||||
for (const setKey of Object.keys(skinsBySet)) {
|
|
||||||
const set = skinsBySet[setKey];
|
|
||||||
|
|
||||||
const keys = set.map(item => item.key);
|
|
||||||
|
|
||||||
const options = keys.map(optionKey => {
|
|
||||||
const option = this.mapKeysToOption(optionKey, 'skin', '', setKey);
|
|
||||||
|
|
||||||
return option;
|
|
||||||
});
|
|
||||||
|
|
||||||
let text = this.$t(setKey);
|
|
||||||
if (appearanceSets[setKey] && appearanceSets[setKey].text) {
|
|
||||||
text = appearanceSets[setKey].text();
|
|
||||||
}
|
|
||||||
|
|
||||||
const compiledSet = {
|
|
||||||
key: setKey,
|
|
||||||
options,
|
|
||||||
keys,
|
|
||||||
text,
|
|
||||||
};
|
|
||||||
seasonalSkins.push(compiledSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
return seasonalSkins;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -134,6 +134,12 @@
|
||||||
>
|
>
|
||||||
{{ $t('quests') }}
|
{{ $t('quests') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
class="topbar-dropdown-item dropdown-item"
|
||||||
|
:to="{name: 'customizations'}"
|
||||||
|
>
|
||||||
|
{{ $t('customizations') }}
|
||||||
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
class="topbar-dropdown-item dropdown-item"
|
class="topbar-dropdown-item dropdown-item"
|
||||||
:to="{name: 'seasonal'}"
|
:to="{name: 'seasonal'}"
|
||||||
|
|
|
||||||
|
|
@ -35,13 +35,9 @@
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
class="topbar-dropdown-item dropdown-item"
|
class="topbar-dropdown-item dropdown-item dropdown-separated"
|
||||||
@click="showAvatar('body', 'size')"
|
@click="showAvatar('body', 'size')"
|
||||||
>{{ $t('editAvatar') }}</a>
|
>{{ $t('editAvatar') }}</a>
|
||||||
<a
|
|
||||||
class="topbar-dropdown-item dropdown-item dropdown-separated"
|
|
||||||
@click="showAvatar('backgrounds', '2024')"
|
|
||||||
>{{ $t('backgrounds') }}</a>
|
|
||||||
<a
|
<a
|
||||||
class="topbar-dropdown-item dropdown-item"
|
class="topbar-dropdown-item dropdown-item"
|
||||||
@click="showProfile('profile')"
|
@click="showProfile('profile')"
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@
|
||||||
:right="true"
|
:right="true"
|
||||||
:hide-icon="false"
|
:hide-icon="false"
|
||||||
:inline-dropdown="false"
|
:inline-dropdown="false"
|
||||||
|
:direct-select="true"
|
||||||
@select="groupBy = $event"
|
@select="groupBy = $event"
|
||||||
>
|
>
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
|
|
|
||||||
|
|
@ -444,7 +444,7 @@ export default {
|
||||||
const isSearched = !searchText || item.text()
|
const isSearched = !searchText || item.text()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.indexOf(searchText) !== -1;
|
.indexOf(searchText) !== -1;
|
||||||
if (isSearched) {
|
if (isSearched && item) {
|
||||||
itemsArray.push({
|
itemsArray.push({
|
||||||
...item,
|
...item,
|
||||||
class: `${group.classPrefix}${item.key}`,
|
class: `${group.classPrefix}${item.key}`,
|
||||||
|
|
|
||||||
|
|
@ -134,56 +134,57 @@
|
||||||
v-for="(petGroup) in petGroups"
|
v-for="(petGroup) in petGroups"
|
||||||
v-if="!anyFilterSelected || viewOptions[petGroup.key].selected"
|
v-if="!anyFilterSelected || viewOptions[petGroup.key].selected"
|
||||||
:key="petGroup.key"
|
:key="petGroup.key"
|
||||||
|
:class="{ hide: viewOptions[petGroup.key].animalCount === 0 }"
|
||||||
>
|
>
|
||||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||||
<h4 v-if="viewOptions[petGroup.key].animalCount !== 0">
|
<h4 v-if="viewOptions[petGroup.key].animalCount !== 0">
|
||||||
{{ petGroup.label }}
|
{{ petGroup.label }}
|
||||||
</h4>
|
</h4>
|
||||||
<!-- eslint-disable vue/no-use-v-if-with-v-for, max-len -->
|
<!-- eslint-disable vue/no-use-v-if-with-v-for, max-len -->
|
||||||
<div
|
<div class="d-inline-flex flex-column">
|
||||||
v-for="(group, key, index) in pets(petGroup, hideMissing, selectedSortBy, searchTextThrottled)"
|
|
||||||
v-if="index === 0 || $_openedItemRows_isToggled(petGroup.key)"
|
|
||||||
:key="key"
|
|
||||||
class="pet-row d-flex"
|
|
||||||
>
|
|
||||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
|
||||||
<div
|
<div
|
||||||
v-for="item in group"
|
v-for="(group, key, index) in pets(petGroup, hideMissing, selectedSortBy, searchTextThrottled)"
|
||||||
v-show="show('pet', item)"
|
v-if="index === 0 || $_openedItemRows_isToggled(petGroup.key)"
|
||||||
:key="item.key"
|
:key="key"
|
||||||
v-drag.drop.food="item.key"
|
class="pet-row d-flex"
|
||||||
class="pet-group"
|
|
||||||
:class="{'last': item.isLastInRow}"
|
|
||||||
@itemDragOver="onDragOver($event, item)"
|
|
||||||
@itemDropped="onDrop($event, item)"
|
|
||||||
@itemDragLeave="onDragLeave()"
|
|
||||||
>
|
>
|
||||||
<petItem
|
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||||
:item="item"
|
<div
|
||||||
:popover-position="'top'"
|
v-for="item in group"
|
||||||
:show-popover="currentDraggingFood == null"
|
v-show="show('pet', item)"
|
||||||
:highlight-border="highlightPet == item.key"
|
:key="item.key"
|
||||||
@click="petClicked(item)"
|
v-drag.drop.food="item.key"
|
||||||
|
class="pet-group"
|
||||||
|
@itemDragOver="onDragOver($event, item)"
|
||||||
|
@itemDropped="onDrop($event, item)"
|
||||||
|
@itemDragLeave="onDragLeave()"
|
||||||
>
|
>
|
||||||
<template
|
<petItem
|
||||||
slot="itemBadge"
|
:item="item"
|
||||||
slot-scope="context"
|
:popover-position="'top'"
|
||||||
|
:show-popover="currentDraggingFood == null"
|
||||||
|
:highlight-border="highlightPet == item.key"
|
||||||
|
@click="petClicked(item)"
|
||||||
>
|
>
|
||||||
<equip-badge
|
<template
|
||||||
:equipped="context.item.key === currentPet"
|
slot="itemBadge"
|
||||||
:show="isOwned('pet', context.item)"
|
slot-scope="context"
|
||||||
@click="selectPet(context.item)"
|
>
|
||||||
/>
|
<equip-badge
|
||||||
</template>
|
:equipped="context.item.key === currentPet"
|
||||||
</petItem>
|
:show="isOwned('pet', context.item)"
|
||||||
|
@click="selectPet(context.item)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</petItem>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<show-more-button
|
||||||
|
v-if="petRowCount[petGroup.key] > 1 && petGroup.key !== 'specialPets' && !(petGroup.key === 'wackyPets' && selectedSortBy !== 'sortByColor')"
|
||||||
|
:show-all="$_openedItemRows_isToggled(petGroup.key)"
|
||||||
|
@click="setShowMore(petGroup.key)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<show-more-button
|
|
||||||
v-if="petRowCount[petGroup.key] > 1 && petGroup.key !== 'specialPets' && !(petGroup.key === 'wackyPets' && selectedSortBy !== 'sortByColor')"
|
|
||||||
:show-all="$_openedItemRows_isToggled(petGroup.key)"
|
|
||||||
class="show-more-button"
|
|
||||||
@click="setShowMore(petGroup.key)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<h2>
|
<h2>
|
||||||
{{ $t('mounts') }}
|
{{ $t('mounts') }}
|
||||||
|
|
@ -196,52 +197,55 @@
|
||||||
v-for="mountGroup in mountGroups"
|
v-for="mountGroup in mountGroups"
|
||||||
v-if="!anyFilterSelected || viewOptions[mountGroup.key].selected"
|
v-if="!anyFilterSelected || viewOptions[mountGroup.key].selected"
|
||||||
:key="mountGroup.key"
|
:key="mountGroup.key"
|
||||||
|
:class="{ hide: viewOptions[mountGroup.key].animalCount === 0 }"
|
||||||
>
|
>
|
||||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||||
<h4 v-if="viewOptions[mountGroup.key].animalCount != 0">
|
<h4 v-if="viewOptions[mountGroup.key].animalCount != 0">
|
||||||
{{ mountGroup.label }}
|
{{ mountGroup.label }}
|
||||||
</h4>
|
</h4>
|
||||||
<!-- eslint-disable vue/no-use-v-if-with-v-for, max-len -->
|
<!-- eslint-disable vue/no-use-v-if-with-v-for, max-len -->
|
||||||
<div
|
<div class="d-inline-flex flex-column">
|
||||||
v-for="(group, key, index) in mounts(mountGroup, hideMissing, selectedSortBy, searchTextThrottled)"
|
|
||||||
v-if="index === 0 || $_openedItemRows_isToggled(mountGroup.key)"
|
|
||||||
:key="key"
|
|
||||||
class="pet-row d-flex"
|
|
||||||
>
|
|
||||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
|
||||||
<div
|
<div
|
||||||
v-for="item in group"
|
v-for="(group, key, index) in mounts(mountGroup, hideMissing, selectedSortBy, searchTextThrottled)"
|
||||||
v-show="show('mount', item)"
|
v-if="index === 0 || $_openedItemRows_isToggled(mountGroup.key)"
|
||||||
:key="item.key"
|
:key="key"
|
||||||
class="pet-group"
|
class="pet-row d-flex"
|
||||||
>
|
>
|
||||||
<mountItem
|
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||||
|
<div
|
||||||
|
v-for="item in group"
|
||||||
|
v-show="show('mount', item)"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
:item="item"
|
class="pet-group"
|
||||||
:popover-position="'top'"
|
|
||||||
:show-popover="true"
|
|
||||||
@click="selectMount(item)"
|
|
||||||
>
|
>
|
||||||
<span slot="popoverContent">
|
<mountItem
|
||||||
<h4 class="popover-content-title">{{ item.name }}</h4>
|
:key="item.key"
|
||||||
</span>
|
:item="item"
|
||||||
<template
|
:popover-position="'top'"
|
||||||
slot="itemBadge"
|
:show-popover="true"
|
||||||
|
@click="selectMount(item)"
|
||||||
>
|
>
|
||||||
<equip-badge
|
<span slot="popoverContent">
|
||||||
:equipped="item.key === currentMount"
|
<h4 class="popover-content-title">{{ item.name }}</h4>
|
||||||
:show="isOwned('mount', item)"
|
</span>
|
||||||
@click="selectMount(item)"
|
<template
|
||||||
/>
|
slot="itemBadge"
|
||||||
</template>
|
>
|
||||||
</mountItem>
|
<equip-badge
|
||||||
|
:equipped="item.key === currentMount"
|
||||||
|
:show="isOwned('mount', item)"
|
||||||
|
@click="selectMount(item)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</mountItem>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<show-more-button
|
||||||
|
v-if="mountRowCount[mountGroup.key] > 1 && mountGroup.key !== 'specialMounts'"
|
||||||
|
:show-all="$_openedItemRows_isToggled(mountGroup.key)"
|
||||||
|
@click="setShowMore(mountGroup.key)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<show-more-button
|
|
||||||
v-if="mountRowCount[mountGroup.key] > 1 && mountGroup.key !== 'specialMounts'"
|
|
||||||
:show-all="$_openedItemRows_isToggled(mountGroup.key)"
|
|
||||||
@click="setShowMore(mountGroup.key)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<inventoryDrawer>
|
<inventoryDrawer>
|
||||||
<template
|
<template
|
||||||
|
|
@ -310,13 +314,8 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pet-row {
|
.hide {
|
||||||
max-width: 100%;
|
height: 0px;
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
.item {
|
|
||||||
margin-right: .5em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
@ -330,6 +329,14 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pet-row {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.pet-group:not(:last-of-type) {
|
||||||
|
margin-right: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.GreyedOut {
|
.GreyedOut {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
@ -343,24 +350,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.stable {
|
.stable {
|
||||||
|
|
||||||
.standard-page {
|
|
||||||
padding-right:0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.standard-page .clearfix .float-right {
|
|
||||||
margin-right: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg-icon.inline.icon-16 {
|
.svg-icon.inline.icon-16 {
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.last {
|
|
||||||
margin-right: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-focus:focus {
|
.no-focus:focus {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,41 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="d-flex justify-content-around">
|
<div class="d-flex align-items-center">
|
||||||
<span
|
<div
|
||||||
v-for="currency of currencies"
|
v-for="currency of currencies"
|
||||||
:key="currency.key"
|
:key="currency.key"
|
||||||
|
class="d-flex align-items-center"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="svg-icon ml-1"
|
class="svg-icon icon-16 ml-1"
|
||||||
v-html="currency.icon"
|
v-html="currency.icon"
|
||||||
></div>
|
></div>
|
||||||
<span
|
<div
|
||||||
:class="{'notEnough': currency.notEnough}"
|
:class="{'notEnough': currency.notEnough}"
|
||||||
class="mx-1"
|
class="currency-value mx-1 my-auto"
|
||||||
>
|
>
|
||||||
{{ currency.value | roundBigNumber }}
|
{{ currency.value | roundBigNumber }}
|
||||||
</span>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '~@/assets/scss/colors.scss';
|
||||||
|
|
||||||
span {
|
.currency-value {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
line-height: 1.33;
|
line-height: 1.33;
|
||||||
color: $gray-100;
|
color: $gray-100;
|
||||||
margin-bottom: 16px;
|
display: inline-block;
|
||||||
margin-top: -4px;
|
}
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg-icon {
|
|
||||||
vertical-align: middle;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notEnough {
|
.notEnough {
|
||||||
color: #f23035 !important;
|
color: #f23035 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.svg-icon {
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
||||||
|
|
@ -89,19 +89,19 @@
|
||||||
v-if="item.value > 0 && !(item.key === 'gem' && gemsLeft < 1)"
|
v-if="item.value > 0 && !(item.key === 'gem' && gemsLeft < 1)"
|
||||||
class="purchase-amount"
|
class="purchase-amount"
|
||||||
>
|
>
|
||||||
<!-- this is where the pretty item cost element lives -->
|
<div class="item-cost justify-content-center my-3">
|
||||||
<div class="item-cost">
|
|
||||||
<span
|
<span
|
||||||
class="cost"
|
class="cost d-flex mx-auto"
|
||||||
:class="getPriceClass()"
|
:class="getPriceClass()"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="svg-icon inline icon-24"
|
class="svg-icon icon-24 my-auto mr-1"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
v-html="icons[getPriceClass()]"
|
v-html="icons[getPriceClass()]"
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
class="my-auto"
|
||||||
:class="getPriceClass()"
|
:class="getPriceClass()"
|
||||||
>{{ item.value }}</span>
|
>{{ item.value }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -181,7 +181,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<countdown-banner
|
<countdown-banner
|
||||||
v-if="item.event && item.owned == null"
|
v-if="item.end && item.owned == null"
|
||||||
:end-date="endDate"
|
:end-date="endDate"
|
||||||
class="limitedTime available"
|
class="limitedTime available"
|
||||||
/>
|
/>
|
||||||
|
|
@ -218,11 +218,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
slot="modal-footer"
|
slot="modal-footer"
|
||||||
class="clearfix"
|
|
||||||
>
|
>
|
||||||
<span class="user-balance float-left">{{ $t('yourBalance') }}</span>
|
<span class="user-balance ml-3 my-auto">{{ $t('yourBalance') }}</span>
|
||||||
<balanceInfo
|
<balanceInfo
|
||||||
class="currency-totals"
|
class="mr-3"
|
||||||
:currency-needed="getPriceClass()"
|
:currency-needed="getPriceClass()"
|
||||||
:amount-needed="item.value"
|
:amount-needed="item.value"
|
||||||
/>
|
/>
|
||||||
|
|
@ -250,24 +249,21 @@
|
||||||
border-bottom-left-radius: 8px;
|
border-bottom-left-radius: 8px;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 24px 0 0 0;
|
margin: 24px 0 0 0;
|
||||||
padding: 16px 24px;
|
padding: 0px;
|
||||||
align-content: center;
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.user-balance {
|
.user-balance {
|
||||||
width: 150px;
|
|
||||||
height: 16px;
|
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 1.33;
|
line-height: 1.33;
|
||||||
color: $gray-100;
|
color: $gray-100;
|
||||||
margin-bottom: 16px;
|
|
||||||
margin-top: -4px;
|
|
||||||
margin-left: -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.currency-totals {
|
|
||||||
margin-right: -8px;
|
|
||||||
float: right;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -452,14 +448,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-cost {
|
.item-cost {
|
||||||
display: inline-flex;
|
|
||||||
margin: 16px 0;
|
|
||||||
align-items: center;
|
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cost {
|
.cost {
|
||||||
display: inline-block;
|
width: fit-content;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
@ -470,19 +463,16 @@
|
||||||
&.gems {
|
&.gems {
|
||||||
color: $green-10;
|
color: $green-10;
|
||||||
background-color: rgba(36, 204, 143, 0.15);
|
background-color: rgba(36, 204, 143, 0.15);
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.gold {
|
&.gold {
|
||||||
color: $yellow-5;
|
color: $yellow-5;
|
||||||
background-color: rgba(255, 190, 93, 0.15);
|
background-color: rgba(255, 190, 93, 0.15);
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.hourglasses {
|
&.hourglasses {
|
||||||
color: $hourglass-color;
|
color: $hourglass-color;
|
||||||
background-color: rgba(41, 149, 205, 0.15);
|
background-color: rgba(41, 149, 205, 0.15);
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -547,10 +537,6 @@
|
||||||
margin: auto -1rem -1rem;
|
margin: auto -1rem -1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
// .pt-015 {
|
|
||||||
// padding-top: 0.15rem;
|
|
||||||
// }
|
|
||||||
|
|
||||||
.gems-left {
|
.gems-left {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
background-color: $green-100;
|
background-color: $green-100;
|
||||||
|
|
@ -602,8 +588,10 @@ 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 { drops as dropEggs } from '@/../../common/script/content/eggs';
|
||||||
import { drops as dropPotions } from '@/../../common/script/content/hatching-potions';
|
import { drops as dropPotions } from '@/../../common/script/content/hatching-potions';
|
||||||
import spellsMixin from '@/mixins/spells';
|
import { avatarEditorUtilities } from '@/mixins/avatarEditUtilities';
|
||||||
import numberInvalid from '@/mixins/numberInvalid';
|
import numberInvalid from '@/mixins/numberInvalid';
|
||||||
|
import spellsMixin from '@/mixins/spells';
|
||||||
|
import sync from '@/mixins/sync';
|
||||||
|
|
||||||
import svgClose from '@/assets/svg/close.svg';
|
import svgClose from '@/assets/svg/close.svg';
|
||||||
import svgGold from '@/assets/svg/gold.svg';
|
import svgGold from '@/assets/svg/gold.svg';
|
||||||
|
|
@ -637,7 +625,7 @@ const amountOfDropPotions = size(dropPotions);
|
||||||
const hideAmountSelectionForPurchaseTypes = [
|
const hideAmountSelectionForPurchaseTypes = [
|
||||||
'gear', 'backgrounds', 'mystery_set', 'card',
|
'gear', 'backgrounds', 'mystery_set', 'card',
|
||||||
'rebirth_orb', 'fortify', 'armoire', 'keys',
|
'rebirth_orb', 'fortify', 'armoire', 'keys',
|
||||||
'debuffPotion', 'pets', 'mounts',
|
'debuffPotion', 'pets', 'mounts', 'customization',
|
||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -650,7 +638,15 @@ export default {
|
||||||
CountdownBanner,
|
CountdownBanner,
|
||||||
numberIncrement,
|
numberIncrement,
|
||||||
},
|
},
|
||||||
mixins: [buyMixin, currencyMixin, notifications, numberInvalid, spellsMixin],
|
mixins: [
|
||||||
|
avatarEditorUtilities,
|
||||||
|
buyMixin,
|
||||||
|
currencyMixin,
|
||||||
|
notifications,
|
||||||
|
numberInvalid,
|
||||||
|
spellsMixin,
|
||||||
|
sync,
|
||||||
|
],
|
||||||
props: {
|
props: {
|
||||||
// eslint-disable-next-line vue/require-default-prop
|
// eslint-disable-next-line vue/require-default-prop
|
||||||
item: {
|
item: {
|
||||||
|
|
@ -690,7 +686,8 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({ user: 'user.data' }),
|
...mapState({ user: 'user.data' }),
|
||||||
showAvatar () {
|
showAvatar () {
|
||||||
return ['backgrounds', 'gear', 'mystery_set'].includes(this.item.purchaseType);
|
return ['backgrounds', 'gear', 'mystery_set', 'customization']
|
||||||
|
.includes(this.item.purchaseType);
|
||||||
},
|
},
|
||||||
|
|
||||||
preventHealthPotion () {
|
preventHealthPotion () {
|
||||||
|
|
@ -741,7 +738,7 @@ export default {
|
||||||
return (!this.user.purchased.plan.customerId && !this.user.purchased.plan.consecutive.trinkets && this.getPriceClass() === 'hourglasses');
|
return (!this.user.purchased.plan.customerId && !this.user.purchased.plan.consecutive.trinkets && this.getPriceClass() === 'hourglasses');
|
||||||
},
|
},
|
||||||
endDate () {
|
endDate () {
|
||||||
return moment(this.item.event.end);
|
return moment(this.item.end);
|
||||||
},
|
},
|
||||||
totalOwned () {
|
totalOwned () {
|
||||||
return this.user.items[this.item.purchaseType][this.item.key] || 0;
|
return this.user.items[this.item.purchaseType][this.item.key] || 0;
|
||||||
|
|
@ -759,7 +756,7 @@ export default {
|
||||||
this.selectedAmountToBuy = 1;
|
this.selectedAmountToBuy = 1;
|
||||||
},
|
},
|
||||||
|
|
||||||
buyItem () {
|
async buyItem () {
|
||||||
// @TODO: I think we should buying to the items.
|
// @TODO: I think we should buying to the items.
|
||||||
// Turn the items into classes, and use polymorphism
|
// Turn the items into classes, and use polymorphism
|
||||||
if (this.item.buy) {
|
if (this.item.buy) {
|
||||||
|
|
@ -824,17 +821,25 @@ export default {
|
||||||
) return;
|
) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldConfirmPurchase = this.item.currency === 'gems' || this.item.currency === 'hourglasses';
|
if (this.item.purchaseType === 'customization') {
|
||||||
if (
|
const buySuccess = await this.unlock(this.item.path);
|
||||||
shouldConfirmPurchase
|
if (!buySuccess) return;
|
||||||
&& !this.confirmPurchase(this.item.currency, this.item.value * this.selectedAmountToBuy)
|
this.sync();
|
||||||
) {
|
this.$root.$emit('playSound', 'Reward');
|
||||||
return;
|
this.$root.$emit('buyModal::boughtItem', this.item);
|
||||||
}
|
|
||||||
|
|
||||||
if (this.genericPurchase) {
|
|
||||||
this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
|
|
||||||
this.purchased(this.item.text);
|
this.purchased(this.item.text);
|
||||||
|
} else {
|
||||||
|
const shouldConfirmPurchase = this.item.currency === 'gems' || this.item.currency === 'hourglasses';
|
||||||
|
if (
|
||||||
|
shouldConfirmPurchase
|
||||||
|
&& !this.confirmPurchase(this.item.currency, this.item.value * this.selectedAmountToBuy)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.genericPurchase) {
|
||||||
|
this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
|
||||||
|
this.purchased(this.item.text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$emit('buyPressed', this.item);
|
this.$emit('buyPressed', this.item);
|
||||||
|
|
@ -891,6 +896,27 @@ export default {
|
||||||
|
|
||||||
return gear;
|
return gear;
|
||||||
}
|
}
|
||||||
|
case 'customization': {
|
||||||
|
if (item.type === 'skin') {
|
||||||
|
return {
|
||||||
|
skin: item.key,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (item.type === 'shirt') {
|
||||||
|
return {
|
||||||
|
shirt: item.key,
|
||||||
|
armor: 'armor_base_0',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (['base', 'beard', 'color', 'mustache'].includes(item.type)) {
|
||||||
|
return {
|
||||||
|
hair: {
|
||||||
|
[item.type]: item.key,
|
||||||
|
},
|
||||||
|
head: 'head_base_0',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
|
|
|
||||||
265
website/client/src/components/shops/customizations/index.vue
Normal file
265
website/client/src/components/shops/customizations/index.vue
Normal file
|
|
@ -0,0 +1,265 @@
|
||||||
|
<template>
|
||||||
|
<div class="row market">
|
||||||
|
<div class="standard-sidebar">
|
||||||
|
<filter-sidebar>
|
||||||
|
<div
|
||||||
|
slot="search"
|
||||||
|
class="form-group"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="searchText"
|
||||||
|
class="form-control input-search"
|
||||||
|
type="text"
|
||||||
|
:placeholder="$t('search')"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<filter-group>
|
||||||
|
<checkbox
|
||||||
|
v-for="category in unfilteredCategories"
|
||||||
|
:id="`category-${category.identifier}`"
|
||||||
|
:key="category.identifier"
|
||||||
|
:checked.sync="viewOptions[category.identifier].selected"
|
||||||
|
:text="category.text"
|
||||||
|
/>
|
||||||
|
</filter-group>
|
||||||
|
</filter-sidebar>
|
||||||
|
</div>
|
||||||
|
<div class="standard-page p-0">
|
||||||
|
<div
|
||||||
|
class="background"
|
||||||
|
:style="{'background-image': imageURLs.background}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="npc"
|
||||||
|
:style="{'background-image': imageURLs.npc}"
|
||||||
|
>
|
||||||
|
<div class="featured-label">
|
||||||
|
<span class="rectangle"></span><span
|
||||||
|
v-once
|
||||||
|
class="text"
|
||||||
|
>{{ $t('customizationsNPC') }}</span><span class="rectangle"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<h1
|
||||||
|
v-once
|
||||||
|
>
|
||||||
|
{{ $t('customizations') }}
|
||||||
|
</h1>
|
||||||
|
<div
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category.identifier"
|
||||||
|
>
|
||||||
|
<h2 class="mb-3 mt-4">
|
||||||
|
{{ category.text }}
|
||||||
|
</h2>
|
||||||
|
<item-rows
|
||||||
|
:items="customizationsItems({category, searchBy: searchTextThrottled})"
|
||||||
|
:type="category.identifier"
|
||||||
|
:fold-button="category.identifier === 'background'"
|
||||||
|
:item-width="94"
|
||||||
|
:item-margin="24"
|
||||||
|
:max-items-per-row="8"
|
||||||
|
:no-items-label="emptyStateString(category.identifier)"
|
||||||
|
@emptyClick="emptyClick(category.identifier, $event)"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
slot="item"
|
||||||
|
slot-scope="ctx"
|
||||||
|
>
|
||||||
|
<shop-item
|
||||||
|
:key="ctx.item.path"
|
||||||
|
:item="ctx.item"
|
||||||
|
:price="ctx.item.value"
|
||||||
|
:price-type="ctx.item.currency"
|
||||||
|
:empty-item="false"
|
||||||
|
:show-popover="Boolean(ctx.item.text)"
|
||||||
|
@click="selectItem(ctx.item)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</item-rows>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~@/assets/scss/colors.scss';
|
||||||
|
@import '~@/assets/scss/shops.scss';
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
line-height: 32px;
|
||||||
|
color: $purple-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.background, .npc {
|
||||||
|
height: 216px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-label {
|
||||||
|
margin-left: 90px;
|
||||||
|
margin-top: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.npc {
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import shops from '@/../../common/script/libs/shops';
|
||||||
|
import throttle from 'lodash/throttle';
|
||||||
|
import { mapState } from '@/libs/store';
|
||||||
|
|
||||||
|
import Checkbox from '@/components/ui/checkbox';
|
||||||
|
import FilterGroup from '@/components/ui/filterGroup';
|
||||||
|
import FilterSidebar from '@/components/ui/filterSidebar';
|
||||||
|
import ItemRows from '@/components/ui/itemRows';
|
||||||
|
import ShopItem from '../shopItem';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Checkbox,
|
||||||
|
FilterGroup,
|
||||||
|
FilterSidebar,
|
||||||
|
ItemRows,
|
||||||
|
ShopItem,
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
searchText: null,
|
||||||
|
searchTextThrottled: null,
|
||||||
|
unfilteredCategories: [],
|
||||||
|
viewOptions: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
// content: 'content',
|
||||||
|
user: 'user.data',
|
||||||
|
currentEventList: 'worldState.data.currentEventList',
|
||||||
|
}),
|
||||||
|
anyFilterSelected () {
|
||||||
|
return Object.values(this.viewOptions).some(g => g.selected);
|
||||||
|
},
|
||||||
|
imageURLs () {
|
||||||
|
return {
|
||||||
|
background: 'url(/static/npc/normal/customizations_background.png)',
|
||||||
|
npc: 'url(/static/npc/normal/customizations_npc.png)',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
categories () {
|
||||||
|
const { unfilteredCategories } = this;
|
||||||
|
|
||||||
|
return unfilteredCategories.filter(category => !this.anyFilterSelected
|
||||||
|
|| this.viewOptions[category.identifier].selected);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// TODO mixin?
|
||||||
|
searchText: throttle(function throttleSearch () {
|
||||||
|
this.searchTextThrottled = this.searchText.toLowerCase();
|
||||||
|
}, 250),
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.$store.dispatch('common:setTitle', {
|
||||||
|
subSection: this.$t('customizations'),
|
||||||
|
section: this.$t('shops'),
|
||||||
|
});
|
||||||
|
this.updateShop();
|
||||||
|
this.$root.$on('buyModal::boughtItem', () => {
|
||||||
|
this.updateShop();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
customizationsItems (options = {}) {
|
||||||
|
const { category, searchBy } = options;
|
||||||
|
return category.items.filter(item => !searchBy
|
||||||
|
|| item.text.toLowerCase().includes(searchBy));
|
||||||
|
},
|
||||||
|
emptyClick (identifier, event) {
|
||||||
|
if (event.target.tagName !== 'A') return;
|
||||||
|
this.$store.state.avatarEditorOptions.editingUser = true;
|
||||||
|
switch (identifier) {
|
||||||
|
case 'animalEars':
|
||||||
|
this.$store.state.avatarEditorOptions.startingPage = 'extra';
|
||||||
|
this.$store.state.avatarEditorOptions.subpage = 'ears';
|
||||||
|
break;
|
||||||
|
case 'animalTails':
|
||||||
|
this.$store.state.avatarEditorOptions.startingPage = 'extra';
|
||||||
|
this.$store.state.avatarEditorOptions.subpage = 'tails';
|
||||||
|
break;
|
||||||
|
case 'backgrounds':
|
||||||
|
this.$store.state.avatarEditorOptions.startingPage = 'background';
|
||||||
|
this.$store.state.avatarEditorOptions.subpage = '2024';
|
||||||
|
break;
|
||||||
|
case 'facialHair':
|
||||||
|
this.$store.state.avatarEditorOptions.startingPage = 'hair';
|
||||||
|
this.$store.state.avatarEditorOptions.subpage = 'beard';
|
||||||
|
break;
|
||||||
|
case 'color':
|
||||||
|
this.$store.state.avatarEditorOptions.startingPage = 'hair';
|
||||||
|
this.$store.state.avatarEditorOptions.subpage = 'color';
|
||||||
|
break;
|
||||||
|
case 'base':
|
||||||
|
this.$store.state.avatarEditorOptions.startingPage = 'hair';
|
||||||
|
this.$store.state.avatarEditorOptions.subpage = 'style';
|
||||||
|
break;
|
||||||
|
case 'shirt':
|
||||||
|
this.$store.state.avatarEditorOptions.startingPage = 'body';
|
||||||
|
this.$store.state.avatarEditorOptions.subpage = 'shirt';
|
||||||
|
break;
|
||||||
|
case 'skin':
|
||||||
|
this.$store.state.avatarEditorOptions.startingPage = 'skin';
|
||||||
|
this.$store.state.avatarEditorOptions.subpage = 'color';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown identifier ${identifier}`);
|
||||||
|
}
|
||||||
|
this.$root.$emit('bv::show::modal', 'avatar-modal');
|
||||||
|
},
|
||||||
|
emptyStateString (identifier) {
|
||||||
|
const { $t } = this;
|
||||||
|
switch (identifier) {
|
||||||
|
case 'animalEars':
|
||||||
|
return $t('allCustomizationsOwned');
|
||||||
|
case 'animalTails':
|
||||||
|
return $t('allCustomizationsOwned');
|
||||||
|
case 'backgrounds':
|
||||||
|
return `${$t('allCustomizationsOwned')} ${$t('checkNextMonth')}`;
|
||||||
|
case 'facialHair':
|
||||||
|
return $t('allCustomizationsOwned');
|
||||||
|
case 'color':
|
||||||
|
return `${$t('allCustomizationsOwned')} ${$t('checkNextSeason')}`;
|
||||||
|
case 'base':
|
||||||
|
return $t('allCustomizationsOwned');
|
||||||
|
case 'shirt':
|
||||||
|
return $t('allCustomizationsOwned');
|
||||||
|
case 'skin':
|
||||||
|
return `${$t('allCustomizationsOwned')} ${$t('checkNextSeason')}`;
|
||||||
|
default:
|
||||||
|
return `Unknown identifier ${identifier}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectItem (item) {
|
||||||
|
this.$root.$emit('buyModal::showItem', item);
|
||||||
|
},
|
||||||
|
updateShop () {
|
||||||
|
const shop = shops.getCustomizationsShop(this.user);
|
||||||
|
|
||||||
|
shop.categories.forEach(category => {
|
||||||
|
// do not reset the viewOptions if already set once
|
||||||
|
if (typeof this.viewOptions[category.identifier] === 'undefined') {
|
||||||
|
this.$set(this.viewOptions, category.identifier, {
|
||||||
|
selected: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.unfilteredCategories = shop.categories;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -89,7 +89,7 @@ export default {
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.featuredItems {
|
.featuredItems {
|
||||||
height: 216px;
|
height: 192px;
|
||||||
|
|
||||||
.background {
|
.background {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
|
|
@ -3,26 +3,32 @@
|
||||||
<secondary-menu class="col-12">
|
<secondary-menu class="col-12">
|
||||||
<router-link
|
<router-link
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
:to="{name: 'market'}"
|
:to="{ name: 'market' }"
|
||||||
exact="exact"
|
exact="exact"
|
||||||
>
|
>
|
||||||
{{ $t('market') }}
|
{{ $t('market') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
:to="{name: 'quests'}"
|
:to="{ name: 'quests' }"
|
||||||
>
|
>
|
||||||
{{ $t('quests') }}
|
{{ $t('quests') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
:to="{name: 'seasonal'}"
|
:to="{ name: 'customizations' }"
|
||||||
|
>
|
||||||
|
{{ $t('customizations') }}
|
||||||
|
</router-link>
|
||||||
|
<router-link
|
||||||
|
class="nav-link"
|
||||||
|
:to="{ name: 'seasonal' }"
|
||||||
>
|
>
|
||||||
{{ $t('titleSeasonalShop') }}
|
{{ $t('titleSeasonalShop') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
:to="{name: 'time'}"
|
:to="{ name: 'time' }"
|
||||||
>
|
>
|
||||||
{{ $t('titleTimeTravelers') }}
|
{{ $t('titleTimeTravelers') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
:initial-item="selectedGearCategory"
|
:initial-item="selectedGearCategory"
|
||||||
:items="marketGearCategories"
|
:items="marketGearCategories"
|
||||||
:with-icon="true"
|
:with-icon="true"
|
||||||
|
:direct-select="true"
|
||||||
@selected="selectedGroupGearByClass = $event.id"
|
@selected="selectedGroupGearByClass = $event.id"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|
@ -23,6 +24,7 @@
|
||||||
:label="$t('sortBy')"
|
:label="$t('sortBy')"
|
||||||
:initial-item="selectedSortGearBy"
|
:initial-item="selectedSortGearBy"
|
||||||
:items="sortGearBy"
|
:items="sortGearBy"
|
||||||
|
:direct-select="true"
|
||||||
@selected="selectedSortGearBy = $event"
|
@selected="selectedSortGearBy = $event"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|
@ -40,7 +42,7 @@
|
||||||
:item-width="94"
|
:item-width="94"
|
||||||
:item-margin="24"
|
:item-margin="24"
|
||||||
:type="'gear'"
|
:type="'gear'"
|
||||||
:no-items-label="$t('noGearItemsOfClass')"
|
:no-items-label="noItemsLabel"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
slot="item"
|
slot="item"
|
||||||
|
|
@ -75,6 +77,7 @@
|
||||||
import _filter from 'lodash/filter';
|
import _filter from 'lodash/filter';
|
||||||
import _orderBy from 'lodash/orderBy';
|
import _orderBy from 'lodash/orderBy';
|
||||||
import shops from '@/../../common/script/libs/shops';
|
import shops from '@/../../common/script/libs/shops';
|
||||||
|
import { remainingGearInSet } from '@/../../common/script/count';
|
||||||
import { getClassName } from '@/../../common/script/libs/getClassName';
|
import { getClassName } from '@/../../common/script/libs/getClassName';
|
||||||
import { mapState } from '@/libs/store';
|
import { mapState } from '@/libs/store';
|
||||||
import LayoutSection from '@/components/ui/layoutSection';
|
import LayoutSection from '@/components/ui/layoutSection';
|
||||||
|
|
@ -93,7 +96,7 @@ import pinUtils from '../../../mixins/pinUtils';
|
||||||
const sortGearTypes = [
|
const sortGearTypes = [
|
||||||
'sortByType', 'sortByPrice', 'sortByCon',
|
'sortByType', 'sortByPrice', 'sortByCon',
|
||||||
'sortByPer', 'sortByStr', 'sortByInt',
|
'sortByPer', 'sortByStr', 'sortByInt',
|
||||||
].map(g => ({ id: g }));
|
].map(g => ({ id: g, identifier: g }));
|
||||||
|
|
||||||
const sortGearTypeMap = {
|
const sortGearTypeMap = {
|
||||||
sortByType: 'type',
|
sortByType: 'type',
|
||||||
|
|
@ -134,6 +137,17 @@ export default {
|
||||||
userItems: 'user.data.items',
|
userItems: 'user.data.items',
|
||||||
userStats: 'user.data.stats',
|
userStats: 'user.data.stats',
|
||||||
}),
|
}),
|
||||||
|
armoireCount () {
|
||||||
|
return remainingGearInSet(this.userItems.gear.owned, 'armoire');
|
||||||
|
},
|
||||||
|
noItemsLabel () {
|
||||||
|
if (this.armoireCount > 0) {
|
||||||
|
return `${this.$t('gearItemsCompleted', { klass: this.$t(this.selectedGroupGearByClass) })}
|
||||||
|
${this.$t('moreArmoireGearAvailable', { armoireCount: this.armoireCount })}`;
|
||||||
|
}
|
||||||
|
return `${this.$t('gearItemsCompleted', { klass: this.$t(this.selectedGroupGearByClass) })}
|
||||||
|
${this.$t('moreArmoireGearComing')}`;
|
||||||
|
},
|
||||||
marketGearCategories () {
|
marketGearCategories () {
|
||||||
return shops.getMarketGearCategories(this.user).map(c => {
|
return shops.getMarketGearCategories(this.user).map(c => {
|
||||||
c.id = c.identifier;
|
c.id = c.identifier;
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
/>
|
/>
|
||||||
<h1
|
<h1
|
||||||
v-once
|
v-once
|
||||||
class="mb-4 page-header"
|
class="page-header mt-4 mb-4"
|
||||||
>
|
>
|
||||||
{{ $t('market') }}
|
{{ $t('market') }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
@ -35,6 +35,7 @@
|
||||||
:hide-pinned="hidePinned"
|
:hide-pinned="hidePinned"
|
||||||
:hide-locked="hideLocked"
|
:hide-locked="hideLocked"
|
||||||
:search-by="searchTextThrottled"
|
:search-by="searchTextThrottled"
|
||||||
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
<layout-section :title="$t('items')">
|
<layout-section :title="$t('items')">
|
||||||
<div slot="filters">
|
<div slot="filters">
|
||||||
|
|
@ -42,6 +43,7 @@
|
||||||
:label="$t('sortBy')"
|
:label="$t('sortBy')"
|
||||||
:initial-item="selectedSortItemsBy"
|
:initial-item="selectedSortItemsBy"
|
||||||
:items="sortItemsBy"
|
:items="sortItemsBy"
|
||||||
|
:direct-select="true"
|
||||||
@selected="selectedSortItemsBy = $event"
|
@selected="selectedSortItemsBy = $event"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|
@ -121,6 +123,10 @@
|
||||||
height: 112px;
|
height: 112px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
max-width: 944px;
|
||||||
|
}
|
||||||
|
|
||||||
.market {
|
.market {
|
||||||
.avatar {
|
.avatar {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
@ -133,7 +139,7 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -14px;
|
bottom: -14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
left: 80px;
|
left: 75px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -152,6 +158,7 @@ import _map from 'lodash/map';
|
||||||
import _throttle from 'lodash/throttle';
|
import _throttle from 'lodash/throttle';
|
||||||
import getItemInfo from '@/../../common/script/libs/getItemInfo';
|
import getItemInfo from '@/../../common/script/libs/getItemInfo';
|
||||||
import shops from '@/../../common/script/libs/shops';
|
import shops from '@/../../common/script/libs/shops';
|
||||||
|
import { getScheduleMatchingGroup } from '@/../../common/script/content/constants/schedule';
|
||||||
import { mapState } from '@/libs/store';
|
import { mapState } from '@/libs/store';
|
||||||
|
|
||||||
import KeysToKennel from './keysToKennel';
|
import KeysToKennel from './keysToKennel';
|
||||||
|
|
@ -175,7 +182,7 @@ import inventoryUtils from '@/mixins/inventoryUtils';
|
||||||
import pinUtils from '@/mixins/pinUtils';
|
import pinUtils from '@/mixins/pinUtils';
|
||||||
import { worldStateMixin } from '@/mixins/worldState';
|
import { worldStateMixin } from '@/mixins/worldState';
|
||||||
|
|
||||||
const sortItems = ['AZ', 'sortByNumber'].map(g => ({ id: g }));
|
const sortItems = ['AZ', 'sortByNumber'].map(g => ({ id: g, identifier: g }));
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -218,6 +225,7 @@ export default {
|
||||||
|
|
||||||
hideLocked: false,
|
hideLocked: false,
|
||||||
hidePinned: false,
|
hidePinned: false,
|
||||||
|
cardMatcher: getScheduleMatchingGroup('cards'),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -241,7 +249,8 @@ export default {
|
||||||
categories.push({
|
categories.push({
|
||||||
identifier: 'cards',
|
identifier: 'cards',
|
||||||
text: this.$t('cards'),
|
text: this.$t('cards'),
|
||||||
items: _map(_filter(this.content.cardTypes, value => value.yearRound), value => ({
|
items: _map(_filter(this.content.cardTypes, value => value.yearRound
|
||||||
|
|| this.cardMatcher.items.indexOf(value.key) !== -1), value => ({
|
||||||
...getItemInfo(this.user, 'card', value),
|
...getItemInfo(this.user, 'card', value),
|
||||||
showCount: false,
|
showCount: false,
|
||||||
})),
|
})),
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<countdown-banner
|
<countdown-banner
|
||||||
v-if="item.event"
|
v-if="item.end"
|
||||||
:end-date="endDate"
|
:end-date="endDate"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
|
|
@ -470,7 +470,7 @@ export default {
|
||||||
return this.icons.gems;
|
return this.icons.gems;
|
||||||
},
|
},
|
||||||
endDate () {
|
endDate () {
|
||||||
return moment(this.item.event.end);
|
return moment(this.item.end);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
||||||
|
|
@ -115,23 +115,25 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h1
|
<div class="d-flex justify-content-between w-75">
|
||||||
v-once
|
<h1
|
||||||
class="mb-4 page-header"
|
v-once
|
||||||
>
|
class="mb-4 page-header"
|
||||||
{{ $t('quests') }}
|
>
|
||||||
</h1>
|
{{ $t('quests') }}
|
||||||
<div class="clearfix">
|
</h1>
|
||||||
<div class="float-right">
|
<div class="clearfix">
|
||||||
<span class="dropdown-label">{{ $t('sortBy') }}</span>
|
<div class="float-right">
|
||||||
<select-translated-array
|
<span class="dropdown-label">{{ $t('sortBy') }}</span>
|
||||||
:right="true"
|
<select-translated-array
|
||||||
:value="selectedSortItemsBy"
|
:right="true"
|
||||||
:items="sortItemsBy"
|
:value="selectedSortItemsBy"
|
||||||
:inline-dropdown="false"
|
:items="sortItemsBy"
|
||||||
class="inline"
|
:inline-dropdown="false"
|
||||||
@select="selectedSortItemsBy = $event"
|
class="inline"
|
||||||
/>
|
@select="selectedSortItemsBy = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
||||||
|
|
@ -197,48 +199,52 @@
|
||||||
</itemRows>
|
</itemRows>
|
||||||
<div
|
<div
|
||||||
v-else-if="category.identifier === 'unlockable' || category.identifier === 'gold'"
|
v-else-if="category.identifier === 'unlockable' || category.identifier === 'gold'"
|
||||||
class="grouped-parent"
|
class="d-flex justify-content-between flex-wrap w-75"
|
||||||
>
|
>
|
||||||
<!-- eslint-disable vue/no-use-v-if-with-v-for, max-len -->
|
|
||||||
<div
|
<div
|
||||||
v-for="(items, key) in getGrouped(questItems(category, selectedSortItemsBy,searchTextThrottled, hideLocked, hidePinned))"
|
v-for="(items, key) in getGrouped(
|
||||||
|
questItems(
|
||||||
|
category, selectedSortItemsBy,searchTextThrottled, hideLocked, hidePinned
|
||||||
|
)
|
||||||
|
)"
|
||||||
:key="key"
|
:key="key"
|
||||||
class="group"
|
class="quest-group mb-3"
|
||||||
>
|
>
|
||||||
<!-- eslint-enable vue/no-use-v-if-with-v-for, max-len -->
|
<div class="quest-container">
|
||||||
<h3>{{ $t(key) }}</h3>
|
<h3>{{ $t(key) }}</h3>
|
||||||
<div class="items">
|
<div class="items d-flex justify-content-left">
|
||||||
<shopItem
|
<shopItem
|
||||||
v-for="item in items"
|
v-for="item in items"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
:item="item"
|
:item="item"
|
||||||
:price="item.value"
|
:price="item.value"
|
||||||
:empty-item="false"
|
:empty-item="false"
|
||||||
:popover-position="'top'"
|
:popover-position="'top'"
|
||||||
:owned="!isNaN(userItems.quests[item.key])"
|
:owned="!isNaN(userItems.quests[item.key])"
|
||||||
@click="selectItem(item)"
|
@click="selectItem(item)"
|
||||||
>
|
|
||||||
<span slot="popoverContent">
|
|
||||||
<quest-popover :item="item" />
|
|
||||||
</span>
|
|
||||||
<template
|
|
||||||
slot="itemBadge"
|
|
||||||
slot-scope="ctx"
|
|
||||||
>
|
>
|
||||||
<span
|
<span slot="popoverContent">
|
||||||
class="badge-top"
|
<quest-popover :item="item" />
|
||||||
@click.prevent.stop="togglePinned(ctx.item)"
|
|
||||||
>
|
|
||||||
<pin-badge
|
|
||||||
:pinned="ctx.item.pinned"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
<countBadge
|
<template
|
||||||
:show="userItems.quests[ctx.item.key] > 0"
|
slot="itemBadge"
|
||||||
:count="userItems.quests[ctx.item.key] || 0"
|
slot-scope="ctx"
|
||||||
/>
|
>
|
||||||
</template>
|
<span
|
||||||
</shopItem>
|
class="badge-top"
|
||||||
|
@click.prevent.stop="togglePinned(ctx.item)"
|
||||||
|
>
|
||||||
|
<pin-badge
|
||||||
|
:pinned="ctx.item.pinned"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<countBadge
|
||||||
|
:show="userItems.quests[ctx.item.key] > 0"
|
||||||
|
:count="userItems.quests[ctx.item.key] || 0"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</shopItem>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -317,18 +323,16 @@
|
||||||
margin: 24px auto;
|
margin: 24px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group {
|
.quest-container {
|
||||||
display: inline-block;
|
min-width: 330px;
|
||||||
width: 33%;
|
}
|
||||||
margin-bottom: 24px;
|
|
||||||
vertical-align: top;
|
|
||||||
|
|
||||||
|
.quest-group {
|
||||||
.items {
|
.items {
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
width: fit-content;
|
||||||
background-color: #edecee;
|
background-color: #edecee;
|
||||||
display: inline-block;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-right: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-wrapper {
|
.item-wrapper {
|
||||||
|
|
@ -389,7 +393,7 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -14px;
|
bottom: -14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
left: 70px;
|
left: 62px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -526,7 +530,7 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.shop.categories;
|
return this.shop.categories.filter(category => category.items.length > 0);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="quest.event && !abbreviated"
|
v-if="quest.end && !abbreviated"
|
||||||
class="m-auto"
|
class="m-auto"
|
||||||
>
|
>
|
||||||
{{ limitedString }}
|
{{ limitedString }}
|
||||||
|
|
@ -210,14 +210,14 @@ export default {
|
||||||
return collect.text;
|
return collect.text;
|
||||||
},
|
},
|
||||||
countdownString () {
|
countdownString () {
|
||||||
if (!this.quest.event || this.purchased) return;
|
if (!this.quest.end || this.purchased) return;
|
||||||
const diffDuration = moment.duration(moment(this.quest.event.end).diff(moment()));
|
const diffDuration = moment.duration(moment(this.quest.end).diff(moment()));
|
||||||
|
|
||||||
if (diffDuration.asSeconds() <= 0) {
|
if (diffDuration.asSeconds() <= 0) {
|
||||||
this.limitedString = this.$t('noLongerAvailable');
|
this.limitedString = this.$t('noLongerAvailable');
|
||||||
} else if (diffDuration.days() > 0 || diffDuration.months() > 0) {
|
} else if (diffDuration.days() > 0 || diffDuration.months() > 0) {
|
||||||
this.limitedString = this.$t('limitedAvailabilityDays', {
|
this.limitedString = this.$t('limitedAvailabilityDays', {
|
||||||
days: moment(this.quest.event.end).diff(moment(), 'days'),
|
days: moment(this.quest.end).diff(moment(), 'days'),
|
||||||
hours: diffDuration.hours(),
|
hours: diffDuration.hours(),
|
||||||
minutes: diffDuration.minutes(),
|
minutes: diffDuration.minutes(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@
|
||||||
<div class="featuredItems">
|
<div class="featuredItems">
|
||||||
<div
|
<div
|
||||||
class="background"
|
class="background"
|
||||||
:class="{opened: seasonal.opened}"
|
|
||||||
:style="{'background-image': imageURLs.background}"
|
:style="{'background-image': imageURLs.background}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -54,20 +53,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!seasonal.opened"
|
v-if="seasonal.featured.items.length !== 0"
|
||||||
class="content"
|
|
||||||
>
|
|
||||||
<div class="featured-label with-border closed">
|
|
||||||
<span class="rectangle"></span>
|
|
||||||
<span
|
|
||||||
class="text"
|
|
||||||
v-html="seasonal.notes"
|
|
||||||
></span>
|
|
||||||
<span class="rectangle"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="seasonal.featured.items.length !== 0"
|
|
||||||
class="content"
|
class="content"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -135,17 +121,6 @@
|
||||||
<h2 class="float-left mb-3">
|
<h2 class="float-left mb-3">
|
||||||
{{ $t('classArmor') }}
|
{{ $t('classArmor') }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="float-right">
|
|
||||||
<span class="dropdown-label">{{ $t('sortBy') }}</span>
|
|
||||||
<select-translated-array
|
|
||||||
:right="true"
|
|
||||||
:value="selectedSortItemsBy"
|
|
||||||
:items="sortItemsBy"
|
|
||||||
:inline-dropdown="false"
|
|
||||||
class="inline"
|
|
||||||
@select="selectedSortItemsBy = $event"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="(groupSets, categoryGroup) in getGroupedCategories(categories)"
|
v-for="(groupSets, categoryGroup) in getGroupedCategories(categories)"
|
||||||
|
|
@ -174,7 +149,7 @@
|
||||||
<div class="items">
|
<div class="items">
|
||||||
<!-- eslint-disable max-len -->
|
<!-- eslint-disable max-len -->
|
||||||
<shopItem
|
<shopItem
|
||||||
v-for="item in seasonalItems(category, selectedSortItemsBy, searchTextThrottled, viewOptions, hidePinned)"
|
v-for="item in seasonalItems(category, 'AZ', searchTextThrottled, viewOptions, hidePinned)"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
:item="item"
|
:item="item"
|
||||||
:price="item.value"
|
:price="item.value"
|
||||||
|
|
@ -227,6 +202,10 @@
|
||||||
background-color: #edecee;
|
background-color: #edecee;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-wrapper {
|
.item-wrapper {
|
||||||
|
|
@ -319,7 +298,7 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -14px;
|
bottom: -14px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
left: 60px;
|
left: 32px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -367,15 +346,11 @@ import svgWizard from '@/assets/svg/wizard.svg';
|
||||||
import svgRogue from '@/assets/svg/rogue.svg';
|
import svgRogue from '@/assets/svg/rogue.svg';
|
||||||
import svgHealer from '@/assets/svg/healer.svg';
|
import svgHealer from '@/assets/svg/healer.svg';
|
||||||
|
|
||||||
import SelectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
|
|
||||||
import FilterSidebar from '@/components/ui/filterSidebar';
|
import FilterSidebar from '@/components/ui/filterSidebar';
|
||||||
import FilterGroup from '@/components/ui/filterGroup';
|
|
||||||
import { worldStateMixin } from '@/mixins/worldState';
|
import { worldStateMixin } from '@/mixins/worldState';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
SelectTranslatedArray,
|
|
||||||
FilterGroup,
|
|
||||||
FilterSidebar,
|
FilterSidebar,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
PinBadge,
|
PinBadge,
|
||||||
|
|
@ -407,13 +382,14 @@ export default {
|
||||||
eyewear: i18n.t('eyewear'),
|
eyewear: i18n.t('eyewear'),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
sortItemsBy: ['AZ'],
|
|
||||||
selectedSortItemsBy: 'AZ',
|
|
||||||
|
|
||||||
hidePinned: false,
|
hidePinned: false,
|
||||||
featuredGearBought: false,
|
featuredGearBought: false,
|
||||||
currentEvent: null,
|
currentEvent: null,
|
||||||
backgroundUpdate: new Date(),
|
backgroundUpdate: new Date(),
|
||||||
|
imageURLs: {
|
||||||
|
background: '',
|
||||||
|
npc: '',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -484,29 +460,16 @@ export default {
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
|
|
||||||
anyFilterSelected () {
|
anyFilterSelected () {
|
||||||
return Object.values(this.viewOptions).some(g => g.selected);
|
return Object.values(this.viewOptions).some(g => g.selected);
|
||||||
},
|
},
|
||||||
imageURLs () {
|
|
||||||
if (!this.seasonal.opened || !this.currentEvent || !this.currentEvent.season) {
|
|
||||||
return {
|
|
||||||
background: 'url(/static/npc/normal/seasonal_shop_closed_background.png)',
|
|
||||||
npc: 'url(/static/npc/normal/seasonal_shop_closed_npc.png)',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
background: `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_background.png)`,
|
|
||||||
npc: `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_npc.png)`,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
searchText: _throttle(function throttleSearch () {
|
searchText: _throttle(function throttleSearch () {
|
||||||
this.searchTextThrottled = this.searchText.toLowerCase();
|
this.searchTextThrottled = this.searchText.toLowerCase();
|
||||||
}, 250),
|
}, 250),
|
||||||
},
|
},
|
||||||
mounted () {
|
async mounted () {
|
||||||
this.$store.dispatch('common:setTitle', {
|
this.$store.dispatch('common:setTitle', {
|
||||||
subSection: this.$t('seasonalShop'),
|
subSection: this.$t('seasonalShop'),
|
||||||
section: this.$t('shops'),
|
section: this.$t('shops'),
|
||||||
|
|
@ -516,8 +479,10 @@ export default {
|
||||||
this.backgroundUpdate = new Date();
|
this.backgroundUpdate = new Date();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.triggerGetWorldState();
|
await this.triggerGetWorldState();
|
||||||
this.currentEvent = _find(this.currentEventList, event => Boolean(['winter', 'spring', 'summer', 'fall'].includes(event.season)));
|
this.currentEvent = _find(this.currentEventList, event => Boolean(['winter', 'spring', 'summer', 'fall'].includes(event.season)));
|
||||||
|
this.imageURLs.background = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_background.png)`;
|
||||||
|
this.imageURLs.npc = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_npc.png)`;
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
this.$root.$off('buyModal::boughtItem');
|
this.$root.$off('buyModal::boughtItem');
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
:emptyItem="emptyItem"
|
:emptyItem="emptyItem"
|
||||||
></slot>
|
></slot>
|
||||||
<span
|
<span
|
||||||
v-if="item.event && item.owned == null && showEventBadge"
|
v-if="item.end && item.owned == null && showEventBadge"
|
||||||
class="badge badge-round badge-item badge-clock"
|
class="badge badge-round badge-item badge-clock"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|
@ -114,7 +114,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="item.event && item.purchaseType !== 'quests'"
|
v-if="item.end && item.purchaseType !== 'quests'"
|
||||||
:class="item.purchaseType === 'gear' ? 'mt-4' : 'mt-2'"
|
:class="item.purchaseType === 'gear' ? 'mt-4' : 'mt-2'"
|
||||||
>
|
>
|
||||||
{{ limitedString }}
|
{{ limitedString }}
|
||||||
|
|
@ -142,6 +142,22 @@
|
||||||
&.locked .price {
|
&.locked .price {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hair, .facial-hair, .shirt, .skin {
|
||||||
|
height: 68px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hair {
|
||||||
|
background-position: -24px -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.facial-hair, .skin {
|
||||||
|
background-position: -24px -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shirt {
|
||||||
|
background-position: -23px -32px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
|
|
@ -149,11 +165,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.price {
|
.price {
|
||||||
height: 1.75rem;
|
border-radius: 0px 0px 4px 4px;
|
||||||
width: 94px;
|
font-size: 0.75rem;
|
||||||
|
line-height: 1;
|
||||||
margin-left: -1px;
|
margin-left: -1px;
|
||||||
margin-right: -1px;
|
margin-right: -1px;
|
||||||
border-radius: 0px 0px 4px 4px;
|
padding: 0.375rem 0;
|
||||||
|
|
||||||
&.gems {
|
&.gems {
|
||||||
background-color: rgba($green-100, 0.15);
|
background-color: rgba($green-100, 0.15);
|
||||||
|
|
@ -174,9 +191,7 @@
|
||||||
|
|
||||||
.price-label {
|
.price-label {
|
||||||
font-family: Roboto;
|
font-family: Roboto;
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 1.33;
|
|
||||||
|
|
||||||
&.gems {
|
&.gems {
|
||||||
color: $green-1;
|
color: $green-1;
|
||||||
|
|
@ -351,6 +366,7 @@ export default {
|
||||||
this.$emit('click', {});
|
this.$emit('click', {});
|
||||||
},
|
},
|
||||||
blur () {
|
blur () {
|
||||||
|
if (!this.$refs?.popover) return;
|
||||||
this.$refs.popover.$emit('enable');
|
this.$refs.popover.$emit('enable');
|
||||||
},
|
},
|
||||||
getPrice () {
|
getPrice () {
|
||||||
|
|
@ -370,14 +386,14 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
countdownString () {
|
countdownString () {
|
||||||
if (!this.item.event) return;
|
if (!this.item.end) return;
|
||||||
const diffDuration = moment.duration(moment(this.item.event.end).diff(moment()));
|
const diffDuration = moment.duration(moment(this.item.end).diff(moment()));
|
||||||
|
|
||||||
if (diffDuration.asSeconds() <= 0) {
|
if (diffDuration.asSeconds() <= 0) {
|
||||||
this.limitedString = this.$t('noLongerAvailable');
|
this.limitedString = this.$t('noLongerAvailable');
|
||||||
} else if (diffDuration.days() > 0 || diffDuration.months() > 0) {
|
} else if (diffDuration.days() > 0 || diffDuration.months() > 0) {
|
||||||
this.limitedString = this.$t('limitedAvailabilityDays', {
|
this.limitedString = this.$t('limitedAvailabilityDays', {
|
||||||
days: moment(this.item.event.end).diff(moment(), 'days'),
|
days: moment(this.item.end).diff(moment(), 'days'),
|
||||||
hours: diffDuration.hours(),
|
hours: diffDuration.hours(),
|
||||||
minutes: diffDuration.minutes(),
|
minutes: diffDuration.minutes(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="row timeTravelers">
|
<div class="row timeTravelers">
|
||||||
<div
|
<div
|
||||||
v-if="!closed"
|
|
||||||
class="standard-sidebar d-none d-sm-block"
|
class="standard-sidebar d-none d-sm-block"
|
||||||
>
|
>
|
||||||
<filter-sidebar>
|
<filter-sidebar>
|
||||||
|
|
@ -69,26 +68,34 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div><div
|
</div>
|
||||||
v-if="!closed"
|
<div class="d-flex justify-content-between w-items">
|
||||||
class="clearfix"
|
<h1
|
||||||
>
|
v-once
|
||||||
<div class="float-right">
|
class="page-header mt-4 mb-4"
|
||||||
<span class="dropdown-label">{{ $t('sortBy') }}</span>
|
>
|
||||||
<select-translated-array
|
{{ $t('timeTravelers') }}
|
||||||
:right="true"
|
</h1>
|
||||||
:value="selectedSortItemsBy"
|
<div
|
||||||
:items="sortItemsBy"
|
class="clearfix mt-4"
|
||||||
:inline-dropdown="false"
|
>
|
||||||
class="inline"
|
<div class="float-right">
|
||||||
@select="selectedSortItemsBy = $event"
|
<span class="dropdown-label">{{ $t('sortBy') }}</span>
|
||||||
/>
|
<select-translated-array
|
||||||
|
:right="true"
|
||||||
|
:value="selectedSortItemsBy"
|
||||||
|
:items="sortItemsBy"
|
||||||
|
:inline-dropdown="false"
|
||||||
|
class="inline"
|
||||||
|
@select="selectedSortItemsBy = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
||||||
<div
|
<div
|
||||||
v-for="category in categories"
|
v-for="category in categories"
|
||||||
v-if="!anyFilterSelected || (!closed && viewOptions[category.identifier].selected)"
|
v-if="!anyFilterSelected || viewOptions[category.identifier].selected"
|
||||||
:key="category.identifier"
|
:key="category.identifier"
|
||||||
:class="category.identifier"
|
:class="category.identifier"
|
||||||
>
|
>
|
||||||
|
|
@ -100,6 +107,9 @@
|
||||||
:item-width="94"
|
:item-width="94"
|
||||||
:item-margin="24"
|
:item-margin="24"
|
||||||
:type="category.identifier"
|
:type="category.identifier"
|
||||||
|
:fold-button="false"
|
||||||
|
:no-items-label="$t('allEquipmentOwned')"
|
||||||
|
:click-handler="false"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
slot="item"
|
slot="item"
|
||||||
|
|
@ -112,34 +122,7 @@
|
||||||
:price-type="ctx.item.currency"
|
:price-type="ctx.item.currency"
|
||||||
:empty-item="false"
|
:empty-item="false"
|
||||||
@click="selectItemToBuy(ctx.item)"
|
@click="selectItemToBuy(ctx.item)"
|
||||||
>
|
/>
|
||||||
<span
|
|
||||||
v-if="category !== 'quests'"
|
|
||||||
slot="popoverContent"
|
|
||||||
slot-scope="ctx"
|
|
||||||
><div><h4 class="popover-content-title">{{ ctx.item.text }}</h4></div></span>
|
|
||||||
<span
|
|
||||||
v-if="category === 'quests'"
|
|
||||||
slot="popoverContent"
|
|
||||||
><div class="questPopover">
|
|
||||||
<h4 class="popover-content-title">{{ item.text }}</h4>
|
|
||||||
<questInfo :quest="item" />
|
|
||||||
</div></span>
|
|
||||||
<template
|
|
||||||
slot="itemBadge"
|
|
||||||
slot-scope="ctx"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="ctx.item.pinType !== 'IGNORE'"
|
|
||||||
class="badge-top"
|
|
||||||
@click.prevent.stop="togglePinned(ctx.item)"
|
|
||||||
>
|
|
||||||
<pin-badge
|
|
||||||
:pinned="ctx.item.pinned"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</shopItem>
|
|
||||||
</template>
|
</template>
|
||||||
</itemRows>
|
</itemRows>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -164,108 +147,14 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- eslint-disable max-len -->
|
<style lang="scss" scoped>
|
||||||
<style lang="scss">
|
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '~@/assets/scss/colors.scss';
|
||||||
|
@import '~@/assets/scss/shops.scss';
|
||||||
|
|
||||||
// these styles may be applied to other pages too
|
.w-items {
|
||||||
|
max-width: 920px;
|
||||||
.featured-label {
|
|
||||||
margin: 24px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group {
|
|
||||||
display: inline-block;
|
|
||||||
width: 33%;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
.items {
|
|
||||||
border-radius: 2px;
|
|
||||||
background-color: #edecee;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-wrapper {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.items > div:not(:last-of-type) {
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeTravelers {
|
|
||||||
.standard-page {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-pin:not(.pinned) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item:hover .badge-pin {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
cursor: default;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featuredItems {
|
|
||||||
height: 216px;
|
|
||||||
|
|
||||||
.background {
|
|
||||||
background-repeat: repeat-x;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.background-open {
|
|
||||||
height: 188px;
|
|
||||||
}
|
|
||||||
.background-closed {
|
|
||||||
height: 216px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.npc {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 216px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
|
|
||||||
&.closed {
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-label {
|
|
||||||
position: absolute;
|
|
||||||
bottom: -14px;
|
|
||||||
margin: 0;
|
|
||||||
left: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<!-- eslint-enable max-len -->
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import _filter from 'lodash/filter';
|
import _filter from 'lodash/filter';
|
||||||
|
|
@ -281,8 +170,6 @@ import { mapState } from '@/libs/store';
|
||||||
import ShopItem from '../shopItem';
|
import ShopItem from '../shopItem';
|
||||||
import Item from '@/components/inventory/item';
|
import Item from '@/components/inventory/item';
|
||||||
import ItemRows from '@/components/ui/itemRows';
|
import ItemRows from '@/components/ui/itemRows';
|
||||||
import QuestInfo from '../quests/questInfo.vue';
|
|
||||||
import PinBadge from '@/components/ui/pinBadge';
|
|
||||||
import toggleSwitch from '@/components/ui/toggleSwitch';
|
import toggleSwitch from '@/components/ui/toggleSwitch';
|
||||||
|
|
||||||
import BuyQuestModal from '../quests/buyQuestModal.vue';
|
import BuyQuestModal from '../quests/buyQuestModal.vue';
|
||||||
|
|
@ -304,9 +191,7 @@ export default {
|
||||||
ShopItem,
|
ShopItem,
|
||||||
Item,
|
Item,
|
||||||
ItemRows,
|
ItemRows,
|
||||||
PinBadge,
|
|
||||||
toggleSwitch,
|
toggleSwitch,
|
||||||
QuestInfo,
|
|
||||||
|
|
||||||
BuyQuestModal,
|
BuyQuestModal,
|
||||||
},
|
},
|
||||||
|
|
@ -332,6 +217,11 @@ export default {
|
||||||
backgroundUpdate: new Date(),
|
backgroundUpdate: new Date(),
|
||||||
|
|
||||||
currentEvent: null,
|
currentEvent: null,
|
||||||
|
|
||||||
|
imageURLs: {
|
||||||
|
background: '',
|
||||||
|
npc: '',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -392,19 +282,6 @@ export default {
|
||||||
anyFilterSelected () {
|
anyFilterSelected () {
|
||||||
return Object.values(this.viewOptions).some(g => g.selected);
|
return Object.values(this.viewOptions).some(g => g.selected);
|
||||||
},
|
},
|
||||||
imageURLs () {
|
|
||||||
if (!this.currentEvent || !this.currentEvent.season || this.currentEvent.season === 'thanksgiving') {
|
|
||||||
return {
|
|
||||||
background: 'url(/static/npc/normal/time_travelers_background.png)',
|
|
||||||
npc: this.closed ? 'url(/static/npc/normal/time_travelers_closed_banner.png)'
|
|
||||||
: 'url(/static/npc/normal/time_travelers_open_banner.png)',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
background: `url(/static/npc/${this.currentEvent.season}/time_travelers_background.png)`,
|
|
||||||
npc: `url(/static/npc/${this.currentEvent.season}/time_travelers_open_banner.png)`,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
searchText: _throttle(function throttleSearch () {
|
searchText: _throttle(function throttleSearch () {
|
||||||
|
|
@ -425,6 +302,14 @@ export default {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.currentEvent = _find(this.currentEventList, event => Boolean(['winter', 'spring', 'summer', 'fall'].includes(event.season)));
|
this.currentEvent = _find(this.currentEventList, event => Boolean(['winter', 'spring', 'summer', 'fall'].includes(event.season)));
|
||||||
|
if (!this.currentEvent || !this.currentEvent.season || this.currentEvent.season === 'thanksgiving' || this.closed) {
|
||||||
|
this.imageURLs.background = 'url(/static/npc/normal/time_travelers_background.png)';
|
||||||
|
this.imageURLs.npc = this.closed ? 'url(/static/npc/normal/time_travelers_closed_banner.png)'
|
||||||
|
: 'url(/static/npc/normal/time_travelers_open_banner.png)';
|
||||||
|
} else {
|
||||||
|
this.imageURLs.background = `url(/static/npc/${this.currentEvent.season}/time_travelers_background.png)`;
|
||||||
|
this.imageURLs.npc = `url(/static/npc/${this.currentEvent.season}/time_travelers_open_banner.png)`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
this.$root.$off('buyModal::boughtItem');
|
this.$root.$off('buyModal::boughtItem');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-once class="top-container mx-auto">
|
<div
|
||||||
|
v-once
|
||||||
|
class="top-container mx-auto"
|
||||||
|
>
|
||||||
<div class="main-text mr-4">
|
<div class="main-text mr-4">
|
||||||
<div class="title-details">
|
<div class="title-details">
|
||||||
<h1>{{ $t('contentFaqTitle') }}</h1>
|
<h1>{{ $t('contentFaqTitle') }}</h1>
|
||||||
|
|
@ -23,7 +26,8 @@
|
||||||
</ul>
|
</ul>
|
||||||
<h3>{{ $t('contentQuestion2') }}</h3>
|
<h3>{{ $t('contentQuestion2') }}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>{{ $t('contentAnswer20') }}
|
<li>
|
||||||
|
{{ $t('contentAnswer20') }}
|
||||||
<ul>
|
<ul>
|
||||||
<li v-html="$t('contentAnswer200')"></li>
|
<li v-html="$t('contentAnswer200')"></li>
|
||||||
<li v-html="$t('contentAnswer201')"></li>
|
<li v-html="$t('contentAnswer201')"></li>
|
||||||
|
|
@ -54,7 +58,8 @@
|
||||||
<p>{{ $t('contentAnswer410') }}</p>
|
<p>{{ $t('contentAnswer410') }}</p>
|
||||||
<h3>{{ $t('contentQuestion5') }}</h3>
|
<h3>{{ $t('contentQuestion5') }}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>{{ $t('contentAnswer50') }}
|
<li>
|
||||||
|
{{ $t('contentAnswer50') }}
|
||||||
<ul>
|
<ul>
|
||||||
<li>{{ $t('backgrounds') }}</li>
|
<li>{{ $t('backgrounds') }}</li>
|
||||||
<li>{{ $t('contentAnswer501') }}</li>
|
<li>{{ $t('contentAnswer501') }}</li>
|
||||||
|
|
@ -78,9 +83,11 @@
|
||||||
<li>{{ $t('contentAnswer70') }}</li>
|
<li>{{ $t('contentAnswer70') }}</li>
|
||||||
<li>{{ $t('contentAnswer71') }}</li>
|
<li>{{ $t('contentAnswer71') }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p v-html="$t('contentFaqPara3',
|
<p
|
||||||
{ mailto: '<a href=mailto:admin@habitica.com>admin@habitica.com</a>'}
|
v-html="$t('contentFaqPara3',
|
||||||
)"></p>
|
{ mailto: '<a href=mailto:admin@habitica.com>admin@habitica.com</a>'}
|
||||||
|
)"
|
||||||
|
></p>
|
||||||
</div>
|
</div>
|
||||||
<faq-sidebar />
|
<faq-sidebar />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:value="selected"
|
:value="selected"
|
||||||
:hide-icon="true"
|
:hide-icon="true"
|
||||||
|
:direct-select="true"
|
||||||
@select="$emit('select', $event.value)"
|
@select="$emit('select', $event.value)"
|
||||||
>
|
>
|
||||||
<template #item="{ item, button }">
|
<template #item="{ item, button }">
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
:hide-icon="false"
|
:hide-icon="false"
|
||||||
:inline-dropdown="inlineDropdown"
|
:inline-dropdown="inlineDropdown"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
|
:direct-select="true"
|
||||||
@select="selectItem($event)"
|
@select="selectItem($event)"
|
||||||
>
|
>
|
||||||
<template #item="{ item }">
|
<template #item="{ item }">
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue