mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-05-13 11:31:23 -05:00
Compare commits
333 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 23059231ce | |||
| 23163043c2 | |||
| a18b8265a5 | |||
| ce1db6923b | |||
| 2465189fb1 | |||
| 04554c5309 | |||
| 5ef88b5c56 | |||
| 892c4934d5 | |||
| b90457c04f | |||
| 379d98a91e | |||
| 07352480cd | |||
| 1fb44bbe73 | |||
| 5323849f90 | |||
| 034327f647 | |||
| de9aac0988 | |||
| f55d836398 | |||
| 287014518d | |||
| b46e2da61b | |||
| ef47d6cf0b | |||
| 1f5d66cd58 | |||
| a88602a21f | |||
| 760c05df5d | |||
| 26d070f2c3 | |||
| bc9577439e | |||
| 10cd596f0b | |||
| d180062ad2 | |||
| bfacf4b36e | |||
| 2912f31dec | |||
| c47b287a89 | |||
| 3aa626d2ae | |||
| 647ee2a073 | |||
| 2080c3f7b8 | |||
| 6f65c72921 | |||
| 22bbdd6a28 | |||
| 1a3d6f6520 | |||
| aa1e78ac94 | |||
| 858caa4582 | |||
| a7e1091f3f | |||
| 78fca804b7 | |||
| a919ef99fe | |||
| de9ca06607 | |||
| b5c5990e56 | |||
| 756af8aafb | |||
| 54f84af274 | |||
| c151b6e1bc | |||
| effd729222 | |||
| b7cdbc5c94 | |||
| 28193f86fb | |||
| 877fe48225 | |||
| e0f6f79c5b | |||
| f62254d68e | |||
| d054e6fc16 | |||
| 7231f699c1 | |||
| 1dae0793fd | |||
| f18fbe86b6 | |||
| 61a61724ca | |||
| 93cf30eb18 | |||
| 379f41ff04 | |||
| cff08adcd0 | |||
| 71936c1f0a | |||
| 4da2ed4a1f | |||
| 11c5b26c59 | |||
| 19c79ce510 | |||
| 0ba14c18b1 | |||
| ac85bb2e2d | |||
| 74dfb2710f | |||
| d2a0ab684a | |||
| 12d38fa813 | |||
| 8dbd3c3db1 | |||
| 74b3b348ff | |||
| 3386d61fde | |||
| db41e00990 | |||
| 5d5275ce70 | |||
| e39c63700e | |||
| 550ac2db9d | |||
| 19da14531c | |||
| 254dd80f24 | |||
| 0bc3f16b4b | |||
| 5184973bd5 | |||
| b6accca5ca | |||
| fec68e6211 | |||
| fc63c906dd | |||
| 3333f8f0f5 | |||
| 89a3ac3dde | |||
| 16551ec83f | |||
| 2645bf6023 | |||
| f5f4974a73 | |||
| 162e337d14 | |||
| 21a7d36b7b | |||
| f2506c3231 | |||
| d47641e25a | |||
| 983e01cb3f | |||
| a55ede9175 | |||
| fb8479ad1e | |||
| 28491cb01d | |||
| b49dddeb47 | |||
| 31e501f65a | |||
| 3810cf3ef3 | |||
| 050c227e6f | |||
| ddb8725052 | |||
| e11b9ebe26 | |||
| 4da53f83c9 | |||
| d05da3722c | |||
| b8a3440ef2 | |||
| 44d63032d8 | |||
| 9d7da91ec6 | |||
| 39add61618 | |||
| 1c1543f012 | |||
| 10a27354bb | |||
| a00f199d18 | |||
| 6c5bff7843 | |||
| 388c3d38ed | |||
| 485584c144 | |||
| b83f62bd82 | |||
| 544d67e7e5 | |||
| 1f0a4dad23 | |||
| eb3220c96b | |||
| d4ba96796c | |||
| ebdac0b388 | |||
| 6d13a257dd | |||
| c2b370f4d3 | |||
| 3313584d60 | |||
| b76585cce3 | |||
| 8d9af82521 | |||
| dcf25c0b4a | |||
| 31036ad9e4 | |||
| 1ba85b403f | |||
| c26f410cc3 | |||
| 242e64cedc | |||
| a44418c4fa | |||
| ea66e4e4a1 | |||
| 6889b65123 | |||
| fd5bc8f0b9 | |||
| f33aff577c | |||
| 9ba986f5e5 | |||
| bf46c798a6 | |||
| 54b1afd5b4 | |||
| b501e06f27 | |||
| 2062c68877 | |||
| 19521b1894 | |||
| 688e7181f0 | |||
| a53a9be4b7 | |||
| 66c56225a4 | |||
| cabc08c04b | |||
| 9ae6063f78 | |||
| 7936677fd8 | |||
| 88a0b57335 | |||
| 2303d5de32 | |||
| 1f5de1ab42 | |||
| 435047cace | |||
| c0d6338eba | |||
| 36b589e92d | |||
| 42cafbeaab | |||
| 5b5d5a39a4 | |||
| 8d479e358d | |||
| 3bb1cceed1 | |||
| 0756d36fb3 | |||
| 57deadaa5c | |||
| becdf640b5 | |||
| 756e99c089 | |||
| 709a14fd51 | |||
| 33181c0ac4 | |||
| ee974dfa19 | |||
| b697598d75 | |||
| 6e5b13668a | |||
| 4c13f3193e | |||
| 15cea33c4b | |||
| cac0a84763 | |||
| c56c07a0f8 | |||
| 929778bdad | |||
| d6dba9767d | |||
| 7e7ce44c77 | |||
| 4d38880249 | |||
| 46d164ddd1 | |||
| 31e4c51c3f | |||
| eebfb81bd2 | |||
| 06623991b3 | |||
| fe697898ee | |||
| f3fc14bd53 | |||
| 7f0b0a3909 | |||
| 0f395bcc3e | |||
| d366b2cde1 | |||
| 7d8611bae2 | |||
| 17964c0ab7 | |||
| 5b6cc23fb7 | |||
| efe8cff1ad | |||
| 6591f6780c | |||
| 592c320d1d | |||
| df641d0866 | |||
| da4606df5e | |||
| ceb6b93dc1 | |||
| e006f3f7de | |||
| 246cc25b6d | |||
| a1bb61793b | |||
| 6523ed08cd | |||
| ab34257c03 | |||
| 6afdffae92 | |||
| c3b17e3db0 | |||
| b9e128b387 | |||
| cac14ab2cc | |||
| c64a6eb66e | |||
| cd33a539cf | |||
| ec76757f93 | |||
| 03adff80f7 | |||
| a73abcca74 | |||
| a8c8fffa7c | |||
| f835cf2761 | |||
| 96fed21fbd | |||
| 7d62c87de3 | |||
| b28251dc9e | |||
| b713e10c14 | |||
| fce5371fce | |||
| a9cefd284a | |||
| 6ed422cd28 | |||
| 02914685dc | |||
| 856ed24dcb | |||
| 4a9ec734c1 | |||
| 61d151d2bb | |||
| 32cb201b81 | |||
| 44a7006295 | |||
| 21a0bf7d65 | |||
| 0089506165 | |||
| fbce5aae32 | |||
| a8726eee0b | |||
| 9efe370d33 | |||
| 47df62e716 | |||
| dac792dd27 | |||
| eacf6de19a | |||
| c1ca4e84b8 | |||
| 87fc01cb81 | |||
| 71f21c643c | |||
| 1ced4a18d6 | |||
| 9dabe79d5e | |||
| 4c2cdfe5b8 | |||
| f05888b116 | |||
| ae8607c0c3 | |||
| d6cabeedb4 | |||
| 99a7b90247 | |||
| fbdaa50fcf | |||
| 30e81297da | |||
| b373eaff39 | |||
| 80a212683d | |||
| b0a4ed30d4 | |||
| c2cb37ffe6 | |||
| 558894fafd | |||
| 4bbdf27f48 | |||
| daa296f2af | |||
| 4c51212315 | |||
| 372763c57c | |||
| 3bf323032c | |||
| 7a50b2d2ff | |||
| 8df326bf92 | |||
| 2d6555fe0f | |||
| b0d6f7722b | |||
| aedbe7d333 | |||
| 6079dd4af6 | |||
| 5c448188cf | |||
| d7dc878b1c | |||
| 7baec4e48e | |||
| 5cd58d4119 | |||
| 4cdfefd92b | |||
| 28b936e2d1 | |||
| e4ec7e3e1e | |||
| 3c7ecef6a8 | |||
| e2a5a1ab39 | |||
| 5f64b2fb25 | |||
| f2a2d4cde5 | |||
| edc3c58876 | |||
| 4277c08324 | |||
| 424f29a82b | |||
| e52c7ff9ce | |||
| fdc709d1c2 | |||
| ea750571a0 | |||
| f8fbea4654 | |||
| e0f4a4ecb8 | |||
| 5ce12d97be | |||
| 4fdd064cd6 | |||
| 37731b236a | |||
| b06c708480 | |||
| f8c452ae3f | |||
| 9befbec2b0 | |||
| 7b46f3bc23 | |||
| 64a500987c | |||
| 92eaece5eb | |||
| ee73d5b628 | |||
| a7eda1355b | |||
| 8b9b79db8e | |||
| 69c0488335 | |||
| c18e06f071 | |||
| a50c0eb1e7 | |||
| fb56f7df20 | |||
| 3540a274b3 | |||
| fe2c02679e | |||
| bca3e96e9c | |||
| 041edb3042 | |||
| 6e96085f99 | |||
| 249394b4ad | |||
| 2a84561e00 | |||
| 962456204e | |||
| 593524905e | |||
| 1b12e9d8b7 | |||
| 127f105934 | |||
| 2dfe5585eb | |||
| 93011f182f | |||
| d11e95ab26 | |||
| f99ddbe60f | |||
| 982069df36 | |||
| db4bec37e3 | |||
| 736ef16430 | |||
| 129cb7627c | |||
| f223b5dd2a | |||
| b3521be629 | |||
| 17db6a1772 | |||
| 278d9b74f9 | |||
| ce796fa1d9 | |||
| ec0275e6f6 | |||
| 39252c7828 | |||
| 75a88ab25a | |||
| 70434b17cc | |||
| a921a8bc61 | |||
| 0aa9d4d1d5 | |||
| 0ead06937b | |||
| 037fb6737d | |||
| 4e0d8cba51 | |||
| ecc8a65d28 | |||
| 28fef8df86 | |||
| 33b54a734e | |||
| 1f8aa7d778 | |||
| 09ff3ee865 | |||
| cbfeb18517 | |||
| 63e7ace693 | |||
| 0f9cf48b55 | |||
| 5a48436eff |
+2
-1
@@ -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
|
||||
|
||||
+5
-2
@@ -3,10 +3,13 @@ FROM node:20
|
||||
# Install global packages
|
||||
RUN npm install -g gulp-cli mocha
|
||||
|
||||
# Copy package.json and package-lock.json into image, then install
|
||||
# dependencies.
|
||||
# Copy package.json and package-lock.json into image
|
||||
WORKDIR /usr/src/habitica
|
||||
COPY ["package.json", "package-lock.json", "./"]
|
||||
# Copy the remaining source files in.
|
||||
COPY . /usr/src/habitica
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
RUN npm run postinstall
|
||||
RUN npm run client:build
|
||||
RUN gulp build:prod
|
||||
|
||||
+5
-1
@@ -32,6 +32,7 @@
|
||||
"LOGGLY_CLIENT_TOKEN": "token",
|
||||
"LOGGLY_SUBDOMAIN": "example-subdomain",
|
||||
"LOGGLY_TOKEN": "example-token",
|
||||
"LOG_REQUESTS_EXCESSIVE_MODE": "false",
|
||||
"MAINTENANCE_MODE": "false",
|
||||
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
|
||||
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
|
||||
@@ -88,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
|
||||
}
|
||||
|
||||
+5
-5
@@ -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
Submodule habitica-images updated: aa72332019...617a3d6e6c
@@ -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
|
||||
}
|
||||
};
|
||||
Generated
+857
-565
File diff suppressed because it is too large
Load Diff
+9
-9
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.25.1",
|
||||
"version": "5.26.1",
|
||||
"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",
|
||||
@@ -106,7 +107,8 @@
|
||||
"debug": "gulp nodemon --inspect",
|
||||
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
|
||||
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
|
||||
"apidoc": "gulp apidoc"
|
||||
"apidoc": "gulp apidoc",
|
||||
"heroku-postbuild": "npm run client:build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^1.4.0",
|
||||
@@ -115,13 +117,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.rmSync(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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@ import { TooManyRequests } from '../../../../website/server/libs/errors';
|
||||
import { apiError } from '../../../../website/server/libs/apiError';
|
||||
import logger from '../../../../website/server/libs/logger';
|
||||
|
||||
describe('rateLimiter middleware', () => {
|
||||
describe.only('rateLimiter middleware', () => {
|
||||
const pathToRateLimiter = '../../../../website/server/middlewares/rateLimiter';
|
||||
|
||||
let res; let req; let next; let nconfGetStub;
|
||||
@@ -54,6 +54,7 @@ describe('rateLimiter middleware', () => {
|
||||
|
||||
it('does not throw when there are available points', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
@@ -71,6 +72,7 @@ describe('rateLimiter middleware', () => {
|
||||
|
||||
it('does not throw when an unknown error is thrown by the rate limiter', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
sandbox.stub(logger, 'error');
|
||||
sandbox.stub(RateLimiterMemory.prototype, 'consume')
|
||||
.returns(Promise.reject(new Error('Unknown error.')));
|
||||
@@ -104,6 +106,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('limits when LIVELINESS_PROBE_KEY is incorrect', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.query.liveliness = 'das';
|
||||
@@ -120,6 +123,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('limits when LIVELINESS_PROBE_KEY is not set', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns(undefined);
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
await attachRateLimiter(req, res, next);
|
||||
@@ -135,6 +139,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('throws when LIVELINESS_PROBE_KEY is blank', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.query.liveliness = '';
|
||||
@@ -150,6 +155,7 @@ describe('rateLimiter middleware', () => {
|
||||
|
||||
it('throws when there are no available points remaining', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
// call for 31 times
|
||||
@@ -173,6 +179,7 @@ describe('rateLimiter middleware', () => {
|
||||
|
||||
it('uses the user id if supplied or the ip address', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.ip = 1;
|
||||
@@ -199,4 +206,71 @@ describe('rateLimiter middleware', () => {
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
});
|
||||
|
||||
it('applies increased cost for registration calls with and without user id', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_REGISTRATION_COST').returns(3);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
req.path = '/api/v4/user/auth/local/register';
|
||||
|
||||
req.ip = 1;
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
req.headers['x-api-user'] = 'user-1';
|
||||
await attachRateLimiter(req, res, next);
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
// user id an ip are counted as separate sources
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 27, // 2 calls with user id
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
|
||||
req.headers['x-api-user'] = undefined;
|
||||
await attachRateLimiter(req, res, next);
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 24, // 3 calls with only ip
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
});
|
||||
|
||||
it('applies increased cost for unauthenticated API calls', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(10);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.ip = 1;
|
||||
await attachRateLimiter(req, res, next);
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 10,
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
});
|
||||
|
||||
describe('authentication rate limiting', async () => {
|
||||
it('applies cost for failed login attempts', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.path = '/api/v4/user/auth/local/login';
|
||||
|
||||
req.ip = 1;
|
||||
await attachRateLimiter(req, res, next);
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 28,
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/* eslint-disable global-require */
|
||||
import requireAgain from 'require-again';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateNext,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
|
||||
describe('requestLogHandler middleware', () => {
|
||||
let res; let req; let
|
||||
next;
|
||||
const pathToMiddleWare = '../../../../website/server/middlewares/requestLogHandler';
|
||||
|
||||
beforeEach(() => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
next = generateNext();
|
||||
});
|
||||
|
||||
it('attaches start time and request ID object to req', () => {
|
||||
const middleware = requireAgain(pathToMiddleWare);
|
||||
|
||||
middleware.logRequestData(req, res, next);
|
||||
|
||||
expect(req.requestStartTime).to.exist;
|
||||
expect(req.requestStartTime).to.be.a('number');
|
||||
expect(req.requestIdentifier).to.exist;
|
||||
expect(req.requestIdentifier).to.be.a('string');
|
||||
});
|
||||
|
||||
it('calls next', () => {
|
||||
const middleware = requireAgain(pathToMiddleWare);
|
||||
const spy = sinon.spy();
|
||||
middleware.logRequestData(req, res, spy);
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,11 @@
|
||||
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 => {
|
||||
if (type === 'all') return;
|
||||
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;
|
||||
}
|
||||
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;
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
/* eslint-disable global-require */
|
||||
import forEach from 'lodash/forEach';
|
||||
import {
|
||||
expectValidTranslationString,
|
||||
} from '../helpers/content.helper';
|
||||
import armoire from '../../website/common/script/content/gear/sets/armoire';
|
||||
|
||||
describe('armoire', () => {
|
||||
let clock;
|
||||
afterEach(() => {
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not return unreleased gear', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-02'));
|
||||
const items = armoire.all;
|
||||
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 = armoire.all;
|
||||
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 = armoire.all;
|
||||
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 = armoire.all;
|
||||
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 = armoire.all;
|
||||
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-07T09:00:00.000Z'));
|
||||
const febuaryItems = armoire.all;
|
||||
expect(febuaryItems.length).to.equal(384);
|
||||
});
|
||||
|
||||
it('sets have at least 2 items', () => {
|
||||
const setMap = {};
|
||||
forEach(armoire.all, item => {
|
||||
// Gotta have one outlier
|
||||
if (!item.set || item.set.startsWith('armoire-')) return;
|
||||
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
-18
@@ -5,29 +5,51 @@ import {
|
||||
expectValidTranslationString,
|
||||
} from '../helpers/content.helper';
|
||||
|
||||
import * as eggs from '../../website/common/script/content/eggs';
|
||||
import eggs from '../../website/common/script/content/eggs';
|
||||
|
||||
describe('eggs', () => {
|
||||
describe('all', () => {
|
||||
it('is a combination of drop and quest eggs', () => {
|
||||
const dropNumber = Object.keys(eggs.drops).length;
|
||||
const questNumber = Object.keys(eggs.quests).length;
|
||||
const allNumber = Object.keys(eggs.all).length;
|
||||
let clock;
|
||||
|
||||
expect(allNumber).to.be.greaterThan(0);
|
||||
expect(allNumber).to.equal(dropNumber + questNumber);
|
||||
});
|
||||
afterEach(() => {
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('contains basic information about each egg', () => {
|
||||
each(eggs.all, (egg, key) => {
|
||||
expectValidTranslationString(egg.text);
|
||||
expectValidTranslationString(egg.adjective);
|
||||
expectValidTranslationString(egg.mountText);
|
||||
expectValidTranslationString(egg.notes);
|
||||
expect(egg.canBuy).to.be.a('function');
|
||||
expect(egg.value).to.be.a('number');
|
||||
expect(egg.key).to.equal(key);
|
||||
const eggTypes = [
|
||||
'drops',
|
||||
'quests',
|
||||
];
|
||||
|
||||
eggTypes.forEach(eggType => {
|
||||
describe(eggType, () => {
|
||||
it('contains basic information about each egg', () => {
|
||||
each(eggs[eggType], (egg, key) => {
|
||||
expectValidTranslationString(egg.text);
|
||||
expectValidTranslationString(egg.adjective);
|
||||
expectValidTranslationString(egg.mountText);
|
||||
expectValidTranslationString(egg.notes);
|
||||
expect(egg.canBuy).to.be.a('function');
|
||||
expect(egg.value).to.be.a('number');
|
||||
expect(egg.key).to.equal(key);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not contain unreleased eggs', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-05-20'));
|
||||
const questEggs = eggs.quests;
|
||||
expect(questEggs.Giraffe).to.not.exist;
|
||||
});
|
||||
|
||||
it('Releases eggs when appropriate without needing restarting', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-05-20'));
|
||||
const mayEggs = eggs.quests;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const juneEggs = eggs.quests;
|
||||
expect(juneEggs.Giraffe).to.exist;
|
||||
expect(Object.keys(mayEggs).length).to.equal(Object.keys(juneEggs).length - 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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,28 +5,50 @@ import {
|
||||
expectValidTranslationString,
|
||||
} from '../helpers/content.helper';
|
||||
|
||||
import * as hatchingPotions from '../../website/common/script/content/hatching-potions';
|
||||
import hatchingPotions 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();
|
||||
}
|
||||
});
|
||||
|
||||
it('contains basic information about each potion', () => {
|
||||
each(hatchingPotions.all, (potion, key) => {
|
||||
expectValidTranslationString(potion.text);
|
||||
expectValidTranslationString(potion.notes);
|
||||
expect(potion.canBuy).to.be.a('function');
|
||||
expect(potion.value).to.be.a('number');
|
||||
expect(potion.key).to.equal(key);
|
||||
const potionTypes = [
|
||||
'drops',
|
||||
'quests',
|
||||
'premium',
|
||||
'wacky',
|
||||
];
|
||||
potionTypes.forEach(potionType => {
|
||||
describe(potionType, () => {
|
||||
it('contains basic information about each potion', () => {
|
||||
each(hatchingPotions.all, (potion, key) => {
|
||||
expectValidTranslationString(potion.text);
|
||||
expectValidTranslationString(potion.notes);
|
||||
expect(potion.canBuy).to.be.a('function');
|
||||
expect(potion.value).to.be.a('number');
|
||||
expect(potion.key).to.equal(key);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not contain unreleased potions', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-05-20'));
|
||||
const premiumPotions = hatchingPotions.premium;
|
||||
expect(premiumPotions.Koi).to.not.exist;
|
||||
});
|
||||
|
||||
it('Releases potions when appropriate without needing restarting', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-05-20'));
|
||||
const mayPotions = hatchingPotions.premium;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const junePotions = hatchingPotions.premium;
|
||||
expect(junePotions.Koi).to.exist;
|
||||
expect(Object.keys(mayPotions).length).to.equal(Object.keys(junePotions).length - 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import content from '../../website/common/script/content';
|
||||
|
||||
describe('content index', () => {
|
||||
let clock;
|
||||
|
||||
afterEach(() => {
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('Releases eggs when appropriate without needing restarting', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const mayEggs = content.eggs;
|
||||
expect(mayEggs.Chameleon).to.not.exist;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-07-20'));
|
||||
const juneEggs = content.eggs;
|
||||
expect(juneEggs.Chameleon).to.exist;
|
||||
expect(Object.keys(mayEggs).length, '').to.equal(Object.keys(juneEggs).length - 1);
|
||||
});
|
||||
|
||||
it('Releases hatching potions when appropriate without needing restarting', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-05-20'));
|
||||
const mayHatchingPotions = content.hatchingPotions;
|
||||
expect(mayHatchingPotions.Koi).to.not.exist;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const juneHatchingPotions = content.hatchingPotions;
|
||||
expect(juneHatchingPotions.Koi).to.exist;
|
||||
expect(Object.keys(mayHatchingPotions).length, '').to.equal(Object.keys(juneHatchingPotions).length - 1);
|
||||
});
|
||||
|
||||
it('Releases armoire gear when appropriate without needing restarting', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const juneGear = content.gear.flat;
|
||||
expect(juneGear.armor_armoire_corsairsCoatAndCape).to.not.exist;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-07-10'));
|
||||
const julyGear = content.gear.flat;
|
||||
expect(julyGear.armor_armoire_corsairsCoatAndCape).to.exist;
|
||||
expect(Object.keys(juneGear).length, '').to.equal(Object.keys(julyGear).length - 3);
|
||||
});
|
||||
|
||||
it('Releases pets gear when appropriate without needing restarting', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const junePets = content.petInfo;
|
||||
expect(junePets['Chameleon-Base']).to.not.exist;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-07-10'));
|
||||
const julyPets = content.petInfo;
|
||||
expect(julyPets['Chameleon-Base']).to.exist;
|
||||
expect(Object.keys(junePets).length, '').to.equal(Object.keys(julyPets).length - 10);
|
||||
});
|
||||
|
||||
it('Releases mounts gear when appropriate without needing restarting', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const juneMounts = content.mountInfo;
|
||||
expect(juneMounts['Chameleon-Base']).to.not.exist;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-07-10'));
|
||||
const julyMounts = content.mountInfo;
|
||||
expect(julyMounts['Chameleon-Base']).to.exist;
|
||||
expect(Object.keys(juneMounts).length, '').to.equal(Object.keys(julyMounts).length - 10);
|
||||
});
|
||||
|
||||
it('marks regular food as buyable and droppable without any events', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const { food } = content;
|
||||
Object.keys(food).forEach(key => {
|
||||
if (key === 'Saddle') {
|
||||
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
|
||||
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
|
||||
return;
|
||||
}
|
||||
let expected = true;
|
||||
if (key.startsWith('Cake_')) {
|
||||
expected = false;
|
||||
} else if (key.startsWith('Candy_')) {
|
||||
expected = false;
|
||||
} else if (key.startsWith('Pie_')) {
|
||||
expected = false;
|
||||
}
|
||||
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
|
||||
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('marks candy as buyable and droppable during habitoween', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-10-31'));
|
||||
const { food } = content;
|
||||
Object.keys(food).forEach(key => {
|
||||
if (key === 'Saddle') {
|
||||
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
|
||||
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
|
||||
return;
|
||||
}
|
||||
let expected = false;
|
||||
if (key.startsWith('Cake_')) {
|
||||
expected = false;
|
||||
} else if (key.startsWith('Candy_')) {
|
||||
expected = true;
|
||||
} else if (key.startsWith('Pie_')) {
|
||||
expected = false;
|
||||
}
|
||||
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
|
||||
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('marks cake as buyable and droppable during birthday', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-31'));
|
||||
const { food } = content;
|
||||
Object.keys(food).forEach(key => {
|
||||
if (key === 'Saddle') {
|
||||
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
|
||||
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
|
||||
return;
|
||||
}
|
||||
let expected = false;
|
||||
if (key.startsWith('Cake_')) {
|
||||
expected = true;
|
||||
} else if (key.startsWith('Candy_')) {
|
||||
expected = false;
|
||||
} else if (key.startsWith('Pie_')) {
|
||||
expected = false;
|
||||
}
|
||||
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
|
||||
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it('marks pie as buyable and droppable during pi day', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-03-14'));
|
||||
const { food } = content;
|
||||
Object.keys(food).forEach(key => {
|
||||
if (key === 'Saddle') {
|
||||
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
|
||||
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
|
||||
return;
|
||||
}
|
||||
let expected = false;
|
||||
if (key.startsWith('Cake_')) {
|
||||
expected = false;
|
||||
} else if (key.startsWith('Candy_')) {
|
||||
expected = false;
|
||||
} else if (key.startsWith('Pie_')) {
|
||||
expected = true;
|
||||
}
|
||||
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
|
||||
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import find from 'lodash/find';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import {
|
||||
ARMOIRE_RELEASE_DATES,
|
||||
EGGS_RELEASE_DATES,
|
||||
HATCHING_POTIONS_RELEASE_DATES,
|
||||
} from '../../website/common/script/content/constants/releaseDates';
|
||||
import armoire from '../../website/common/script/content/gear/sets/armoire';
|
||||
import eggs from '../../website/common/script/content/eggs';
|
||||
import hatchingPotions from '../../website/common/script/content/hatching-potions';
|
||||
|
||||
describe('releaseDates', () => {
|
||||
let clock;
|
||||
|
||||
afterEach(() => {
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
describe('armoire', () => {
|
||||
it('should only contain valid armoire names', () => {
|
||||
const lastReleaseDate = maxBy(Object.values(ARMOIRE_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-20`));
|
||||
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-20`));
|
||||
Object.keys(ARMOIRE_RELEASE_DATES).forEach(key => {
|
||||
expect(find(armoire.all, { set: key }), `${key} is not a valid armoire set`).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
it('should contain a valid year and month', () => {
|
||||
Object.keys(ARMOIRE_RELEASE_DATES).forEach(key => {
|
||||
const date = ARMOIRE_RELEASE_DATES[key];
|
||||
expect(date.year, `${key} year is not a valid year`).to.be.a('number');
|
||||
expect(date.year).to.be.at.least(2023);
|
||||
expect(date.month, `${key} month is not a valid month`).to.be.a('number');
|
||||
expect(date.month).to.be.within(1, 12);
|
||||
expect(date.day).to.not.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('eggs', () => {
|
||||
it('should only contain valid egg names', () => {
|
||||
const lastReleaseDate = maxBy(Object.values(EGGS_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-${value.day}`));
|
||||
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-${lastReleaseDate.day}`));
|
||||
Object.keys(EGGS_RELEASE_DATES).forEach(key => {
|
||||
expect(eggs.all[key], `${key} is not a valid egg name`).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
it('should contain a valid year, month and date', () => {
|
||||
Object.keys(EGGS_RELEASE_DATES).forEach(key => {
|
||||
const date = EGGS_RELEASE_DATES[key];
|
||||
expect(date.year, `${key} year is not a valid year`).to.be.a('number');
|
||||
expect(date.year).to.be.at.least(2024);
|
||||
expect(date.month, `${key} month is not a valid month`).to.be.a('number');
|
||||
expect(date.month).to.be.within(1, 12);
|
||||
expect(date.day, `${key} day is not a valid day`).to.be.a('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hatchingPotions', () => {
|
||||
it('should only contain valid potion names', () => {
|
||||
const lastReleaseDate = maxBy(Object.values(HATCHING_POTIONS_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-${value.day}`));
|
||||
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-${lastReleaseDate.day}`));
|
||||
Object.keys(HATCHING_POTIONS_RELEASE_DATES).forEach(key => {
|
||||
expect(hatchingPotions.all[key], `${key} is not a valid potion name`).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
it('should contain a valid year, month and date', () => {
|
||||
Object.keys(HATCHING_POTIONS_RELEASE_DATES).forEach(key => {
|
||||
const date = HATCHING_POTIONS_RELEASE_DATES[key];
|
||||
expect(date.year, `${key} year is not a valid year`).to.be.a('number');
|
||||
expect(date.year).to.be.at.least(2024);
|
||||
expect(date.month, `${key} month is not a valid month`).to.be.a('number');
|
||||
expect(date.month).to.be.within(1, 12);
|
||||
expect(date.day, `${key} day is not a valid day`).to.be.a('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 potions 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(potions.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;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,12 +6,21 @@ import {
|
||||
} from '../helpers/content.helper';
|
||||
import t from '../../website/common/script/content/translation';
|
||||
|
||||
import * as stable from '../../website/common/script/content/stable';
|
||||
import * as eggs from '../../website/common/script/content/eggs';
|
||||
import * as potions from '../../website/common/script/content/hatching-potions';
|
||||
import stable from '../../website/common/script/content/stable';
|
||||
import eggs from '../../website/common/script/content/eggs';
|
||||
import potions from '../../website/common/script/content/hatching-potions';
|
||||
|
||||
describe('stable', () => {
|
||||
describe('dropPets', () => {
|
||||
let clock;
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers(new Date('2020-05-20'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
it('contains a pet for each drop potion * each drop egg', () => {
|
||||
const numberOfDropPotions = Object.keys(potions.drops).length;
|
||||
const numberOfDropEggs = Object.keys(eggs.drops).length;
|
||||
|
||||
@@ -6,23 +6,105 @@ import timeTravelers from '../../website/common/script/content/time-travelers';
|
||||
|
||||
describe('time-travelers store', () => {
|
||||
let user;
|
||||
let date;
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
});
|
||||
|
||||
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;
|
||||
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_201601 = true; // eslint-disable-line camelcase
|
||||
const items = timeTravelers.timeTravelerStore(user, date);
|
||||
expect(items['201601']).to.not.exist;
|
||||
expect(items['201801']).to.exist;
|
||||
expect(items['202207']).to.exist;
|
||||
});
|
||||
|
||||
it('removes unopened mystery item sets from the time travelers store', () => {
|
||||
user.purchased = {
|
||||
plan: {
|
||||
mysteryItems: ['head_mystery_201601'],
|
||||
},
|
||||
};
|
||||
const items = timeTravelers.timeTravelerStore(user, date);
|
||||
expect(items['201601']).to.not.exist;
|
||||
expect(items['201607']).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
it('removes unopened mystery item sets from the time travelers store', () => {
|
||||
user.purchased = {
|
||||
plan: {
|
||||
mysteryItems: ['head_mystery_201602'],
|
||||
},
|
||||
};
|
||||
expect(timeTravelers.timeTravelerStore(user)['201602']).to.not.exist;
|
||||
expect(timeTravelers.timeTravelerStore(user)['201603']).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(),
|
||||
@@ -59,7 +60,17 @@ export function generateReq (options = {}) {
|
||||
header (header) {
|
||||
return this.headers[header];
|
||||
},
|
||||
listeners: {},
|
||||
session: {},
|
||||
on (key, func) {
|
||||
if (!this.listeners[key]) {
|
||||
this.listeners[key] = [];
|
||||
}
|
||||
this.listeners[key].push(func);
|
||||
},
|
||||
end () {
|
||||
this.listeners.close.forEach(func => func());
|
||||
},
|
||||
};
|
||||
|
||||
const req = defaultsDeep(options, defaultReq);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Generated
+363
-66
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,15 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading-screen">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.62 28.86c-.32-.706-1.057-1.048-1.538-.706-.48.341-1.147.393-1.78.24-.633-.153-.753-1.604-.616-3.278.136-1.673.363-2.318.506-2.925.162-.61.877-.562.962-.084.086.479.582.479 1.307-.391.724-.87.617-3.409-.218-5.474-.836-2.065.326-1.865.664-1.66.337.205.544-.102.462-1.28-.082-1.178-1.166-2.098-2.039-2.663-.873-.564-1.936-1.186-1.911-2.697.025-1.511 2.08-1.464 2.358-1.439.279.025.815-.093.506-1.663-.31-1.57-1.43-1.869-2.133-1.826-.703.042-1.177.428-2.17.053-.995-.376-1.655-.23-2.58-.023-.926.206-2.138.776-3.646 1.183-.795.219-1.064.274-1.93.288-.532.008-.755.653-.043 1.444.563.643 1.839.814 2.606.707.494-.07.608.258.563.74a8.013 8.013 0 0 0-.01 1.795c.08.6.18 1.62-.103 2.286-.14.326-.545.677-.98.653-.565-.034-1.022-.7-1.414-1.49-.825-1.662-1.793-2.014-5.404-3.535-3.248-1.367-5.007-3.5-6.096-4.874-.969-1.217-1.939-.756-1.85.342.07.852.592 3.604 1.912 5.257 1.623 2.525 4.128 3.67 7.013 3.895.755.06 1.226.208 1.29.553.095.735-.622 1.244-1.959 1.09-1.336-.157-1.907.087-1.641.848.85 1.79 2.809 1.869 3.623 1.942.275.05 1.246 0 1.764.143.605.166.735 1.005-.14 1.459-1.558.76-2.237 1.391-3.025 2.83-.595 1.13-1.108 3.022-.574 5.745.513 2.648-3.337 2.733-5 2.357-.716-.151-1.47-1.512.287-2.65 1.421-.922 1.708-1.49 1.645-2.657-.074-1.36-.824-1.458-.822-2.64v-2.82a.435.435 0 0 0-.435-.435H7.698a.435.435 0 0 1-.435-.434v-1.7a.435.435 0 0 0-.435-.435H5.501a.435.435 0 0 1-.435-.435v-1.524a.435.435 0 0 0-.435-.435H3.015a.435.435 0 0 1-.435-.435v-1.603a.435.435 0 0 0-.435-.434H.435a.435.435 0 0 0-.435.434v1.705c0 .24.195.435.435.435h1.62c.24 0 .435.195.435.435v6.076c0 .241.195.435.435.435h1.71c.241 0 .436.196.436.435v1.988c0 .24.195.434.435.434h2.402c.734-.052.862.934.854 1.286-.016.803-.923 1.06-1.352 1.395-1.145.884-2.031 1.783-1.513 3.512l.013.036c.945 2.007 3.542 1.8 5.183 1.8h10.326c.584 0 1.184.135 1.046-.545-.136-.68-.425-1.61-1.265-1.61-.84 0-.703.467-1.524.228-.821-.238-.822-1.348.411-3.279 1.276-1.649 3.46-1.524 4.781-.358 1.32 1.166.93 3.191.653 4.354-.158.82.218 1.224.669 1.213h5.242c.806-.014.647-.556.185-1.614h.003z" fill="#fff"/>
|
||||
<svg width="80" height="80" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M79.05 72.15c-.8-1.766-2.643-2.62-3.845-1.766-1.201.855-2.867.985-4.448.602-1.584-.385-1.885-4.01-1.543-8.195.342-4.184.909-5.795 1.267-7.314.404-1.524 2.191-1.404 2.405-.209.215 1.196 1.454 1.196 3.266-.979 1.811-2.175 1.543-8.52-.546-13.684-2.088-5.163.817-4.661 1.66-4.149.844.513 1.362-.255 1.156-3.2-.204-2.945-2.916-5.247-5.096-6.657-2.184-1.41-4.842-2.967-4.78-6.745.063-3.777 5.2-3.658 5.897-3.596.697.063 2.037-.233 1.264-4.157-.773-3.924-3.575-4.673-5.332-4.567-1.758.106-2.943 1.071-5.427.133-2.484-.938-4.136-.572-6.45-.057-2.313.515-5.343 1.94-9.112 2.959-1.989.545-2.661.683-4.828.718-1.33.02-1.885 1.633-.106 3.61 1.408 1.608 4.597 2.036 6.515 1.768 1.236-.174 1.521.645 1.407 1.85a20.023 20.023 0 0 0-.024 4.488c.198 1.5.45 4.051-.258 5.713-.35.817-1.361 1.693-2.449 1.633-1.413-.084-2.555-1.75-3.537-3.726-2.06-4.152-4.48-5.033-13.509-8.835-8.12-3.417-12.516-8.749-15.24-12.185-2.421-3.042-4.846-1.89-4.626.855.179 2.128 1.48 9.008 4.781 13.141 4.058 6.314 10.32 9.177 17.534 9.739 1.885.149 3.065.52 3.225 1.383.236 1.835-1.557 3.11-4.898 2.722-3.341-.39-4.768.22-4.103 2.121 2.123 4.477 7.021 4.672 9.058 4.857.686.122 3.114 0 4.41.355 1.51.418 1.836 2.514-.353 3.648-3.892 1.903-5.59 3.479-7.561 7.075-1.486 2.826-2.77 7.555-1.435 14.365 1.283 6.62-8.342 6.83-12.497 5.89-1.793-.377-3.675-3.778.716-6.625 3.553-2.305 4.269-3.724 4.111-6.642-.184-3.4-2.058-3.644-2.053-6.598v-7.05c0-.602-.488-1.088-1.087-1.088h-3.334a1.087 1.087 0 0 1-1.087-1.087v-4.25c0-.602-.488-1.087-1.088-1.087h-3.317a1.087 1.087 0 0 1-1.087-1.088v-3.81c0-.602-.489-1.087-1.088-1.087h-4.04a1.087 1.087 0 0 1-1.089-1.088V26.25c0-.602-.488-1.088-1.087-1.088H1.088C.485 25.161 0 25.65 0 26.25v4.26c0 .602.488 1.087 1.088 1.087h4.049c.602 0 1.087.489 1.087 1.088v15.192c0 .602.489 1.087 1.088 1.087h4.277c.602 0 1.088.489 1.088 1.088v4.968c0 .602.488 1.087 1.087 1.087h6.005c1.836-.13 2.156 2.335 2.137 3.214-.04 2.007-2.308 2.652-3.382 3.487-2.861 2.21-5.077 4.459-3.78 8.781l.032.09c2.362 5.017 8.855 4.499 12.956 4.499h25.817c1.459 0 2.959.339 2.614-1.362-.342-1.7-1.063-4.024-3.162-4.024-2.1 0-1.758 1.166-3.81.57-2.054-.597-2.057-3.371 1.027-8.198 3.19-4.122 8.652-3.81 11.952-.895 3.301 2.915 2.325 7.978 1.633 10.885-.396 2.048.545 3.06 1.67 3.032H78.58c2.015-.035 1.62-1.391.464-4.035h.008z"
|
||||
fill="#fff"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
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,9 +59,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.flat .item {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
.flat {
|
||||
.item {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.item-wrapper:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bordered-item .item {
|
||||
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -28,15 +28,15 @@
|
||||
<div class="form-group">
|
||||
<label>About</label>
|
||||
<div class="row about-row">
|
||||
<textarea
|
||||
v-model="hero.profile.blurb"
|
||||
class="form-control col"
|
||||
rows="10"
|
||||
></textarea>
|
||||
<div
|
||||
v-markdown="hero.profile.blurb"
|
||||
class="markdownPreview col"
|
||||
></div>
|
||||
<textarea
|
||||
v-model="hero.profile.blurb"
|
||||
class="form-control col"
|
||||
rows="10"
|
||||
></textarea>
|
||||
<div
|
||||
v-markdown="hero.profile.blurb"
|
||||
class="markdownPreview col"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
|
||||
@@ -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',
|
||||
@@ -178,7 +193,7 @@ export default {
|
||||
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
||||
},
|
||||
specialShirts () {
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.specialShirtKeys;
|
||||
const options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
|
||||
return options;
|
||||
@@ -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);
|
||||
@@ -236,7 +266,7 @@ export default {
|
||||
animalItems (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 backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.animalItemKeys[category];
|
||||
|
||||
const noneOption = this.createGearItem(0, category, 'base', category);
|
||||
@@ -248,36 +278,22 @@ export default {
|
||||
for (const key of keys) {
|
||||
const newKey = `${category}_special_${key}`;
|
||||
const userPurchased = this.user.items.gear.owned[newKey];
|
||||
|
||||
const option = {};
|
||||
option.key = key;
|
||||
option.active = this.user.preferences.costume
|
||||
? this.user.items.gear.costume[category] === newKey
|
||||
: this.user.items.gear.equipped[category] === newKey;
|
||||
option.class = `headAccessory_special_${option.key} ${category}`;
|
||||
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);
|
||||
if (userPurchased) {
|
||||
const option = {};
|
||||
option.key = key;
|
||||
option.active = this.user.preferences.costume
|
||||
? this.user.items.gear.costume[category] === newKey
|
||||
: this.user.items.gear.equipped[category] === newKey;
|
||||
option.class = `headAccessory_special_${option.key} ${category}`;
|
||||
if (category === 'back') {
|
||||
option.class = `icon_back_special_${option.key} back`;
|
||||
}
|
||||
const type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||
return this.equip(newKey, type);
|
||||
};
|
||||
|
||||
options.push(option);
|
||||
option.click = () => {
|
||||
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
|
||||
userHairStyles () {
|
||||
const emptyHairStyle = {
|
||||
...this.mapKeysToFreeOption(0, 'hair', 'base'),
|
||||
none: true,
|
||||
};
|
||||
const freeHairStyles = [1, 3].map(s => this.mapKeysToFreeOption(s, 'hair', 'base'));
|
||||
const ownedHairStyles = Object.keys(this.user.purchased.hair.base || {})
|
||||
.filter(k => this.user.purchased.hair.base[k])
|
||||
.map(h => this.mapKeysToFreeOption(h, 'hair', 'base'));
|
||||
return [emptyHairStyle, ...freeHairStyles, ...ownedHairStyles];
|
||||
},
|
||||
userMustaches () {
|
||||
const emptyMustache = {
|
||||
...this.mapKeysToFreeOption(0, 'hair', 'mustache'),
|
||||
none: true,
|
||||
};
|
||||
const ownedMustaches = Object.keys(this.user.purchased.hair.mustache || {})
|
||||
.filter(k => this.user.purchased.hair.mustache[k])
|
||||
.map(h => this.mapKeysToFreeOption(h, 'hair', 'mustache'));
|
||||
|
||||
const seasonalHairColors = [];
|
||||
for (const key of Object.keys(hairColorBySet)) {
|
||||
const set = hairColorBySet[key];
|
||||
return [emptyMustache, ...ownedMustaches];
|
||||
},
|
||||
userBeards () {
|
||||
const emptyBeard = {
|
||||
...this.mapKeysToFreeOption(0, 'hair', 'beard'),
|
||||
none: true,
|
||||
};
|
||||
const ownedBeards = Object.keys(this.user.purchased.hair.beard || {})
|
||||
.filter(k => this.user.purchased.hair.beard[k])
|
||||
.map(h => this.mapKeysToFreeOption(h, 'hair', 'beard'));
|
||||
|
||||
const keys = set.map(item => item.key);
|
||||
|
||||
const options = keys.map(optionKey => {
|
||||
const option = this.mapKeysToOption(optionKey, 'hair', 'color', key);
|
||||
return option;
|
||||
});
|
||||
|
||||
let text = this.$t(key);
|
||||
if (appearanceSets[key] && appearanceSets[key].text) {
|
||||
text = appearanceSets[key].text();
|
||||
}
|
||||
|
||||
const compiledSet = {
|
||||
key,
|
||||
options,
|
||||
keys,
|
||||
text,
|
||||
};
|
||||
seasonalHairColors.push(compiledSet);
|
||||
}
|
||||
|
||||
return seasonalHairColors;
|
||||
},
|
||||
premiumHairColors () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
||||
// this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.premiumHairColorKeys;
|
||||
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'color'));
|
||||
return options;
|
||||
},
|
||||
baseHair2 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
||||
// this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.baseHair2Keys;
|
||||
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'base'));
|
||||
return options;
|
||||
},
|
||||
baseHair3 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
||||
// this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.baseHair3Keys;
|
||||
const options = keys.map(key => {
|
||||
const option = this.mapKeysToOption(key, 'hair', 'base');
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
},
|
||||
baseHair4 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
||||
// this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.baseHair4Keys;
|
||||
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'base'));
|
||||
return options;
|
||||
},
|
||||
baseHair5 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
||||
// this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.baseHair5Keys;
|
||||
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'mustache'));
|
||||
return options;
|
||||
},
|
||||
baseHair6 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
||||
// this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.baseHair6Keys;
|
||||
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'beard'));
|
||||
return options;
|
||||
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 () {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -320,7 +320,7 @@
|
||||
<script>
|
||||
import each from 'lodash/each';
|
||||
import * as quests from '@/../../common/script/content/quests';
|
||||
import { mountInfo, petInfo } from '@/../../common/script/content/stable';
|
||||
import stable from '@/../../common/script/content/stable';
|
||||
import content from '@/../../common/script/content';
|
||||
import gear from '@/../../common/script/content/gear';
|
||||
import styleHelper from '@/mixins/styleHelper';
|
||||
@@ -330,6 +330,8 @@ import userLink from '../userLink';
|
||||
import PurchaseHistoryTable from '../ui/purchaseHistoryTable.vue';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
|
||||
const { mountInfo, petInfo } = stable;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
userLink,
|
||||
|
||||
@@ -16,10 +16,13 @@
|
||||
class="brand"
|
||||
aria-label="Habitica"
|
||||
>
|
||||
<div
|
||||
<router-link to="/">
|
||||
<div
|
||||
class="logo svg-icon svg color gryphon"
|
||||
v-html="icons.melior"
|
||||
></div>
|
||||
></div>
|
||||
<div class="svg-icon"></div>
|
||||
</router-link>
|
||||
<div class="svg-icon"></div>
|
||||
</b-navbar-brand>
|
||||
<b-navbar-toggle
|
||||
@@ -134,6 +137,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,56 +134,57 @@
|
||||
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
|
||||
v-for="(group, key, index) in pets(petGroup, hideMissing, selectedSortBy, searchTextThrottled)"
|
||||
v-if="index === 0 || $_openedItemRows_isToggled(petGroup.key)"
|
||||
:key="key"
|
||||
class="pet-row d-flex"
|
||||
>
|
||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||
<div class="d-inline-flex flex-column">
|
||||
<div
|
||||
v-for="item in group"
|
||||
v-show="show('pet', item)"
|
||||
: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()"
|
||||
v-for="(group, key, index) in pets(petGroup, hideMissing, selectedSortBy, searchTextThrottled)"
|
||||
v-if="index === 0 || $_openedItemRows_isToggled(petGroup.key)"
|
||||
:key="key"
|
||||
class="pet-row d-flex"
|
||||
>
|
||||
<petItem
|
||||
:item="item"
|
||||
:popover-position="'top'"
|
||||
:show-popover="currentDraggingFood == null"
|
||||
:highlight-border="highlightPet == item.key"
|
||||
@click="petClicked(item)"
|
||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||
<div
|
||||
v-for="item in group"
|
||||
v-show="show('pet', item)"
|
||||
:key="item.key"
|
||||
v-drag.drop.food="item.key"
|
||||
class="pet-group"
|
||||
@itemDragOver="onDragOver($event, item)"
|
||||
@itemDropped="onDrop($event, item)"
|
||||
@itemDragLeave="onDragLeave()"
|
||||
>
|
||||
<template
|
||||
slot="itemBadge"
|
||||
slot-scope="context"
|
||||
<petItem
|
||||
:item="item"
|
||||
:popover-position="'top'"
|
||||
:show-popover="currentDraggingFood == null"
|
||||
:highlight-border="highlightPet == item.key"
|
||||
@click="petClicked(item)"
|
||||
>
|
||||
<equip-badge
|
||||
:equipped="context.item.key === currentPet"
|
||||
:show="isOwned('pet', context.item)"
|
||||
@click="selectPet(context.item)"
|
||||
/>
|
||||
</template>
|
||||
</petItem>
|
||||
<template
|
||||
slot="itemBadge"
|
||||
slot-scope="context"
|
||||
>
|
||||
<equip-badge
|
||||
:equipped="context.item.key === currentPet"
|
||||
:show="isOwned('pet', context.item)"
|
||||
@click="selectPet(context.item)"
|
||||
/>
|
||||
</template>
|
||||
</petItem>
|
||||
</div>
|
||||
</div>
|
||||
<show-more-button
|
||||
v-if="petRowCount[petGroup.key] > 1 && petGroup.key !== 'specialPets' && !(petGroup.key === 'wackyPets' && selectedSortBy !== 'sortByColor')"
|
||||
:show-all="$_openedItemRows_isToggled(petGroup.key)"
|
||||
@click="setShowMore(petGroup.key)"
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
<h2>
|
||||
{{ $t('mounts') }}
|
||||
@@ -196,52 +197,55 @@
|
||||
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
|
||||
v-for="(group, key, index) in mounts(mountGroup, hideMissing, selectedSortBy, searchTextThrottled)"
|
||||
v-if="index === 0 || $_openedItemRows_isToggled(mountGroup.key)"
|
||||
:key="key"
|
||||
class="pet-row d-flex"
|
||||
>
|
||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||
<div class="d-inline-flex flex-column">
|
||||
<div
|
||||
v-for="item in group"
|
||||
v-show="show('mount', item)"
|
||||
:key="item.key"
|
||||
class="pet-group"
|
||||
v-for="(group, key, index) in mounts(mountGroup, hideMissing, selectedSortBy, searchTextThrottled)"
|
||||
v-if="index === 0 || $_openedItemRows_isToggled(mountGroup.key)"
|
||||
:key="key"
|
||||
class="pet-row d-flex"
|
||||
>
|
||||
<mountItem
|
||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||
<div
|
||||
v-for="item in group"
|
||||
v-show="show('mount', item)"
|
||||
:key="item.key"
|
||||
:item="item"
|
||||
:popover-position="'top'"
|
||||
:show-popover="true"
|
||||
@click="selectMount(item)"
|
||||
class="pet-group"
|
||||
>
|
||||
<span slot="popoverContent">
|
||||
<h4 class="popover-content-title">{{ item.name }}</h4>
|
||||
</span>
|
||||
<template
|
||||
slot="itemBadge"
|
||||
<mountItem
|
||||
:key="item.key"
|
||||
:item="item"
|
||||
:popover-position="'top'"
|
||||
:show-popover="true"
|
||||
@click="selectMount(item)"
|
||||
>
|
||||
<equip-badge
|
||||
:equipped="item.key === currentMount"
|
||||
:show="isOwned('mount', item)"
|
||||
@click="selectMount(item)"
|
||||
/>
|
||||
</template>
|
||||
</mountItem>
|
||||
<span slot="popoverContent">
|
||||
<h4 class="popover-content-title">{{ item.name }}</h4>
|
||||
</span>
|
||||
<template
|
||||
slot="itemBadge"
|
||||
>
|
||||
<equip-badge
|
||||
:equipped="item.key === currentMount"
|
||||
:show="isOwned('mount', item)"
|
||||
@click="selectMount(item)"
|
||||
/>
|
||||
</template>
|
||||
</mountItem>
|
||||
</div>
|
||||
</div>
|
||||
<show-more-button
|
||||
v-if="mountRowCount[mountGroup.key] > 1 && mountGroup.key !== 'specialMounts'"
|
||||
:show-all="$_openedItemRows_isToggled(mountGroup.key)"
|
||||
@click="setShowMore(mountGroup.key)"
|
||||
/>
|
||||
</div>
|
||||
<show-more-button
|
||||
v-if="mountRowCount[mountGroup.key] > 1 && mountGroup.key !== 'specialMounts'"
|
||||
:show-all="$_openedItemRows_isToggled(mountGroup.key)"
|
||||
@click="setShowMore(mountGroup.key)"
|
||||
/>
|
||||
</div>
|
||||
<inventoryDrawer>
|
||||
<template
|
||||
@@ -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;
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mountInfo } from '@/../../common/script/content/stable';
|
||||
import stable from '@/../../common/script/content/stable';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
|
||||
export default {
|
||||
@@ -105,7 +105,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
openDialog (mountKey) {
|
||||
this.mount = mountInfo[mountKey];
|
||||
this.mount = stable.mountInfo[mountKey];
|
||||
this.$root.$emit('bv::show::modal', 'mount-raised-modal');
|
||||
},
|
||||
close () {
|
||||
|
||||
@@ -1,47 +1,41 @@
|
||||
<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 {
|
||||
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;
|
||||
}
|
||||
.currency-value {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.33;
|
||||
color: $gray-100;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.notEnough {
|
||||
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;
|
||||
@@ -600,10 +586,12 @@ import reduce from 'lodash/reduce';
|
||||
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 eggs from '@/../../common/script/content/eggs';
|
||||
import hatchingPotions from '@/../../common/script/content/hatching-potions';
|
||||
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';
|
||||
@@ -629,6 +617,9 @@ import EquipmentAttributesGrid from '../inventory/equipment/attributesGrid.vue';
|
||||
import Item from '@/components/inventory/item';
|
||||
import Avatar from '@/components/avatar';
|
||||
|
||||
const dropEggs = eggs.drops;
|
||||
const dropPotions = hatchingPotions.drops;
|
||||
|
||||
const dropEggKeys = keys(dropEggs);
|
||||
|
||||
const amountOfDropEggs = size(dropEggs);
|
||||
@@ -637,7 +628,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 +641,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 +689,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 +741,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 +759,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,17 +824,25 @@ export default {
|
||||
) return;
|
||||
}
|
||||
|
||||
const shouldConfirmPurchase = this.item.currency === 'gems' || this.item.currency === 'hourglasses';
|
||||
if (
|
||||
shouldConfirmPurchase
|
||||
&& !this.confirmPurchase(this.item.currency, this.item.value * this.selectedAmountToBuy)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.genericPurchase) {
|
||||
this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
|
||||
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
|
||||
&& !this.confirmPurchase(this.item.currency, this.item.value * this.selectedAmountToBuy)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (this.genericPurchase) {
|
||||
this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
|
||||
this.purchased(this.item.text);
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('buyPressed', this.item);
|
||||
@@ -891,6 +899,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 {};
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -3,26 +3,32 @@
|
||||
<secondary-menu class="col-12">
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:to="{name: 'market'}"
|
||||
:to="{ name: 'market' }"
|
||||
exact="exact"
|
||||
>
|
||||
{{ $t('market') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:to="{name: 'quests'}"
|
||||
:to="{ name: 'quests' }"
|
||||
>
|
||||
{{ $t('quests') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:to="{name: 'seasonal'}"
|
||||
:to="{ name: 'customizations' }"
|
||||
>
|
||||
{{ $t('customizations') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:to="{ name: 'seasonal' }"
|
||||
>
|
||||
{{ $t('titleSeasonalShop') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:to="{name: 'time'}"
|
||||
:to="{ name: 'time' }"
|
||||
>
|
||||
{{ $t('titleTimeTravelers') }}
|
||||
</router-link>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user