diff --git a/.gitignore b/.gitignore index 85493ee023..f80fc6ce3d 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/config.json.example b/config.json.example index ebca73675c..520c122003 100644 --- a/config.json.example +++ b/config.json.example @@ -89,5 +89,8 @@ "REDIS_HOST": "aaabbbcccdddeeefff", "REDIS_PORT": "1234", "REDIS_PASSWORD": "12345678", - "TRUSTED_DOMAINS": "localhost,https://habitica.com" + "TRUSTED_DOMAINS": "localhost,https://habitica.com", + "TIME_TRAVEL_ENABLED": "false", + "DEBUG_ENABLED": "false", + "CONTENT_SWITCHOVER_TIME_OFFSET": 8 } diff --git a/gulp/gulp-tests.js b/gulp/gulp-tests.js index f46fd53fa7..64ac63902f 100644 --- a/gulp/gulp-tests.js +++ b/gulp/gulp-tests.js @@ -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, ), )); diff --git a/habitica-images b/habitica-images index aa72332019..617a3d6e6c 160000 --- a/habitica-images +++ b/habitica-images @@ -1 +1 @@ -Subproject commit aa723320199d7f03ce749d431b46e8d7f95cc8de +Subproject commit 617a3d6e6cd870172690a22b4dd16327f5e9d997 diff --git a/migrations/archive/2024/20240621_veteran_pets.js b/migrations/archive/2024/20240621_veteran_pets.js new file mode 100644 index 0000000000..bc4617084a --- /dev/null +++ b/migrations/archive/2024/20240621_veteran_pets.js @@ -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 + } +}; diff --git a/package-lock.json b/package-lock.json index 52ddb3a89a..f7d36bd71d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "habitica", - "version": "5.25.2", + "version": "5.25.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "habitica", - "version": "5.25.2", + "version": "5.25.8", "hasInstallScript": true, "dependencies": { "@babel/core": "^7.22.10", @@ -72,6 +72,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", @@ -89,13 +90,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" }, @@ -2814,6 +2813,86 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==" }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", @@ -3254,7 +3333,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, "dependencies": { "type-detect": "4.0.8" } @@ -3263,7 +3341,6 @@ "version": "10.3.0", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, "dependencies": { "@sinonjs/commons": "^3.0.0" } @@ -3272,7 +3349,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", - "dev": true, "dependencies": { "@sinonjs/commons": "^2.0.0", "lodash.get": "^4.4.2", @@ -3283,7 +3359,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, "dependencies": { "type-detect": "4.0.8" } @@ -3291,8 +3366,7 @@ "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==", - "dev": true + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==" }, "node_modules/@slack/types": { "version": "1.10.0", @@ -3852,6 +3926,28 @@ "node": ">= 6.0.0" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aggregate-error/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4114,18 +4210,6 @@ "node": ">=0.10.0" } }, - "node_modules/append-transform": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", - "integrity": "sha512-Yisb7ew0ZEyDtRYQ+b+26o9KbiYPFxwcsxKzbssigzRRMJ9LpExPVUg6Fos7eP7yP3q7///tzze4nm4lTptPBw==", - "dev": true, - "dependencies": { - "default-require-extensions": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/apple-auth": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/apple-auth/-/apple-auth-1.0.9.tgz", @@ -4684,112 +4768,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==", - "dev": true, - "dependencies": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - } - }, - "node_modules/babel-code-frame/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babel-code-frame/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babel-code-frame/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", - "dev": true, - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babel-code-frame/node_modules/js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", - "dev": true - }, - "node_modules/babel-code-frame/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babel-generator": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", - "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", - "dev": true, - "dependencies": { - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "detect-indent": "^4.0.0", - "jsesc": "^1.3.0", - "lodash": "^4.17.4", - "source-map": "^0.5.7", - "trim-right": "^1.0.1" - } - }, - "node_modules/babel-generator/node_modules/jsesc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", - "integrity": "sha512-Mke0DA0QjUWuJlhsE0ZPPhYiJkRap642SmI/4ztCFaUs6V2AiH1sfecc+57NgaryfAA2VR3v6O+CSjC1jZJKOA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - } - }, - "node_modules/babel-generator/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==", - "dev": true, - "dependencies": { - "babel-runtime": "^6.22.0" - } - }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.8", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz", @@ -4841,106 +4819,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", - "dev": true, - "dependencies": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - } - }, - "node_modules/babel-runtime/node_modules/regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - }, - "node_modules/babel-template": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", - "integrity": "sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==", - "dev": true, - "dependencies": { - "babel-runtime": "^6.26.0", - "babel-traverse": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "lodash": "^4.17.4" - } - }, - "node_modules/babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==", - "dev": true, - "dependencies": { - "babel-code-frame": "^6.26.0", - "babel-messages": "^6.23.0", - "babel-runtime": "^6.26.0", - "babel-types": "^6.26.0", - "babylon": "^6.18.0", - "debug": "^2.6.8", - "globals": "^9.18.0", - "invariant": "^2.2.2", - "lodash": "^4.17.4" - } - }, - "node_modules/babel-traverse/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/babel-traverse/node_modules/globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babel-traverse/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==", - "dev": true, - "dependencies": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - } - }, - "node_modules/babel-types/node_modules/to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true, - "bin": { - "babylon": "bin/babylon.js" - } - }, "node_modules/bach": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", @@ -6006,6 +5884,36 @@ "node": ">=8" } }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/caching-transform/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz", @@ -6240,6 +6148,15 @@ "node": ">=0.10.0" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -6885,14 +6802,6 @@ "node": ">=0.10.0" } }, - "node_modules/core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "dev": true, - "hasInstallScript": true - }, "node_modules/core-js-compat": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz", @@ -7360,18 +7269,6 @@ "node": ">=0.10.0" } }, - "node_modules/default-require-extensions": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-1.0.0.tgz", - "integrity": "sha512-Dn2eAftOqXhNXs5f/Xjn7QTZ6kDYkx7u0EXQInN1oyYwsZysu11q7oTtaKcbzLxZRJiDHa8VmwpWmb4lY5FqgA==", - "dev": true, - "dependencies": { - "strip-bom": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/default-resolution": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", @@ -7488,18 +7385,6 @@ "node": ">=0.10.0" } }, - "node_modules/detect-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", - "integrity": "sha512-BDKtmHlOzwI7iRuEkhzsnPoi5ypEhWAJB5RvHWe1kMr06js3uK5B3734i3ui5Yd+wOJV1cpE4JnivPD283GU/A==", - "dev": true, - "dependencies": { - "repeating": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/detect-libc": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", @@ -8002,6 +7887,12 @@ "node": ">=0.10" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, "node_modules/es6-iterator": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", @@ -9399,12 +9290,6 @@ "node": ">=0.10.0" } }, - "node_modules/expect.js": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz", - "integrity": "sha512-okDF/FAPEul1ZFLae4hrgpIqAeapoo5TRdcg/lD0iN9S3GWrBFIJwNezGH1DMtIz+RxU4RrFmMq7WUUvDg3J6A==", - "dev": true - }, "node_modules/expose-loader": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-3.1.0.tgz", @@ -9960,36 +9845,6 @@ "node": ">=4" } }, - "node_modules/fileset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", - "integrity": "sha512-UxowFKnAFIwtmSxgKjWAVgjE3Fk7MQJT0ZIyl0NwIFZTrx4913rLaonGJ84V+x/2+w/pe4ULHRns+GZPs1TVuw==", - "dev": true, - "dependencies": { - "glob": "^7.0.3", - "minimatch": "^3.0.3" - } - }, - "node_modules/fileset/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -10281,6 +10136,19 @@ "node": ">=0.10.0" } }, + "node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -10402,6 +10270,26 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -10464,19 +10352,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", @@ -10600,6 +10475,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-pixels": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/get-pixels/-/get-pixels-3.3.3.tgz", @@ -11909,7 +11793,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", - "devOptional": true, + "optional": true, "dependencies": { "ansi-regex": "^2.0.0" }, @@ -11921,7 +11805,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "devOptional": true, + "optional": true, "engines": { "node": ">=0.10.0" } @@ -11934,15 +11818,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/has-property-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", @@ -12074,6 +11949,31 @@ "node": ">=0.10.0" } }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasha/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", @@ -12147,6 +12047,12 @@ ], "optional": true }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -12711,15 +12617,6 @@ "node": ">=4" } }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", @@ -12926,7 +12823,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "devOptional": true, + "optional": true, "engines": { "node": ">=0.10.0" }, @@ -13261,216 +13158,39 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, - "node_modules/istanbul": { - "version": "1.1.0-alpha.1", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-1.1.0-alpha.1.tgz", - "integrity": "sha512-acLlAtOI1itxA7agb2+jKoRFjQa9vuV2G48jUyerkrPXaSBh0q4hMre7qYhGoskk8R5XFbt6v5mOITAlf5LFgQ==", + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", "dev": true, "dependencies": { - "abbrev": "1.0.x", - "async": "1.x", - "istanbul-api": "^1.1.0-alpha", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "which": "^1.1.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "istanbul": "lib/cli.js" - } - }, - "node_modules/istanbul-api": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-1.3.7.tgz", - "integrity": "sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA==", - "dev": true, - "dependencies": { - "async": "^2.1.4", - "fileset": "^2.0.2", - "istanbul-lib-coverage": "^1.2.1", - "istanbul-lib-hook": "^1.2.2", - "istanbul-lib-instrument": "^1.10.2", - "istanbul-lib-report": "^1.1.5", - "istanbul-lib-source-maps": "^1.2.6", - "istanbul-reports": "^1.5.1", - "js-yaml": "^3.7.0", - "mkdirp": "^0.5.1", - "once": "^1.4.0" - } - }, - "node_modules/istanbul-api/node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "dependencies": { - "lodash": "^4.17.14" - } - }, - "node_modules/istanbul-lib-coverage": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz", - "integrity": "sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ==", - "dev": true - }, - "node_modules/istanbul-lib-hook": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz", - "integrity": "sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw==", - "dev": true, - "dependencies": { - "append-transform": "^0.4.0" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz", - "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", - "dev": true, - "dependencies": { - "babel-generator": "^6.18.0", - "babel-template": "^6.16.0", - "babel-traverse": "^6.18.0", - "babel-types": "^6.18.0", - "babylon": "^6.18.0", - "istanbul-lib-coverage": "^1.2.1", - "semver": "^5.3.0" - } - }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/istanbul-lib-report": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz", - "integrity": "sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^1.2.1", - "mkdirp": "^0.5.1", - "path-parse": "^1.0.5", - "supports-color": "^3.1.2" - } - }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==", - "dev": true, - "dependencies": { - "has-flag": "^1.0.0" + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" }, "engines": { - "node": ">=0.8.0" + "node": ">=8" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz", - "integrity": "sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg==", - "dev": true, - "dependencies": { - "debug": "^3.1.0", - "istanbul-lib-coverage": "^1.2.1", - "mkdirp": "^0.5.1", - "rimraf": "^2.6.1", - "source-map": "^0.5.3" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "node_modules/istanbul-lib-processinfo/node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/istanbul-reports": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-1.5.1.tgz", - "integrity": "sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw==", + "node_modules/istanbul-lib-processinfo/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, - "dependencies": { - "handlebars": "^4.0.3" - } - }, - "node_modules/istanbul/node_modules/async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", - "dev": true - }, - "node_modules/istanbul/node_modules/nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", - "dev": true, - "dependencies": { - "abbrev": "1" - }, "bin": { - "nopt": "bin/nopt.js" - } - }, - "node_modules/istanbul/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" + "uuid": "dist/bin/uuid" } }, "node_modules/isurl": { @@ -13783,8 +13503,7 @@ "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==", - "dev": true + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==" }, "node_modules/jwa": { "version": "2.0.0", @@ -14103,11 +13822,16 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, "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==", - "dev": true + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, "node_modules/lodash.includes": { "version": "4.3.0", @@ -14213,18 +13937,6 @@ "node": ">=0.10.0" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, "node_modules/loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", @@ -15553,7 +15265,6 @@ "version": "5.1.9", "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", - "dev": true, "dependencies": { "@sinonjs/commons": "^3.0.0", "@sinonjs/fake-timers": "^11.2.2", @@ -15566,7 +15277,6 @@ "version": "11.2.2", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", - "dev": true, "dependencies": { "@sinonjs/commons": "^3.0.0" } @@ -15574,8 +15284,7 @@ "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==", - "dev": true + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" }, "node_modules/node-abi": { "version": "2.30.1", @@ -15687,6 +15396,18 @@ "node": "*" } }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -15894,6 +15615,453 @@ "node": ">=0.10.0" } }, + "node_modules/nyc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", + "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", + "dev": true, + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/nyc/node_modules/ansi-styles": { + "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" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/nyc/node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/node_modules/color-convert": { + "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" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/nyc/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==", + "dev": true + }, + "node_modules/nyc/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/nyc/node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/nyc/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/nyc/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nyc/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" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nyc/node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nyc/node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/nyc/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/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==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, + "node_modules/nyc/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/oauth": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", @@ -16365,6 +16533,18 @@ "node": ">=6" } }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-map-series": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz", @@ -16417,6 +16597,21 @@ "node": ">=6" } }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -17110,6 +17305,18 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "node_modules/process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -17623,6 +17830,18 @@ "jsesc": "bin/jsesc" } }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/remove-bom-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", @@ -17678,7 +17897,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", - "devOptional": true, + "optional": true, "dependencies": { "is-finite": "^1.0.0" }, @@ -18616,7 +18835,6 @@ "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz", "integrity": "sha512-nPS85arNqwBXaIsFCkolHjGIkFo+Oxu9vbgmBJizLAhqe6P2o3Qmj3KCUoRkfhHtvgDhZdWD3risLHAUJ8npjw==", "deprecated": "16.1.1", - "dev": true, "dependencies": { "@sinonjs/commons": "^3.0.0", "@sinonjs/fake-timers": "^10.3.0", @@ -18653,7 +18871,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", - "dev": true, "engines": { "node": ">=0.3.1" } @@ -18662,7 +18879,6 @@ "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" } @@ -18671,7 +18887,6 @@ "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" }, @@ -18931,6 +19146,38 @@ "memory-pager": "^1.0.2" } }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-wrap/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -19542,7 +19789,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "devOptional": true, + "optional": true, "engines": { "node": ">=0.8.0" } @@ -19936,6 +20183,40 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -20173,15 +20454,6 @@ "node": ">=0.10.0" } }, - "node_modules/trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -20269,7 +20541,6 @@ "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==", - "dev": true, "engines": { "node": ">=4" } @@ -20363,6 +20634,15 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, "node_modules/uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", @@ -21510,6 +21790,18 @@ "node": ">=4" } }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "node_modules/xml-crypto": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-0.10.1.tgz", diff --git a/package.json b/package.json index ea5bbc0c52..f166b266f1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "5.25.2", + "version": "5.25.8", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.22.10", @@ -67,6 +67,7 @@ "remove-markdown": "^0.5.0", "rimraf": "^3.0.2", "short-uuid": "^4.2.2", + "sinon": "^15.2.0", "stripe": "^12.18.0", "superagent": "^8.1.2", "universal-analytics": "^0.5.3", @@ -93,11 +94,11 @@ "test:api-v3:integration:separate-server": "NODE_ENV=test gulp test:api-v3:integration:separate-server", "test:api-v4:integration": "gulp test:api-v4:integration", "test:api-v4:integration:separate-server": "NODE_ENV=test gulp test:api-v4:integration:separate-server", - "test:sanity": "istanbul cover --dir coverage/sanity --report lcovonly node_modules/mocha/bin/_mocha -- test/sanity --recursive", - "test:common": "istanbul cover --dir coverage/common --report lcovonly node_modules/mocha/bin/_mocha -- test/common --recursive", - "test:content": "istanbul cover --dir coverage/content --report lcovonly node_modules/mocha/bin/_mocha -- test/content --recursive", + "test:sanity": "nyc --silent --no-clean mocha test/sanity --recursive", + "test:common": "nyc --silent --no-clean mocha test/common --recursive", + "test:content": "nyc --silent --no-clean mocha test/content --recursive", "test:nodemon": "gulp test:nodemon", - "coverage": "COVERAGE=true mocha --require register-handlers.js --reporter html-cov > coverage.html; open coverage.html", + "coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html", "sprites": "gulp sprites:compile", "client:dev": "cd website/client && npm run serve", "client:build": "cd website/client && npm run build", @@ -115,13 +116,11 @@ "chai-moment": "^0.1.0", "chalk": "^5.3.0", "cross-spawn": "^7.0.3", - "expect.js": "^0.3.1", - "istanbul": "^1.1.0-alpha.1", "mocha": "^5.1.1", "monk": "^7.3.4", + "nyc": "^15.1.0", "require-again": "^2.0.0", "run-rs": "^0.7.7", - "sinon": "^15.2.0", "sinon-chai": "^3.7.0", "sinon-stub-promise": "^4.0.0" } diff --git a/test/api/unit/libs/content.test.js b/test/api/unit/libs/content.test.js index 368d8fe9fd..53c85e4205 100644 --- a/test/api/unit/libs/content.test.js +++ b/test/api/unit/libs/content.test.js @@ -1,5 +1,9 @@ +import fs from 'fs'; import * as contentLib from '../../../../website/server/libs/content'; import content from '../../../../website/common/script/content'; +import { + generateRes, +} from '../../../helpers/api-unit.helper'; describe('contentLib', () => { describe('CONTENT_CACHE_PATH', () => { @@ -13,5 +17,90 @@ describe('contentLib', () => { contentLib.getLocalizedContentResponse(); expect(typeof content.backgrounds.backgrounds062014.beach.text).to.equal('function'); }); + + it('removes keys from the content data', () => { + const response = contentLib.localizeContentData(content, 'en', { backgroundsFlat: true, dropHatchingPotions: true }); + expect(response.backgroundsFlat).to.not.exist; + expect(response.backgrounds).to.exist; + expect(response.dropHatchingPotions).to.not.exist; + expect(response.hatchingPotions).to.exist; + }); + + it('removes nested keys from the content data', () => { + const response = contentLib.localizeContentData(content, 'en', { gear: { tree: true } }); + expect(response.gear.tree).to.not.exist; + expect(response.gear.flat).to.exist; + }); + }); + + it('generates a hash for a filter', () => { + const hash = contentLib.hashForFilter('backgroundsFlat,gear.flat'); + expect(hash).to.equal('-1791877526'); + }); + + it('serves content', () => { + const resSpy = generateRes(); + contentLib.serveContent(resSpy, 'en', '', false); + expect(resSpy.send).to.have.been.calledOnce; + }); + + it('serves filtered content', () => { + const resSpy = generateRes(); + contentLib.serveContent(resSpy, 'en', 'backgroundsFlat,gear.flat', false); + expect(resSpy.send).to.have.been.calledOnce; + }); + + describe('caches content', async () => { + let resSpy; + beforeEach(() => { + resSpy = generateRes(); + if (fs.existsSync(contentLib.CONTENT_CACHE_PATH)) { + fs.rmdirSync(contentLib.CONTENT_CACHE_PATH, { recursive: true }); + } + fs.mkdirSync(contentLib.CONTENT_CACHE_PATH); + }); + + it('does not cache requests in development mode', async () => { + contentLib.serveContent(resSpy, 'en', '', false); + expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.false; + }); + + it('caches unfiltered requests', async () => { + expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.false; + contentLib.serveContent(resSpy, 'en', '', true); + expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en.json`)).to.be.true; + }); + + it('serves cached requests', async () => { + fs.writeFileSync( + `${contentLib.CONTENT_CACHE_PATH}en.json`, + '{"success": true, "data": {"all": {}}}', + 'utf8', + ); + contentLib.serveContent(resSpy, 'en', '', true); + expect(resSpy.sendFile).to.have.been.calledOnce; + expect(resSpy.sendFile).to.have.been.calledWith(`${contentLib.CONTENT_CACHE_PATH}en.json`); + }); + + it('caches filtered requests', async () => { + const filter = 'backgroundsFlat,gear.flat'; + const hash = contentLib.hashForFilter(filter); + expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`)).to.be.false; + contentLib.serveContent(resSpy, 'en', filter, true); + expect(fs.existsSync(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`)).to.be.true; + }); + + it('serves filtered cached requests', async () => { + const filter = 'backgroundsFlat,gear.flat'; + const hash = contentLib.hashForFilter(filter); + fs.writeFileSync( + `${contentLib.CONTENT_CACHE_PATH}en${hash}.json`, + '{"success": true, "data": {}}', + 'utf8', + ); + contentLib.serveContent(resSpy, 'en', filter, true); + expect(resSpy.sendFile).to.have.been.calledOnce; + expect(resSpy.sendFile).to.have.been.calledWith(`${contentLib.CONTENT_CACHE_PATH}en${hash}.json`); + }); }); }); diff --git a/test/api/unit/libs/items/utils.test.js b/test/api/unit/libs/items/utils.test.js index d1bf1f126e..9ff116ef95 100644 --- a/test/api/unit/libs/items/utils.test.js +++ b/test/api/unit/libs/items/utils.test.js @@ -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); }); diff --git a/test/api/unit/middlewares/ensureDevelopmentMode.js b/test/api/unit/middlewares/ensureDevelopmentMode.js new file mode 100644 index 0000000000..da5d2cffc0 --- /dev/null +++ b/test/api/unit/middlewares/ensureDevelopmentMode.js @@ -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; + }); +}); diff --git a/test/api/unit/middlewares/ensureDevelpmentMode.js b/test/api/unit/middlewares/ensureDevelpmentMode.js deleted file mode 100644 index 39bbb2fe6f..0000000000 --- a/test/api/unit/middlewares/ensureDevelpmentMode.js +++ /dev/null @@ -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; - }); -}); diff --git a/test/api/unit/middlewares/ensureTimeTravelMode.js b/test/api/unit/middlewares/ensureTimeTravelMode.js new file mode 100644 index 0000000000..6be7d5f4eb --- /dev/null +++ b/test/api/unit/middlewares/ensureTimeTravelMode.js @@ -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; + }); +}); diff --git a/test/api/v3/integration/content/GET-content.test.js b/test/api/v3/integration/content/GET-content.test.js index a241058be5..0dbd2779e4 100644 --- a/test/api/v3/integration/content/GET-content.test.js +++ b/test/api/v3/integration/content/GET-content.test.js @@ -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'); + }); }); diff --git a/test/api/v3/integration/debug/GET-debug-timeTravelTime.test.js b/test/api/v3/integration/debug/GET-debug-timeTravelTime.test.js new file mode 100644 index 0000000000..c2bcc3b87b --- /dev/null +++ b/test/api/v3/integration/debug/GET-debug-timeTravelTime.test.js @@ -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.', + }); + }); +}); diff --git a/test/api/v3/integration/debug/POST-debug_addHourglass.test.js b/test/api/v3/integration/debug/POST-debug_addHourglass.test.js index b0ad288348..6d7650e07a 100644 --- a/test/api/v3/integration/debug/POST-debug_addHourglass.test.js +++ b/test/api/v3/integration/debug/POST-debug_addHourglass.test.js @@ -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({ diff --git a/test/api/v3/integration/debug/POST-debug_addTenGems.test.js b/test/api/v3/integration/debug/POST-debug_addTenGems.test.js index 74c39a153f..560381b4b0 100644 --- a/test/api/v3/integration/debug/POST-debug_addTenGems.test.js +++ b/test/api/v3/integration/debug/POST-debug_addTenGems.test.js @@ -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({ diff --git a/test/api/v3/integration/debug/POST-debug_jumpTime.test.js b/test/api/v3/integration/debug/POST-debug_jumpTime.test.js new file mode 100644 index 0000000000..68988f682d --- /dev/null +++ b/test/api/v3/integration/debug/POST-debug_jumpTime.test.js @@ -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.', + }); + }); +}); diff --git a/test/api/v3/integration/debug/POST-debug_make-admin.test.js b/test/api/v3/integration/debug/POST-debug_make-admin.test.js index c03fa4627f..dcbcc2685b 100644 --- a/test/api/v3/integration/debug/POST-debug_make-admin.test.js +++ b/test/api/v3/integration/debug/POST-debug_make-admin.test.js @@ -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({ diff --git a/test/api/v3/integration/debug/POST-debug_modify-inventory.test.js b/test/api/v3/integration/debug/POST-debug_modify-inventory.test.js index 8b9f6ac10d..1cd8e368b1 100644 --- a/test/api/v3/integration/debug/POST-debug_modify-inventory.test.js +++ b/test/api/v3/integration/debug/POST-debug_modify-inventory.test.js @@ -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({ diff --git a/test/api/v3/integration/debug/POST-debug_quest-progress.test.js b/test/api/v3/integration/debug/POST-debug_quest-progress.test.js index c74f3bf6af..a0bc27f379 100644 --- a/test/api/v3/integration/debug/POST-debug_quest-progress.test.js +++ b/test/api/v3/integration/debug/POST-debug_quest-progress.test.js @@ -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({ diff --git a/test/api/v3/integration/debug/POST-debug_set-cron.test.js b/test/api/v3/integration/debug/POST-debug_set-cron.test.js index dfcd99c625..671160e552 100644 --- a/test/api/v3/integration/debug/POST-debug_set-cron.test.js +++ b/test/api/v3/integration/debug/POST-debug_set-cron.test.js @@ -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({ diff --git a/test/api/v3/integration/shops/GET-shops_backgrounds.test.js b/test/api/v3/integration/shops/GET-shops_backgrounds.test.js index 387a3bf40f..ec38a83460 100644 --- a/test/api/v3/integration/shops/GET-shops_backgrounds.test.js +++ b/test/api/v3/integration/shops/GET-shops_backgrounds.test.js @@ -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'); }); }); diff --git a/test/api/v3/integration/shops/GET-shops_time_travelers.test.js b/test/api/v3/integration/shops/GET-shops_time_travelers.test.js index 3e81459eac..cfe384bf1c 100644 --- a/test/api/v3/integration/shops/GET-shops_time_travelers.test.js +++ b/test/api/v3/integration/shops/GET-shops_time_travelers.test.js @@ -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 () => { diff --git a/test/api/v3/integration/user/POST-user_purchase.test.js b/test/api/v3/integration/user/POST-user_purchase.test.js index c21692bc4c..0763913717 100644 --- a/test/api/v3/integration/user/POST-user_purchase.test.js +++ b/test/api/v3/integration/user/POST-user_purchase.test.js @@ -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({ diff --git a/test/api/v3/integration/user/POST-user_unlock.js b/test/api/v3/integration/user/POST-user_unlock.js index 26016ef275..eeae6ceb36 100644 --- a/test/api/v3/integration/user/POST-user_unlock.js +++ b/test/api/v3/integration/user/POST-user_unlock.js @@ -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; diff --git a/test/api/v3/integration/user/PUT-user.test.js b/test/api/v3/integration/user/PUT-user.test.js index 49f365465a..6f1b2b5bba 100644 --- a/test/api/v3/integration/user/PUT-user.test.js +++ b/test/api/v3/integration/user/PUT-user.test.js @@ -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', () => { diff --git a/test/api/v3/integration/user/buy/POST-user_buy.test.js b/test/api/v3/integration/user/buy/POST-user_buy.test.js index f91831d37a..4590d7bc62 100644 --- a/test/api/v3/integration/user/buy/POST-user_buy.test.js +++ b/test/api/v3/integration/user/buy/POST-user_buy.test.js @@ -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 () => { diff --git a/test/api/v3/integration/world-state/GET-world-state.test.js b/test/api/v3/integration/world-state/GET-world-state.test.js index cec7086d6f..6c33296921 100644 --- a/test/api/v3/integration/world-state/GET-world-state.test.js +++ b/test/api/v3/integration/world-state/GET-world-state.test.js @@ -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'); + }); + }); }); diff --git a/test/common/fns/datedMemoize.test.js b/test/common/fns/datedMemoize.test.js new file mode 100644 index 0000000000..1e32eb3888 --- /dev/null +++ b/test/common/fns/datedMemoize.test.js @@ -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; + }); +}); diff --git a/test/common/libs/cleanupPinnedItems.test.js b/test/common/libs/cleanupPinnedItems.test.js new file mode 100644 index 0000000000..1c1cb2ee43 --- /dev/null +++ b/test/common/libs/cleanupPinnedItems.test.js @@ -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; + }); +}); diff --git a/test/common/libs/shops.js b/test/common/libs/shops.js deleted file mode 100644 index 54ed48dc54..0000000000 --- a/test/common/libs/shops.js +++ /dev/null @@ -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); - }); - }); - }); - }); - }); -}); diff --git a/test/common/libs/shops.test.js b/test/common/libs/shops.test.js new file mode 100644 index 0000000000..576d31c32d --- /dev/null +++ b/test/common/libs/shops.test.js @@ -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); + } + }); + }); + }); + }); +}); diff --git a/test/common/ops/armoireCanOwn.js b/test/common/ops/armoireCanOwn.js index a0dd802aa0..a226c91df2 100644 --- a/test/common/ops/armoireCanOwn.js +++ b/test/common/ops/armoireCanOwn.js @@ -1,11 +1,10 @@ -import * as armoireSet from '../../../website/common/script/content/gear/sets/armoire'; +import armoireSet from '../../../website/common/script/content/gear/sets/armoire'; describe('armoireSet items', () => { it('checks if canOwn has the same id', () => { Object.keys(armoireSet).forEach(type => { Object.keys(armoireSet[type]).forEach(itemKey => { const ownedKey = `${type}_armoire_${itemKey}`; - expect(armoireSet[type][itemKey].canOwn({ items: { gear: { diff --git a/test/common/ops/buy/buy.js b/test/common/ops/buy/buy.test.js similarity index 99% rename from test/common/ops/buy/buy.js rename to test/common/ops/buy/buy.test.js index 2ac5fc6407..ff553f1aac 100644 --- a/test/common/ops/buy/buy.js +++ b/test/common/ops/buy/buy.test.js @@ -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); diff --git a/test/common/ops/buy/buyArmoire.js b/test/common/ops/buy/buyArmoire.test.js similarity index 98% rename from test/common/ops/buy/buyArmoire.js rename to test/common/ops/buy/buyArmoire.test.js index b1e1545f91..172625a9f6 100644 --- a/test/common/ops/buy/buyArmoire.js +++ b/test/common/ops/buy/buyArmoire.test.js @@ -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; }); }); diff --git a/test/common/ops/buy/buyGem.js b/test/common/ops/buy/buyGem.test.js similarity index 100% rename from test/common/ops/buy/buyGem.js rename to test/common/ops/buy/buyGem.test.js diff --git a/test/common/ops/buy/buyHealthPotion.js b/test/common/ops/buy/buyHealthPotion.test.js similarity index 100% rename from test/common/ops/buy/buyHealthPotion.js rename to test/common/ops/buy/buyHealthPotion.test.js diff --git a/test/common/ops/buy/buyMarketGear.js b/test/common/ops/buy/buyMarketGear.test.js similarity index 81% rename from test/common/ops/buy/buyMarketGear.js rename to test/common/ops/buy/buyMarketGear.test.js index 37725ff1b1..82d9b9ef53 100644 --- a/test/common/ops/buy/buyMarketGear.js +++ b/test/common/ops/buy/buyMarketGear.test.js @@ -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'); + } + }); }); }); diff --git a/test/common/ops/buy/buyMysterySet.js b/test/common/ops/buy/buyMysterySet.test.js similarity index 71% rename from test/common/ops/buy/buyMysterySet.js rename to test/common/ops/buy/buyMysterySet.test.js index 1a75de3451..f2d74046ba 100644 --- a/test/common/ops/buy/buyMysterySet.js +++ b/test/common/ops/buy/buyMysterySet.test.js @@ -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); + }); }); }); }); diff --git a/test/common/ops/buy/buyQuestGems.js b/test/common/ops/buy/buyQuestGems.test.js similarity index 66% rename from test/common/ops/buy/buyQuestGems.js rename to test/common/ops/buy/buyQuestGems.test.js index bb5346b420..aa5080a526 100644 --- a/test/common/ops/buy/buyQuestGems.js +++ b/test/common/ops/buy/buyQuestGems.test.js @@ -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; diff --git a/test/common/ops/buy/buyQuestGold.js b/test/common/ops/buy/buyQuestGold.test.js similarity index 100% rename from test/common/ops/buy/buyQuestGold.js rename to test/common/ops/buy/buyQuestGold.test.js diff --git a/test/common/ops/buy/buySpell.js b/test/common/ops/buy/buySpell.js deleted file mode 100644 index f752e6c5a8..0000000000 --- a/test/common/ops/buy/buySpell.js +++ /dev/null @@ -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; - }); -}); diff --git a/test/common/ops/buy/buySpell.test.js b/test/common/ops/buy/buySpell.test.js new file mode 100644 index 0000000000..f914ca8c6f --- /dev/null +++ b/test/common/ops/buy/buySpell.test.js @@ -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')); + } + }); + }); +}); diff --git a/test/common/ops/buy/hourglassPurchase.js b/test/common/ops/buy/hourglassPurchase.test.js similarity index 100% rename from test/common/ops/buy/hourglassPurchase.js rename to test/common/ops/buy/hourglassPurchase.test.js diff --git a/test/common/ops/buy/purchase.js b/test/common/ops/buy/purchase.test.js similarity index 67% rename from test/common/ops/buy/purchase.js rename to test/common/ops/buy/purchase.test.js index 8f70523d1c..a61b3a0d59 100644 --- a/test/common/ops/buy/purchase.js +++ b/test/common/ops/buy/purchase.test.js @@ -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')); + } + }); }); }); diff --git a/test/common/ops/unlock.js b/test/common/ops/unlock.test.js similarity index 83% rename from test/common/ops/unlock.js rename to test/common/ops/unlock.test.js index f08608e615..24aed655a5 100644 --- a/test/common/ops/unlock.js +++ b/test/common/ops/unlock.test.js @@ -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')); + } + }); }); diff --git a/test/common/test_helper.js b/test/common/test_helper.js deleted file mode 100644 index c04e76f478..0000000000 --- a/test/common/test_helper.js +++ /dev/null @@ -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'); - }; -}; diff --git a/test/content/armoire.test.js b/test/content/armoire.test.js new file mode 100644 index 0000000000..cbcb0e253c --- /dev/null +++ b/test/content/armoire.test.js @@ -0,0 +1,83 @@ +/* eslint-disable global-require */ +import forEach from 'lodash/forEach'; +import { + expectValidTranslationString, +} from '../helpers/content.helper'; + +function makeArmoireIitemList () { + const armoire = require('../../website/common/script/content/gear/sets/armoire').default; + const items = []; + items.push(...Object.values(armoire.armor)); + items.push(...Object.values(armoire.body)); + items.push(...Object.values(armoire.eyewear)); + items.push(...Object.values(armoire.head)); + items.push(...Object.values(armoire.headAccessory)); + items.push(...Object.values(armoire.shield)); + items.push(...Object.values(armoire.weapon)); + return items; +} + +describe('armoire', () => { + let clock; + beforeEach(() => { + delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')]; + }); + afterEach(() => { + clock.restore(); + }); + it('does not return unreleased gear', async () => { + clock = sinon.useFakeTimers(new Date('2024-01-02')); + const items = makeArmoireIitemList(); + expect(items.length).to.equal(377); + expect(items.filter(item => item.set === 'pottersSet' || item.set === 'optimistSet' || item.set === 'schoolUniform')).to.be.an('array').that.is.empty; + }); + + it('released gear has all required properties', async () => { + clock = sinon.useFakeTimers(new Date('2024-05-08')); + const items = makeArmoireIitemList(); + expect(items.length).to.equal(396); + forEach(items, item => { + if (item.set !== undefined) { + expect(item.set, item.key).to.be.a('string'); + expect(item.set, item.key).to.not.be.empty; + } + expectValidTranslationString(item.text); + expect(item.value, item.key).to.be.a('number'); + }); + }); + + it('releases gear when appropriate', async () => { + clock = sinon.useFakeTimers(new Date('2024-01-01T00:00:00.000Z')); + const items = makeArmoireIitemList(); + expect(items.length).to.equal(377); + clock.restore(); + delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')]; + clock = sinon.useFakeTimers(new Date('2024-01-08')); + const januaryItems = makeArmoireIitemList(); + expect(januaryItems.length).to.equal(381); + clock.restore(); + delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')]; + clock = sinon.useFakeTimers(new Date('2024-02-07')); + const januaryItems2 = makeArmoireIitemList(); + expect(januaryItems2.length).to.equal(381); + clock.restore(); + delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')]; + clock = sinon.useFakeTimers(new Date('2024-02-07T16:00:00.000Z')); + const febuaryItems = makeArmoireIitemList(); + expect(febuaryItems.length).to.equal(384); + }); + + it('sets have at least 2 items', () => { + const armoire = makeArmoireIitemList(); + const setMap = {}; + forEach(armoire, item => { + if (setMap[item.set] === undefined) { + setMap[item.set] = 0; + } + setMap[item.set] += 1; + }); + Object.keys(setMap).forEach(set => { + expect(setMap[set], set).to.be.at.least(2); + }); + }); +}); diff --git a/test/content/events.test.js b/test/content/events.test.js new file mode 100644 index 0000000000..03ffd08ed9 --- /dev/null +++ b/test/content/events.test.js @@ -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'); + }); +}); diff --git a/test/content/food.test.js b/test/content/food.test.js new file mode 100644 index 0000000000..5cdb2b80bc --- /dev/null +++ b/test/content/food.test.js @@ -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); + }); +}); diff --git a/test/content/gear.js b/test/content/gear.test.js similarity index 77% rename from test/content/gear.js rename to test/content/gear.test.js index 49c6fe0e87..3268ef6b2d 100644 --- a/test/content/gear.js +++ b/test/content/gear.test.js @@ -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; diff --git a/test/content/hatching-potions.test.js b/test/content/hatching-potions.test.js index a4e31d7b91..1d1fd575a5 100644 --- a/test/content/hatching-potions.test.js +++ b/test/content/hatching-potions.test.js @@ -5,27 +5,33 @@ import { expectValidTranslationString, } from '../helpers/content.helper'; -import * as hatchingPotions from '../../website/common/script/content/hatching-potions'; +import { all } from '../../website/common/script/content/hatching-potions'; describe('hatchingPotions', () => { - describe('all', () => { - it('is a combination of drop, premium, and wacky potions', () => { - const dropNumber = Object.keys(hatchingPotions.drops).length; - const premiumNumber = Object.keys(hatchingPotions.premium).length; - const wackyNumber = Object.keys(hatchingPotions.wacky).length; - const allNumber = Object.keys(hatchingPotions.all).length; + let clock; - expect(allNumber).to.be.greaterThan(0); - expect(allNumber).to.equal(dropNumber + premiumNumber + wackyNumber); - }); + afterEach(() => { + if (clock) { + clock.restore(); + } + }); - 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(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); + }); }); }); }); diff --git a/test/content/schedule.test.js b/test/content/schedule.test.js new file mode 100644 index 0000000000..09d4ebfd6f --- /dev/null +++ b/test/content/schedule.test.js @@ -0,0 +1,271 @@ +// eslint-disable-next-line max-len +import moment from 'moment'; +import nconf from 'nconf'; +import { + getAllScheduleMatchingGroups, clearCachedMatchers, MONTHLY_SCHEDULE, GALA_SCHEDULE, +} from '../../website/common/script/content/constants/schedule'; +import QUEST_PETS from '../../website/common/script/content/quests/pets'; +import QUEST_HATCHINGPOTIONS from '../../website/common/script/content/quests/potions'; +import QUEST_BUNDLES from '../../website/common/script/content/bundles'; +import { premium } from '../../website/common/script/content/hatching-potions'; +import SPELLS from '../../website/common/script/content/spells'; +import QUEST_SEASONAL from '../../website/common/script/content/quests/seasonal'; + +function validateMatcher (matcher, checkedDate) { + expect(matcher.end).to.be.a('date'); + expect(matcher.end).to.be.greaterThan(checkedDate); +} + +describe('Content Schedule', () => { + let switchoverTime; + + beforeEach(() => { + switchoverTime = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0; + clearCachedMatchers(); + }); + + it('assembles scheduled items on january 15th', () => { + const date = new Date('2024-01-15'); + const matchers = getAllScheduleMatchingGroups(date); + for (const key in matchers) { + if (matchers[key]) { + validateMatcher(matchers[key], date); + } + } + }); + + it('assembles scheduled items on january 31th', () => { + const date = new Date('2024-01-31'); + const matchers = getAllScheduleMatchingGroups(date); + for (const key in matchers) { + if (matchers[key]) { + validateMatcher(matchers[key], date); + } + } + }); + + it('assembles scheduled items on march 2nd', () => { + const date = new Date('2024-03-02'); + const matchers = getAllScheduleMatchingGroups(date); + for (const key in matchers) { + if (matchers[key]) { + validateMatcher(matchers[key], date); + } + } + }); + + it('assembles scheduled items on march 22st', () => { + const date = new Date('2024-03-22'); + const matchers = getAllScheduleMatchingGroups(date); + for (const key in matchers) { + if (matchers[key]) { + validateMatcher(matchers[key], date); + } + } + }); + + it('assembles scheduled items on october 7th', () => { + const date = new Date('2024-10-07'); + const matchers = getAllScheduleMatchingGroups(date); + for (const key in matchers) { + if (matchers[key]) { + validateMatcher(matchers[key], date); + } + } + }); + it('assembles scheduled items on november 1th', () => { + const date = new Date('2024-11-01'); + const matchers = getAllScheduleMatchingGroups(date); + for (const key in matchers) { + if (matchers[key]) { + validateMatcher(matchers[key], date); + } + } + }); + + it('assembles scheduled items on december 20th', () => { + const date = new Date('2024-12-20'); + const matchers = getAllScheduleMatchingGroups(date); + for (const key in matchers) { + if (matchers[key]) { + validateMatcher(matchers[key], date); + } + } + }); + + it('sets the end date if its in the same month', () => { + const date = new Date('2024-04-03'); + const matchers = getAllScheduleMatchingGroups(date); + expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-04-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate()); + }); + + it('sets the end date if its in the next day', () => { + const date = new Date('2024-05-06T14:00:00.000Z'); + const matchers = getAllScheduleMatchingGroups(date); + expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-05-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate()); + }); + + it('sets the end date if its on the release day', () => { + const date = new Date('2024-05-07T07:00:00.000Z'); + const matchers = getAllScheduleMatchingGroups(date); + expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-06-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate()); + }); + + it('sets the end date if its next month', () => { + const date = new Date('2024-05-20T01:00:00.000Z'); + const matchers = getAllScheduleMatchingGroups(date); + expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-06-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate()); + }); + + it('sets the end date for a gala', () => { + const date = new Date('2024-05-20'); + const matchers = getAllScheduleMatchingGroups(date); + expect(matchers.seasonalGear.end).to.eql(moment.utc(`2024-06-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate()); + }); + + it('contains content for repeating events', () => { + const date = new Date('2024-04-15'); + const matchers = getAllScheduleMatchingGroups(date); + expect(matchers.premiumHatchingPotions).to.exist; + expect(matchers.premiumHatchingPotions.items.length).to.equal(4); + expect(matchers.premiumHatchingPotions.items.indexOf('Garden')).to.not.equal(-1); + expect(matchers.premiumHatchingPotions.items.indexOf('Porcelain')).to.not.equal(-1); + }); + + describe('only contains valid keys for', () => { + it('pet quests', () => { + const petKeys = Object.keys(QUEST_PETS); + Object.keys(MONTHLY_SCHEDULE).forEach(key => { + const petQuests = MONTHLY_SCHEDULE[key][14].find(item => item.type === 'petQuests'); + for (const petQuest of petQuests.items) { + expect(petQuest).to.be.a('string'); + expect(petKeys).to.include(petQuest); + } + }); + }); + + it('hatchingpotion quests', () => { + const potionKeys = Object.keys(QUEST_HATCHINGPOTIONS); + Object.keys(MONTHLY_SCHEDULE).forEach(key => { + const potionQuests = MONTHLY_SCHEDULE[key][14].find(item => item.type === 'hatchingPotionQuests'); + for (const potionQuest of potionQuests.items) { + expect(potionQuest).to.be.a('string'); + expect(potionKeys).to.include(potionQuest); + } + }); + }); + + it('bundles', () => { + const bundleKeys = Object.keys(QUEST_BUNDLES); + Object.keys(MONTHLY_SCHEDULE).forEach(key => { + const bundles = MONTHLY_SCHEDULE[key][14].find(item => item.type === 'bundles'); + for (const bundle of bundles.items) { + expect(bundle).to.be.a('string'); + expect(bundleKeys).to.include(bundle); + } + }); + }); + + it('premium hatching potions', () => { + const potionKeys = Object.keys(premium); + Object.keys(MONTHLY_SCHEDULE).forEach(key => { + const monthlyPotions = MONTHLY_SCHEDULE[key][21].find(item => item.type === 'premiumHatchingPotions'); + for (const potion of monthlyPotions.items) { + expect(potion).to.be.a('string'); + expect(potionKeys).to.include(potion); + } + }); + }); + + it('seasonal quests', () => { + const questKeys = Object.keys(QUEST_SEASONAL); + Object.keys(GALA_SCHEDULE).forEach(key => { + const quests = GALA_SCHEDULE[key].matchers.find(item => item.type === 'seasonalQuests'); + for (const quest of quests.items) { + expect(quest).to.be.a('string'); + expect(questKeys).to.include(quest); + } + }); + }); + + it('seasonal spells', () => { + const spellKeys = Object.keys(SPELLS.special); + Object.keys(GALA_SCHEDULE).forEach(key => { + const petQuests = GALA_SCHEDULE[key].matchers.find(item => item.type === 'seasonalSpells'); + for (const petQuest of petQuests.items) { + expect(petQuest).to.be.a('string'); + expect(spellKeys).to.include(petQuest); + } + }); + }); + }); + + describe('backgrounds matcher', () => { + it('allows background matching the month for new backgrounds', () => { + const date = new Date('2024-07-08'); + const matcher = getAllScheduleMatchingGroups(date).backgrounds; + expect(matcher.match('backgroundkey072024')).to.be.true; + }); + + it('disallows background in the future', () => { + const date = new Date('2024-07-08'); + const matcher = getAllScheduleMatchingGroups(date).backgrounds; + expect(matcher.match('backgroundkey072025')).to.be.false; + }); + + it('disallows background for the inverse month for new backgrounds', () => { + const date = new Date('2024-07-08'); + const matcher = getAllScheduleMatchingGroups(date).backgrounds; + expect(matcher.match('backgroundkey012024')).to.be.false; + }); + + it('allows background for the inverse month for old backgrounds', () => { + const date = new Date('2024-08-08'); + const matcher = getAllScheduleMatchingGroups(date).backgrounds; + expect(matcher.match('backgroundkey022023')).to.be.true; + expect(matcher.match('backgroundkey022021')).to.be.true; + }); + + it('allows background even yeared backgrounds in first half of year', () => { + const date = new Date('2025-02-08'); + const matcher = getAllScheduleMatchingGroups(date).backgrounds; + expect(matcher.match('backgroundkey022024')).to.be.true; + expect(matcher.match('backgroundkey082022')).to.be.true; + }); + + it('allows background odd yeared backgrounds in second half of year', () => { + const date = new Date('2024-08-08'); + const matcher = getAllScheduleMatchingGroups(date).backgrounds; + expect(matcher.match('backgroundkey022023')).to.be.true; + expect(matcher.match('backgroundkey082021')).to.be.true; + }); + }); + + describe('timeTravelers matcher', () => { + it('allows sets matching the month', () => { + const date = new Date('2024-07-08'); + const matcher = getAllScheduleMatchingGroups(date).timeTravelers; + expect(matcher.match('202307')).to.be.true; + expect(matcher.match('202207')).to.be.true; + }); + + it('disallows sets not matching the month', () => { + const date = new Date('2024-07-08'); + const matcher = getAllScheduleMatchingGroups(date).timeTravelers; + expect(matcher.match('202306')).to.be.false; + expect(matcher.match('202402')).to.be.false; + }); + + it('disallows sets from current month', () => { + const date = new Date('2024-07-08'); + const matcher = getAllScheduleMatchingGroups(date).timeTravelers; + expect(matcher.match('202407')).to.be.false; + }); + + it('disallows sets from the future', () => { + const date = new Date('2024-07-08'); + const matcher = getAllScheduleMatchingGroups(date).backgrounds; + expect(matcher.match('202507')).to.be.false; + }); + }); +}); diff --git a/test/content/shop-featuredItems.test.js b/test/content/shop-featuredItems.test.js new file mode 100644 index 0000000000..6ec7d57592 --- /dev/null +++ b/test/content/shop-featuredItems.test.js @@ -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); + }); + }); +}); diff --git a/test/content/spells.test.js b/test/content/spells.test.js new file mode 100644 index 0000000000..23f0cecb93 --- /dev/null +++ b/test/content/spells.test.js @@ -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); + } + } + }); +}); diff --git a/test/content/time-travelers.test.js b/test/content/time-travelers.test.js index 1e9f5bbd94..83dc40dd38 100644 --- a/test/content/time-travelers.test.js +++ b/test/content/time-travelers.test.js @@ -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; + }); }); }); diff --git a/test/helpers/api-unit.helper.js b/test/helpers/api-unit.helper.js index f56f2c477a..3cbc2a53e3 100644 --- a/test/helpers/api-unit.helper.js +++ b/test/helpers/api-unit.helper.js @@ -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(), diff --git a/test/helpers/content.helper.js b/test/helpers/content.helper.js index 93ceb51922..5dae94ce4b 100644 --- a/test/helpers/content.helper.js +++ b/test/helpers/content.helper.js @@ -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); } diff --git a/website/client/package-lock.json b/website/client/package-lock.json index 115c37a0f8..017f4c2aa9 100644 --- a/website/client/package-lock.json +++ b/website/client/package-lock.json @@ -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", diff --git a/website/client/package.json b/website/client/package.json index b7323b01f9..9d785ec6d3 100644 --- a/website/client/package.json +++ b/website/client/package.json @@ -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" } } diff --git a/website/client/public/static/npc/normal/customizations_background.png b/website/client/public/static/npc/normal/customizations_background.png new file mode 100644 index 0000000000..0d1cc9c273 Binary files /dev/null and b/website/client/public/static/npc/normal/customizations_background.png differ diff --git a/website/client/public/static/npc/normal/customizations_npc.png b/website/client/public/static/npc/normal/customizations_npc.png new file mode 100644 index 0000000000..6f0d483062 Binary files /dev/null and b/website/client/public/static/npc/normal/customizations_npc.png differ diff --git a/website/client/src/assets/css/sprites/spritesmith-main.css b/website/client/src/assets/css/sprites/spritesmith-main.css index 76665016b4..c64f12c9be 100644 --- a/website/client/src/assets/css/sprites/spritesmith-main.css +++ b/website/client/src/assets/css/sprites/spritesmith-main.css @@ -1875,6 +1875,11 @@ width: 141px; height: 147px; } +.background_river_bottom { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_river_bottom.png'); + width: 141px; + height: 147px; +} .background_river_of_lava { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_river_of_lava.png'); width: 141px; @@ -1940,6 +1945,11 @@ width: 141px; height: 147px; } +.background_shell_gate { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_shell_gate.png'); + width: 141px; + height: 147px; +} .background_shimmering_ice_prism { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_shimmering_ice_prism.png'); width: 141px; @@ -3768,6 +3778,11 @@ width: 68px; height: 68px; } +.icon_background_river_bottom { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_river_bottom.png'); + width: 68px; + height: 68px; +} .icon_background_river_of_lava { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_river_of_lava.png'); width: 68px; @@ -3833,6 +3848,11 @@ width: 68px; height: 68px; } +.icon_background_shell_gate { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_shell_gate.png'); + width: 68px; + height: 68px; +} .icon_background_shimmering_ice_prism { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_shimmering_ice_prism.png'); width: 68px; @@ -19992,11 +20012,21 @@ width: 68px; height: 68px; } +.icon_hair_bangs_1_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_1_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_bangs_1_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_1_midnight.png'); width: 68px; height: 68px; } +.icon_hair_bangs_1_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_1_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_bangs_1_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_1_pblue.png'); width: 68px; @@ -20082,11 +20112,31 @@ width: 68px; height: 68px; } +.icon_hair_bangs_1_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_1_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_bangs_1_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_1_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_bangs_1_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_1_snowy.png'); width: 68px; height: 68px; } +.icon_hair_bangs_1_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_1_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_bangs_1_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_1_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_bangs_1_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_1_white.png'); width: 68px; @@ -20187,11 +20237,21 @@ width: 68px; height: 68px; } +.icon_hair_bangs_2_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_2_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_bangs_2_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_2_midnight.png'); width: 68px; height: 68px; } +.icon_hair_bangs_2_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_2_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_bangs_2_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_2_pblue.png'); width: 68px; @@ -20277,11 +20337,31 @@ width: 68px; height: 68px; } +.icon_hair_bangs_2_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_2_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_bangs_2_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_2_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_bangs_2_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_2_snowy.png'); width: 68px; height: 68px; } +.icon_hair_bangs_2_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_2_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_bangs_2_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_2_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_bangs_2_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_2_white.png'); width: 68px; @@ -20382,11 +20462,21 @@ width: 68px; height: 68px; } +.icon_hair_bangs_3_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_3_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_bangs_3_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_3_midnight.png'); width: 68px; height: 68px; } +.icon_hair_bangs_3_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_3_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_bangs_3_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_3_pblue.png'); width: 68px; @@ -20472,11 +20562,31 @@ width: 68px; height: 68px; } +.icon_hair_bangs_3_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_3_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_bangs_3_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_3_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_bangs_3_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_3_snowy.png'); width: 68px; height: 68px; } +.icon_hair_bangs_3_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_3_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_bangs_3_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_3_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_bangs_3_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_3_white.png'); width: 68px; @@ -20577,11 +20687,21 @@ width: 68px; height: 68px; } +.icon_hair_bangs_4_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_4_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_bangs_4_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_4_midnight.png'); width: 68px; height: 68px; } +.icon_hair_bangs_4_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_4_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_bangs_4_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_4_pblue.png'); width: 68px; @@ -20667,11 +20787,31 @@ width: 68px; height: 68px; } +.icon_hair_bangs_4_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_4_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_bangs_4_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_4_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_bangs_4_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_4_snowy.png'); width: 68px; height: 68px; } +.icon_hair_bangs_4_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_4_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_bangs_4_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_4_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_bangs_4_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_bangs_4_white.png'); width: 68px; @@ -20772,11 +20912,21 @@ width: 68px; height: 68px; } +.icon_hair_base_10_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_10_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_10_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_10_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_10_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_10_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_10_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_10_pblue.png'); width: 68px; @@ -20862,11 +21012,31 @@ width: 68px; height: 68px; } +.icon_hair_base_10_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_10_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_10_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_10_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_10_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_10_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_10_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_10_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_10_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_10_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_10_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_10_white.png'); width: 68px; @@ -20967,11 +21137,21 @@ width: 68px; height: 68px; } +.icon_hair_base_11_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_11_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_11_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_11_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_11_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_11_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_11_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_11_pblue.png'); width: 68px; @@ -21057,11 +21237,31 @@ width: 68px; height: 68px; } +.icon_hair_base_11_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_11_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_11_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_11_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_11_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_11_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_11_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_11_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_11_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_11_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_11_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_11_white.png'); width: 68px; @@ -21162,11 +21362,21 @@ width: 68px; height: 68px; } +.icon_hair_base_12_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_12_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_12_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_12_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_12_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_12_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_12_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_12_pblue.png'); width: 68px; @@ -21252,11 +21462,31 @@ width: 68px; height: 68px; } +.icon_hair_base_12_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_12_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_12_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_12_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_12_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_12_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_12_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_12_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_12_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_12_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_12_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_12_white.png'); width: 68px; @@ -21357,11 +21587,21 @@ width: 68px; height: 68px; } +.icon_hair_base_13_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_13_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_13_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_13_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_13_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_13_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_13_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_13_pblue.png'); width: 68px; @@ -21447,11 +21687,31 @@ width: 68px; height: 68px; } +.icon_hair_base_13_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_13_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_13_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_13_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_13_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_13_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_13_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_13_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_13_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_13_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_13_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_13_white.png'); width: 68px; @@ -21552,11 +21812,21 @@ width: 68px; height: 68px; } +.icon_hair_base_14_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_14_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_14_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_14_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_14_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_14_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_14_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_14_pblue.png'); width: 68px; @@ -21642,11 +21912,31 @@ width: 68px; height: 68px; } +.icon_hair_base_14_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_14_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_14_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_14_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_14_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_14_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_14_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_14_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_14_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_14_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_14_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_14_white.png'); width: 68px; @@ -21747,11 +22037,21 @@ width: 68px; height: 68px; } +.icon_hair_base_15_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_15_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_15_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_15_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_15_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_15_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_15_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_15_pblue.png'); width: 68px; @@ -21837,11 +22137,31 @@ width: 68px; height: 68px; } +.icon_hair_base_15_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_15_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_15_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_15_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_15_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_15_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_15_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_15_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_15_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_15_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_15_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_15_white.png'); width: 68px; @@ -21942,11 +22262,21 @@ width: 68px; height: 68px; } +.icon_hair_base_16_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_16_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_16_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_16_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_16_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_16_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_16_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_16_pblue.png'); width: 68px; @@ -22032,11 +22362,31 @@ width: 68px; height: 68px; } +.icon_hair_base_16_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_16_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_16_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_16_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_16_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_16_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_16_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_16_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_16_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_16_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_16_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_16_white.png'); width: 68px; @@ -22137,11 +22487,21 @@ width: 68px; height: 68px; } +.icon_hair_base_17_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_17_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_17_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_17_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_17_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_17_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_17_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_17_pblue.png'); width: 68px; @@ -22227,11 +22587,31 @@ width: 68px; height: 68px; } +.icon_hair_base_17_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_17_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_17_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_17_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_17_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_17_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_17_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_17_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_17_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_17_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_17_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_17_white.png'); width: 68px; @@ -22332,11 +22712,21 @@ width: 68px; height: 68px; } +.icon_hair_base_18_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_18_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_18_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_18_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_18_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_18_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_18_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_18_pblue.png'); width: 68px; @@ -22422,11 +22812,31 @@ width: 68px; height: 68px; } +.icon_hair_base_18_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_18_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_18_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_18_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_18_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_18_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_18_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_18_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_18_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_18_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_18_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_18_white.png'); width: 68px; @@ -22527,11 +22937,21 @@ width: 68px; height: 68px; } +.icon_hair_base_19_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_19_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_19_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_19_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_19_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_19_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_19_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_19_pblue.png'); width: 68px; @@ -22617,11 +23037,31 @@ width: 68px; height: 68px; } +.icon_hair_base_19_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_19_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_19_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_19_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_19_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_19_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_19_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_19_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_19_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_19_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_19_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_19_white.png'); width: 68px; @@ -22722,11 +23162,21 @@ width: 68px; height: 68px; } +.icon_hair_base_1_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_1_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_1_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_1_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_1_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_1_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_1_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_1_pblue.png'); width: 68px; @@ -22812,11 +23262,31 @@ width: 68px; height: 68px; } +.icon_hair_base_1_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_1_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_1_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_1_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_1_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_1_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_1_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_1_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_1_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_1_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_1_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_1_white.png'); width: 68px; @@ -22917,11 +23387,21 @@ width: 68px; height: 68px; } +.icon_hair_base_20_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_20_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_20_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_20_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_20_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_20_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_20_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_20_pblue.png'); width: 68px; @@ -23007,11 +23487,31 @@ width: 68px; height: 68px; } +.icon_hair_base_20_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_20_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_20_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_20_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_20_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_20_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_20_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_20_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_20_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_20_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_20_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_20_white.png'); width: 68px; @@ -23112,11 +23612,21 @@ width: 68px; height: 68px; } +.icon_hair_base_2_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_2_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_2_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_2_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_2_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_2_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_2_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_2_pblue.png'); width: 68px; @@ -23202,11 +23712,31 @@ width: 68px; height: 68px; } +.icon_hair_base_2_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_2_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_2_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_2_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_2_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_2_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_2_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_2_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_2_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_2_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_2_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_2_white.png'); width: 68px; @@ -23307,11 +23837,21 @@ width: 68px; height: 68px; } +.icon_hair_base_3_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_3_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_3_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_3_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_3_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_3_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_3_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_3_pblue.png'); width: 68px; @@ -23397,11 +23937,31 @@ width: 68px; height: 68px; } +.icon_hair_base_3_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_3_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_3_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_3_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_3_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_3_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_3_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_3_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_3_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_3_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_3_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_3_white.png'); width: 68px; @@ -23502,11 +24062,21 @@ width: 68px; height: 68px; } +.icon_hair_base_4_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_4_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_4_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_4_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_4_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_4_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_4_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_4_pblue.png'); width: 68px; @@ -23592,11 +24162,31 @@ width: 68px; height: 68px; } +.icon_hair_base_4_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_4_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_4_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_4_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_4_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_4_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_4_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_4_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_4_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_4_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_4_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_4_white.png'); width: 68px; @@ -23697,11 +24287,21 @@ width: 68px; height: 68px; } +.icon_hair_base_5_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_5_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_5_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_5_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_5_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_5_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_5_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_5_pblue.png'); width: 68px; @@ -23787,11 +24387,31 @@ width: 68px; height: 68px; } +.icon_hair_base_5_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_5_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_5_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_5_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_5_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_5_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_5_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_5_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_5_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_5_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_5_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_5_white.png'); width: 68px; @@ -23892,11 +24512,21 @@ width: 68px; height: 68px; } +.icon_hair_base_6_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_6_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_6_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_6_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_6_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_6_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_6_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_6_pblue.png'); width: 68px; @@ -23982,11 +24612,31 @@ width: 68px; height: 68px; } +.icon_hair_base_6_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_6_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_6_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_6_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_6_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_6_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_6_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_6_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_6_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_6_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_6_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_6_white.png'); width: 68px; @@ -24087,11 +24737,21 @@ width: 68px; height: 68px; } +.icon_hair_base_7_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_7_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_7_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_7_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_7_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_7_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_7_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_7_pblue.png'); width: 68px; @@ -24177,11 +24837,31 @@ width: 68px; height: 68px; } +.icon_hair_base_7_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_7_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_7_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_7_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_7_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_7_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_7_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_7_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_7_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_7_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_7_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_7_white.png'); width: 68px; @@ -24282,11 +24962,21 @@ width: 68px; height: 68px; } +.icon_hair_base_8_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_8_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_8_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_8_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_8_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_8_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_8_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_8_pblue.png'); width: 68px; @@ -24372,11 +25062,31 @@ width: 68px; height: 68px; } +.icon_hair_base_8_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_8_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_8_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_8_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_8_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_8_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_8_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_8_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_8_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_8_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_8_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_8_white.png'); width: 68px; @@ -24477,11 +25187,21 @@ width: 68px; height: 68px; } +.icon_hair_base_9_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_9_lava.png'); + width: 68px; + height: 68px; +} .icon_hair_base_9_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_9_midnight.png'); width: 68px; height: 68px; } +.icon_hair_base_9_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_9_orchid.png'); + width: 68px; + height: 68px; +} .icon_hair_base_9_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_9_pblue.png'); width: 68px; @@ -24567,11 +25287,31 @@ width: 68px; height: 68px; } +.icon_hair_base_9_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_9_sandnsea.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_9_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_9_seasprite.png'); + width: 68px; + height: 68px; +} .icon_hair_base_9_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_9_snowy.png'); width: 68px; height: 68px; } +.icon_hair_base_9_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_9_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.icon_hair_base_9_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_9_sunset.png'); + width: 68px; + height: 68px; +} .icon_hair_base_9_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_base_9_white.png'); width: 68px; @@ -24762,6 +25502,17 @@ width: 60px; height: 60px; } +.icon_hair_beard_1_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_1_lava.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_1_lava { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_1_lava.png'); + width: 60px; + height: 60px; +} .icon_hair_beard_1_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_1_midnight.png'); width: 68px; @@ -24773,6 +25524,17 @@ width: 60px; height: 60px; } +.icon_hair_beard_1_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_1_orchid.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_1_orchid { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_1_orchid.png'); + width: 60px; + height: 60px; +} .icon_hair_beard_1_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_1_pblue.png'); width: 68px; @@ -24960,6 +25722,28 @@ width: 60px; height: 60px; } +.icon_hair_beard_1_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_1_sandnsea.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_1_sandnsea { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_1_sandnsea.png'); + width: 60px; + height: 60px; +} +.icon_hair_beard_1_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_1_seasprite.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_1_seasprite { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_1_seasprite.png'); + width: 60px; + height: 60px; +} .icon_hair_beard_1_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_1_snowy.png'); width: 68px; @@ -24971,6 +25755,28 @@ width: 60px; height: 60px; } +.icon_hair_beard_1_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_1_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_1_sunlitwaves { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_1_sunlitwaves.png'); + width: 60px; + height: 60px; +} +.icon_hair_beard_1_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_1_sunset.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_1_sunset { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_1_sunset.png'); + width: 60px; + height: 60px; +} .icon_hair_beard_1_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_1_white.png'); width: 68px; @@ -25191,6 +25997,17 @@ width: 60px; height: 60px; } +.icon_hair_beard_2_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_2_lava.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_2_lava { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_2_lava.png'); + width: 60px; + height: 60px; +} .icon_hair_beard_2_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_2_midnight.png'); width: 68px; @@ -25202,6 +26019,17 @@ width: 60px; height: 60px; } +.icon_hair_beard_2_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_2_orchid.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_2_orchid { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_2_orchid.png'); + width: 60px; + height: 60px; +} .icon_hair_beard_2_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_2_pblue.png'); width: 68px; @@ -25389,6 +26217,28 @@ width: 60px; height: 60px; } +.icon_hair_beard_2_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_2_sandnsea.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_2_sandnsea { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_2_sandnsea.png'); + width: 60px; + height: 60px; +} +.icon_hair_beard_2_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_2_seasprite.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_2_seasprite { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_2_seasprite.png'); + width: 60px; + height: 60px; +} .icon_hair_beard_2_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_2_snowy.png'); width: 68px; @@ -25400,6 +26250,28 @@ width: 60px; height: 60px; } +.icon_hair_beard_2_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_2_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_2_sunlitwaves { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_2_sunlitwaves.png'); + width: 60px; + height: 60px; +} +.icon_hair_beard_2_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_2_sunset.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_2_sunset { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_2_sunset.png'); + width: 60px; + height: 60px; +} .icon_hair_beard_2_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_2_white.png'); width: 68px; @@ -25620,6 +26492,17 @@ width: 60px; height: 60px; } +.icon_hair_beard_3_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_3_lava.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_3_lava { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_3_lava.png'); + width: 60px; + height: 60px; +} .icon_hair_beard_3_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_3_midnight.png'); width: 68px; @@ -25631,6 +26514,17 @@ width: 60px; height: 60px; } +.icon_hair_beard_3_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_3_orchid.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_3_orchid { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_3_orchid.png'); + width: 60px; + height: 60px; +} .icon_hair_beard_3_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_3_pblue.png'); width: 68px; @@ -25818,6 +26712,28 @@ width: 60px; height: 60px; } +.icon_hair_beard_3_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_3_sandnsea.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_3_sandnsea { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_3_sandnsea.png'); + width: 60px; + height: 60px; +} +.icon_hair_beard_3_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_3_seasprite.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_3_seasprite { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_3_seasprite.png'); + width: 60px; + height: 60px; +} .icon_hair_beard_3_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_3_snowy.png'); width: 68px; @@ -25829,6 +26745,28 @@ width: 60px; height: 60px; } +.icon_hair_beard_3_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_3_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_3_sunlitwaves { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_3_sunlitwaves.png'); + width: 60px; + height: 60px; +} +.icon_hair_beard_3_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_3_sunset.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_beard_3_sunset { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_3_sunset.png'); + width: 60px; + height: 60px; +} .icon_hair_beard_3_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_beard_3_white.png'); width: 68px; @@ -26225,6 +27163,17 @@ width: 60px; height: 60px; } +.icon_hair_mustache_1_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_1_lava.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_mustache_1_lava { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_1_lava.png'); + width: 60px; + height: 60px; +} .icon_hair_mustache_1_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_1_midnight.png'); width: 68px; @@ -26236,6 +27185,17 @@ width: 60px; height: 60px; } +.icon_hair_mustache_1_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_1_orchid.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_mustache_1_orchid { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_1_orchid.png'); + width: 60px; + height: 60px; +} .icon_hair_mustache_1_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_1_pblue.png'); width: 68px; @@ -26423,6 +27383,28 @@ width: 60px; height: 60px; } +.icon_hair_mustache_1_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_1_sandnsea.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_mustache_1_sandnsea { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_1_sandnsea.png'); + width: 60px; + height: 60px; +} +.icon_hair_mustache_1_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_1_seasprite.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_mustache_1_seasprite { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_1_seasprite.png'); + width: 60px; + height: 60px; +} .icon_hair_mustache_1_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_1_snowy.png'); width: 68px; @@ -26434,6 +27416,28 @@ width: 60px; height: 60px; } +.icon_hair_mustache_1_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_1_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_mustache_1_sunlitwaves { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_1_sunlitwaves.png'); + width: 60px; + height: 60px; +} +.icon_hair_mustache_1_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_1_sunset.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_mustache_1_sunset { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_1_sunset.png'); + width: 60px; + height: 60px; +} .icon_hair_mustache_1_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_1_white.png'); width: 68px; @@ -26654,6 +27658,17 @@ width: 60px; height: 60px; } +.icon_hair_mustache_2_lava { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_2_lava.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_mustache_2_lava { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_2_lava.png'); + width: 60px; + height: 60px; +} .icon_hair_mustache_2_midnight { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_2_midnight.png'); width: 68px; @@ -26665,6 +27680,17 @@ width: 60px; height: 60px; } +.icon_hair_mustache_2_orchid { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_2_orchid.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_mustache_2_orchid { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_2_orchid.png'); + width: 60px; + height: 60px; +} .icon_hair_mustache_2_pblue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_2_pblue.png'); width: 68px; @@ -26852,6 +27878,28 @@ width: 60px; height: 60px; } +.icon_hair_mustache_2_sandnsea { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_2_sandnsea.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_mustache_2_sandnsea { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_2_sandnsea.png'); + width: 60px; + height: 60px; +} +.icon_hair_mustache_2_seasprite { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_2_seasprite.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_mustache_2_seasprite { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_2_seasprite.png'); + width: 60px; + height: 60px; +} .icon_hair_mustache_2_snowy { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_2_snowy.png'); width: 68px; @@ -26863,6 +27911,28 @@ width: 60px; height: 60px; } +.icon_hair_mustache_2_sunlitwaves { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_2_sunlitwaves.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_mustache_2_sunlitwaves { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_2_sunlitwaves.png'); + width: 60px; + height: 60px; +} +.icon_hair_mustache_2_sunset { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_2_sunset.png'); + width: 68px; + height: 68px; +} +.customize-option.icon_hair_mustache_2_sunset { + background-position: -25px -15px; + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_2_sunset.png'); + width: 60px; + height: 60px; +} .icon_hair_mustache_2_white { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_hair_mustache_2_white.png'); width: 68px; @@ -30382,6 +31452,11 @@ width: 90px; height: 90px; } +.broad_armor_armoire_corsairsCoatAndCape { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_corsairsCoatAndCape.png'); + width: 114px; + height: 90px; +} .broad_armor_armoire_coverallsOfBookbinding { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_coverallsOfBookbinding.png'); width: 114px; @@ -30947,6 +32022,11 @@ width: 114px; height: 90px; } +.head_armoire_corsairsBandana { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_corsairsBandana.png'); + width: 114px; + height: 90px; +} .head_armoire_crownOfDiamonds { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_crownOfDiamonds.png'); width: 114px; @@ -31812,6 +32892,11 @@ width: 68px; height: 68px; } +.shop_armor_armoire_corsairsCoatAndCape { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_armoire_corsairsCoatAndCape.png'); + width: 68px; + height: 68px; +} .shop_armor_armoire_coverallsOfBookbinding { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_armoire_coverallsOfBookbinding.png'); width: 68px; @@ -32437,6 +33522,11 @@ width: 68px; height: 68px; } +.shop_head_armoire_corsairsBandana { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_armoire_corsairsBandana.png'); + width: 68px; + height: 68px; +} .shop_head_armoire_crownOfDiamonds { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_armoire_crownOfDiamonds.png'); width: 68px; @@ -33297,6 +34387,11 @@ width: 68px; height: 68px; } +.shop_weapon_armoire_corsairsBlade { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_corsairsBlade.png'); + width: 68px; + height: 68px; +} .shop_weapon_armoire_crystalCrescentStaff { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_crystalCrescentStaff.png'); width: 68px; @@ -33812,6 +34907,11 @@ width: 90px; height: 90px; } +.slim_armor_armoire_corsairsCoatAndCape { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_corsairsCoatAndCape.png'); + width: 114px; + height: 90px; +} .slim_armor_armoire_coverallsOfBookbinding { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_coverallsOfBookbinding.png'); width: 114px; @@ -34327,6 +35427,11 @@ width: 90px; height: 90px; } +.weapon_armoire_corsairsBlade { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_corsairsBlade.png'); + width: 114px; + height: 90px; +} .weapon_armoire_crystalCrescentStaff { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_crystalCrescentStaff.png'); width: 90px; @@ -40972,6 +42077,36 @@ width: 114px; height: 90px; } +.broad_armor_mystery_202407 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202407.png'); + width: 117px; + height: 120px; +} +.head_mystery_202407 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202407.png'); + width: 117px; + height: 120px; +} +.shop_armor_mystery_202407 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_mystery_202407.png'); + width: 68px; + height: 68px; +} +.shop_head_mystery_202407 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_mystery_202407.png'); + width: 68px; + height: 68px; +} +.shop_set_mystery_202407 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_set_mystery_202407.png'); + width: 68px; + height: 68px; +} +.slim_armor_mystery_202407 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202407.png'); + width: 117px; + height: 120px; +} .broad_armor_mystery_301404 { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_301404.png'); width: 90px; @@ -43407,6 +44542,26 @@ width: 114px; height: 120px; } +.broad_armor_special_summer2024Healer { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2024Healer.png'); + width: 114px; + height: 90px; +} +.broad_armor_special_summer2024Mage { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2024Mage.png'); + width: 114px; + height: 117px; +} +.broad_armor_special_summer2024Rogue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2024Rogue.png'); + width: 114px; + height: 117px; +} +.broad_armor_special_summer2024Warrior { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2024Warrior.png'); + width: 114px; + height: 117px; +} .broad_armor_special_summerHealer { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summerHealer.png'); width: 90px; @@ -43617,6 +44772,26 @@ width: 114px; height: 120px; } +.head_special_summer2024Healer { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2024Healer.png'); + width: 114px; + height: 90px; +} +.head_special_summer2024Mage { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2024Mage.png'); + width: 114px; + height: 117px; +} +.head_special_summer2024Rogue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2024Rogue.png'); + width: 114px; + height: 117px; +} +.head_special_summer2024Warrior { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2024Warrior.png'); + width: 114px; + height: 117px; +} .head_special_summerHealer { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summerHealer.png'); width: 90px; @@ -43777,6 +44952,21 @@ width: 114px; height: 120px; } +.shield_special_summer2024Healer { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summer2024Healer.png'); + width: 114px; + height: 90px; +} +.shield_special_summer2024Rogue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summer2024Rogue.png'); + width: 114px; + height: 117px; +} +.shield_special_summer2024Warrior { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summer2024Warrior.png'); + width: 114px; + height: 117px; +} .shield_special_summerHealer { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summerHealer.png'); width: 90px; @@ -43972,6 +45162,26 @@ width: 68px; height: 68px; } +.shop_armor_special_summer2024Healer { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_summer2024Healer.png'); + width: 68px; + height: 68px; +} +.shop_armor_special_summer2024Mage { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_summer2024Mage.png'); + width: 68px; + height: 68px; +} +.shop_armor_special_summer2024Rogue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_summer2024Rogue.png'); + width: 68px; + height: 68px; +} +.shop_armor_special_summer2024Warrior { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_summer2024Warrior.png'); + width: 68px; + height: 68px; +} .shop_armor_special_summerHealer { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_summerHealer.png'); width: 68px; @@ -44022,6 +45232,11 @@ width: 68px; height: 68px; } +.shop_eyewear_mystery_202406 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_eyewear_mystery_202406.png'); + width: 68px; + height: 68px; +} .shop_eyewear_special_summerRogue { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_eyewear_special_summerRogue.png'); width: 68px; @@ -44212,6 +45427,26 @@ width: 68px; height: 68px; } +.shop_head_special_summer2024Healer { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_summer2024Healer.png'); + width: 68px; + height: 68px; +} +.shop_head_special_summer2024Mage { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_summer2024Mage.png'); + width: 68px; + height: 68px; +} +.shop_head_special_summer2024Rogue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_summer2024Rogue.png'); + width: 68px; + height: 68px; +} +.shop_head_special_summer2024Warrior { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_summer2024Warrior.png'); + width: 68px; + height: 68px; +} .shop_head_special_summerHealer { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_summerHealer.png'); width: 68px; @@ -44372,6 +45607,21 @@ width: 68px; height: 68px; } +.shop_shield_special_summer2024Healer { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_special_summer2024Healer.png'); + width: 68px; + height: 68px; +} +.shop_shield_special_summer2024Rogue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_special_summer2024Rogue.png'); + width: 68px; + height: 68px; +} +.shop_shield_special_summer2024Warrior { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_special_summer2024Warrior.png'); + width: 68px; + height: 68px; +} .shop_shield_special_summerHealer { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_special_summerHealer.png'); width: 68px; @@ -44567,6 +45817,26 @@ width: 68px; height: 68px; } +.shop_weapon_special_summer2024Healer { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_summer2024Healer.png'); + width: 68px; + height: 68px; +} +.shop_weapon_special_summer2024Mage { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_summer2024Mage.png'); + width: 68px; + height: 68px; +} +.shop_weapon_special_summer2024Rogue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_summer2024Rogue.png'); + width: 68px; + height: 68px; +} +.shop_weapon_special_summer2024Warrior { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_summer2024Warrior.png'); + width: 68px; + height: 68px; +} .shop_weapon_special_summerHealer { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_summerHealer.png'); width: 68px; @@ -44587,6 +45857,11 @@ width: 68px; height: 68px; } +.slim_armor_mystery_202406 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202406.png'); + width: 114px; + height: 90px; +} .slim_armor_special_summer2015Healer { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2015Healer.png'); width: 90px; @@ -44767,6 +46042,26 @@ width: 114px; height: 120px; } +.slim_armor_special_summer2024Healer { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2024Healer.png'); + width: 114px; + height: 90px; +} +.slim_armor_special_summer2024Mage { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2024Mage.png'); + width: 114px; + height: 117px; +} +.slim_armor_special_summer2024Rogue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2024Rogue.png'); + width: 114px; + height: 117px; +} +.slim_armor_special_summer2024Warrior { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2024Warrior.png'); + width: 114px; + height: 117px; +} .slim_armor_special_summerHealer { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summerHealer.png'); width: 90px; @@ -44967,6 +46262,26 @@ width: 114px; height: 120px; } +.weapon_special_summer2024Healer { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2024Healer.png'); + width: 114px; + height: 90px; +} +.weapon_special_summer2024Mage { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2024Mage.png'); + width: 114px; + height: 117px; +} +.weapon_special_summer2024Rogue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2024Rogue.png'); + width: 114px; + height: 117px; +} +.weapon_special_summer2024Warrior { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2024Warrior.png'); + width: 114px; + height: 117px; +} .weapon_special_summerHealer { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summerHealer.png'); width: 90px; @@ -48895,6 +50210,11 @@ width: 28px; height: 28px; } +.icon_pet_veteran_cactus { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_pet_veteran_cactus.png'); + width: 28px; + height: 28px; +} .icon_pet_veteran_dragon { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_pet_veteran_dragon.png'); width: 28px; @@ -49235,6 +50555,11 @@ width: 219px; height: 219px; } +.quest_chameleon { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_chameleon.png'); + width: 216px; + height: 216px; +} .quest_cheetah { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_cheetah.png'); width: 219px; @@ -49945,6 +51270,11 @@ width: 68px; height: 68px; } +.inventory_quest_scroll_chameleon { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/inventory_quest_scroll_chameleon.png'); + width: 68px; + height: 68px; +} .inventory_quest_scroll_cheetah { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/inventory_quest_scroll_cheetah.png'); width: 68px; @@ -50820,6 +52150,11 @@ width: 68px; height: 68px; } +.Pet_Egg_Chameleon { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_Egg_Chameleon.png'); + width: 68px; + height: 68px; +} .Pet_Egg_Cheetah { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_Egg_Cheetah.png'); width: 68px; @@ -51615,6 +52950,11 @@ width: 105px; height: 105px; } +.Mount_Body_BearCub-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_BearCub-Koi.png'); + width: 105px; + height: 105px; +} .Mount_Body_BearCub-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_BearCub-Moonglow.png'); width: 105px; @@ -52050,6 +53390,11 @@ width: 105px; height: 105px; } +.Mount_Body_Cactus-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Cactus-Koi.png'); + width: 105px; + height: 105px; +} .Mount_Body_Cactus-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Cactus-Moonglow.png'); width: 105px; @@ -52210,6 +53555,56 @@ width: 105px; height: 105px; } +.Mount_Body_Chameleon-Base { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Chameleon-Base.png'); + width: 105px; + height: 105px; +} +.Mount_Body_Chameleon-CottonCandyBlue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Chameleon-CottonCandyBlue.png'); + width: 105px; + height: 105px; +} +.Mount_Body_Chameleon-CottonCandyPink { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Chameleon-CottonCandyPink.png'); + width: 105px; + height: 105px; +} +.Mount_Body_Chameleon-Desert { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Chameleon-Desert.png'); + width: 105px; + height: 105px; +} +.Mount_Body_Chameleon-Golden { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Chameleon-Golden.png'); + width: 105px; + height: 105px; +} +.Mount_Body_Chameleon-Red { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Chameleon-Red.png'); + width: 105px; + height: 105px; +} +.Mount_Body_Chameleon-Shade { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Chameleon-Shade.png'); + width: 105px; + height: 105px; +} +.Mount_Body_Chameleon-Skeleton { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Chameleon-Skeleton.png'); + width: 105px; + height: 105px; +} +.Mount_Body_Chameleon-White { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Chameleon-White.png'); + width: 105px; + height: 105px; +} +.Mount_Body_Chameleon-Zombie { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Chameleon-Zombie.png'); + width: 105px; + height: 105px; +} .Mount_Body_Cheetah-Base { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Cheetah-Base.png'); width: 105px; @@ -52580,6 +53975,11 @@ width: 105px; height: 105px; } +.Mount_Body_Dragon-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Koi.png'); + width: 105px; + height: 105px; +} .Mount_Body_Dragon-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Moonglow.png'); width: 105px; @@ -53010,6 +54410,11 @@ width: 105px; height: 105px; } +.Mount_Body_FlyingPig-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_FlyingPig-Koi.png'); + width: 105px; + height: 105px; +} .Mount_Body_FlyingPig-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_FlyingPig-Moonglow.png'); width: 105px; @@ -53290,6 +54695,11 @@ width: 105px; height: 105px; } +.Mount_Body_Fox-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Fox-Koi.png'); + width: 105px; + height: 105px; +} .Mount_Body_Fox-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Fox-Moonglow.png'); width: 105px; @@ -54010,6 +55420,11 @@ width: 105px; height: 105px; } +.Mount_Body_LionCub-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_LionCub-Koi.png'); + width: 105px; + height: 105px; +} .Mount_Body_LionCub-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_LionCub-Moonglow.png'); width: 105px; @@ -54510,6 +55925,11 @@ width: 105px; height: 105px; } +.Mount_Body_PandaCub-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_PandaCub-Koi.png'); + width: 105px; + height: 105px; +} .Mount_Body_PandaCub-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_PandaCub-Moonglow.png'); width: 105px; @@ -55745,6 +57165,11 @@ width: 105px; height: 105px; } +.Mount_Body_TigerCub-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_TigerCub-Koi.png'); + width: 105px; + height: 105px; +} .Mount_Body_TigerCub-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_TigerCub-Moonglow.png'); width: 105px; @@ -56335,6 +57760,11 @@ width: 135px; height: 135px; } +.Mount_Body_Wolf-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Wolf-Koi.png'); + width: 135px; + height: 135px; +} .Mount_Body_Wolf-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Wolf-Moonglow.png'); width: 135px; @@ -56865,6 +58295,11 @@ width: 105px; height: 105px; } +.Mount_Head_BearCub-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_BearCub-Koi.png'); + width: 105px; + height: 105px; +} .Mount_Head_BearCub-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_BearCub-Moonglow.png'); width: 105px; @@ -57300,6 +58735,11 @@ width: 105px; height: 105px; } +.Mount_Head_Cactus-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Cactus-Koi.png'); + width: 105px; + height: 105px; +} .Mount_Head_Cactus-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Cactus-Moonglow.png'); width: 105px; @@ -57460,6 +58900,56 @@ width: 105px; height: 105px; } +.Mount_Head_Chameleon-Base { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Chameleon-Base.png'); + width: 105px; + height: 105px; +} +.Mount_Head_Chameleon-CottonCandyBlue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Chameleon-CottonCandyBlue.png'); + width: 105px; + height: 105px; +} +.Mount_Head_Chameleon-CottonCandyPink { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Chameleon-CottonCandyPink.png'); + width: 105px; + height: 105px; +} +.Mount_Head_Chameleon-Desert { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Chameleon-Desert.png'); + width: 105px; + height: 105px; +} +.Mount_Head_Chameleon-Golden { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Chameleon-Golden.png'); + width: 105px; + height: 105px; +} +.Mount_Head_Chameleon-Red { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Chameleon-Red.png'); + width: 105px; + height: 105px; +} +.Mount_Head_Chameleon-Shade { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Chameleon-Shade.png'); + width: 105px; + height: 105px; +} +.Mount_Head_Chameleon-Skeleton { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Chameleon-Skeleton.png'); + width: 105px; + height: 105px; +} +.Mount_Head_Chameleon-White { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Chameleon-White.png'); + width: 105px; + height: 105px; +} +.Mount_Head_Chameleon-Zombie { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Chameleon-Zombie.png'); + width: 105px; + height: 105px; +} .Mount_Head_Cheetah-Base { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Cheetah-Base.png'); width: 105px; @@ -57830,6 +59320,11 @@ width: 105px; height: 105px; } +.Mount_Head_Dragon-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Koi.png'); + width: 105px; + height: 105px; +} .Mount_Head_Dragon-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Moonglow.png'); width: 105px; @@ -58260,6 +59755,11 @@ width: 105px; height: 105px; } +.Mount_Head_FlyingPig-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_FlyingPig-Koi.png'); + width: 105px; + height: 105px; +} .Mount_Head_FlyingPig-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_FlyingPig-Moonglow.png'); width: 105px; @@ -58540,6 +60040,11 @@ width: 105px; height: 105px; } +.Mount_Head_Fox-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Fox-Koi.png'); + width: 105px; + height: 105px; +} .Mount_Head_Fox-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Fox-Moonglow.png'); width: 105px; @@ -59260,6 +60765,11 @@ width: 105px; height: 105px; } +.Mount_Head_LionCub-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_LionCub-Koi.png'); + width: 105px; + height: 105px; +} .Mount_Head_LionCub-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_LionCub-Moonglow.png'); width: 105px; @@ -59760,6 +61270,11 @@ width: 105px; height: 105px; } +.Mount_Head_PandaCub-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_PandaCub-Koi.png'); + width: 105px; + height: 105px; +} .Mount_Head_PandaCub-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_PandaCub-Moonglow.png'); width: 105px; @@ -60995,6 +62510,11 @@ width: 105px; height: 105px; } +.Mount_Head_TigerCub-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_TigerCub-Koi.png'); + width: 105px; + height: 105px; +} .Mount_Head_TigerCub-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_TigerCub-Moonglow.png'); width: 105px; @@ -61585,6 +63105,11 @@ width: 135px; height: 135px; } +.Mount_Head_Wolf-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Wolf-Koi.png'); + width: 135px; + height: 135px; +} .Mount_Head_Wolf-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Wolf-Moonglow.png'); width: 135px; @@ -62120,6 +63645,11 @@ width: 81px; height: 99px; } +.Mount_Icon_BearCub-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_BearCub-Koi.png'); + width: 81px; + height: 99px; +} .Mount_Icon_BearCub-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_BearCub-Moonglow.png'); width: 81px; @@ -62555,6 +64085,11 @@ width: 81px; height: 99px; } +.Mount_Icon_Cactus-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Cactus-Koi.png'); + width: 81px; + height: 99px; +} .Mount_Icon_Cactus-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Cactus-Moonglow.png'); width: 81px; @@ -62715,6 +64250,56 @@ width: 81px; height: 99px; } +.Mount_Icon_Chameleon-Base { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Chameleon-Base.png'); + width: 81px; + height: 99px; +} +.Mount_Icon_Chameleon-CottonCandyBlue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Chameleon-CottonCandyBlue.png'); + width: 81px; + height: 99px; +} +.Mount_Icon_Chameleon-CottonCandyPink { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Chameleon-CottonCandyPink.png'); + width: 81px; + height: 99px; +} +.Mount_Icon_Chameleon-Desert { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Chameleon-Desert.png'); + width: 81px; + height: 99px; +} +.Mount_Icon_Chameleon-Golden { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Chameleon-Golden.png'); + width: 81px; + height: 99px; +} +.Mount_Icon_Chameleon-Red { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Chameleon-Red.png'); + width: 81px; + height: 99px; +} +.Mount_Icon_Chameleon-Shade { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Chameleon-Shade.png'); + width: 81px; + height: 99px; +} +.Mount_Icon_Chameleon-Skeleton { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Chameleon-Skeleton.png'); + width: 81px; + height: 99px; +} +.Mount_Icon_Chameleon-White { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Chameleon-White.png'); + width: 81px; + height: 99px; +} +.Mount_Icon_Chameleon-Zombie { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Chameleon-Zombie.png'); + width: 81px; + height: 99px; +} .Mount_Icon_Cheetah-Base { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Cheetah-Base.png'); width: 81px; @@ -63085,6 +64670,11 @@ width: 81px; height: 99px; } +.Mount_Icon_Dragon-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Dragon-Koi.png'); + width: 81px; + height: 99px; +} .Mount_Icon_Dragon-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Dragon-Moonglow.png'); width: 81px; @@ -63515,6 +65105,11 @@ width: 81px; height: 99px; } +.Mount_Icon_FlyingPig-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_FlyingPig-Koi.png'); + width: 81px; + height: 99px; +} .Mount_Icon_FlyingPig-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_FlyingPig-Moonglow.png'); width: 81px; @@ -63795,6 +65390,11 @@ width: 81px; height: 99px; } +.Mount_Icon_Fox-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Fox-Koi.png'); + width: 81px; + height: 99px; +} .Mount_Icon_Fox-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Fox-Moonglow.png'); width: 81px; @@ -64520,6 +66120,11 @@ width: 81px; height: 99px; } +.Mount_Icon_LionCub-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_LionCub-Koi.png'); + width: 81px; + height: 99px; +} .Mount_Icon_LionCub-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_LionCub-Moonglow.png'); width: 81px; @@ -65020,6 +66625,11 @@ width: 81px; height: 99px; } +.Mount_Icon_PandaCub-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_PandaCub-Koi.png'); + width: 81px; + height: 99px; +} .Mount_Icon_PandaCub-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_PandaCub-Moonglow.png'); width: 81px; @@ -66255,6 +67865,11 @@ width: 81px; height: 99px; } +.Mount_Icon_TigerCub-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_TigerCub-Koi.png'); + width: 81px; + height: 99px; +} .Mount_Icon_TigerCub-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_TigerCub-Moonglow.png'); width: 81px; @@ -66845,6 +68460,11 @@ width: 81px; height: 99px; } +.Mount_Icon_Wolf-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Wolf-Koi.png'); + width: 81px; + height: 99px; +} .Mount_Icon_Wolf-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Wolf-Moonglow.png'); width: 81px; @@ -67390,6 +69010,11 @@ width: 81px; height: 99px; } +.Pet-BearCub-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Koi.png'); + width: 81px; + height: 99px; +} .Pet-BearCub-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Moonglow.png'); width: 81px; @@ -67850,6 +69475,11 @@ width: 81px; height: 99px; } +.Pet-Cactus-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Koi.png'); + width: 81px; + height: 99px; +} .Pet-Cactus-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Moonglow.png'); width: 81px; @@ -68000,6 +69630,11 @@ width: 81px; height: 99px; } +.Pet-Cactus-Veteran { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Veteran.png'); + width: 81px; + height: 99px; +} .Pet-Cactus-VirtualPet { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-VirtualPet.png'); width: 81px; @@ -68025,6 +69660,56 @@ width: 81px; height: 99px; } +.Pet-Chameleon-Base { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Chameleon-Base.png'); + width: 81px; + height: 99px; +} +.Pet-Chameleon-CottonCandyBlue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Chameleon-CottonCandyBlue.png'); + width: 81px; + height: 99px; +} +.Pet-Chameleon-CottonCandyPink { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Chameleon-CottonCandyPink.png'); + width: 81px; + height: 99px; +} +.Pet-Chameleon-Desert { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Chameleon-Desert.png'); + width: 81px; + height: 99px; +} +.Pet-Chameleon-Golden { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Chameleon-Golden.png'); + width: 81px; + height: 99px; +} +.Pet-Chameleon-Red { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Chameleon-Red.png'); + width: 81px; + height: 99px; +} +.Pet-Chameleon-Shade { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Chameleon-Shade.png'); + width: 81px; + height: 99px; +} +.Pet-Chameleon-Skeleton { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Chameleon-Skeleton.png'); + width: 81px; + height: 99px; +} +.Pet-Chameleon-White { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Chameleon-White.png'); + width: 81px; + height: 99px; +} +.Pet-Chameleon-Zombie { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Chameleon-Zombie.png'); + width: 81px; + height: 99px; +} .Pet-Cheetah-Base { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cheetah-Base.png'); width: 81px; @@ -68410,6 +70095,11 @@ width: 81px; height: 99px; } +.Pet-Dragon-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Koi.png'); + width: 81px; + height: 99px; +} .Pet-Dragon-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Moonglow.png'); width: 81px; @@ -68870,6 +70560,11 @@ width: 81px; height: 99px; } +.Pet-FlyingPig-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Koi.png'); + width: 81px; + height: 99px; +} .Pet-FlyingPig-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Moonglow.png'); width: 81px; @@ -69175,6 +70870,11 @@ width: 81px; height: 99px; } +.Pet-Fox-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Koi.png'); + width: 81px; + height: 99px; +} .Pet-Fox-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Moonglow.png'); width: 81px; @@ -69925,6 +71625,11 @@ width: 81px; height: 99px; } +.Pet-LionCub-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Koi.png'); + width: 81px; + height: 99px; +} .Pet-LionCub-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Moonglow.png'); width: 81px; @@ -70450,6 +72155,11 @@ width: 81px; height: 99px; } +.Pet-PandaCub-Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Koi.png'); + width: 81px; + height: 99px; +} .Pet-PandaCub-Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Moonglow.png'); width: 81px; @@ -72660,11 +74370,6 @@ width: 68px; height: 68px; } -.Pet_HatchingPotion_Fungi { - background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.png'); - width: 68px; - height: 68px; -} .Pet_HatchingPotion_Ghost { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Ghost.png'); width: 68px; @@ -72695,6 +74400,11 @@ width: 68px; height: 68px; } +.Pet_HatchingPotion_Koi { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Koi.png'); + width: 68px; + height: 68px; +} .Pet_HatchingPotion_Moonglow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Moonglow.png'); width: 68px; diff --git a/website/client/src/assets/scss/button.scss b/website/client/src/assets/scss/button.scss index c45f9a8396..62eb95422f 100644 --- a/website/client/src/assets/scss/button.scss +++ b/website/client/src/assets/scss/button.scss @@ -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; diff --git a/website/client/src/assets/scss/icon.scss b/website/client/src/assets/scss/icon.scss index 3ab036333a..ad57951ac6 100644 --- a/website/client/src/assets/scss/icon.scss +++ b/website/client/src/assets/scss/icon.scss @@ -12,7 +12,7 @@ } &.color { - svg path { + svg path, svg polygon { fill: currentColor; } } diff --git a/website/client/src/assets/scss/item.scss b/website/client/src/assets/scss/item.scss index ab52d935d1..c9f0d65f7e 100644 --- a/website/client/src/assets/scss/item.scss +++ b/website/client/src/assets/scss/item.scss @@ -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 { diff --git a/website/client/src/assets/scss/shops.scss b/website/client/src/assets/scss/shops.scss new file mode 100644 index 0000000000..afcb8353a4 --- /dev/null +++ b/website/client/src/assets/scss/shops.scss @@ -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; + } + } + } +} \ No newline at end of file diff --git a/website/client/src/components/admin-panel/user-support/customizationsOwned.vue b/website/client/src/components/admin-panel/user-support/customizationsOwned.vue index 9b244efe60..136baf2afb 100644 --- a/website/client/src/components/admin-panel/user-support/customizationsOwned.vue +++ b/website/client/src/components/admin-panel/user-support/customizationsOwned.vue @@ -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({ diff --git a/website/client/src/components/admin-panel/user-support/itemsOwned.vue b/website/client/src/components/admin-panel/user-support/itemsOwned.vue index 0385857eca..c637596766 100644 --- a/website/client/src/components/admin-panel/user-support/itemsOwned.vue +++ b/website/client/src/components/admin-panel/user-support/itemsOwned.vue @@ -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 }, }, }; diff --git a/website/client/src/components/admin-panel/user-support/userProfile.vue b/website/client/src/components/admin-panel/user-support/userProfile.vue index 2cfe84cc6d..adb100382e 100644 --- a/website/client/src/components/admin-panel/user-support/userProfile.vue +++ b/website/client/src/components/admin-panel/user-support/userProfile.vue @@ -28,15 +28,15 @@
- -
+ +
+ -1 Day + -7 Days + -30 Days +
+ Time Traveling! It is {{ new Date().toLocaleDateString() }} + Reset +
+ +1 Day + +7 Days + +30 Days +
+ +