Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b5bd8e1ab | |||
| e39eafd3f0 | |||
| 92cf506bad | |||
| 5f97cb31b8 | |||
| 6d26fbc5f2 | |||
| bb9912de89 | |||
| 947e8a1836 | |||
| 7bdc974704 | |||
| fe8780d49c | |||
| 2fc4d0f00c | |||
| 4300c7b1bf | |||
| 2cd0ed5973 | |||
| 6e8bdf4cdf | |||
| 0bac1102cc | |||
| 3e96e54ad8 | |||
| 3458d89c1d | |||
| 25e72ad907 | |||
| 5cf6a67a36 | |||
| 9dcce382a3 | |||
| f6484c872a | |||
| 249ba77c01 | |||
| 7ff590cd88 | |||
| f297fef89e | |||
| b037cb0722 | |||
| b8c58a7e4f | |||
| f973bf1038 | |||
| aaea985cf2 | |||
| 1d0e08419f | |||
| fd6244eb15 | |||
| f8aa756d52 | |||
| ae7df804cb | |||
| de37eb1bb2 | |||
| cf03261bbf | |||
| 3ec95ad821 | |||
| 57d11d5b20 | |||
| 039e7d40b8 | |||
| 4389a9b478 | |||
| fd2c4e3265 | |||
| dd91bada8f | |||
| d724933640 | |||
| e4b74bc347 | |||
| c609db09c1 | |||
| 55feebdf9e | |||
| d8a99647e7 | |||
| 353b4aed05 | |||
| 411f82202b | |||
| 5a5a6e4c5d | |||
| 914eee015e | |||
| a301f817e9 | |||
| 519af8f1b6 | |||
| a71abea032 | |||
| 3c623b08c4 | |||
| ddfa3f8a91 | |||
| e2d1de0cf0 | |||
| 9281de1801 | |||
| 4960171565 | |||
| d063a57faa | |||
| 0960eaf571 | |||
| fd0ec41c53 | |||
| 1420e1c8d7 | |||
| c7c854664f | |||
| 68e5679340 | |||
| 32a9dda2c6 | |||
| 8b19c0ad69 | |||
| 33e8b64df6 | |||
| 427251ed1d |
@@ -1,4 +1,6 @@
|
||||
node_modules/**
|
||||
content_cache
|
||||
content_cache/**
|
||||
website/client/**
|
||||
test/**
|
||||
.git/**
|
||||
|
||||
@@ -74,5 +74,6 @@
|
||||
"TRANSIFEX_SLACK_CHANNEL": "transifex",
|
||||
"WEB_CONCURRENCY": 1,
|
||||
"SKIP_SSL_CHECK_KEY": "key",
|
||||
"ENABLE_STACKDRIVER_TRACING": "false"
|
||||
"ENABLE_STACKDRIVER_TRACING": "false",
|
||||
"BLOCKED_IPS": ""
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import gulp from 'gulp';
|
||||
import babel from 'gulp-babel';
|
||||
|
||||
gulp.task('build:src', () => gulp.src('website/server/**/*.js')
|
||||
gulp.task('build:babel:server', () => gulp.src('website/server/**/*.js')
|
||||
.pipe(babel())
|
||||
.pipe(gulp.dest('website/transpiled-babel/')));
|
||||
|
||||
gulp.task('build:common', () => gulp.src('website/common/script/**/*.js')
|
||||
gulp.task('build:babel:common', () => gulp.src('website/common/script/**/*.js')
|
||||
.pipe(babel())
|
||||
.pipe(gulp.dest('website/common/transpiled-babel/')));
|
||||
|
||||
gulp.task('build:server', gulp.series('build:src', 'build:common', done => done()));
|
||||
gulp.task('build:babel', gulp.parallel('build:babel:server', 'build:babel:common', done => done()));
|
||||
|
||||
gulp.task('build:prod', gulp.series(
|
||||
'build:server',
|
||||
'build:babel',
|
||||
'apidoc',
|
||||
'content:cache',
|
||||
done => done(),
|
||||
));
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import gulp from 'gulp';
|
||||
import fs from 'fs';
|
||||
|
||||
// TODO parallelize, use gulp file helpers
|
||||
gulp.task('content:cache', done => {
|
||||
// Requiring at runtime because these files access `common`
|
||||
// code which in production works only if transpiled so after
|
||||
// gulp build:babel:common has run
|
||||
const { CONTENT_CACHE_PATH, getLocalizedContent } = require('../website/server/libs/content'); // eslint-disable-line global-require
|
||||
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
|
||||
|
||||
try {
|
||||
// create the cache folder (if it doesn't exist)
|
||||
try {
|
||||
fs.mkdirSync(CONTENT_CACHE_PATH);
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') throw err;
|
||||
}
|
||||
|
||||
// clone the content for each language and save
|
||||
// localize it
|
||||
// save the result
|
||||
langCodes.forEach(langCode => {
|
||||
fs.writeFileSync(
|
||||
`${CONTENT_CACHE_PATH}${langCode}.json`,
|
||||
getLocalizedContent(langCode),
|
||||
'utf8',
|
||||
);
|
||||
});
|
||||
done();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
@@ -13,8 +13,16 @@ const gulp = require('gulp');
|
||||
|
||||
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
|
||||
require('./gulp/gulp-apidoc'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-content'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-build'); // eslint-disable-line global-require
|
||||
} else {
|
||||
require('glob').sync('./gulp/gulp-*').forEach(require); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-apidoc'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-content'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-build'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-console'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-sprites'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-start'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-tests'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-transifex-test'); // eslint-disable-line global-require
|
||||
require('gulp').task('default', gulp.series('test')); // eslint-disable-line global-require
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ function setUpServer () {
|
||||
setUpServer();
|
||||
|
||||
// Replace this with your migration
|
||||
const processUsers = () => {}; // require('').default;
|
||||
const processUsers = require().default;
|
||||
|
||||
processUsers()
|
||||
.then(() => {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// @migrationName = 'RewardsMigrationFlipNegativeCostsValues';
|
||||
// @authorName = 'hamboomger';
|
||||
// @authorUuid = '80b61b73-2278-4947-b713-a10112cfe7f5';
|
||||
|
||||
/*
|
||||
* For each reward with negative cost, make it positive
|
||||
* by assigning it an absolute value of itself
|
||||
*/
|
||||
|
||||
import { Task } from '../../website/server/models/task';
|
||||
|
||||
async function flipNegativeCostsValues () {
|
||||
const query = {
|
||||
type: 'reward',
|
||||
value: { $lt: 0 },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
value: 1,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const rewards = await Task
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({ _id: 1 })
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (rewards.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const promises = rewards.map(reward => {
|
||||
const positiveValue = Math.abs(reward.value);
|
||||
return Task.update({ _id: reward._id }, { $set: { value: positiveValue } }).exec();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.all(promises);
|
||||
|
||||
query._id = {
|
||||
$gt: rewards[rewards.length - 1]._id,
|
||||
};
|
||||
}
|
||||
|
||||
console.log('All rewards with negative values were updated, migration finished');
|
||||
}
|
||||
|
||||
export default flipNegativeCostsValues;
|
||||
@@ -33,6 +33,9 @@ async function updateUser (user) {
|
||||
each(keys(content.specialPets), pet => {
|
||||
set[`items.pets.${pet}`] = 5;
|
||||
});
|
||||
each(keys(content.wackyPets), pet => {
|
||||
set[`items.pets.${pet}`] = 5;
|
||||
});
|
||||
each(keys(content.mounts), mount => {
|
||||
set[`items.mounts.${mount}`] = true;
|
||||
});
|
||||
@@ -54,7 +57,7 @@ async function updateUser (user) {
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.local.username': 'olson22',
|
||||
'auth.local.username': 'SabreTest',
|
||||
};
|
||||
|
||||
const fields = {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.137.1",
|
||||
"version": "4.138.4",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.8.7",
|
||||
"@babel/preset-env": "^7.8.7",
|
||||
"@babel/register": "^7.8.6",
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.0",
|
||||
"@babel/register": "^7.9.0",
|
||||
"@google-cloud/trace-agent": "^4.2.5",
|
||||
"@slack/client": "^3.8.1",
|
||||
"accepts": "^1.3.5",
|
||||
@@ -14,7 +14,7 @@
|
||||
"amplitude": "^3.5.0",
|
||||
"apidoc": "^0.17.5",
|
||||
"apn": "^2.2.0",
|
||||
"aws-sdk": "^2.635.0",
|
||||
"aws-sdk": "^2.648.0",
|
||||
"bcrypt": "^3.0.8",
|
||||
"body-parser": "^1.18.3",
|
||||
"compression": "^1.7.4",
|
||||
@@ -37,7 +37,7 @@
|
||||
"gulp-nodemon": "^2.5.0",
|
||||
"gulp.spritesmith": "^6.9.0",
|
||||
"habitica-markdown": "^1.3.2",
|
||||
"helmet": "^3.21.3",
|
||||
"helmet": "^3.22.0",
|
||||
"image-size": "^0.8.3",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"js2xmlparser": "^4.0.1",
|
||||
@@ -46,8 +46,8 @@
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.24.0",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^5.9.3",
|
||||
"morgan": "^1.7.0",
|
||||
"mongoose": "^5.9.6",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.10.0",
|
||||
"node-gcm": "^1.0.2",
|
||||
"pageres": "^5.1.0",
|
||||
@@ -58,7 +58,7 @@
|
||||
"paypal-ipn": "3.0.0",
|
||||
"paypal-rest-sdk": "^1.8.1",
|
||||
"ps-tree": "^1.0.0",
|
||||
"regenerator-runtime": "^0.13.3",
|
||||
"regenerator-runtime": "^0.13.5",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^3.0.0",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import * as contentLib from '../../../../website/server/libs/content';
|
||||
import content from '../../../../website/common/script/content';
|
||||
|
||||
describe('contentLib', () => {
|
||||
describe('CONTENT_CACHE_PATH', () => {
|
||||
it('exports CONTENT_CACHE_PATH', () => {
|
||||
expect(contentLib.CONTENT_CACHE_PATH).to.be.a.string;
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocalizedContent', () => {
|
||||
it('clones, not modify, the original content data', () => {
|
||||
contentLib.getLocalizedContent();
|
||||
expect(typeof content.backgrounds.backgrounds062014.beach.text).to.equal('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
CustomError,
|
||||
NotAuthorized,
|
||||
BadRequest,
|
||||
Forbidden,
|
||||
InternalServerError,
|
||||
NotFound,
|
||||
NotificationNotFound,
|
||||
@@ -113,6 +114,32 @@ describe('Custom Errors', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Forbidden', () => {
|
||||
it('is an instance of CustomError', () => {
|
||||
const forbiddenError = new Forbidden();
|
||||
|
||||
expect(forbiddenError).to.be.an.instanceOf(CustomError);
|
||||
});
|
||||
|
||||
it('it returns an http code of 401', () => {
|
||||
const forbiddenError = new Forbidden();
|
||||
|
||||
expect(forbiddenError.httpCode).to.eql(403);
|
||||
});
|
||||
|
||||
it('returns a default message', () => {
|
||||
const forbiddenError = new Forbidden();
|
||||
|
||||
expect(forbiddenError.message).to.eql('Access forbidden.');
|
||||
});
|
||||
|
||||
it('allows a custom message', () => {
|
||||
const forbiddenError = new Forbidden('Custom Error Message');
|
||||
|
||||
expect(forbiddenError.message).to.eql('Custom Error Message');
|
||||
});
|
||||
});
|
||||
|
||||
describe('InternalServerError', () => {
|
||||
it('is an instance of CustomError', () => {
|
||||
const internalServerError = new InternalServerError();
|
||||
|
||||
@@ -30,18 +30,52 @@ describe('logger', () => {
|
||||
|
||||
describe('info', () => {
|
||||
it('calls winston\'s info log', () => {
|
||||
logger.info(1, 2, 3);
|
||||
logger.info('1', 2);
|
||||
expect(infoSpy).to.be.calledOnce;
|
||||
expect(infoSpy).to.be.calledWith(1, 2, 3);
|
||||
expect(infoSpy).to.be.calledWith('1', { extraData: 2 });
|
||||
});
|
||||
|
||||
it('allows up to two arguments', () => {
|
||||
expect(() => logger.info('1', 2, 3)).to.throw;
|
||||
expect(infoSpy).to.not.be.called;
|
||||
});
|
||||
|
||||
it('has default message', () => {
|
||||
logger.info(1);
|
||||
expect(infoSpy).to.be.calledOnce;
|
||||
expect(infoSpy).to.be.calledWith('No message provided for log.', { extraData: 1 });
|
||||
});
|
||||
|
||||
it('wraps non objects', () => {
|
||||
logger.info('message', [1, 2]);
|
||||
expect(infoSpy).to.be.calledOnce;
|
||||
expect(infoSpy).to.be.calledWithMatch('message', { extraData: [1, 2] });
|
||||
});
|
||||
|
||||
it('does not wrap objects', () => {
|
||||
logger.info('message', { a: 1, b: 2 });
|
||||
expect(infoSpy).to.be.calledOnce;
|
||||
expect(infoSpy).to.be.calledWithMatch('message', { a: 1, b: 2 });
|
||||
});
|
||||
|
||||
it('throws if two arguments and no message', () => {
|
||||
expect(() => logger.info({ a: 1 }, { b: 2 })).to.throw;
|
||||
expect(infoSpy).to.not.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', () => {
|
||||
context('non-error object', () => {
|
||||
it('passes through arguments if the first arg is not an error object', () => {
|
||||
logger.error(1, 2, 3, 4);
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWith(1, 2, 3, 4);
|
||||
it('allows up to two arguments', () => {
|
||||
expect(() => logger.error('1', 2, 3)).to.throw;
|
||||
expect(errorSpy).to.not.be.called;
|
||||
});
|
||||
|
||||
it('handled non-error object', () => {
|
||||
logger.error(1, 2);
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWithMatch('logger.error expects an Error instance', {
|
||||
invalidErr: 1,
|
||||
extraData: 2,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,14 +84,12 @@ describe('logger', () => {
|
||||
const errInstance = new Error('An error.');
|
||||
logger.error(errInstance, {
|
||||
data: 1,
|
||||
}, 2, 3);
|
||||
});
|
||||
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWith(
|
||||
errInstance.stack,
|
||||
{ data: 1, fullError: errInstance },
|
||||
2,
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -68,56 +100,60 @@ describe('logger', () => {
|
||||
logger.error(errInstance, {
|
||||
data: 1,
|
||||
fullError: anotherError,
|
||||
}, 2, 3);
|
||||
});
|
||||
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWith(
|
||||
errInstance.stack,
|
||||
{ data: 1, fullError: anotherError },
|
||||
2,
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
it('logs the error when errorData is null', () => {
|
||||
const errInstance = new Error('An error.');
|
||||
|
||||
logger.error(errInstance, null, 2, 3);
|
||||
logger.error(errInstance, null);
|
||||
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWith(
|
||||
expect(errorSpy).to.be.calledWithMatch(
|
||||
errInstance.stack,
|
||||
null,
|
||||
2,
|
||||
3,
|
||||
{ },
|
||||
);
|
||||
});
|
||||
|
||||
it('logs the error when errorData is not an object', () => {
|
||||
const errInstance = new Error('An error.');
|
||||
|
||||
logger.error(errInstance, true, 2, 3);
|
||||
logger.error(errInstance, true);
|
||||
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWith(
|
||||
expect(errorSpy).to.be.calledWithMatch(
|
||||
errInstance.stack,
|
||||
true,
|
||||
2,
|
||||
3,
|
||||
{ extraData: true },
|
||||
);
|
||||
});
|
||||
|
||||
it('logs the error when errorData is a string', () => {
|
||||
const errInstance = new Error('An error.');
|
||||
|
||||
logger.error(errInstance, 'a string');
|
||||
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWithMatch(
|
||||
errInstance.stack,
|
||||
{ extraMessage: 'a string' },
|
||||
);
|
||||
});
|
||||
|
||||
it('logs the error when errorData does not include isHandledError property', () => {
|
||||
const errInstance = new Error('An error.');
|
||||
|
||||
logger.error(errInstance, { httpCode: 400 }, 2, 3);
|
||||
logger.error(errInstance, { httpCode: 400 });
|
||||
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWith(
|
||||
errInstance.stack,
|
||||
{ httpCode: 400, fullError: errInstance },
|
||||
2,
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -127,14 +163,12 @@ describe('logger', () => {
|
||||
logger.error(errInstance, {
|
||||
isHandledError: true,
|
||||
httpCode: 502,
|
||||
}, 2, 3);
|
||||
});
|
||||
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWith(
|
||||
errInstance.stack,
|
||||
{ httpCode: 502, isHandledError: true, fullError: errInstance },
|
||||
2,
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -144,14 +178,12 @@ describe('logger', () => {
|
||||
logger.error(errInstance, {
|
||||
isHandledError: true,
|
||||
httpCode: 403,
|
||||
}, 2, 3);
|
||||
});
|
||||
|
||||
expect(warnSpy).to.be.calledOnce;
|
||||
expect(warnSpy).to.be.calledWith(
|
||||
errInstance.stack,
|
||||
{ httpCode: 403, isHandledError: true, fullError: errInstance },
|
||||
2,
|
||||
3,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -160,7 +192,7 @@ describe('logger', () => {
|
||||
|
||||
errInstance.customField = 'Some interesting data';
|
||||
|
||||
logger.error(errInstance, {}, 2, 3);
|
||||
logger.error(errInstance, {});
|
||||
|
||||
expect(errorSpy).to.be.calledOnce;
|
||||
expect(errorSpy).to.be.calledWith(
|
||||
@@ -170,8 +202,6 @@ describe('logger', () => {
|
||||
customField: 'Some interesting data',
|
||||
},
|
||||
},
|
||||
2,
|
||||
3,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ import { model as User } from '../../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../../website/server/models/group';
|
||||
import {
|
||||
generateGroup,
|
||||
sleep,
|
||||
} from '../../../../../helpers/api-unit.helper';
|
||||
|
||||
describe('Purchasing a group plan for group', () => {
|
||||
@@ -293,7 +292,7 @@ describe('Purchasing a group plan for group', () => {
|
||||
});
|
||||
|
||||
it('sends appropriate emails when subscribed member of group must manually cancel recurring Android subscription', async () => {
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL');
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS_TECH_ASSISTANCE_EMAIL');
|
||||
plan.customerId = 'random';
|
||||
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;
|
||||
|
||||
@@ -308,26 +307,46 @@ describe('Purchasing a group plan for group', () => {
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
await sleep(0.5);
|
||||
|
||||
expect(sender.sendTxn).to.have.callCount(4);
|
||||
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details');
|
||||
expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.args[1][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[1][2]).to.eql([
|
||||
const adminUserSubscriptionDetails = sender.sendTxn.args.find(sendTxnArgs => {
|
||||
const emailType = sendTxnArgs[1];
|
||||
return emailType === 'admin-user-subscription-details';
|
||||
});
|
||||
expect(adminUserSubscriptionDetails).to.exist;
|
||||
expect(adminUserSubscriptionDetails[0].email).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
|
||||
const groupMemberJoinOne = sender.sendTxn.args.find(sendTxnArgs => {
|
||||
const emailType = sendTxnArgs[1];
|
||||
const emailRecipient = sendTxnArgs[0];
|
||||
return emailType === 'group-member-join' && emailRecipient._id === recipient._id;
|
||||
});
|
||||
expect(groupMemberJoinOne).to.exist;
|
||||
expect(groupMemberJoinOne[0]._id).to.equal(recipient._id);
|
||||
expect(groupMemberJoinOne[2]).to.eql([
|
||||
{ name: 'LEADER', content: groupLeaderName },
|
||||
{ name: 'GROUP_NAME', content: groupName },
|
||||
{ name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_GOOGLE },
|
||||
]);
|
||||
expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[2][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins');
|
||||
|
||||
const groupMemberJoinTwo = sender.sendTxn.args.find(sendTxnArgs => {
|
||||
const emailType = sendTxnArgs[1];
|
||||
const emailRecipient = sendTxnArgs[0];
|
||||
return emailType === 'group-member-join' && emailRecipient._id === group.leader;
|
||||
});
|
||||
expect(groupMemberJoinTwo).to.exist;
|
||||
expect(groupMemberJoinTwo[0]._id).to.equal(group.leader);
|
||||
|
||||
const groupSubscriptionBegins = sender.sendTxn.args.find(sendTxnArgs => {
|
||||
const emailType = sendTxnArgs[1];
|
||||
return emailType === 'group-subscription-begins';
|
||||
});
|
||||
expect(groupSubscriptionBegins).to.exist;
|
||||
expect(groupSubscriptionBegins[0]._id).to.equal(group.leader);
|
||||
});
|
||||
|
||||
it('sends appropriate emails when subscribed member of group must manually cancel recurring iOS subscription', async () => {
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('TECH_ASSISTANCE_EMAIL');
|
||||
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS_TECH_ASSISTANCE_EMAIL');
|
||||
plan.customerId = 'random';
|
||||
plan.paymentMethod = api.constants.IOS_PAYMENT_METHOD;
|
||||
|
||||
@@ -342,22 +361,43 @@ describe('Purchasing a group plan for group', () => {
|
||||
data.groupId = group._id;
|
||||
|
||||
await api.createSubscription(data);
|
||||
await sleep(0.5);
|
||||
|
||||
expect(sender.sendTxn).to.have.callCount(4);
|
||||
expect(sender.sendTxn.args[0][0]._id).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
expect(sender.sendTxn.args[0][1]).to.equal('admin-user-subscription-details');
|
||||
expect(sender.sendTxn.args[1][0]._id).to.equal(recipient._id);
|
||||
expect(sender.sendTxn.args[1][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[1][2]).to.eql([
|
||||
|
||||
const adminUserSubscriptionDetails = sender.sendTxn.args.find(sendTxnArgs => {
|
||||
const emailType = sendTxnArgs[1];
|
||||
return emailType === 'admin-user-subscription-details';
|
||||
});
|
||||
expect(adminUserSubscriptionDetails).to.exist;
|
||||
expect(adminUserSubscriptionDetails[0].email).to.equal(TECH_ASSISTANCE_EMAIL);
|
||||
|
||||
const groupMemberJoinOne = sender.sendTxn.args.find(sendTxnArgs => {
|
||||
const emailType = sendTxnArgs[1];
|
||||
const emailRecipient = sendTxnArgs[0];
|
||||
return emailType === 'group-member-join' && emailRecipient._id === recipient._id;
|
||||
});
|
||||
expect(groupMemberJoinOne).to.exist;
|
||||
expect(groupMemberJoinOne[0]._id).to.equal(recipient._id);
|
||||
expect(groupMemberJoinOne[2]).to.eql([
|
||||
{ name: 'LEADER', content: groupLeaderName },
|
||||
{ name: 'GROUP_NAME', content: groupName },
|
||||
{ name: 'PREVIOUS_SUBSCRIPTION_TYPE', content: EMAIL_TEMPLATE_SUBSCRIPTION_TYPE_IOS },
|
||||
]);
|
||||
expect(sender.sendTxn.args[2][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[2][1]).to.equal('group-member-join');
|
||||
expect(sender.sendTxn.args[3][0]._id).to.equal(group.leader);
|
||||
expect(sender.sendTxn.args[3][1]).to.equal('group-subscription-begins');
|
||||
|
||||
const groupMemberJoinTwo = sender.sendTxn.args.find(sendTxnArgs => {
|
||||
const emailType = sendTxnArgs[1];
|
||||
const emailRecipient = sendTxnArgs[0];
|
||||
return emailType === 'group-member-join' && emailRecipient._id === group.leader;
|
||||
});
|
||||
expect(groupMemberJoinTwo).to.exist;
|
||||
expect(groupMemberJoinTwo[0]._id).to.equal(group.leader);
|
||||
|
||||
const groupSubscriptionBegins = sender.sendTxn.args.find(sendTxnArgs => {
|
||||
const emailType = sendTxnArgs[1];
|
||||
return emailType === 'group-subscription-begins';
|
||||
});
|
||||
expect(groupSubscriptionBegins).to.exist;
|
||||
expect(groupSubscriptionBegins[0]._id).to.equal(group.leader);
|
||||
});
|
||||
|
||||
it('adds months to members with existing gift subscription', async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import got from 'got';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
WebhookSender,
|
||||
taskScoredWebhook,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
import {
|
||||
generateUser,
|
||||
defer,
|
||||
sleep,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
|
||||
|
||||
@@ -322,6 +324,99 @@ describe('webhooks', () => {
|
||||
json: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('failures', () => {
|
||||
let sendWebhook;
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox.restore();
|
||||
sandbox.stub(got, 'post').returns(Promise.reject());
|
||||
|
||||
sendWebhook = new WebhookSender({ type: 'taskActivity' });
|
||||
user.webhooks = [{
|
||||
url: 'http://custom-url.com', enabled: true, type: 'taskActivity',
|
||||
}];
|
||||
await user.save();
|
||||
|
||||
expect(user.webhooks[0].failures).to.equal(0);
|
||||
expect(user.webhooks[0].lastFailureAt).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('does not increase failures counter if request is successfull', async () => {
|
||||
sandbox.restore();
|
||||
sandbox.stub(got, 'post').returns(Promise.resolve());
|
||||
|
||||
const body = {};
|
||||
sendWebhook.send(user, body);
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
json: true,
|
||||
body,
|
||||
});
|
||||
|
||||
await sleep(0.1);
|
||||
user = await User.findById(user._id).exec();
|
||||
|
||||
expect(user.webhooks[0].failures).to.equal(0);
|
||||
expect(user.webhooks[0].lastFailureAt).to.equal(undefined);
|
||||
});
|
||||
|
||||
it('records failures', async () => {
|
||||
const body = {};
|
||||
sendWebhook.send(user, body);
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
json: true,
|
||||
body,
|
||||
});
|
||||
|
||||
await sleep(0.1);
|
||||
user = await User.findById(user._id).exec();
|
||||
|
||||
expect(user.webhooks[0].failures).to.equal(1);
|
||||
expect((Date.now() - user.webhooks[0].lastFailureAt.getTime()) < 10000).to.be.true;
|
||||
});
|
||||
|
||||
it('disables a webhook after 10 failures', async () => {
|
||||
const times = 10;
|
||||
for (let i = 0; i < times; i += 1) {
|
||||
sendWebhook.send(user, {});
|
||||
await sleep(0.1); // eslint-disable-line no-await-in-loop
|
||||
user = await User.findById(user._id).exec(); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
|
||||
expect(got.post).to.be.callCount(10);
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com');
|
||||
|
||||
await sleep(0.1);
|
||||
user = await User.findById(user._id).exec();
|
||||
|
||||
expect(user.webhooks[0].enabled).to.equal(false);
|
||||
expect(user.webhooks[0].failures).to.equal(0);
|
||||
});
|
||||
|
||||
it('resets failures after a month ', async () => {
|
||||
const oneMonthAgo = moment().subtract(1, 'months').subtract(1, 'days').toDate();
|
||||
user.webhooks[0].lastFailureAt = oneMonthAgo;
|
||||
user.webhooks[0].failures = 9;
|
||||
|
||||
await user.save();
|
||||
|
||||
sendWebhook.send(user, []);
|
||||
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com');
|
||||
|
||||
await sleep(0.1);
|
||||
user = await User.findById(user._id).exec();
|
||||
|
||||
expect(user.webhooks[0].failures).to.equal(1);
|
||||
// Check that the stored date is whitin 10s from now
|
||||
expect((Date.now() - user.webhooks[0].lastFailureAt.getTime()) < 10000).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('taskScoredWebhook', () => {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateNext,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import { Forbidden } from '../../../../website/server/libs/errors';
|
||||
import apiError from '../../../../website/server/libs/apiError';
|
||||
|
||||
function checkErrorThrown (next) {
|
||||
expect(next).to.have.been.calledOnce;
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(calledWith[0].message).to.equal(apiError('ipAddressBlocked'));
|
||||
expect(calledWith[0] instanceof Forbidden).to.equal(true);
|
||||
}
|
||||
|
||||
function checkErrorNotThrown (next) {
|
||||
expect(next).to.have.been.calledOnce;
|
||||
const calledWith = next.getCall(0).args;
|
||||
expect(typeof calledWith[0] === 'undefined').to.equal(true);
|
||||
}
|
||||
|
||||
describe('ipBlocker middleware', () => {
|
||||
const pathToIpBlocker = '../../../../website/server/middlewares/ipBlocker';
|
||||
|
||||
let res; let req; let next;
|
||||
|
||||
beforeEach(() => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
next = generateNext();
|
||||
});
|
||||
|
||||
it('is disabled when the env var is not defined', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns(undefined);
|
||||
const attachIpBlocker = requireAgain(pathToIpBlocker).default;
|
||||
attachIpBlocker(req, res, next);
|
||||
|
||||
checkErrorNotThrown(next);
|
||||
});
|
||||
|
||||
it('is disabled when the env var is an empty string', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('');
|
||||
const attachIpBlocker = requireAgain(pathToIpBlocker).default;
|
||||
attachIpBlocker(req, res, next);
|
||||
|
||||
checkErrorNotThrown(next);
|
||||
});
|
||||
|
||||
it('is disabled when the env var contains comma separated empty strings', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns(' , , ');
|
||||
const attachIpBlocker = requireAgain(pathToIpBlocker).default;
|
||||
attachIpBlocker(req, res, next);
|
||||
|
||||
checkErrorNotThrown(next);
|
||||
});
|
||||
|
||||
it('does not throw when the ip does not match', () => {
|
||||
req.headers['x-forwarded-for'] = '192.168.1.1';
|
||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('192.168.1.2');
|
||||
const attachIpBlocker = requireAgain(pathToIpBlocker).default;
|
||||
attachIpBlocker(req, res, next);
|
||||
|
||||
checkErrorNotThrown(next);
|
||||
});
|
||||
|
||||
it('throws when a matching ip exist in x-forwarded-for', () => {
|
||||
req.headers['x-forwarded-for'] = '192.168.1.1';
|
||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('192.168.1.1');
|
||||
const attachIpBlocker = requireAgain(pathToIpBlocker).default;
|
||||
attachIpBlocker(req, res, next);
|
||||
|
||||
checkErrorThrown(next);
|
||||
});
|
||||
|
||||
it('trims ips in x-forwarded-for', () => {
|
||||
req.headers['x-forwarded-for'] = '192.168.1.1';
|
||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns(', 192.168.1.1 , 192.168.1.4, ');
|
||||
const attachIpBlocker = requireAgain(pathToIpBlocker).default;
|
||||
attachIpBlocker(req, res, next);
|
||||
|
||||
checkErrorThrown(next);
|
||||
});
|
||||
|
||||
it('works when multiple ips are passed in x-forwarded-for', () => {
|
||||
req.headers['x-forwarded-for'] = '192.168.1.4';
|
||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('192.168.1.1, 192.168.1.4, 192.168.1.3');
|
||||
const attachIpBlocker = requireAgain(pathToIpBlocker).default;
|
||||
attachIpBlocker(req, res, next);
|
||||
|
||||
checkErrorThrown(next);
|
||||
});
|
||||
});
|
||||
@@ -11,7 +11,9 @@ import apiError from '../../../../../website/server/libs/apiError';
|
||||
|
||||
describe('GET /groups', () => {
|
||||
let user;
|
||||
let userInGuild;
|
||||
const NUMBER_OF_PUBLIC_GUILDS = 3; // 2 + the tavern
|
||||
const NUMBER_OF_PUBLIC_GUILDS_USER_IS_LEADER = 2;
|
||||
const NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER = 1;
|
||||
const NUMBER_OF_USERS_PRIVATE_GUILDS = 1;
|
||||
const NUMBER_OF_GROUPS_USER_CAN_VIEW = 5;
|
||||
@@ -33,14 +35,20 @@ describe('GET /groups', () => {
|
||||
name: 'public guild - is member',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
summary: 'ohayou kombonwa',
|
||||
description: 'oyasumi',
|
||||
});
|
||||
await leader.post(`/groups/${publicGuildUserIsMemberOf._id}/invite`, { uuids: [user._id] });
|
||||
await user.post(`/groups/${publicGuildUserIsMemberOf._id}/join`);
|
||||
|
||||
userInGuild = await generateUser({ guilds: [publicGuildUserIsMemberOf._id] });
|
||||
|
||||
publicGuildNotMember = await generateGroup(leader, {
|
||||
name: 'public guild - is not member',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
summary: 'Natsume Soseki',
|
||||
description: 'Kinnosuke no Hondana',
|
||||
categories,
|
||||
});
|
||||
|
||||
@@ -150,6 +158,35 @@ describe('GET /groups', () => {
|
||||
|
||||
expect(guilds.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('filters public guilds by leader role', async () => {
|
||||
const guilds = await user.get('/groups?type=publicGuilds&leader=true');
|
||||
expect(guilds.length).to.equal(NUMBER_OF_PUBLIC_GUILDS_USER_IS_LEADER);
|
||||
});
|
||||
|
||||
it('filters public guilds by member role', async () => {
|
||||
const guilds = await userInGuild.get('/groups?type=publicGuilds&member=true');
|
||||
expect(guilds.length).to.equal(1);
|
||||
expect(guilds[0].name).to.have.string('is member');
|
||||
});
|
||||
|
||||
it('filters public guilds by single-word search term', async () => {
|
||||
const guilds = await user.get('/groups?type=publicGuilds&search=kom');
|
||||
expect(guilds.length).to.equal(1);
|
||||
expect(guilds[0].summary).to.have.string('ohayou kombonwa');
|
||||
});
|
||||
|
||||
it('filters public guilds by single-word search term left and right-padded by spaces', async () => {
|
||||
const guilds = await user.get('/groups?type=publicGuilds&search=++++ohayou+kombonwa+++++');
|
||||
expect(guilds.length).to.equal(1);
|
||||
expect(guilds[0].summary).to.have.string('ohayou kombonwa');
|
||||
});
|
||||
|
||||
it('filters public guilds by two-words search term separated by multiple spaces', async () => {
|
||||
const guilds = await user.get('/groups?type=publicGuilds&search=kinnosuke+++++hon');
|
||||
expect(guilds.length).to.equal(1);
|
||||
expect(guilds[0].description).to.have.string('Kinnosuke');
|
||||
});
|
||||
});
|
||||
|
||||
describe('public guilds pagination', () => {
|
||||
|
||||
@@ -55,6 +55,18 @@ describe('POST /tasks/user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if reward value is a negative number', async () => {
|
||||
await expect(user.post('/tasks/user', {
|
||||
type: 'reward',
|
||||
text: 'reward with negative value',
|
||||
value: -10,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'reward validation failed',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not update user.tasksOrder.{taskType} when the task is not saved because invalid', async () => {
|
||||
const originalHabitsOrder = (await user.get('/user')).tasksOrder.habits;
|
||||
await expect(user.post('/tasks/user', {
|
||||
|
||||
@@ -530,5 +530,15 @@ describe('PUT /tasks/:id', () => {
|
||||
|
||||
expect(savedReward.value).to.eql(100);
|
||||
});
|
||||
|
||||
it('returns an error if reward value is a negative number', async () => {
|
||||
await expect(user.put(`/tasks/${reward._id}`, {
|
||||
value: -10,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'reward validation failed',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,6 +81,16 @@ describe('POST /user/webhook', () => {
|
||||
expect(webhook.type).to.eql('taskActivity');
|
||||
});
|
||||
|
||||
it('ignores protected fields', async () => {
|
||||
body.failures = 3;
|
||||
body.lastFailureAt = new Date();
|
||||
|
||||
const webhook = await user.post('/user/webhook', body);
|
||||
|
||||
expect(webhook.failures).to.eql(0);
|
||||
expect(webhook.lastFailureAt).to.eql(undefined);
|
||||
});
|
||||
|
||||
it('successfully adds the webhook', async () => {
|
||||
expect(user.webhooks).to.eql([]);
|
||||
|
||||
|
||||
@@ -63,6 +63,21 @@ describe('PUT /user/webhook/:id', () => {
|
||||
expect(webhook.options).to.eql(options);
|
||||
});
|
||||
|
||||
it('ignores protected fields', async () => {
|
||||
const failures = 3;
|
||||
const lastFailureAt = new Date();
|
||||
|
||||
await user.put(`/user/webhook/${webhookToUpdate.id}`, {
|
||||
failures, lastFailureAt,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
const webhook = user.webhooks.find(hook => webhookToUpdate.id === hook.id);
|
||||
|
||||
expect(webhook.failures).to.eql(0);
|
||||
expect(webhook.lastFailureAt).to.eql(undefined);
|
||||
});
|
||||
|
||||
it('updates a webhook with empty label', async () => {
|
||||
const url = 'http://a-new-url.com';
|
||||
const type = 'groupChatReceived';
|
||||
|
||||
@@ -18,18 +18,18 @@
|
||||
"@vue/cli-plugin-router": "^4.2.3",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.2.3",
|
||||
"@vue/cli-service": "^4.2.3",
|
||||
"@storybook/addon-actions": "^5.3.14",
|
||||
"@storybook/addon-knobs": "^5.3.14",
|
||||
"@storybook/addon-links": "^5.3.14",
|
||||
"@storybook/addon-notes": "^5.3.14",
|
||||
"@storybook/vue": "^5.3.14",
|
||||
"@storybook/addon-actions": "^5.3.17",
|
||||
"@storybook/addon-knobs": "^5.3.17",
|
||||
"@storybook/addon-links": "^5.3.17",
|
||||
"@storybook/addon-notes": "^5.3.17",
|
||||
"@storybook/vue": "^5.3.17",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^5.9.0",
|
||||
"amplitude-js": "^5.10.0",
|
||||
"axios": "^0.19.2",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^4.4.1",
|
||||
"bootstrap-vue": "^2.6.1",
|
||||
"bootstrap-vue": "^2.9.0",
|
||||
"chai": "^4.1.2",
|
||||
"core-js": "^3.6.4",
|
||||
"eslint": "^6.8.0",
|
||||
@@ -44,7 +44,7 @@
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.24.0",
|
||||
"nconf": "^0.10.0",
|
||||
"sass": "^1.26.2",
|
||||
"sass": "^1.26.3",
|
||||
"sass-loader": "^8.0.2",
|
||||
"smartbanner.js": "^1.15.0",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
@@ -58,9 +58,9 @@
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.1.6",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vue2-perfect-scrollbar": "^1.3.0",
|
||||
"vue2-perfect-scrollbar": "^1.4.0",
|
||||
"vuedraggable": "^2.23.1",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
|
||||
"webpack": "^4.42.0"
|
||||
"webpack": "^4.42.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
.promo_achievement_CottonCandyPink {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -328px -316px;
|
||||
background-position: -952px -740px;
|
||||
width: 204px;
|
||||
height: 102px;
|
||||
}
|
||||
.promo_april_fools_2020 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -952px 0px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_armoire_backgrounds_202003 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -445px;
|
||||
background-position: -340px -659px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
@@ -16,45 +22,93 @@
|
||||
width: 623px;
|
||||
height: 167px;
|
||||
}
|
||||
.promo_egg_quest {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -952px -444px;
|
||||
width: 354px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_hugabug_bundle {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -445px;
|
||||
background-position: -952px -296px;
|
||||
width: 420px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_mystery_202003 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -624px -211px;
|
||||
background-position: -952px -592px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_mystery_202004 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -624px -277px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_pi_day {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -593px;
|
||||
background-position: -316px -316px;
|
||||
width: 273px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_seasonal_shop_spring {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -764px -659px;
|
||||
width: 162px;
|
||||
height: 138px;
|
||||
}
|
||||
.promo_spring_2019 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -430px -475px;
|
||||
width: 432px;
|
||||
height: 162px;
|
||||
}
|
||||
.promo_spring_2020 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -475px;
|
||||
width: 429px;
|
||||
height: 183px;
|
||||
}
|
||||
.promo_spring_potions_2020 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -952px -148px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -624px -359px;
|
||||
background-position: -1235px -592px;
|
||||
width: 96px;
|
||||
height: 69px;
|
||||
}
|
||||
.scene_QuartzFox {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -870px;
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.scene_dailies {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -168px;
|
||||
background-position: -624px 0px;
|
||||
width: 327px;
|
||||
height: 276px;
|
||||
}
|
||||
.scene_gaining_achievement {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -624px 0px;
|
||||
background-position: 0px -659px;
|
||||
width: 339px;
|
||||
height: 210px;
|
||||
}
|
||||
.scene_shanaqui {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -328px -168px;
|
||||
background-position: -316px -168px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
}
|
||||
.scene_tough_times {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -168px;
|
||||
width: 315px;
|
||||
height: 306px;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 278 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 426 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 214 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 140 KiB |
@@ -5,14 +5,7 @@
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
&:not([class*="FlyingPig"]) {
|
||||
top: -28px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.Pet[class*="FlyingPig"] {
|
||||
top: 7px !important;
|
||||
top: -28px !important;
|
||||
}
|
||||
|
||||
.Pet.Pet-Dragon-Hydra {
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// possible values are: normal, fall, habitoween, thanksgiving, winter, nye, birthday, valentines, spring, summer
|
||||
// more to be added on future seasons
|
||||
|
||||
$npc_market_flavor: 'normal';
|
||||
$npc_quests_flavor: 'normal';
|
||||
$npc_seasonal_flavor: 'normal';
|
||||
$npc_timetravelers_flavor: 'normal';
|
||||
$npc_tavern_flavor: 'normal';
|
||||
$npc_market_flavor: 'aprilfools';
|
||||
$npc_quests_flavor: 'aprilfools';
|
||||
$npc_seasonal_flavor: 'aprilfools';
|
||||
$npc_timetravelers_flavor: 'aprilfools';
|
||||
$npc_tavern_flavor: 'aprilfools';
|
||||
|
||||
$restingToolbarHeight: 40px;
|
||||
$menuToolbarHeight: 56px;
|
||||
|
||||
@@ -225,30 +225,30 @@ export default {
|
||||
classGear (heroClass) {
|
||||
if (heroClass === 'rogue') {
|
||||
return {
|
||||
armor: 'armor_rogue_5',
|
||||
head: 'head_rogue_5',
|
||||
shield: 'shield_rogue_6',
|
||||
weapon: 'weapon_rogue_6',
|
||||
armor: 'armor_special_spring2020Rogue',
|
||||
head: 'head_special_spring2020Rogue',
|
||||
shield: 'shield_special_spring2020Rogue',
|
||||
weapon: 'weapon_special_spring2020Rogue',
|
||||
};
|
||||
} if (heroClass === 'wizard') {
|
||||
return {
|
||||
armor: 'armor_wizard_5',
|
||||
head: 'head_wizard_5',
|
||||
weapon: 'weapon_wizard_6',
|
||||
armor: 'armor_special_spring2020Mage',
|
||||
head: 'head_special_spring2020Mage',
|
||||
weapon: 'weapon_special_spring2020Mage',
|
||||
};
|
||||
} if (heroClass === 'healer') {
|
||||
return {
|
||||
armor: 'armor_healer_5',
|
||||
head: 'head_healer_5',
|
||||
shield: 'shield_healer_5',
|
||||
weapon: 'weapon_healer_6',
|
||||
armor: 'armor_special_spring2020Healer',
|
||||
head: 'head_special_spring2020Healer',
|
||||
shield: 'shield_special_spring2020Healer',
|
||||
weapon: 'weapon_special_spring2020Healer',
|
||||
};
|
||||
}
|
||||
return {
|
||||
armor: 'armor_warrior_5',
|
||||
head: 'head_warrior_5',
|
||||
shield: 'shield_warrior_5',
|
||||
weapon: 'weapon_warrior_6',
|
||||
armor: 'armor_special_spring2020Warrior',
|
||||
head: 'head_special_spring2020Warrior',
|
||||
shield: 'shield_special_spring2020Warrior',
|
||||
weapon: 'weapon_special_spring2020Warrior',
|
||||
};
|
||||
},
|
||||
selectionBox (selectedClass, heroClass) {
|
||||
|
||||
@@ -79,9 +79,8 @@
|
||||
></span>
|
||||
<!-- Pet-->
|
||||
<span
|
||||
v-if="member.items.currentPet"
|
||||
class="current-pet"
|
||||
:class="'Pet-' + member.items.currentPet"
|
||||
:class="foolPet(member.items.currentPet)"
|
||||
></span>
|
||||
</template>
|
||||
</div>
|
||||
@@ -124,6 +123,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import includes from 'lodash/includes';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
import ClassBadge from '@/components/members/classBadge';
|
||||
@@ -189,7 +189,7 @@ export default {
|
||||
let val = '27px';
|
||||
|
||||
if (!this.avatarOnly) {
|
||||
if (this.member.items.currentPet) val = '24px';
|
||||
// if (this.member.items.currentPet) val = '24px';
|
||||
if (this.member.items.currentMount) val = '0px';
|
||||
}
|
||||
|
||||
@@ -276,6 +276,52 @@ export default {
|
||||
|
||||
return !buffs.snowball && !buffs.spookySparkles && !buffs.shinySeed && !buffs.seafoam;
|
||||
},
|
||||
foolPet (pet) {
|
||||
const SPECIAL_PETS = [
|
||||
'Wolf-Veteran',
|
||||
'Wolf-Cerberus',
|
||||
'Dragon-Hydra',
|
||||
'Turkey-Base',
|
||||
'BearCub-Polar',
|
||||
'MantisShrimp-Base',
|
||||
'JackOLantern-Base',
|
||||
'Mammoth-Base',
|
||||
'Tiger-Veteran',
|
||||
'Phoenix-Base',
|
||||
'Turkey-Gilded',
|
||||
'MagicalBee-Base',
|
||||
'Lion-Veteran',
|
||||
'Gryphon-RoyalPurple',
|
||||
'JackOLantern-Ghost',
|
||||
'Jackalope-RoyalPurple',
|
||||
'Orca-Base',
|
||||
'Bear-Veteran',
|
||||
'Hippogriff-Hopeful',
|
||||
'Fox-Veteran',
|
||||
'JackOLantern-Glow',
|
||||
'Gryphon-Gryphatrice',
|
||||
];
|
||||
const BASE_PETS = [
|
||||
'Wolf',
|
||||
'TigerCub',
|
||||
'PandaCub',
|
||||
'LionCub',
|
||||
'Fox',
|
||||
'FlyingPig',
|
||||
'BearCub',
|
||||
'Dragon',
|
||||
'Cactus',
|
||||
];
|
||||
if (!pet) return 'Pet-Cactus-Dessert';
|
||||
if (SPECIAL_PETS.indexOf(pet) !== -1) {
|
||||
return 'Pet-LionCub-Dessert';
|
||||
}
|
||||
const species = pet.slice(0, pet.indexOf('-'));
|
||||
if (includes(BASE_PETS, species)) {
|
||||
return `Pet-${species}-Dessert`;
|
||||
}
|
||||
return 'Pet-FlyingPig-Dessert';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="hasParty"
|
||||
ref="partyMembersDiv"
|
||||
v-resize="1500"
|
||||
class="party-members d-flex"
|
||||
@resized="setPartyMembersWidth($event)"
|
||||
@@ -153,6 +154,15 @@ export default {
|
||||
inviteModalGroupType: undefined,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
hideHeader () {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.partyMembersDiv) {
|
||||
this.setPartyMembersWidth({ width: this.$refs.partyMembersDiv.clientWidth });
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
user: 'user:data',
|
||||
|
||||
@@ -161,7 +161,10 @@ export default {
|
||||
},
|
||||
getPetItemClass () {
|
||||
if (this.isOwned() || (this.mountOwned() && this.isHatchable())) {
|
||||
return `Pet Pet-${this.item.key} ${this.item.eggKey}`;
|
||||
if (this.isSpecial()) {
|
||||
return 'Pet Pet-LionCub-Dessert';
|
||||
}
|
||||
return `${this.item.class} ${this.item.eggKey}`;
|
||||
}
|
||||
|
||||
if (!this.isOwned() && this.isSpecial()) {
|
||||
@@ -173,7 +176,7 @@ export default {
|
||||
}
|
||||
|
||||
if (this.mountOwned()) {
|
||||
return `GreyedOut Pet Pet-${this.item.key} ${this.item.eggKey}`;
|
||||
return `GreyedOut ${this.item.class} ${this.item.eggKey}`;
|
||||
}
|
||||
|
||||
// Can't hatch
|
||||
|
||||
@@ -833,6 +833,7 @@ export default {
|
||||
},
|
||||
async deleteSocialAuth (network) {
|
||||
await axios.delete(`/api/v4/user/auth/social/${network.key}`);
|
||||
this.user.auth[network.key] = {};
|
||||
this.text(this.$t('detachedSocial', { network: network.name }));
|
||||
},
|
||||
async socialAuth (network) {
|
||||
|
||||
@@ -138,7 +138,8 @@
|
||||
class="inline-edit-input checklist-item form-control"
|
||||
type="text"
|
||||
:placeholder="$t('newChecklistItem')"
|
||||
@keydown.enter="addChecklistItem($event)"
|
||||
@keypress.enter="setHasPossibilityOfIMEConversion(false)"
|
||||
@keyup.enter="addChecklistItem($event)"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
@@ -1222,6 +1223,7 @@ export default {
|
||||
per: 'perception',
|
||||
},
|
||||
calendarHighlights: { dates: [new Date()] },
|
||||
hasPossibilityOfIMEConversion: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -1384,7 +1386,12 @@ export default {
|
||||
sorting.splice(data.newIndex, 0, movingItem);
|
||||
this.task.checklist = sorting;
|
||||
},
|
||||
setHasPossibilityOfIMEConversion (bool) {
|
||||
this.hasPossibilityOfIMEConversion = bool;
|
||||
},
|
||||
addChecklistItem (e) {
|
||||
if (e) e.preventDefault();
|
||||
if (this.hasPossibilityOfIMEConversion) return;
|
||||
const checkListItem = {
|
||||
id: uuid.v4(),
|
||||
text: this.newChecklistItem,
|
||||
@@ -1394,7 +1401,7 @@ export default {
|
||||
// @TODO: managing checklist separately to help with sorting on the UI
|
||||
this.checklist.push(checkListItem);
|
||||
this.newChecklistItem = null;
|
||||
if (e) e.preventDefault();
|
||||
this.setHasPossibilityOfIMEConversion(true);
|
||||
},
|
||||
removeChecklistItem (i) {
|
||||
this.task.checklist.splice(i, 1);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
|
||||
import content from '@/../../common/script/content';
|
||||
|
||||
const specialPets = Object.keys(content.specialPets);
|
||||
const wackyPets = Object.keys(content.wackyPets);
|
||||
const questPets = Object.keys(content.questPets);
|
||||
const premiumPets = Object.keys(content.premiumPets);
|
||||
const dropPets = Object.keys(content.pets);
|
||||
|
||||
function getText (textOrFunction) {
|
||||
if (textOrFunction instanceof Function) {
|
||||
@@ -34,10 +36,20 @@ export function isSpecial (animal) {
|
||||
|
||||
export function createAnimal (egg, potion, type, _content, userItems) {
|
||||
const animalKey = `${egg.key}-${potion.key}`;
|
||||
let fooledKey = '';
|
||||
if (questPets.includes(animalKey)) {
|
||||
fooledKey = 'FlyingPig-Dessert';
|
||||
} else if (dropPets.includes(animalKey)
|
||||
|| premiumPets.includes(animalKey)
|
||||
|| wackyPets.includes(animalKey)) {
|
||||
fooledKey = `${egg.key}-Dessert`;
|
||||
} else {
|
||||
fooledKey = animalKey;
|
||||
}
|
||||
|
||||
return {
|
||||
key: animalKey,
|
||||
class: type === 'pet' ? `Pet Pet-${animalKey}` : `Mount_Icon_${animalKey}`,
|
||||
class: type === 'pet' ? `Pet Pet-${fooledKey}` : `Mount_Icon_${animalKey}`,
|
||||
eggKey: egg.key,
|
||||
eggName: getText(egg.text),
|
||||
potionKey: potion.key,
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
import intersection from 'lodash/intersection';
|
||||
import _ from 'lodash';
|
||||
|
||||
const containsAnyCi = (target, patterns) => patterns.some(el => target.match(new RegExp(el, 'i')));
|
||||
const isPassedSearch = ({ name, summary, description }, search) => {
|
||||
if (!search) return false;
|
||||
|
||||
const searchWords = _.escapeRegExp(search.trim()).split(/\s+/);
|
||||
|
||||
if (containsAnyCi(name, searchWords)) return true;
|
||||
|
||||
if (!summary) return false;
|
||||
if (containsAnyCi(summary, searchWords)) return true;
|
||||
|
||||
if (!description) return false;
|
||||
if (containsAnyCi(description, searchWords)) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export default {
|
||||
filters: {
|
||||
@@ -56,9 +74,7 @@ export default {
|
||||
|
||||
if (group._id === this.$store.state.constants.TAVERN_ID || group._id === 'habitrpg') return false;
|
||||
|
||||
if (search) {
|
||||
passedSearch = group.name.toLowerCase().indexOf(search.toLowerCase()) >= 0;
|
||||
}
|
||||
if (search) passedSearch = isPassedSearch(group, search);
|
||||
|
||||
if (filters.categories && filters.categories.length > 0) {
|
||||
const intersectingCats = intersection(filters.categories, group.categorySlugs);
|
||||
@@ -75,11 +91,11 @@ export default {
|
||||
}
|
||||
|
||||
if (filters.guildSize && filters.guildSize.indexOf('gold_tier') !== -1) {
|
||||
correctSize = group.memberCount > 1000;
|
||||
correctSize = group.memberCount >= 1000;
|
||||
}
|
||||
|
||||
if (filters.guildSize && filters.guildSize.indexOf('silver_tier') !== -1) {
|
||||
correctSize = group.memberCount > 100 && group.memberCount < 1000;
|
||||
correctSize = group.memberCount >= 100 && group.memberCount < 1000;
|
||||
}
|
||||
|
||||
if (filters.guildSize && filters.guildSize.indexOf('bronze_tier') !== -1) {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import Store from '@/libs/store';
|
||||
import myGuilds from '@/components/groups/myGuilds';
|
||||
import PublicGuildItem from '@/components/groups/publicGuildItem';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Store);
|
||||
|
||||
describe('myGuilds component', () => {
|
||||
let computed;
|
||||
const guilds = [{
|
||||
_id: '1',
|
||||
type: 'guild',
|
||||
name: 'Crimson Vow',
|
||||
summary: 'testing',
|
||||
description: 'testing',
|
||||
}, {
|
||||
_id: '2',
|
||||
type: 'guild',
|
||||
name: 'Log Horizon',
|
||||
summary: 'testing',
|
||||
description: 'testing',
|
||||
}, {
|
||||
_id: '3',
|
||||
type: 'guild',
|
||||
name: 'CAD Cads',
|
||||
summary: '3D',
|
||||
description: '3D',
|
||||
}, {
|
||||
_id: '4',
|
||||
type: 'guild',
|
||||
name: 'Santa Claus',
|
||||
summary: '3d',
|
||||
description: 'hohoho',
|
||||
}];
|
||||
const store = new Store({
|
||||
state: {
|
||||
user: {
|
||||
data: {
|
||||
_id: '999',
|
||||
guilds: ['1', '2', '3', '4'],
|
||||
},
|
||||
},
|
||||
editingGroup: {},
|
||||
constants: {
|
||||
TAVERN_ID: '9999',
|
||||
},
|
||||
},
|
||||
getters: {},
|
||||
actions: {
|
||||
'guilds:getMyGuilds': () => guilds,
|
||||
},
|
||||
});
|
||||
|
||||
function makeWrapper (opts = {}) {
|
||||
return shallowMount(myGuilds, {
|
||||
data () {
|
||||
return {
|
||||
filter: {},
|
||||
search: '',
|
||||
};
|
||||
},
|
||||
store,
|
||||
localVue,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
|
||||
before(() => {
|
||||
computed = {
|
||||
guilds: () => guilds,
|
||||
};
|
||||
});
|
||||
|
||||
it('renders all guilds with no filter and no search', () => {
|
||||
const wrapper = makeWrapper({ computed });
|
||||
expect(wrapper.findAll(PublicGuildItem).length).to.equal(4);
|
||||
});
|
||||
|
||||
it('renders guilds with name matching against a single-word search term', () => {
|
||||
const search = 'vow';
|
||||
const wrapper = makeWrapper({ computed });
|
||||
wrapper.setData({ search });
|
||||
expect(wrapper.findAll(PublicGuildItem).length).to.equal(1);
|
||||
});
|
||||
|
||||
it('renders guilds with summary matching against a single-word search term', () => {
|
||||
const search = '3d';
|
||||
const wrapper = makeWrapper({ computed });
|
||||
wrapper.setData({ search });
|
||||
expect(wrapper.findAll(PublicGuildItem).length).to.equal(2);
|
||||
});
|
||||
|
||||
it('renders guilds with description matching against a single-word search term', () => {
|
||||
const search = 'hoho';
|
||||
const wrapper = makeWrapper({ computed });
|
||||
wrapper.setData({ search });
|
||||
expect(wrapper.findAll(PublicGuildItem).length).to.equal(1);
|
||||
});
|
||||
|
||||
it('renders guilds with summary matching against two search terms with space in between', () => {
|
||||
const search = '3d ohayou';
|
||||
const wrapper = makeWrapper({ computed });
|
||||
wrapper.setData({ search });
|
||||
expect(wrapper.findAll(PublicGuildItem).length).to.equal(2);
|
||||
});
|
||||
});
|
||||
@@ -61,4 +61,172 @@ describe('Groups Utilities Mixin', () => {
|
||||
})).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterGuild', () => {
|
||||
let testGroup;
|
||||
let testGroup2;
|
||||
|
||||
before(() => {
|
||||
testGroup = {
|
||||
type: 'guild',
|
||||
_id: user.guilds[0],
|
||||
name: 'Crimson Vow',
|
||||
summary: 'testing',
|
||||
description: 'dummy 1',
|
||||
leader: user.guilds[0], // test user is not guild leader
|
||||
categories: [{
|
||||
_id: '123',
|
||||
slug: 'hobbies_occupations',
|
||||
name: 'hobbies_occupations',
|
||||
}],
|
||||
categorySlugs: ['hobbies_occupations'],
|
||||
memberCount: 1000,
|
||||
};
|
||||
testGroup2 = {
|
||||
type: 'guild',
|
||||
_id: '790',
|
||||
name: 'CAD Cads',
|
||||
summary: '3D',
|
||||
description: 'My dummy',
|
||||
leader: user._id, // test user is guild leader
|
||||
categories: [{
|
||||
_id: '123',
|
||||
slug: 'hobbies_occupations',
|
||||
name: 'hobbies_occupations',
|
||||
}],
|
||||
categorySlugs: ['hobbies_occupations'],
|
||||
memberCount: 100,
|
||||
};
|
||||
});
|
||||
|
||||
it('returns true with no filter and no search', () => {
|
||||
const filter = {};
|
||||
const search = '';
|
||||
expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns false with no filter and one search word not matching against any of the guild name, summary, and description', () => {
|
||||
const filter = {};
|
||||
const search = '3d';
|
||||
expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns true with no filter and one search word matched successfully against guild name', () => {
|
||||
const filter = {};
|
||||
const search = 'vow';
|
||||
expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns true with no filter and one search word matched successfully against guild summary', () => {
|
||||
const filter = {};
|
||||
const search = 'test';
|
||||
expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns true with no filter and one search word matched successfully against guild description', () => {
|
||||
const filter = {};
|
||||
const search = 'dum';
|
||||
expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns true with no filter and two search words with two spaces in between matched successfully against guild name', () => {
|
||||
const filter = {};
|
||||
const search = 'cad test';
|
||||
expect(instance.filterGuild(testGroup2, filter, search, user)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns true with no filter and two search words with two spaces in between matched successfully against guild summary', () => {
|
||||
const filter = {};
|
||||
const search = 'cad 3d';
|
||||
expect(instance.filterGuild(testGroup2, filter, search, user)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns true with no filter and two search words with two spaces in between matched successfully against guild description', () => {
|
||||
const filter = {};
|
||||
const search = 'my dummy';
|
||||
expect(instance.filterGuild(testGroup2, filter, search, user)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns false with no search word and one filter category that does not match against any guild categories', () => {
|
||||
const filter = {
|
||||
categories: ['academics'],
|
||||
};
|
||||
const search = '';
|
||||
expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns true with no search word and one filter category that matches successfully against any guild categories', () => {
|
||||
const filter = {
|
||||
categories: ['hobbies_occupations'],
|
||||
};
|
||||
const search = '';
|
||||
expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns false with no search word and one filter role that does not match against guild role', () => {
|
||||
const filter = {
|
||||
roles: ['guild_leader'],
|
||||
};
|
||||
const search = '';
|
||||
expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns true with no search word and one filter role that matches successfully against guild role', () => {
|
||||
const filter = {
|
||||
roles: ['member'],
|
||||
};
|
||||
const search = '';
|
||||
expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns true with no search word and filter size silver tier that matches against a guild size of 1000, the max guild size belonging to silver tier', () => {
|
||||
const filter = {
|
||||
guildSize: 'gold_tier',
|
||||
};
|
||||
const search = '';
|
||||
expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns true with no search word and filter size bronze tier that matches against a guild size of 100, the max guild size belonging to bronze tier', () => {
|
||||
const filter = {
|
||||
guildSize: 'silver_tier',
|
||||
};
|
||||
const search = '';
|
||||
expect(instance.filterGuild(testGroup2, filter, search, user)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns false with no search word and filter category that matches successfully against one guild category and filter role that does not match against guild role', () => {
|
||||
const filter = {
|
||||
categories: ['hobbies_occupations'],
|
||||
roles: ['guild_leader'],
|
||||
};
|
||||
const search = '';
|
||||
expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns true with no search word and filter category that matches successfully against one guild category and filter role that matches successfully against guild role', () => {
|
||||
const filter = {
|
||||
categories: ['hobbies_occupations'],
|
||||
roles: ['guild_leader'],
|
||||
};
|
||||
const search = '';
|
||||
expect(instance.filterGuild(testGroup2, filter, search, user)).to.equal(true);
|
||||
});
|
||||
|
||||
it('returns false with one search word that does not match against guild name and one filter category that matches successfully against guild categories', () => {
|
||||
const filter = {
|
||||
categories: ['hobbies_occupations'],
|
||||
};
|
||||
const search = 'konnichiwa';
|
||||
expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(false);
|
||||
});
|
||||
|
||||
it('returns true with one search word that matches against guild name and one filter role that matches successfully against guild role', () => {
|
||||
const filter = {
|
||||
categories: ['hobbies_occupations'],
|
||||
};
|
||||
const search = 'vow';
|
||||
expect(instance.filterGuild(testGroup, filter, search, user)).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"reachedLevel": "Dosáhl jsi úrovně <%= level %>",
|
||||
"achievementLostMasterclasser": "Dokončení výprav: Série Mistra třídy",
|
||||
"achievementLostMasterclasserText": "Splnil všech šestnáct výprav v sérii výprav Mistra třídy a vyřešil záhadu Ztraceného Mistra!",
|
||||
"achievementLostMasterclasserModalText": "Dokončil jsi všech 16 masterclass výprav a vyřešil jsi tajemství ztracené masterclass!",
|
||||
"achievementLostMasterclasserModalText": "Dokončil jsi všech šestnáct výprav Mistra třídy a vyřešil jsi tajemství Ztraceného Mistra!",
|
||||
"achievementMindOverMatter": "Mysl nad hmotou",
|
||||
"achievementMindOverMatterText": "Dokončil/a kamennou, slizovou a vlněnou mazlíčkovou výpravu.",
|
||||
"achievementMindOverMatterModalText": "Dokončil jsi kamennou, slizovou a vlněnou mazlíčkovou výpravu!",
|
||||
@@ -55,5 +55,12 @@
|
||||
"achievementCreatedTask": "Vytvořte úkol",
|
||||
"earnedAchievement": "Získali jste úspěch!",
|
||||
"viewAchievements": "Zobrazit úspěchy",
|
||||
"letsGetStarted": "Začněme!"
|
||||
"letsGetStarted": "Začněme!",
|
||||
"foundNewItemsCTA": "Podívej se do Tvého Inventáře a zkus zkombinovat Tvůj nový líhnoucí lektvar a vajíčko!",
|
||||
"foundNewItemsExplanation": "Splnění úkolů Ti dá šanci najít předměty, jako vajíčka, líhnoucí lektvary a jídlo.",
|
||||
"foundNewItems": "Našel jsi nové předměty!",
|
||||
"hideAchievements": "Schovat <%= kategorie %>",
|
||||
"onboardingCompleteDesc": "Získáš <strong>5 ocenění</strong> a <strong class=\"gold-amount\">100</strong> zlaťáků za dokončení seznamu.",
|
||||
"onboardingProgress": "<%= percentage %>% postup",
|
||||
"gettingStartedDesc": "Vytvoř si úkol, splň jej a pak se podívej na své odměny. Dostaneš <strong>5 ocenění</strong> a <strong class=“gold-amount”>100 zlaťáků</strong>, jakmile budeš hotový!"
|
||||
}
|
||||
|
||||
@@ -72,5 +72,6 @@
|
||||
"achievementTickledPink": "Rosige Bäckchen",
|
||||
"foundNewItems": "Du hast neue Gegenstände gefunden!",
|
||||
"foundNewItemsCTA": "Schau in Dein Inventar und versuche, Dein neues Schlüpfelixier mit einem Ei zu kombinieren!",
|
||||
"foundNewItemsExplanation": "Durch das Abschließen von Aufgaben erhältst Du die Chance Gegenstände, wie etwa Eier, Schlüpfelixiere und Futter, zu finden."
|
||||
"foundNewItemsExplanation": "Durch das Abschließen von Aufgaben erhältst Du die Chance Gegenstände, wie etwa Eier, Schlüpfelixiere und Futter, zu finden.",
|
||||
"achievementBugBonanza": "Kostbarer Käfer"
|
||||
}
|
||||
|
||||
@@ -681,7 +681,7 @@
|
||||
"questRubyCollectRubyGems": "Rubine",
|
||||
"questRubyCollectVenusRunes": "Venus-Runen",
|
||||
"questRubyCollectAquariusRunes": "Wassermann-Tierkreis-Runen",
|
||||
"questRubyText": "Die Rubinrote Lösung",
|
||||
"questRubyText": "Rubinrote Reaktion",
|
||||
"questRubyCompletion": "Nachdem die notwendigen Gegenstände sicher verstaut sind, eilen Sie drei zurück nach Habit City und treffen sich in @ beffymaroos Labor. \"Ausgezeichnete Arbeit!\" @beffymaroo sagt. \"Du hast die Zutaten für den Trank gesammelt!“ <br> <br> @beffymaroo kombiniert sorgfältig die Runen und Rubine zu einem leuchtend roten Trank und gießt einen Teil davon auf zwei Haustier-Eier. Wenn Sie die Ergebnisse beobachten, bemerken Sie, dass die beiden Haustiere völlig uninteressiert aneinander zu sein scheinen! <br> <br> \"Hat es nicht funktioniert?“ @Gully fragt. Aber bevor jemand antworten kann, merkt man plötzlich, dass es nicht der Trank ist, der Freundschaft und Liebe schafft, sondern die Erfahrung, gemeinsam auf ein gemeinsames Ziel hinzuarbeiten. Du kommst von der Suche weg, nachdem du neue Freunde gewonnen hast ... und einige auffällige neue Haustiere!",
|
||||
"questRubyNotes": "Die normalerweise geschäftigen Gipfel der Stoïkalm-Vulkane liegen still im Schnee. \"Ich nehme an, die Wanderer und Seher halten Winterschlaf?\" @gully sagt zu dir und @Aspiring_Advocate. \"Das erleichtert uns die Suche.\" <br> <br> Wenn Sie den Gipfel erreichen, verschmilzt der kühle Wind mit dem Dampf, der aus dem Krater aufsteigt. \"Dort!\" @Aspiring_Advocate ruft aus und zeigt auf eine heiße Quelle. \"Welchen besseren Ort gibt es, um kühle Runen des Wassermanns und leidenschaftliche Runen der Venus zu finden, als wo sich Eis und Feuer treffen?“ <br> <br> Sie drei beeilen sich in Richtung der heißen Quelle. \"Laut meiner Forschung\", sagt @Aspiring_Advocate, \"wird die Kombination der Runen mit herzförmigen Rubinen einen Schlupftrank erzeugen, der Freundschaft und Liebe fördern kann!\" <br> <br> Aufgeregt von der Aussicht auf eine neue Entdeckung, Sie alle Lächeln. \"In Ordnung\", sagt @gully, \"fangen wir an zu suchen!\""
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@
|
||||
"goToSettings": "Gehe zu Einstellungen",
|
||||
"usernameVerifiedConfirmation": "Dein Benutzername, <%= username %>, ist bestätigt!",
|
||||
"usernameNotVerified": "Bitte bestätige Deinen Benutzernamen.",
|
||||
"changeUsernameDisclaimer": "Dieser Benutzername wird für Einladungen, @Erwähnungen im Chat und Nachrichten verwendet werden.",
|
||||
"changeUsernameDisclaimer": "Dein Benutzername wird für Einladungen, @Erwähnungen im Chat und Nachrichten verwendet. Er muss zwischen 1 und 20 Zeichen haben, darf nur Buchstaben von a bis z, Ziffern von 0 bis 9, Bindestriche oder Unterstriche beinhalten und darf keine unpassenden Ausdrücke enthalten.",
|
||||
"verifyUsernameVeteranPet": "Eines dieser Veteranen-Haustiere wartet auf Dich wenn Du die Bestätigung abgeschlossen hast!",
|
||||
"subscriptionReminders": "Abonnement-Erinnerung",
|
||||
"newPMNotificationTitle": "Neue Nachricht von <%= name %>",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"showAllAchievements": "Show All <%= category %>",
|
||||
"hideAchievements": "Hide <%= category %>",
|
||||
"foundNewItems": "You found new items!",
|
||||
"foundNewItemsExplanation": "Completing tasks gives you a chance to find items, like eggs, hatching potions, and food.",
|
||||
"foundNewItemsExplanation": "Completing tasks gives you a chance to find items, like Eggs, Hatching Potions, and Pet Food.",
|
||||
"foundNewItemsCTA": "Head to your Inventory and try combining your new hatching potion and egg!",
|
||||
"achievementLostMasterclasser": "Quest Completionist: Masterclasser Series",
|
||||
"achievementLostMasterclasserText": "Completed all sixteen quests in the Masterclasser Quest Series and solved the mystery of the Lost Masterclasser!",
|
||||
|
||||
@@ -298,6 +298,7 @@
|
||||
"hatchingPotionAmber": "Amber",
|
||||
"hatchingPotionAurora": "Aurora",
|
||||
"hatchingPotionRuby": "Ruby",
|
||||
"hatchingPotionBirchBark": "Birch Bark",
|
||||
|
||||
"hatchingPotionNotes": "Pour this on an egg, and it will hatch as a <%= potText(locale) %> pet.",
|
||||
"premiumPotionAddlNotes": "Not usable on quest pet eggs. Available for purchase until <%= date(locale) %>.",
|
||||
|
||||
@@ -350,6 +350,15 @@
|
||||
"weaponSpecialWinter2020HealerText": "Clove Scepter",
|
||||
"weaponSpecialWinter2020HealerNotes": "Wave it about, and its aroma will summon your friends and helpers to begin cooking and baking! Increases Intelligence by <%= int %>. Limited Edition 2019-2020 Winter Gear.",
|
||||
|
||||
"weaponSpecialSpring2020RogueText": "Lazurite Blade",
|
||||
"weaponSpecialSpring2020RogueNotes": "You'll strike so fast it'll look even MORE blue! Increases Strength by <%= str %>. Limited Edition 2020 Spring Gear.",
|
||||
"weaponSpecialSpring2020WarriorText": "Sharpened Wing",
|
||||
"weaponSpecialSpring2020WarriorNotes": "Fight or flight, this wing will serve you well! Increases Strength by <%= str %>. Limited Edition 2020 Spring Gear.",
|
||||
"weaponSpecialSpring2020MageText": "Raindrops",
|
||||
"weaponSpecialSpring2020MageNotes": "They keep falling on your head! But you'll never stop them by complaining. Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition 2020 Spring Gear.",
|
||||
"weaponSpecialSpring2020HealerText": "Sword-Lily Staff",
|
||||
"weaponSpecialSpring2020HealerNotes": "An iris is beautiful, but the leaves are like swords... don't be deceived by the flowers, this staff is tough as steel! Increases Intelligence by <%= int %>. Limited Edition 2020 Spring Gear.",
|
||||
|
||||
"weaponMystery201411Text": "Pitchfork of Feasting",
|
||||
"weaponMystery201411Notes": "Stab your enemies or dig in to your favorite foods - this versatile pitchfork does it all! Confers no benefit. November 2014 Subscriber Item.",
|
||||
"weaponMystery201502Text": "Shimmery Winged Staff of Love and Also Truth",
|
||||
@@ -809,6 +818,15 @@
|
||||
"armorSpecialWinter2020HealerText": "Orange Peel Gown",
|
||||
"armorSpecialWinter2020HealerNotes": "An opulent gown for those with festive zest! Increases Constitution by <%= con %>. Limited Edition 2019-2020 Winter Gear.",
|
||||
|
||||
"armorSpecialSpring2020RogueText": "Ultramarine Armor",
|
||||
"armorSpecialSpring2020RogueNotes": "The color of twilight, of a multitude of precious stones, of the deepest sea! Increases Perception by <%= per %>. Limited Edition 2020 Spring Gear.",
|
||||
"armorSpecialSpring2020WarriorText": "Exoskeleton Armor",
|
||||
"armorSpecialSpring2020WarriorNotes": "This rigid carapace can keep you safe from even the most crushing attacks. Increases Constitution by <%= con %>. Limited Edition 2020 Spring Gear.",
|
||||
"armorSpecialSpring2020MageText": "Whirlpuddle Gown",
|
||||
"armorSpecialSpring2020MageNotes": "If you can't resist stomping through the leavings of rainstorms, this armor is for you! Turn a childish impulse into a display of mystic artistry. Increases Intelligence by <%= int %>. Limited Edition 2020 Spring Gear.",
|
||||
"armorSpecialSpring2020HealerText": "Protective Petals",
|
||||
"armorSpecialSpring2020HealerNotes": "Wrap yourself in soft iris leaves and petals to fool enemies into underestimating your healing power. Increases Constitution by <%= con %>. Limited Edition 2020 Spring Gear.",
|
||||
|
||||
"armorMystery201402Text": "Messenger Robes",
|
||||
"armorMystery201402Notes": "Shimmering and strong, these robes have many pockets to carry letters. Confers no benefit. February 2014 Subscriber Item.",
|
||||
"armorMystery201403Text": "Forest Walker Armor",
|
||||
@@ -1361,6 +1379,15 @@
|
||||
"headSpecialWinter2020HealerText": "Star Anise Emblem",
|
||||
"headSpecialWinter2020HealerNotes": "Please remove it from your head before attempting to brew chai or coffee with it. Increases Intelligence by <%= int %>. Limited Edition 2019-2020 Winter Gear.",
|
||||
|
||||
"headSpecialSpring2020RogueText": "Lapis Kabuto",
|
||||
"headSpecialSpring2020RogueNotes": "So vibrant and valuable, you'll be tempted to steal it off your own head. Increases Perception by <%= per %>. Limited Edition 2020 Spring Gear.",
|
||||
"headSpecialSpring2020WarriorText": "Beetle Helm",
|
||||
"headSpecialSpring2020WarriorNotes": "Your enemies' blows will glance off this beetle-inspired helm! Increases Strength by <%= str %>. Limited Edition 2020 Spring Gear.",
|
||||
"headSpecialSpring2020MageText": "Drip Top Cap",
|
||||
"headSpecialSpring2020MageNotes": "Is the sky clear? Humidity low? Don't worry, we've got you. Moisten your magic without dampening your spirits! Increases Perception by <%= per %>. Limited Edition 2020 Spring Gear.",
|
||||
"headSpecialSpring2020HealerText": "Iris Fascinator",
|
||||
"headSpecialSpring2020HealerNotes": "Beguile your foes with this headpiece made of flowers! Increases Intelligence by <%= int %>. Limited Edition 2020 Spring Gear.",
|
||||
|
||||
"headSpecialGaymerxText": "Rainbow Warrior Helm",
|
||||
"headSpecialGaymerxNotes": "In celebration of the GaymerX Conference, this special helmet is decorated with a radiant, colorful rainbow pattern! GaymerX is a game convention celebrating LGTBQ and gaming and is open to everyone.",
|
||||
|
||||
@@ -1814,6 +1841,11 @@
|
||||
"shieldSpecialWinter2020HealerText": "Giant Cinnamon Stick",
|
||||
"shieldSpecialWinter2020HealerNotes": "Do you feel you are too good for this world, too pure? Only this beauty of a spice will do. Increases Constitution by <%= con %>. Limited Edition 2019-2020 Winter Gear.",
|
||||
|
||||
"shieldSpecialSpring2020WarriorText": "Iridescent Shield",
|
||||
"shieldSpecialSpring2020WarriorNotes": "Don't let the delicate colors fool you. This shield has got you covered! Increases Constitution by <%= con %>. Limited Edition 2020 Spring Gear.",
|
||||
"shieldSpecialSpring2020HealerText": "Perfumed Shield",
|
||||
"shieldSpecialSpring2020HealerNotes": "Ward off those musty old To-Dos with this sweet-smelling shield. Increases Constitution by <%= con %>. Limited Edition 2020 Spring Gear.",
|
||||
|
||||
"shieldMystery201601Text": "Resolution Slayer",
|
||||
"shieldMystery201601Notes": "This blade can be used to parry away all distractions. Confers no benefit. January 2016 Subscriber Item.",
|
||||
"shieldMystery201701Text": "Time-Freezer Shield",
|
||||
@@ -1952,6 +1984,8 @@
|
||||
"backMystery201912Notes": "Glide silently across sparkling snowfields and shimmering mountains with these icy wings. Confers no benefit. December 2019 Subscriber Item.",
|
||||
"backMystery202001Text": "Five Tails of Fable",
|
||||
"backMystery202001Notes": "These fluffy tails contain celestial power, and also a high level of cuteness! Confers no benefit. January 2020 Subscriber Item.",
|
||||
"backMystery202004Text": "Mighty Monarch Wings",
|
||||
"backMystery202004Notes": "Make a quick flutter to the nearest flowery meadow or migrate across the continent with these beautiful wings! Confers no benefit. April 2020 Subscriber Item.",
|
||||
|
||||
"backSpecialWonderconRedText": "Mighty Cape",
|
||||
"backSpecialWonderconRedNotes": "Swishes with strength and beauty. Confers no benefit. Special Edition Convention Item.",
|
||||
@@ -2130,6 +2164,8 @@
|
||||
"headAccessoryMystery201906Notes": "Legend has it these finny ears help merfolk hear the calls and songs of all the denizens of the deep! Confers no benefit. June 2019 Subscriber Item.",
|
||||
"headAccessoryMystery201908Text": "Footloose Faun Horns",
|
||||
"headAccessoryMystery201908Notes": "If wearing horns floats your goat, you're in luck! Confers no benefit. August 2019 Subscriber Item.",
|
||||
"headAccessoryMystery202004Text": "Mighty Monarch Antennae",
|
||||
"headAccessoryMystery202004Notes": "They twitch just a bit if the scent of flowers drifts by--use them to find a pretty garden! Confers no benefit. April 2020 Subscriber Item.",
|
||||
"headAccessoryMystery301405Text": "Headwear Goggles",
|
||||
"headAccessoryMystery301405Notes": "\"Goggles are for your eyes,\" they said. \"Nobody wants goggles that you can only wear on your head,\" they said. Hah! You sure showed them! Confers no benefit. August 3015 Subscriber Item.",
|
||||
|
||||
|
||||
@@ -85,45 +85,45 @@
|
||||
"scarecrowWarriorSet": "Scarecrow Warrior (Warrior)",
|
||||
"stitchWitchSet": "Stitch Witch (Mage)",
|
||||
"potionerSet": "Potioner (Healer)",
|
||||
"battleRogueSet": "Bat-tle Rogue (Rogue)",
|
||||
"battleRogueSet": "Bat-tle (Rogue)",
|
||||
"springingBunnySet": "Springing Bunny (Healer)",
|
||||
"grandMalkinSet": "Grand Malkin (Mage)",
|
||||
"cleverDogSet": "Clever Dog (Rogue)",
|
||||
"braveMouseSet": "Brave Mouse (Warrior)",
|
||||
"summer2016SharkWarriorSet": "Shark Warrior (Warrior)",
|
||||
"summer2016DolphinMageSet": "Dolphin Mage (Mage)",
|
||||
"summer2016SeahorseHealerSet": "Seahorse Healer (Healer)",
|
||||
"summer2016EelSet": "Eel Rogue (Rogue)",
|
||||
"summer2016SharkWarriorSet": "Shark (Warrior)",
|
||||
"summer2016DolphinMageSet": "Dolphin (Mage)",
|
||||
"summer2016SeahorseHealerSet": "Seahorse (Healer)",
|
||||
"summer2016EelSet": "Eel (Rogue)",
|
||||
"fall2016SwampThingSet": "Swamp Thing (Warrior)",
|
||||
"fall2016WickedSorcererSet": "Wicked Sorcerer (Mage)",
|
||||
"fall2016GorgonHealerSet": "Gorgon Healer (Healer)",
|
||||
"fall2016BlackWidowSet": "Black Widow Rogue (Rogue)",
|
||||
"fall2016GorgonHealerSet": "Gorgon (Healer)",
|
||||
"fall2016BlackWidowSet": "Black Widow (Rogue)",
|
||||
"winter2017IceHockeySet": "Ice Hockey (Warrior)",
|
||||
"winter2017WinterWolfSet": "Winter Wolf (Mage)",
|
||||
"winter2017SugarPlumSet": "Sugar Plum Healer (Healer)",
|
||||
"winter2017FrostyRogueSet": "Frosty Rogue (Rogue)",
|
||||
"spring2017FelineWarriorSet": "Feline Warrior (Warrior)",
|
||||
"winter2017SugarPlumSet": "Sugar Plum (Healer)",
|
||||
"winter2017FrostyRogueSet": "Frosty (Rogue)",
|
||||
"spring2017FelineWarriorSet": "Feline (Warrior)",
|
||||
"spring2017CanineConjurorSet": "Canine Conjuror (Mage)",
|
||||
"spring2017FloralMouseSet": "Floral Mouse (Healer)",
|
||||
"spring2017SneakyBunnySet": "Sneaky Bunny (Rogue)",
|
||||
"summer2017SandcastleWarriorSet": "Sandcastle Warrior (Warrior)",
|
||||
"summer2017WhirlpoolMageSet": "Whirlpool Mage (Mage)",
|
||||
"summer2017SandcastleWarriorSet": "Sandcastle (Warrior)",
|
||||
"summer2017WhirlpoolMageSet": "Whirlpool (Mage)",
|
||||
"summer2017SeashellSeahealerSet": "Seashell Seahealer (Healer)",
|
||||
"summer2017SeaDragonSet": "Sea Dragon (Rogue)",
|
||||
"fall2017HabitoweenSet": "Habitoween Warrior (Warrior)",
|
||||
"fall2017MasqueradeSet": "Masquerade Mage (Mage)",
|
||||
"fall2017HauntedHouseSet": "Haunted House Healer (Healer)",
|
||||
"fall2017TrickOrTreatSet": "Trick or Treat Rogue (Rogue)",
|
||||
"winter2018ConfettiSet": "Confetti Mage (Mage)",
|
||||
"winter2018GiftWrappedSet": "Gift-Wrapped Warrior (Warrior)",
|
||||
"winter2018MistletoeSet": "Mistletoe Healer (Healer)",
|
||||
"winter2018ReindeerSet": "Reindeer Rogue (Rogue)",
|
||||
"spring2018SunriseWarriorSet": "Sunrise Warrior (Warrior)",
|
||||
"spring2018TulipMageSet": "Tulip Mage (Mage)",
|
||||
"spring2018GarnetHealerSet": "Garnet Healer (Healer)",
|
||||
"spring2018DucklingRogueSet": "Duckling Rogue (Rogue)",
|
||||
"summer2018BettaFishWarriorSet": "Betta Fish Warrior (Warrior)",
|
||||
"summer2018LionfishMageSet": "Lionfish Mage (Mage)",
|
||||
"fall2017HabitoweenSet": "Habitoween (Warrior)",
|
||||
"fall2017MasqueradeSet": "Masquerade (Mage)",
|
||||
"fall2017HauntedHouseSet": "Haunted House (Healer)",
|
||||
"fall2017TrickOrTreatSet": "Trick or Treat (Rogue)",
|
||||
"winter2018ConfettiSet": "Confetti (Mage)",
|
||||
"winter2018GiftWrappedSet": "Gift-Wrapped (Warrior)",
|
||||
"winter2018MistletoeSet": "Mistletoe (Healer)",
|
||||
"winter2018ReindeerSet": "Reindeer (Rogue)",
|
||||
"spring2018SunriseWarriorSet": "Sunrise (Warrior)",
|
||||
"spring2018TulipMageSet": "Tulip (Mage)",
|
||||
"spring2018GarnetHealerSet": "Garnet (Healer)",
|
||||
"spring2018DucklingRogueSet": "Duckling (Rogue)",
|
||||
"summer2018BettaFishWarriorSet": "Betta Fish (Warrior)",
|
||||
"summer2018LionfishMageSet": "Lionfish (Mage)",
|
||||
"summer2018MerfolkMonarchSet": "Merfolk Monarch (Healer)",
|
||||
"summer2018FisherRogueSet": "Fisher-Rogue (Rogue)",
|
||||
"fall2018MinotaurWarriorSet": "Minotaur (Warrior)",
|
||||
@@ -151,6 +151,10 @@
|
||||
"winter2020CarolOfTheMageSet": "Carol of the Mage (Mage)",
|
||||
"winter2020WinterSpiceSet": "Winter Spice (Healer)",
|
||||
"winter2020LanternSet": "Lantern (Rogue)",
|
||||
"spring2020BeetleWarriorSet": "Rhinoceros Beetle (Warrior)",
|
||||
"spring2020PuddleMageSet": "Puddle (Mage)",
|
||||
"spring2020IrisHealerSet": "Iris (Healer)",
|
||||
"spring2020LapisLazuliRogueSet": "Lapis Lazuli (Rogue)",
|
||||
"eventAvailability": "Available for purchase until <%= date(locale) %>.",
|
||||
"eventAvailabilityReturning": "Available for purchase until <%= availableDate(locale) %>. This potion was last available in <%= previousDate(locale) %>.",
|
||||
"dateEndMarch": "April 30",
|
||||
@@ -159,6 +163,7 @@
|
||||
"dateEndJune": "June 14",
|
||||
"augustYYYY": "August <%= year %>",
|
||||
"decemberYYYY": "December <%= year %>",
|
||||
"marchYYYY": "March <%= year %>",
|
||||
"dateEndJuly": "July 31",
|
||||
"dateEndAugust": "August 31",
|
||||
"dateEndSeptember": "September 21",
|
||||
|
||||
@@ -271,7 +271,7 @@
|
||||
"questGroupDilatoryDistress": "Dilatory Distress",
|
||||
"questDilatoryDistress1Text": "Dilatory Distress, Part 1: Message in a Bottle",
|
||||
"questDilatoryDistress1Notes": "A message in a bottle arrived from the newly rebuilt city of Dilatory! It reads: \"Dear Habiticans, we need your help once again. Our princess has disappeared and the city is under siege by some unknown watery demons! The mantis shrimps are holding the attackers at bay. Please aid us!\" To make the long journey to the sunken city, one must be able to breathe water. Fortunately, the alchemists @Benga and @hazel can make it all possible! You only have to find the proper ingredients.",
|
||||
"questDilatoryDistress1Completion": "You don the the finned armor and swim to Dilatory as quickly as you can. The merfolk and their mantis shrimp allies have managed to keep the monsters outside the city for the moment, but they are losing. No sooner are you within the castle walls than the horrifying siege descends!",
|
||||
"questDilatoryDistress1Completion": "You don the finned armor and swim to Dilatory as quickly as you can. The merfolk and their mantis shrimp allies have managed to keep the monsters outside the city for the moment, but they are losing. No sooner are you within the castle walls than the horrifying siege descends!",
|
||||
"questDilatoryDistress1CollectFireCoral": "Fire Coral",
|
||||
"questDilatoryDistress1CollectBlueFins": "Blue Fins",
|
||||
"questDilatoryDistress1DropArmor": "Finned Oceanic Armor (Armor)",
|
||||
@@ -661,7 +661,7 @@
|
||||
|
||||
"questBadgerText": "Stop Badgering Me!",
|
||||
"questBadgerNotes": "Ah, winter in the Taskwoods. The softly falling snow, the branches sparkling with frost, the Flourishing Fairies… still not snoozing?<br><br>“Why are they still awake?” cries @LilithofAlfheim. “If they don't hibernate soon, they'll never have the energy for planting season.”<br><br>As you and @Willow the Witty hurry to investigate, a furry head pops up from the ground. Before you can yell, “It’s the Badgering Bother!” it’s back in its burrow—but not before snatching up the Fairies' “Hibernate” To-Dos and dropping a giant list of pesky tasks in their place!<br><br>“No wonder the fairies aren't resting, if they're constantly being badgered like that!” @plumilla says. Can you chase off this beast and save the Taskwood’s harvest this year?",
|
||||
"questBadgerCompletion": "You finally drive away the the Badgering Bother and hurry into its burrow. At the end of a tunnel, you find its hoard of the faeries’ “Hibernate” To-Dos. The den is otherwise abandoned, except for three eggs that look ready to hatch.",
|
||||
"questBadgerCompletion": "You finally drive away the Badgering Bother and hurry into its burrow. At the end of a tunnel, you find its hoard of the faeries’ “Hibernate” To-Dos. The den is otherwise abandoned, except for three eggs that look ready to hatch.",
|
||||
"questBadgerBoss": "The Badgering Bother",
|
||||
"questBadgerDropBadgerEgg": "Badger (Egg)",
|
||||
"questBadgerUnlockText": "Unlocks Badger Eggs for purchase in the Market",
|
||||
|
||||
@@ -172,6 +172,7 @@
|
||||
"mysterySet202001": "Fabled Fox Set",
|
||||
"mysterySet202002": "Stylish Sweetheart Set",
|
||||
"mysterySet202003": "Barbed Battler Set",
|
||||
"mysterySet202004": "Mighty Monarch Set",
|
||||
"mysterySet301404": "Steampunk Standard Set",
|
||||
"mysterySet301405": "Steampunk Accessories Set",
|
||||
"mysterySet301703": "Peacock Steampunk Set",
|
||||
|
||||
@@ -69,5 +69,11 @@
|
||||
"achievementRosyOutlook": "Rosy Lookout",
|
||||
"achievementTickledPinkModalText": "Ye've collected all th' Cotton Candy Pink Critters!",
|
||||
"achievementTickledPinkText": "'as collected all Cotton Candy Pink Critters.",
|
||||
"achievementTickledPink": "Pickled Pink"
|
||||
"achievementTickledPink": "Pickled Pink",
|
||||
"foundNewItemsCTA": "Head t' yer inventory an' try combinin' yer new 'atchin' potion an' egg!",
|
||||
"foundNewItemsExplanation": "Completin' tasks gives ye a chance ta find new items, like eggs, 'atchin' potions, an' vittles.",
|
||||
"foundNewItems": "Ye found somethin' new!",
|
||||
"achievementBugBonanzaModalText": "Ye've kermpleted th' Beetle, Butterfly, Snail, an' Spidey pet quests!",
|
||||
"achievementBugBonanzaText": "'as kermpleted Beetle, Butterfly, Snail, an' Spidey pet quests.",
|
||||
"achievementBugBonanza": "Crawly Catcher"
|
||||
}
|
||||
|
||||
@@ -1813,5 +1813,7 @@
|
||||
"weaponSpecialWinter2020MageNotes": "Wiv practice, ye kin perject this aural magic (in waves! Like th' sea!!) any way ye like: a thoughtful hum, a festive chime, er a RED TASK O'ERBOARD ALARM. Raises yer Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition 2019-2020 Winter Gear.",
|
||||
"weaponSpecialWinter2020MageText": "Ripplin' Waves o' Sound",
|
||||
"weaponSpecialWinter2020WarriorNotes": "Avast, squirrels! Ye'll get no piece o' this! ...But iffen ye wanna hang out an' 'ave cocoa, that be cool. Raises yer Strength by <%= str %>. Limited Edition 2019-2020 Winter Gear.",
|
||||
"weaponSpecialWinter2020WarriorText": "Pointy Conny-fer Cone"
|
||||
"weaponSpecialWinter2020WarriorText": "Pointy Conny-fer Cone",
|
||||
"weaponSpecialSpring2020RogueNotes": "Ye'll strike so fast it'll look e'en MORE blue! Raises yer Strength by <%= str %>. Limited Edition 2020 Spring Gear.",
|
||||
"weaponSpecialSpring2020RogueText": "Laz-yer-rite Blade"
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@
|
||||
"achievementTickledPinkText": "Has collected all Cotton Candy Pink Pets.",
|
||||
"achievementTickledPink": "Tickled Pink",
|
||||
"foundNewItemsCTA": "Head to your Inventory and try combining your new hatching potion and egg!",
|
||||
"foundNewItemsExplanation": "Completing tasks gives you a chance to find items, like eggs, hatching potions, and food.",
|
||||
"foundNewItems": "You found new items!"
|
||||
"foundNewItemsExplanation": "Completing tasks gives you a chance to find items, like Eggs, Hatching Potions, and Pet Food.",
|
||||
"foundNewItems": "You found new items!",
|
||||
"achievementBugBonanzaModalText": "You completed the Beetle, Butterfly, Snail, and Spider pet quests!",
|
||||
"achievementBugBonanzaText": "Has completed Beetle, Butterfly, Snail, and Spider pet quests.",
|
||||
"achievementBugBonanza": "Bug Bonanza"
|
||||
}
|
||||
|
||||
@@ -354,5 +354,6 @@
|
||||
"questEggDolphinAdjective": "a chipper",
|
||||
"questEggDolphinMountText": "Dolphin",
|
||||
"questEggDolphinText": "Dolphin",
|
||||
"hatchingPotionRuby": "Ruby"
|
||||
"hatchingPotionRuby": "Ruby",
|
||||
"hatchingPotionBirchBark": "Birch Bark"
|
||||
}
|
||||
|
||||
@@ -2051,5 +2051,33 @@
|
||||
"armorArmoireBaseballUniformNotes": "Pinstripes never go out of style. Increases Constitution and Strength by <%= attrs %> each. Enchanted Armoire: Baseball Set (Item 2 of 4).",
|
||||
"armorArmoireBaseballUniformText": "Baseball Uniform",
|
||||
"weaponArmoireBaseballBatNotes": "Get a home run on those good habits! Increases Constitution by <%= con %>. Enchanted Armoire: Baseball Set (Item 3 of 4).",
|
||||
"weaponArmoireBaseballBatText": "Baseball Bat"
|
||||
"weaponArmoireBaseballBatText": "Baseball Bat",
|
||||
"shieldSpecialSpring2020HealerNotes": "Ward off those musty old To-Dos with this sweet-smelling shield. Increases Constitution by <%= con %>. Limited Edition 2020 Spring Gear.",
|
||||
"shieldSpecialSpring2020HealerText": "Perfumed Shield",
|
||||
"shieldSpecialSpring2020WarriorNotes": "Don't let the delicate colors fool you. This shield has got you covered! Increases Constitution by <%= con %>. Limited Edition 2020 Spring Gear.",
|
||||
"shieldSpecialSpring2020WarriorText": "Iridescent Shield",
|
||||
"headSpecialSpring2020HealerNotes": "Beguile your foes with this headpiece made of flowers! Increases Intelligence by <%= int %>. Limited Edition 2020 Spring Gear.",
|
||||
"headSpecialSpring2020HealerText": "Iris Fascinator",
|
||||
"headSpecialSpring2020MageNotes": "Is the sky clear? Humidity low? Don't worry, we've got you. Moisten your magic without dampening your spirits! Increases Perception by <%= per %>. Limited Edition 2020 Spring Gear.",
|
||||
"headSpecialSpring2020MageText": "Drip Top Cap",
|
||||
"headSpecialSpring2020WarriorNotes": "Your enemies' blows will glance off this beetle-inspired helm! Increases Strength by <%= str %>. Limited Edition 2020 Spring Gear.",
|
||||
"headSpecialSpring2020WarriorText": "Beetle Helm",
|
||||
"headSpecialSpring2020RogueNotes": "So vibrant and valuable, you'll be tempted to steal it off your own head. Increases Perception by <%= per %>. Limited Edition 2020 Spring Gear.",
|
||||
"headSpecialSpring2020RogueText": "Lapis Kabuto",
|
||||
"armorSpecialSpring2020HealerNotes": "Wrap yourself in soft iris leaves and petals to fool enemies into underestimating your healing power. Increases Constitution by <%= con %>. Limited Edition 2020 Spring Gear.",
|
||||
"armorSpecialSpring2020HealerText": "Protective Petals",
|
||||
"armorSpecialSpring2020MageNotes": "If you can't resist stomping through the leavings of rainstorms, this armour is for you! Turn a childish impulse into a display of mystic artistry. Increases Intelligence by <%= int %>. Limited Edition 2020 Spring Gear.",
|
||||
"armorSpecialSpring2020MageText": "Whirlpuddle Gown",
|
||||
"armorSpecialSpring2020WarriorNotes": "This rigid carapace can keep you safe from even the most crushing attacks. Increases Constitution by <%= con %>. Limited Edition 2020 Spring Gear.",
|
||||
"armorSpecialSpring2020WarriorText": "Exoskeleton Armour",
|
||||
"armorSpecialSpring2020RogueNotes": "The colour of twilight, of a multitude of precious stones, of the deepest sea! Increases Perception by <%= per %>. Limited Edition 2020 Spring Gear.",
|
||||
"armorSpecialSpring2020RogueText": "Ultramarine Armour",
|
||||
"weaponSpecialSpring2020HealerNotes": "An iris is beautiful, but the leaves are like swords... don't be deceived by the flowers, this staff is tough as steel! Increases Intelligence by <%= int %>. Limited Edition 2020 Spring Gear.",
|
||||
"weaponSpecialSpring2020HealerText": "Sword-Lily Staff",
|
||||
"weaponSpecialSpring2020MageNotes": "They keep falling on your head! But you'll never stop them by complaining. Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition 2020 Spring Gear.",
|
||||
"weaponSpecialSpring2020MageText": "Raindrops",
|
||||
"weaponSpecialSpring2020WarriorNotes": "Fight or flight, this wing will serve you well! Increases Strength by <%= str %>. Limited Edition 2020 Spring Gear.",
|
||||
"weaponSpecialSpring2020WarriorText": "Sharpened Wing",
|
||||
"weaponSpecialSpring2020RogueNotes": "You'll strike so fast it'll look even MORE blue! Increases Strength by <%= str %>. Limited Edition 2020 Spring Gear.",
|
||||
"weaponSpecialSpring2020RogueText": "Lazurite Blade"
|
||||
}
|
||||
|
||||
@@ -85,45 +85,45 @@
|
||||
"scarecrowWarriorSet": "Scarecrow Warrior (Warrior)",
|
||||
"stitchWitchSet": "Stitch Witch (Mage)",
|
||||
"potionerSet": "Potioner (Healer)",
|
||||
"battleRogueSet": "Bat-tle Rogue (Rogue)",
|
||||
"battleRogueSet": "Bat-tle (Rogue)",
|
||||
"springingBunnySet": "Springing Bunny (Healer)",
|
||||
"grandMalkinSet": "Grand Malkin (Mage)",
|
||||
"cleverDogSet": "Clever Dog (Rogue)",
|
||||
"braveMouseSet": "Brave Mouse (Warrior)",
|
||||
"summer2016SharkWarriorSet": "Shark Warrior (Warrior)",
|
||||
"summer2016DolphinMageSet": "Dolphin Mage (Mage)",
|
||||
"summer2016SeahorseHealerSet": "Seahorse Healer (Healer)",
|
||||
"summer2016EelSet": "Eel Rogue (Rogue)",
|
||||
"summer2016SharkWarriorSet": "Shark (Warrior)",
|
||||
"summer2016DolphinMageSet": "Dolphin (Mage)",
|
||||
"summer2016SeahorseHealerSet": "Seahorse (Healer)",
|
||||
"summer2016EelSet": "Eel (Rogue)",
|
||||
"fall2016SwampThingSet": "Swamp Thing (Warrior)",
|
||||
"fall2016WickedSorcererSet": "Wicked Sorcerer (Mage)",
|
||||
"fall2016GorgonHealerSet": "Gorgon Healer (Healer)",
|
||||
"fall2016BlackWidowSet": "Black Widow Rogue (Rogue)",
|
||||
"fall2016GorgonHealerSet": "Gorgon (Healer)",
|
||||
"fall2016BlackWidowSet": "Black Widow (Rogue)",
|
||||
"winter2017IceHockeySet": "Ice Hockey (Warrior)",
|
||||
"winter2017WinterWolfSet": "Winter Wolf (Mage)",
|
||||
"winter2017SugarPlumSet": "Sugar Plum Healer (Healer)",
|
||||
"winter2017FrostyRogueSet": "Frosty Rogue (Rogue)",
|
||||
"spring2017FelineWarriorSet": "Feline Warrior (Warrior)",
|
||||
"winter2017SugarPlumSet": "Sugar Plum (Healer)",
|
||||
"winter2017FrostyRogueSet": "Frosty (Rogue)",
|
||||
"spring2017FelineWarriorSet": "Feline (Warrior)",
|
||||
"spring2017CanineConjurorSet": "Canine Conjuror (Mage)",
|
||||
"spring2017FloralMouseSet": "Floral Mouse (Healer)",
|
||||
"spring2017SneakyBunnySet": "Sneaky Bunny (Rogue)",
|
||||
"summer2017SandcastleWarriorSet": "Sandcastle Warrior (Warrior)",
|
||||
"summer2017WhirlpoolMageSet": "Whirlpool Mage (Mage)",
|
||||
"summer2017SandcastleWarriorSet": "Sandcastle (Warrior)",
|
||||
"summer2017WhirlpoolMageSet": "Whirlpool (Mage)",
|
||||
"summer2017SeashellSeahealerSet": "Seashell Seahealer (Healer)",
|
||||
"summer2017SeaDragonSet": "Sea Dragon (Rogue)",
|
||||
"fall2017HabitoweenSet": "Habitoween Warrior (Warrior)",
|
||||
"fall2017MasqueradeSet": "Masquerade Mage (Mage)",
|
||||
"fall2017HauntedHouseSet": "Haunted House Healer (Healer)",
|
||||
"fall2017TrickOrTreatSet": "Trick or Treat Rogue (Rogue)",
|
||||
"winter2018ConfettiSet": "Confetti Mage (Mage)",
|
||||
"winter2018GiftWrappedSet": "Gift-Wrapped Warrior (Warrior)",
|
||||
"winter2018MistletoeSet": "Mistletoe Healer (Healer)",
|
||||
"winter2018ReindeerSet": "Reindeer Rogue (Rogue)",
|
||||
"spring2018SunriseWarriorSet": "Sunrise Warrior (Warrior)",
|
||||
"spring2018TulipMageSet": "Tulip Mage (Mage)",
|
||||
"spring2018GarnetHealerSet": "Garnet Healer (Healer)",
|
||||
"spring2018DucklingRogueSet": "Duckling Rogue (Rogue)",
|
||||
"summer2018BettaFishWarriorSet": "Betta Fish Warrior (Warrior)",
|
||||
"summer2018LionfishMageSet": "Lionfish Mage (Mage)",
|
||||
"fall2017HabitoweenSet": "Habitoween (Warrior)",
|
||||
"fall2017MasqueradeSet": "Masquerade (Mage)",
|
||||
"fall2017HauntedHouseSet": "Haunted House (Healer)",
|
||||
"fall2017TrickOrTreatSet": "Trick or Treat (Rogue)",
|
||||
"winter2018ConfettiSet": "Confetti (Mage)",
|
||||
"winter2018GiftWrappedSet": "Gift-Wrapped (Warrior)",
|
||||
"winter2018MistletoeSet": "Mistletoe (Healer)",
|
||||
"winter2018ReindeerSet": "Reindeer (Rogue)",
|
||||
"spring2018SunriseWarriorSet": "Sunrise (Warrior)",
|
||||
"spring2018TulipMageSet": "Tulip (Mage)",
|
||||
"spring2018GarnetHealerSet": "Garnet (Healer)",
|
||||
"spring2018DucklingRogueSet": "Duckling (Rogue)",
|
||||
"summer2018BettaFishWarriorSet": "Betta Fish (Warrior)",
|
||||
"summer2018LionfishMageSet": "Lionfish (Mage)",
|
||||
"summer2018MerfolkMonarchSet": "Merfolk Monarch (Healer)",
|
||||
"summer2018FisherRogueSet": "Fisher-Rogue (Rogue)",
|
||||
"fall2018MinotaurWarriorSet": "Minotaur (Warrior)",
|
||||
@@ -173,5 +173,10 @@
|
||||
"summer2019ConchHealerSet": "Conch (Healer)",
|
||||
"summer2019WaterLilyMageSet": "Water Lily (Mage)",
|
||||
"summer2019SeaTurtleWarriorSet": "Sea Turtle (Warrior)",
|
||||
"june2018": "June 2018"
|
||||
"june2018": "June 2018",
|
||||
"marchYYYY": "March <%= year %>",
|
||||
"spring2020LapisLazuliRogueSet": "Lapis Lazuli (Rogue)",
|
||||
"spring2020IrisHealerSet": "Iris (Healer)",
|
||||
"spring2020PuddleMageSet": "Puddle (Mage)",
|
||||
"spring2020BeetleWarriorSet": "Rhinoceros Beetle (Warrior)"
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
"questGroupDilatoryDistress": "Dilatory Distress",
|
||||
"questDilatoryDistress1Text": "Dilatory Distress, Part 1: Message in a Bottle",
|
||||
"questDilatoryDistress1Notes": "A message in a bottle arrived from the newly rebuilt city of Dilatory! It reads: \"Dear Habiticans, we need your help once again. Our princess has disappeared and the city is under siege by some unknown watery demons! The mantis shrimps are holding the attackers at bay. Please aid us!\" To make the long journey to the sunken city, one must be able to breathe water. Fortunately, the alchemists @Benga and @hazel can make it all possible! You only have to find the proper ingredients.",
|
||||
"questDilatoryDistress1Completion": "You don the the finned armour and swim to Dilatory as quickly as you can. The merfolk and their mantis shrimp allies have managed to keep the monsters outside the city for the moment, but they are losing. No sooner are you within the castle walls than the horrifying siege descends!",
|
||||
"questDilatoryDistress1Completion": "You don the finned armor and swim to Dilatory as quickly as you can. The merfolk and their mantis shrimp allies have managed to keep the monsters outside the city for the moment, but they are losing. No sooner are you within the castle walls than the horrifying siege descends!",
|
||||
"questDilatoryDistress1CollectFireCoral": "Fire Coral",
|
||||
"questDilatoryDistress1CollectBlueFins": "Blue Fins",
|
||||
"questDilatoryDistress1DropArmor": "Finned Oceanic Armour (Armour)",
|
||||
@@ -568,7 +568,7 @@
|
||||
"questPterodactylUnlockText": "Unlocks Pterodactyl Eggs for purchase in the Market",
|
||||
"questBadgerText": "Stop Badgering Me!",
|
||||
"questBadgerNotes": "Ah, winter in the Taskwoods. The softly falling snow, the branches sparkling with frost, the Flourishing Fairies… still not snoozing?<br><br>“Why are they still awake?” cries @LilithofAlfheim. “If they don't hibernate soon, they'll never have the energy for planting season.”<br><br>As you and @Willow the Witty hurry to investigate, a furry head pops up from the ground. Before you can yell, “It’s the Badgering Bother!” it’s back in its burrow—but not before snatching up the Fairies' “Hibernate” To-Dos and dropping a giant list of pesky tasks in their place!<br><br>“No wonder the fairies aren't resting, if they're constantly being badgered like that!” @plumilla says. Can you chase off this beast and save the Taskwood’s harvest this year?",
|
||||
"questBadgerCompletion": "You finally drive away the the Badgering Bother and hurry into its burrow. At the end of a tunnel, you find its hoard of the faeries’ “Hibernate” To-Dos. The den is otherwise abandoned, except for three eggs that look ready to hatch.",
|
||||
"questBadgerCompletion": "You finally drive away the Badgering Bother and hurry into its burrow. At the end of a tunnel, you find its hoard of the faeries’ “Hibernate” To-Dos. The den is otherwise abandoned, except for three eggs that look ready to hatch.",
|
||||
"questBadgerBoss": "The Badgering Bother",
|
||||
"questBadgerDropBadgerEgg": "Badger (Egg)",
|
||||
"questBadgerUnlockText": "Unlocks Badger Eggs for purchase in the Market",
|
||||
@@ -683,5 +683,5 @@
|
||||
"questRubyCollectAquariusRunes": "Aquarius Zodiac Runes",
|
||||
"questRubyCompletion": "With the necessary items safely packed away, the three of you rush back to Habit City and meet in @beffymaroo's lab. “Excellent work!” @beffymaroo says. “You've gathered the ingredients for the potion!”<br><br>@beffymaroo carefully combines the runes and the rubies to create a brilliant red potion and pours some of it on two pet eggs. As you observe the results, you notice that the two pets seem completely uninterested in one another!<br><br>“Did it not work?” @gully asks. But before anyone can answer, you suddenly realize that it isn't the potion that creates friendship and love, but rather it is the experience of working together toward a common goal. You come away from the quest having gained some new friends...and some flashy new pets!",
|
||||
"questRubyNotes": "The normally bustling peaks of the Stoïkalm Volcanoes lie silent in the snow. “I suppose the hikers and sight-seers are hibernating?” @gully says to you and @Aspiring_Advocate. “That makes our search easier.”<br><br>As you reach the summit, the chill wind merges with the steam billowing from the crater. “There!” @Aspiring_Advocate exclaims, pointing toward a hot spring. “What better place to find cool runes of Aquarius and passionate runes of Venus than where ice and fire meet?”<br><br>The three of you hurry toward the hot spring. “According to my research,” @Aspiring_Advocate says, “combining the runes with heart-shaped rubies will create a hatching potion that can foster friendship and love!”<br><br>Excited by the prospect of a new discovery, you all smile. “All right,” @gully says, “let's start searching!”",
|
||||
"questRubyText": "The Ruby Solution"
|
||||
"questRubyText": "Ruby Rapport"
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@
|
||||
"goToSettings": "Go to Settings",
|
||||
"usernameVerifiedConfirmation": "Your username, <%= username %>, is confirmed!",
|
||||
"usernameNotVerified": "Please confirm your username.",
|
||||
"changeUsernameDisclaimer": "This username will be used for invitations, @mentions in chat, and messaging.",
|
||||
"changeUsernameDisclaimer": "Your username is used for invitations, @mentions in chat, and messaging. It must be 1 to 20 characters, containing only letters a to z, numbers 0 to 9, hyphens, or underscores, and cannot include any inappropriate terms.",
|
||||
"verifyUsernameVeteranPet": "One of these Veteran Pets will be waiting for you after you've finished confirming!",
|
||||
"everywhere": "Everywhere",
|
||||
"onlyPrivateSpaces": "Only in private spaces",
|
||||
|
||||