Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc7dac47c4 | ||
|
|
c8189360d6 | ||
|
|
88183149c5 | ||
|
|
a8f397c674 | ||
|
|
c5aeab652d | ||
|
|
cc04761c24 | ||
|
|
29dccdd148 | ||
|
|
186b929e59 | ||
|
|
551abf292c | ||
|
|
1e1c058d82 | ||
|
|
2ea9070a9b | ||
|
|
168a3a6e89 | ||
|
|
67bd2d9130 | ||
|
|
cb0280ca8b | ||
|
|
259b15877b | ||
|
|
dd31f559c7 | ||
|
|
6c29ea8c4c | ||
|
|
dc316ad1c8 | ||
|
|
a5b37fcc02 | ||
|
|
16f1c7286b | ||
|
|
d5b3e4ec9f | ||
|
|
47eb9bf3ff | ||
|
|
2977346ccb | ||
|
|
a545cc72d0 | ||
|
|
82548f69e5 | ||
|
|
5aacae3ead | ||
|
|
9f8162b82c | ||
|
|
98a749b239 | ||
|
|
0cef9eff87 | ||
|
|
3189a944d7 | ||
|
|
79066165e2 | ||
|
|
c6f6df3f14 | ||
|
|
7bfd6ceca1 | ||
|
|
400f4dfb01 | ||
|
|
57a9fb5241 | ||
|
|
08c63f94fc | ||
|
|
9c6c90a2e9 | ||
|
|
39765895ee | ||
|
|
2a8fc7aea2 | ||
|
|
0a86d04a15 | ||
|
|
3afa7e6da5 | ||
|
|
0ac0723be5 | ||
|
|
0f2d2ddad6 | ||
|
|
e19837f58e | ||
|
|
0ea1ce9758 | ||
|
|
9d16ab7dba | ||
|
|
776e9834f3 | ||
|
|
1d98929453 | ||
|
|
80664b6c5f | ||
|
|
01123367b1 | ||
|
|
f6a80d18b6 | ||
|
|
71af306f02 | ||
|
|
5ccd2ae262 | ||
|
|
a1a1fd939d | ||
|
|
9ac7840940 | ||
|
|
100275f460 | ||
|
|
489b9c4019 | ||
|
|
ba61da4acb | ||
|
|
903851f1fd | ||
|
|
371a1542e7 | ||
|
|
0c640f07d1 | ||
|
|
cbc6e7aa0c | ||
|
|
b6eab67e6a | ||
|
|
f920a441a5 | ||
|
|
12a9eec540 | ||
|
|
f83d86b7f3 | ||
|
|
0410c97001 | ||
|
|
4482b734a5 | ||
|
|
0131cc07bf | ||
|
|
70a9f66dcd | ||
|
|
3057fdbd4a | ||
|
|
cc48479c66 | ||
|
|
f4e573f684 | ||
|
|
a2e59d0920 | ||
|
|
39fe86c688 | ||
|
|
78ceb427a3 | ||
|
|
7c17a32bbb | ||
|
|
9a34c16fa2 | ||
|
|
38c24763fa | ||
|
|
776f3d288b | ||
|
|
e4661c3763 | ||
|
|
156d15c540 | ||
|
|
7d9b8a5ceb | ||
|
|
23b19853b4 | ||
|
|
c6839c4478 | ||
|
|
167f4f07b8 | ||
|
|
95c9dbaa73 | ||
|
|
d9f7772453 | ||
|
|
8efed37241 | ||
|
|
66422d4235 | ||
|
|
eb4e382ecf |
1
.gitignore
vendored
@@ -37,6 +37,7 @@ yarn.lock
|
||||
.elasticbeanstalk/*
|
||||
!.elasticbeanstalk/*.cfg.yml
|
||||
!.elasticbeanstalk/*.global.yml
|
||||
/.vscode
|
||||
|
||||
# webstorm fake webpack for path intellisense
|
||||
webpack.webstorm.config
|
||||
|
||||
@@ -9,3 +9,4 @@ test/**
|
||||
*.swp
|
||||
*.swx
|
||||
website/raw_sprites/**
|
||||
content_cache/**
|
||||
|
||||
@@ -75,5 +75,9 @@
|
||||
"WEB_CONCURRENCY": 1,
|
||||
"SKIP_SSL_CHECK_KEY": "key",
|
||||
"ENABLE_STACKDRIVER_TRACING": "false",
|
||||
"APPLE_AUTH_PRIVATE_KEY": "",
|
||||
"APPLE_TEAM_ID": "",
|
||||
"APPLE_AUTH_CLIENT_ID": "",
|
||||
"APPLE_AUTH_KEY_ID": "",
|
||||
"BLOCKED_IPS": ""
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 { CONTENT_CACHE_PATH, getLocalizedContentResponse } = require('../website/server/libs/content'); // eslint-disable-line global-require
|
||||
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
|
||||
|
||||
try {
|
||||
@@ -23,7 +23,7 @@ gulp.task('content:cache', done => {
|
||||
langCodes.forEach(langCode => {
|
||||
fs.writeFileSync(
|
||||
`${CONTENT_CACHE_PATH}${langCode}.json`,
|
||||
getLocalizedContent(langCode),
|
||||
getLocalizedContentResponse(langCode),
|
||||
'utf8',
|
||||
);
|
||||
});
|
||||
|
||||
827
package-lock.json
generated
12
package.json
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.139.0",
|
||||
"version": "4.140.4",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.5",
|
||||
"@babel/register": "^7.9.0",
|
||||
"@google-cloud/trace-agent": "^4.2.5",
|
||||
"@slack/client": "^4.12.0",
|
||||
@@ -14,7 +14,7 @@
|
||||
"amplitude": "^3.5.0",
|
||||
"apidoc": "^0.17.5",
|
||||
"apn": "^2.2.0",
|
||||
"aws-sdk": "^2.648.0",
|
||||
"apple-auth": "^1.0.5",
|
||||
"bcrypt": "^3.0.8",
|
||||
"body-parser": "^1.18.3",
|
||||
"compression": "^1.7.4",
|
||||
@@ -41,12 +41,14 @@
|
||||
"image-size": "^0.8.3",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"js2xmlparser": "^4.0.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwks-rsa": "^1.7.0",
|
||||
"lodash": "^4.17.15",
|
||||
"merge-stream": "^2.0.0",
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.24.0",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^5.9.6",
|
||||
"mongoose": "^5.9.7",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.10.0",
|
||||
"node-gcm": "^1.0.2",
|
||||
@@ -69,7 +71,7 @@
|
||||
"validator": "^11.0.0",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"winston": "^3.2.1",
|
||||
"winston-loggly-bulk": "^3.0.1",
|
||||
"winston-loggly-bulk": "^3.1.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"private": true,
|
||||
|
||||
@@ -8,9 +8,9 @@ describe('contentLib', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLocalizedContent', () => {
|
||||
describe('getLocalizedContentResponse', () => {
|
||||
it('clones, not modify, the original content data', () => {
|
||||
contentLib.getLocalizedContent();
|
||||
contentLib.getLocalizedContentResponse();
|
||||
expect(typeof content.backgrounds.backgrounds062014.beach.text).to.equal('function');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,6 +18,16 @@ function getUser () {
|
||||
value: 'email@facebook',
|
||||
}],
|
||||
},
|
||||
google: {
|
||||
emails: [{
|
||||
value: 'email@google',
|
||||
}],
|
||||
},
|
||||
apple: {
|
||||
emails: [{
|
||||
value: 'email@apple',
|
||||
}],
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
name: 'profile name',
|
||||
@@ -58,6 +68,8 @@ describe('emails', () => {
|
||||
const user = getUser();
|
||||
delete user.profile.name;
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.google.emails;
|
||||
delete user.auth.apple.emails;
|
||||
|
||||
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
|
||||
@@ -67,12 +79,48 @@ describe('emails', () => {
|
||||
expect(data).to.have.property('canSend', true);
|
||||
});
|
||||
|
||||
it('returns correct user data [google users]', () => {
|
||||
const attachEmail = requireAgain(pathToEmailLib);
|
||||
const { getUserInfo } = attachEmail;
|
||||
const user = getUser();
|
||||
delete user.profile.name;
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.facebook.emails;
|
||||
delete user.auth.apple.emails;
|
||||
|
||||
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
|
||||
expect(data).to.have.property('name', user.auth.local.username);
|
||||
expect(data).to.have.property('email', user.auth.google.emails[0].value);
|
||||
expect(data).to.have.property('_id', user._id);
|
||||
expect(data).to.have.property('canSend', true);
|
||||
});
|
||||
|
||||
it('returns correct user data [apple users]', () => {
|
||||
const attachEmail = requireAgain(pathToEmailLib);
|
||||
const { getUserInfo } = attachEmail;
|
||||
const user = getUser();
|
||||
delete user.profile.name;
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.google.emails;
|
||||
delete user.auth.facebook.emails;
|
||||
|
||||
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
|
||||
expect(data).to.have.property('name', user.auth.local.username);
|
||||
expect(data).to.have.property('email', user.auth.apple.emails[0].value);
|
||||
expect(data).to.have.property('_id', user._id);
|
||||
expect(data).to.have.property('canSend', true);
|
||||
});
|
||||
|
||||
it('has fallbacks for missing data', () => {
|
||||
const attachEmail = requireAgain(pathToEmailLib);
|
||||
const { getUserInfo } = attachEmail;
|
||||
const user = getUser();
|
||||
delete user.auth.local.email;
|
||||
delete user.auth.facebook;
|
||||
delete user.auth.google;
|
||||
delete user.auth.apple;
|
||||
|
||||
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
|
||||
|
||||
|
||||
22
test/api/unit/libs/localizeContentData.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import faq from '../../../../website/common/script/content/faq';
|
||||
import common from '../../../../website/common';
|
||||
import { localizeContentData } from '../../../../website/server/libs/content';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('localizeContentData', () => {
|
||||
it('Should take a an object with localization identifiers and '
|
||||
+ 'return an object with actual translations in English', () => {
|
||||
const faqInEnglish = localizeContentData(faq, 'en');
|
||||
|
||||
expect(faqInEnglish).to.have.property('stillNeedHelp');
|
||||
expect(faqInEnglish.stillNeedHelp.ios).to.equal(i18n.t('iosFaqStillNeedHelp', 'en'));
|
||||
});
|
||||
it('Should take an object with localization identifiers and '
|
||||
+ 'return an object with actual translations in German', () => {
|
||||
const faqInEnglish = localizeContentData(faq, 'de');
|
||||
|
||||
expect(faqInEnglish).to.have.property('stillNeedHelp');
|
||||
expect(faqInEnglish.stillNeedHelp.ios).to.equal(i18n.t('iosFaqStillNeedHelp', 'de'));
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,15 @@
|
||||
import requireAgain from 'require-again';
|
||||
import apn from 'apn/mock';
|
||||
import _ from 'lodash';
|
||||
import nconf from 'nconf';
|
||||
import gcmLib from 'node-gcm'; // works with FCM notifications too
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import {
|
||||
sendNotification as sendPushNotification,
|
||||
MAX_MESSAGE_LENGTH,
|
||||
} from '../../../../website/server/libs/pushNotifications';
|
||||
|
||||
describe('pushNotifications', () => {
|
||||
let user;
|
||||
let sendPushNotification;
|
||||
const pathToPushNotifications = '../../../../website/server/libs/pushNotifications';
|
||||
let fcmSendSpy;
|
||||
let apnSendSpy;
|
||||
|
||||
@@ -28,8 +30,6 @@ describe('pushNotifications', () => {
|
||||
on: () => null,
|
||||
send: apnSendSpy,
|
||||
});
|
||||
|
||||
sendPushNotification = requireAgain(pathToPushNotifications).sendNotification;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -86,6 +86,67 @@ describe('pushNotifications', () => {
|
||||
expect(apnSendSpy).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('cuts the message to 300 chars', () => {
|
||||
const longMessage = `12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
|
||||
|
||||
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
|
||||
|
||||
const details = {
|
||||
identifier,
|
||||
title,
|
||||
message: longMessage,
|
||||
payload: {
|
||||
message: longMessage,
|
||||
},
|
||||
};
|
||||
|
||||
sendPushNotification(user, details);
|
||||
|
||||
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
||||
expect(details.payload.message)
|
||||
.to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
||||
|
||||
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
||||
expect(details.payload.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
||||
});
|
||||
|
||||
it('cuts the message to 300 chars (no payload)', () => {
|
||||
const longMessage = `12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
|
||||
|
||||
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
|
||||
|
||||
const details = {
|
||||
identifier,
|
||||
title,
|
||||
message: longMessage,
|
||||
};
|
||||
|
||||
sendPushNotification(user, details);
|
||||
|
||||
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
||||
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
||||
});
|
||||
|
||||
// TODO disabled because APN relies on a Promise
|
||||
xit('uses APN for iOS devices', () => {
|
||||
user.pushDevices.push({
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
defer,
|
||||
sleep,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
|
||||
import logger from '../../../../website/server/libs/logger';
|
||||
|
||||
describe('webhooks', () => {
|
||||
let webhooks; let
|
||||
@@ -356,6 +356,7 @@ describe('webhooks', () => {
|
||||
});
|
||||
|
||||
it('records failures', async () => {
|
||||
sinon.stub(logger, 'error');
|
||||
const body = {};
|
||||
sendWebhook.send(user, body);
|
||||
|
||||
@@ -369,6 +370,9 @@ describe('webhooks', () => {
|
||||
|
||||
expect(user.webhooks[0].failures).to.equal(1);
|
||||
expect((Date.now() - user.webhooks[0].lastFailureAt.getTime()) < 10000).to.be.true;
|
||||
|
||||
expect(logger.error).to.be.calledOnce;
|
||||
logger.error.restore();
|
||||
});
|
||||
|
||||
it('disables a webhook after 10 failures', async () => {
|
||||
|
||||
@@ -346,4 +346,23 @@ describe('DELETE /user', () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
context('user with Apple auth', async () => {
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({
|
||||
auth: {
|
||||
apple: {
|
||||
id: 'apple-id',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes a Apple user', async () => {
|
||||
await user.del('/user', {
|
||||
password: DELETE_CONFIRMATION,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,4 +95,42 @@ describe('DELETE social registration', () => {
|
||||
expect(user.auth.goodl).to.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
context('Apple', () => {
|
||||
it('fails if user does not have an alternative registration method', async () => {
|
||||
await user.update({
|
||||
'auth.apple.id': 'some-apple-id',
|
||||
'auth.local': { ok: true },
|
||||
});
|
||||
await expect(user.del('/user/auth/social/apple')).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cantDetachSocial'),
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds if user has a local registration', async () => {
|
||||
await user.update({
|
||||
'auth.apple.id': 'some-apple-id',
|
||||
});
|
||||
|
||||
const response = await user.del('/user/auth/social/apple');
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.auth.apple).to.be.undefined;
|
||||
});
|
||||
|
||||
it('succeeds if user has a facebook registration', async () => {
|
||||
await user.update({
|
||||
'auth.apple.id': 'some-apple-id',
|
||||
'auth.facebook.id': 'some-facebook-id',
|
||||
'auth.local': { ok: true },
|
||||
});
|
||||
|
||||
const response = await user.del('/user/auth/social/apple');
|
||||
expect(response).to.eql({});
|
||||
await user.sync();
|
||||
expect(user.auth.goodl).to.be.undefined;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
getProperty,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import * as appleAuth from '../../../../../../website/server/libs/auth/apple';
|
||||
|
||||
describe('GET /user/auth/apple', () => {
|
||||
let api;
|
||||
let user;
|
||||
const appleEndpoint = '/user/auth/apple';
|
||||
|
||||
before(async () => {
|
||||
const expectedResult = { id: 'appleId', name: 'an apple user' };
|
||||
sandbox.stub(appleAuth, 'appleProfile').returns(Promise.resolve(expectedResult));
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
api = requester();
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('registers a new user', async () => {
|
||||
const response = await api.get(appleEndpoint);
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.true;
|
||||
await expect(getProperty('users', response.id, 'auth.apple.id')).to.eventually.equal('appleId');
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('an apple user');
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
const registerResponse = await api.get(appleEndpoint);
|
||||
|
||||
const response = await api.get(appleEndpoint);
|
||||
|
||||
expect(response.apiToken).to.eql(registerResponse.apiToken);
|
||||
expect(response.id).to.eql(registerResponse.id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('add social auth to an existing user', async () => {
|
||||
const response = await user.get(appleEndpoint);
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
});
|
||||
@@ -492,6 +492,74 @@ describe('POST /user/auth/local/register', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('attach to google user', () => {
|
||||
let user;
|
||||
const email = 'some@email-google.net';
|
||||
const username = 'some-username-google';
|
||||
const password = 'some-password';
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
it('checks onlySocialAttachLocal', async () => {
|
||||
await expect(user.post('/user/auth/local/register', {
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlySocialAttachLocal'),
|
||||
});
|
||||
});
|
||||
it('succeeds', async () => {
|
||||
await user.update({ 'auth.google.id': 'some-google-id', 'auth.local': { ok: true } });
|
||||
await user.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
await user.sync();
|
||||
expect(user.auth.local.username).to.eql(username);
|
||||
expect(user.auth.local.email).to.eql(email);
|
||||
});
|
||||
});
|
||||
|
||||
context('attach to apple user', () => {
|
||||
let user;
|
||||
const email = 'some@email-apple.net';
|
||||
const username = 'some-username-apple';
|
||||
const password = 'some-password';
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
it('checks onlySocialAttachLocal', async () => {
|
||||
await expect(user.post('/user/auth/local/register', {
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlySocialAttachLocal'),
|
||||
});
|
||||
});
|
||||
it('succeeds', async () => {
|
||||
await user.update({ 'auth.apple.id': 'some-apple-id', 'auth.local': { ok: true } });
|
||||
await user.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
await user.sync();
|
||||
expect(user.auth.local.username).to.eql(username);
|
||||
expect(user.auth.local.email).to.eql(email);
|
||||
});
|
||||
});
|
||||
|
||||
context('login is already taken', () => {
|
||||
let username; let email; let
|
||||
api;
|
||||
|
||||
@@ -51,6 +51,7 @@ describe('POST /user/auth/social', () => {
|
||||
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user');
|
||||
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.exist;
|
||||
await expect(getProperty('users', response.id, 'auth.facebook.id')).to.eventually.equal(facebookId);
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
@@ -106,6 +107,7 @@ describe('POST /user/auth/social', () => {
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
expect(response.newUser).to.be.true;
|
||||
await expect(getProperty('users', response.id, 'auth.google.id')).to.eventually.equal(googleId);
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
|
||||
});
|
||||
|
||||
|
||||
65
test/api/v4/faq/GET-faq.test.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
requester,
|
||||
translate,
|
||||
} from '../../../helpers/api-integration/v4';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
|
||||
describe('GET /faq', () => {
|
||||
describe('language parameter', () => {
|
||||
it('returns faq (and does not require authentication)', async () => {
|
||||
const res = await requester().get('/faq');
|
||||
|
||||
expect(res).to.have.property('stillNeedHelp');
|
||||
expect(res.stillNeedHelp.ios).to.equal(translate('iosFaqStillNeedHelp'));
|
||||
expect(res).to.have.property('questions');
|
||||
expect(res.questions[0].question).to.equal(translate('faqQuestion0'));
|
||||
});
|
||||
|
||||
it('returns faq not in English', async () => {
|
||||
const res = await requester().get('/faq?language=de');
|
||||
expect(res).to.have.nested.property('stillNeedHelp.ios');
|
||||
expect(res.stillNeedHelp.ios).to.equal(i18n.t('iosFaqStillNeedHelp', 'de'));
|
||||
});
|
||||
|
||||
it('falls back to English if the desired language is not found', async () => {
|
||||
const res = await requester().get('/faq?language=wrong');
|
||||
expect(res).to.have.nested.property('stillNeedHelp.ios');
|
||||
expect(res.stillNeedHelp.ios).to.equal(translate('iosFaqStillNeedHelp'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('platform parameter', () => {
|
||||
it('returns faq with answers for ios platform only', async () => {
|
||||
const res = await requester().get('/faq?platform=ios');
|
||||
|
||||
expect(res).to.have.property('stillNeedHelp');
|
||||
expect(res.stillNeedHelp).to.eql({ ios: translate('iosFaqStillNeedHelp') });
|
||||
|
||||
expect(res).to.have.property('questions');
|
||||
expect(res.questions[0]).to.eql({
|
||||
question: translate('faqQuestion0'),
|
||||
ios: translate('iosFaqAnswer0'),
|
||||
});
|
||||
});
|
||||
it('returns an error when invalid platform parameter is specified', async () => {
|
||||
const request = requester().get('/faq?platform=wrong');
|
||||
await expect(request)
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: i18n.t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
it('falls back to "web" description if there is no description for specified platform', async () => {
|
||||
const res = await requester().get('/faq?platform=android');
|
||||
expect(res).to.have.property('stillNeedHelp');
|
||||
expect(res.stillNeedHelp).to.eql({ web: translate('webFaqStillNeedHelp') });
|
||||
|
||||
expect(res).to.have.property('questions');
|
||||
expect(res.questions[0]).to.eql({
|
||||
question: translate('faqQuestion0'),
|
||||
android: translate('androidFaqAnswer0'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -457,6 +457,74 @@ describe('POST /user/auth/local/register', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('attach to google user', () => {
|
||||
let user;
|
||||
const email = 'some-google@email.net';
|
||||
const username = 'some-username-google';
|
||||
const password = 'some-password';
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
it('checks onlySocialAttachLocal', async () => {
|
||||
await expect(user.post('/user/auth/local/register', {
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlySocialAttachLocal'),
|
||||
});
|
||||
});
|
||||
it('succeeds', async () => {
|
||||
await user.update({ 'auth.google.id': 'some-google-id', 'auth.local': { ok: true } });
|
||||
await user.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
await user.sync();
|
||||
expect(user.auth.local.username).to.eql(username);
|
||||
expect(user.auth.local.email).to.eql(email);
|
||||
});
|
||||
});
|
||||
|
||||
context('attach to apple user', () => {
|
||||
let user;
|
||||
const email = 'some-apple@email.net';
|
||||
const username = 'some-username-apple';
|
||||
const password = 'some-password';
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
it('checks onlySocialAttachLocal', async () => {
|
||||
await expect(user.post('/user/auth/local/register', {
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('onlySocialAttachLocal'),
|
||||
});
|
||||
});
|
||||
it('succeeds', async () => {
|
||||
await user.update({ 'auth.apple.id': 'some-apple-id', 'auth.local': { ok: true } });
|
||||
await user.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
await user.sync();
|
||||
expect(user.auth.local.username).to.eql(username);
|
||||
expect(user.auth.local.email).to.eql(email);
|
||||
});
|
||||
});
|
||||
|
||||
context('login is already taken', () => {
|
||||
let username; let email; let
|
||||
api;
|
||||
|
||||
3246
website/client/package-lock.json
generated
@@ -13,25 +13,25 @@
|
||||
"test:unit": "vue-cli-service test:unit --require ./tests/unit/helpers.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.2.3",
|
||||
"@vue/cli-plugin-eslint": "^4.2.3",
|
||||
"@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.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/cli-plugin-babel": "^4.3.1",
|
||||
"@vue/cli-plugin-eslint": "^4.3.1",
|
||||
"@vue/cli-plugin-router": "^4.3.1",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.3.1",
|
||||
"@vue/cli-service": "^4.3.1",
|
||||
"@storybook/addon-actions": "^5.3.18",
|
||||
"@storybook/addon-knobs": "^5.3.18",
|
||||
"@storybook/addon-links": "^5.3.18",
|
||||
"@storybook/addon-notes": "^5.3.18",
|
||||
"@storybook/vue": "^5.3.18",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^5.10.0",
|
||||
"amplitude-js": "^5.11.0",
|
||||
"axios": "^0.19.2",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^4.4.1",
|
||||
"bootstrap-vue": "^2.9.0",
|
||||
"bootstrap-vue": "^2.11.0",
|
||||
"chai": "^4.1.2",
|
||||
"core-js": "^3.6.4",
|
||||
"core-js": "^3.6.5",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-habitrpg": "^6.2.0",
|
||||
"eslint-plugin-mocha": "^5.3.0",
|
||||
@@ -40,7 +40,7 @@
|
||||
"hellojs": "^1.18.4",
|
||||
"inspectpack": "^4.4.0",
|
||||
"intro.js": "^2.9.3",
|
||||
"jquery": "^3.4.1",
|
||||
"jquery": "^3.5.0",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.24.0",
|
||||
"nconf": "^0.10.0",
|
||||
|
||||
@@ -20,10 +20,6 @@
|
||||
</svg>
|
||||
<!-- eslint-enable max-len -->
|
||||
</div>
|
||||
<div class="col-12 text-center">
|
||||
<h2>{{ $t('tipTitle', {tipNumber: currentTipNumber}) }}</h2>
|
||||
<p>{{ currentTip }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -297,7 +293,6 @@ export default {
|
||||
audioSuffix: null,
|
||||
|
||||
loading: true,
|
||||
currentTipNumber: 0,
|
||||
bannerHidden: false,
|
||||
};
|
||||
},
|
||||
@@ -310,15 +305,6 @@ export default {
|
||||
castingSpell () {
|
||||
return this.$store.state.spellOptions.castingSpell;
|
||||
},
|
||||
currentTip () {
|
||||
const numberOfTips = 35 + 1;
|
||||
const min = 1;
|
||||
const randomNumber = Math.random() * (numberOfTips - min) + min;
|
||||
const tipNumber = Math.floor(randomNumber);
|
||||
this.currentTipNumber = tipNumber; // eslint-disable-line vue/no-side-effects-in-computed-properties, max-len
|
||||
|
||||
return this.$t(`tip${tipNumber}`);
|
||||
},
|
||||
showRestingBanner () {
|
||||
return !this.bannerHidden && this.user && this.user.preferences.sleep;
|
||||
},
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert {
|
||||
background: url("~@/assets/images/animated/Pet_HatchingPotion_Dessert.gif") no-repeat;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Veggie {
|
||||
background: url("~@/assets/images/animated/Pet_HatchingPotion_Veggie.gif") no-repeat;
|
||||
width: 68px;
|
||||
|
||||
@@ -1,27 +1,51 @@
|
||||
.promo_armoire_backgrounds_202004 {
|
||||
.promo_april_fools_2020 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -445px -184px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_egg_quest {
|
||||
.promo_armoire_backgrounds_202004 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -500px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_egg_quest {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -355px -648px;
|
||||
width: 354px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_mystery_202004 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -355px -500px;
|
||||
background-position: -875px 0px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_pastel_skin_hair {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -648px;
|
||||
width: 354px;
|
||||
height: 147px;
|
||||
}
|
||||
.customize-option.promo_pastel_skin_hair {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -25px -663px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_seasonal_shop_spring {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -638px -500px;
|
||||
background-position: -875px -299px;
|
||||
width: 162px;
|
||||
height: 138px;
|
||||
}
|
||||
.promo_shiny_seeds {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -500px;
|
||||
width: 360px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_spring_2019 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -337px;
|
||||
@@ -42,7 +66,7 @@
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -151px -648px;
|
||||
background-position: -1026px -148px;
|
||||
width: 96px;
|
||||
height: 69px;
|
||||
}
|
||||
@@ -54,7 +78,7 @@
|
||||
}
|
||||
.scene_meditation {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -648px;
|
||||
background-position: -875px -148px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 104 KiB |
@@ -53,8 +53,8 @@
|
||||
|
||||
.close-icon {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
cursor: pointer;
|
||||
|
||||
& svg path {
|
||||
|
||||
4
website/client/src/assets/svg/apple.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="170px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 170 170" version="1.1" height="170px">
|
||||
<path d="m150.37 130.25c-2.45 5.66-5.35 10.87-8.71 15.66-4.58 6.53-8.33 11.05-11.22 13.56-4.48 4.12-9.28 6.23-14.42 6.35-3.69 0-8.14-1.05-13.32-3.18-5.197-2.12-9.973-3.17-14.34-3.17-4.58 0-9.492 1.05-14.746 3.17-5.262 2.13-9.501 3.24-12.742 3.35-4.929 0.21-9.842-1.96-14.746-6.52-3.13-2.73-7.045-7.41-11.735-14.04-5.032-7.08-9.169-15.29-12.41-24.65-3.471-10.11-5.211-19.9-5.211-29.378 0-10.857 2.346-20.221 7.045-28.068 3.693-6.303 8.606-11.275 14.755-14.925s12.793-5.51 19.948-5.629c3.915 0 9.049 1.211 15.429 3.591 6.362 2.388 10.447 3.599 12.238 3.599 1.339 0 5.877-1.416 13.57-4.239 7.275-2.618 13.415-3.702 18.445-3.275 13.63 1.1 23.87 6.473 30.68 16.153-12.19 7.386-18.22 17.731-18.1 31.002 0.11 10.337 3.86 18.939 11.23 25.769 3.34 3.17 7.07 5.62 11.22 7.36-0.9 2.61-1.85 5.11-2.86 7.51zm-31.26-123.01c0 8.1021-2.96 15.667-8.86 22.669-7.12 8.324-15.732 13.134-25.071 12.375-0.119-0.972-0.188-1.995-0.188-3.07 0-7.778 3.386-16.102 9.399-22.908 3.002-3.446 6.82-6.3113 11.45-8.597 4.62-2.2516 8.99-3.4968 13.1-3.71 0.12 1.0831 0.17 2.1663 0.17 3.2409z" fill="#FFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
55
website/client/src/assets/svg/apple_black.svg
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="1000"
|
||||
viewBox="0 0 1000 1187.198"
|
||||
version="1.1"
|
||||
height="1187.198"
|
||||
id="svg2"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="Apple_1998.svg">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="705"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.1767767"
|
||||
inkscape:cx="-1066.5045"
|
||||
inkscape:cy="964.94669"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<path
|
||||
d="m 979.04184,925.18785 c -17.95397,41.47737 -39.20563,79.65705 -63.82824,114.75895 -33.56298,47.8528 -61.04356,80.9761 -82.22194,99.3698 -32.83013,30.192 -68.00529,45.6544 -105.67203,46.5338 -27.04089,0 -59.6512,-7.6946 -97.61105,-23.3035 -38.08442,-15.5358 -73.08371,-23.2303 -105.08578,-23.2303 -33.56296,0 -69.55888,7.6945 -108.06101,23.2303 -38.5608,15.6089 -69.62484,23.7432 -93.37541,24.5493 -36.12049,1.5389 -72.1237,-14.3632 -108.06101,-47.7796 -22.93711,-20.0059 -51.62684,-54.3017 -85.99592,-102.8874 C 92.254176,984.54592 61.937588,924.38175 38.187028,855.7902 12.750995,781.70252 0,709.95986 0,640.50361 0,560.94181 17.191859,492.32094 51.626869,434.81688 78.689754,388.62753 114.69299,352.19192 159.75381,325.44413 c 45.06086,-26.74775 93.74914,-40.37812 146.18212,-41.25019 28.68971,0 66.3125,8.8744 113.06613,26.31542 46.62174,17.49964 76.55727,26.37404 89.68198,26.37404 9.8124,0 43.06758,-10.37669 99.4431,-31.06405 53.31237,-19.18512 98.30724,-27.12887 135.16787,-23.99975 99.8828,8.06098 174.92313,47.43518 224.82789,118.37174 -89.33023,54.12578 -133.51903,129.93556 -132.63966,227.18753 0.8061,75.75115 28.28668,138.78795 82.2952,188.8393 24.47603,23.23022 51.81008,41.18421 82.22186,53.93522 -6.59525,19.12648 -13.557,37.44688 -20.95846,55.03446 z M 749.96366,23.751237 c 0,59.37343 -21.69138,114.810233 -64.92748,166.121963 -52.17652,60.99961 -115.28658,96.24803 -183.72426,90.68597 -0.87204,-7.12298 -1.37769,-14.61967 -1.37769,-22.49743 0,-56.99843 24.81315,-117.99801 68.87738,-167.873453 21.99909,-25.25281 49.978,-46.25018 83.90738,-63.00018 C 686.57507,10.688027 718.59913,1.5631274 748.71783,5.2734376e-4 749.59727,7.9378274 749.96366,15.875627 749.96366,23.750467 Z"
|
||||
id="path4"
|
||||
inkscape:connector-curvature="0" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -7,20 +7,31 @@
|
||||
>
|
||||
<div class="content text-center">
|
||||
<span
|
||||
v-once
|
||||
class="close-icon svg-icon inline icon-10"
|
||||
@click="close()"
|
||||
v-html="icons.close"
|
||||
></span>
|
||||
<h2>{{ $t('congratulations') }}</h2>
|
||||
<h2 v-once>
|
||||
{{ $t('onboardingComplete') }}
|
||||
</h2>
|
||||
<img
|
||||
class="onboarding-complete-banner d-block"
|
||||
src="~@/assets/images/onboarding-complete-banner@2x.png"
|
||||
>
|
||||
<p
|
||||
v-once
|
||||
class="onboarding-complete-text"
|
||||
v-html="$t('onboardingCompleteDesc')"
|
||||
></p>
|
||||
<p
|
||||
v-once
|
||||
class="onboarding-complete-text-small"
|
||||
>
|
||||
{{ $t('onboardingCompleteDescSmall') }}
|
||||
</p>
|
||||
<button
|
||||
v-once
|
||||
class="btn btn-primary"
|
||||
@click="closeWithAction()"
|
||||
>
|
||||
@@ -37,7 +48,7 @@
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding-top: 1em;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -58,7 +69,7 @@ h2 {
|
||||
|
||||
.content {
|
||||
padding: 0 8px;
|
||||
margin-top: 18px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.onboarding-complete-banner {
|
||||
@@ -68,13 +79,21 @@ h2 {
|
||||
}
|
||||
|
||||
.onboarding-complete-text {
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.onboarding-complete-text ::v-deep .gold-amount {
|
||||
color: $yellow-5;
|
||||
}
|
||||
|
||||
.onboarding-complete-text-small {
|
||||
margin-bottom: 1.5rem;
|
||||
color: $gray-100;
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="form">
|
||||
<div class="form-group row text-center">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="col-12">
|
||||
<div
|
||||
class="btn btn-secondary social-button"
|
||||
@click="socialAuth('facebook')"
|
||||
@@ -15,7 +15,9 @@
|
||||
: $t('loginWithSocial', {social: 'Facebook'}) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
</div>
|
||||
<div class="form-group row text-center">
|
||||
<div class="col-12">
|
||||
<div
|
||||
class="btn btn-secondary social-button"
|
||||
@click="socialAuth('google')"
|
||||
@@ -30,6 +32,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row text-center">
|
||||
<div class="col-12">
|
||||
<div
|
||||
class="btn btn-secondary social-button"
|
||||
@click="socialAuth('apple')"
|
||||
>
|
||||
<div
|
||||
class="svg-icon social-icon apple-icon"
|
||||
v-html="icons.appleIcon"
|
||||
></div>
|
||||
<span>{{ registering
|
||||
? $t('signUpWithSocial', {social: 'Apple'})
|
||||
: $t('loginWithSocial', {social: 'Apple'}) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="registering"
|
||||
class="form-group"
|
||||
@@ -199,7 +217,11 @@
|
||||
height: 18px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: .2em;
|
||||
margin-top: .1em;
|
||||
}
|
||||
|
||||
.apple-icon {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
small.form-text {
|
||||
@@ -219,10 +241,11 @@
|
||||
import hello from 'hellojs';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
import { setUpAxios } from '@/libs/auth';
|
||||
import { setUpAxios, buildAppleAuthUrl } from '@/libs/auth';
|
||||
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
|
||||
import googleIcon from '@/assets/svg/google.svg';
|
||||
import appleIcon from '@/assets/svg/apple_black.svg';
|
||||
|
||||
export default {
|
||||
name: 'AuthForm',
|
||||
@@ -239,6 +262,7 @@ export default {
|
||||
data.icons = Object.freeze({
|
||||
facebookIcon: facebookSquareIcon,
|
||||
googleIcon,
|
||||
appleIcon,
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -307,27 +331,31 @@ export default {
|
||||
}, 500),
|
||||
// @TODO: Abstract hello in to action or lib
|
||||
async socialAuth (network) {
|
||||
try {
|
||||
await hello(network).logout();
|
||||
} catch (e) {} // eslint-disable-line
|
||||
if (network === 'apple') {
|
||||
window.location.href = buildAppleAuthUrl();
|
||||
} else {
|
||||
try {
|
||||
await hello(network).logout();
|
||||
} catch (e) {} // eslint-disable-line
|
||||
|
||||
try {
|
||||
const redirectUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
const auth = await hello(network).login({
|
||||
scope: 'email',
|
||||
redirect_uri: redirectUrl, // eslint-disable-line camelcase
|
||||
});
|
||||
try {
|
||||
const redirectUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
const auth = await hello(network).login({
|
||||
scope: 'email',
|
||||
redirect_uri: redirectUrl, // eslint-disable-line camelcase
|
||||
});
|
||||
|
||||
await this.$store.dispatch('auth:socialAuth', {
|
||||
auth,
|
||||
});
|
||||
await this.$store.dispatch('auth:socialAuth', {
|
||||
auth,
|
||||
});
|
||||
|
||||
await this.finishAuth();
|
||||
} catch (err) {
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
// logout the user
|
||||
await hello(network).logout();
|
||||
this.socialAuth(network); // login again
|
||||
await this.finishAuth();
|
||||
} catch (err) {
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
// logout the user
|
||||
await hello(network).logout();
|
||||
this.socialAuth(network); // login again
|
||||
}
|
||||
}
|
||||
},
|
||||
async register () {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row text-center">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="col-12 col-md-12">
|
||||
<div
|
||||
class="btn btn-secondary social-button"
|
||||
@click="socialAuth('facebook')"
|
||||
@@ -54,7 +54,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
</div>
|
||||
<div class="form-group row text-center">
|
||||
<div class="col-12 col-md-12">
|
||||
<div
|
||||
class="btn btn-secondary social-button"
|
||||
@click="socialAuth('google')"
|
||||
@@ -73,6 +75,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row text-center">
|
||||
<div class="col-12 col-md-12">
|
||||
<div
|
||||
class="btn btn-secondary social-button"
|
||||
@click="socialAuth('apple')"
|
||||
>
|
||||
<div
|
||||
class="svg-icon social-icon"
|
||||
v-html="icons.appleIcon"
|
||||
></div>
|
||||
<div
|
||||
class="text"
|
||||
>
|
||||
{{ registering
|
||||
? $t('signUpWithSocial', {social: 'Apple'})
|
||||
: $t('loginWithSocial', {social: 'Apple'}) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="strike">
|
||||
<span>{{ $t('or') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="registering"
|
||||
class="form-group"
|
||||
@@ -496,12 +521,13 @@
|
||||
}
|
||||
|
||||
.social-icon {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: .2em;
|
||||
margin-top: .1em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,6 +607,42 @@
|
||||
.exclamation {
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
.strike {
|
||||
display: block;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.strike > span {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
line-height: 1.14;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.strike > span:before,
|
||||
.strike > span:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 9999px;
|
||||
height: 1px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.strike > span:before {
|
||||
right: 100%;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.strike > span:after {
|
||||
left: 100%;
|
||||
margin-left: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -589,6 +651,7 @@ import hello from 'hellojs';
|
||||
import moment from 'moment';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
import { buildAppleAuthUrl } from '../../libs/auth';
|
||||
|
||||
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||
import exclamation from '@/assets/svg/exclamation.svg';
|
||||
@@ -596,6 +659,7 @@ import gryphon from '@/assets/svg/gryphon.svg';
|
||||
import habiticaIcon from '@/assets/svg/habitica-logo.svg';
|
||||
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
|
||||
import googleIcon from '@/assets/svg/google.svg';
|
||||
import appleIcon from '@/assets/svg/apple_black.svg';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
@@ -618,6 +682,7 @@ export default {
|
||||
habiticaIcon,
|
||||
facebookIcon: facebookSquareIcon,
|
||||
googleIcon,
|
||||
appleIcon,
|
||||
});
|
||||
|
||||
return data;
|
||||
@@ -799,35 +864,39 @@ export default {
|
||||
},
|
||||
// @TODO: Abstract hello in to action or lib
|
||||
async socialAuth (network) {
|
||||
try {
|
||||
await hello(network).logout();
|
||||
} catch (e) {} // eslint-disable-line
|
||||
|
||||
const redirectUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
const auth = await hello(network).login({
|
||||
scope: 'email',
|
||||
// explicitly pass the redirect url or it might redirect to /home
|
||||
redirect_uri: redirectUrl, // eslint-disable-line camelcase
|
||||
});
|
||||
|
||||
await this.$store.dispatch('auth:socialAuth', {
|
||||
auth,
|
||||
});
|
||||
|
||||
let redirectTo;
|
||||
|
||||
if (this.$route.query.redirectTo) {
|
||||
redirectTo = this.$route.query.redirectTo;
|
||||
if (network === 'apple') {
|
||||
window.location.href = buildAppleAuthUrl();
|
||||
} else {
|
||||
redirectTo = '/';
|
||||
}
|
||||
try {
|
||||
await hello(network).logout();
|
||||
} catch (e) {} // eslint-disable-line
|
||||
|
||||
// @TODO do not reload entire page
|
||||
// problem is that app.vue created hook should be called again
|
||||
// after user is logged in / just signed up
|
||||
// ALSO it's the only way to make sure language data
|
||||
// is reloaded and correct for the logged in user
|
||||
window.location.href = redirectTo;
|
||||
const redirectUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
const auth = await hello(network).login({
|
||||
scope: 'email',
|
||||
// explicitly pass the redirect url or it might redirect to /home
|
||||
redirect_uri: redirectUrl, // eslint-disable-line camelcase
|
||||
});
|
||||
|
||||
await this.$store.dispatch('auth:socialAuth', {
|
||||
auth,
|
||||
});
|
||||
|
||||
let redirectTo;
|
||||
|
||||
if (this.$route.query.redirectTo) {
|
||||
redirectTo = this.$route.query.redirectTo;
|
||||
} else {
|
||||
redirectTo = '/';
|
||||
}
|
||||
|
||||
// @TODO do not reload entire page
|
||||
// problem is that app.vue created hook should be called again
|
||||
// after user is logged in / just signed up
|
||||
// ALSO it's the only way to make sure language data
|
||||
// is reloaded and correct for the logged in user
|
||||
window.location.href = redirectTo;
|
||||
}
|
||||
},
|
||||
handleSubmit () {
|
||||
if (this.registering) {
|
||||
|
||||
@@ -15,13 +15,17 @@
|
||||
class="onboarding-complete-banner d-block"
|
||||
src="~@/assets/images/onboarding-complete-banner@2x.png"
|
||||
>
|
||||
<h3>{{ $t('congratulations') }}</h3>
|
||||
<h3 v-once>
|
||||
{{ $t('onboardingComplete') }}
|
||||
</h3>
|
||||
<p
|
||||
v-once
|
||||
class="onboarding-complete-text"
|
||||
v-html="$t('onboardingCompleteDesc')"
|
||||
></p>
|
||||
<div class="notifications-buttons">
|
||||
<div
|
||||
v-once
|
||||
class="btn btn-small btn-primary btn-block"
|
||||
>
|
||||
{{ $t('viewAchievements') }}
|
||||
|
||||
@@ -10,20 +10,25 @@
|
||||
v-html="icons.down"
|
||||
></div>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon onboarding-guide-banner"
|
||||
v-html="icons.onboardingGuideBanner"
|
||||
></div>
|
||||
<h3 class="getting-started">
|
||||
<h3
|
||||
v-once
|
||||
class="getting-started"
|
||||
>
|
||||
{{ $t('gettingStarted') }}
|
||||
</h3>
|
||||
<span
|
||||
v-once
|
||||
class="getting-started-desc"
|
||||
v-html="$t('gettingStartedDesc')"
|
||||
></span>
|
||||
<div
|
||||
class="onboarding-progress-box d-flex flex-row justify-content-between small-text mb-2"
|
||||
>
|
||||
<strong>Your Progress</strong>
|
||||
<strong v-once>{{ $t('yourProgress') }}</strong>
|
||||
<span :class="{'has-progress': progress > 0}">{{ progressText }}</span>
|
||||
</div>
|
||||
<div class="onboarding-progress-bar mb-3">
|
||||
@@ -48,8 +53,14 @@
|
||||
<div :class="`achievement-icon ${getAchievementIcon(achievement)}`"></div>
|
||||
</div>
|
||||
<div class="achievement-info d-flex flex-column">
|
||||
<strong class="achievement-title">{{ achievement.title }}</strong>
|
||||
<span class="small-text achievement-desc">{{ getAchievementText(key) }}</span>
|
||||
<strong
|
||||
v-once
|
||||
class="achievement-title"
|
||||
>{{ achievement.title }}</strong>
|
||||
<span
|
||||
v-once
|
||||
class="small-text achievement-desc"
|
||||
>{{ getAchievementText(key) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</b-collapse>
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="petGroup.key !== 'specialPets' && petGroup.key !== 'wackyPets'"
|
||||
v-if="petGroup.key !== 'specialPets' && !(petGroup.key === 'wackyPets' && selectedSortBy !== 'sortByColor')"
|
||||
class="btn btn-flat btn-show-more"
|
||||
@click="setShowMore(petGroup.key)"
|
||||
>
|
||||
@@ -790,7 +790,7 @@ export default {
|
||||
const pets = this.listAnimals(animalGroup, 'pet', hideMissing, sortBy, searchText);
|
||||
|
||||
// Don't group special
|
||||
if (animalGroup.key === 'specialPets' || animalGroup.key === 'wackyPets') {
|
||||
if (animalGroup.key === 'specialPets' || (animalGroup.key === 'wackyPets' && sortBy !== 'sortByColor')) {
|
||||
return { none: pets };
|
||||
}
|
||||
|
||||
|
||||
@@ -561,6 +561,8 @@ import { SUPPORTED_SOCIAL_NETWORKS } from '@/../../common/script/constants';
|
||||
import changeClass from '@/../../common/script/ops/changeClass';
|
||||
import notificationsMixin from '../../mixins/notifications';
|
||||
import sounds from '../../libs/sounds';
|
||||
import { buildAppleAuthUrl } from '../../libs/auth';
|
||||
|
||||
// @TODO: this needs our window.env fix
|
||||
// import { availableLanguages } from '../../../server/libs/i18n';
|
||||
|
||||
@@ -837,13 +839,17 @@ export default {
|
||||
this.text(this.$t('detachedSocial', { network: network.name }));
|
||||
},
|
||||
async socialAuth (network) {
|
||||
const auth = await hello(network).login({ scope: 'email' });
|
||||
if (network === 'apple') {
|
||||
window.location.href = buildAppleAuthUrl();
|
||||
} else {
|
||||
const auth = await hello(network).login({ scope: 'email' });
|
||||
|
||||
await this.$store.dispatch('auth:socialAuth', {
|
||||
auth,
|
||||
});
|
||||
await this.$store.dispatch('auth:socialAuth', {
|
||||
auth,
|
||||
});
|
||||
|
||||
window.location.href = '/';
|
||||
window.location.href = '/';
|
||||
}
|
||||
},
|
||||
async changeClassForUser (confirmationNeeded) {
|
||||
if (confirmationNeeded && !window.confirm(this.$t('changeClassConfirmCost'))) return;
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
<span slot="popoverContent">
|
||||
<strong v-if="item.key === 'gem' && gemsLeft === 0">{{ $t('maxBuyGems') }}</strong>
|
||||
<h4 class="popover-content-title">{{ item.text }}</h4>
|
||||
<div
|
||||
v-if="item.event"
|
||||
class="mt-2"
|
||||
>
|
||||
{{ limitedString }}
|
||||
</div>
|
||||
</span>
|
||||
<template
|
||||
slot="itemBadge"
|
||||
@@ -26,14 +32,15 @@
|
||||
import _filter from 'lodash/filter';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import _map from 'lodash/map';
|
||||
import moment from 'moment';
|
||||
import { mapState } from '@/libs/store';
|
||||
import pinUtils from '@/mixins/pinUtils';
|
||||
import planGemLimits from '@/../../common/script/libs/planGemLimits';
|
||||
import seasonalShopConfig from '@/../../common/script/libs/shops-seasonal.config';
|
||||
|
||||
import ShopItem from '../shopItem';
|
||||
import CategoryItem from './categoryItem';
|
||||
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CategoryItem,
|
||||
@@ -53,6 +60,9 @@ export default {
|
||||
return planGemLimits.convCap
|
||||
+ this.user.purchased.plan.consecutive.gemCapExtra - this.user.purchased.plan.gemsBought;
|
||||
},
|
||||
limitedString () {
|
||||
return this.$t('limitedOffer', { date: moment(seasonalShopConfig.dateRange.end).format('LL') });
|
||||
},
|
||||
sortedMarketItems () {
|
||||
let result = _map(this.category.items, e => ({
|
||||
...e,
|
||||
|
||||
@@ -82,6 +82,16 @@
|
||||
>
|
||||
<questDialogDrops :item="item" />
|
||||
</div>
|
||||
<div
|
||||
v-if="item.event"
|
||||
class="limitedTime"
|
||||
>
|
||||
<span
|
||||
class="svg-icon inline icon-16 clock-icon"
|
||||
v-html="icons.clock"
|
||||
></span>
|
||||
<span class="limitedString">{{ limitedString }}</span>
|
||||
</div>
|
||||
<div
|
||||
slot="modal-footer"
|
||||
class="clearfix"
|
||||
@@ -121,6 +131,9 @@
|
||||
margin: 33px auto auto;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.questInfo {
|
||||
width: 70%;
|
||||
@@ -208,6 +221,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
.limitedTime {
|
||||
height: 32px;
|
||||
background-color: $purple-300;
|
||||
width: calc(100% + 30px);
|
||||
margin: 0 -15px; // the modal content has its own padding
|
||||
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.limitedString {
|
||||
height: 16px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.notEnough {
|
||||
pointer-events: none;
|
||||
opacity: 0.55;
|
||||
@@ -247,14 +281,17 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { mapState } from '@/libs/store';
|
||||
import seasonalShopConfig from '@/../../common/script/libs/shops-seasonal.config';
|
||||
|
||||
import svgClock from '@/assets/svg/clock.svg';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import svgGold from '@/assets/svg/gold.svg';
|
||||
import svgGem from '@/assets/svg/gem.svg';
|
||||
import svgPin from '@/assets/svg/pin.svg';
|
||||
import svgExperience from '@/assets/svg/experience.svg';
|
||||
import svgGem from '@/assets/svg/gem.svg';
|
||||
import svgGold from '@/assets/svg/gold.svg';
|
||||
import svgHourglasses from '@/assets/svg/hourglass.svg';
|
||||
import svgPin from '@/assets/svg/pin.svg';
|
||||
|
||||
import BalanceInfo from '../balanceInfo.vue';
|
||||
import currencyMixin from '../_currencyMixin';
|
||||
@@ -286,12 +323,13 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
clock: svgClock,
|
||||
close: svgClose,
|
||||
gold: svgGold,
|
||||
gem: svgGem,
|
||||
pin: svgPin,
|
||||
experience: svgExperience,
|
||||
gem: svgGem,
|
||||
gold: svgGold,
|
||||
hourglass: svgHourglasses,
|
||||
pin: svgPin,
|
||||
}),
|
||||
|
||||
isPinned: false,
|
||||
@@ -319,6 +357,9 @@ export default {
|
||||
if (this.priceType === 'hourglasses') return this.icons.hourglass;
|
||||
return this.icons.gem;
|
||||
},
|
||||
limitedString () {
|
||||
return this.$t('limitedOffer', { date: moment(seasonalShopConfig.dateRange.end).format('LL') });
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
item: function itemChanged () {
|
||||
|
||||
@@ -299,7 +299,10 @@
|
||||
<span slot="popoverContent">
|
||||
<div class="questPopover">
|
||||
<h4 class="popover-content-title">{{ item.text }}</h4>
|
||||
<questInfo :quest="item" />
|
||||
<questInfo
|
||||
:quest="item"
|
||||
:popover-version="true"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
<template
|
||||
|
||||
@@ -1,40 +1,43 @@
|
||||
<template>
|
||||
<div
|
||||
class="row"
|
||||
:class="{'small-version': smallVersion}"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
v-if="quest.collect"
|
||||
class="table-row"
|
||||
class="row"
|
||||
>
|
||||
<dt>{{ $t('collect') + ':' }}</dt>
|
||||
<dd>
|
||||
<div
|
||||
v-for="(collect, key) of quest.collect"
|
||||
:key="key"
|
||||
>
|
||||
<span>{{ collect.count }} {{ getCollectText(collect) }}</span>
|
||||
</div>
|
||||
</dd>
|
||||
<div
|
||||
v-if="quest.collect"
|
||||
class="table-row"
|
||||
>
|
||||
<dt>{{ $t('collect') + ':' }}</dt>
|
||||
<dd>
|
||||
<div
|
||||
v-for="(collect, key) of quest.collect"
|
||||
:key="key"
|
||||
>
|
||||
<span>{{ collect.count }} {{ getCollectText(collect) }}</span>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div
|
||||
v-if="quest.boss"
|
||||
class="table-row"
|
||||
>
|
||||
<dt>{{ $t('bossHP') + ':' }}</dt>
|
||||
<dd>{{ quest.boss.hp }}</dd>
|
||||
</div>
|
||||
<div class="table-row">
|
||||
<dt>{{ $t('difficulty') + ':' }}</dt>
|
||||
<dd>
|
||||
<div
|
||||
v-for="star of stars()"
|
||||
:key="star"
|
||||
class="svg-icon inline icon-16"
|
||||
v-html="icons[star]"
|
||||
></div>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="quest.boss"
|
||||
class="table-row"
|
||||
>
|
||||
<dt>{{ $t('bossHP') + ':' }}</dt>
|
||||
<dd>{{ quest.boss.hp }}</dd>
|
||||
</div>
|
||||
<div class="table-row">
|
||||
<dt>{{ $t('difficulty') + ':' }}</dt>
|
||||
<dd>
|
||||
<div
|
||||
v-for="star of stars()"
|
||||
:key="star"
|
||||
class="svg-icon inline"
|
||||
:class="smallVersion ? 'icon-12' : 'icon-16'"
|
||||
v-html="icons[star]"
|
||||
></div>
|
||||
</dd>
|
||||
<div v-if="quest.event && popoverVersion">
|
||||
{{ limitedString }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -115,16 +118,20 @@ dt {
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
|
||||
import svgStar from '@/assets/svg/difficulty-star.svg';
|
||||
import svgStarHalf from '@/assets/svg/difficulty-star-half.svg';
|
||||
import svgStarEmpty from '@/assets/svg/difficulty-star-empty.svg';
|
||||
|
||||
import seasonalShopConfig from '@/../../common/script/libs/shops-seasonal.config';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
quest: {
|
||||
type: Object,
|
||||
},
|
||||
smallVersion: {
|
||||
popoverVersion: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
@@ -146,6 +153,9 @@ export default {
|
||||
|
||||
return 1;
|
||||
},
|
||||
limitedString () {
|
||||
return this.$t('limitedOffer', { date: moment(seasonalShopConfig.dateRange.end).format('LL') });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
stars () {
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="item.event"
|
||||
class="mt-4"
|
||||
:class="item.purchaseType === 'gear' ? 'mt-4' : 'mt-2'"
|
||||
>
|
||||
{{ limitedString }}
|
||||
</div>
|
||||
|
||||
35
website/client/src/components/static/appleRedirect.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="static-view">
|
||||
<p>Redirecting...</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
@import '~@/assets/scss/static.scss';
|
||||
.static-view {
|
||||
height: 400px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.static-view p {
|
||||
padding-top: 100px;
|
||||
font-size: 2em
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
async mounted () {
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const reqParams = { code: urlParams.get('code') };
|
||||
if (urlParams.has('name')) {
|
||||
reqParams.name = urlParams.get('name');
|
||||
}
|
||||
await this.$store.dispatch('auth:appleAuth', reqParams);
|
||||
|
||||
window.location.href = '/';
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -28,31 +28,6 @@
|
||||
<h3 class="text-center">
|
||||
{{ $t('singUpForFree') }}
|
||||
</h3>
|
||||
<div class="text-center">
|
||||
<button
|
||||
class="social-button"
|
||||
@click="socialAuth('facebook')"
|
||||
>
|
||||
<div
|
||||
class="svg-icon social-icon"
|
||||
v-html="icons.facebookIcon"
|
||||
></div>
|
||||
<span>{{ $t('signUpWithSocial', {social: 'Facebook'}) }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="social-button"
|
||||
@click="socialAuth('google')"
|
||||
>
|
||||
<div
|
||||
class="svg-icon social-icon"
|
||||
v-html="icons.googleIcon"
|
||||
></div>
|
||||
<span>{{ $t('signUpWithSocial', {social: 'Google'}) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="strike">
|
||||
<span>{{ $t('or') }}</span>
|
||||
</div>
|
||||
<form
|
||||
class="form"
|
||||
@submit.prevent.stop="register()"
|
||||
@@ -127,6 +102,41 @@
|
||||
{{ $t('signup') }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="strike">
|
||||
<span>{{ $t('or') }}</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button
|
||||
class="social-button"
|
||||
@click="socialAuth('facebook')"
|
||||
>
|
||||
<div
|
||||
class="svg-icon social-icon"
|
||||
v-html="icons.facebookIcon"
|
||||
></div>
|
||||
<span>{{ $t('signUpWithSocial', {social: 'Facebook'}) }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="social-button"
|
||||
@click="socialAuth('google')"
|
||||
>
|
||||
<div
|
||||
class="svg-icon social-icon"
|
||||
v-html="icons.googleIcon"
|
||||
></div>
|
||||
<span>{{ $t('signUpWithSocial', {social: 'Google'}) }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="social-button"
|
||||
@click="socialAuth('apple')"
|
||||
>
|
||||
<div
|
||||
class="svg-icon social-icon apple-icon"
|
||||
v-html="icons.appleIcon"
|
||||
></div>
|
||||
<span>{{ $t('signUpWithSocial', {social: 'Apple'}) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div
|
||||
@@ -450,17 +460,17 @@
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.social-button {
|
||||
border-radius: 2px;
|
||||
border: solid 2px #bda8ff;
|
||||
width: 48%;
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
padding: .5em;
|
||||
background: transparent;
|
||||
margin-right: .5em;
|
||||
margin-bottom: .5em;
|
||||
color: #bda8ff;
|
||||
transition: .5s;
|
||||
|
||||
@@ -481,7 +491,11 @@
|
||||
height: 18px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: .2em;
|
||||
margin-top: .1em;
|
||||
}
|
||||
|
||||
.apple-icon {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.strike {
|
||||
@@ -781,6 +795,7 @@
|
||||
import hello from 'hellojs';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
import { buildAppleAuthUrl } from '../../libs/auth';
|
||||
import googlePlay from '@/assets/images/home/google-play-badge.svg';
|
||||
import iosAppStore from '@/assets/images/home/ios-app-store.svg';
|
||||
import iphones from '@/assets/images/home/iphones.svg';
|
||||
@@ -790,6 +805,7 @@ import pixelHorizontal2 from '@/assets/images/home/pixel-horizontal-2.svg';
|
||||
import pixelHorizontal3 from '@/assets/images/home/pixel-horizontal-3.svg';
|
||||
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
|
||||
import googleIcon from '@/assets/svg/google.svg';
|
||||
import appleIcon from '@/assets/svg/apple.svg';
|
||||
import cnet from '@/assets/svg/cnet.svg';
|
||||
import fastCompany from '@/assets/svg/fast-company.svg';
|
||||
import discover from '@/assets/images/home/discover.svg';
|
||||
@@ -814,6 +830,7 @@ export default {
|
||||
pixelHorizontal3,
|
||||
facebookIcon: facebookSquareIcon,
|
||||
googleIcon,
|
||||
appleIcon,
|
||||
cnet,
|
||||
fastCompany,
|
||||
discover,
|
||||
@@ -885,9 +902,9 @@ export default {
|
||||
});
|
||||
|
||||
hello.init({
|
||||
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line
|
||||
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line
|
||||
// windows: WINDOWS_CLIENT_ID,
|
||||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
@@ -946,22 +963,26 @@ export default {
|
||||
},
|
||||
// @TODO: Abstract hello in to action or lib
|
||||
async socialAuth (network) {
|
||||
try {
|
||||
await hello(network).logout();
|
||||
} catch (e) {} // eslint-disable-line
|
||||
if (network === 'apple') {
|
||||
window.location.href = buildAppleAuthUrl();
|
||||
} else {
|
||||
try {
|
||||
await hello(network).logout();
|
||||
} catch (e) {} // eslint-disable-line
|
||||
|
||||
const redirectUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
const auth = await hello(network).login({
|
||||
scope: 'email',
|
||||
// explicitly pass the redirect url or it might redirect to /home
|
||||
redirect_uri: redirectUrl, // eslint-disable-line camelcase
|
||||
});
|
||||
const redirectUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
const auth = await hello(network).login({
|
||||
scope: 'email',
|
||||
// explicitly pass the redirect url or it might redirect to /home
|
||||
redirect_uri: redirectUrl, // eslint-disable-line camelcase
|
||||
});
|
||||
|
||||
await this.$store.dispatch('auth:socialAuth', {
|
||||
auth,
|
||||
});
|
||||
await this.$store.dispatch('auth:socialAuth', {
|
||||
auth,
|
||||
});
|
||||
|
||||
window.location.href = '/';
|
||||
window.location.href = '/';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -349,16 +349,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="achievementsCategories[key].number > 5"
|
||||
class="btn btn-flat btn-show-more"
|
||||
@click="toggleAchievementsCategory(key)"
|
||||
>
|
||||
{{ achievementsCategories[key].open ?
|
||||
$t('hideAchievements', {category: $t(key+'Achievs')}) :
|
||||
$t('showAllAchievements', {category: $t(key+'Achievs')})
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
v-if="achievementsCategories[key].number > 5"
|
||||
class="btn btn-flat btn-show-more"
|
||||
@click="toggleAchievementsCategory(key)"
|
||||
>
|
||||
{{ achievementsCategories[key].open ?
|
||||
$t('hideAchievements', {category: $t(key+'Achievs')}) :
|
||||
$t('showAllAchievements', {category: $t(key+'Achievs')})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="col-12">
|
||||
|
||||
@@ -266,7 +266,7 @@
|
||||
class="col-12 col-md-3"
|
||||
>
|
||||
<div class="box white row col-12">
|
||||
<div class="col-9">
|
||||
<div class="col-9 text-nowrap">
|
||||
<div :class="stat">
|
||||
{{ $t(stats[stat].title) }}
|
||||
</div>
|
||||
|
||||
@@ -21,3 +21,8 @@ export function setUpAxios (AUTH_SETTINGS) { // eslint-disable-line import/prefe
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function buildAppleAuthUrl () {
|
||||
const redirectUrl = `${window.location.protocol}//${window.location.host}/api/v4/user/auth/apple`;
|
||||
return `https://appleid.apple.com/auth/authorize?response_mode=form_post&scope=name%20email&response_type=code&version=2&redirect_uri=${redirectUrl}&client_id=${process.env.APPLE_AUTH_CLIENT_ID}`;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const StaticWrapper = () => import(/* webpackChunkName: "entry" */'@/components/
|
||||
const HomePage = () => import(/* webpackChunkName: "entry" */'@/components/static/home');
|
||||
|
||||
const AppPage = () => import(/* webpackChunkName: "static" */'@/components/static/app');
|
||||
const AppleRedirectPage = () => import(/* webpackChunkName: "static" */'@/components/static/appleRedirect');
|
||||
const ClearBrowserDataPage = () => import(/* webpackChunkName: "static" */'@/components/static/clearBrowserData');
|
||||
const CommunityGuidelinesPage = () => import(/* webpackChunkName: "static" */'@/components/static/communityGuidelines');
|
||||
const ContactPage = () => import(/* webpackChunkName: "static" */'@/components/static/contact');
|
||||
@@ -272,6 +273,9 @@ const router = new VueRouter({
|
||||
{
|
||||
name: 'app', path: 'app', component: AppPage, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'appleRedirect', path: 'apple-redirect', component: AppleRedirectPage, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'clearBrowserData', path: 'clear-browser-data', component: ClearBrowserDataPage, meta: { requiresLogin: false },
|
||||
},
|
||||
|
||||
@@ -82,6 +82,27 @@ export async function socialAuth (store, params) {
|
||||
localStorage.setItem(LOCALSTORAGE_AUTH_KEY, userLocalData);
|
||||
}
|
||||
|
||||
export async function appleAuth (store, params) {
|
||||
const url = '/api/v4/user/auth/apple';
|
||||
const result = await axios.get(url, {
|
||||
params: {
|
||||
code: params.code,
|
||||
name: params.name,
|
||||
},
|
||||
});
|
||||
|
||||
const user = result.data.data;
|
||||
|
||||
const userLocalData = JSON.stringify({
|
||||
auth: {
|
||||
apiId: user.id,
|
||||
apiToken: user.apiToken,
|
||||
},
|
||||
});
|
||||
|
||||
localStorage.setItem(LOCALSTORAGE_AUTH_KEY, userLocalData);
|
||||
}
|
||||
|
||||
export function logout (store, options = {}) {
|
||||
localStorage.clear();
|
||||
const query = options.redirectToLogin === true ? '?redirectToLogin=true' : '';
|
||||
|
||||
@@ -25,6 +25,7 @@ const envVars = [
|
||||
'STRIPE_PUB_KEY',
|
||||
'FACEBOOK_KEY',
|
||||
'GOOGLE_CLIENT_ID',
|
||||
'APPLE_AUTH_CLIENT_ID',
|
||||
'AMPLITUDE_KEY',
|
||||
'LOGGLY_CLIENT_TOKEN',
|
||||
// TODO necessary? if yes how not to mess up with vue cli? 'NODE_ENV'
|
||||
|
||||
@@ -513,5 +513,12 @@
|
||||
"backgroundButterflyGardenText": "Schmetterlingsgarten",
|
||||
"backgroundAmongGiantFlowersNotes": "Flaniere auf und Unter Gigantischen Blumen.",
|
||||
"backgroundAmongGiantFlowersText": "Unter Gigantischen Blumen",
|
||||
"backgrounds032020": "Set 70: Veröffentlicht im März 2020"
|
||||
"backgrounds032020": "Set 70: Veröffentlicht im März 2020",
|
||||
"backgroundRainyBarnyardNotes": "Mach einen durchnässten, spritzigen Spaziergang auf einem regnerischen Scheunenhof.",
|
||||
"backgroundRainyBarnyardText": "Regnerischer Scheunenhof",
|
||||
"backgroundHeatherFieldNotes": "Genieße den Duft einess Feldes voller Heidenkraut.",
|
||||
"backgroundHeatherFieldText": "Heidenkrautfeld",
|
||||
"backgroundAnimalCloudsNotes": "Trainiere Deine Vorstellungskraft, indem du Tierformen in den Wolken suchst.",
|
||||
"backgroundAnimalCloudsText": "Tierwolken",
|
||||
"backgrounds042020": "Set 71: Veröffentlicht im April 2020"
|
||||
}
|
||||
|
||||
@@ -355,5 +355,6 @@
|
||||
"hatchingPotionAmber": "Bernstein",
|
||||
"hatchingPotionAurora": "Polarlicht",
|
||||
"hatchingPotionRuby": "Rubinrotes",
|
||||
"hatchingPotionBirchBark": "Birkenborke"
|
||||
"hatchingPotionBirchBark": "Birkenborke",
|
||||
"hatchingPotionDessert": "Konfekt"
|
||||
}
|
||||
|
||||
@@ -2051,5 +2051,38 @@
|
||||
"weaponArmoireBaseballBatText": "Baseballschläger",
|
||||
"shieldArmoireBaseballGloveNotes": "Perfekt für das große Turnier oder ein freundschaftliches Fangspiel zwischen zwei Aufgaben. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Baseball-Set (Gegenstand 4 von 4).",
|
||||
"armorArmoireBaseballUniformNotes": "Nadelstreifen kommen nie aus der Mode. Erhöht Ausdauer und Stärke um jeweils <%= attrs %>. Verzauberter Schrank: Baseball-Set (Gegenstand 2 von 4).",
|
||||
"weaponArmoireBaseballBatNotes": "Hol Dir einen Homerun für die guten Gewohnheiten! Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Baseball-Set (Gegenstand 3 von 4)."
|
||||
"weaponArmoireBaseballBatNotes": "Hol Dir einen Homerun für die guten Gewohnheiten! Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Baseball-Set (Gegenstand 3 von 4).",
|
||||
"headAccessoryMystery202004Text": "Mächtige Monarchfalterfühler",
|
||||
"backMystery202004Text": "Mächtige Monarchfalterflügel",
|
||||
"shieldSpecialSpring2020HealerText": "Duftschild",
|
||||
"shieldSpecialSpring2020WarriorText": "Irisierender Schild",
|
||||
"headSpecialSpring2020HealerText": "Iris-Fascinator",
|
||||
"headSpecialSpring2020WarriorText": "Käferhelm",
|
||||
"headSpecialSpring2020RogueText": "Lapislazuli Kabuto-Helm",
|
||||
"armorSpecialSpring2020HealerText": "Beschützendes Blütenblatt",
|
||||
"armorSpecialSpring2020MageText": "Whirlpfützenumhang",
|
||||
"armorSpecialSpring2020WarriorNotes": "Diese harte Schale kann Dich selbst vor den vernichtendsten Attacken schützen. Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2020 Frühlingsausrüstung.",
|
||||
"armorSpecialSpring2020WarriorText": "Exoskelettrüstung",
|
||||
"armorSpecialSpring2020RogueNotes": "Die Farbe der Dämmerung, einer Vielzahl wertvoller Steine, der tiefsten Stelle des Meeres! Erhöht Wahrnehmung um <%= per %>. Limitierte Ausgabe 2020 Frühlingsausrüstung.",
|
||||
"armorSpecialSpring2020RogueText": "Ultramarinblaue Rüstung",
|
||||
"weaponSpecialSpring2020HealerNotes": "Eine Iris ist schön, aber ihre Blätter sind scharf wie Schwerter... lass Dich nicht von den Blumen in die Irre führen, dieser Stab ist hart wie Stahl! Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2020 Frühlingsausrüstung.",
|
||||
"weaponSpecialSpring2020HealerText": "Schwertlilienstab",
|
||||
"weaponSpecialSpring2020MageNotes": "Sie fallen Dir unaufhörlich auf den Kopf! Aber Du wirst sie nie aufhalten, indem Du Dich beklagst. Erhöht Intelligenz um <%= int %> und Wahrnehmung um <%= per %>. Limitierte Ausgabe 2020 Frühlingsausrüstung.",
|
||||
"weaponSpecialSpring2020MageText": "Regentropfen",
|
||||
"weaponSpecialSpring2020WarriorNotes": "Kämpfen oder Fliehen, dieser Flügel wird Dir einen guten Dienst erweisen! Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2020 Frühlingsausrüstung.",
|
||||
"weaponSpecialSpring2020WarriorText": "Geschliffener Flügel",
|
||||
"weaponSpecialSpring2020RogueNotes": "Du wirst so schnell zuschlagen, dass sie NOCH blauer aussehen wird! Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2020 Frühlingsausrüstung.",
|
||||
"weaponSpecialSpring2020RogueText": "Lapislazuli Klinge",
|
||||
"armorArmoireBoxArmorNotes": "Schachtelrüstung: Es passt, also sitzt Du… äh, also ziehst Du damit in die Schlacht, als stolzer Ritter, der Du bist! Erhöht Wahrnehmung und Ausdauer um je <%= attrs %>. Verzauberter Schrank: Papierritter-Set (Gegenstand 3 von 3). ",
|
||||
"armorArmoireBoxArmorText": "Schachtelrüstung",
|
||||
"armorSpecialSpring2020HealerNotes": "Wickle Dich in weiche Blätter und Blüten der Iris um Feinde zu verleiten, Deine heilenden Kräfte zu unterschätzen. Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2020 Frühlingsausrüstung.",
|
||||
"armorSpecialSpring2020MageNotes": "Wenn auch Du einer Regenwasserpfütze nicht widerstehen kannst, ist diese Rüstung wie gemacht für Dich! Verwandle einen kindischen Impuls in eine Vorführung eines mystischen Kunstwerks. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2020 Frühlingsausrüstung.",
|
||||
"weaponArmoirePaperCutterNotes": "Das mag nicht furchteinflössend aussehen, aber hast Du schon mal einen Papierschnitt gehabt? Erhöht Stärke um <%= str %>. Verzauberter Schrank: Papierritter-Set (Gegenstand 1 von 3).",
|
||||
"weaponArmoirePaperCutterText": "Papiermesser",
|
||||
"shieldArmoireHobbyHorseText": "Steckenpferd",
|
||||
"headSpecialSpring2020HealerNotes": "Fessle Deine Feinde mit diesem Kopfschmuck aus Blumen! Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2020 Frühlingsausrüstung.",
|
||||
"headSpecialSpring2020MageNotes": "Ist der Himmel klar? Die Luftfeuchtigkeit niedrig? Keine Sorge, wir haben uns drum gekümmert. Versprühe Deine Magie, ohne Deine Stimmung zu trüben! Erhöht Wahrnehmung um <%= per %>. Limitierte Ausgabe 2020 Frühlingsausrüstung.",
|
||||
"headSpecialSpring2020MageText": "Tropfkantenhut",
|
||||
"headSpecialSpring2020WarriorNotes": "Die Schläge Deiner Gegener werden von diesem durch Käfer inspirierten Helm abprallen! Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2020 Frühlingsausrüstung.",
|
||||
"headSpecialSpring2020RogueNotes": "So knallig und kostbar, dass Du in Versuchung kommen wirst, ihn von Deinem eigenen Kopf zu stehlen. Erhöht Wahrnehmung um <%= per %>. Limitierte Ausgabe 2020 Frühlingsausrüstung."
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"hatchingPotion": "Schlüpfelixier",
|
||||
"noHatchingPotions": "Du hast im Moment keine Schlüpfelixiere.",
|
||||
"inventoryText": "Klicke auf ein Ei um die anwendbaren Elixiere grün hervorgehoben zu sehen. Klicke dann auf ein hervorgehobenes Elixier, um Dein Haustier auszubrüten. Falls kein Elixier hervorgehoben wird, klicke auf das Ei um es abzuwählen und klicke diesmal zuerst auf das Elixier, um die Eier hervorzuheben. Du kannst überflüssige Gegenstände auch an Alexander den Händler verkaufen.",
|
||||
"haveHatchablePet": "Du hast ein <%= potion %> Schlüpfelixier und ein <%= egg %>-Ei, um dieses Haustier auszubrüten! <b>Klicke</b> auf den Pfotenabdruck, damit es schlüpft.",
|
||||
"haveHatchablePet": "Du hast ein <%= potion %> Schlüpfelixier und ein <%= egg %>-Ei, um dieses Haustier auszubrüten! <b>Klicke</b>, damit es schlüpft.",
|
||||
"quickInventory": "Schnell-Inventar",
|
||||
"foodText": "Futter",
|
||||
"food": "Futter und magische Sättel",
|
||||
|
||||
@@ -467,7 +467,7 @@
|
||||
"questPeacockUnlockText": "Schaltet den Kauf von Pfaueiern auf dem Marktplatz frei",
|
||||
"questButterflyText": "Flieg' weiter, Funkenfalter",
|
||||
"questButterflyNotes": "Deine Gärtner-Freundin @Megan schickt Dir eine Einladung: \"Diese warmen Tage wären doch perfekt für einen Besuch des Schmetterlingsgartens in Aufgabistan. Komm, lass uns Schmetterlinge beobachten!\" Doch als Du ankommst, ist der Garten verdorrt -- es ist kaum mehr als verbranntes Gras und vertrocknetes Unkraut übrig. Es war so heiß, dass die Habiticaner nicht vorbei gekommen sind, um die Blumen zu gießen, und die dunkelroten Tagesaufgaben haben alles zu einer trockenen, sonnenverglühten Brandgefahr gemacht. Es ist nur noch ein Schmetterling übrig, aber etwas ist seltsam an ihm...<br><br>\"Oh nein! Das sind perfekte Schlüpfbedingungen für den Entflammten Funkenfalter\", ruft @Leephon.<br><br>\"Wenn wir ihn nicht einfangen, wird er alles zerstören!\" keucht @Eevachu.<br><br>Es wird Zeit, dem Funkenfalter auf Wiedersehen zu sagen!",
|
||||
"questButterflyCompletion": "Nach einer lodernden Schlacht ist der Entflammte Funkenfalter gefangen. \"Tolle Leistung, diesen Möchtegern-Brandstifter einzufangen\", sagt @Megan mit einem Seufzer der Erleichterung. \"Dennoch ist es schwer, selbst den fiesesten Falter zu verunglimpfen. Wir sollten diesen Funkenfalter besser an einem sicheren Ort freilassen... wie der Wüste.\"<br><br>Eine der anderen Gärtnerinnen, @Beffymaroo, kommt zu euch, angesengt aber mit einem Lächeln. \"Möchtest Du uns dabei helfen, diese verwaisten Schmetterlingspuppen großzuziehen, die wir gefunden haben? Vielleicht werden wir nächstes Jahr einen grüneren Garten für sie haben.\"",
|
||||
"questButterflyCompletion": "Nach einer lodernden Schlacht ist der Entflammte Funkenfalter gefangen. \"Tolle Leistung, diesen Möchtegern-Brandstifter einzufangen\", sagt @Megan mit einem Seufzer der Erleichterung. \"Dennoch ist es schwer, selbst den fiesesten Falter zu verunglimpfen. Wir sollten diesen Funkenfalter besser an einem sicheren Ort freilassen... zum Beispiel in der Wüste.\"<br><br>Eine der anderen Gärtnerinnen, @Beffymaroo, kommt zu euch - angesengt, aber mit einem Lächeln. \"Möchtest Du uns dabei helfen, diese verwaisten Schmetterlingspuppen großzuziehen, die wir gefunden haben? Vielleicht werden wir nächstes Jahr einen grüneren Garten für sie haben.\"",
|
||||
"questButterflyBoss": "Entflammter Funkenfalter",
|
||||
"questButterflyDropButterflyEgg": "Raupe (Ei)",
|
||||
"questButterflyUnlockText": "Schaltet den Kauf von Raupeneiern auf dem Marktplatz frei",
|
||||
@@ -683,5 +683,13 @@
|
||||
"questRubyCollectAquariusRunes": "Wassermann-Tierkreis-Runen",
|
||||
"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!\""
|
||||
"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!\"",
|
||||
"questWaffleUnlockText": "Schaltet Konfekt-Schlüpfelixire zum Kauf im Marktplatz frei",
|
||||
"questWaffleDropDessertPotion": "Konfekt-Schlüpfelixir",
|
||||
"questWaffleRageDescription": "Ahornsumpf: Diese Leiste füllt sich, wenn Ihr Eure Tagesaufgaben nicht erledigt. Wenn sie voll ist, wird die Fürchterliche Waffel den Schadensfortschritt Eurer Party zurücksetzen!",
|
||||
"questWaffleRageTitle": "Ahornsumpf",
|
||||
"questWaffleBoss": "Fürchterliche Waffel",
|
||||
"questWaffleNotes": "\"April-Scherzkeks!\" entrüstet sich Lady Glaciate verunsichert. \"Du sagtest, Dein Wüsten-Scherz sei 'erledigt und komplett weggeräumt'!\"<br><br>\"Nun ja, das war er und ist er immer noch, meine Liebe\", antwortet der Scherzkeks verwirrt. \"Und ich bin der ehrlichste aller Scherzkekse. Ist etwas nicht in Ordnung?\"<br><br>\"Da ist ein gigantisches, zuckriges Monster, das sich Habit City nähert!\"<br><br>\"Hmm\", sinniert der Scherzkeks. \"Ich habe auf der Suche nach mystischen Reagenzien für meinen letzten Event einige Höhlen geplündert. Vielleicht habe ich da unerwünschte Aufmerksamkeit erweckt. Ist es die Saccharose-Schlange? Die Torten-Kröte? Ein Tiramisu Rex?\"<br><br>\"Nein! Es ist eine Art… fürchterliche Waffel!\"<br><br>\"Häh. Die ist mir neu! Vielleicht entsprang die all der Umgebungs-Schabernack-Energie.\" Er wendet sich an Dich und @beffymaroo mit einem schiefen Lächeln. \"Ich nehme nicht an, ihr wärt zu einer Heldentat bereit?\"",
|
||||
"questWaffleText": "An die Waffe(l) gegen den Scherzkeks: Desaster-Frühstück!",
|
||||
"questWaffleRageEffect": "`Fürchterliche Waffel setzt AHORNSUMPF ein!` Schmieriger saftiger Sirup lässt Deine Schläge und Sprüche stocken!"
|
||||
}
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
"onwards": "Onwards!",
|
||||
"levelup": "By accomplishing your real life goals, you leveled up and are now fully healed!",
|
||||
"reachedLevel": "You Reached Level <%= level %>",
|
||||
"gettingStartedDesc": "Let’s create a task, complete it, then check out your rewards. You’ll earn <strong>5 achievements</strong> and <strong class=\"gold-amount\">100 gold</strong> once you’re done!",
|
||||
"gettingStartedDesc": "Complete these onboarding tasks and you’ll earn <strong>5 Achievements</strong> and <strong class=\"gold-amount\">100 Gold</strong> once you’re done!",
|
||||
"onboardingProgress": "<%= percentage %>% progress",
|
||||
"yourProgress": "Your Progress",
|
||||
"letsGetStarted": "Let's get started!",
|
||||
"viewAchievements": "View Achievements",
|
||||
"earnedAchievement": "You earned an achievement!",
|
||||
"onboardingCompleteDesc": "You earned <strong>5 achievements</strong> and <strong class=\"gold-amount\">100</strong> gold for completing the list.",
|
||||
"onboardingComplete": "You completed your onboarding tasks!",
|
||||
"onboardingCompleteDesc": "You earned <strong>5 Achievements</strong> and <strong class=\"gold-amount\">100 Gold</strong> for completing the list.",
|
||||
"onboardingCompleteDescSmall": "If you want even more, check out Achievements and start collecting!",
|
||||
"showAllAchievements": "Show All <%= category %>",
|
||||
"hideAchievements": "Hide <%= category %>",
|
||||
"foundNewItems": "You found new items!",
|
||||
@@ -46,21 +49,21 @@
|
||||
"achievementUndeadUndertaker": "Undead Undertaker",
|
||||
"achievementUndeadUndertakerText": "Has tamed all Zombie Mounts.",
|
||||
"achievementUndeadUndertakerModalText": "You tamed all the Zombie Mounts!",
|
||||
"achievementCreatedTask": "Create a Task",
|
||||
"achievementCreatedTask": "Create your first task",
|
||||
"achievementCreatedTaskText": "Created their first task.",
|
||||
"achievementCreatedTaskModalText": "Add a task for something you would like to accomplish this week",
|
||||
"achievementCompletedTask": "Complete a Task",
|
||||
"achievementCompletedTask": "Complete a task",
|
||||
"achievementCompletedTaskText": "Completed their first task.",
|
||||
"achievementCompletedTaskModalText": "Check off any of your tasks to earn rewards",
|
||||
"achievementHatchedPet": "Hatch a Pet",
|
||||
"achievementHatchedPetText": "Hatched their first pet.",
|
||||
"achievementHatchedPetModalText": "Head over to your inventory and try combining a hatching potion and an egg",
|
||||
"achievementHatchedPetModalText": "Head over to your inventory and try combining a hatching Potion and an Egg",
|
||||
"achievementFedPet": "Feed a Pet",
|
||||
"achievementFedPetText": "Fed their first pet.",
|
||||
"achievementFedPetModalText": "There are many different types of food, but pets can be picky",
|
||||
"achievementPurchasedEquipment": "Purchase Equipment",
|
||||
"achievementFedPetModalText": "There are many different types of food, but Pets can be picky",
|
||||
"achievementPurchasedEquipment": "Purchase a piece of Equipment",
|
||||
"achievementPurchasedEquipmentText": "Purchased their first piece of equipment.",
|
||||
"achievementPurchasedEquipmentModalText": "Equipment is a way to customize your avatar and improve your stats",
|
||||
"achievementPurchasedEquipmentModalText": "Equipment is a way to customize your avatar and improve your Stats",
|
||||
"achievementPrimedForPainting": "Primed for Painting",
|
||||
"achievementPrimedForPaintingText": "Has collected all White Pets.",
|
||||
"achievementPrimedForPaintingModalText": "You collected all the White Pets!",
|
||||
|
||||
@@ -299,6 +299,7 @@
|
||||
"hatchingPotionAurora": "Aurora",
|
||||
"hatchingPotionRuby": "Ruby",
|
||||
"hatchingPotionBirchBark": "Birch Bark",
|
||||
"hatchingPotionDessert": "Confection",
|
||||
|
||||
"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) %>.",
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"tipTitle": "Tip #<%= tipNumber %>",
|
||||
"tip1": "Check tasks on the go with the Habitica mobile apps.",
|
||||
"tip2": "Click any equipment to see a preview, or equip it instantly by clicking the star in its upper-left corner!",
|
||||
"tip3": "Use emoji to quickly differentiate between your tasks.",
|
||||
"tip4": "Use the # sign before a task name to make it really big!",
|
||||
"tip5": "It’s best to use skills that cause buffs in the morning so they last longer.",
|
||||
"tip6": "Hover over a task and click the dots to access advanced task controls, such as the ability to push tasks to the top/bottom of your list.",
|
||||
"tip7": "Some backgrounds connect perfectly if Party members use the same background. Ex: Mountain Lake, Pagodas, and Rolling Hills.",
|
||||
"tip8": "Send a Message to someone by clicking their name in chat and then clicking the envelope icon at the top of their profile!",
|
||||
"tip9": "Use the filters + search bar in the Inventories, Shops, Guilds, and Challenges to quickly find what you want.",
|
||||
"tip10": "You can win gems by competing in Challenges. New ones are added every day!",
|
||||
"tip11": "Having more than four Party members increases accountability!",
|
||||
"tip12": "Add checklists to your To-Dos to multiply your rewards!",
|
||||
"tip13": "Click “Tags” on your task page to make an unwieldy task list very manageable!",
|
||||
"tip14": "You can add headers or inspirational quotes to your list as Habits with no (+/-).",
|
||||
"tip15": "Complete all the Masterclasser Quest-lines to learn about Habitica’s secret lore.",
|
||||
"tip16": "Click the link to the Data Display Tool in the footer for valuable insights on your progress.",
|
||||
"tip17": "Use the mobile apps to set reminders for your tasks.",
|
||||
"tip18": "Habits that are just positive or just negative gradually “fade” and return to yellow.",
|
||||
"tip19": "Boost your Intelligence Stat to gain more experience when you complete a task.",
|
||||
"tip20": "Boost your Perception Stat to get more drops and gold.",
|
||||
"tip21": "Boost your Strength Stat to do more boss damage or get critical hits.",
|
||||
"tip22": "Boost your Constitution Stat to lessen the damage from incomplete Dailies.",
|
||||
"tip23": "Reach level 100 to unlock the Orb of Rebirth for free and start a new adventure!",
|
||||
"tip24": "Have a question? Ask in the Habitica Help Guild!",
|
||||
"tip25": "The four seasonal Grand Galas start near the solstices and equinoxes.",
|
||||
"tip26": "You can look for a Party or find Party members in the Party Wanted Guild!",
|
||||
"tip27": "Did a Daily yesterday, but forgot to check it off? Don't worry! With Record Yesterday's Activity, you'll have a chance to record what you did before starting your new day.",
|
||||
"tip28": "Set a Custom Day Start under User Icon > Settings to control when your day restarts.",
|
||||
"tip29": "Complete all your Dailies to get a Perfect Day Buff that increases your Stats!",
|
||||
"tip30": "You can invite people to Guilds, not just Parties.",
|
||||
"tip31": "Check out the pre-made lists in the Library of Tasks and Challenges Guild for example tasks.",
|
||||
"tip32": "Lots of Habitica’s code, art, and writing is made by volunteer contributors! Head to the Aspiring Legends Guild to help.",
|
||||
"tip33": "Check out The Bulletin Board Guild for news about Guilds, Challenges, and other player-created events - and announce your own there!",
|
||||
"tip34": "Occasionally re-evaluate your tasks to make sure they’re up-to-date!",
|
||||
"tip35": "Users who are part of a Group Plan gain the ability to assign tasks to other users in that Group for extra task management and accountability."
|
||||
}
|
||||
@@ -542,7 +542,7 @@
|
||||
|
||||
"questButterflyText": "Bye, Bye, Butterfry",
|
||||
"questButterflyNotes": "Your gardener friend @Megan sends you an invitation: “These warm days are the perfect time to visit Habitica’s butterfly garden in the Taskan countryside. Come see the butterflies migrate!” When you arrive, however, the garden is in shambles -- little more than scorched grass and dried-out weeds. It’s been so hot that the Habiticans haven’t come out to water the flowers, and the dark-red Dailies have turned it into a dry, sun-baked, fire-hazard. There's only one butterfly there, and there's something odd about it...<br><br>“Oh no! This is the perfect hatching ground for the Flaming Butterfry,” cries @Leephon.<br><br>“If we don’t catch it, it’ll destroy everything!” gasps @Eevachu.<br><br>Time to say bye, bye to Butterfry!",
|
||||
"questButterflyCompletion": "After a blazing battle, the Flaming Butterfry is captured. “Great job catching the that would-be arsonist,” says @Megan with a sigh of relief. “Still, it’s hard to vilify even the vilest butterfly. We’d better free this Butterfry someplace safe…like the desert.”<br><br>One of the other gardeners, @Beffymaroo, comes up to you, singed but smiling. “Will you help raise these foundling chrysalises we found? Perhaps next year we’ll have a greener garden for them.”",
|
||||
"questButterflyCompletion": "After a blazing battle, the Flaming Butterfry is captured. “Great job catching that would-be arsonist,” says @Megan with a sigh of relief. “Still, it’s hard to vilify even the vilest butterfly. We’d better free this Butterfry someplace safe…like the desert.”<br><br>One of the other gardeners, @Beffymaroo, comes up to you, singed but smiling. “Will you help raise these foundling chrysalises we found? Perhaps next year we’ll have a greener garden for them.”",
|
||||
"questButterflyBoss": "Flaming Butterfry",
|
||||
"questButterflyDropButterflyEgg": "Caterpillar (Egg)",
|
||||
"questButterflyUnlockText": "Unlocks Caterpillar Eggs for purchase in the Market",
|
||||
@@ -796,5 +796,15 @@
|
||||
"questRubyCollectVenusRunes": "Venus Runes",
|
||||
"questRubyCollectRubyGems": "Ruby Gems",
|
||||
"questRubyDropRubyPotion": "Ruby Hatching Potion",
|
||||
"questRubyUnlockText": "Unlocks Ruby Hatching Potions for purchase in the Market"
|
||||
"questRubyUnlockText": "Unlocks Ruby Hatching Potions for purchase in the Market",
|
||||
|
||||
"questWaffleText": "Waffling with the Fool: Disaster Breakfast!",
|
||||
"questWaffleNotes": "“April Fool!” storms a flustered Lady Glaciate. “You said your dessert-themed prank was ‘over with and completely cleaned up’!”<br><br>“Why, it was and is, my dear,” replies the Fool, puzzled. “And I am the most honest of Fools. What's wrong?”<br><br>“There's a giant sugary monster approaching Habit City!”<br><br>“Hmm,” muses the Fool. “I did raid a few lairs for the mystic reagents for my last event. Maybe I attracted some unwanted attention. Is it the Saccharine Serpent? The Torte-oise? Tiramisu Rex?”<br><br>“No! It's some sort of... Awful Waffle!”<br><br>“Huh. That's a new one! Perhaps it spawned from all the ambient shenanigan energy.” He turns to you and @beffymaroo with a lopsided smile. “I don't suppose you'd be available for some heroics?”",
|
||||
"questWaffleCompletion": "Battered and buttered but triumphant, you savor sweet victory as the Awful Waffle collapses into a pool of sticky goo.<br><br>“Wow, you really creamed that monster,” says Lady Glaciate, impressed.<br><br>“A piece of cake!” beams the April Fool.<br><br>“Kind of a shame, though,” says @beffymaroo. “It looked good enough to eat.”<br><br>The Fool takes a set of potion bottles from somewhere in his cape, fills them with the syrupy leavings of the Waffle, and mixes in a pinch of sparkling dust. The liquid swirls with color--new Hatching Potions! He tosses them into your arms. “All that adventure has given me an appetite. Who wants to join me for breakfast?”",
|
||||
"questWaffleBoss": "Awful Waffle",
|
||||
"questWaffleRageTitle": "Maple Mire",
|
||||
"questWaffleRageDescription": "Maple Mire: This bar fills when you don't complete your Dailies. When it is full, the Awful Waffle will set back the party's attack progress!",
|
||||
"questWaffleRageEffect": "`Awful Waffle uses MAPLE MIRE!` Sticky sappy syrup slows your swings and spells!",
|
||||
"questWaffleDropDessertPotion": "Confection Hatching Potion",
|
||||
"questWaffleUnlockText": "Unlocks Confection Hatching Potions for purchase in the Market"
|
||||
}
|
||||
|
||||
@@ -513,5 +513,12 @@
|
||||
"backgroundSucculentGardenNotes": "Take in the arid beauty of a Succulent Garden.",
|
||||
"backgroundSucculentGardenText": "Succulent Garden",
|
||||
"backgroundButterflyGardenNotes": "Party with pollinators in a Butterfly Garden.",
|
||||
"backgroundButterflyGardenText": "Butterfly Garden"
|
||||
"backgroundButterflyGardenText": "Butterfly Garden",
|
||||
"backgroundRainyBarnyardNotes": "Take a soggy splashy stroll through a Rainy Barnyard.",
|
||||
"backgroundRainyBarnyardText": "Rainy Barnyard",
|
||||
"backgroundHeatherFieldNotes": "Enjoy the aroma of a Field of Heather.",
|
||||
"backgroundHeatherFieldText": "Heather Field",
|
||||
"backgroundAnimalCloudsNotes": "Exercise your imagination finding Animal shapes in the Clouds.",
|
||||
"backgroundAnimalCloudsText": "Animal Clouds",
|
||||
"backgrounds042020": "SET 71: Released April 2020"
|
||||
}
|
||||
|
||||
@@ -355,5 +355,6 @@
|
||||
"questEggDolphinMountText": "Dolphin",
|
||||
"questEggDolphinText": "Dolphin",
|
||||
"hatchingPotionRuby": "Ruby",
|
||||
"hatchingPotionBirchBark": "Birch Bark"
|
||||
"hatchingPotionBirchBark": "Birch Bark",
|
||||
"hatchingPotionDessert": "Confection"
|
||||
}
|
||||
|
||||
@@ -2083,5 +2083,11 @@
|
||||
"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.",
|
||||
"headAccessoryMystery202004Text": "Mighty Monarch Antennae",
|
||||
"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.",
|
||||
"backMystery202004Text": "Mighty Monarch Wings"
|
||||
"backMystery202004Text": "Mighty Monarch Wings",
|
||||
"shieldArmoireHobbyHorseNotes": "Ride your handsome hobby-horse steed toward your just Rewards! Increases Perception and Constitution by <%= attrs %> each. Enchanted Armoire: Paper Knight Set (Item 2 of 3).",
|
||||
"shieldArmoireHobbyHorseText": "Hobby Horse",
|
||||
"armorArmoireBoxArmorNotes": "Box Armor: It fits, therefore you sits... uh, therefore you wear it into battle, like the bold knight you are! Increases Perception and Constitution by <%= attrs %> each. Enchanted Armoire: Paper Knight Set (Item 3 of 3). ",
|
||||
"armorArmoireBoxArmorText": "Box Armor",
|
||||
"weaponArmoirePaperCutterNotes": "This may not look fearsome, but have you never had a papercut? Increases Strength by <%= str %>. Enchanted Armoire: Paper Knight Set (Item 1 of 3).",
|
||||
"weaponArmoirePaperCutterText": "Paper Cutter"
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"hatchingPotion": "hatching potion",
|
||||
"noHatchingPotions": "You don't have any hatching potions.",
|
||||
"inventoryText": "Click an egg to see usable potions highlighted in green and then click one of the highlighted potions to hatch your pet. If no potions are highlighted, click that egg again to deselect it, and instead click a potion first to have the usable eggs highlighted. You can also sell unwanted drops to Alexander the Merchant.",
|
||||
"haveHatchablePet": "You have a <%= potion %> hatching potion and <%= egg %> egg to hatch this pet! <b>Click</b> the paw print to hatch.",
|
||||
"haveHatchablePet": "You have a <%= potion %> hatching potion and <%= egg %> egg to hatch this pet! <b>Click</b> to hatch!",
|
||||
"quickInventory": "Quick Inventory",
|
||||
"foodText": "food",
|
||||
"food": "Pet Food and Saddles",
|
||||
|
||||
@@ -467,7 +467,7 @@
|
||||
"questPeacockUnlockText": "Unlocks Peacock Eggs for purchase in the Market",
|
||||
"questButterflyText": "Bye, Bye, Butterfry",
|
||||
"questButterflyNotes": "Your gardener friend @Megan sends you an invitation: “These warm days are the perfect time to visit Habitica’s butterfly garden in the Taskan countryside. Come see the butterflies migrate!” When you arrive, however, the garden is in shambles -- little more than scorched grass and dried-out weeds. It’s been so hot that the Habiticans haven’t come out to water the flowers, and the dark-red Dailies have turned it into a dry, sun-baked, fire-hazard. There's only one butterfly there, and there's something odd about it...<br><br>“Oh no! This is the perfect hatching ground for the Flaming Butterfry,” cries @Leephon.<br><br>“If we don’t catch it, it’ll destroy everything!” gasps @Eevachu.<br><br>Time to say bye, bye to Butterfry!",
|
||||
"questButterflyCompletion": "After a blazing battle, the Flaming Butterfry is captured. “Great job catching the that would-be arsonist,” says @Megan with a sigh of relief. “Still, it’s hard to vilify even the vilest butterfly. We’d better free this Butterfry someplace safe…like the desert.”<br><br>One of the other gardeners, @Beffymaroo, comes up to you, singed but smiling. “Will you help raise these foundling chrysalises we found? Perhaps next year we’ll have a greener garden for them.”",
|
||||
"questButterflyCompletion": "After a blazing battle, the Flaming Butterfry is captured. “Great job catching that would-be arsonist,” says @Megan with a sigh of relief. “Still, it’s hard to vilify even the vilest butterfly. We’d better free this Butterfry someplace safe…like the desert.”<br><br>One of the other gardeners, @Beffymaroo, comes up to you, singed but smiling. “Will you help raise these foundling chrysalises we found? Perhaps next year we’ll have a greener garden for them.”",
|
||||
"questButterflyBoss": "Flaming Butterfry",
|
||||
"questButterflyDropButterflyEgg": "Caterpillar (Egg)",
|
||||
"questButterflyUnlockText": "Unlocks Caterpillar Eggs for purchase in the Market",
|
||||
@@ -683,5 +683,14 @@
|
||||
"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": "Ruby Rapport"
|
||||
"questRubyText": "Ruby Rapport",
|
||||
"questWaffleRageTitle": "Maple Mire",
|
||||
"questWaffleBoss": "Awful Waffle",
|
||||
"questWaffleCompletion": "Battered and buttered but triumphant, you savor sweet victory as the Awful Waffle collapses into a pool of sticky goo.<br><br>“Wow, you really creamed that monster,” says Lady Glaciate, impressed.<br><br>“A piece of cake!” beams the April Fool.<br><br>“Kind of a shame, though,” says @beffymaroo. “It looked good enough to eat.”<br><br>The Fool takes a set of potion bottles from somewhere in his cape, fills them with the syrupy leavings of the Waffle, and mixes in a pinch of sparkling dust. The liquid swirls with color--new Hatching Potions! He tosses them into your arms. “All that adventure has given me an appetite. Who wants to join me for breakfast?”",
|
||||
"questWaffleNotes": "“April Fool!” storms a flustered Lady Glaciate. “You said your dessert-themed prank was ‘over with and completely cleaned up’!”<br><br>“Why, it was and is, my dear,” replies the Fool, puzzled. “And I am the most honest of Fools. What's wrong?”<br><br>“There's a giant sugary monster approaching Habit City!”<br><br>“Hmm,” muses the Fool. “I did raid a few lairs for the mystic reagents for my last event. Maybe I attracted some unwanted attention. Is it the Saccharine Serpent? The Torte-oise? Tiramisu Rex?”<br><br>“No! It's some sort of... Awful Waffle!”<br><br>“Huh. That's a new one! Perhaps it spawned from all the ambient shenanigan energy.” He turns to you and @beffymaroo with a lopsided smile. “I don't suppose you'd be available for some heroics?”",
|
||||
"questWaffleText": "Waffling with the Fool: Disaster Breakfast!",
|
||||
"questWaffleUnlockText": "Unlocks Confection Hatching Potions for purchase in the Market",
|
||||
"questWaffleDropDessertPotion": "Confection Hatching Potion",
|
||||
"questWaffleRageEffect": "`Awful Waffle uses MAPLE MIRE!` Sticky sappy syrup slows your swings and spells!",
|
||||
"questWaffleRageDescription": "Maple Mire: This bar fills when you don't complete your Dailies. When it is full, the Awful Waffle will set back the party's attack progress!"
|
||||
}
|
||||
|
||||
@@ -513,5 +513,12 @@
|
||||
"backgroundButterflyGardenText": "Jardin à papillons",
|
||||
"backgroundAmongGiantFlowersNotes": "Badinez parmi les fleurs géantes.",
|
||||
"backgroundAmongGiantFlowersText": "Parmi les fleurs géantes",
|
||||
"backgrounds032020": "Ensemble 70 : sorti en mars 2020"
|
||||
"backgrounds032020": "Ensemble 70 : sorti en mars 2020",
|
||||
"backgroundRainyBarnyardNotes": "Faites une promenade détrempée et éclaboussante dans une basse-cour pluvieuse.",
|
||||
"backgroundRainyBarnyardText": "Basse-cour pluvieuse",
|
||||
"backgroundHeatherFieldNotes": "Appréciez les arômes d'un champ de bruyère.",
|
||||
"backgroundHeatherFieldText": "Champ de bruyère",
|
||||
"backgroundAnimalCloudsNotes": "Entraînez votre imagination à reconnaître les formes des animaux dans les nuages.",
|
||||
"backgroundAnimalCloudsText": "Nuages en formes d'animaux",
|
||||
"backgrounds042020": "Ensemble 71 : sorti en avril 2020"
|
||||
}
|
||||
|
||||
@@ -355,5 +355,6 @@
|
||||
"hatchingPotionAmber": "d'ambre",
|
||||
"hatchingPotionAurora": "Aurore",
|
||||
"hatchingPotionRuby": "Rubis",
|
||||
"hatchingPotionBirchBark": "Écorce de bouleau"
|
||||
"hatchingPotionBirchBark": "Écorce de bouleau",
|
||||
"hatchingPotionDessert": "Confiserie"
|
||||
}
|
||||
|
||||
@@ -2079,5 +2079,15 @@
|
||||
"weaponSpecialSpring2020WarriorNotes": "En vol ou au sol, ces ailes vous serviront bien ! Augmente la force de <%= str %>. Équipement en édition limitée du printemps 2020.",
|
||||
"weaponSpecialSpring2020WarriorText": "Ailes affutées",
|
||||
"weaponSpecialSpring2020RogueNotes": "Vous frapperez si vite que aura l'air encore PLUS bleu ! Augmente la force de <%= str %>. Équipement en édition limitée du printemps 2020.",
|
||||
"weaponSpecialSpring2020RogueText": "Épée de Lazurite"
|
||||
"weaponSpecialSpring2020RogueText": "Épée de Lazurite",
|
||||
"headAccessoryMystery202004Notes": "Elles se trémoussent juste un peu si l'odeur des fleurs passe devant elles : utilisez-les pour trouver un joli jardin ! Ne confère aucun bonus. Équipement d'abonnement d'avril 2020.",
|
||||
"headAccessoryMystery202004Text": "Antennes de merveilleux monarque",
|
||||
"backMystery202004Notes": "Faites un vol rapide vers la prairie fleurie la plus proche ou migrez à travers le continent avec ces jolies ailes ! Ne confère aucun bonus. Équipement d'abonnement d'avril 2020.",
|
||||
"backMystery202004Text": "Ailes de merveilleux monarque",
|
||||
"shieldArmoireHobbyHorseNotes": "Chevauchez votre magnifique monture de jeu vers vos récompenses méritées ! Augmente la perception et la constitution de <%= attrs %> chacune. Armoire enchantée : ensemble de chevalerie de papier (objet 2 de 3).",
|
||||
"shieldArmoireHobbyHorseText": "Cheval de jeu",
|
||||
"armorArmoireBoxArmorNotes": "Attention, un chat y dort peut-être... mais ça ne vous empêchera pas de porter cette armure au combat, avec l'audace qui vous caractérise ! Augmente la perception et la constitution de <%= attrs %> chacune. Armoire enchantée : ensemble de chevalerie de papier (objet 3 de 3). ",
|
||||
"armorArmoireBoxArmorText": "Armure boite",
|
||||
"weaponArmoirePaperCutterNotes": "Ca n'a pas l'air terrible, mais vous êtes vous déjà coupé avec une feuille de papier ? Augmente la force de <%= str %>. Armoire enchantée : ensemble de chevalerie de papier (objet 1 de 3).",
|
||||
"weaponArmoirePaperCutterText": "Coupe papier"
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"hatchingPotion": "potion d'éclosion",
|
||||
"noHatchingPotions": "Vous n'avez pas de potion d'éclosion.",
|
||||
"inventoryText": "Cliquez sur un œuf pour voir les potions utilisables surlignées en vert, puis cliquez sur une des potions surlignées pour faire éclore votre familier. Si aucune potion n'est surlignée, cliquez à nouveau sur l’œuf pour le désélectionner et cliquez plutôt sur une potion d'abord pour voir les œufs utilisables. Vous pouvez aussi vendre votre surplus d'objets à Alexander le marchand.",
|
||||
"haveHatchablePet": "Vous avez une potion d'éclosion <%= potion %> et un œuf de <%= egg %> qui peuvent faire éclore ce familier ! <b>Cliquez</b> sur l'empreinte pour le faire naître.",
|
||||
"haveHatchablePet": "Vous avez une potion d'éclosion <%= potion %> et un œuf de <%= egg %> qui peuvent faire éclore ce familier ! <b>Cliquez</b> pour le faire naître !",
|
||||
"quickInventory": "Inventaire rapide",
|
||||
"foodText": "nourriture",
|
||||
"food": "Nourriture de familiers et selles",
|
||||
|
||||
@@ -683,5 +683,14 @@
|
||||
"questRubyCollectAquariusRunes": "Runes zodiacales du verseau",
|
||||
"questRubyCompletion": "Les objets nécessaires étant bien emballés, vous vous précipitez tous les trois à Habitiville et vous retrouvez dans le laboratoire de @beffymaroo. \"Excellent travail !\" dit @beffymaroo. \"Vous avez réuni les ingrédients de la potion !\"<br><br>@beffymaroo combine soigneusement les runes et les rubis pour créer une potion rouge brillante et en verse une partie sur deux œufs d'animaux de compagnie. En observant les résultats, vous remarquez que les deux animaux de compagnie semblent complètement désintéressés l'un par rapport à l'autre ! <br><br> \"Ça n'a pas marché ?\" demande @gully. Mais avant de pouvoir répondre, vous vous rendez soudain compte que ce n'est pas la potion qui crée l'amitié et l'amour, mais plutôt l'expérience de travailler ensemble vers un but commun. Vous sortez de la quête en ayant gagné de nouveaux amis... et de nouveaux animaux de compagnie tape-à-l'œil !",
|
||||
"questRubyNotes": "Les sommets des volcans du Stoïkalm, normalement très animés, sont silencieux dans la neige. \"Je suppose que les randonneurs et les observateurs sont en hibernation ?\" vous disent @gully et @Aspiring_Advocate. \"Cela facilite notre recherche.\"<br><br>En atteignant le sommet, le vent froid se confond avec la vapeur qui s'échappe du cratère. \"Là !\" s'exclame @Aspiring_Advocate, montrant une source chaude. \"Quel meilleur endroit pour trouver des runes fraîches du Verseau et des runes passionnées de Vénus que là où la glace et le feu se rencontrent ?\"<br><br>Vous vous précipitez vers la source chaude. \"Selon mes recherches,\" dit @Aspiring_Advocate, \"combiner les runes avec des rubis en forme de coeur permettra de créer une potion d'éclosion qui pourra favoriser l'amitié et l'amour !\"<br><br>Excités par la perspective d'une nouvelle découverte, vous souriez tous. \"Très bien\", dit @gully, \"commençons à chercher !\"",
|
||||
"questRubyText": "Rapport rubis"
|
||||
"questRubyText": "Rapport rubis",
|
||||
"questWaffleUnlockText": "Déverrouille l'achat de potion d'éclosion de confiserie au marché",
|
||||
"questWaffleDropDessertPotion": "Potion d'éclosion de confiserie",
|
||||
"questWaffleRageEffect": "`L'affreuse gaufre utilise BOURBIER D'ERABLE !` Un sirop de sève collante ralentit vos coups et vos sorts !",
|
||||
"questWaffleRageDescription": "Bourbier d'érable : Cette barre se remplit quand vous n'effectuez pas vos quotidiennes. Lorsqu'elle est pleine, l'affreuse gaufre annulera l'attaque de l'équipe !",
|
||||
"questWaffleRageTitle": "Bourbier d'érable",
|
||||
"questWaffleBoss": "Affreuse gaufre",
|
||||
"questWaffleCompletion": "Battue et beurrée mais triomphante, vous savourez la douce victoire alors que l'Affreuse Gaufre s'effondre dans une mare gluante.<br><br> \"Wow, vous avez vraiment écrémé ce monstre\", dit Lady Glaciate, impressionnée.<br><br> \"C'était pas de la tarte !\" rayonne le Poison d'avril.<br><br> \"Un peu la honte, cependant\", dit @beffymaroo. \"Ça avait l'air assez bon à manger.\"<br><br>\"Le Poison prend un ensemble de flacons de potion quelque part dans sa cape, les remplit avec les restes sirupeux de la Gaufre, et les mélange dans une pincée de poussière pétillante. Le liquide tourbillonne de couleurs... de nouvelles potions d'éclosion ! Il les jette dans vos bras. \"Toute cette aventure m'a donné de l'appétit. Qui veut se joindre à moi pour le petit déjeuner ?\"",
|
||||
"questWaffleNotes": "Le \"Poison d'avril\" prend d'assaut Lady Glaciate. \"Vous avez dit que votre farce sur le thème du dessert était 'terminée et complètement nettoyée' !\" <br><br> \"Mais, c'était et c'est encore le cas, ma chère\", répond le Poison, perplexe. \"Et je suis le plus honnête des Poisons. Qu'est-ce qui ne va pas ? \"<br><br>\"Il y a un monstre géant sucré qui approche de Habitiville !\"<br><br>\"Hmm,\" dit le Poison. \"J'ai fait un raid dans quelques tanières pour les réactifs mystiques de mon dernier événement. J'ai peut-être attiré une attention non désirée. Est-ce le Serpent Saccharine ? La Torte-oise ? Le Tiramisu Rex ?\"<br><br>\"Non ! C'est une sorte de... d'Affreuse Gaufre !\"<br><br>\"Huh. C'est une nouvelle ! Peut-être qu'elle est née de toute l'énergie ambiante des bêtises.\" Il se tourne vers vous et @beffymaroo avec un sourire en coin. \"Je suppose que vous ne seriez pas disponible pour des actes héroïques ?\"",
|
||||
"questWaffleText": "Gaufrer le poisson : Petit déjeuner désastreux !"
|
||||
}
|
||||
|
||||
@@ -247,5 +247,6 @@
|
||||
"monthlyMysteryItems": "Objets mystère mensuels",
|
||||
"subscribersReceiveBenefits": "Les personnes abonnées reçoivent ces bénéfices utiles !",
|
||||
"mysterySet202003": "Ensemble de combat barbelé",
|
||||
"giftASubscription": "Offrez un abonnement"
|
||||
"giftASubscription": "Offrez un abonnement",
|
||||
"mysterySet202004": "Ensemble du merveilleux monarque"
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
"add": "追加",
|
||||
"undo": "元に戻す",
|
||||
"continue": "続ける",
|
||||
"accept": "承諾",
|
||||
"accept": "承認",
|
||||
"reject": "拒否",
|
||||
"neverMind": "結構です",
|
||||
"buyMoreGems": "ジェムを買う",
|
||||
|
||||
@@ -428,7 +428,7 @@
|
||||
"reverseChat": "チャットの表示順を逆にする",
|
||||
"invites": "招待",
|
||||
"details": "詳細",
|
||||
"participantDesc": "一旦すべてのメンバーが承諾するか拒否するとクエストが開始されます。'承諾する'をクリックした人だけがクエストに参加して報酬を受け取ることができます。",
|
||||
"participantDesc": "一旦すべてのメンバーが承認するか拒否するとクエストが開始されます。「承認する」をクリックした人だけがクエストに参加して報酬を受け取ることができます。",
|
||||
"groupGems": "グループのジェム",
|
||||
"groupGemsDesc": "ギルドのジェムはチャレンジを作るために使うことができます!将来的に、より多くのギルドのジェムを追加することができるようになります。",
|
||||
"groupTaskBoard": "タスクボード",
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
"messageGroupChatAdminClearFlagCount": "フラグ数をクリアーできるのは管理者だけです!",
|
||||
"messageCannotFlagSystemMessages": "システムメッセージを報告することはできません。このメッセージに関してコミュニティガイドラインの違反を報告する必要がある場合は、スクリーンショットと説明をコミュニティー管理者(<%= communityManagerEmail %>)にメールで送ってください。",
|
||||
"messageGroupChatSpam": "おおっと!たくさんのメッセージを投稿しすぎたようです!少しだけ待ってから再度お試しください。キャンプ場では200個のメッセージまでしか一度に表示されませんので,Habiticaでは熟慮と吟味がなされた返信を奨励しています。あなたが言いかけたことを楽しみにしています。:)",
|
||||
"messageCannotLeaveWhileQuesting": "あなたはクエストを実行中のため、このパーティーへの招待を承諾することができません。もしこのパーティーに加わりたいのであれば、まずパーティー画面から実行中のクエストを中止してください。中止したクエストの巻物は手元に戻ります。",
|
||||
"messageCannotLeaveWhileQuesting": "あなたはクエストを実行中のため、このパーティーへの招待を承認できません。もしこのパーティーに加わりたい場合は、まずパーティー画面から実行中のクエストを中止してください。中止したクエストの巻物は手元に戻ります。",
|
||||
"messageUserOperationProtected": "「<%= operation %>」パスは、保護されたパスなので保存できません。",
|
||||
"messageUserOperationNotFound": "<%= operation %> の操作は見つかりません",
|
||||
"messageNotificationNotFound": "通知はありません。",
|
||||
|
||||