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
|
||||
*.swp
|
||||
.idea*
|
||||
config.json
|
||||
config*.json
|
||||
npm-debug.log*
|
||||
lib
|
||||
newrelic_agent.log
|
||||
|
|
@ -48,3 +48,4 @@ webpack.webstorm.config
|
|||
# mongodb replica set for local dev
|
||||
mongodb-*.tgz
|
||||
/mongodb-data
|
||||
/.nyc_output
|
||||
|
|
|
|||
|
|
@ -89,5 +89,8 @@
|
|||
"REDIS_HOST": "aaabbbcccdddeeefff",
|
||||
"REDIS_PORT": "1234",
|
||||
"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));
|
||||
}
|
||||
|
||||
function integrationTestCommand (testDir, coverageDir) {
|
||||
return `istanbul cover --dir coverage/${coverageDir} --report lcovonly node_modules/mocha/bin/_mocha -- ${testDir} --recursive --require ./test/helpers/start-server`;
|
||||
function integrationTestCommand (testDir) {
|
||||
return `nyc --silent --no-clean mocha ${testDir} --recursive --require ./test/helpers/start-server`;
|
||||
}
|
||||
|
||||
/* Test task definitions */
|
||||
|
|
@ -148,7 +148,7 @@ gulp.task('test:content:safe', gulp.series('test:prepare:build', cb => {
|
|||
|
||||
gulp.task(
|
||||
'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())));
|
||||
|
|
@ -156,7 +156,7 @@ gulp.task('test:api:unit:watch', () => gulp.watch(['website/server/libs/*', 'tes
|
|||
gulp.task('test:api-v3:integration', gulp.series(
|
||||
'test:prepare:mongo',
|
||||
runInChildProcess(
|
||||
integrationTestCommand('test/api/v3/integration', 'coverage/api-v3-integration'),
|
||||
integrationTestCommand('test/api/v3/integration'),
|
||||
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(
|
||||
'test:prepare:mongo',
|
||||
runInChildProcess(
|
||||
integrationTestCommand('test/api/v4', 'api-v4-integration'),
|
||||
integrationTestCommand('test/api/v4'),
|
||||
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",
|
||||
"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",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
|
|
@ -67,6 +67,7 @@
|
|||
"remove-markdown": "^0.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^4.2.2",
|
||||
"sinon": "^15.2.0",
|
||||
"stripe": "^12.18.0",
|
||||
"superagent": "^8.1.2",
|
||||
"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-v4:integration": "gulp test:api-v4:integration",
|
||||
"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:common": "istanbul cover --dir coverage/common --report lcovonly node_modules/mocha/bin/_mocha -- test/common --recursive",
|
||||
"test:content": "istanbul cover --dir coverage/content --report lcovonly node_modules/mocha/bin/_mocha -- test/content --recursive",
|
||||
"test:sanity": "nyc --silent --no-clean mocha test/sanity --recursive",
|
||||
"test:common": "nyc --silent --no-clean mocha test/common --recursive",
|
||||
"test:content": "nyc --silent --no-clean mocha test/content --recursive",
|
||||
"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",
|
||||
"client:dev": "cd website/client && npm run serve",
|
||||
"client:build": "cd website/client && npm run build",
|
||||
|
|
@ -115,13 +116,11 @@
|
|||
"chai-moment": "^0.1.0",
|
||||
"chalk": "^5.3.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"expect.js": "^0.3.1",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
"mocha": "^5.1.1",
|
||||
"monk": "^7.3.4",
|
||||
"nyc": "^15.1.0",
|
||||
"require-again": "^2.0.0",
|
||||
"run-rs": "^0.7.7",
|
||||
"sinon": "^15.2.0",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"sinon-stub-promise": "^4.0.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import fs from 'fs';
|
||||
import * as contentLib from '../../../../website/server/libs/content';
|
||||
import content from '../../../../website/common/script/content';
|
||||
import {
|
||||
generateRes,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
|
||||
describe('contentLib', () => {
|
||||
describe('CONTENT_CACHE_PATH', () => {
|
||||
|
|
@ -13,5 +17,90 @@ describe('contentLib', () => {
|
|||
contentLib.getLocalizedContentResponse();
|
||||
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', () => {
|
||||
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', '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', 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.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', () => {
|
||||
let userToGetHourGlass;
|
||||
let nconfStub;
|
||||
|
||||
before(async () => {
|
||||
userToGetHourGlass = await generateUser();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
nconf.set('IS_PROD', false);
|
||||
beforeEach(() => {
|
||||
nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nconfStub.restore();
|
||||
});
|
||||
|
||||
it('adds Hourglass to the current user', async () => {
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||
await userToGetHourGlass.post('/debug/add-hourglass');
|
||||
|
||||
const userWithHourGlass = await userToGetHourGlass.get('/user');
|
||||
|
|
@ -23,7 +30,7 @@ describe('POST /debug/add-hourglass', () => {
|
|||
});
|
||||
|
||||
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'))
|
||||
.eventually.be.rejected.and.to.deep.equal({
|
||||
|
|
|
|||
|
|
@ -5,16 +5,23 @@ import {
|
|||
|
||||
describe('POST /debug/add-ten-gems', () => {
|
||||
let userToGainTenGems;
|
||||
let nconfStub;
|
||||
|
||||
before(async () => {
|
||||
userToGainTenGems = await generateUser();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
nconf.set('IS_PROD', false);
|
||||
beforeEach(() => {
|
||||
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 () => {
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||
await userToGainTenGems.post('/debug/add-ten-gems');
|
||||
|
||||
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 () => {
|
||||
nconf.set('IS_PROD', true);
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||
|
||||
await expect(userToGainTenGems.post('/debug/add-ten-gems'))
|
||||
.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', () => {
|
||||
let user;
|
||||
let nconfStub;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nconf.set('IS_PROD', false);
|
||||
nconfStub.restore();
|
||||
});
|
||||
|
||||
it('makes user an admin', async () => {
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||
await user.post('/debug/make-admin');
|
||||
|
||||
await user.sync();
|
||||
|
|
@ -23,7 +30,7 @@ describe('POST /debug/make-admin', () => {
|
|||
});
|
||||
|
||||
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'))
|
||||
.eventually.be.rejected.and.to.deep.equal({
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
describe('POST /debug/modify-inventory', () => {
|
||||
let user; let
|
||||
originalItems;
|
||||
let nconfStub;
|
||||
|
||||
before(async () => {
|
||||
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(() => {
|
||||
nconf.set('IS_PROD', false);
|
||||
nconfStub.restore();
|
||||
});
|
||||
|
||||
it('sets equipment', async () => {
|
||||
|
|
@ -149,7 +156,7 @@ describe('POST /debug/modify-inventory', () => {
|
|||
});
|
||||
|
||||
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'))
|
||||
.eventually.be.rejected.and.to.deep.equal({
|
||||
|
|
|
|||
|
|
@ -5,13 +5,20 @@ import {
|
|||
|
||||
describe('POST /debug/quest-progress', () => {
|
||||
let user;
|
||||
let nconfStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nconf.set('IS_PROD', false);
|
||||
nconfStub.restore();
|
||||
});
|
||||
|
||||
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 () => {
|
||||
nconf.set('IS_PROD', true);
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||
|
||||
await expect(user.post('/debug/quest-progress'))
|
||||
.eventually.be.rejected.and.to.deep.equal({
|
||||
|
|
|
|||
|
|
@ -5,13 +5,20 @@ import {
|
|||
|
||||
describe('POST /debug/set-cron', () => {
|
||||
let user;
|
||||
let nconfStub;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nconf.set('IS_PROD', false);
|
||||
nconfStub.restore();
|
||||
});
|
||||
|
||||
it('sets last cron', async () => {
|
||||
|
|
@ -27,7 +34,7 @@ describe('POST /debug/set-cron', () => {
|
|||
});
|
||||
|
||||
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'))
|
||||
.eventually.be.rejected.and.to.deep.equal({
|
||||
|
|
|
|||
|
|
@ -17,9 +17,5 @@ describe('GET /shops/backgrounds', () => {
|
|||
expect(shop.notes).to.eql(t('backgroundShop'));
|
||||
expect(shop.imageName).to.equal('background_shop');
|
||||
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', () => {
|
||||
let user;
|
||||
let clock;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-08'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const oldBalance = user.balance;
|
||||
await user.updateOne({
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
|
||||
describe('POST /user/unlock', () => {
|
||||
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 unlockCost = 1.25;
|
||||
const usersStartingGems = 5;
|
||||
|
|
|
|||
|
|
@ -274,6 +274,14 @@ describe('PUT /user', () => {
|
|||
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', () => {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const { content } = shared;
|
|||
|
||||
describe('POST /user/buy/:key', () => {
|
||||
let user;
|
||||
let clock;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
|
|
@ -18,6 +19,12 @@ describe('POST /user/buy/:key', () => {
|
|||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
// More tests in common code unit tests
|
||||
|
||||
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 () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-10-31T00:00:00Z'));
|
||||
const key = 'spookySparkles';
|
||||
const item = content.special[key];
|
||||
const stub = sinon.stub(item, 'canOwn').returns(true);
|
||||
|
||||
await user.updateOne({ 'stats.gp': 250 });
|
||||
const res = await user.post(`/user/buy/${key}`);
|
||||
|
|
@ -83,8 +90,6 @@ describe('POST /user/buy/:key', () => {
|
|||
expect(res.message).to.equal(t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
|
||||
stub.restore();
|
||||
});
|
||||
|
||||
it('allows for bulk purchases', async () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { TAVERN_ID } from '../../../../../website/server/models/group';
|
||||
import { updateDocument } from '../../../../helpers/mongo';
|
||||
import {
|
||||
requester,
|
||||
resetHabiticaDB,
|
||||
|
|
@ -18,7 +16,9 @@ describe('GET /world-state', () => {
|
|||
});
|
||||
|
||||
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');
|
||||
expect(res).to.have.nested.property('worldBoss');
|
||||
|
|
@ -33,15 +33,29 @@ describe('GET /world-state', () => {
|
|||
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', () => {
|
||||
beforeEach(async () => {
|
||||
sinon.stub(worldState, 'getCurrentEvent').returns(null);
|
||||
sinon.stub(worldState, 'getCurrentEventList').returns([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
worldState.getCurrentEvent.restore();
|
||||
worldState.getCurrentEventList.restore();
|
||||
});
|
||||
|
||||
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 = {
|
||||
...common.content.events.fall2020,
|
||||
event: 'fall2020',
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
sinon.stub(worldState, 'getCurrentEvent').returns(evt);
|
||||
sinon.stub(worldState, 'getCurrentEventList').returns([evt]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
worldState.getCurrentEvent.restore();
|
||||
worldState.getCurrentEventList.restore();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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', () => {
|
||||
it('checks if canOwn has the same id', () => {
|
||||
Object.keys(armoireSet).forEach(type => {
|
||||
Object.keys(armoireSet[type]).forEach(itemKey => {
|
||||
const ownedKey = `${type}_armoire_${itemKey}`;
|
||||
|
||||
expect(armoireSet[type][itemKey].canOwn({
|
||||
items: {
|
||||
gear: {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ describe('shared.ops.buy', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('recovers 15 hp', async () => {
|
||||
it('buys health potion', async () => {
|
||||
user.stats.hp = 30;
|
||||
await buy(user, { params: { key: 'potion' } }, analytics);
|
||||
expect(user.stats.hp).to.eql(45);
|
||||
|
|
@ -17,9 +17,7 @@ function getFullArmoire () {
|
|||
|
||||
_.each(content.gearTypes, type => {
|
||||
_.each(content.gear.tree[type].armoire, gearObject => {
|
||||
if (gearObject.released) {
|
||||
fullArmoire[gearObject.key] = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -22,6 +22,7 @@ async function buyGear (user, req, analytics) {
|
|||
describe('shared.ops.buyMarketGear', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser({
|
||||
|
|
@ -54,6 +55,10 @@ describe('shared.ops.buyMarketGear', () => {
|
|||
shared.fns.predictableRandom.restore();
|
||||
shared.onboarding.checkOnboardingStatus.restore();
|
||||
analytics.track.restore();
|
||||
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
context('Gear', () => {
|
||||
|
|
@ -184,30 +189,28 @@ describe('shared.ops.buyMarketGear', () => {
|
|||
});
|
||||
|
||||
// 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.preferences.autoEquip = true;
|
||||
await buyGear(user, { params: { key: 'shield_warrior_1' } });
|
||||
user.ops.equip({ params: { key: 'shield_warrior_1' } });
|
||||
await buyGear(user, { params: { key: 'weapon_warrior_1' } });
|
||||
user.ops.equip({ params: { key: 'weapon_warrior_1' } });
|
||||
user.items.gear.equipped.weapon = 'weapon_warrior_1';
|
||||
user.items.gear.equipped.shield = 'shield_warrior_1';
|
||||
user.stats.class = 'wizard';
|
||||
|
||||
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('weapon', 'weapon_wizard_1');
|
||||
expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_wizard_0');
|
||||
});
|
||||
|
||||
// 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.preferences.autoEquip = false;
|
||||
await buyGear(user, { params: { key: 'shield_warrior_1' } });
|
||||
user.ops.equip({ params: { key: 'shield_warrior_1' } });
|
||||
await buyGear(user, { params: { key: 'weapon_warrior_1' } });
|
||||
user.ops.equip({ params: { key: 'weapon_warrior_1' } });
|
||||
user.items.gear.equipped.weapon = 'weapon_warrior_1';
|
||||
user.items.gear.equipped.shield = 'shield_warrior_1';
|
||||
user.stats.class = 'wizard';
|
||||
|
||||
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('weapon', 'weapon_warrior_1');
|
||||
|
|
@ -283,5 +286,40 @@ describe('shared.ops.buyMarketGear', () => {
|
|||
|
||||
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', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser({
|
||||
|
|
@ -31,6 +32,9 @@ describe('shared.ops.buyMysterySet', () => {
|
|||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
context('Mystery Sets', () => {
|
||||
|
|
@ -41,7 +45,7 @@ describe('shared.ops.buyMysterySet', () => {
|
|||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
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'));
|
||||
}
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
|
@ -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('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', () => {
|
||||
let user;
|
||||
let clock;
|
||||
const goldPoints = 40;
|
||||
const analytics = { track () {} };
|
||||
|
||||
|
|
@ -26,11 +27,13 @@ describe('shared.ops.buyQuestGems', () => {
|
|||
beforeEach(() => {
|
||||
sinon.stub(analytics, 'track');
|
||||
sinon.spy(pinnedGearUtils, 'removeItemByPath');
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-16'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
pinnedGearUtils.removeItemByPath.restore();
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
context('single purchase', () => {
|
||||
|
|
@ -44,7 +47,7 @@ describe('shared.ops.buyQuestGems', () => {
|
|||
user.pinnedItems.push({ type: 'quests', key: 'gryphon' });
|
||||
});
|
||||
|
||||
it('successfully purchases quest', async () => {
|
||||
it('successfully purchases pet quest', async () => {
|
||||
const key = 'gryphon';
|
||||
|
||||
await buyQuest(user, { params: { key } });
|
||||
|
|
@ -52,6 +55,61 @@ describe('shared.ops.buyQuestGems', () => {
|
|||
expect(user.items.quests[key]).to.equal(1);
|
||||
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 () => {
|
||||
const key = 'dustbunnies';
|
||||
user.items.quests[key] = -1;
|
||||
|
|
@ -61,6 +119,7 @@ describe('shared.ops.buyQuestGems', () => {
|
|||
expect(user.items.quests[key]).to.equal(1);
|
||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||
});
|
||||
|
||||
it('errors if the user has not completed prerequisite quests', async () => {
|
||||
const key = 'atom3';
|
||||
user.achievements.quests.atom1 = 1;
|
||||
|
|
@ -73,6 +132,7 @@ describe('shared.ops.buyQuestGems', () => {
|
|||
expect(user.items.quests[key]).to.eql(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
it('successfully purchases quest if user has completed all prerequisite quests', async () => {
|
||||
const key = 'atom3';
|
||||
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', () => {
|
||||
const SEASONAL_FOOD = moment().isBefore('2021-11-02T20:00-04:00') ? 'Candy_Base' : 'Meat';
|
||||
let user;
|
||||
let clock;
|
||||
const goldPoints = 40;
|
||||
const analytics = { track () {} };
|
||||
|
||||
|
|
@ -25,11 +26,13 @@ describe('shared.ops.purchase', () => {
|
|||
beforeEach(() => {
|
||||
sinon.stub(analytics, 'track');
|
||||
sinon.spy(pinnedGearUtils, 'removeItemByPath');
|
||||
clock = sandbox.useFakeTimers(new Date('2024-01-10'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
pinnedGearUtils.removeItemByPath.restore();
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
await purchase(user, { params: { type: 'gear', key: 'headAccessory_special_wolfEars' } });
|
||||
await purchase(user, { params: { type: 'gear', key: 'shield_special_winter2019Healer' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
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 () => {
|
||||
const params = { key: 'notExisting', type: 'food' };
|
||||
|
||||
|
|
@ -99,44 +166,6 @@ describe('shared.ops.purchase', () => {
|
|||
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', () => {
|
||||
|
|
@ -150,7 +179,7 @@ describe('shared.ops.purchase', () => {
|
|||
user.pinnedItems.push({ type: 'eggs', key: 'Wolf' });
|
||||
user.pinnedItems.push({ type: 'hatchingPotions', key: 'Base' });
|
||||
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' });
|
||||
});
|
||||
|
||||
|
|
@ -185,9 +214,9 @@ describe('shared.ops.purchase', () => {
|
|||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||
});
|
||||
|
||||
it('purchases gear', async () => {
|
||||
it('purchases past seasonal gear', async () => {
|
||||
const type = 'gear';
|
||||
const key = 'headAccessory_special_tigerEars';
|
||||
const key = 'shield_special_winter2019Healer';
|
||||
|
||||
await purchase(user, { params: { type, key } });
|
||||
|
||||
|
|
@ -195,9 +224,39 @@ describe('shared.ops.purchase', () => {
|
|||
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 () => {
|
||||
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 key = 'cuddleBuddies';
|
||||
const price = 1.75;
|
||||
|
|
@ -216,7 +275,6 @@ describe('shared.ops.purchase', () => {
|
|||
expect(user.balance).to.equal(startingBalance - price);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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 i18n from '../../../website/common/script/i18n';
|
||||
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', () => {
|
||||
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 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 facialHairUnlockPath = 'hair.mustache.1,hair.mustache.2,hair.beard.1,hair.beard.2,hair.beard.3';
|
||||
const usersStartingGems = 50 / 4;
|
||||
|
|
@ -17,6 +20,11 @@ describe('shared.ops.unlock', () => {
|
|||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
user.balance = usersStartingGems;
|
||||
clock = sandbox.useFakeTimers(new Date('2024-04-10'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
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 () => {
|
||||
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);
|
||||
});
|
||||
|
|
@ -95,7 +105,9 @@ describe('shared.ops.unlock', () => {
|
|||
|
||||
it('returns an error if gear is not from the animal set', async () => {
|
||||
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) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
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[5] } });
|
||||
await unlock(user, { query: { path: partialUnlockPaths[6] } });
|
||||
await unlock(user, { query: { path: partialUnlockPaths[7] } });
|
||||
|
||||
await unlock(user, { query: { path: unlockPath } });
|
||||
});
|
||||
|
|
@ -163,7 +174,9 @@ describe('shared.ops.unlock', () => {
|
|||
|
||||
await unlock(user, { query: { path: backgroundUnlockPath } });
|
||||
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(response.message).to.not.exist;
|
||||
|
|
@ -176,7 +189,9 @@ describe('shared.ops.unlock', () => {
|
|||
await unlock(user, { query: { path: backgroundUnlockPath } }); // unlock
|
||||
const afterBalance = user.balance;
|
||||
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(response.message).to.not.exist;
|
||||
|
|
@ -192,8 +207,9 @@ describe('shared.ops.unlock', () => {
|
|||
individualPaths.forEach(path => {
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
});
|
||||
expect(Object.keys(user.purchased.shirt).length)
|
||||
.to.equal(initialShirts + individualPaths.length);
|
||||
expect(Object.keys(user.purchased.shirt).length).to.equal(
|
||||
initialShirts + individualPaths.length,
|
||||
);
|
||||
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
||||
});
|
||||
|
||||
|
|
@ -208,8 +224,9 @@ describe('shared.ops.unlock', () => {
|
|||
individualPaths.forEach(path => {
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
});
|
||||
expect(Object.keys(user.purchased.hair.color).length)
|
||||
.to.equal(initialHairColors + individualPaths.length);
|
||||
expect(Object.keys(user.purchased.hair.color).length).to.equal(
|
||||
initialHairColors + individualPaths.length,
|
||||
);
|
||||
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 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'));
|
||||
const individualPaths = facialHairUnlockPath.split(',');
|
||||
individualPaths.forEach(path => {
|
||||
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);
|
||||
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
||||
});
|
||||
|
||||
it('unlocks a full set of gear', async () => {
|
||||
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'));
|
||||
|
||||
|
|
@ -241,32 +265,21 @@ describe('shared.ops.unlock', () => {
|
|||
individualPaths.forEach(path => {
|
||||
expect(get(user, path)).to.be.true;
|
||||
});
|
||||
expect(Object.keys(user.items.gear.owned).length)
|
||||
.to.equal(initialGear + individualPaths.length);
|
||||
expect(Object.keys(user.items.gear.owned).length).to.equal(
|
||||
initialGear + individualPaths.length,
|
||||
);
|
||||
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 () => {
|
||||
const path = unlockPath.split(',')[0];
|
||||
const initialShirts = Object.keys(user.purchased.shirt).length;
|
||||
const [, message] = await unlock(user, { query: { path } });
|
||||
|
||||
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(user.balance).to.equal(usersStartingGems - 0.5);
|
||||
});
|
||||
|
|
@ -279,7 +292,9 @@ describe('shared.ops.unlock', () => {
|
|||
const [, message] = await unlock(user, { query: { path } });
|
||||
|
||||
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(user.balance).to.equal(usersStartingGems - 0.5);
|
||||
});
|
||||
|
|
@ -295,8 +310,12 @@ describe('shared.ops.unlock', () => {
|
|||
|
||||
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.beard).length).to.equal(initialBeard);
|
||||
expect(Object.keys(user.purchased.hair.mustache).length).to.equal(
|
||||
initialMustache + 1,
|
||||
);
|
||||
expect(Object.keys(user.purchased.hair.beard).length).to.equal(
|
||||
initialBeard,
|
||||
);
|
||||
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
||||
|
|
@ -315,11 +334,24 @@ describe('shared.ops.unlock', () => {
|
|||
|
||||
it('unlocks an item (background)', async () => {
|
||||
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(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(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,
|
||||
} from '../helpers/content.helper';
|
||||
|
||||
import { CLASSES } from '../../website/common/script/content/constants';
|
||||
|
||||
import gearData from '../../website/common/script/content/gear';
|
||||
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';
|
||||
|
|
@ -17,35 +19,48 @@ describe('Gear', () => {
|
|||
context(`${klass} ${gearType}s`, () => {
|
||||
it('have a value of at least 0 for each stat', () => {
|
||||
each(items, gear => {
|
||||
expect(gear.con).to.be.at.least(0);
|
||||
expect(gear.int).to.be.at.least(0);
|
||||
expect(gear.per).to.be.at.least(0);
|
||||
expect(gear.str).to.be.at.least(0);
|
||||
expect(gear.con, gear.key).to.be.at.least(0);
|
||||
expect(gear.int, gear.key).to.be.at.least(0);
|
||||
expect(gear.per, gear.key).to.be.at.least(0);
|
||||
expect(gear.str, gear.key).to.be.at.least(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('have a purchase value of at least 0', () => {
|
||||
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', () => {
|
||||
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', () => {
|
||||
each(items, gear => {
|
||||
expectValidTranslationString(gear.text);
|
||||
expectValidTranslationString(gear.notes);
|
||||
expectValidTranslationString(gear.text, gear.key);
|
||||
expectValidTranslationString(gear.notes, gear.key);
|
||||
});
|
||||
});
|
||||
|
||||
it('has a set attribue', () => {
|
||||
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', () => {
|
||||
let user;
|
||||
|
||||
|
|
@ -5,22 +5,27 @@ import {
|
|||
expectValidTranslationString,
|
||||
} 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('all', () => {
|
||||
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;
|
||||
let clock;
|
||||
|
||||
expect(allNumber).to.be.greaterThan(0);
|
||||
expect(allNumber).to.equal(dropNumber + premiumNumber + wackyNumber);
|
||||
afterEach(() => {
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
const potionTypes = [
|
||||
'drops',
|
||||
'quests',
|
||||
'premium',
|
||||
'wacky',
|
||||
];
|
||||
potionTypes.forEach(potionType => {
|
||||
describe(potionType, () => {
|
||||
it('contains basic information about each potion', () => {
|
||||
each(hatchingPotions.all, (potion, key) => {
|
||||
each(all, (potion, key) => {
|
||||
expectValidTranslationString(potion.text);
|
||||
expectValidTranslationString(potion.notes);
|
||||
expect(potion.canBuy).to.be.a('function');
|
||||
|
|
@ -30,3 +35,4 @@ describe('hatchingPotions', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
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', () => {
|
||||
let user;
|
||||
let date;
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
});
|
||||
|
||||
describe('on january 15th', () => {
|
||||
beforeEach(() => {
|
||||
date = new Date('2024-01-15');
|
||||
});
|
||||
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_201602 = true; // eslint-disable-line camelcase
|
||||
expect(timeTravelers.timeTravelerStore(user)['201602']).to.not.exist;
|
||||
expect(timeTravelers.timeTravelerStore(user)['201603']).to.exist;
|
||||
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_201602'],
|
||||
mysteryItems: ['head_mystery_201601'],
|
||||
},
|
||||
};
|
||||
expect(timeTravelers.timeTravelerStore(user)['201602']).to.not.exist;
|
||||
expect(timeTravelers.timeTravelerStore(user)['201603']).to.exist;
|
||||
const items = timeTravelers.timeTravelerStore(user, date);
|
||||
expect(items['201601']).to.not.exist;
|
||||
expect(items['201607']).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe('on may 1st', () => {
|
||||
beforeEach(() => {
|
||||
date = new Date('2024-05-01');
|
||||
});
|
||||
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}(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(),
|
||||
render: sandbox.stub(),
|
||||
send: sandbox.stub(),
|
||||
sendFile: sandbox.stub(),
|
||||
sendStatus: sandbox.stub().returnsThis(),
|
||||
set: sandbox.stub(),
|
||||
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_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
|
||||
|
||||
export function expectValidTranslationString (attribute) {
|
||||
expect(attribute).to.be.a('function');
|
||||
export function expectValidTranslationString (attribute, contextKey) {
|
||||
expect(attribute, contextKey).to.be.a('function');
|
||||
|
||||
const translatedString = attribute();
|
||||
|
||||
expect(translatedString.trim()).to.not.be.empty;
|
||||
expect(translatedString).to.not.contain('function func(lang)');
|
||||
expect(translatedString).to.not.eql(STRING_ERROR_MSG);
|
||||
expect(translatedString).to.not.match(STRING_DOES_NOT_EXIST_MSG);
|
||||
expect(translatedString.trim(), contextKey).to.not.be.empty;
|
||||
expect(translatedString, contextKey).to.not.contain('function func(lang)');
|
||||
expect(translatedString, contextKey).to.not.eql(STRING_ERROR_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/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^8.21.3",
|
||||
"assert": "^2.1.0",
|
||||
"axios": "^0.28.0",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^4.6.0",
|
||||
"bootstrap-vue": "^2.23.1",
|
||||
"chai": "^5.1.0",
|
||||
"core-js": "^3.33.1",
|
||||
"dompurify": "^3.0.3",
|
||||
"eslint": "7.32.0",
|
||||
|
|
@ -30,16 +30,18 @@
|
|||
"eslint-plugin-vue": "7.20.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"hellojs": "^1.20.0",
|
||||
"inspectpack": "^4.7.1",
|
||||
"intro.js": "^7.2.0",
|
||||
"jquery": "^3.7.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"moment-locales-webpack-plugin": "^1.2.0",
|
||||
"nconf": "^0.12.1",
|
||||
"sass": "^1.63.4",
|
||||
"sass-loader": "^14.1.1",
|
||||
"sinon": "^17.0.1",
|
||||
"smartbanner.js": "^1.19.3",
|
||||
"stopword": "^2.0.8",
|
||||
"timers-browserify": "^2.0.12",
|
||||
"uuid": "^9.0.1",
|
||||
"validator": "^13.9.0",
|
||||
"vue": "^2.7.10",
|
||||
|
|
@ -49,11 +51,15 @@
|
|||
"vue-template-babel-compiler": "^2.0.0",
|
||||
"vue-template-compiler": "^2.7.10",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
|
||||
"webpack": "^5.89.0"
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0"
|
||||
},
|
||||
"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": {
|
||||
|
|
@ -2120,6 +2126,45 @@
|
|||
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
|
||||
"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": {
|
||||
"version": "1.8.1",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
|
|
@ -3683,9 +3741,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.28.0.tgz",
|
||||
"integrity": "sha512-Tu7NYoGY4Yoc7I+Npf9HhUMtEEpV7ZiLH9yndTCoNhcpBH0kwcvFbzYN9/u5QKI5A6uefjsNNWaz5olJVYS62Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.28.1.tgz",
|
||||
"integrity": "sha512-iUcGA5a7p0mVb4Gm/sy+FSECNkPFT4y7wt6OM/CDpO/OnNCvSs3PoMG8ibrC9jRoGYU0gUK5pXVC4NPXq6lHRQ==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
|
|
@ -3759,6 +3817,19 @@
|
|||
"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": {
|
||||
"version": "0.4.7",
|
||||
"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": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
|
||||
"integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.1",
|
||||
"set-function-length": "^1.1.1"
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"set-function-length": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
|
|
@ -4142,12 +4218,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.0.tgz",
|
||||
"integrity": "sha512-kDZ7MZyM6Q1DhR9jy7dalKohXQ2yrlXkk59CR52aRKxJrobmlBNqnFQxX9xOX8w+4mz8SYlKJa/7D7ddltFXCw==",
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
|
||||
"integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"assertion-error": "^2.0.1",
|
||||
"check-error": "^2.0.0",
|
||||
"check-error": "^2.1.1",
|
||||
"deep-eql": "^5.0.1",
|
||||
"loupe": "^3.1.0",
|
||||
"pathval": "^2.0.0"
|
||||
|
|
@ -4170,9 +4247,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/check-error": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.0.0.tgz",
|
||||
"integrity": "sha512-tjLAOBHKVxtPoHe/SA7kNOMvhCRdCJ3vETdeY0RuAc9popf+hyaSV6ZEg9hr4cpWF7jmo/JSWEnLDrnijS9Tog==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
||||
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
|
|
@ -5046,6 +5124,7 @@
|
|||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.1.tgz",
|
||||
"integrity": "sha512-nwQCf6ne2gez3o1MxWifqkciwt0zhl0LO1/UwVu4uMBuPmflWM4oQ70XMqHqnBJA+nhzncaqL9HVL6KkHJ28lw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
|
|
@ -5141,16 +5220,19 @@
|
|||
}
|
||||
},
|
||||
"node_modules/define-data-property": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
|
||||
"integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.1",
|
||||
"gopd": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.0"
|
||||
"es-define-property": "^1.0.0",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/define-lazy-prop": {
|
||||
|
|
@ -5521,6 +5603,25 @@
|
|||
"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": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz",
|
||||
|
|
@ -6929,7 +7030,8 @@
|
|||
"node_modules/fp-ts": {
|
||||
"version": "2.16.1",
|
||||
"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": {
|
||||
"version": "4.3.7",
|
||||
|
|
@ -6975,19 +7077,6 @@
|
|||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
|
|
@ -7046,20 +7135,25 @@
|
|||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
|
||||
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
|
||||
"integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"has-proto": "^1.0.1",
|
||||
"has-symbols": "^1.0.3",
|
||||
"hasown": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
|
|
@ -7253,11 +7347,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/has-property-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.2"
|
||||
"es-define-property": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
|
|
@ -7660,6 +7754,7 @@
|
|||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/inspectpack/-/inspectpack-4.7.1.tgz",
|
||||
"integrity": "sha512-XoDJbKSM9I2KA+8+OLFJHm8m4NM2pMEgsDD2hze6swVfynEed9ngCx36mRR+otzOsskwnxIZWXjI23FTW1uHqA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"fp-ts": "^2.6.1",
|
||||
|
|
@ -7680,6 +7775,7 @@
|
|||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
|
|
@ -7694,6 +7790,7 @@
|
|||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
|
|
@ -7709,6 +7806,7 @@
|
|||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
|
|
@ -7719,12 +7817,14 @@
|
|||
"node_modules/inspectpack/node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
|
|
@ -7733,6 +7833,7 @@
|
|||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
|
|
@ -7770,6 +7871,7 @@
|
|||
"version": "2.2.21",
|
||||
"resolved": "https://registry.npmjs.org/io-ts/-/io-ts-2.2.21.tgz",
|
||||
"integrity": "sha512-zz2Z69v9ZIC3mMLYWIeoUcwWD6f+O7yP92FMVVaXEOSZH1jnVBmET/urd/uoarD1WGBY4rCj8TAyMPzsGNzMFQ==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"fp-ts": "^2.5.0"
|
||||
}
|
||||
|
|
@ -7778,6 +7880,7 @@
|
|||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/io-ts-reporters/-/io-ts-reporters-1.2.2.tgz",
|
||||
"integrity": "sha512-igASwWWkDY757OutNcM6zTtdJf/eTZYkoe2ymsX2qpm5bKZLo74FJYjsCtMQOEdY7dRHLLEulCyFQwdN69GBCg==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"fp-ts": "^2.0.2",
|
||||
"io-ts": "^2.0.0"
|
||||
|
|
@ -7791,6 +7894,21 @@
|
|||
"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": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
|
||||
|
|
@ -7931,6 +8049,20 @@
|
|||
"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": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
|
|
@ -7950,6 +8082,21 @@
|
|||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
|
||||
|
|
@ -8335,6 +8482,11 @@
|
|||
"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": {
|
||||
"version": "4.5.4",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
|
||||
|
|
@ -8682,9 +8844,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/loupe": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.0.tgz",
|
||||
"integrity": "sha512-qKl+FrLXUhFuHUoDJG7f8P8gEMHq9NFS0c6ghXG1J0rldmZFQZoNVv/vyirE9qwCIhWZDsvEFd1sbFu3GvRQFg==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
|
||||
"integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"get-func-name": "^2.0.1"
|
||||
}
|
||||
|
|
@ -9447,6 +9610,18 @@
|
|||
"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": {
|
||||
"version": "2.0.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
||||
|
|
@ -9693,6 +9885,21 @@
|
|||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||
|
|
@ -10135,6 +10342,7 @@
|
|||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
|
||||
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 14.16"
|
||||
}
|
||||
|
|
@ -10159,6 +10367,7 @@
|
|||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
|
||||
"integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
|
@ -11275,6 +11484,12 @@
|
|||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
|
|
@ -11434,9 +11649,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/sass-loader": {
|
||||
"version": "14.1.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz",
|
||||
"integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==",
|
||||
"version": "14.2.1",
|
||||
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.2.1.tgz",
|
||||
"integrity": "sha512-G0VcnMYU18a4N7VoNDegg2OuMjYtxnqzQWARVWCIVSZwJeiL9kg8QMsuIZOplsJgTzZLF6jGxI3AClj8I9nRdQ==",
|
||||
"dependencies": {
|
||||
"neo-async": "^2.6.2"
|
||||
},
|
||||
|
|
@ -11533,7 +11748,8 @@
|
|||
"node_modules/semver-compare": {
|
||||
"version": "1.0.0",
|
||||
"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": {
|
||||
"version": "0.18.0",
|
||||
|
|
@ -11674,15 +11890,16 @@
|
|||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
|
||||
"integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||
"dependencies": {
|
||||
"define-data-property": "^1.1.1",
|
||||
"define-data-property": "^1.1.4",
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-intrinsic": "^1.2.2",
|
||||
"get-intrinsic": "^1.2.4",
|
||||
"gopd": "^1.0.1",
|
||||
"has-property-descriptors": "^1.0.1"
|
||||
"has-property-descriptors": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
|
|
@ -11701,6 +11918,11 @@
|
|||
"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": {
|
||||
"version": "1.2.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
|
||||
|
|
@ -12261,15 +12527,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.9",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz",
|
||||
"integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==",
|
||||
"version": "5.3.10",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
|
||||
"integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.17",
|
||||
"@jridgewell/trace-mapping": "^0.3.20",
|
||||
"jest-worker": "^27.4.5",
|
||||
"schema-utils": "^3.1.1",
|
||||
"serialize-javascript": "^6.0.1",
|
||||
"terser": "^5.16.8"
|
||||
"terser": "^5.26.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
|
|
@ -12404,6 +12670,17 @@
|
|||
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
|
|
@ -12515,6 +12792,14 @@
|
|||
"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": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
|
||||
|
|
@ -12718,6 +13003,18 @@
|
|||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@
|
|||
"@vue/cli-service": "^5.0.8",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^8.21.3",
|
||||
"assert": "^2.1.0",
|
||||
"axios": "^0.28.0",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^4.6.0",
|
||||
"bootstrap-vue": "^2.23.1",
|
||||
"chai": "^5.1.0",
|
||||
"core-js": "^3.33.1",
|
||||
"dompurify": "^3.0.3",
|
||||
"eslint": "7.32.0",
|
||||
|
|
@ -32,16 +32,18 @@
|
|||
"eslint-plugin-vue": "7.20.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"hellojs": "^1.20.0",
|
||||
"inspectpack": "^4.7.1",
|
||||
"intro.js": "^7.2.0",
|
||||
"jquery": "^3.7.1",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"moment-locales-webpack-plugin": "^1.2.0",
|
||||
"nconf": "^0.12.1",
|
||||
"sass": "^1.63.4",
|
||||
"sass-loader": "^14.1.1",
|
||||
"sinon": "^17.0.1",
|
||||
"smartbanner.js": "^1.19.3",
|
||||
"stopword": "^2.0.8",
|
||||
"timers-browserify": "^2.0.12",
|
||||
"uuid": "^9.0.1",
|
||||
"validator": "^13.9.0",
|
||||
"vue": "^2.7.10",
|
||||
|
|
@ -51,10 +53,14 @@
|
|||
"vue-template-babel-compiler": "^2.0.0",
|
||||
"vue-template-compiler": "^2.7.10",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
|
||||
"webpack": "^5.89.0"
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0"
|
||||
},
|
||||
"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 {
|
||||
display: block;
|
||||
width: 50%;
|
||||
max-width: 448px;
|
||||
margin: 0 auto;
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.43;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
background: $gray-600;
|
||||
background: $gray-500;
|
||||
color: $gray-200 !important; // Otherwise it gets ignored when used on an A element
|
||||
box-shadow: none;
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
}
|
||||
|
||||
&.color {
|
||||
svg path {
|
||||
svg path, svg polygon {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
// TODO move to item component?
|
||||
|
||||
.item, .item-wrapper, .item > div > div {
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.items > div {
|
||||
display: inline-block;
|
||||
margin-right: 24px;
|
||||
|
|
@ -9,34 +15,22 @@
|
|||
position: relative;
|
||||
display: inline-block;
|
||||
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 {
|
||||
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 {
|
||||
position: relative;
|
||||
width: 94px;
|
||||
|
|
@ -56,11 +50,6 @@
|
|||
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 {
|
||||
box-shadow: 0 0 8px 8px rgba($black, 0.16), 0 5px 10px 0 rgba($black, 0.12) !important;
|
||||
}
|
||||
|
|
@ -70,11 +59,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.flat .item {
|
||||
.flat {
|
||||
.item {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.item-wrapper:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bordered-item .item {
|
||||
background: $gray-700 !important;
|
||||
margin: 0 auto;
|
||||
|
|
|
|||
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 (
|
||||
// ignore items the user owns because we captured them above:
|
||||
!(key in ownedItems)
|
||||
&& allItems[key].price > 0
|
||||
) {
|
||||
const item = allItems[key];
|
||||
itemData.push({
|
||||
|
|
|
|||
|
|
@ -282,20 +282,16 @@ export default {
|
|||
item.modified = true;
|
||||
|
||||
// for non-integer items, toggle through the allowed values:
|
||||
if (item.itemType === 'gear') {
|
||||
// Allowed starting values are true, false, and '' (never owned)
|
||||
// Allowed values to switch to are true and false
|
||||
item.value = !item.value;
|
||||
} else if (item.itemType === 'mounts') {
|
||||
// Allowed starting values are true, null, and "never owned"
|
||||
// Allowed values to switch to are true and null
|
||||
if (item.value === true) {
|
||||
item.value = null;
|
||||
if (item.itemType === 'gear' || item.itemType === 'mounts') {
|
||||
// Allowed starting values are true, false, and undefined (never owned)
|
||||
if (item.value && item.value !== '') {
|
||||
item.value = false;
|
||||
} else if (typeof item.value === 'boolean') {
|
||||
item.value = '';
|
||||
} else {
|
||||
item.value = true;
|
||||
}
|
||||
}
|
||||
// @TODO add a delete option
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -291,7 +291,44 @@
|
|||
</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"
|
||||
>
|
||||
<button
|
||||
|
|
@ -772,6 +809,7 @@ h3 {
|
|||
// modules
|
||||
import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
import Vue from 'vue';
|
||||
|
||||
// images
|
||||
import melior from '@/assets/svg/melior.svg';
|
||||
|
|
@ -785,13 +823,24 @@ import heart from '@/assets/svg/heart.svg';
|
|||
import { mapState } from '@/libs/store';
|
||||
import buyGemsModal from './payments/buyGemsModal.vue';
|
||||
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 {
|
||||
components: {
|
||||
buyGemsModal,
|
||||
},
|
||||
mixins: [reportBug],
|
||||
mixins: [
|
||||
reportBug,
|
||||
worldStateMixin,
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
|
|
@ -803,7 +852,9 @@ export default {
|
|||
heart,
|
||||
}),
|
||||
debugMenuShown: false,
|
||||
IS_PRODUCTION,
|
||||
DEBUG_ENABLED,
|
||||
TIME_TRAVEL_ENABLED,
|
||||
lastTimeJump: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -865,6 +916,27 @@ export default {
|
|||
'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 () {
|
||||
// @TODO: Name these variables better
|
||||
let exp = 0;
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
<span :class="[skinClass, specialMountClass]"></span>
|
||||
<!-- eslint-disable max-len-->
|
||||
<span
|
||||
:class="[member.preferences.size + '_shirt_' + member.preferences.shirt, specialMountClass]"
|
||||
:class="[shirtClass, specialMountClass]"
|
||||
></span>
|
||||
<!-- eslint-enable max-len-->
|
||||
<span :class="['head_0', specialMountClass]"></span>
|
||||
|
|
@ -46,12 +46,10 @@
|
|||
<template
|
||||
v-for="type in ['bangs', 'base', 'mustache', 'beard']"
|
||||
>
|
||||
<!-- eslint-disable max-len-->
|
||||
<span
|
||||
:key="type"
|
||||
:class="['hair_' + type + '_' + member.preferences.hair[type] + '_' + member.preferences.hair.color, specialMountClass]"
|
||||
:class="[hairClass(type), specialMountClass]"
|
||||
></span>
|
||||
<!-- eslint-enable max-len-->
|
||||
</template>
|
||||
<span :class="[getGearClass('body'), specialMountClass]"></span>
|
||||
<span :class="[getGearClass('eyewear'), specialMountClass]"></span>
|
||||
|
|
@ -233,10 +231,20 @@ export default {
|
|||
},
|
||||
skinClass () {
|
||||
if (!this.member) return '';
|
||||
if (this.overrideAvatarGear?.skin) {
|
||||
return `skin_${this.overrideAvatarGear.skin}`;
|
||||
}
|
||||
const baseClass = `skin_${this.member.preferences.skin}`;
|
||||
|
||||
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 () {
|
||||
return this.member?.preferences.costume ? 'costume' : 'equipped';
|
||||
},
|
||||
|
|
@ -269,6 +277,17 @@ export default {
|
|||
|
||||
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) {
|
||||
if (!this.member) return true;
|
||||
if (gearType === 'weapon') {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<div
|
||||
id="body"
|
||||
class="section customize-section"
|
||||
class="customize-section d-flex flex-column"
|
||||
:class="{ 'justify-content-between': editing }"
|
||||
>
|
||||
<sub-menu
|
||||
class="text-center"
|
||||
|
|
@ -17,17 +18,11 @@
|
|||
</div>
|
||||
<div v-if="activeSubPage === 'shirt'">
|
||||
<customize-options
|
||||
:items="freeShirts"
|
||||
:items="userShirts"
|
||||
: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>
|
||||
<customize-banner v-if="editing" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -35,33 +30,27 @@
|
|||
import appearance from '@/../../common/script/content/appearance';
|
||||
import { subPageMixin } from '../../mixins/subPage';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
|
||||
import subMenu from './sub-menu';
|
||||
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||
import customizeBanner from './customize-banner.vue';
|
||||
import customizeOptions from './customize-options';
|
||||
import gem from '@/assets/svg/gem.svg';
|
||||
|
||||
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);
|
||||
import subMenu from './sub-menu';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
subMenu,
|
||||
customizeBanner,
|
||||
customizeOptions,
|
||||
},
|
||||
mixins: [
|
||||
subPageMixin,
|
||||
userStateMixin,
|
||||
avatarEditorUtilies,
|
||||
avatarEditorUtilities,
|
||||
],
|
||||
props: [
|
||||
'editing',
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
specialShirtKeys,
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
id: 'size',
|
||||
|
|
@ -78,25 +67,19 @@ export default {
|
|||
sizes () {
|
||||
return ['slim', 'broad'].map(s => this.mapKeysToFreeOption(s, 'size'));
|
||||
},
|
||||
freeShirts () {
|
||||
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
||||
},
|
||||
specialShirts () {
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.specialShirtKeys;
|
||||
const options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
|
||||
return options;
|
||||
userShirts () {
|
||||
const freeShirts = Object.keys(appearance.shirt)
|
||||
.filter(k => appearance.shirt[k].price === 0)
|
||||
.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
||||
const ownedShirts = Object.keys(this.user.purchased.shirt)
|
||||
.filter(k => this.user.purchased.shirt[k])
|
||||
.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
||||
|
||||
return [...freeShirts, ...ownedShirts];
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.changeSubPage('size');
|
||||
},
|
||||
methods: {
|
||||
|
||||
},
|
||||
};
|
||||
</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>
|
||||
<div
|
||||
class="customize-options"
|
||||
:class="{'background-set': fullSet}"
|
||||
v-if="items.length > 1"
|
||||
class="customize-options mb-4"
|
||||
>
|
||||
<div
|
||||
v-for="option in items"
|
||||
:key="option.key"
|
||||
class="outer-option-background"
|
||||
:class="{
|
||||
locked: option.gemLocked || option.goldLocked,
|
||||
premium: Boolean(option.gem),
|
||||
active: option.active || currentValue === option.key,
|
||||
none: option.none,
|
||||
|
|
@ -28,38 +27,6 @@
|
|||
</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>
|
||||
</template>
|
||||
|
|
@ -67,13 +34,13 @@
|
|||
<script>
|
||||
import gem from '@/assets/svg/gem.svg';
|
||||
import gold from '@/assets/svg/gold.svg';
|
||||
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
|
||||
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
avatarEditorUtilies,
|
||||
avatarEditorUtilities,
|
||||
],
|
||||
props: ['items', 'currentValue', 'fullSet'],
|
||||
props: ['items', 'currentValue'],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
|
|
@ -150,7 +117,7 @@ export default {
|
|||
|
||||
&:not(.locked):not(.active) {
|
||||
.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-left: 0;
|
||||
|
||||
&.color-bangs {
|
||||
margin-top: 3px;
|
||||
}
|
||||
&.skin {
|
||||
margin-top: -4px;
|
||||
margin-left: -4px;
|
||||
|
|
@ -237,14 +201,14 @@ export default {
|
|||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
&.color, &.bangs {
|
||||
margin-top: 4px;
|
||||
margin-left: -3px;
|
||||
&.color, &.bangs, &.beard, &.flower, &.mustache {
|
||||
background-position-x: -6px;
|
||||
background-position-y: -12px;
|
||||
}
|
||||
|
||||
&.hair.base {
|
||||
margin-top: 0px;
|
||||
margin-left: -5px;
|
||||
background-position-x: -6px;
|
||||
background-position-y: -4px;
|
||||
}
|
||||
|
||||
&.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>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<div
|
||||
id="extra"
|
||||
class="section container customize-section"
|
||||
class="customize-section d-flex flex-column"
|
||||
:class="{ 'justify-content-between': !showEmptySection}"
|
||||
>
|
||||
<sub-menu
|
||||
class="text-center"
|
||||
|
|
@ -20,9 +21,8 @@
|
|||
id="animal-ears"
|
||||
>
|
||||
<customize-options
|
||||
v-if="animalItems('back').length > 0"
|
||||
:items="animalItems('headAccessory')"
|
||||
:full-set="!animalItemsOwned('headAccessory')"
|
||||
@unlock="unlock(animalItemsUnlockString('headAccessory'))"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -30,9 +30,8 @@
|
|||
id="animal-tails"
|
||||
>
|
||||
<customize-options
|
||||
v-if="animalItems('back').length > 0"
|
||||
:items="animalItems('back')"
|
||||
:full-set="!animalItemsOwned('back')"
|
||||
@unlock="unlock(animalItemsUnlockString('back'))"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -53,6 +52,24 @@
|
|||
>
|
||||
<customize-options :items="flowers" />
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
|
@ -60,23 +77,24 @@
|
|||
import appearance from '@/../../common/script/content/appearance';
|
||||
import { subPageMixin } from '../../mixins/subPage';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
|
||||
import subMenu from './sub-menu';
|
||||
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||
import customizeBanner from './customize-banner';
|
||||
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 {
|
||||
components: {
|
||||
subMenu,
|
||||
customizeBanner,
|
||||
customizeOptions,
|
||||
subMenu,
|
||||
},
|
||||
mixins: [
|
||||
subPageMixin,
|
||||
userStateMixin,
|
||||
avatarEditorUtilies,
|
||||
avatarEditorUtilities,
|
||||
],
|
||||
props: [
|
||||
'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'],
|
||||
specialShirtKeys,
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
id: 'size',
|
||||
|
|
@ -193,6 +208,11 @@ export default {
|
|||
|
||||
for (const key of keys) {
|
||||
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);
|
||||
}
|
||||
|
|
@ -222,12 +242,22 @@ export default {
|
|||
option.none = true;
|
||||
}
|
||||
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 });
|
||||
return option;
|
||||
});
|
||||
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 () {
|
||||
this.changeSubPage(this.extraSubMenuItems[0].id);
|
||||
|
|
@ -248,7 +278,7 @@ export default {
|
|||
for (const key of keys) {
|
||||
const newKey = `${category}_special_${key}`;
|
||||
const userPurchased = this.user.items.gear.owned[newKey];
|
||||
|
||||
if (userPurchased) {
|
||||
const option = {};
|
||||
option.key = key;
|
||||
option.active = this.user.preferences.costume
|
||||
|
|
@ -258,27 +288,13 @@ export default {
|
|||
if (category === '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';
|
||||
return this.equip(newKey, type);
|
||||
};
|
||||
|
||||
options.push(option);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
|
|
@ -287,17 +303,6 @@ export default {
|
|||
|
||||
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) {
|
||||
const newKey = `${gearType}_${subGearType ? `${subGearType}_` : ''}${key}`;
|
||||
const option = {};
|
||||
|
|
@ -339,7 +344,3 @@ export default {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<div
|
||||
id="hair"
|
||||
class="section customize-section"
|
||||
class="customize-section d-flex flex-column"
|
||||
:class="{ 'justify-content-between': editing && !showEmptySection}"
|
||||
>
|
||||
<sub-menu
|
||||
class="text-center"
|
||||
|
|
@ -14,37 +15,9 @@
|
|||
id="hair-color"
|
||||
>
|
||||
<customize-options
|
||||
:items="freeHairColors"
|
||||
:items="userHairColors"
|
||||
: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
|
||||
v-if="activeSubPage === 'bangs'"
|
||||
|
|
@ -55,67 +28,73 @@
|
|||
:current-value="user.preferences.hair.bangs"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="activeSubPage === 'style'"
|
||||
id="style"
|
||||
>
|
||||
<customize-options
|
||||
:items="userHairStyles"
|
||||
:current-value="user.preferences.hair.base"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="activeSubPage === 'facialhair'"
|
||||
id="facialhair"
|
||||
>
|
||||
<customize-options
|
||||
v-if="editing"
|
||||
:items="mustacheList"
|
||||
v-if="userMustaches.length > 1"
|
||||
:items="userMustaches"
|
||||
/>
|
||||
<!-- eslint-disable max-len -->
|
||||
<customize-options
|
||||
v-if="editing"
|
||||
:items="beardList"
|
||||
:full-set="isPurchaseAllNeeded('hair', ['baseHair5', 'baseHair6'], ['mustache', 'beard'])"
|
||||
@unlock="unlock(`hair.mustache.${baseHair5Keys.join(',hair.mustache.')},hair.beard.${baseHair6Keys.join(',hair.beard.')}`)"
|
||||
v-if="userBeards.length > 1"
|
||||
:items="userBeards"
|
||||
/>
|
||||
<!-- 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>
|
||||
<customize-banner
|
||||
v-if="editing && !showEmptySection"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import appearance from '@/../../common/script/content/appearance';
|
||||
import appearanceSets from '@/../../common/script/content/appearance/sets';
|
||||
import { subPageMixin } from '../../mixins/subPage';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
|
||||
import subMenu from './sub-menu';
|
||||
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||
import customizeBanner from './customize-banner';
|
||||
import customizeOptions from './customize-options';
|
||||
import gem from '@/assets/svg/gem.svg';
|
||||
|
||||
const hairColorBySet = groupBy(appearance.hair.color, 'set.key');
|
||||
const freeHairColorKeys = hairColorBySet[undefined].map(s => s.key);
|
||||
import subMenu from './sub-menu';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
subMenu,
|
||||
customizeBanner,
|
||||
customizeOptions,
|
||||
subMenu,
|
||||
},
|
||||
mixins: [
|
||||
subPageMixin,
|
||||
userStateMixin,
|
||||
avatarEditorUtilies,
|
||||
avatarEditorUtilities,
|
||||
],
|
||||
props: [
|
||||
'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: {
|
||||
hairSubMenuItems () {
|
||||
const items = [
|
||||
|
|
@ -142,91 +121,46 @@ export default {
|
|||
|
||||
return items;
|
||||
},
|
||||
freeHairColors () {
|
||||
return freeHairColorKeys.map(s => this.mapKeysToFreeOption(s, 'hair', 'color'));
|
||||
userHairColors () {
|
||||
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 () {
|
||||
// @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 seasonalHairColors = [];
|
||||
for (const key of Object.keys(hairColorBySet)) {
|
||||
const set = hairColorBySet[key];
|
||||
|
||||
const keys = set.map(item => item.key);
|
||||
|
||||
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,
|
||||
userHairStyles () {
|
||||
const emptyHairStyle = {
|
||||
...this.mapKeysToFreeOption(0, 'hair', 'base'),
|
||||
none: true,
|
||||
};
|
||||
seasonalHairColors.push(compiledSet);
|
||||
}
|
||||
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'));
|
||||
|
||||
return seasonalHairColors;
|
||||
return [emptyMustache, ...ownedMustaches];
|
||||
},
|
||||
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;
|
||||
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'));
|
||||
|
||||
return [emptyBeard, ...ownedBeards];
|
||||
},
|
||||
hairBangs () {
|
||||
const none = this.mapKeysToFreeOption(0, 'hair', 'bangs');
|
||||
|
|
@ -236,136 +170,13 @@ export default {
|
|||
|
||||
return [none, ...options];
|
||||
},
|
||||
mustacheList () {
|
||||
const noneOption = this.mapKeysToFreeOption(0, 'hair', 'mustache');
|
||||
noneOption.none = true;
|
||||
|
||||
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;
|
||||
showEmptySection () {
|
||||
return this.activeSubPage === 'facialhair'
|
||||
&& this.userMustaches.length === 1 && this.userBeards.length === 1;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<div
|
||||
id="skin"
|
||||
class="section customize-section"
|
||||
class="customize-section d-flex flex-column"
|
||||
:class="{ 'justify-content-between': editing }"
|
||||
>
|
||||
<sub-menu
|
||||
class="text-center"
|
||||
|
|
@ -10,63 +11,39 @@
|
|||
@changeSubPage="changeSubPage($event)"
|
||||
/>
|
||||
<customize-options
|
||||
:items="freeSkins"
|
||||
:items="userSkins"
|
||||
:current-value="user.preferences.skin"
|
||||
/>
|
||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
||||
<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>
|
||||
<customize-banner v-if="editing" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import appearance from '@/../../common/script/content/appearance';
|
||||
import appearanceSets from '@/../../common/script/content/appearance/sets';
|
||||
import { subPageMixin } from '../../mixins/subPage';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import { avatarEditorUtilies } from '../../mixins/avatarEditUtilities';
|
||||
import subMenu from './sub-menu';
|
||||
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||
import customizeBanner from './customize-banner.vue';
|
||||
import customizeOptions from './customize-options';
|
||||
import gem from '@/assets/svg/gem.svg';
|
||||
|
||||
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);
|
||||
import subMenu from './sub-menu';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
subMenu,
|
||||
customizeBanner,
|
||||
customizeOptions,
|
||||
},
|
||||
mixins: [
|
||||
subPageMixin,
|
||||
userStateMixin,
|
||||
avatarEditorUtilies,
|
||||
avatarEditorUtilities,
|
||||
],
|
||||
props: [
|
||||
'editing',
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
freeSkinKeys,
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
}),
|
||||
skinSubMenuItems: [
|
||||
{
|
||||
id: 'color',
|
||||
|
|
@ -76,41 +53,13 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
freeSkins () {
|
||||
return freeSkinKeys.map(s => this.mapKeysToFreeOption(s, 'skin'));
|
||||
},
|
||||
seasonalSkins () {
|
||||
// @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 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;
|
||||
userSkins () {
|
||||
const freeSkins = groupBy(appearance.skin, 'set.key')[undefined]
|
||||
.map(s => s.key).map(s => this.mapKeysToFreeOption(s, 'skin'));
|
||||
const ownedSkins = Object.keys(this.user.purchased.skin)
|
||||
.filter(k => this.user.purchased.skin[k])
|
||||
.map(s => this.mapKeysToFreeOption(s, 'skin'));
|
||||
return [...freeSkins, ...ownedSkins];
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
|
|
|
|||
|
|
@ -9,12 +9,10 @@
|
|||
:no-close-on-esc="!editing"
|
||||
:no-close-on-backdrop="!editing"
|
||||
>
|
||||
<span
|
||||
<close-x
|
||||
v-if="editing"
|
||||
class="close-icon svg-icon inline icon-10"
|
||||
@click="close()"
|
||||
v-html="icons.close"
|
||||
></span>
|
||||
@close="close()"
|
||||
/>
|
||||
<div
|
||||
v-if="modalPage === 1 && !editing"
|
||||
class="section row welcome-section"
|
||||
|
|
@ -29,41 +27,48 @@
|
|||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<h2
|
||||
v-if="editing"
|
||||
class="text-center pt-2 mt-4 mb-4"
|
||||
>
|
||||
{{ $t('editAvatar') }}
|
||||
</h2>
|
||||
<div
|
||||
v-if="modalPage > 1"
|
||||
class="avatar-section row"
|
||||
class="avatar-section d-flex justify-content-center"
|
||||
:class="{'page-2': modalPage === 2}"
|
||||
>
|
||||
<div class="col-6 offset-3">
|
||||
<div>
|
||||
<div
|
||||
v-if="!editing"
|
||||
class="user-creation-bg"
|
||||
></div>
|
||||
class="user-creation-bg mt-5"
|
||||
>
|
||||
<avatar
|
||||
class="new-user"
|
||||
:member="user"
|
||||
:avatar-only="!editing"
|
||||
:class="{'edit-avatar': editing}"
|
||||
:avatar-only="true"
|
||||
:override-top-padding="'0px'"
|
||||
/>
|
||||
</div>
|
||||
<avatar
|
||||
v-else
|
||||
:member="user"
|
||||
:avatar-only="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="modalPage === 2"
|
||||
class="section"
|
||||
:class="{'edit-modal': editing}"
|
||||
>
|
||||
<!-- @TODO Implement in V2 .section.row.col-12.text-center
|
||||
button.btn.btn-secondary(v-once) {{$t('randomize')}}-->
|
||||
<div
|
||||
id="options-nav"
|
||||
class="container section text-center customize-menu"
|
||||
class="container section text-center customize-menu px-5"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="row justify-content-around">
|
||||
<div
|
||||
class="menu-container"
|
||||
:class="{
|
||||
'col-3': !editing,
|
||||
'col-2 offset-1': editing,
|
||||
active: activeTopPage === 'body'}"
|
||||
:class="{active: activeTopPage === 'body'}"
|
||||
@click="changeTopPage('body', 'size')"
|
||||
>
|
||||
<div class="menu-item">
|
||||
|
|
@ -77,7 +82,7 @@
|
|||
</div>
|
||||
<div
|
||||
class="menu-container"
|
||||
:class="{'col-3': !editing, 'col-2': editing, active: activeTopPage === 'skin'}"
|
||||
:class="{active: activeTopPage === 'skin'}"
|
||||
@click="changeTopPage('skin', 'color')"
|
||||
>
|
||||
<div class="menu-item">
|
||||
|
|
@ -91,7 +96,7 @@
|
|||
</div>
|
||||
<div
|
||||
class="menu-container"
|
||||
:class="{'col-3': !editing, 'col-2': editing, active: activeTopPage === 'hair'}"
|
||||
:class="{active: activeTopPage === 'hair'}"
|
||||
@click="changeTopPage('hair', 'color')"
|
||||
>
|
||||
<div class="menu-item">
|
||||
|
|
@ -105,7 +110,7 @@
|
|||
</div>
|
||||
<div
|
||||
class="menu-container"
|
||||
:class="{'col-3': !editing, 'col-2': editing, active: activeTopPage === 'extra'}"
|
||||
:class="{active: activeTopPage === 'extra'}"
|
||||
@click="changeTopPage('extra', 'glasses')"
|
||||
>
|
||||
<div class="menu-item">
|
||||
|
|
@ -119,9 +124,9 @@
|
|||
</div>
|
||||
<div
|
||||
v-if="editing"
|
||||
class="menu-container col-2"
|
||||
class="menu-container"
|
||||
:class="{active: activeTopPage === 'backgrounds'}"
|
||||
@click="changeTopPage('backgrounds', '2024')"
|
||||
@click="changeTopPage('backgrounds')"
|
||||
>
|
||||
<div class="menu-item">
|
||||
<div
|
||||
|
|
@ -153,43 +158,37 @@
|
|||
<div
|
||||
v-if="activeTopPage === 'backgrounds'"
|
||||
id="backgrounds"
|
||||
class="section container customize-section"
|
||||
class="section customize-section pt-4"
|
||||
>
|
||||
<div class="row title-row">
|
||||
<toggle-switch
|
||||
v-model="filterBackgrounds"
|
||||
class="backgroundFilterToggle"
|
||||
:label="$t('hideLockedBackgrounds')"
|
||||
/>
|
||||
<div class="row justify-content-center title-row mb-3">
|
||||
<strong>{{ $t('incentiveBackgrounds') }}</strong>
|
||||
</div>
|
||||
<div
|
||||
v-if="!filterBackgrounds"
|
||||
class="row text-center title-row"
|
||||
>
|
||||
<strong>{{ backgroundShopSets[0].text }}</strong>
|
||||
</div>
|
||||
<div
|
||||
v-if="!filterBackgrounds"
|
||||
class="row title-row"
|
||||
>
|
||||
<div
|
||||
v-if="showPlainBackgroundBlurb(
|
||||
backgroundShopSets[0].identifier, backgroundShopSets[0].items
|
||||
)"
|
||||
class="col-12"
|
||||
v-if="standardBackgrounds.length < standardBackgroundMax"
|
||||
class="row justify-content-center title-row mb-3"
|
||||
>
|
||||
<div>
|
||||
{{ $t('incentiveBackgroundsUnlockedWithCheckins') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="background-row d-flex justify-content-center mb-4">
|
||||
<div
|
||||
v-for="bg in backgroundShopSets[0].items"
|
||||
v-for="bg in standardBackgrounds"
|
||||
:id="bg.key"
|
||||
:key="bg.key"
|
||||
class="col-2"
|
||||
class="background-item"
|
||||
:class="{ selected: bg.key === user.preferences.background }"
|
||||
@click="unlock('background.' + bg.key)"
|
||||
>
|
||||
<div
|
||||
v-if="bg.key === ''"
|
||||
class="incentive-background deselect"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="incentive-background"
|
||||
:class="[`background_${bg.key}`]"
|
||||
:class="`background_${bg.key}`"
|
||||
>
|
||||
<div class="small-rectangle"></div>
|
||||
</div>
|
||||
|
|
@ -198,204 +197,111 @@
|
|||
triggers="hover focus"
|
||||
placement="bottom"
|
||||
:prevent-overflow="false"
|
||||
:content="bg.notes"
|
||||
:content="bg.notes(user.preferences.language)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!filterBackgrounds && user.purchased.background.birthday_bash"
|
||||
v-if="user.purchased.background.birthday_bash"
|
||||
>
|
||||
<div
|
||||
class="row text-center title-row"
|
||||
class="row justify-content-center title-row mb-3"
|
||||
>
|
||||
<strong>{{ backgroundShopSets[2].text }}</strong>
|
||||
<strong>{{ $t('eventBackgrounds') }}</strong>
|
||||
</div>
|
||||
<div
|
||||
class="row title-row"
|
||||
class="background-row d-flex justify-content-center mb-4"
|
||||
>
|
||||
<div
|
||||
v-for="bg in backgroundShopSets[2].items"
|
||||
v-for="bg in eventBackgrounds"
|
||||
:id="bg.key"
|
||||
:key="bg.key"
|
||||
class="col-4 text-center customize-option background-button"
|
||||
class="background-item"
|
||||
:class="{selected: bg.key === user.preferences.background}"
|
||||
@click="unlock('background.' + bg.key)"
|
||||
>
|
||||
<div
|
||||
class="background"
|
||||
:class="`background_${bg.key}`"
|
||||
:class="`icon_background_${bg.key}`"
|
||||
></div>
|
||||
<b-popover
|
||||
:target="bg.key"
|
||||
triggers="hover focus"
|
||||
placement="bottom"
|
||||
:prevent-overflow="false"
|
||||
:content="bg.notes"
|
||||
:content="bg.notes(user.preferences.language)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!filterBackgrounds">
|
||||
<div
|
||||
class="row text-center title-row"
|
||||
v-if="timeTravelBackgrounds.length > 0"
|
||||
:key="`ttbg${timeTravelBackgrounds.length}`"
|
||||
>
|
||||
<strong>{{ backgroundShopSets[1].text }}</strong>
|
||||
<div
|
||||
class="row justify-content-center title-row mb-3"
|
||||
>
|
||||
<strong>{{ $t('timeTravelBackgrounds') }}</strong>
|
||||
</div>
|
||||
<div
|
||||
class="row title-row"
|
||||
class="background-row d-flex justify-content-center mb-4"
|
||||
>
|
||||
<div
|
||||
v-for="bg in backgroundShopSets[1].items"
|
||||
v-for="bg in timeTravelBackgrounds"
|
||||
:id="bg.key"
|
||||
:key="bg.key"
|
||||
class="col-4 text-center customize-option background-button"
|
||||
@click="!user.purchased.background[bg.key]
|
||||
? backgroundSelected(bg) : unlock('background.' + bg.key)"
|
||||
>
|
||||
<div
|
||||
class="background"
|
||||
:class="[`background_${bg.key}`, backgroundLockedStatus(bg.key)]"
|
||||
></div>
|
||||
<i
|
||||
v-if="!user.purchased.background[bg.key]"
|
||||
class="glyphicon glyphicon-lock"
|
||||
></i>
|
||||
<div
|
||||
v-if="!user.purchased.background[bg.key]"
|
||||
class="purchase-background single d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<div
|
||||
class="svg-icon hourglass"
|
||||
v-html="icons.hourglass"
|
||||
></div>
|
||||
<span class="price">1</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="!user.purchased.background[bg.key]"
|
||||
class="badge-top"
|
||||
@click.stop.prevent="togglePinned(bg)"
|
||||
>
|
||||
<pin-badge
|
||||
:pinned="isBackgroundPinned(bg)"
|
||||
/>
|
||||
</span>
|
||||
<b-popover
|
||||
:target="bg.key"
|
||||
triggers="hover focus"
|
||||
placement="bottom"
|
||||
:prevent-overflow="false"
|
||||
:content="bg.notes"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<sub-menu
|
||||
v-if="!filterBackgrounds"
|
||||
class="text-center"
|
||||
:items="bgSubMenuItems"
|
||||
:active-sub-page="activeSubPage"
|
||||
@changeSubPage="changeSubPage($event)"
|
||||
/>
|
||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
||||
<div
|
||||
v-for="(sets, key) in backgroundShopSetsByYear"
|
||||
v-if="!filterBackgrounds"
|
||||
:key="key"
|
||||
class="row customize-menu"
|
||||
>
|
||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
||||
<div
|
||||
v-for="set in sets"
|
||||
v-if="activeSubPage === key"
|
||||
:key="set.identifier"
|
||||
class="row background-set"
|
||||
>
|
||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||
<div class="col-8 offset-2 text-center set-title">
|
||||
<strong>{{ set.text }}</strong>
|
||||
</div>
|
||||
<div
|
||||
v-for="bg in set.items"
|
||||
:id="bg.key"
|
||||
:key="bg.key"
|
||||
class="col-4 text-center customize-option background-button"
|
||||
@click="!user.purchased.background[bg.key]
|
||||
? backgroundSelected(bg) : unlock('background.' + bg.key)"
|
||||
>
|
||||
<div
|
||||
class="background"
|
||||
:class="[`background_${bg.key}`, backgroundLockedStatus(bg.key)]"
|
||||
></div>
|
||||
<i
|
||||
v-if="!user.purchased.background[bg.key]"
|
||||
class="glyphicon glyphicon-lock"
|
||||
></i>
|
||||
<div
|
||||
v-if="!user.purchased.background[bg.key]"
|
||||
class="purchase-background single d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<div
|
||||
class="svg-icon gem"
|
||||
v-html="icons.gem"
|
||||
></div>
|
||||
<span class="price">7</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="!user.purchased.background[bg.key]"
|
||||
class="badge-top"
|
||||
@click.stop.prevent="togglePinned(bg)"
|
||||
>
|
||||
<pin-badge
|
||||
:pinned="isBackgroundPinned(bg)"
|
||||
/>
|
||||
</span>
|
||||
<b-popover
|
||||
:target="bg.key"
|
||||
triggers="hover focus"
|
||||
placement="bottom"
|
||||
:prevent-overflow="false"
|
||||
:content="bg.notes"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="!ownsSet('background', set.items) && set.identifier !== 'incentiveBackgrounds'"
|
||||
class="purchase-background set"
|
||||
@click="unlock(setKeys('background', set.items))"
|
||||
>
|
||||
<span class="label">{{ $t('purchaseAll') }}</span>
|
||||
<div
|
||||
class="svg-icon gem"
|
||||
v-html="icons.gem"
|
||||
></div>
|
||||
<span class="price">15</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="filterBackgrounds"
|
||||
class="row customize-menu"
|
||||
>
|
||||
<div
|
||||
v-for="(bg) in ownedBackgrounds"
|
||||
:id="bg.key"
|
||||
:key="bg.key"
|
||||
class="col-4 text-center customize-option background-button"
|
||||
class="background-item"
|
||||
:class="{selected: bg.key === user.preferences.background}"
|
||||
@click="unlock('background.' + bg.key)"
|
||||
>
|
||||
<div
|
||||
class="background"
|
||||
:class="[`background_${bg.key}`, backgroundLockedStatus(bg.key)]"
|
||||
:class="`icon_background_${bg.key}`"
|
||||
></div>
|
||||
<b-popover
|
||||
:target="bg.key"
|
||||
triggers="hover focus"
|
||||
placement="bottom"
|
||||
:prevent-overflow="false"
|
||||
:content="bg.notes"
|
||||
:content="bg.notes(user.preferences.language)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="monthlyBackgrounds.length > 0"
|
||||
:key="`monthly${monthlyBackgrounds.length}`"
|
||||
>
|
||||
<div
|
||||
class="row text-center title-row mb-3"
|
||||
>
|
||||
<strong>{{ $t('monthlyBackgrounds') }}</strong>
|
||||
</div>
|
||||
<div class="background-row d-flex justify-content-center mx-auto mb-4 px-5">
|
||||
<div
|
||||
v-for="(bg) in monthlyBackgrounds"
|
||||
:id="bg.key"
|
||||
:key="bg.key"
|
||||
class="background-item"
|
||||
:class="{selected: bg.key === user.preferences.background}"
|
||||
@click="unlock('background.' + bg.key)"
|
||||
>
|
||||
<div
|
||||
class="background"
|
||||
:class="`icon_background_${bg.key}`"
|
||||
></div>
|
||||
<b-popover
|
||||
:target="bg.key"
|
||||
triggers="hover focus"
|
||||
placement="bottom"
|
||||
:prevent-overflow="false"
|
||||
:content="bg.notes(user.preferences.language)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<customize-banner class="padding-fix" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="modalPage === 3 && !editing"
|
||||
|
|
@ -671,18 +577,7 @@
|
|||
$dialogMarginTop: 56px;
|
||||
$userCreationBgHeight: 105px;
|
||||
|
||||
/* @TODO do not rely on avatar-modal___BV_modal_body_,
|
||||
it already changed once when bootstrap-vue reached version 1 */
|
||||
|
||||
#avatar-modal___BV_modal_body_, #avatar-modal___BV_modal_body_ {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page-2 {
|
||||
#avatar-modal___BV_modal_body_ {
|
||||
margin-top: $dialogMarginTop;
|
||||
}
|
||||
|
||||
&#avatar-modal {
|
||||
.modal-dialog.modal-md {
|
||||
margin-top: 186px;
|
||||
|
|
@ -691,6 +586,30 @@
|
|||
}
|
||||
|
||||
#avatar-modal {
|
||||
h2 {
|
||||
color: $purple-300;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
cursor: auto;
|
||||
|
||||
&:not(.new-user)[class*=background] {
|
||||
box-shadow: 0px 1px 3px 0px rgba(26, 24, 29, 0.12), 0px 1px 2px 0px rgba(26, 24, 29, 0.24);
|
||||
}
|
||||
|
||||
&.new-user {
|
||||
padding-top: 0px;
|
||||
padding-left: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.first-page-footer {
|
||||
margin-bottom: 32px;
|
||||
|
|
@ -698,7 +617,9 @@
|
|||
|
||||
.customize-section {
|
||||
text-align: center;
|
||||
padding-bottom: 2em;
|
||||
background-color: #f9f9f9;
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
}
|
||||
|
||||
#creator-background {
|
||||
|
|
@ -719,17 +640,13 @@
|
|||
}
|
||||
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
color: $gray-200;
|
||||
color: $gray-100;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.purchase-all {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.edit-modal {
|
||||
margin-top: 10em;
|
||||
.padding-fix {
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.row.sub-menu + .row.sub-menu {
|
||||
|
|
@ -739,6 +656,13 @@
|
|||
.welcome-section {
|
||||
margin-top: 2.5em;
|
||||
margin-bottom: 2.5em;
|
||||
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
color: $gray-200;
|
||||
line-height: 1.71;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
|
|
@ -753,15 +677,9 @@
|
|||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
position: absolute !important; // was overwritten in production build
|
||||
top: -22px;
|
||||
left: 4em;
|
||||
}
|
||||
|
||||
.top {
|
||||
position: absolute;
|
||||
top: -($dialogMarginTop + $userCreationBgHeight - 16px);
|
||||
top: -80px;
|
||||
right: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
|
@ -774,10 +692,6 @@
|
|||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.edit-avatar {
|
||||
left: 9.2em;
|
||||
}
|
||||
|
||||
.justin-section {
|
||||
position: relative;
|
||||
}
|
||||
|
|
@ -849,14 +763,18 @@
|
|||
}
|
||||
|
||||
.customize-menu {
|
||||
.menu-item .svg-icon {
|
||||
.menu-item {
|
||||
width: 83px;
|
||||
|
||||
.svg-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-container {
|
||||
color: #a5a1ac;
|
||||
color: $gray-100;
|
||||
}
|
||||
|
||||
.menu-container:hover, .menu-container.active {
|
||||
|
|
@ -879,44 +797,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.text-center {
|
||||
.gem-lock, .gold-lock {
|
||||
display: inline-block;
|
||||
margin-right: 1em;
|
||||
margin-bottom: 1.6em;
|
||||
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
|
||||
}
|
||||
|
||||
.gold-lock span {
|
||||
color: $yellow-10
|
||||
}
|
||||
|
||||
.customize-section {
|
||||
background-color: #f9f9f9;
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.interests-section {
|
||||
margin-top: 3em;
|
||||
margin-bottom: 60px;
|
||||
|
|
@ -928,27 +808,43 @@
|
|||
}
|
||||
|
||||
#backgrounds {
|
||||
padding-top: 12px;
|
||||
|
||||
.title-row {
|
||||
margin-bottom: 1em;
|
||||
.background-row {
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.backgroundFilterToggle {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.set-title {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.background-item {
|
||||
outline: 4px solid transparent;
|
||||
|
||||
.background {
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background-position: -4px -4px;
|
||||
}
|
||||
|
||||
.deselect {
|
||||
height: 4px;
|
||||
display: block;
|
||||
opacity: 0.24;
|
||||
background: red;
|
||||
transform: rotate(-45deg);
|
||||
top: 0;
|
||||
margin-top: 32px;
|
||||
margin-left: -1px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
outline: 4px solid rgba($purple-300, .25);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-radius: 4px;
|
||||
outline: 4px solid $purple-300;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
|
|
@ -959,9 +855,8 @@
|
|||
background-image: none;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
border-radius: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #92b6bd;
|
||||
margin: 0 auto;
|
||||
padding-top: .3em;
|
||||
|
||||
.small-rectangle {
|
||||
|
|
@ -1001,61 +896,6 @@
|
|||
.incentive-background:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.background:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.purchase-background {
|
||||
margin: 0 auto;
|
||||
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, .hourglass {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
&.single {
|
||||
width: 141px;
|
||||
}
|
||||
|
||||
&.set {
|
||||
width: 100%;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.gem, .coin {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gem, .coin, .hourglass {
|
||||
margin: 0 .5em;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.background-set {
|
||||
width: 100%;
|
||||
margin: 10px;
|
||||
background-color: #edecee;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
|
@ -1141,39 +981,24 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.background-button {
|
||||
margin-bottom: 15px;
|
||||
|
||||
.badge-pin:not(.pinned) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover .badge-pin {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import map from 'lodash/map';
|
||||
import shops from '@/../../common/script/libs/shops';
|
||||
import isPinned from '@/../../common/script/libs/isPinned';
|
||||
import forEach from 'lodash/forEach';
|
||||
import content from '@/../../common/script/content/index';
|
||||
import { mapState } from '@/libs/store';
|
||||
import avatar from './avatar';
|
||||
import usernameForm from './settings/usernameForm';
|
||||
import guide from '@/mixins/guide';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import PinBadge from '@/components/ui/pinBadge';
|
||||
import toggleSwitch from '@/components/ui/toggleSwitch';
|
||||
import customizeBanner from './avatarModal/customize-banner';
|
||||
import bodySettings from './avatarModal/body-settings';
|
||||
import skinSettings from './avatarModal/skin-settings';
|
||||
import hairSettings from './avatarModal/hair-settings';
|
||||
import extraSettings from './avatarModal/extra-settings';
|
||||
import subMenu from './avatarModal/sub-menu';
|
||||
import closeX from './ui/closeX';
|
||||
|
||||
import logoPurple from '@/assets/svg/logo-purple.svg';
|
||||
import bodyIcon from '@/assets/svg/body.svg';
|
||||
|
|
@ -1187,29 +1012,29 @@ import gold from '@/assets/svg/gold.svg';
|
|||
import arrowRight from '@/assets/svg/arrow_right.svg';
|
||||
import arrowLeft from '@/assets/svg/arrow_left.svg';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import { avatarEditorUtilies } from '../mixins/avatarEditUtilities';
|
||||
import { avatarEditorUtilities } from '../mixins/avatarEditUtilities';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
avatar,
|
||||
closeX,
|
||||
customizeBanner,
|
||||
bodySettings,
|
||||
extraSettings,
|
||||
hairSettings,
|
||||
PinBadge,
|
||||
skinSettings,
|
||||
subMenu,
|
||||
toggleSwitch,
|
||||
usernameForm,
|
||||
},
|
||||
mixins: [guide, notifications, avatarEditorUtilies],
|
||||
mixins: [guide, notifications, avatarEditorUtilities],
|
||||
data () {
|
||||
const backgroundShopSets = shops.getBackgroundShopSets();
|
||||
|
||||
return {
|
||||
loading: false,
|
||||
backgroundShopSets,
|
||||
backgroundUpdate: new Date(),
|
||||
filterBackgrounds: false,
|
||||
allBackgrounds: content.backgroundsFlat,
|
||||
eventBackgrounds: [],
|
||||
monthlyBackgrounds: [],
|
||||
standardBackgrounds: [],
|
||||
standardBackgroundMax: 1,
|
||||
timeTravelBackgrounds: [],
|
||||
|
||||
icons: Object.freeze({
|
||||
logoPurple,
|
||||
|
|
@ -1235,11 +1060,6 @@ export default {
|
|||
label: this.$t('color'),
|
||||
},
|
||||
],
|
||||
|
||||
bgSubMenuItems: ['2024', '2023', '2022', '2021', '2020', '2019', '2018', '2017', '2016', '2015', '2014'].map(y => ({
|
||||
id: y,
|
||||
label: y,
|
||||
})),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -1253,59 +1073,8 @@ export default {
|
|||
startingPage () {
|
||||
return this.$store.state.avatarEditorOptions.startingPage;
|
||||
},
|
||||
backgroundShopSetsByYear () {
|
||||
// @TODO: add dates to backgrounds
|
||||
const backgroundShopSetsByYear = {
|
||||
2014: [],
|
||||
2015: [],
|
||||
2016: [],
|
||||
2017: [],
|
||||
2018: [],
|
||||
2019: [],
|
||||
2020: [],
|
||||
2021: [],
|
||||
2022: [],
|
||||
2023: [],
|
||||
2024: [],
|
||||
};
|
||||
|
||||
// Hack to force update for now until we restructure the data
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
|
||||
this.backgroundShopSets.forEach(set => {
|
||||
const year = set.identifier.substr(set.identifier.length - 4);
|
||||
if (!backgroundShopSetsByYear[year]) return;
|
||||
|
||||
let setOwnedByUser = false;
|
||||
for (const key in set.items) {
|
||||
if (this.user.purchased.background[key]) setOwnedByUser = true;
|
||||
}
|
||||
set.userOwns = setOwnedByUser;
|
||||
|
||||
backgroundShopSetsByYear[year].push(set);
|
||||
});
|
||||
return backgroundShopSetsByYear;
|
||||
},
|
||||
ownedBackgrounds () {
|
||||
const ownedBackgrounds = [];
|
||||
|
||||
// Hack to force update for now until we restructure the data
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
|
||||
this.backgroundShopSets.forEach(set => {
|
||||
set.items.forEach(bg => {
|
||||
if (this.user.purchased.background[bg.key]) {
|
||||
ownedBackgrounds.push(bg);
|
||||
}
|
||||
});
|
||||
});
|
||||
return ownedBackgrounds;
|
||||
},
|
||||
imageURL () {
|
||||
if (!this.currentEvent || !this.currentEvent.season) {
|
||||
return 'url(/static/npc/normal/npc_justin.png)';
|
||||
}
|
||||
return `url(/static/npc/${this.currentEvent.season}/npc_justin.png)`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -1321,20 +1090,36 @@ export default {
|
|||
},
|
||||
},
|
||||
mounted () {
|
||||
this.updateBackgrounds();
|
||||
if (this.editing) this.modalPage = 2;
|
||||
// Buy modal is global, so we listen at root. I'd like to not
|
||||
this.$root.$on('buyModal::boughtItem', this.backgroundPurchased);
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'avatar-modal');
|
||||
},
|
||||
purchase (type, key) {
|
||||
this.$store.dispatch('shops:purchase', {
|
||||
type,
|
||||
key,
|
||||
updateBackgrounds () {
|
||||
this.eventBackgrounds = [];
|
||||
this.monthlyBackgrounds = [];
|
||||
this.standardBackgrounds = [
|
||||
{ key: '', notes: () => this.$t('noBackground') },
|
||||
];
|
||||
this.timeTravelBackgrounds = [];
|
||||
forEach(this.allBackgrounds, bg => {
|
||||
if (bg.set === 'incentiveBackgrounds') {
|
||||
this.standardBackgroundMax += 1;
|
||||
}
|
||||
if (this.user.purchased.background[bg.key]) {
|
||||
if (bg.set === 'eventBackgrounds') {
|
||||
this.eventBackgrounds.push(bg);
|
||||
} else if (bg.set === 'incentiveBackgrounds') {
|
||||
this.standardBackgrounds.push(bg);
|
||||
} else if (bg.set === 'timeTravelBackgrounds') {
|
||||
this.timeTravelBackgrounds.push(bg);
|
||||
} else {
|
||||
this.monthlyBackgrounds.push(bg);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.backgroundUpdate = new Date();
|
||||
},
|
||||
prev () {
|
||||
this.modalPage -= 1;
|
||||
|
|
@ -1343,6 +1128,9 @@ export default {
|
|||
this.modalPage += 1;
|
||||
},
|
||||
changeTopPage (page, subpage) {
|
||||
if (page === 'backgrounds') {
|
||||
this.updateBackgrounds();
|
||||
}
|
||||
this.activeTopPage = page;
|
||||
if (subpage) this.activeSubPage = subpage;
|
||||
},
|
||||
|
|
@ -1392,45 +1180,6 @@ export default {
|
|||
this.goto('intro', 0);
|
||||
}, 1000);
|
||||
},
|
||||
showPlainBackgroundBlurb (identifier, set) {
|
||||
return identifier === 'incentiveBackgrounds' && !this.ownsSet('background', set);
|
||||
},
|
||||
ownsSet (type, set) {
|
||||
let setOwnedByUser = false;
|
||||
|
||||
for (let key of Object.keys(set)) {
|
||||
const value = set[key];
|
||||
if (type === 'background') key = value.key;
|
||||
if (this.user.purchased[type][key]) setOwnedByUser = true;
|
||||
}
|
||||
|
||||
return setOwnedByUser;
|
||||
},
|
||||
setKeys (type, _set) {
|
||||
return map(_set, (v, k) => {
|
||||
if (type === 'background') k = v.key; // eslint-disable-line no-param-reassign
|
||||
return `${type}.${k}`;
|
||||
}).join(',');
|
||||
},
|
||||
backgroundLockedStatus (bgKey) {
|
||||
let backgroundClass = 'background-locked';
|
||||
if (this.user.purchased.background[bgKey]) backgroundClass = 'background-unlocked';
|
||||
return backgroundClass;
|
||||
},
|
||||
isBackgroundPinned (bg) {
|
||||
return isPinned(this.user, bg);
|
||||
},
|
||||
togglePinned (bg) {
|
||||
if (!this.$store.dispatch('user:togglePinnedItem', { type: bg.pinType, path: bg.path })) {
|
||||
this.text(this.$t('unpinnedItem', { item: bg.text }));
|
||||
}
|
||||
},
|
||||
backgroundSelected (bg) {
|
||||
this.$root.$emit('buyModal::showItem', bg);
|
||||
},
|
||||
backgroundPurchased () {
|
||||
this.backgroundUpdate = new Date();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -134,6 +134,12 @@
|
|||
>
|
||||
{{ $t('quests') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
:to="{name: 'customizations'}"
|
||||
>
|
||||
{{ $t('customizations') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
:to="{name: 'seasonal'}"
|
||||
|
|
|
|||
|
|
@ -35,13 +35,9 @@
|
|||
/>
|
||||
</a>
|
||||
<a
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
class="topbar-dropdown-item dropdown-item dropdown-separated"
|
||||
@click="showAvatar('body', 'size')"
|
||||
>{{ $t('editAvatar') }}</a>
|
||||
<a
|
||||
class="topbar-dropdown-item dropdown-item dropdown-separated"
|
||||
@click="showAvatar('backgrounds', '2024')"
|
||||
>{{ $t('backgrounds') }}</a>
|
||||
<a
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
@click="showProfile('profile')"
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@
|
|||
:right="true"
|
||||
:hide-icon="false"
|
||||
:inline-dropdown="false"
|
||||
:direct-select="true"
|
||||
@select="groupBy = $event"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
|
|
|
|||
|
|
@ -444,7 +444,7 @@ export default {
|
|||
const isSearched = !searchText || item.text()
|
||||
.toLowerCase()
|
||||
.indexOf(searchText) !== -1;
|
||||
if (isSearched) {
|
||||
if (isSearched && item) {
|
||||
itemsArray.push({
|
||||
...item,
|
||||
class: `${group.classPrefix}${item.key}`,
|
||||
|
|
|
|||
|
|
@ -134,12 +134,14 @@
|
|||
v-for="(petGroup) in petGroups"
|
||||
v-if="!anyFilterSelected || viewOptions[petGroup.key].selected"
|
||||
:key="petGroup.key"
|
||||
:class="{ hide: viewOptions[petGroup.key].animalCount === 0 }"
|
||||
>
|
||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||
<h4 v-if="viewOptions[petGroup.key].animalCount !== 0">
|
||||
{{ petGroup.label }}
|
||||
</h4>
|
||||
<!-- eslint-disable vue/no-use-v-if-with-v-for, max-len -->
|
||||
<div class="d-inline-flex flex-column">
|
||||
<div
|
||||
v-for="(group, key, index) in pets(petGroup, hideMissing, selectedSortBy, searchTextThrottled)"
|
||||
v-if="index === 0 || $_openedItemRows_isToggled(petGroup.key)"
|
||||
|
|
@ -153,7 +155,6 @@
|
|||
:key="item.key"
|
||||
v-drag.drop.food="item.key"
|
||||
class="pet-group"
|
||||
:class="{'last': item.isLastInRow}"
|
||||
@itemDragOver="onDragOver($event, item)"
|
||||
@itemDropped="onDrop($event, item)"
|
||||
@itemDragLeave="onDragLeave()"
|
||||
|
|
@ -181,10 +182,10 @@
|
|||
<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>
|
||||
{{ $t('mounts') }}
|
||||
<span
|
||||
|
|
@ -196,12 +197,14 @@
|
|||
v-for="mountGroup in mountGroups"
|
||||
v-if="!anyFilterSelected || viewOptions[mountGroup.key].selected"
|
||||
:key="mountGroup.key"
|
||||
:class="{ hide: viewOptions[mountGroup.key].animalCount === 0 }"
|
||||
>
|
||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||
<h4 v-if="viewOptions[mountGroup.key].animalCount != 0">
|
||||
{{ mountGroup.label }}
|
||||
</h4>
|
||||
<!-- eslint-disable vue/no-use-v-if-with-v-for, max-len -->
|
||||
<div class="d-inline-flex flex-column">
|
||||
<div
|
||||
v-for="(group, key, index) in mounts(mountGroup, hideMissing, selectedSortBy, searchTextThrottled)"
|
||||
v-if="index === 0 || $_openedItemRows_isToggled(mountGroup.key)"
|
||||
|
|
@ -243,6 +246,7 @@
|
|||
@click="setShowMore(mountGroup.key)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<inventoryDrawer>
|
||||
<template
|
||||
slot="item"
|
||||
|
|
@ -310,13 +314,8 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pet-row {
|
||||
max-width: 100%;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.item {
|
||||
margin-right: .5em;
|
||||
}
|
||||
.hide {
|
||||
height: 0px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
@ -330,6 +329,14 @@
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.pet-row {
|
||||
flex-wrap: wrap;
|
||||
|
||||
.pet-group:not(:last-of-type) {
|
||||
margin-right: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.GreyedOut {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
|
@ -343,24 +350,11 @@
|
|||
}
|
||||
|
||||
.stable {
|
||||
|
||||
.standard-page {
|
||||
padding-right:0;
|
||||
}
|
||||
|
||||
.standard-page .clearfix .float-right {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.svg-icon.inline.icon-16 {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.last {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.no-focus:focus {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
|
|
|
|||
|
|
@ -1,40 +1,31 @@
|
|||
<template>
|
||||
<div class="d-flex justify-content-around">
|
||||
<span
|
||||
<div class="d-flex align-items-center">
|
||||
<div
|
||||
v-for="currency of currencies"
|
||||
:key="currency.key"
|
||||
class="d-flex align-items-center"
|
||||
>
|
||||
<div
|
||||
class="svg-icon ml-1"
|
||||
class="svg-icon icon-16 ml-1"
|
||||
v-html="currency.icon"
|
||||
></div>
|
||||
<span
|
||||
<div
|
||||
:class="{'notEnough': currency.notEnough}"
|
||||
class="mx-1"
|
||||
class="currency-value mx-1 my-auto"
|
||||
>
|
||||
{{ currency.value | roundBigNumber }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
span {
|
||||
.currency-value {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.33;
|
||||
color: $gray-100;
|
||||
margin-bottom: 16px;
|
||||
margin-top: -4px;
|
||||
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
vertical-align: middle;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
|
@ -42,6 +33,9 @@ span {
|
|||
color: #f23035 !important;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
margin-top: 1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -89,19 +89,19 @@
|
|||
v-if="item.value > 0 && !(item.key === 'gem' && gemsLeft < 1)"
|
||||
class="purchase-amount"
|
||||
>
|
||||
<!-- this is where the pretty item cost element lives -->
|
||||
<div class="item-cost">
|
||||
<div class="item-cost justify-content-center my-3">
|
||||
<span
|
||||
class="cost"
|
||||
class="cost d-flex mx-auto"
|
||||
:class="getPriceClass()"
|
||||
>
|
||||
<span
|
||||
class="svg-icon inline icon-24"
|
||||
class="svg-icon icon-24 my-auto mr-1"
|
||||
aria-hidden="true"
|
||||
v-html="icons[getPriceClass()]"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
class="my-auto"
|
||||
:class="getPriceClass()"
|
||||
>{{ item.value }}</span>
|
||||
</span>
|
||||
|
|
@ -181,7 +181,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<countdown-banner
|
||||
v-if="item.event && item.owned == null"
|
||||
v-if="item.end && item.owned == null"
|
||||
:end-date="endDate"
|
||||
class="limitedTime available"
|
||||
/>
|
||||
|
|
@ -218,11 +218,10 @@
|
|||
</div>
|
||||
<div
|
||||
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
|
||||
class="currency-totals"
|
||||
class="mr-3"
|
||||
:currency-needed="getPriceClass()"
|
||||
:amount-needed="item.value"
|
||||
/>
|
||||
|
|
@ -250,24 +249,21 @@
|
|||
border-bottom-left-radius: 8px;
|
||||
display: block;
|
||||
margin: 24px 0 0 0;
|
||||
padding: 16px 24px;
|
||||
align-content: center;
|
||||
padding: 0px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.user-balance {
|
||||
width: 150px;
|
||||
height: 16px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.33;
|
||||
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 {
|
||||
display: inline-flex;
|
||||
margin: 16px 0;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.cost {
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
font-family: sans-serif;
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
|
|
@ -470,19 +463,16 @@
|
|||
&.gems {
|
||||
color: $green-10;
|
||||
background-color: rgba(36, 204, 143, 0.15);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.gold {
|
||||
color: $yellow-5;
|
||||
background-color: rgba(255, 190, 93, 0.15);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.hourglasses {
|
||||
color: $hourglass-color;
|
||||
background-color: rgba(41, 149, 205, 0.15);
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -547,10 +537,6 @@
|
|||
margin: auto -1rem -1rem;
|
||||
}
|
||||
|
||||
// .pt-015 {
|
||||
// padding-top: 0.15rem;
|
||||
// }
|
||||
|
||||
.gems-left {
|
||||
height: 32px;
|
||||
background-color: $green-100;
|
||||
|
|
@ -602,8 +588,10 @@ import moment from 'moment';
|
|||
import planGemLimits from '@/../../common/script/libs/planGemLimits';
|
||||
import { drops as dropEggs } from '@/../../common/script/content/eggs';
|
||||
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 spellsMixin from '@/mixins/spells';
|
||||
import sync from '@/mixins/sync';
|
||||
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import svgGold from '@/assets/svg/gold.svg';
|
||||
|
|
@ -637,7 +625,7 @@ const amountOfDropPotions = size(dropPotions);
|
|||
const hideAmountSelectionForPurchaseTypes = [
|
||||
'gear', 'backgrounds', 'mystery_set', 'card',
|
||||
'rebirth_orb', 'fortify', 'armoire', 'keys',
|
||||
'debuffPotion', 'pets', 'mounts',
|
||||
'debuffPotion', 'pets', 'mounts', 'customization',
|
||||
];
|
||||
|
||||
export default {
|
||||
|
|
@ -650,7 +638,15 @@ export default {
|
|||
CountdownBanner,
|
||||
numberIncrement,
|
||||
},
|
||||
mixins: [buyMixin, currencyMixin, notifications, numberInvalid, spellsMixin],
|
||||
mixins: [
|
||||
avatarEditorUtilities,
|
||||
buyMixin,
|
||||
currencyMixin,
|
||||
notifications,
|
||||
numberInvalid,
|
||||
spellsMixin,
|
||||
sync,
|
||||
],
|
||||
props: {
|
||||
// eslint-disable-next-line vue/require-default-prop
|
||||
item: {
|
||||
|
|
@ -690,7 +686,8 @@ export default {
|
|||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
showAvatar () {
|
||||
return ['backgrounds', 'gear', 'mystery_set'].includes(this.item.purchaseType);
|
||||
return ['backgrounds', 'gear', 'mystery_set', 'customization']
|
||||
.includes(this.item.purchaseType);
|
||||
},
|
||||
|
||||
preventHealthPotion () {
|
||||
|
|
@ -741,7 +738,7 @@ export default {
|
|||
return (!this.user.purchased.plan.customerId && !this.user.purchased.plan.consecutive.trinkets && this.getPriceClass() === 'hourglasses');
|
||||
},
|
||||
endDate () {
|
||||
return moment(this.item.event.end);
|
||||
return moment(this.item.end);
|
||||
},
|
||||
totalOwned () {
|
||||
return this.user.items[this.item.purchaseType][this.item.key] || 0;
|
||||
|
|
@ -759,7 +756,7 @@ export default {
|
|||
this.selectedAmountToBuy = 1;
|
||||
},
|
||||
|
||||
buyItem () {
|
||||
async buyItem () {
|
||||
// @TODO: I think we should buying to the items.
|
||||
// Turn the items into classes, and use polymorphism
|
||||
if (this.item.buy) {
|
||||
|
|
@ -824,6 +821,14 @@ export default {
|
|||
) return;
|
||||
}
|
||||
|
||||
if (this.item.purchaseType === 'customization') {
|
||||
const buySuccess = await this.unlock(this.item.path);
|
||||
if (!buySuccess) return;
|
||||
this.sync();
|
||||
this.$root.$emit('playSound', 'Reward');
|
||||
this.$root.$emit('buyModal::boughtItem', this.item);
|
||||
this.purchased(this.item.text);
|
||||
} else {
|
||||
const shouldConfirmPurchase = this.item.currency === 'gems' || this.item.currency === 'hourglasses';
|
||||
if (
|
||||
shouldConfirmPurchase
|
||||
|
|
@ -831,11 +836,11 @@ export default {
|
|||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.genericPurchase) {
|
||||
this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
|
||||
this.purchased(this.item.text);
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('buyPressed', this.item);
|
||||
this.hideDialog();
|
||||
|
|
@ -891,6 +896,27 @@ export default {
|
|||
|
||||
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 {};
|
||||
|
|
|
|||
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>
|
||||
.featuredItems {
|
||||
height: 216px;
|
||||
height: 192px;
|
||||
|
||||
.background {
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@
|
|||
>
|
||||
{{ $t('quests') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:to="{ name: 'customizations' }"
|
||||
>
|
||||
{{ $t('customizations') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:to="{ name: 'seasonal' }"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
:initial-item="selectedGearCategory"
|
||||
:items="marketGearCategories"
|
||||
:with-icon="true"
|
||||
:direct-select="true"
|
||||
@selected="selectedGroupGearByClass = $event.id"
|
||||
>
|
||||
<span
|
||||
|
|
@ -23,6 +24,7 @@
|
|||
:label="$t('sortBy')"
|
||||
:initial-item="selectedSortGearBy"
|
||||
:items="sortGearBy"
|
||||
:direct-select="true"
|
||||
@selected="selectedSortGearBy = $event"
|
||||
>
|
||||
<span
|
||||
|
|
@ -40,7 +42,7 @@
|
|||
:item-width="94"
|
||||
:item-margin="24"
|
||||
:type="'gear'"
|
||||
:no-items-label="$t('noGearItemsOfClass')"
|
||||
:no-items-label="noItemsLabel"
|
||||
>
|
||||
<template
|
||||
slot="item"
|
||||
|
|
@ -75,6 +77,7 @@
|
|||
import _filter from 'lodash/filter';
|
||||
import _orderBy from 'lodash/orderBy';
|
||||
import shops from '@/../../common/script/libs/shops';
|
||||
import { remainingGearInSet } from '@/../../common/script/count';
|
||||
import { getClassName } from '@/../../common/script/libs/getClassName';
|
||||
import { mapState } from '@/libs/store';
|
||||
import LayoutSection from '@/components/ui/layoutSection';
|
||||
|
|
@ -93,7 +96,7 @@ import pinUtils from '../../../mixins/pinUtils';
|
|||
const sortGearTypes = [
|
||||
'sortByType', 'sortByPrice', 'sortByCon',
|
||||
'sortByPer', 'sortByStr', 'sortByInt',
|
||||
].map(g => ({ id: g }));
|
||||
].map(g => ({ id: g, identifier: g }));
|
||||
|
||||
const sortGearTypeMap = {
|
||||
sortByType: 'type',
|
||||
|
|
@ -134,6 +137,17 @@ export default {
|
|||
userItems: 'user.data.items',
|
||||
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 () {
|
||||
return shops.getMarketGearCategories(this.user).map(c => {
|
||||
c.id = c.identifier;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
/>
|
||||
<h1
|
||||
v-once
|
||||
class="mb-4 page-header"
|
||||
class="page-header mt-4 mb-4"
|
||||
>
|
||||
{{ $t('market') }}
|
||||
</h1>
|
||||
|
|
@ -35,6 +35,7 @@
|
|||
:hide-pinned="hidePinned"
|
||||
:hide-locked="hideLocked"
|
||||
:search-by="searchTextThrottled"
|
||||
class="mb-4"
|
||||
/>
|
||||
<layout-section :title="$t('items')">
|
||||
<div slot="filters">
|
||||
|
|
@ -42,6 +43,7 @@
|
|||
:label="$t('sortBy')"
|
||||
:initial-item="selectedSortItemsBy"
|
||||
:items="sortItemsBy"
|
||||
:direct-select="true"
|
||||
@selected="selectedSortItemsBy = $event"
|
||||
>
|
||||
<span
|
||||
|
|
@ -121,6 +123,10 @@
|
|||
height: 112px;
|
||||
}
|
||||
|
||||
.items {
|
||||
max-width: 944px;
|
||||
}
|
||||
|
||||
.market {
|
||||
.avatar {
|
||||
cursor: default;
|
||||
|
|
@ -133,7 +139,7 @@
|
|||
position: absolute;
|
||||
bottom: -14px;
|
||||
margin: 0;
|
||||
left: 80px;
|
||||
left: 75px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -152,6 +158,7 @@ import _map from 'lodash/map';
|
|||
import _throttle from 'lodash/throttle';
|
||||
import getItemInfo from '@/../../common/script/libs/getItemInfo';
|
||||
import shops from '@/../../common/script/libs/shops';
|
||||
import { getScheduleMatchingGroup } from '@/../../common/script/content/constants/schedule';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
import KeysToKennel from './keysToKennel';
|
||||
|
|
@ -175,7 +182,7 @@ import inventoryUtils from '@/mixins/inventoryUtils';
|
|||
import pinUtils from '@/mixins/pinUtils';
|
||||
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 {
|
||||
components: {
|
||||
|
|
@ -218,6 +225,7 @@ export default {
|
|||
|
||||
hideLocked: false,
|
||||
hidePinned: false,
|
||||
cardMatcher: getScheduleMatchingGroup('cards'),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -241,7 +249,8 @@ export default {
|
|||
categories.push({
|
||||
identifier: '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),
|
||||
showCount: false,
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<countdown-banner
|
||||
v-if="item.event"
|
||||
v-if="item.end"
|
||||
:end-date="endDate"
|
||||
/>
|
||||
<div
|
||||
|
|
@ -470,7 +470,7 @@ export default {
|
|||
return this.icons.gems;
|
||||
},
|
||||
endDate () {
|
||||
return moment(this.item.event.end);
|
||||
return moment(this.item.end);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between w-75">
|
||||
<h1
|
||||
v-once
|
||||
class="mb-4 page-header"
|
||||
|
|
@ -134,6 +135,7 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
||||
<div
|
||||
v-for="category in categories"
|
||||
|
|
@ -197,17 +199,20 @@
|
|||
</itemRows>
|
||||
<div
|
||||
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
|
||||
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"
|
||||
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>
|
||||
<div class="items">
|
||||
<div class="items d-flex justify-content-left">
|
||||
<shopItem
|
||||
v-for="item in items"
|
||||
:key="item.key"
|
||||
|
|
@ -242,6 +247,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="items"
|
||||
|
|
@ -317,18 +323,16 @@
|
|||
margin: 24px auto;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: inline-block;
|
||||
width: 33%;
|
||||
margin-bottom: 24px;
|
||||
vertical-align: top;
|
||||
.quest-container {
|
||||
min-width: 330px;
|
||||
}
|
||||
|
||||
.quest-group {
|
||||
.items {
|
||||
border-radius: 2px;
|
||||
width: fit-content;
|
||||
background-color: #edecee;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.item-wrapper {
|
||||
|
|
@ -389,7 +393,7 @@
|
|||
position: absolute;
|
||||
bottom: -14px;
|
||||
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 [];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="quest.event && !abbreviated"
|
||||
v-if="quest.end && !abbreviated"
|
||||
class="m-auto"
|
||||
>
|
||||
{{ limitedString }}
|
||||
|
|
@ -210,14 +210,14 @@ export default {
|
|||
return collect.text;
|
||||
},
|
||||
countdownString () {
|
||||
if (!this.quest.event || this.purchased) return;
|
||||
const diffDuration = moment.duration(moment(this.quest.event.end).diff(moment()));
|
||||
if (!this.quest.end || this.purchased) return;
|
||||
const diffDuration = moment.duration(moment(this.quest.end).diff(moment()));
|
||||
|
||||
if (diffDuration.asSeconds() <= 0) {
|
||||
this.limitedString = this.$t('noLongerAvailable');
|
||||
} else if (diffDuration.days() > 0 || diffDuration.months() > 0) {
|
||||
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(),
|
||||
minutes: diffDuration.minutes(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@
|
|||
<div class="featuredItems">
|
||||
<div
|
||||
class="background"
|
||||
:class="{opened: seasonal.opened}"
|
||||
:style="{'background-image': imageURLs.background}"
|
||||
>
|
||||
<div
|
||||
|
|
@ -54,20 +53,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!seasonal.opened"
|
||||
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"
|
||||
v-if="seasonal.featured.items.length !== 0"
|
||||
class="content"
|
||||
>
|
||||
<div
|
||||
|
|
@ -135,17 +121,6 @@
|
|||
<h2 class="float-left mb-3">
|
||||
{{ $t('classArmor') }}
|
||||
</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
|
||||
v-for="(groupSets, categoryGroup) in getGroupedCategories(categories)"
|
||||
|
|
@ -174,7 +149,7 @@
|
|||
<div class="items">
|
||||
<!-- eslint-disable max-len -->
|
||||
<shopItem
|
||||
v-for="item in seasonalItems(category, selectedSortItemsBy, searchTextThrottled, viewOptions, hidePinned)"
|
||||
v-for="item in seasonalItems(category, 'AZ', searchTextThrottled, viewOptions, hidePinned)"
|
||||
:key="item.key"
|
||||
:item="item"
|
||||
:price="item.value"
|
||||
|
|
@ -227,6 +202,10 @@
|
|||
background-color: #edecee;
|
||||
display: inline-block;
|
||||
padding: 8px;
|
||||
|
||||
> div {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.item-wrapper {
|
||||
|
|
@ -319,7 +298,7 @@
|
|||
position: absolute;
|
||||
bottom: -14px;
|
||||
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 svgHealer from '@/assets/svg/healer.svg';
|
||||
|
||||
import SelectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
|
||||
import FilterSidebar from '@/components/ui/filterSidebar';
|
||||
import FilterGroup from '@/components/ui/filterGroup';
|
||||
import { worldStateMixin } from '@/mixins/worldState';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SelectTranslatedArray,
|
||||
FilterGroup,
|
||||
FilterSidebar,
|
||||
Checkbox,
|
||||
PinBadge,
|
||||
|
|
@ -407,13 +382,14 @@ export default {
|
|||
eyewear: i18n.t('eyewear'),
|
||||
}),
|
||||
|
||||
sortItemsBy: ['AZ'],
|
||||
selectedSortItemsBy: 'AZ',
|
||||
|
||||
hidePinned: false,
|
||||
featuredGearBought: false,
|
||||
currentEvent: null,
|
||||
backgroundUpdate: new Date(),
|
||||
imageURLs: {
|
||||
background: '',
|
||||
npc: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -484,29 +460,16 @@ export default {
|
|||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
anyFilterSelected () {
|
||||
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: {
|
||||
searchText: _throttle(function throttleSearch () {
|
||||
this.searchTextThrottled = this.searchText.toLowerCase();
|
||||
}, 250),
|
||||
},
|
||||
mounted () {
|
||||
async mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
subSection: this.$t('seasonalShop'),
|
||||
section: this.$t('shops'),
|
||||
|
|
@ -516,8 +479,10 @@ export default {
|
|||
this.backgroundUpdate = new Date();
|
||||
});
|
||||
|
||||
this.triggerGetWorldState();
|
||||
await this.triggerGetWorldState();
|
||||
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 () {
|
||||
this.$root.$off('buyModal::boughtItem');
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
:emptyItem="emptyItem"
|
||||
></slot>
|
||||
<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"
|
||||
>
|
||||
<span
|
||||
|
|
@ -114,7 +114,7 @@
|
|||
</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'"
|
||||
>
|
||||
{{ limitedString }}
|
||||
|
|
@ -142,6 +142,22 @@
|
|||
&.locked .price {
|
||||
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 {
|
||||
|
|
@ -149,11 +165,12 @@
|
|||
}
|
||||
|
||||
.price {
|
||||
height: 1.75rem;
|
||||
width: 94px;
|
||||
border-radius: 0px 0px 4px 4px;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1;
|
||||
margin-left: -1px;
|
||||
margin-right: -1px;
|
||||
border-radius: 0px 0px 4px 4px;
|
||||
padding: 0.375rem 0;
|
||||
|
||||
&.gems {
|
||||
background-color: rgba($green-100, 0.15);
|
||||
|
|
@ -174,9 +191,7 @@
|
|||
|
||||
.price-label {
|
||||
font-family: Roboto;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 1.33;
|
||||
|
||||
&.gems {
|
||||
color: $green-1;
|
||||
|
|
@ -351,6 +366,7 @@ export default {
|
|||
this.$emit('click', {});
|
||||
},
|
||||
blur () {
|
||||
if (!this.$refs?.popover) return;
|
||||
this.$refs.popover.$emit('enable');
|
||||
},
|
||||
getPrice () {
|
||||
|
|
@ -370,14 +386,14 @@ export default {
|
|||
};
|
||||
},
|
||||
countdownString () {
|
||||
if (!this.item.event) return;
|
||||
const diffDuration = moment.duration(moment(this.item.event.end).diff(moment()));
|
||||
if (!this.item.end) return;
|
||||
const diffDuration = moment.duration(moment(this.item.end).diff(moment()));
|
||||
|
||||
if (diffDuration.asSeconds() <= 0) {
|
||||
this.limitedString = this.$t('noLongerAvailable');
|
||||
} else if (diffDuration.days() > 0 || diffDuration.months() > 0) {
|
||||
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(),
|
||||
minutes: diffDuration.minutes(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<div class="row timeTravelers">
|
||||
<div
|
||||
v-if="!closed"
|
||||
class="standard-sidebar d-none d-sm-block"
|
||||
>
|
||||
<filter-sidebar>
|
||||
|
|
@ -69,9 +68,16 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><div
|
||||
v-if="!closed"
|
||||
class="clearfix"
|
||||
</div>
|
||||
<div class="d-flex justify-content-between w-items">
|
||||
<h1
|
||||
v-once
|
||||
class="page-header mt-4 mb-4"
|
||||
>
|
||||
{{ $t('timeTravelers') }}
|
||||
</h1>
|
||||
<div
|
||||
class="clearfix mt-4"
|
||||
>
|
||||
<div class="float-right">
|
||||
<span class="dropdown-label">{{ $t('sortBy') }}</span>
|
||||
|
|
@ -85,10 +91,11 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
||||
<div
|
||||
v-for="category in categories"
|
||||
v-if="!anyFilterSelected || (!closed && viewOptions[category.identifier].selected)"
|
||||
v-if="!anyFilterSelected || viewOptions[category.identifier].selected"
|
||||
:key="category.identifier"
|
||||
:class="category.identifier"
|
||||
>
|
||||
|
|
@ -100,6 +107,9 @@
|
|||
:item-width="94"
|
||||
:item-margin="24"
|
||||
:type="category.identifier"
|
||||
:fold-button="false"
|
||||
:no-items-label="$t('allEquipmentOwned')"
|
||||
:click-handler="false"
|
||||
>
|
||||
<template
|
||||
slot="item"
|
||||
|
|
@ -112,34 +122,7 @@
|
|||
:price-type="ctx.item.currency"
|
||||
:empty-item="false"
|
||||
@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>
|
||||
</itemRows>
|
||||
</div>
|
||||
|
|
@ -164,108 +147,14 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- eslint-disable max-len -->
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/shops.scss';
|
||||
|
||||
// these styles may be applied to other pages too
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.w-items {
|
||||
max-width: 920px;
|
||||
}
|
||||
</style>
|
||||
<!-- eslint-enable max-len -->
|
||||
|
||||
<script>
|
||||
import _filter from 'lodash/filter';
|
||||
|
|
@ -281,8 +170,6 @@ import { mapState } from '@/libs/store';
|
|||
import ShopItem from '../shopItem';
|
||||
import Item from '@/components/inventory/item';
|
||||
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 BuyQuestModal from '../quests/buyQuestModal.vue';
|
||||
|
|
@ -304,9 +191,7 @@ export default {
|
|||
ShopItem,
|
||||
Item,
|
||||
ItemRows,
|
||||
PinBadge,
|
||||
toggleSwitch,
|
||||
QuestInfo,
|
||||
|
||||
BuyQuestModal,
|
||||
},
|
||||
|
|
@ -332,6 +217,11 @@ export default {
|
|||
backgroundUpdate: new Date(),
|
||||
|
||||
currentEvent: null,
|
||||
|
||||
imageURLs: {
|
||||
background: '',
|
||||
npc: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -392,19 +282,6 @@ export default {
|
|||
anyFilterSelected () {
|
||||
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: {
|
||||
searchText: _throttle(function throttleSearch () {
|
||||
|
|
@ -425,6 +302,14 @@ export default {
|
|||
}
|
||||
});
|
||||
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 () {
|
||||
this.$root.$off('buyModal::boughtItem');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
<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="title-details">
|
||||
<h1>{{ $t('contentFaqTitle') }}</h1>
|
||||
|
|
@ -23,7 +26,8 @@
|
|||
</ul>
|
||||
<h3>{{ $t('contentQuestion2') }}</h3>
|
||||
<ul>
|
||||
<li>{{ $t('contentAnswer20') }}
|
||||
<li>
|
||||
{{ $t('contentAnswer20') }}
|
||||
<ul>
|
||||
<li v-html="$t('contentAnswer200')"></li>
|
||||
<li v-html="$t('contentAnswer201')"></li>
|
||||
|
|
@ -54,7 +58,8 @@
|
|||
<p>{{ $t('contentAnswer410') }}</p>
|
||||
<h3>{{ $t('contentQuestion5') }}</h3>
|
||||
<ul>
|
||||
<li>{{ $t('contentAnswer50') }}
|
||||
<li>
|
||||
{{ $t('contentAnswer50') }}
|
||||
<ul>
|
||||
<li>{{ $t('backgrounds') }}</li>
|
||||
<li>{{ $t('contentAnswer501') }}</li>
|
||||
|
|
@ -78,9 +83,11 @@
|
|||
<li>{{ $t('contentAnswer70') }}</li>
|
||||
<li>{{ $t('contentAnswer71') }}</li>
|
||||
</ul>
|
||||
<p v-html="$t('contentFaqPara3',
|
||||
<p
|
||||
v-html="$t('contentFaqPara3',
|
||||
{ mailto: '<a href=mailto:admin@habitica.com>admin@habitica.com</a>'}
|
||||
)"></p>
|
||||
)"
|
||||
></p>
|
||||
</div>
|
||||
<faq-sidebar />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
:disabled="disabled"
|
||||
:value="selected"
|
||||
:hide-icon="true"
|
||||
:direct-select="true"
|
||||
@select="$emit('select', $event.value)"
|
||||
>
|
||||
<template #item="{ item, button }">
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
:hide-icon="false"
|
||||
:inline-dropdown="inlineDropdown"
|
||||
:placeholder="placeholder"
|
||||
:direct-select="true"
|
||||
@select="selectItem($event)"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
|
|
|
|||
|
|
@ -15,16 +15,15 @@
|
|||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.modal-close {
|
||||
color: $black;
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
.svg-close {
|
||||
color: $gray-50;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
vertical-align: middle;
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue