mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-05-09 19:20:41 -05:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 47c156d9b1 | |||
| e6418e4356 | |||
| 3c23989e99 | |||
| 2d1f341256 | |||
| 44adfd611a | |||
| ab50c41287 | |||
| c43abe82fe | |||
| ac0b4a324f | |||
| efa0a325a2 | |||
| bc970d33ac | |||
| 09e432cf32 | |||
| 40aa2e214d | |||
| 9f563b741d | |||
| 9db541f4c3 | |||
| ce4a20e3d8 |
@@ -21,4 +21,3 @@ services:
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
retries: 30
|
||||
|
||||
Generated
+547
-193
File diff suppressed because it is too large
Load Diff
+5
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.47.6",
|
||||
"version": "5.46.4",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -19,6 +19,7 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.3",
|
||||
"bootstrap": "^4.6.2",
|
||||
"bullmq": "^5.71.1",
|
||||
"compression": "^1.8.1",
|
||||
"cookie-session": "^2.1.1",
|
||||
"coupon-code": "^0.4.5",
|
||||
@@ -43,6 +44,7 @@
|
||||
"heapdump": "^0.3.15",
|
||||
"helmet": "^4.6.0",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"ioredis": "^5.10.1",
|
||||
"js2xmlparser": "^5.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwks-rsa": "^2.1.5",
|
||||
@@ -52,7 +54,7 @@
|
||||
"micromustache": "^8.0.3",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
|
||||
"mongoose": "^8.23.0",
|
||||
"mongoose": "^8.9.5",
|
||||
"morgan": "^1.10.1",
|
||||
"nan": "^2.25.0",
|
||||
"nconf": "^0.12.1",
|
||||
@@ -66,7 +68,6 @@
|
||||
"pp-ipn": "^1.1.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"rate-limiter-flexible": "^2.4.2",
|
||||
"redis": "^3.1.2",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^4.2.2",
|
||||
@@ -76,6 +77,7 @@
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^9.0.0",
|
||||
"validator": "^13.11.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"winston": "^3.10.0",
|
||||
"winston-loggly-bulk": "^3.3.0",
|
||||
"xml2js": "^0.6.2"
|
||||
|
||||
@@ -0,0 +1,560 @@
|
||||
/* eslint-disable camelcase */
|
||||
import nconf from 'nconf';
|
||||
import Amplitude from 'amplitude';
|
||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('analyticsService', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.stub(Amplitude.prototype, 'track').returns(Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('#getServiceByEnvironment', () => {
|
||||
it('returns mock methods when not in production', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
||||
expect(analyticsService.getAnalyticsServiceByEnvironment())
|
||||
.to.equal(analyticsService.mockAnalyticsService);
|
||||
});
|
||||
|
||||
it('returns real methods when in production', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
expect(analyticsService.getAnalyticsServiceByEnvironment().track)
|
||||
.to.equal(analyticsService.track);
|
||||
expect(analyticsService.getAnalyticsServiceByEnvironment().trackPurchase)
|
||||
.to.equal(analyticsService.trackPurchase);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#track', () => {
|
||||
let eventType; let
|
||||
data;
|
||||
|
||||
beforeEach(() => {
|
||||
eventType = 'Cron';
|
||||
data = {
|
||||
category: 'behavior',
|
||||
uuid: 'unique-user-id',
|
||||
resting: true,
|
||||
cronCount: 5,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
user: {
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
context('Amplitude', () => {
|
||||
it('calls out to amplitude', () => analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledOnce;
|
||||
}));
|
||||
|
||||
it('uses a dummy user id if none is provided', () => {
|
||||
delete data.uuid;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_id: 'no-user-id-was-provided',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('platform', () => {
|
||||
it('logs web platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-web' };
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Web',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs iOS platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-ios' };
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'iOS',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs Android platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-android' };
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Android',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs 3rd Party platform', () => {
|
||||
data.headers = { 'x-client': 'some-third-party' };
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: '3rd Party',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Unknown',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Operating System', () => {
|
||||
it('sets default', () => {
|
||||
data.headers = {
|
||||
'x-client': 'third-party',
|
||||
'user-agent': 'foo',
|
||||
};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Other',
|
||||
os_version: '0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets iOS', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-ios',
|
||||
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)',
|
||||
};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'iOS',
|
||||
os_version: '9.3.0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Android', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-android',
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
|
||||
};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Android',
|
||||
os_version: '4.0.4',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: undefined,
|
||||
os_version: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends details about event', () => analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
category: 'behavior',
|
||||
resting: true,
|
||||
cronCount: 5,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('sends english item name for gear if itemKey is provided', () => {
|
||||
data.itemKey = 'headAccessory_special_foxEars';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Fox Ears',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for egg if itemKey is provided', () => {
|
||||
data.itemKey = 'Wolf';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Wolf Egg',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for food if itemKey is provided', () => {
|
||||
data.itemKey = 'Cake_Skeleton';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Bare Bones Cake',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for hatching potion if itemKey is provided', () => {
|
||||
data.itemKey = 'Golden';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Golden Hatching Potion',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for quest if itemKey is provided', () => {
|
||||
data.itemKey = 'atom1';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Attack of the Mundane, Part 1: Dish Disaster!',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for purchased spell if itemKey is provided', () => {
|
||||
data.itemKey = 'seafoam';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Seafoam',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends user data if provided', () => {
|
||||
const stats = {
|
||||
class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30,
|
||||
};
|
||||
const user = {
|
||||
stats,
|
||||
contributor: { level: 1 },
|
||||
purchased: { plan: { planId: 'foo-plan' } },
|
||||
flags: { tour: { intro: -2 } },
|
||||
habits: [{ _id: 'habit' }],
|
||||
dailys: [{ _id: 'daily' }],
|
||||
todos: [{ _id: 'todo' }],
|
||||
rewards: [{ _id: 'reward' }],
|
||||
balance: 12,
|
||||
loginIncentives: 1,
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
};
|
||||
|
||||
data.user = user;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_properties: {
|
||||
Class: 'wizard',
|
||||
Experience: 5,
|
||||
Gold: 23,
|
||||
Health: 10,
|
||||
Level: 4,
|
||||
Mana: 30,
|
||||
tutorialComplete: true,
|
||||
'Number Of Tasks': {
|
||||
habits: 1,
|
||||
dailys: 1,
|
||||
todos: 1,
|
||||
rewards: 1,
|
||||
},
|
||||
contributorLevel: 1,
|
||||
subscription: 'foo-plan',
|
||||
balance: 12,
|
||||
balanceGemAmount: 48,
|
||||
loginIncentives: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#trackPurchase', () => {
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
uuid: 'user-id',
|
||||
sku: 'paypal-checkout',
|
||||
paymentMethod: 'PayPal',
|
||||
itemPurchased: 'Gems',
|
||||
purchaseValue: 8,
|
||||
purchaseType: 'checkout',
|
||||
gift: false,
|
||||
quantity: 1,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
user: {
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
context('Amplitude', () => {
|
||||
it('calls out to amplitude', () => analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledOnce;
|
||||
}));
|
||||
|
||||
it('uses a dummy user id if none is provided', () => {
|
||||
delete data.uuid;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_id: 'no-user-id-was-provided',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('platform', () => {
|
||||
it('logs web platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-web' };
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Web',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs iOS platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-ios' };
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'iOS',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs Android platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-android' };
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Android',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs 3rd Party platform', () => {
|
||||
data.headers = { 'x-client': 'some-third-party' };
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: '3rd Party',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Unknown',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Operating System', () => {
|
||||
it('sets default', () => {
|
||||
data.headers = {
|
||||
'x-client': 'third-party',
|
||||
'user-agent': 'foo',
|
||||
};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Other',
|
||||
os_version: '0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets iOS', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-ios',
|
||||
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)',
|
||||
};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'iOS',
|
||||
os_version: '9.3.0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Android', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-android',
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
|
||||
};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Android',
|
||||
os_version: '4.0.4',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: undefined,
|
||||
os_version: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends details about purchase', () => analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
gift: false,
|
||||
itemPurchased: 'Gems',
|
||||
paymentMethod: 'PayPal',
|
||||
purchaseType: 'checkout',
|
||||
quantity: 1,
|
||||
sku: 'paypal-checkout',
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('sends user data if provided', () => {
|
||||
const stats = {
|
||||
class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30,
|
||||
};
|
||||
const user = {
|
||||
stats,
|
||||
contributor: { level: 1 },
|
||||
purchased: { plan: { planId: 'foo-plan' } },
|
||||
flags: { tour: { intro: -2 } },
|
||||
habits: [{ _id: 'habit' }],
|
||||
dailys: [{ _id: 'daily' }],
|
||||
todos: [{ _id: 'todo' }],
|
||||
rewards: [{ _id: 'reward' }],
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
};
|
||||
|
||||
data.user = user;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_properties: {
|
||||
Class: 'wizard',
|
||||
Experience: 5,
|
||||
Gold: 23,
|
||||
Health: 10,
|
||||
Level: 4,
|
||||
Mana: 30,
|
||||
tutorialComplete: true,
|
||||
'Number Of Tasks': {
|
||||
habits: 1,
|
||||
dailys: 1,
|
||||
todos: 1,
|
||||
rewards: 1,
|
||||
},
|
||||
contributorLevel: 1,
|
||||
subscription: 'foo-plan',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mockAnalyticsService', () => {
|
||||
it('has stubbed track method', () => {
|
||||
expect(analyticsService.mockAnalyticsService).to.respondTo('track');
|
||||
});
|
||||
|
||||
it('has stubbed trackPurchase method', () => {
|
||||
expect(analyticsService.mockAnalyticsService).to.respondTo('trackPurchase');
|
||||
});
|
||||
});
|
||||
});
|
||||
+136
-116
@@ -13,6 +13,7 @@ import { cron, cronWrapper } from '../../../../website/server/libs/cron';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import * as Tasks from '../../../../website/server/models/task';
|
||||
import common from '../../../../website/common';
|
||||
import * as analytics from '../../../../website/server/libs/analyticsService';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
|
||||
const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime();
|
||||
@@ -40,17 +41,20 @@ describe('cron', async () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
sinon.spy(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (clock !== null) clock.restore();
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
it('updates user.preferences.timezoneOffsetAtLastCron', async () => {
|
||||
const timezoneUtcOffsetFromUserPrefs = -1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, timezoneUtcOffsetFromUserPrefs,
|
||||
user, tasksByType, daysMissed, analytics, timezoneUtcOffsetFromUserPrefs,
|
||||
});
|
||||
|
||||
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(1);
|
||||
@@ -59,7 +63,7 @@ describe('cron', async () => {
|
||||
it('resets user.items.lastDrop.count', async () => {
|
||||
user.items.lastDrop.count = 4;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
});
|
||||
@@ -67,11 +71,26 @@ describe('cron', async () => {
|
||||
it('increments user cron count', async () => {
|
||||
const cronCountBefore = user.flags.cronCount;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.flags.cronCount).to.be.greaterThan(cronCountBefore);
|
||||
});
|
||||
|
||||
it('calls analytics', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(analytics.track.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('calls analytics when user is sleeping', async () => {
|
||||
user.preferences.sleep = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(analytics.track.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
describe('end of the month perks', async () => {
|
||||
beforeEach(async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
@@ -82,7 +101,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateUpdated = new Date('2018-12-11');
|
||||
clock = sinon.useFakeTimers(new Date('2019-01-29'));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.mysteryItems.length).to.eql(2);
|
||||
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
|
||||
@@ -93,7 +112,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateUpdated = new Date('2018-11-11');
|
||||
clock = sinon.useFakeTimers(new Date('2019-01-29'));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.mysteryItems.length).to.eql(4);
|
||||
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
|
||||
@@ -103,7 +122,7 @@ describe('cron', async () => {
|
||||
it('resets plan.gemsBought on a new month', async () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
@@ -112,7 +131,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
user.purchased.plan.dateUpdated = undefined;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
@@ -123,7 +142,7 @@ describe('cron', async () => {
|
||||
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(10);
|
||||
});
|
||||
@@ -131,7 +150,7 @@ describe('cron', async () => {
|
||||
it('resets plan.dateUpdated on a new month', async () => {
|
||||
const currentMonth = moment().startOf('month');
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(moment(user.purchased.plan.dateUpdated).startOf('month').isSame(currentMonth)).to.eql(true);
|
||||
});
|
||||
@@ -139,7 +158,7 @@ describe('cron', async () => {
|
||||
it('increments plan.consecutive.count', async () => {
|
||||
user.purchased.plan.consecutive.count = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(1);
|
||||
});
|
||||
@@ -147,7 +166,7 @@ describe('cron', async () => {
|
||||
it('increments plan.cumulativeCount', async () => {
|
||||
user.purchased.plan.cumulativeCount = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.cumulativeCount).to.equal(1);
|
||||
});
|
||||
@@ -156,7 +175,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(2, 'months').toDate();
|
||||
user.purchased.plan.consecutive.count = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(2);
|
||||
});
|
||||
@@ -165,7 +184,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(3, 'months').toDate();
|
||||
user.purchased.plan.cumulativeCount = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.cumulativeCount).to.equal(3);
|
||||
});
|
||||
@@ -177,7 +196,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.consecutive.trinkets = 1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
|
||||
@@ -187,7 +206,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.consecutive.gemCapExtra = 26;
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
|
||||
});
|
||||
@@ -195,7 +214,7 @@ describe('cron', async () => {
|
||||
it('does not reset plan stats if we are before the last day of the cancelled month', async () => {
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.customerId).to.exist;
|
||||
});
|
||||
@@ -206,7 +225,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.purchased.plan.customerId).to.not.exist;
|
||||
@@ -245,7 +264,7 @@ describe('cron', async () => {
|
||||
// Add 2 days so that we're sure we're not affected by any start-of-month effects
|
||||
// e.g., from time zone oddness.
|
||||
await cron({
|
||||
user: user1, tasksByType, daysMissed,
|
||||
user: user1, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user1.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user1.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
@@ -257,7 +276,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user1, tasksByType, daysMissed,
|
||||
user: user1, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user1.purchased.plan.consecutive.count).to.equal(10);
|
||||
expect(user1.purchased.plan.consecutive.trinkets).to.equal(11);
|
||||
@@ -292,7 +311,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3, tasksByType, daysMissed,
|
||||
user: user3, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user3.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
@@ -304,7 +323,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3, tasksByType, daysMissed,
|
||||
user: user3, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user3.purchased.plan.consecutive.count).to.equal(10);
|
||||
expect(user3.purchased.plan.consecutive.trinkets).to.equal(11);
|
||||
@@ -339,7 +358,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user6, tasksByType, daysMissed,
|
||||
user: user6, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user6.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
@@ -372,7 +391,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user12, tasksByType, daysMissed,
|
||||
user: user12, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user12.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user12.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
@@ -384,7 +403,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user12, tasksByType, daysMissed,
|
||||
user: user12, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user12.purchased.plan.consecutive.count).to.equal(10);
|
||||
expect(user12.purchased.plan.consecutive.trinkets).to.equal(11);
|
||||
@@ -420,7 +439,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3g, tasksByType, daysMissed,
|
||||
user: user3g, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user3g.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user3g.purchased.plan.cumulativeCount).to.equal(1);
|
||||
@@ -433,7 +452,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3g, tasksByType, daysMissed,
|
||||
user: user3g, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
// subscription has been erased by now
|
||||
expect(user3g.purchased.plan.consecutive.count).to.equal(0);
|
||||
@@ -452,7 +471,7 @@ describe('cron', async () => {
|
||||
it('resets plan.gemsBought on a new month', async () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
@@ -463,14 +482,14 @@ describe('cron', async () => {
|
||||
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(10);
|
||||
});
|
||||
|
||||
it('does not reset plan.dateUpdated on a new month', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.dateUpdated).to.be.empty;
|
||||
});
|
||||
@@ -478,7 +497,7 @@ describe('cron', async () => {
|
||||
it('does not increment plan.consecutive.count', async () => {
|
||||
user.purchased.plan.consecutive.count = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(0);
|
||||
});
|
||||
@@ -486,7 +505,7 @@ describe('cron', async () => {
|
||||
it('does not increment plan.cumulativeCount', async () => {
|
||||
user.purchased.plan.cumulativeCount = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.cumulativeCount).to.equal(0);
|
||||
});
|
||||
@@ -494,7 +513,7 @@ describe('cron', async () => {
|
||||
it('does not increment plan.consecutive.trinkets when user has reached a month that is a multiple of 3', async () => {
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.equal(0);
|
||||
});
|
||||
@@ -502,7 +521,7 @@ describe('cron', async () => {
|
||||
it('does not increment plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', async () => {
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(0);
|
||||
});
|
||||
@@ -511,7 +530,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.consecutive.gemCapExtra = 26;
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
|
||||
});
|
||||
@@ -519,7 +538,7 @@ describe('cron', async () => {
|
||||
it('does nothing to plan stats if we are before the last day of the cancelled month', async () => {
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.customerId).to.not.exist;
|
||||
});
|
||||
@@ -545,7 +564,7 @@ describe('cron', async () => {
|
||||
it('should make uncompleted todos redder', async () => {
|
||||
const valueBefore = tasksByType.todos[0].value;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.todos[0].value).to.be.lessThan(valueBefore);
|
||||
});
|
||||
@@ -554,7 +573,7 @@ describe('cron', async () => {
|
||||
tasksByType.todos[0].completed = true;
|
||||
const valueBefore = tasksByType.todos[0].value;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.todos[0].value).to.equal(valueBefore);
|
||||
});
|
||||
@@ -563,7 +582,7 @@ describe('cron', async () => {
|
||||
tasksByType.todos[0].completed = true;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.history.todos).to.be.lengthOf(1);
|
||||
@@ -589,7 +608,7 @@ describe('cron', async () => {
|
||||
expect(user.tasksOrder.todos).to.be.lengthOf(3);
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
// user.tasksOrder.todos should be filtered while tasks by type remains unchanged
|
||||
@@ -616,7 +635,7 @@ describe('cron', async () => {
|
||||
const original = user.tasksOrder.todos; // Preserve the original order
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
let listsAreEqual = true;
|
||||
@@ -656,7 +675,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].isDue).to.be.false;
|
||||
});
|
||||
@@ -667,7 +686,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().toDate();
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].isDue).to.exist;
|
||||
});
|
||||
@@ -677,14 +696,14 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].nextDue.length).to.eql(6);
|
||||
});
|
||||
|
||||
it('should add history', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
|
||||
});
|
||||
@@ -692,7 +711,7 @@ describe('cron', async () => {
|
||||
it('should set tasks completed to false', async () => {
|
||||
tasksByType.dailys[0].completed = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||
});
|
||||
@@ -701,7 +720,7 @@ describe('cron', async () => {
|
||||
user.preferences.sleep = true;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||
});
|
||||
@@ -710,7 +729,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
|
||||
tasksByType.dailys[0].completed = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
||||
});
|
||||
@@ -720,7 +739,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
|
||||
tasksByType.dailys[0].completed = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
||||
});
|
||||
@@ -730,7 +749,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
||||
});
|
||||
@@ -740,7 +759,7 @@ describe('cron', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.stats.hp).to.be.lessThan(hpBefore);
|
||||
});
|
||||
@@ -751,7 +770,7 @@ describe('cron', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.stats.hp).to.equal(hpBefore);
|
||||
});
|
||||
@@ -765,7 +784,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
cronOverride({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.hp).to.equal(hpBefore);
|
||||
@@ -778,7 +797,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.hp).to.equal(hpBefore);
|
||||
@@ -789,7 +808,7 @@ describe('cron', async () => {
|
||||
let hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
const hpDifferenceOfFullyIncompleteDaily = hpBefore - user.stats.hp;
|
||||
|
||||
@@ -797,7 +816,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test', completed: true });
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test2', completed: false });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
const hpDifferenceOfPartiallyIncompleteDaily = hpBefore - user.stats.hp;
|
||||
|
||||
@@ -810,7 +829,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
const progress = await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(progress.down).to.equal(-1);
|
||||
@@ -822,7 +841,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
const progress = await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(progress.down).to.equal(0);
|
||||
@@ -843,7 +862,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[1].frequency = 'daily';
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.hp).to.equal(48);
|
||||
@@ -867,7 +886,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].down = false;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].value).to.be.lessThan(1);
|
||||
@@ -878,7 +897,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].up = false;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].value).to.be.lessThan(1);
|
||||
@@ -890,7 +909,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].down = true;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].value).to.equal(1);
|
||||
@@ -909,7 +928,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].counterDown = 1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -922,7 +941,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].counterDown = 1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -936,7 +955,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -945,7 +964,7 @@ describe('cron', async () => {
|
||||
// should reset
|
||||
daysMissed = 8;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -969,7 +988,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -983,7 +1002,7 @@ describe('cron', async () => {
|
||||
|
||||
// should reset after user CDS
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1007,7 +1026,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1017,7 +1036,7 @@ describe('cron', async () => {
|
||||
// should reset
|
||||
daysMissed = 2;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1041,7 +1060,7 @@ describe('cron', async () => {
|
||||
|
||||
// should reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1065,7 +1084,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1079,7 +1098,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1088,7 +1107,7 @@ describe('cron', async () => {
|
||||
// should reset
|
||||
daysMissed = 32;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1113,7 +1132,7 @@ describe('cron', async () => {
|
||||
|
||||
// should reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1137,7 +1156,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1147,7 +1166,7 @@ describe('cron', async () => {
|
||||
// should reset
|
||||
daysMissed = 2;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1180,7 +1199,7 @@ describe('cron', async () => {
|
||||
user.stats.lvl = 2;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.history.exp).to.have.lengthOf(1);
|
||||
@@ -1193,7 +1212,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].isDue = true;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.achievements.perfect).to.equal(1);
|
||||
@@ -1205,7 +1224,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].isDue = false;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.achievements.perfect).to.equal(0);
|
||||
@@ -1219,7 +1238,7 @@ describe('cron', async () => {
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
@@ -1237,7 +1256,7 @@ describe('cron', async () => {
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
@@ -1261,7 +1280,7 @@ describe('cron', async () => {
|
||||
};
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
@@ -1288,7 +1307,7 @@ describe('cron', async () => {
|
||||
};
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
@@ -1314,7 +1333,7 @@ describe('cron', async () => {
|
||||
};
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
@@ -1341,7 +1360,7 @@ describe('cron', async () => {
|
||||
};
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
@@ -1362,7 +1381,7 @@ describe('cron', async () => {
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
cronOverride({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
@@ -1382,7 +1401,7 @@ describe('cron', async () => {
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
cronOverride({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
@@ -1401,7 +1420,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].completed = true;
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.stats.mp).to.be.greaterThan(mpBefore);
|
||||
|
||||
@@ -1417,7 +1436,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].completed = true;
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.stats.mp).to.equal(mpBefore);
|
||||
|
||||
@@ -1430,7 +1449,7 @@ describe('cron', async () => {
|
||||
user.stats.mp = 120;
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.stats.mp).to.equal(common.statsComputed(user).maxMP);
|
||||
|
||||
@@ -1463,7 +1482,7 @@ describe('cron', async () => {
|
||||
|
||||
it('resets user progress', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.party.quest.progress.up).to.equal(0);
|
||||
expect(user.party.quest.progress.down).to.equal(0);
|
||||
@@ -1472,7 +1491,7 @@ describe('cron', async () => {
|
||||
|
||||
it('applies the user progress', async () => {
|
||||
const progress = await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(progress.down).to.equal(-1);
|
||||
});
|
||||
@@ -1510,19 +1529,19 @@ describe('cron', async () => {
|
||||
describe('login incentives', async () => {
|
||||
it('increments incentive counter each cron', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(2);
|
||||
});
|
||||
|
||||
it('pushes a notification of the day\'s incentive each cron', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.notifications.length).to.eql(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
@@ -1530,13 +1549,13 @@ describe('cron', async () => {
|
||||
|
||||
it('replaces previous notifications', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
const filteredNotifications = user.notifications.filter(n => n.type === 'LOGIN_INCENTIVE');
|
||||
@@ -1547,7 +1566,7 @@ describe('cron', async () => {
|
||||
it('increments loginIncentives by 1 even if days are skipped in between', async () => {
|
||||
daysMissed = 3;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
});
|
||||
@@ -1555,14 +1574,14 @@ describe('cron', async () => {
|
||||
it('increments loginIncentives by 1 even if user is sleeping', async () => {
|
||||
user.preferences.sleep = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
});
|
||||
|
||||
it('awards user bard robes if login incentive is 1', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
expect(user.items.gear.owned.armor_special_bardRobes).to.eql(true);
|
||||
@@ -1572,7 +1591,7 @@ describe('cron', async () => {
|
||||
it('awards user incentive backgrounds if login incentive is 2', async () => {
|
||||
user.loginIncentives = 1;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(2);
|
||||
expect(user.purchased.background.blue).to.eql(true);
|
||||
@@ -1586,7 +1605,7 @@ describe('cron', async () => {
|
||||
it('awards user Bard Hat if login incentive is 3', async () => {
|
||||
user.loginIncentives = 2;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(3);
|
||||
expect(user.items.gear.owned.head_special_bardHat).to.eql(true);
|
||||
@@ -1596,7 +1615,7 @@ describe('cron', async () => {
|
||||
it('awards user RoyalPurple Hatching Potion if login incentive is 4', async () => {
|
||||
user.loginIncentives = 3;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(4);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1606,7 +1625,7 @@ describe('cron', async () => {
|
||||
it('awards user a Chocolate, Meat and Pink Contton Candy if login incentive is 5', async () => {
|
||||
user.loginIncentives = 4;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(5);
|
||||
|
||||
@@ -1620,7 +1639,7 @@ describe('cron', async () => {
|
||||
it('awards user moon quest if login incentive is 7', async () => {
|
||||
user.loginIncentives = 6;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(7);
|
||||
expect(user.items.quests.moon1).to.eql(1);
|
||||
@@ -1630,7 +1649,7 @@ describe('cron', async () => {
|
||||
it('awards user RoyalPurple Hatching Potion if login incentive is 10', async () => {
|
||||
user.loginIncentives = 9;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(10);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1640,7 +1659,7 @@ describe('cron', async () => {
|
||||
it('awards user a Strawberry, Patato and Blue Contton Candy if login incentive is 14', async () => {
|
||||
user.loginIncentives = 13;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(14);
|
||||
|
||||
@@ -1654,7 +1673,7 @@ describe('cron', async () => {
|
||||
it('awards user a bard instrument if login incentive is 18', async () => {
|
||||
user.loginIncentives = 17;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(18);
|
||||
expect(user.items.gear.owned.weapon_special_bardInstrument).to.eql(true);
|
||||
@@ -1664,7 +1683,7 @@ describe('cron', async () => {
|
||||
it('awards user second moon quest if login incentive is 22', async () => {
|
||||
user.loginIncentives = 21;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(22);
|
||||
expect(user.items.quests.moon2).to.eql(1);
|
||||
@@ -1674,7 +1693,7 @@ describe('cron', async () => {
|
||||
it('awards user a RoyalPurple hatching potion if login incentive is 26', async () => {
|
||||
user.loginIncentives = 25;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(26);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1684,7 +1703,7 @@ describe('cron', async () => {
|
||||
it('awards user Fish, Milk, Rotten Meat and Honey if login incentive is 30', async () => {
|
||||
user.loginIncentives = 29;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(30);
|
||||
|
||||
@@ -1699,7 +1718,7 @@ describe('cron', async () => {
|
||||
it('awards user a RoyalPurple hatching potion if login incentive is 35', async () => {
|
||||
user.loginIncentives = 34;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(35);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1709,7 +1728,7 @@ describe('cron', async () => {
|
||||
it('awards user the third moon quest if login incentive is 40', async () => {
|
||||
user.loginIncentives = 39;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(40);
|
||||
expect(user.items.quests.moon3).to.eql(1);
|
||||
@@ -1719,7 +1738,7 @@ describe('cron', async () => {
|
||||
it('awards user a RoyalPurple hatching potion if login incentive is 45', async () => {
|
||||
user.loginIncentives = 44;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(45);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1729,7 +1748,7 @@ describe('cron', async () => {
|
||||
it('awards user a saddle if login incentive is 50', async () => {
|
||||
user.loginIncentives = 49;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(50);
|
||||
expect(user.items.food.Saddle).to.eql(1);
|
||||
@@ -1747,6 +1766,7 @@ describe('cron wrapper', () => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
user = await res.locals.user.save();
|
||||
res.analytics = analytics;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-disable global-require */
|
||||
import got from 'got';
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import { TAVERN_ID } from '../../../../website/server/models/group';
|
||||
import { defer } from '../../../helpers/api-unit.helper';
|
||||
import worker from '../../../../website/server/libs/worker';
|
||||
|
||||
function getUser () {
|
||||
return {
|
||||
@@ -127,7 +127,7 @@ describe('emails', () => {
|
||||
let sendTxn = null;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(got, 'post').returns(defer().promise);
|
||||
sandbox.stub(worker, 'sendJob').returns(defer().promise);
|
||||
|
||||
const nconfGetStub = sandbox.stub(nconf, 'get');
|
||||
nconfGetStub.withArgs('IS_PROD').returns(true);
|
||||
@@ -149,13 +149,12 @@ describe('emails', () => {
|
||||
};
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(value => Array.isArray(value) && value[0].name === mailingInfo.name, 'matches mailing info array'),
|
||||
},
|
||||
expect(worker.sendJob).to.be.called;
|
||||
expect(worker.sendJob).to.be.calledWith('email', sinon.match({
|
||||
identifier: emailType,
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(value => Array.isArray(value) && value[0].name === mailingInfo.name, 'matches mailing info array'),
|
||||
},
|
||||
}));
|
||||
});
|
||||
@@ -168,7 +167,7 @@ describe('emails', () => {
|
||||
};
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(got.post).not.to.be.called;
|
||||
expect(worker.sendJob).not.to.be.called;
|
||||
});
|
||||
|
||||
it('throws error when mail target is only a string', async () => {
|
||||
@@ -233,13 +232,12 @@ describe('emails', () => {
|
||||
const mailingInfo = getUser();
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(val => val[0]._id === mailingInfo._id),
|
||||
},
|
||||
expect(worker.sendJob).to.be.called;
|
||||
expect(worker.sendJob).to.be.calledWith('email', sinon.match({
|
||||
identifier: emailType,
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(val => val[0]._id === mailingInfo._id),
|
||||
},
|
||||
}));
|
||||
});
|
||||
@@ -253,15 +251,14 @@ describe('emails', () => {
|
||||
const variables = [];
|
||||
|
||||
sendTxn(mailingInfo, emailType, variables);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
|
||||
personalVariables: sinon.match(value => value[0].rcpt === mailingInfo.email
|
||||
&& value[0].vars[0].name === 'RECIPIENT_NAME'
|
||||
expect(worker.sendJob).to.be.called;
|
||||
expect(worker.sendJob).to.be.calledWith('email', sinon.match({
|
||||
identifier: emailType,
|
||||
data: {
|
||||
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
|
||||
personalVariables: sinon.match(value => value[0].rcpt === mailingInfo.email
|
||||
&& value[0].vars[0].name === 'RECIPIENT_NAME'
|
||||
&& value[0].vars[1].name === 'RECIPIENT_UNSUB_URL', 'matches personal variables'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import { RegistrationEventModel } from '../../../../website/server/models/analytics/registrationEvent';
|
||||
import { SubscriptionEventModel } from '../../../../website/server/models/analytics/subscriptionEvent';
|
||||
|
||||
describe('localAnalytics', () => {
|
||||
let user;
|
||||
let localAnalytics;
|
||||
before(() => {
|
||||
const nconfGetStub = sandbox.stub(nconf, 'get');
|
||||
nconfGetStub.withArgs('ANALYTICS_DB').returns('analytics');
|
||||
nconfGetStub.withArgs('DISABLE_LOCAL_ANALYTICS').returns(false);
|
||||
localAnalytics = requireAgain('../../../../website/server/libs/localAnalytics');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User({
|
||||
auth: {
|
||||
local: {
|
||||
username: 'username',
|
||||
email: 'email@example.com',
|
||||
},
|
||||
},
|
||||
registeredThrough: 'habitica-web',
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackRegistrationEvent', () => {
|
||||
afterEach(async () => {
|
||||
await RegistrationEventModel.deleteMany({});
|
||||
});
|
||||
|
||||
it('creates a registration event when a user registers', async () => {
|
||||
user._id = '00000000-0000-0000-0000-000000000001';
|
||||
await localAnalytics.trackRegistrationEvent({ user, ipAddress: '127.0.0.1' });
|
||||
|
||||
const registrationEvents = await RegistrationEventModel.find({ userId: user._id });
|
||||
expect(registrationEvents).to.have.lengthOf(1);
|
||||
expect(registrationEvents[0]).to.have.property('userId', user._id);
|
||||
expect(registrationEvents[0]).to.have.property('ipAddress', '127.0.0.1');
|
||||
});
|
||||
|
||||
it('saves the correct data to the database', async () => {
|
||||
user._id = '00000000-0000-0000-0000-000000000002';
|
||||
user.auth.google = { id: 'abc', emails: [{ value: 'email@example.com' }] };
|
||||
await localAnalytics.trackRegistrationEvent({ user, ipAddress: '127.0.0.2' });
|
||||
|
||||
const registrationEvent = await RegistrationEventModel.findOne({ userId: user._id });
|
||||
expect(registrationEvent).to.have.property('userId', user._id);
|
||||
expect(registrationEvent).to.have.property('ipAddress', '127.0.0.2');
|
||||
expect(registrationEvent).to.have.property('authenticationMethod', 'google');
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackSubscriptionEvent', () => {
|
||||
afterEach(async () => {
|
||||
await SubscriptionEventModel.deleteMany({});
|
||||
});
|
||||
|
||||
it('creates a subscription event when a user subscribes', async () => {
|
||||
user._id = '00000000-0000-0000-0000-000000000003';
|
||||
await localAnalytics.trackSubscriptionEvent({
|
||||
eventType: 'subscribed',
|
||||
user,
|
||||
paymentMethod: 'stripe',
|
||||
customerId: 'cus_123',
|
||||
planId: 'plan_123',
|
||||
});
|
||||
|
||||
const subscriptionEvents = await SubscriptionEventModel.find({ userId: user._id });
|
||||
expect(subscriptionEvents).to.have.lengthOf(1);
|
||||
expect(subscriptionEvents[0]).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvents[0]).to.have.property('eventType', 'subscribed');
|
||||
expect(subscriptionEvents[0]).to.have.property('paymentMethod', 'stripe');
|
||||
expect(subscriptionEvents[0]).to.have.property('customerId', 'cus_123');
|
||||
expect(subscriptionEvents[0]).to.have.property('planId', 'plan_123');
|
||||
});
|
||||
|
||||
it('creates a subscription event with cancellation reason when a user cancels', async () => {
|
||||
user._id = '00000000-0000-0000-0000-000000000004';
|
||||
await localAnalytics.trackSubscriptionEvent({
|
||||
eventType: 'cancelled',
|
||||
user,
|
||||
paymentMethod: 'stripe',
|
||||
customerId: 'cus_456',
|
||||
planId: 'plan_456',
|
||||
cancellationReason: 'No longer needed',
|
||||
});
|
||||
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'cancelled');
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'stripe');
|
||||
expect(subscriptionEvent).to.have.property('customerId', 'cus_456');
|
||||
expect(subscriptionEvent).to.have.property('planId', 'plan_456');
|
||||
expect(subscriptionEvent).to.have.property('cancellationReason', 'No longer needed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import moment from 'moment';
|
||||
import * as sender from '../../../../../website/server/libs/email';
|
||||
import common from '../../../../../website/common';
|
||||
import api from '../../../../../website/server/libs/payments/payments';
|
||||
import * as analytics from '../../../../../website/server/libs/analyticsService';
|
||||
import * as notifications from '../../../../../website/server/libs/pushNotifications';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { translate as t } from '../../../../helpers/api-integration/v3';
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
import * as worldState from '../../../../../website/server/libs/worldState';
|
||||
import { TransactionModel } from '../../../../../website/server/models/transaction';
|
||||
import { REPEATING_EVENTS } from '../../../../../website/common/script/content/constants/events';
|
||||
import { SubscriptionEventModel } from '../../../../../website/server/models/analytics/subscriptionEvent';
|
||||
|
||||
describe('payments/index', () => {
|
||||
let user;
|
||||
@@ -36,6 +36,8 @@ describe('payments/index', () => {
|
||||
|
||||
sandbox.stub(sender, 'sendTxn');
|
||||
sandbox.stub(user, 'sendMessage');
|
||||
sandbox.stub(analytics.mockAnalyticsService, 'trackPurchase');
|
||||
sandbox.stub(analytics.mockAnalyticsService, 'track');
|
||||
sandbox.stub(notifications, 'sendNotification');
|
||||
|
||||
data = {
|
||||
@@ -95,16 +97,6 @@ describe('payments/index', () => {
|
||||
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
});
|
||||
|
||||
it('tracks subscription events', async () => {
|
||||
await api.createSubscription(data);
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: recipient._id });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'subscribed');
|
||||
expect(subscriptionEvent).to.have.property('userId', recipient._id);
|
||||
expect(subscriptionEvent).to.have.property('planId', 'basic_3mo');
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
|
||||
});
|
||||
|
||||
it('adds extra months to an existing subscription', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
|
||||
@@ -306,6 +298,28 @@ describe('payments/index', () => {
|
||||
expect(notifications.sendNotification).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('tracks subscription purchase as gift', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
|
||||
uuid: user._id,
|
||||
groupId: undefined,
|
||||
itemPurchased: 'Subscription',
|
||||
sku: 'payment method-subscription',
|
||||
purchaseType: 'subscribe',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: true,
|
||||
purchaseValue: 15,
|
||||
firstPurchase: true,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
context('No Active Promotion', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(worldState, 'getCurrentEventList').returns([]);
|
||||
@@ -441,16 +455,6 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('tracks subscription events', async () => {
|
||||
await api.createSubscription(data);
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvent).to.have.property('ipAddress');
|
||||
expect(subscriptionEvent).to.have.property('planId', 'basic_3mo');
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
|
||||
});
|
||||
|
||||
it('sets plan.dateCreated if it did not previously exist', async () => {
|
||||
expect(user.purchased.plan.dateCreated).to.not.exist;
|
||||
|
||||
@@ -539,24 +543,29 @@ describe('payments/index', () => {
|
||||
expect(sender.sendTxn).to.be.calledWith(data.user, 'subscription-begins');
|
||||
});
|
||||
|
||||
context('Upgrades subscription', () => {
|
||||
it('tracks subscription events', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
it('tracks subscription purchase', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom = { key: 'basic_earned' };
|
||||
await api.createSubscription(data);
|
||||
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id, planId: 'basic_6mo' });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'upgraded');
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
|
||||
uuid: user._id,
|
||||
groupId: undefined,
|
||||
itemPurchased: 'Subscription',
|
||||
sku: 'payment method-subscription',
|
||||
purchaseType: 'subscribe',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: false,
|
||||
purchaseValue: 15,
|
||||
firstPurchase: true,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
context('Upgrades subscription', () => {
|
||||
it('from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
@@ -599,23 +608,6 @@ describe('payments/index', () => {
|
||||
});
|
||||
|
||||
context('Downgrades subscription', () => {
|
||||
it('tracks subscription events', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
data.sub.key = 'basic_earned';
|
||||
data.updatedFrom = { key: 'basic_6mo' };
|
||||
await api.createSubscription(data);
|
||||
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id, planId: 'basic_earned' });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'downgraded');
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
|
||||
});
|
||||
|
||||
it('from basic_6mo to basic_earned', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
@@ -1144,15 +1136,6 @@ describe('payments/index', () => {
|
||||
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
|
||||
});
|
||||
|
||||
it('tracks subscription events', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'cancelled');
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
});
|
||||
|
||||
it('adds extraMonths to dateTerminated value', async () => {
|
||||
user.purchased.plan.extraMonths = 2;
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/* eslint-disable global-require */
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateNext,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('analytics middleware', () => {
|
||||
let res; let req; let
|
||||
next;
|
||||
const pathToAnalyticsMiddleware = '../../../../website/server/middlewares/analytics';
|
||||
|
||||
beforeEach(() => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
next = generateNext();
|
||||
});
|
||||
|
||||
it('attaches analytics object to res', () => {
|
||||
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
|
||||
|
||||
attachAnalytics(req, res, next);
|
||||
|
||||
expect(res.analytics).to.exist;
|
||||
});
|
||||
|
||||
it('attaches stubbed methods for non-prod environments', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
||||
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
|
||||
|
||||
attachAnalytics(req, res, next);
|
||||
|
||||
expect(res.analytics.track).to.eql(analyticsService.mockAnalyticsService.track);
|
||||
expect(res.analytics.trackPurchase).to.eql(analyticsService.mockAnalyticsService.trackPurchase);
|
||||
});
|
||||
|
||||
it('attaches real methods for prod environments', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
|
||||
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
|
||||
|
||||
attachAnalytics(req, res, next);
|
||||
|
||||
expect(res.analytics.track).to.eql(analyticsService.track);
|
||||
expect(res.analytics.trackPurchase).to.eql(analyticsService.trackPurchase);
|
||||
});
|
||||
});
|
||||
@@ -32,8 +32,7 @@ describe('rateLimiter middleware', () => {
|
||||
|
||||
it('is disabled when the env var is not defined', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns(undefined);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
@@ -44,8 +43,7 @@ describe('rateLimiter middleware', () => {
|
||||
|
||||
it('is disabled when the env var is an not "true"', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('false');
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
@@ -57,8 +55,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('does not throw when there are available points', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
@@ -80,8 +77,7 @@ describe('rateLimiter middleware', () => {
|
||||
sandbox.stub(RateLimiterMemory.prototype, 'consume')
|
||||
.returns(Promise.reject(new Error('Unknown error.')));
|
||||
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
@@ -96,9 +92,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('does not throw when LIVELINESS_PROBE_KEY is correct', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.query.liveliness = 'abc';
|
||||
await attachRateLimiter(req, res, next);
|
||||
@@ -113,8 +107,7 @@ describe('rateLimiter middleware', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.query.liveliness = 'das';
|
||||
await attachRateLimiter(req, res, next);
|
||||
@@ -131,8 +124,7 @@ describe('rateLimiter middleware', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns(undefined);
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
@@ -148,8 +140,7 @@ describe('rateLimiter middleware', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.query.liveliness = '';
|
||||
await attachRateLimiter(req, res, next);
|
||||
@@ -165,8 +156,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('throws when there are no available points remaining', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
// call for 31 times
|
||||
for (let i = 0; i < 31; i += 1) {
|
||||
@@ -190,8 +180,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('uses the user id if supplied or the ip address', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.ip = 1;
|
||||
await attachRateLimiter(req, res, next);
|
||||
@@ -221,8 +210,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('applies increased cost for registration calls with and without user id', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_REGISTRATION_COST').returns(3);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
req.path = '/api/v4/user/auth/local/register';
|
||||
|
||||
req.ip = 1;
|
||||
@@ -253,8 +241,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('applies increased cost for unauthenticated API calls', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(10);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.ip = 1;
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('POST /analytics/track/:eventName', () => {
|
||||
it('calls res.analytics', async () => {
|
||||
const user = await generateUser();
|
||||
sandbox.spy(analytics, 'track');
|
||||
|
||||
const requestWithHeaders = requester(user, { 'x-client': 'habitica-web' });
|
||||
await requestWithHeaders.post('/analytics/track/eventName', { data: 'example' }, { 'x-client': 'habitica-web' });
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
expect(analytics.track).to.be.calledWith('eventName', sandbox.match({ data: 'example' }));
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
});
|
||||
@@ -91,23 +91,6 @@ describe('POST /groups/:groupId/quests/accept', () => {
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.false;
|
||||
});
|
||||
|
||||
it('heals stuck RSVPNeeded when group already has the user accepted', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
await partyMembers[0].updateOne({ 'party.quest.RSVPNeeded': true });
|
||||
await partyMembers[0].sync();
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.true;
|
||||
|
||||
const res = await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
expect(res).to.exist;
|
||||
|
||||
await partyMembers[0].sync();
|
||||
await questingGroup.sync();
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.equal(false);
|
||||
expect(questingGroup.quest.members[partyMembers[0]._id]).to.equal(true);
|
||||
});
|
||||
|
||||
it('does not accept invite for a quest already underway', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
@@ -193,23 +193,6 @@ describe('POST /groups/:groupId/quests/force-start', () => {
|
||||
expect(questingGroup.quest.members[notInPartyUser._id]).to.not.exist;
|
||||
});
|
||||
|
||||
it('removes users who have been deleted from quest.members', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
await partyMembers[0].del('/user', {
|
||||
password: 'password',
|
||||
});
|
||||
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
|
||||
|
||||
await sleep(0.5);
|
||||
|
||||
await questingGroup.sync();
|
||||
|
||||
expect(questingGroup.quest.members[partyMembers[0]._id]).to.not.exist;
|
||||
});
|
||||
|
||||
it('removes users who don\'t have true value in quest.members from quest.members', async () => {
|
||||
const partyMemberThatRejects = partyMembers[1];
|
||||
const partyMemberThatIgnores = partyMembers[2];
|
||||
|
||||
@@ -100,23 +100,6 @@ describe('POST /groups/:groupId/quests/reject', () => {
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.false;
|
||||
});
|
||||
|
||||
it('heals stuck RSVPNeeded when group already has the user rejected', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/reject`);
|
||||
|
||||
await partyMembers[0].updateOne({ 'party.quest.RSVPNeeded': true });
|
||||
await partyMembers[0].sync();
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.true;
|
||||
|
||||
const res = await partyMembers[0].post(`/groups/${questingGroup._id}/quests/reject`);
|
||||
expect(res).to.exist;
|
||||
|
||||
await partyMembers[0].sync();
|
||||
await questingGroup.sync();
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.equal(false);
|
||||
expect(questingGroup.quest.members[partyMembers[0]._id]).to.equal(false);
|
||||
});
|
||||
|
||||
it('return an error when a user rejects an invite already accepted', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import {
|
||||
each,
|
||||
map,
|
||||
} from 'lodash';
|
||||
import {
|
||||
checkExistence,
|
||||
createAndPopulateGroup,
|
||||
generateGroup,
|
||||
generateUser,
|
||||
generateChallenge,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import {
|
||||
@@ -15,6 +9,7 @@ import {
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../website/server/libs/password';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
import sendJob from '../../../../../website/server/libs/worker';
|
||||
|
||||
const DELETE_CONFIRMATION = 'DELETE';
|
||||
|
||||
@@ -47,12 +42,13 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes the user', async () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
|
||||
it('sends deletion job to worker', async () => {
|
||||
const workerStub = sandbox.stub(sendJob, 'sendJob');
|
||||
await user.del('/user', {
|
||||
password,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
expect(workerStub).to.be.calledOnce;
|
||||
workerStub.restore();
|
||||
});
|
||||
|
||||
it('returns an error if excessive feedback is supplied', async () => {
|
||||
@@ -84,53 +80,6 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes the user\'s tasks', async () => {
|
||||
await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
await user.sync();
|
||||
|
||||
// gets the user's tasks ids
|
||||
const ids = [];
|
||||
each(user.tasksOrder, idsForOrder => {
|
||||
ids.push(...idsForOrder);
|
||||
});
|
||||
|
||||
expect(ids.length).to.be.above(0); // make sure the user has some task to delete
|
||||
|
||||
await user.del('/user', {
|
||||
password,
|
||||
});
|
||||
|
||||
await Promise.all(map(ids, id => expect(checkExistence('tasks', id)).to.eventually.eql(false)));
|
||||
});
|
||||
|
||||
it('reduces memberCount in challenges user is linked to', async () => {
|
||||
const populatedGroup = await createAndPopulateGroup({
|
||||
members: 2,
|
||||
});
|
||||
|
||||
const { group } = populatedGroup;
|
||||
const authorizedUser = populatedGroup.members[1];
|
||||
|
||||
const challenge = await generateChallenge(populatedGroup.groupLeader, group);
|
||||
await populatedGroup.groupLeader.post(`/challenges/${challenge._id}/join`);
|
||||
await authorizedUser.post(`/challenges/${challenge._id}/join`);
|
||||
|
||||
await challenge.sync();
|
||||
|
||||
expect(challenge.memberCount).to.eql(2);
|
||||
|
||||
await authorizedUser.del('/user', {
|
||||
password,
|
||||
});
|
||||
|
||||
await challenge.sync();
|
||||
|
||||
expect(challenge.memberCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('sends feedback to the admin email', async () => {
|
||||
sandbox.spy(email, 'sendTxn');
|
||||
|
||||
@@ -158,10 +107,10 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
|
||||
it('deletes the user with a legacy sha1 password', async () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
|
||||
const textPassword = 'mySecretPassword';
|
||||
const salt = sha1MakeSalt();
|
||||
const sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
const workerStub = sandbox.stub(sendJob, 'sendJob');
|
||||
|
||||
await user.updateOne({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
@@ -179,7 +128,8 @@ describe('DELETE /user', () => {
|
||||
await user.del('/user', {
|
||||
password: textPassword,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
expect(workerStub).to.be.calledOnce;
|
||||
workerStub.restore();
|
||||
});
|
||||
|
||||
context('last member of a party', () => {
|
||||
@@ -213,11 +163,12 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
|
||||
it('deletes a Google user', async () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
|
||||
const workerStub = sandbox.stub(sendJob, 'sendJob');
|
||||
await user.del('/user', {
|
||||
password: DELETE_CONFIRMATION,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
expect(workerStub).to.be.calledOnce;
|
||||
workerStub.restore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -232,12 +183,13 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes a Apple user', async () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
|
||||
it('deletes an Apple user', async () => {
|
||||
const workerStub = sandbox.stub(sendJob, 'sendJob');
|
||||
await user.del('/user', {
|
||||
password: DELETE_CONFIRMATION,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
expect(workerStub).to.be.calledOnce;
|
||||
workerStub.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('POST /user/sleep', () => {
|
||||
let user;
|
||||
@@ -22,4 +23,15 @@ describe('POST /user/sleep', () => {
|
||||
await user.sync();
|
||||
expect(user.preferences.sleep).to.be.false;
|
||||
});
|
||||
|
||||
it('sends sleep status to analytics service', async () => {
|
||||
sandbox.spy(analytics, 'track');
|
||||
|
||||
await user.post('/user/sleep');
|
||||
await user.sync();
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
expect(analytics.track).to.be.calledWith('sleep', sandbox.match.has('status', user.preferences.sleep));
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import { ApiUser } from '../../../../../helpers/api-integration/api-classes';
|
||||
import { encrypt } from '../../../../../../website/server/libs/encryption';
|
||||
import { RegistrationEventModel } from '../../../../../../website/server/models/analytics/registrationEvent';
|
||||
|
||||
function generateRandomUserName () {
|
||||
return (Date.now() + uuid()).substring(0, 20);
|
||||
@@ -42,25 +41,6 @@ describe('POST /user/auth/local/register', () => {
|
||||
expect(user.newUser).to.eql(true);
|
||||
});
|
||||
|
||||
it('tracks a registration event', async () => {
|
||||
const username = generateRandomUserName();
|
||||
const email = `${username}@example.com`;
|
||||
const password = 'password';
|
||||
|
||||
const user = await api.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
|
||||
const registrationEvent = await RegistrationEventModel.findOne({ userId: user._id });
|
||||
expect(registrationEvent).to.exist;
|
||||
expect(registrationEvent).to.have.property('userId', user._id);
|
||||
expect(registrationEvent).to.have.property('ipAddress');
|
||||
expect(registrationEvent).to.have.property('authenticationMethod', 'local');
|
||||
});
|
||||
|
||||
it('registers a new user and sets verifiedUsername to true', async () => {
|
||||
const username = generateRandomUserName();
|
||||
const email = `${username}@example.com`;
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
getProperty,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import apiErrorMessages from '../../../../../../website/common/script/errors/apiErrorMessages';
|
||||
import { RegistrationEventModel } from '../../../../../../website/server/models/analytics/registrationEvent';
|
||||
|
||||
describe('POST /user/auth/social', () => {
|
||||
let api;
|
||||
@@ -66,19 +65,6 @@ describe('POST /user/auth/social', () => {
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
|
||||
});
|
||||
|
||||
it('tracks a registration event', async () => {
|
||||
const socialUser = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
const registrationEvent = await RegistrationEventModel.findOne({ userId: socialUser.id });
|
||||
expect(registrationEvent).to.exist;
|
||||
expect(registrationEvent).to.have.property('userId', socialUser.id);
|
||||
expect(registrationEvent).to.have.property('ipAddress');
|
||||
expect(registrationEvent).to.have.property('authenticationMethod', 'google');
|
||||
});
|
||||
|
||||
it('includes sanitized version of provided username', async () => {
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
@@ -245,17 +231,6 @@ describe('POST /user/auth/social', () => {
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('does not track a registration event for existing users', async () => {
|
||||
const beforeEvents = await RegistrationEventModel.find({ userId: user._id });
|
||||
await user.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
const registrationEvents = await RegistrationEventModel.find({ userId: user._id });
|
||||
expect(registrationEvents).to.have.lengthOf(beforeEvents.length);
|
||||
});
|
||||
|
||||
it('does not log into other account if social auth already exists', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
|
||||
@@ -13,6 +13,7 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
|
||||
describe('shared.ops.buy', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser({
|
||||
@@ -31,6 +32,12 @@ describe('shared.ops.buy', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
it('returns error when key is not provided', async () => {
|
||||
@@ -44,8 +51,10 @@ describe('shared.ops.buy', () => {
|
||||
|
||||
it('buys health potion', async () => {
|
||||
user.stats.hp = 30;
|
||||
await buy(user, { params: { key: 'potion' } });
|
||||
await buy(user, { params: { key: 'potion' } }, analytics);
|
||||
expect(user.stats.hp).to.eql(45);
|
||||
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('adds equipment to inventory', async () => {
|
||||
|
||||
@@ -29,9 +29,10 @@ describe('shared.ops.buyArmoire', () => {
|
||||
const YIELD_EQUIPMENT = 0.5;
|
||||
const YIELD_FOOD = 0.7;
|
||||
const YIELD_EXP = 0.9;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyArmoire (_user, _req) {
|
||||
const buyOp = new BuyArmoireOperation(_user, _req);
|
||||
async function buyArmoire (_user, _req, _analytics) {
|
||||
const buyOp = new BuyArmoireOperation(_user, _req, _analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
@@ -49,10 +50,12 @@ describe('shared.ops.buyArmoire', () => {
|
||||
user.items.food = {};
|
||||
|
||||
sandbox.stub(randomValFns, 'trueRandom');
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
randomValFns.trueRandom.restore();
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('failure conditions', () => {
|
||||
@@ -144,7 +147,7 @@ describe('shared.ops.buyArmoire', () => {
|
||||
|
||||
expect(_.size(user.items.gear.owned)).to.equal(2);
|
||||
|
||||
await buyArmoire(user, {});
|
||||
await buyArmoire(user, {}, analytics);
|
||||
|
||||
expect(_.size(user.items.gear.owned)).to.equal(3);
|
||||
|
||||
@@ -152,6 +155,7 @@ describe('shared.ops.buyArmoire', () => {
|
||||
|
||||
expect(armoireCount).to.eql(_.size(getFullArmoire()) - 2);
|
||||
expect(user.stats.gp).to.eql(100);
|
||||
expect(analytics.track).to.be.calledTwice;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import sinon from 'sinon'; // eslint-disable-line no-shadow
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/common.helper';
|
||||
@@ -10,14 +11,15 @@ import i18n from '../../../../website/common/script/i18n';
|
||||
import { BuyGemOperation } from '../../../../website/common/script/ops/buy/buyGem';
|
||||
import planGemLimits from '../../../../website/common/script/libs/planGemLimits';
|
||||
|
||||
async function buyGem (user, req) {
|
||||
const buyOp = new BuyGemOperation(user, req);
|
||||
async function buyGem (user, req, analytics) {
|
||||
const buyOp = new BuyGemOperation(user, req, analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
describe('shared.ops.buyGem', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
const goldPoints = 40;
|
||||
const gemsBought = 40;
|
||||
const userGemAmount = 10;
|
||||
@@ -33,16 +35,23 @@ describe('shared.ops.buyGem', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('Gems', () => {
|
||||
it('purchases gems', async () => {
|
||||
const [, message] = await buyGem(user, { params: { type: 'gems', key: 'gem' } });
|
||||
const [, message] = await buyGem(user, { params: { type: 'gems', key: 'gem' } }, analytics);
|
||||
|
||||
expect(message).to.equal(i18n.t('plusGem', { count: 1 }));
|
||||
expect(user.balance).to.equal(userGemAmount + 0.25);
|
||||
expect(user.purchased.plan.gemsBought).to.equal(1);
|
||||
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('purchases gems with a different language than the default', async () => {
|
||||
|
||||
@@ -10,9 +10,10 @@ import i18n from '../../../../website/common/script/i18n';
|
||||
|
||||
describe('shared.ops.buyHealthPotion', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyHealthPotion (_user, _req) {
|
||||
const buyOp = new BuyHealthPotionOperation(_user, _req);
|
||||
async function buyHealthPotion (_user, _req, _analytics) {
|
||||
const buyOp = new BuyHealthPotionOperation(_user, _req, _analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
@@ -31,13 +32,19 @@ describe('shared.ops.buyHealthPotion', () => {
|
||||
},
|
||||
stats: { gp: 200 },
|
||||
});
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('Potion', () => {
|
||||
it('recovers 15 hp', async () => {
|
||||
user.stats.hp = 30;
|
||||
await buyHealthPotion(user, {});
|
||||
await buyHealthPotion(user, {}, analytics);
|
||||
expect(user.stats.hp).to.eql(45);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('does not increase hp above 50', async () => {
|
||||
|
||||
@@ -13,14 +13,15 @@ import {
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import { errorMessage } from '../../../../website/common/script/libs/errorMessage';
|
||||
|
||||
async function buyGear (user, req) {
|
||||
const buyOp = new BuyMarketGearOperation(user, req);
|
||||
async function buyGear (user, req, analytics) {
|
||||
const buyOp = new BuyMarketGearOperation(user, req, analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
describe('shared.ops.buyMarketGear', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -46,12 +47,14 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
sinon.stub(shared, 'randomVal');
|
||||
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
|
||||
sinon.stub(shared.fns, 'predictableRandom');
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shared.randomVal.restore();
|
||||
shared.fns.predictableRandom.restore();
|
||||
shared.onboarding.checkOnboardingStatus.restore();
|
||||
analytics.track.restore();
|
||||
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
@@ -62,7 +65,7 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
it('adds equipment to inventory', async () => {
|
||||
user.stats.gp = 31;
|
||||
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } });
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
|
||||
|
||||
expect(user.items.gear.owned).to.eql({
|
||||
weapon_warrior_0: true,
|
||||
@@ -89,12 +92,13 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
eyewear_special_whiteHalfMoon: true,
|
||||
eyewear_special_yellowHalfMoon: true,
|
||||
});
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('adds the onboarding achievement to the user and checks the onboarding status', async () => {
|
||||
user.stats.gp = 31;
|
||||
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } });
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
|
||||
|
||||
expect(user.addAchievement).to.be.calledOnce;
|
||||
expect(user.addAchievement).to.be.calledWith('purchasedEquipment');
|
||||
@@ -107,7 +111,7 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
user.stats.gp = 31;
|
||||
user.achievements.purchasedEquipment = true;
|
||||
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } });
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
|
||||
|
||||
expect(user.addAchievement).to.not.be.called;
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
|
||||
describe('shared.ops.buyMysterySet', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -26,9 +27,11 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
@@ -90,7 +93,7 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
context('successful purchases', () => {
|
||||
it('buys Steampunk Accessories Set', async () => {
|
||||
user.purchased.plan.consecutive.trinkets = 1;
|
||||
await buyMysterySet(user, { params: { key: '301404' } });
|
||||
await buyMysterySet(user, { params: { key: '301404' } }, analytics);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.items.gear.owned).to.have.property('weapon_warrior_0', true);
|
||||
@@ -103,7 +106,7 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
it('buys mystery set if it is available', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-16'));
|
||||
user.purchased.plan.consecutive.trinkets = 1;
|
||||
await buyMysterySet(user, { params: { key: '201601' } });
|
||||
await buyMysterySet(user, { params: { key: '201601' } }, analytics);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.items.gear.owned).to.have.property('shield_mystery_201601', true);
|
||||
|
||||
@@ -12,9 +12,10 @@ describe('shared.ops.buyQuestGems', () => {
|
||||
let user;
|
||||
let clock;
|
||||
const goldPoints = 40;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyQuest (_user, _req) {
|
||||
const buyOp = new BuyQuestWithGemOperation(_user, _req);
|
||||
async function buyQuest (_user, _req, _analytics) {
|
||||
const buyOp = new BuyQuestWithGemOperation(_user, _req, _analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
@@ -24,11 +25,13 @@ describe('shared.ops.buyQuestGems', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(analytics, 'track');
|
||||
sinon.spy(pinnedGearUtils, 'removeItemByPath');
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-16'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
pinnedGearUtils.removeItemByPath.restore();
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
@@ -12,15 +12,21 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
|
||||
describe('shared.ops.buyQuest', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyQuest (_user, _req) {
|
||||
const buyOp = new BuyQuestWithGoldOperation(_user, _req);
|
||||
async function buyQuest (_user, _req, _analytics) {
|
||||
const buyOp = new BuyQuestWithGoldOperation(_user, _req, _analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
it('buys a Quest scroll', async () => {
|
||||
@@ -29,11 +35,12 @@ describe('shared.ops.buyQuest', () => {
|
||||
params: {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
});
|
||||
}, analytics);
|
||||
expect(user.items.quests).to.eql({
|
||||
dilatoryDistress1: 1,
|
||||
});
|
||||
expect(user.stats.gp).to.equal(5);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('if a user\'s count of a quest scroll is negative, it will be reset to 0 before incrementing when they buy a new one.', async () => {
|
||||
@@ -42,9 +49,10 @@ describe('shared.ops.buyQuest', () => {
|
||||
user.items.quests[key] = -1;
|
||||
await buyQuest(user, {
|
||||
params: { key },
|
||||
});
|
||||
}, analytics);
|
||||
expect(user.items.quests[key]).to.equal(1);
|
||||
expect(user.stats.gp).to.equal(5);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('buys a Quest scroll with the right quantity if a string is passed for quantity', async () => {
|
||||
@@ -53,13 +61,13 @@ describe('shared.ops.buyQuest', () => {
|
||||
params: {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
});
|
||||
}, analytics);
|
||||
await buyQuest(user, {
|
||||
params: {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
quantity: '3',
|
||||
});
|
||||
}, analytics);
|
||||
|
||||
expect(user.items.quests).to.eql({
|
||||
dilatoryDistress1: 4,
|
||||
@@ -74,7 +82,7 @@ describe('shared.ops.buyQuest', () => {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
quantity: 'a',
|
||||
});
|
||||
}, analytics);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
@@ -179,11 +187,12 @@ describe('shared.ops.buyQuest', () => {
|
||||
params: {
|
||||
key: 'dilatoryDistress3',
|
||||
},
|
||||
});
|
||||
}, analytics);
|
||||
|
||||
expect(user.items.quests).to.eql({
|
||||
dilatoryDistress3: 1,
|
||||
});
|
||||
expect(user.stats.gp).to.equal(100);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,17 +14,20 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
describe('shared.ops.buySpecialSpell', () => {
|
||||
let user;
|
||||
let clock;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buySpecialSpell (_user, _req) {
|
||||
const buyOp = new BuySpellOperation(_user, _req);
|
||||
async function buySpecialSpell (_user, _req, _analytics) {
|
||||
const buyOp = new BuySpellOperation(_user, _req, _analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
@@ -75,7 +78,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
params: {
|
||||
key: 'thankyou',
|
||||
},
|
||||
});
|
||||
}, analytics);
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.thankyou).to.equal(1);
|
||||
@@ -86,6 +89,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('buys a limited card when it is available', async () => {
|
||||
@@ -97,7 +101,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
params: {
|
||||
key: 'nye',
|
||||
},
|
||||
});
|
||||
}, analytics);
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.nye).to.equal(1);
|
||||
@@ -108,6 +112,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('throws an error if the card is not currently available', async () => {
|
||||
@@ -135,7 +140,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
params: {
|
||||
key: 'seafoam',
|
||||
},
|
||||
});
|
||||
}, analytics);
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.seafoam).to.equal(1);
|
||||
@@ -146,6 +151,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('throws an error if the spell is not currently available', async () => {
|
||||
|
||||
@@ -13,15 +13,21 @@ import { BuyHourglassMountOperation } from '../../../../website/common/script/op
|
||||
|
||||
describe('common.ops.hourglassPurchase', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyMount (_user, _req) {
|
||||
const buyOp = new BuyHourglassMountOperation(_user, _req);
|
||||
async function buyMount (_user, _req, _analytics) {
|
||||
const buyOp = new BuyHourglassMountOperation(_user, _req, _analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('failure conditions', () => {
|
||||
@@ -125,11 +131,12 @@ describe('common.ops.hourglassPurchase', () => {
|
||||
it('buys a pet', async () => {
|
||||
user.purchased.plan.consecutive.trinkets = 2;
|
||||
|
||||
const [, message] = await hourglassPurchase(user, { params: { type: 'pets', key: 'MantisShrimp-Base' } });
|
||||
const [, message] = await hourglassPurchase(user, { params: { type: 'pets', key: 'MantisShrimp-Base' } }, analytics);
|
||||
|
||||
expect(message).to.eql(i18n.t('hourglassPurchase'));
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(user.items.pets).to.eql({ 'MantisShrimp-Base': 5 });
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('buys a mount', async () => {
|
||||
|
||||
@@ -17,17 +17,20 @@ describe('shared.ops.purchase', () => {
|
||||
let user;
|
||||
let clock;
|
||||
const goldPoints = 40;
|
||||
const analytics = { track () {} };
|
||||
|
||||
before(() => {
|
||||
user = generateUser({ 'stats.class': 'rogue' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(analytics, 'track');
|
||||
sinon.spy(pinnedGearUtils, 'removeItemByPath');
|
||||
clock = sandbox.useFakeTimers(new Date('2024-01-10'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
pinnedGearUtils.removeItemByPath.restore();
|
||||
clock.restore();
|
||||
});
|
||||
@@ -184,10 +187,11 @@ describe('shared.ops.purchase', () => {
|
||||
const type = 'eggs';
|
||||
const key = 'Wolf';
|
||||
|
||||
await purchase(user, { params: { type, key } });
|
||||
await purchase(user, { params: { type, key } }, analytics);
|
||||
|
||||
expect(user.items[type][key]).to.equal(1);
|
||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('purchases hatchingPotions', async () => {
|
||||
@@ -328,7 +332,7 @@ describe('shared.ops.purchase', () => {
|
||||
const key = 'Wolf';
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: 'jamboree' });
|
||||
await purchase(user, { params: { type, key }, quantity: 'jamboree' }, analytics);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
@@ -341,7 +345,7 @@ describe('shared.ops.purchase', () => {
|
||||
user.balance = 10;
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: -2 });
|
||||
await purchase(user, { params: { type, key }, quantity: -2 }, analytics);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
@@ -354,7 +358,7 @@ describe('shared.ops.purchase', () => {
|
||||
user.balance = 10;
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: 2.9 });
|
||||
await purchase(user, { params: { type, key }, quantity: 2.9 }, analytics);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
|
||||
@@ -54,4 +54,19 @@ describe('armoire', () => {
|
||||
const febuaryItems = armoire.all;
|
||||
expect(febuaryItems.length).to.equal(384);
|
||||
});
|
||||
|
||||
it('sets have at least 2 items', () => {
|
||||
const setMap = {};
|
||||
forEach(armoire.all, item => {
|
||||
// Gotta have one outlier
|
||||
if (!item.set || item.set.startsWith('armoire-')) return;
|
||||
if (setMap[item.set] === undefined) {
|
||||
setMap[item.set] = 0;
|
||||
}
|
||||
setMap[item.set] += 1;
|
||||
});
|
||||
Object.keys(setMap).forEach(set => {
|
||||
expect(setMap[set], set).to.be.at.least(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,7 @@ function _requestMaker (user, method, additionalSets = {}) {
|
||||
|| route.indexOf('/paypal') === 0
|
||||
|| route.indexOf('/amazon') === 0
|
||||
|| route.indexOf('/stripe') === 0
|
||||
|| route.indexOf('/analytics') === 0
|
||||
) {
|
||||
url += `${route}`;
|
||||
} else {
|
||||
|
||||
@@ -12,12 +12,20 @@ module.exports = {
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
// TODO find a way to let eslint understand webpack aliases
|
||||
'import/no-unresolved': 'off',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'import/extensions': 'off',
|
||||
'prefer-regex-literals': 'warn',
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/no-mutating-props': 'warn',
|
||||
// this creates issues with the current way we have to push the process.env vars to webpack
|
||||
// https://github.com/eslint/eslint/issues/14918
|
||||
// https://github.com/webpack/webpack/issues/5392
|
||||
// off for now, because any eslint --fix will then still do it anyway
|
||||
// maybe this can be turned on again once we switch to newer vue/vite
|
||||
// Important! process.env.XYZ should not be destructured
|
||||
'prefer-destructuring': 'off',
|
||||
'vue/html-self-closing': ['error', {
|
||||
html: {
|
||||
void: 'never',
|
||||
|
||||
Generated
+589
-20
@@ -41,6 +41,7 @@
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vue": "^2.7.10",
|
||||
"vue-fragment": "^1.6.0",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuedraggable": "^2.24.3",
|
||||
@@ -54,7 +55,9 @@
|
||||
"jsdom": "^26.0.0",
|
||||
"mocha": "^11.1.0",
|
||||
"playwright": "^1.50.1",
|
||||
"vitest": "^3.0.5"
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"vitest": "^3.0.5",
|
||||
"webpack": "^5.94.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/analytics-connector": {
|
||||
@@ -2108,9 +2111,8 @@
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
@@ -3632,12 +3634,41 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*",
|
||||
"@types/json-schema": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/eslint-scope": {
|
||||
"version": "3.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
|
||||
"integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/eslint": "*",
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json5": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
@@ -3648,9 +3679,8 @@
|
||||
"version": "24.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -3846,6 +3876,181 @@
|
||||
"vue-template-compiler": "^2.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
|
||||
"integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/helper-numbers": "1.13.2",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/floating-point-hex-parser": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
|
||||
"integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-api-error": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
|
||||
"integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-buffer": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
|
||||
"integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-numbers": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
|
||||
"integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/floating-point-hex-parser": "1.13.2",
|
||||
"@webassemblyjs/helper-api-error": "1.13.2",
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
|
||||
"integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-wasm-section": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
|
||||
"integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-buffer": "1.14.1",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
|
||||
"@webassemblyjs/wasm-gen": "1.14.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ieee754": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
|
||||
"integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xtuc/ieee754": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/leb128": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
|
||||
"integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/utf8": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
|
||||
"integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-edit": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
|
||||
"integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-buffer": "1.14.1",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
|
||||
"@webassemblyjs/helper-wasm-section": "1.14.1",
|
||||
"@webassemblyjs/wasm-gen": "1.14.1",
|
||||
"@webassemblyjs/wasm-opt": "1.14.1",
|
||||
"@webassemblyjs/wasm-parser": "1.14.1",
|
||||
"@webassemblyjs/wast-printer": "1.14.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-gen": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
|
||||
"integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
|
||||
"@webassemblyjs/ieee754": "1.13.2",
|
||||
"@webassemblyjs/leb128": "1.13.2",
|
||||
"@webassemblyjs/utf8": "1.13.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-opt": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
|
||||
"integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-buffer": "1.14.1",
|
||||
"@webassemblyjs/wasm-gen": "1.14.1",
|
||||
"@webassemblyjs/wasm-parser": "1.14.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-parser": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
|
||||
"integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-api-error": "1.13.2",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
|
||||
"@webassemblyjs/ieee754": "1.13.2",
|
||||
"@webassemblyjs/leb128": "1.13.2",
|
||||
"@webassemblyjs/utf8": "1.13.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wast-printer": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
|
||||
"integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@xtuc/ieee754": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@xtuc/long": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
|
||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||
@@ -3893,6 +4098,48 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ajv": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats/node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/amplitude-js": {
|
||||
"version": "8.21.9",
|
||||
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.21.9.tgz",
|
||||
@@ -4370,9 +4617,8 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
@@ -4537,6 +4783,16 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/chrome-trace-event": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
|
||||
"integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
@@ -4603,9 +4859,8 @@
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
@@ -4941,6 +5196,20 @@
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enquirer": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
|
||||
@@ -6115,6 +6384,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
|
||||
@@ -6592,6 +6871,13 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-to-regexp": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
|
||||
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "13.24.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
|
||||
@@ -6635,6 +6921,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/habitica-markdown": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/habitica-markdown/-/habitica-markdown-4.1.0.tgz",
|
||||
@@ -7433,6 +7726,37 @@
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-worker": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
|
||||
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"merge-stream": "^2.0.0",
|
||||
"supports-color": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-worker/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jquery": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
||||
@@ -7517,6 +7841,13 @@
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
@@ -7580,6 +7911,20 @@
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/loader-runner": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
|
||||
"integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.11.5"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@@ -7783,6 +8128,13 @@
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
@@ -8106,6 +8458,13 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/neo-async": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
@@ -9145,6 +9504,63 @@
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
|
||||
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"ajv": "^8.9.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"ajv-keywords": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils/node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils/node_modules/ajv-keywords": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
|
||||
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/secure-keys": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/secure-keys/-/secure-keys-1.0.0.tgz",
|
||||
@@ -9422,9 +9838,8 @@
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
@@ -9715,6 +10130,20 @@
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-mini": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-mini/-/tar-mini-0.2.0.tgz",
|
||||
@@ -9725,9 +10154,8 @@
|
||||
"version": "5.44.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
|
||||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
||||
"devOptional": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
@@ -9741,13 +10169,47 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.14",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
|
||||
"integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jest-worker": "^27.4.5",
|
||||
"schema-utils": "^4.3.0",
|
||||
"serialize-javascript": "^6.0.2",
|
||||
"terser": "^5.31.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webpack": "^5.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"uglify-js": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/terser/node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -10123,9 +10585,8 @@
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.4",
|
||||
@@ -10504,6 +10965,15 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-fragment": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-fragment/-/vue-fragment-1.6.0.tgz",
|
||||
"integrity": "sha512-a5T8ZZZK/EQzgVShEl374HbobUJ0a7v12BzOzS6Z/wd/5EE/5SffcyHC+7bf9hP3L7Yc0hhY/GhMdwFQ25O/8A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^2.5.16"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-functional-data-merge": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz",
|
||||
@@ -10571,6 +11041,20 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
|
||||
"integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"graceful-fs": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
@@ -10581,6 +11065,91 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.102.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
|
||||
"integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.8",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@webassemblyjs/ast": "^1.14.1",
|
||||
"@webassemblyjs/wasm-edit": "^1.14.1",
|
||||
"@webassemblyjs/wasm-parser": "^1.14.1",
|
||||
"acorn": "^8.15.0",
|
||||
"acorn-import-phases": "^1.0.3",
|
||||
"browserslist": "^4.26.3",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
"enhanced-resolve": "^5.17.3",
|
||||
"es-module-lexer": "^1.2.1",
|
||||
"eslint-scope": "5.1.1",
|
||||
"events": "^3.2.0",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"json-parse-even-better-errors": "^2.3.1",
|
||||
"loader-runner": "^4.2.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"neo-async": "^2.6.2",
|
||||
"schema-utils": "^4.3.3",
|
||||
"tapable": "^2.3.0",
|
||||
"terser-webpack-plugin": "^5.3.11",
|
||||
"watchpack": "^2.4.4",
|
||||
"webpack-sources": "^3.3.3"
|
||||
},
|
||||
"bin": {
|
||||
"webpack": "bin/webpack.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"webpack-cli": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-sources": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
|
||||
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/acorn-import-phases": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
|
||||
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vue": "^2.7.10",
|
||||
"vue-fragment": "^1.6.0",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuedraggable": "^2.24.3",
|
||||
@@ -59,6 +60,8 @@
|
||||
"jsdom": "^26.0.0",
|
||||
"mocha": "^11.1.0",
|
||||
"playwright": "^1.50.1",
|
||||
"vitest": "^3.0.5"
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"vitest": "^3.0.5",
|
||||
"webpack": "^5.94.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +1,3 @@
|
||||
.quest_lostMasterclasser4 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_lostMasterclasser4.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_windup {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_windup.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_solarSystem {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_solarSystem.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_virtualpet {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_virtualpet.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_alien {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_alien.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup,
|
||||
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid,
|
||||
.Pet_HatchingPotion_Alien {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Dessert.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Veggie {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Veggie.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Windup {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Windup.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_VirtualPet {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_VirtualPet.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Fungi {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Cryptid {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Alien {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Alien.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Gems {
|
||||
display:inline-block;
|
||||
margin-right:5px;
|
||||
@@ -91,6 +26,7 @@
|
||||
margin-left: -3px;
|
||||
margin-top: -18px;
|
||||
}
|
||||
|
||||
.slim_armor_special_0, .broad_armor_special_0, .shield_special_0 {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
@@ -98,7 +34,6 @@
|
||||
|
||||
/* Critical */
|
||||
.weapon_special_critical {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_critical.gif") no-repeat;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
margin-left:-12px;
|
||||
@@ -109,6 +44,7 @@
|
||||
.weapon_special_1 {
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
.broad_armor_special_1, .slim_armor_special_1, .head_special_1 {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
@@ -117,36 +53,15 @@
|
||||
.back_special_heroicAureole {
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_special_heroicAureole.gif") no-repeat;
|
||||
}
|
||||
|
||||
.head_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeHelmet.gif") no-repeat;
|
||||
}
|
||||
.head_special_1 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalHelmet.gif") no-repeat;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.broad_armor_special_0,.slim_armor_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeArmor.gif") no-repeat;
|
||||
}
|
||||
.broad_armor_special_1,.slim_armor_special_1 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalArmor.gif") no-repeat;
|
||||
}
|
||||
|
||||
.shield_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Shield-TormentedSkull.gif") no-repeat;
|
||||
}
|
||||
|
||||
.weapon_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Weapon-DarkSoulsBlade.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet-Wolf-Cerberus {
|
||||
width: 105px;
|
||||
height: 72px;
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-CerberusPup.gif") no-repeat;
|
||||
}
|
||||
|
||||
.broad_armor_special_ks2019, .slim_armor_special_ks2019, .eyewear_special_ks2019, .head_special_ks2019, .shield_special_ks2019 {
|
||||
@@ -154,36 +69,17 @@
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.broad_armor_special_ks2019, .slim_armor_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonArmor.gif") no-repeat;
|
||||
}
|
||||
|
||||
.eyewear_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonVisor.gif") no-repeat;
|
||||
}
|
||||
|
||||
.head_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonHelm.gif") no-repeat;
|
||||
}
|
||||
|
||||
.shield_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonShield.gif") no-repeat;
|
||||
}
|
||||
|
||||
.weapon_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonGlaive.gif") no-repeat;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.Pet-Gryphon-Gryphatrice {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-Gryphatrice.gif") no-repeat;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
|
||||
.Pet-Gryphatrice-Jubilant {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Gryphatrice-Jubilant.gif") no-repeat;
|
||||
width: 81px;
|
||||
height: 96px;
|
||||
}
|
||||
@@ -193,39 +89,11 @@
|
||||
height: 135px;
|
||||
}
|
||||
|
||||
.Mount_Head_Gryphon-Gryphatrice {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Head-Gryphatrice.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Body_Gryphon-Gryphatrice {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Body-Gryphatrice.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Head_Dragon-Hydra {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Hydra.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Body_Dragon-Hydra {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Hydra.gif") no-repeat;
|
||||
}
|
||||
|
||||
.background_airship, .background_clocktower, .background_steamworks {
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
|
||||
.background_airship {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_airship.gif") no-repeat;
|
||||
}
|
||||
|
||||
.background_clocktower {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_clocktower.gif") no-repeat;
|
||||
}
|
||||
|
||||
.background_steamworks {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_steamworks.gif") no-repeat;
|
||||
}
|
||||
|
||||
[class*="Mount_Head_"],
|
||||
[class*="Mount_Body_"] {
|
||||
margin-top:18px; /* Sprite accommodates 105x123 box */
|
||||
|
||||
@@ -16,10 +16,6 @@
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.d-content {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
* {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -108,15 +108,15 @@ export default {
|
||||
const allEmails = [];
|
||||
if (user.auth.local.email) allEmails.push(user.auth.local.email);
|
||||
if (user.auth.google && user.auth.google.emails) {
|
||||
const { emails } = user.auth.google;
|
||||
const emails = user.auth.google.emails;
|
||||
allEmails.push(...this.findSocialEmails(emails));
|
||||
}
|
||||
if (user.auth.apple && user.auth.apple.emails) {
|
||||
const { emails } = user.auth.apple;
|
||||
const emails = user.auth.apple.emails;
|
||||
allEmails.push(...this.findSocialEmails(emails));
|
||||
}
|
||||
if (user.auth.facebook && user.auth.facebook.emails) {
|
||||
const { emails } = user.auth.facebook;
|
||||
const emails = user.auth.facebook.emails;
|
||||
allEmails.push(...this.findSocialEmails(emails));
|
||||
}
|
||||
return allEmails;
|
||||
|
||||
+1
-1
@@ -609,7 +609,7 @@ import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks
|
||||
import saveHero from '../mixins/saveHero';
|
||||
import LoadingSpinner from '@/components/ui/loadingSpinner';
|
||||
|
||||
const { PLAY_CONSOLE_ORDERS_BASE_URL } = import.meta.env;
|
||||
const PLAY_CONSOLE_ORDERS_BASE_URL = import.meta.env.PLAY_CONSOLE_ORDERS_BASE_URL;
|
||||
|
||||
const humanReadablePaymentDetails = {
|
||||
customerId: {
|
||||
|
||||
@@ -20,29 +20,6 @@
|
||||
class="form mx-auto"
|
||||
@submit.prevent.stop="register()"
|
||||
>
|
||||
<div v-if="needsEmailField">
|
||||
<input
|
||||
id="emailInput"
|
||||
v-model="email"
|
||||
class="form-control dark"
|
||||
type="text"
|
||||
:placeholder="$t('emailAddress')"
|
||||
:class="{
|
||||
'mb-3': !emailError,
|
||||
'input-invalid input-with-error mb-2': emailError,
|
||||
'input-valid': email && emailValid,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="emailError"
|
||||
class="input-error"
|
||||
>
|
||||
{{ emailError }}
|
||||
</div>
|
||||
<p class="purple-600 mb-3">
|
||||
{{ $t('emailRequiredForSupport') }}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
id="usernameInput"
|
||||
v-model="username"
|
||||
@@ -81,9 +58,8 @@
|
||||
></label>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-info d-flex justify-content-center
|
||||
align-items-center w-100 sign-up mx-auto mb-5"
|
||||
:disabled="!email || emailError || !username || usernameInvalid || !privacyAccepted"
|
||||
class="btn btn-info d-block w-100 sign-up mx-auto mb-5"
|
||||
:disabled="!username || usernameInvalid || !privacyAccepted"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('getStarted') }}
|
||||
@@ -157,12 +133,10 @@
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
|
||||
|
||||
&:not(:disabled):not(.disabled) {
|
||||
&:focus, &:active {
|
||||
background-color: $blue-50;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
&:focus, &:active {
|
||||
background-color: $blue-50;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,19 +148,23 @@
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
import PrivacyBanner from '@/components/header/banners/privacy';
|
||||
import accountCreation from '@/mixins/accountCreation';
|
||||
import sanitizeRedirect from '@/mixins/sanitizeRedirect';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PrivacyBanner,
|
||||
},
|
||||
mixins: [accountCreation, sanitizeRedirect],
|
||||
mixins: [sanitizeRedirect],
|
||||
data () {
|
||||
return {
|
||||
authData: {},
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
privacyAccepted: false,
|
||||
registrationMethod: null,
|
||||
username: '',
|
||||
usernameIssues: [],
|
||||
needsEmailField: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -205,40 +183,30 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
if (window.sessionStorage.getItem('apple-token')) {
|
||||
this.registrationMethod = 'apple';
|
||||
} else if (!this.$store.state.registrationOptions.registrationMethod) {
|
||||
this.$router.push('/');
|
||||
} else {
|
||||
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
|
||||
}
|
||||
this.authData = this.$store.state.registrationOptions.authData;
|
||||
this.email = this.$store.state.registrationOptions.email;
|
||||
this.username = this.$store.state.registrationOptions.username;
|
||||
this.password = this.$store.state.registrationOptions.password;
|
||||
this.passwordConfirm = this.$store.state.registrationOptions.passwordConfirm;
|
||||
|
||||
if (window.sessionStorage.getItem('apple-token')) {
|
||||
this.registrationMethod = 'apple';
|
||||
if (!this.email) {
|
||||
this.email = window.sessionStorage.getItem('apple-email');
|
||||
}
|
||||
} else if (!this.$store.state.registrationOptions.registrationMethod) {
|
||||
this.$router.push('/');
|
||||
} else {
|
||||
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
|
||||
}
|
||||
|
||||
if (!this.email && this.registrationMethod !== 'apple') {
|
||||
if (!this.email) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((!this.email || this.email === '') && this.registrationMethod === 'apple') {
|
||||
this.needsEmailField = true;
|
||||
}
|
||||
if (this.email) {
|
||||
const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: usernameToCheck,
|
||||
}).then(res => {
|
||||
if (!res.issues) {
|
||||
this.username = usernameToCheck;
|
||||
}
|
||||
});
|
||||
}
|
||||
const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: usernameToCheck,
|
||||
}).then(res => {
|
||||
if (!res.issues) {
|
||||
this.username = usernameToCheck;
|
||||
}
|
||||
});
|
||||
document.getElementById('usernameInput').focus();
|
||||
},
|
||||
methods: {
|
||||
@@ -269,7 +237,6 @@ export default {
|
||||
idToken: window.sessionStorage.getItem('apple-token'),
|
||||
name: window.sessionStorage.getItem('apple-name'),
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
allowRegister: true,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -189,7 +189,7 @@ export default {
|
||||
this.cancel();
|
||||
return [];
|
||||
}
|
||||
this.currentSearch = regexRes[1]; // eslint-disable-line prefer-destructuring
|
||||
this.currentSearch = regexRes[1];
|
||||
|
||||
if (this.currentSearch.length === 0) return [];
|
||||
|
||||
|
||||
@@ -470,7 +470,7 @@ export default {
|
||||
return this.userGuilds.filter(group => {
|
||||
const leaderId = group.leader?._id || group.leader;
|
||||
if (leaderId !== this.user._id) return false;
|
||||
const { purchased } = group;
|
||||
const purchased = group.purchased;
|
||||
if (!purchased?.wasUpgraded) return false;
|
||||
if (this.activeGroupPlanIds.includes(group._id)) return false;
|
||||
if (!purchased.dateTerminated) return false;
|
||||
@@ -492,7 +492,7 @@ export default {
|
||||
},
|
||||
isPartyPreviouslyUpgraded () {
|
||||
if (!this.userParty) return false;
|
||||
const { purchased } = this.userParty;
|
||||
const purchased = this.userParty.purchased;
|
||||
if (!purchased?.wasUpgraded) return false;
|
||||
if (!purchased.dateTerminated) return false;
|
||||
return new Date(purchased.dateTerminated) < new Date();
|
||||
@@ -533,7 +533,7 @@ export default {
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.upgradeableGuilds.length > 0) {
|
||||
[this.selectedOption] = this.upgradeableGuilds;
|
||||
this.selectedOption = this.upgradeableGuilds[0];
|
||||
} else if (this.upgradeableParty) {
|
||||
this.selectedOption = this.upgradeableParty;
|
||||
} else {
|
||||
|
||||
@@ -198,6 +198,7 @@ import dailyIcon from '@/assets/svg/daily.svg?raw';
|
||||
import todoIcon from '@/assets/svg/todo.svg?raw';
|
||||
import rewardIcon from '@/assets/svg/reward.svg?raw';
|
||||
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
@@ -437,6 +438,14 @@ export default {
|
||||
return false;
|
||||
},
|
||||
changeMirrorPreference (newVal) {
|
||||
Analytics.track({
|
||||
eventName: 'mirror tasks',
|
||||
eventAction: 'mirror tasks',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
mirror: newVal,
|
||||
group: this.group._id,
|
||||
}, { trackOnClient: true });
|
||||
const groupsToMirror = this.user.preferences.tasks.mirrorGroupTasks || [];
|
||||
if (newVal) { // we're turning copy ON for this group
|
||||
groupsToMirror.push(this.group._id);
|
||||
|
||||
@@ -240,6 +240,7 @@
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import closeX from '../ui/closeX';
|
||||
|
||||
@@ -275,6 +276,11 @@ export default {
|
||||
this.$store.state.party.data = party;
|
||||
this.user.party._id = party._id;
|
||||
|
||||
Analytics.updateUser({
|
||||
partyID: party._id,
|
||||
partySize: 1,
|
||||
});
|
||||
|
||||
this.$root.$emit('bv::hide::modal', 'create-party-modal');
|
||||
await this.$router.push('/party');
|
||||
},
|
||||
|
||||
@@ -314,6 +314,7 @@ import extend from 'lodash/extend';
|
||||
import groupUtilities from '@/mixins/groupsUtilities';
|
||||
import styleHelper from '@/mixins/styleHelper';
|
||||
import { mapGetters } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import participantListModal from './participantListModal';
|
||||
import groupFormModal from './groupFormModal';
|
||||
import groupGemsModal from '@/components/groups/groupGemsModal';
|
||||
@@ -559,6 +560,7 @@ export default {
|
||||
|
||||
if (this.isParty) {
|
||||
data.type = 'party';
|
||||
Analytics.updateUser({ partySize: null, partyID: null });
|
||||
this.$store.state.partyMembers = [];
|
||||
}
|
||||
|
||||
|
||||
@@ -334,6 +334,7 @@ import orderBy from 'lodash/orderBy';
|
||||
import * as quests from '@/../../common/script/content/quests';
|
||||
import getItemInfo from '@/../../common/script/libs/getItemInfo';
|
||||
import { mapState } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
import navigationBack from '@/assets/svg/navigation_back.svg?raw';
|
||||
import questDialogContent from '../shops/quests/questDialogContent';
|
||||
@@ -420,6 +421,11 @@ export default {
|
||||
async questInit () {
|
||||
this.loading = true;
|
||||
|
||||
Analytics.updateUser({
|
||||
partyID: this.group._id,
|
||||
partySize: this.group.memberCount,
|
||||
});
|
||||
|
||||
const groupId = this.group._id || this.user.party._id;
|
||||
|
||||
const key = this.selectedQuest;
|
||||
|
||||
@@ -123,6 +123,7 @@
|
||||
|
||||
<script>
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapGetters, mapActions } from '@/libs/store';
|
||||
import MemberDetails from '../memberDetails';
|
||||
import createPartyModal from '../groups/createPartyModal';
|
||||
@@ -235,8 +236,22 @@ export default {
|
||||
},
|
||||
async createOrInviteParty () {
|
||||
if (this.user.party._id) {
|
||||
await Analytics.track({
|
||||
eventName: 'Header Party CTA',
|
||||
eventAction: 'Header Party CTA',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
state: 'Find Party Members',
|
||||
});
|
||||
this.$router.push('/looking-for-party');
|
||||
} else {
|
||||
await Analytics.track({
|
||||
eventName: 'Header Party CTA',
|
||||
eventAction: 'Header Party CTA',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
state: 'Get Started',
|
||||
});
|
||||
this.$root.$emit('bv::show::modal', 'create-party-modal');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -114,6 +114,7 @@ import { mapState } from '@/libs/store';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import guide from '@/mixins/guide';
|
||||
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
import yesterdailyModal from './tasks/yesterdailyModal';
|
||||
import newStuff from './news/modal';
|
||||
@@ -647,6 +648,15 @@ export default {
|
||||
// Reset daily analytics actions
|
||||
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 0);
|
||||
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 0);
|
||||
} else {
|
||||
// Note a failed cron event, for our records and investigation
|
||||
Analytics.track({
|
||||
eventName: 'cron failed',
|
||||
eventAction: 'cron failed',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
responseCode: response.status,
|
||||
}, { trackOnClient: true });
|
||||
}
|
||||
|
||||
// Sync
|
||||
|
||||
@@ -433,6 +433,9 @@ import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
|
||||
import notificationsMixin from '@/mixins/notifications';
|
||||
import paymentsMixin from '@/mixins/payments';
|
||||
|
||||
// analytics
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
selectTranslatedArray,
|
||||
@@ -533,6 +536,16 @@ export default {
|
||||
this.close();
|
||||
},
|
||||
submit () {
|
||||
if (this.paymentData.group && !this.paymentData.newGroup) {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventName: 'group plan upgrade',
|
||||
eventAction: 'group plan upgrade',
|
||||
eventCategory: 'behavior',
|
||||
demographics: this.upgradedGroup.demographics,
|
||||
type: this.paymentData.group.type,
|
||||
}, { trackOnClient: true });
|
||||
}
|
||||
this.paymentData = {};
|
||||
this.$root.$emit('bv::hide::modal', 'payments-success-modal');
|
||||
},
|
||||
|
||||
@@ -37,9 +37,6 @@ export default {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
window.sessionStorage.setItem('apple-token', response.idToken);
|
||||
if (response.email) {
|
||||
window.sessionStorage.setItem('apple-email', response.email);
|
||||
}
|
||||
window.location.href = '/username';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<draggable
|
||||
v-if="taskList.length > 0 && !rerendering"
|
||||
v-if="taskList.length > 0"
|
||||
ref="tasksList"
|
||||
class="sortable-tasks"
|
||||
:disabled="activeFilter.label === 'scheduled' || !canBeDragged()"
|
||||
@@ -432,7 +432,6 @@ export default {
|
||||
|
||||
selectedItemToBuy: {},
|
||||
dragging: false,
|
||||
rerendering: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -549,8 +548,8 @@ export default {
|
||||
if (this.taskListOverride) originTasks = this.taskListOverride;
|
||||
|
||||
// Server
|
||||
const taskIdToReplace = filteredList[data.newIndex]._id;
|
||||
const newIndexOnServer = originTasks.findIndex(task => task._id === taskIdToReplace);
|
||||
const taskIdToReplace = filteredList[data.newIndex];
|
||||
const newIndexOnServer = originTasks.findIndex(taskId => taskId === taskIdToReplace);
|
||||
|
||||
let newOrder;
|
||||
if (taskToMove.group.id && !this.isUser) {
|
||||
@@ -569,9 +568,6 @@ export default {
|
||||
// Client
|
||||
const deleted = originTasks.splice(data.oldIndex, 1);
|
||||
originTasks.splice(data.newIndex, 0, deleted[0]);
|
||||
this.rerendering = true;
|
||||
await this.$nextTick();
|
||||
this.rerendering = false;
|
||||
},
|
||||
async moveTo (task, where) { // where is 'top' or 'bottom'
|
||||
const taskIdToMove = task._id;
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
}, `type_${task.type}`
|
||||
]"
|
||||
@click="castEnd($event, task)"
|
||||
tabindex="0"
|
||||
@keypress.enter="$emit('editTask', task)"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
@@ -100,7 +98,9 @@
|
||||
<div
|
||||
class="task-clickable-area pt-1 pl-75 pb-0"
|
||||
:class="{ 'cursor-auto': !teamManagerAccess }"
|
||||
tabindex="0"
|
||||
@click="edit($event, task)"
|
||||
@keypress.enter="edit($event, task)"
|
||||
>
|
||||
<div class="d-flex justify-content-between">
|
||||
<h3
|
||||
@@ -432,6 +432,10 @@
|
||||
outline: none;
|
||||
transition: none;
|
||||
border: $purple-400 solid 1px;
|
||||
|
||||
:not(task-best-control-inner-habit) { // round icon
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.control-bottom-box {
|
||||
@@ -458,13 +462,16 @@
|
||||
&:hover:not(.task-not-editable.task-not-scoreable),
|
||||
&:focus-within:not(.task-not-editable.task-not-scoreable) {
|
||||
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
|
||||
z-index: 11;
|
||||
}
|
||||
}
|
||||
|
||||
.task:not(.groupTask) {
|
||||
&:hover, &:focus {
|
||||
border: none;
|
||||
outline: 1px solid $purple-400;
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
.left-control, .right-control, .task-content {
|
||||
border-color: $purple-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,6 +522,11 @@
|
||||
&-user {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-radius: 4px;
|
||||
border: $purple-400 solid 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.task-title + .task-dropdown ::v-deep .dropdown-menu {
|
||||
|
||||
@@ -55,31 +55,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="d-flex align-items-center">
|
||||
<lockable-label
|
||||
class="mr-auto"
|
||||
:class-override="cssClass('headings')"
|
||||
:locked="challengeAccessRequired"
|
||||
:text="`${$t('text')}*`"
|
||||
/>
|
||||
<div
|
||||
id="spi-alert"
|
||||
class="d-flex align-items-center"
|
||||
:class="cssClass('headings')"
|
||||
>
|
||||
<div
|
||||
class="svg svg-icon color icon-16 mr-1"
|
||||
v-html="icons.alert"
|
||||
></div>
|
||||
<small
|
||||
class="my-1"
|
||||
>
|
||||
<a
|
||||
:class="cssClass('headings')"
|
||||
>{{ $t('avoidSPI') }}</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<lockable-label
|
||||
:class-override="cssClass('headings')"
|
||||
:locked="challengeAccessRequired"
|
||||
:text="`${$t('text')}*`"
|
||||
/>
|
||||
<input
|
||||
ref="inputToFocus"
|
||||
v-model="task.text"
|
||||
@@ -99,20 +79,10 @@
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
>
|
||||
</div>
|
||||
<b-popover
|
||||
:target="'spi-alert'"
|
||||
triggers="hover"
|
||||
placement="bottom"
|
||||
offset="-128"
|
||||
>
|
||||
<div
|
||||
v-html="$t('avoidSPIDetails', spiLinkData)">
|
||||
</div>
|
||||
</b-popover>
|
||||
<div
|
||||
class="form-group mb-0"
|
||||
>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex">
|
||||
<lockable-label
|
||||
class="mr-auto"
|
||||
:class-override="cssClass('headings')"
|
||||
@@ -993,20 +963,6 @@
|
||||
box-shadow: 0px 1px 3px 0px rgba(26, 24, 29, 0.12), 0px 1px 2px 0px rgba(26, 24, 29, 0.24);
|
||||
}
|
||||
}
|
||||
|
||||
.b-popover {
|
||||
margin-top: -5px;
|
||||
max-width: 330px;
|
||||
}
|
||||
|
||||
.popover-body {
|
||||
text-align: left;
|
||||
|
||||
a {
|
||||
color: $gray-500;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
@@ -1240,7 +1196,6 @@ import chevronIcon from '@/assets/svg/chevron.svg?raw';
|
||||
import calendarIcon from '@/assets/svg/calendar.svg?raw';
|
||||
import gripIcon from '@/assets/svg/grip.svg?raw';
|
||||
import InformationIcon from '@/components/ui/informationIcon.vue';
|
||||
import alertIcon from '@/assets/svg/for-css/alert-white.svg?raw';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -1276,7 +1231,6 @@ export default {
|
||||
streak: streakIcon,
|
||||
calendar: calendarIcon,
|
||||
grip: gripIcon,
|
||||
alert: alertIcon,
|
||||
}),
|
||||
members: [],
|
||||
membersNameAndId: [],
|
||||
@@ -1297,11 +1251,6 @@ export default {
|
||||
{ key: 'per', label: 'perception', description: 'perTaskText' },
|
||||
],
|
||||
calendarHighlights: { dates: [new Date()] },
|
||||
spiLinkData: {
|
||||
firstLink: '<a href="/static/privacy#section_1" target="_blank">',
|
||||
secondLink: '<a href="/static/privacy" target="_blank">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -1450,7 +1399,7 @@ export default {
|
||||
this.task.down = !this.task.down;
|
||||
},
|
||||
weekdaysMin (dayNumber) {
|
||||
return this.$t(`weekdaysMin${dayNumber}`);
|
||||
return moment.weekdaysMin(dayNumber);
|
||||
},
|
||||
formattedDate (date) {
|
||||
return moment(date).format('MM/DD/YYYY');
|
||||
|
||||
@@ -6,7 +6,7 @@ import amplitude from 'amplitude-js';
|
||||
import Vue from 'vue';
|
||||
import getStore from '@/store';
|
||||
|
||||
const { AMPLITUDE_KEY } = import.meta.env;
|
||||
const AMPLITUDE_KEY = import.meta.env.AMPLITUDE_KEY;
|
||||
const REQUIRED_FIELDS = ['eventCategory', 'eventAction'];
|
||||
|
||||
let analyticsLoading = false;
|
||||
|
||||
@@ -1,10 +1,28 @@
|
||||
// Vue plugin to globally expose a '$t' method that calls common/i18n.t.
|
||||
// Can be anywhere inside vue as 'this.$t' or '$t' in templates.
|
||||
|
||||
import moment from 'moment';
|
||||
import i18n from '@/../../common/script/i18n';
|
||||
|
||||
function loadLocale (i18nData) {
|
||||
// Load i18n strings
|
||||
i18n.strings = i18nData.strings;
|
||||
|
||||
// Load Moment.js locale
|
||||
const { language } = i18nData;
|
||||
|
||||
if (language && i18nData.momentLang && language.momentLangCode) {
|
||||
// Make moment available under `window` so that the locale can be set
|
||||
window.moment = moment;
|
||||
|
||||
// Execute the script and set the locale
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.text = i18nData.momentLang;
|
||||
head.appendChild(script);
|
||||
moment.updateLocale(language.momentLangCode);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
NavbarPlugin,
|
||||
CollapsePlugin,
|
||||
} from 'bootstrap-vue';
|
||||
import Fragment from 'vue-fragment';
|
||||
import AppComponent from './app';
|
||||
import { setUpLogging } from '@/libs/logging';
|
||||
import router from './router/index';
|
||||
@@ -43,6 +44,7 @@ Vue.use(FormRadioPlugin);
|
||||
Vue.use(TooltipPlugin);
|
||||
Vue.use(NavbarPlugin);
|
||||
Vue.use(CollapsePlugin);
|
||||
Vue.use(Fragment.Plugin);
|
||||
|
||||
setUpLogging();
|
||||
const store = getStore();
|
||||
|
||||
@@ -6,8 +6,9 @@ import { mapState } from '@/libs/store';
|
||||
import encodeParams from '@/libs/encodeParams';
|
||||
import notificationsMixin from '@/mixins/notifications';
|
||||
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
const { STRIPE_PUB_KEY } = import.meta.env;
|
||||
const STRIPE_PUB_KEY = import.meta.env.STRIPE_PUB_KEY;
|
||||
|
||||
let stripeInstance = null;
|
||||
|
||||
@@ -206,6 +207,16 @@ export default {
|
||||
alert(`Error while redirecting to Stripe: ${checkoutSessionResult.error.message}`);
|
||||
throw checkoutSessionResult.error;
|
||||
}
|
||||
if (paymentType === 'groupPlan') {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventName: 'group plan create',
|
||||
eventAction: 'group plan create',
|
||||
eventCategory: 'behavior',
|
||||
demographics: appState.newGroup.demographics,
|
||||
type: appState.newGroup.type,
|
||||
}, { trackOnClient: true });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error while redirecting to Stripe', err); // eslint-disable-line
|
||||
alert(`Error while redirecting to Stripe: ${err.message}`);
|
||||
|
||||
@@ -3,6 +3,7 @@ import Vue from 'vue';
|
||||
import scoreTask from '@/../../common/script/ops/scoreTask';
|
||||
import notifications from './notifications';
|
||||
import { mapState } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
|
||||
|
||||
export default {
|
||||
@@ -57,6 +58,15 @@ export default {
|
||||
|
||||
const tasksScoredCount = getLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT);
|
||||
if (!tasksScoredCount || tasksScoredCount < 2) {
|
||||
Analytics.track({
|
||||
eventName: 'task scored',
|
||||
eventAction: 'task scored',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
uuid: user._id,
|
||||
taskType: task.type,
|
||||
direction,
|
||||
}, { trackOnClient: true });
|
||||
if (!tasksScoredCount) {
|
||||
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 1);
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -90,7 +90,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content" v-if="allowedToChangeClass">
|
||||
<fragment v-if="allowedToChangeClass">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -71,7 +71,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -55,7 +55,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
<div
|
||||
v-once
|
||||
class="feedback"
|
||||
class="feedback mt-3"
|
||||
v-html="$t('feedback')"
|
||||
>
|
||||
</div>
|
||||
@@ -94,7 +94,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -78,7 +78,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -83,7 +83,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr>
|
||||
<td class="settings-label">
|
||||
{{ $t("showHeader") }}
|
||||
@@ -26,7 +26,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -67,7 +67,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-for="network in SOCIAL_AUTH_NETWORKS"
|
||||
:key="network.key"
|
||||
@@ -39,7 +39,7 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -66,7 +66,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -111,7 +111,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -56,7 +56,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -60,7 +60,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -54,7 +54,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -48,7 +48,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -76,7 +76,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
:class="{
|
||||
'casting-spell': castingSpell,
|
||||
}"
|
||||
@dragover.prevent
|
||||
>
|
||||
<!-- <banned-account-modal /> -->
|
||||
<amazon-payments-modal v-if="!isStaticPage" />
|
||||
@@ -131,6 +130,7 @@ import PrivacyBanner from '@/components/header/banners/privacy';
|
||||
import AppFooter from '@/components/appFooter';
|
||||
import notificationsDisplay from '@/components/notifications';
|
||||
import { mapState } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import BuyModal from '@/components/shops/buyModal.vue';
|
||||
import SelectMembersModal from '@/components/selectMembersModal.vue';
|
||||
import notifications from '@/mixins/notifications';
|
||||
@@ -276,6 +276,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
Analytics.updateUser();
|
||||
return this.loadAllTranslations();
|
||||
}).then(() => {
|
||||
this.$store.state.isUserLoaded = true;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import getStore from '@/store';
|
||||
import handleRedirect from './handleRedirect';
|
||||
|
||||
@@ -10,56 +11,56 @@ import { DEPRECATED_ROUTES } from '@/router/deprecated-routes';
|
||||
|
||||
// NOTE: when adding a page make sure to implement the `common:setTitle` action
|
||||
|
||||
const Logout = () => import('@/components/auth/logout');
|
||||
const Logout = () => import(/* webpackChunkName: "auth" */'@/components/auth/logout');
|
||||
|
||||
// Hall
|
||||
const HallPage = () => import('@/components/hall/index');
|
||||
const PatronsPage = () => import('@/components/hall/patrons');
|
||||
const HeroesPage = () => import('@/components/hall/heroes');
|
||||
const HallPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/index');
|
||||
const PatronsPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/patrons');
|
||||
const HeroesPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/heroes');
|
||||
|
||||
// Admin Pages
|
||||
const AdminContainerPage = () => import('@/components/admin/container');
|
||||
const AdminPanelPage = () => import('@/components/admin/admin-panel');
|
||||
const AdminPanelUserPage = () => import('@/components/admin/admin-panel/user-support');
|
||||
const AdminPanelSearchPage = () => import('@/components/admin/admin-panel/search');
|
||||
const GroupAdminPage = () => import('@/components/admin/groups');
|
||||
const GroupAdminGroupPage = () => import('@/components/admin/groups/group-support');
|
||||
const BlockerPage = () => import('@/components/admin/blocker');
|
||||
const AdminContainerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/container');
|
||||
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel');
|
||||
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/user-support');
|
||||
const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/search');
|
||||
const GroupAdminPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups');
|
||||
const GroupAdminGroupPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups/group-support');
|
||||
const BlockerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/blocker');
|
||||
|
||||
// Tasks
|
||||
const UserTasks = () => import('@/components/tasks/user');
|
||||
const UserTasks = () => import(/* webpackChunkName: "userTasks" */'@/components/tasks/user');
|
||||
|
||||
// Inventory
|
||||
const InventoryContainer = () => import('@/components/inventory/index');
|
||||
const ItemsPage = () => import('@/components/inventory/items/index');
|
||||
const EquipmentPage = () => import('@/components/inventory/equipment/index');
|
||||
const StablePage = () => import('@/components/inventory/stable/index');
|
||||
const InventoryContainer = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/index');
|
||||
const ItemsPage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/items/index');
|
||||
const EquipmentPage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/equipment/index');
|
||||
const StablePage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/stable/index');
|
||||
|
||||
// Guilds & Parties
|
||||
const GroupPage = () => import('@/components/groups/group');
|
||||
const GroupPlansAppPage = () => import('@/components/static/groupPlans');
|
||||
const LookingForParty = () => import('@/components/groups/lookingForParty');
|
||||
const GroupPage = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/group');
|
||||
const GroupPlansAppPage = () => import(/* webpackChunkName: "guilds" */ '@/components/static/groupPlans');
|
||||
const LookingForParty = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/lookingForParty');
|
||||
|
||||
// Group Plans
|
||||
const GroupPlanIndex = () => import('@/components/group-plans/index');
|
||||
const GroupPlanTaskInformation = () => import('@/components/group-plans/taskInformation');
|
||||
const GroupPlanBilling = () => import('@/components/group-plans/billing');
|
||||
const GroupPlanIndex = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/index');
|
||||
const GroupPlanTaskInformation = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/taskInformation');
|
||||
const GroupPlanBilling = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/billing');
|
||||
|
||||
const MessagesIndex = () => import('@/pages/private-messages/index.vue');
|
||||
const MessagesIndex = () => import(/* webpackChunkName: "private-messages" */ '@/pages/private-messages/index.vue');
|
||||
|
||||
// Challenges
|
||||
const ChallengeIndex = () => import('@/components/challenges/index');
|
||||
const MyChallenges = () => import('@/components/challenges/myChallenges');
|
||||
const FindChallenges = () => import('@/components/challenges/findChallenges');
|
||||
const ChallengeDetail = () => import('@/components/challenges/challengeDetail');
|
||||
const ChallengeIndex = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/index');
|
||||
const MyChallenges = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/myChallenges');
|
||||
const FindChallenges = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/findChallenges');
|
||||
const ChallengeDetail = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/challengeDetail');
|
||||
|
||||
// Shops
|
||||
const ShopsContainer = () => import('@/components/shops/index');
|
||||
const MarketPage = () => import('@/components/shops/market/index');
|
||||
const QuestsPage = () => import('@/components/shops/quests/index');
|
||||
const CustomizationsPage = () => import('@/components/shops/customizations/index');
|
||||
const SeasonalPage = () => import('@/components/shops/seasonal/index');
|
||||
const TimeTravelersPage = () => import('@/components/shops/timeTravelers/index');
|
||||
const ShopsContainer = () => import(/* webpackChunkName: "shops" */'@/components/shops/index');
|
||||
const MarketPage = () => import(/* webpackChunkName: "shops-market" */'@/components/shops/market/index');
|
||||
const QuestsPage = () => import(/* webpackChunkName: "shops-quest" */'@/components/shops/quests/index');
|
||||
const CustomizationsPage = () => import(/* webpackChunkName: "shops-customizations" */'@/components/shops/customizations/index');
|
||||
const SeasonalPage = () => import(/* webpackChunkName: "shops-seasonal" */'@/components/shops/seasonal/index');
|
||||
const TimeTravelersPage = () => import(/* webpackChunkName: "shops-timetravelers" */'@/components/shops/timeTravelers/index');
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
@@ -317,6 +318,15 @@ router.beforeEach(async (to, from, next) => {
|
||||
router.app.$root.$emit('update-party');
|
||||
}
|
||||
|
||||
if (to.name === 'lookingForParty') {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventName: 'View Find Members',
|
||||
eventAction: 'View Find Members',
|
||||
eventCategory: 'behavior',
|
||||
}, { trackOnClient: true });
|
||||
}
|
||||
|
||||
// Redirect old guild urls
|
||||
if (to.hash.indexOf('#/options/groups/guilds/') !== -1) {
|
||||
const splits = to.hash.split('/');
|
||||
|
||||
@@ -21,8 +21,8 @@ const NewsPage = () => import('@/components/static/newStuff');
|
||||
const OverviewPage = () => import('@/components/static/overview');
|
||||
const PressKitPage = () => import('@/components/static/pressKit');
|
||||
const PrivacyPage = () => import('@/components/static/privacy');
|
||||
const RegisterLoginReset = () => import('@/components/auth/registerLoginReset');
|
||||
const RegisterUsername = () => import('@/components/auth/registerUsername');
|
||||
const RegisterLoginReset = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerLoginReset');
|
||||
const RegisterUsername = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerUsername');
|
||||
const SubscriptionBenefitsFaq = () => import('@/components/static/subscriptionBenefitsFaq');
|
||||
const TermsPage = () => import('@/components/static/terms');
|
||||
|
||||
|
||||
@@ -101,7 +101,6 @@ export async function appleAuth (store, params) {
|
||||
id_token: params.idToken,
|
||||
name: params.name,
|
||||
username: params.username,
|
||||
email: params.email,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -110,10 +109,7 @@ export async function appleAuth (store, params) {
|
||||
}
|
||||
|
||||
if (result.data.message && result.data.id_token) {
|
||||
return {
|
||||
idToken: result.data.id_token,
|
||||
email: result.data.email,
|
||||
};
|
||||
return { idToken: result.data.id_token };
|
||||
}
|
||||
|
||||
const user = result.data.data;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import Vue from 'vue';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
export async function getChat (store, payload) {
|
||||
const response = await axios.get(`/api/v4/groups/${payload.groupId}/chat?limit=400`);
|
||||
@@ -16,6 +17,13 @@ export async function postChat (store, payload) {
|
||||
url += `?previousMsg=${payload.previousMsg}`;
|
||||
}
|
||||
|
||||
if (group.type === 'party') {
|
||||
Analytics.updateUser({
|
||||
partyID: group.id,
|
||||
partySize: group.memberCount,
|
||||
});
|
||||
}
|
||||
|
||||
const response = await axios.post(url, {
|
||||
message: payload.message,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import omit from 'lodash/omit';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { loadAsyncResource } from '@/libs/asyncResource';
|
||||
|
||||
export async function getPublicGuilds (store, payload) {
|
||||
@@ -73,6 +74,7 @@ export async function join (store, payload) {
|
||||
if (invitationI !== -1) invitations.parties.splice(invitationI, 1);
|
||||
|
||||
user.party._id = groupId;
|
||||
Analytics.updateUser({ partyID: groupId });
|
||||
// load the party members so that they get shown in the header
|
||||
store.dispatch('party:getMembers');
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import * as shops from './shops';
|
||||
import * as snackbars from './snackbars';
|
||||
import * as worldState from './worldState';
|
||||
import * as news from './news';
|
||||
import * as analytics from './analytics';
|
||||
import * as faq from './faq';
|
||||
import * as blockers from './blockers';
|
||||
|
||||
@@ -43,6 +44,7 @@ const actions = flattenAndNamespace({
|
||||
snackbars,
|
||||
worldState,
|
||||
news,
|
||||
analytics,
|
||||
faq,
|
||||
blockers,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
import axios from 'axios';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
// export async function initQuest (store) {
|
||||
// }
|
||||
|
||||
export async function sendAction (store, payload) { // eslint-disable-line import/prefer-default-export, max-len
|
||||
// @TODO: Maybe move this to server
|
||||
let partyData = {};
|
||||
if (store.state.party && store.state.party.data) {
|
||||
partyData = {
|
||||
partyID: store.state.party.data._id,
|
||||
partySize: store.state.party.data.memberCount,
|
||||
};
|
||||
} else {
|
||||
partyData = {
|
||||
partyID: store.state.user.data.party._id,
|
||||
partySize: store.state.partyMembers.data.length,
|
||||
};
|
||||
}
|
||||
|
||||
Analytics.updateUser(partyData);
|
||||
|
||||
const response = await axios.post(`/api/v4/groups/${payload.groupId}/${payload.action}`);
|
||||
|
||||
// @TODO: Update user?
|
||||
|
||||
@@ -3,6 +3,7 @@ import Vue from 'vue';
|
||||
import compact from 'lodash/compact';
|
||||
import omit from 'lodash/omit';
|
||||
import { loadAsyncResource } from '@/libs/asyncResource';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
|
||||
|
||||
export function fetchUserTasks (store, options = {}) {
|
||||
@@ -111,6 +112,15 @@ export async function create (store, createdTask) {
|
||||
}
|
||||
const tasksCreatedCount = getLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT);
|
||||
if (!tasksCreatedCount || tasksCreatedCount < 2) {
|
||||
const uuid = store.state.user.data._id;
|
||||
Analytics.track({
|
||||
eventName: 'task created',
|
||||
eventAction: 'task created',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
uuid,
|
||||
taskType: taskRes.type,
|
||||
}, { trackOnClient: true });
|
||||
if (!tasksCreatedCount) {
|
||||
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 1);
|
||||
} else {
|
||||
@@ -158,15 +168,11 @@ export async function collapseChecklist (store, task) {
|
||||
}
|
||||
|
||||
export async function destroy (store, task) {
|
||||
const type = `${task.type}s`;
|
||||
const listIndex = store.state.tasks.data[type].findIndex(t => t._id === task._id);
|
||||
const orderIndex = store.state.user.data.tasksOrder[type].indexOf(task._id);
|
||||
const list = store.state.tasks.data[`${task.type}s`];
|
||||
const taskIndex = list.findIndex(t => t._id === task._id);
|
||||
|
||||
if (listIndex > -1) {
|
||||
store.state.tasks.data[type].splice(listIndex, 1);
|
||||
}
|
||||
if (orderIndex > -1) {
|
||||
store.state.user.data.tasksOrder[type].splice(orderIndex, 1);
|
||||
if (taskIndex > -1) {
|
||||
list.splice(taskIndex, 1);
|
||||
}
|
||||
|
||||
await axios.delete(`/api/v4/tasks/${task._id}`);
|
||||
|
||||
@@ -159,6 +159,10 @@ export default defineConfig({
|
||||
target: DEV_BASE_URL,
|
||||
changeOrigin: true,
|
||||
},
|
||||
'^/analytics': {
|
||||
target: DEV_BASE_URL,
|
||||
changeOrigin: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -162,7 +162,5 @@
|
||||
"achievementCatsModalText": "Nasbíral jsi všechny kočičí mazlíčky!",
|
||||
"achievementRoughRiderModalText": "Nasbíral jsi všechny základní barvy nepohodlných mazlíčků a mountů!",
|
||||
"achievementRodentRulerModalText": "Nasbíral jsi všechny hlodavce!",
|
||||
"achievementCatsText": "Vylíhly se všechny standardní barvy kočičích mazlíčků: gepard, lev, šavlozubý tygr a tygr!",
|
||||
"achievementRodentRuler": "Vládce hlodavců",
|
||||
"achievementCats": "Pasák koček"
|
||||
"achievementCatsText": "Vylíhly se všechny standardní barvy kočičích mazlíčků: gepard, lev, šavlozubý tygr a tygr!"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"brokenChaLink": "Nefunkční odkaz na výzvu",
|
||||
"keepIt": "Ponechat",
|
||||
"removeIt": "Odstranit",
|
||||
"brokenChallenge": "Neplatný odkaz na výzvu",
|
||||
"brokenChallenge": "Nefunkční odkaz na výzvu: tento úkol byl součástí výzvy, ale ta (nebo skupina, která ji vytvořila) byla odstraněna. Co chceš dělat s osiřelými úkoly?",
|
||||
"challengeCompleted": "Výzva byla ukončena a vítězem se stal <span class=\"badge\"><%= user %></span>! Co chceš dělat s osiřelými úkoly?",
|
||||
"unsubChallenge": "Nefunkční odkaz na výzvu: tento úkol byl součástí výzvy, ze které jsi se odhlásil/a. Co chceš dělat s osiřelými úkoly?",
|
||||
"challenges": "Výzvy",
|
||||
@@ -85,7 +85,7 @@
|
||||
"summaryRequired": "Je požadováno shrnutí",
|
||||
"summaryTooLong": "Shrnutí je příliš dlouhé",
|
||||
"descriptionRequired": "Je požadován popis",
|
||||
"locationRequired": "Je nutné vybrat umístění výzvy („Přidat do“)",
|
||||
"locationRequired": "Je požadováno vybrat lokaci výzvy ('Přidat k')",
|
||||
"categoiresRequired": "Musí být vybrána jedna nebo více kategorií",
|
||||
"viewProgressOf": "Zobrazit pokrok",
|
||||
"viewProgress": "Zobrazit pokrok",
|
||||
@@ -94,16 +94,5 @@
|
||||
"selectParticipant": "Zvol účastníka",
|
||||
"filters": "Filtry",
|
||||
"wonChallengeDesc": "Vyhrál/a jsi výzvu <%= challengeName %>! Tvá výhra je zaznamenána ve tvých úspěších.",
|
||||
"yourReward": "Tvá odměna",
|
||||
"brokenTaskDescription": "Tento úkol byl součástí výzvy, ale byl z ní odstraněn. Co chceš udělat?",
|
||||
"brokenChallengeDescription": "Tento úkol byl součástí výzvy, ale výzva (nebo skupina) byla smazána. Co chceš udělat s osiřelými úkoly?",
|
||||
"challengeCompletedDescription": "Vítězem je <%= user %>! Co chceš udělat s osiřelými úkoly?",
|
||||
"messageChallengeFlagAlreadyReported": "Tuto výzvu jsi už nahlásil.",
|
||||
"flaggedNotHidden": "Výzva byla nahlášena jednou, není skrytá",
|
||||
"flaggedAndHidden": "Výzva byla nahlášena a je skrytá",
|
||||
"resetFlagCount": "Resetovat počet nahlášení",
|
||||
"deleteChallengeRefundDescription": "Pokud tuto výzvu smažeš, bude ti vrácena odměna v drahokamech a úkoly z výzvy zůstanou na nástěnkách úkolů účastníků.",
|
||||
"messageChallengeFlagOfficial": "Oficiální výzvy nelze nahlásit.",
|
||||
"brokenTask": "Nefunkční odkaz na výzvu",
|
||||
"removeTasks": "Odstranit Úkoly"
|
||||
"yourReward": "Tvá odměna"
|
||||
}
|
||||
|
||||
@@ -3495,7 +3495,5 @@
|
||||
"backMystery202601Notes": "Dieses Zeichen gewährt dem Anwender die Kontrolle über die Elemente der Jahreszeit von Kälte und Frost. Gewährt keinen Attributbonus. Januar 2026 Abonnentengegenstand.",
|
||||
"backMystery202602Text": "Fünf Schweife der Sakura",
|
||||
"backMystery202602Notes": "Diese flauschigen Schweife haben die Farbe der Kirschblüte, eine Erinnerung, dass der Frühling auf dem Weg ist. Gewährt keinen Autobusbonus. Februar 2026 Abonnentengegenstand.",
|
||||
"backArmoireHarpsichordText": "Cembalo",
|
||||
"weaponSpecialSpring2026HealerText": "Schneeglöckchen Stab",
|
||||
"weaponSpecialSpring2026HealerNotes": "Eine Gelegenheit für einen Neuanfang liegt direkt vor dir, und mit diesem prächtigen Stab wirst du bereit sein! Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe Frühlingsausrüstung 2026."
|
||||
"backArmoireHarpsichordText": "Cembalo"
|
||||
}
|
||||
|
||||
@@ -187,7 +187,5 @@
|
||||
"learnMore": "Learn More",
|
||||
"translateHabitica": "Translate Habitica",
|
||||
"whatToCallYou": "What should we call you?",
|
||||
"acceptPrivacyTOS": "You confirm that you are at least 18 years old, and that you have read and agree to our <a href='/static/terms' target='_blank'>Terms of Service</a> and <a href='/static/privacy' target='_blank'>Privacy Policy</a>",
|
||||
"emailAddress": "Email address",
|
||||
"emailRequiredForSupport": "We require an email address for user support. Please enter an email address to continue creating your account."
|
||||
"acceptPrivacyTOS": "You confirm that you are at least 18 years old, and that you have read and agree to our <a href='/static/terms' target='_blank'>Terms of Service</a> and <a href='/static/privacy' target='_blank'>Privacy Policy</a>"
|
||||
}
|
||||
|
||||
@@ -243,7 +243,5 @@
|
||||
"playerReportModalBody": "You should only report a player who violates the <%= firstLinkStart %>Community Guidelines<%= linkEnd %> and/or <%= secondLinkStart %>Terms of Service<%= linkEnd %>. Submitting a false report is a violation of Habitica’s Community Guidelines.",
|
||||
"targetUserNotExist": "Target User: '<%= userName %>' does not exist.",
|
||||
"rememberToBeKind": "Please remember to be kind, respectful, and follow the <a href='/static/community-guidelines' target='_blank'>Community Guidelines</a>.",
|
||||
"confirmPurchase": "Confirm Purchase",
|
||||
"avoidSPI": "Avoid SPI",
|
||||
"avoidSPIDetails": "For your privacy, avoid including <%= firstLink %>sensitive personal information<%= linkClose %> (SPI) when using Habitica. Your account data, including tasks, is stored on our servers so you can access it from any device.<br><br>To learn more, review our <%= secondLink %>Privacy Policy<%= linkClose %>."
|
||||
"confirmPurchase": "Confirm Purchase"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"resetAccPop": "Start over, removing all levels, gold, gear, history, and tasks.",
|
||||
"deleteAccount": "Delete Account",
|
||||
"deleteAccPop": "Cancel and remove your Habitica account.",
|
||||
"feedback": "If you'd like to give us feedback, please enter it below - we'd love to hear your feedback! It will be anonymous unless you choose to enter your contact details. Don't speak English well? No problem! Use the language you prefer.",
|
||||
"feedback": "We'd love to hear your feedback! If you'd like to share any, enter it below. It will be anonymous unless you choose to include your contact details.",
|
||||
"feedbackPlaceholder": "Add your feedback",
|
||||
"dataExport": "Data Export",
|
||||
"saveData": "Here are a few options for saving your data.",
|
||||
@@ -82,8 +82,8 @@
|
||||
"resetText2": "Another option is using an <b>Orb of Rebirth</b>, which will reset everything else while preserving your Tasks and Equipment.",
|
||||
"resetTextLocal": "If you're absolutely certain, type your password into the text box below.",
|
||||
"resetTextSocial": "If you're absolutely certain, type <b>\"<%= magicWord %>\"</b> into the text box below.",
|
||||
"deleteLocalAccountText": "<b>Are you sure?</b> This will delete your account forever, and it can never be restored! You will need to register a new account to use Habitica again. Banked or spent Gems will not be refunded. If you're absolutely certain, type your password into the text box below.",
|
||||
"deleteSocialAccountText": "<b>Are you sure?</b> This will delete your account forever, and it can never be restored! You will need to register a new account to use Habitica again. Banked or spent Gems will not be refunded. If you're absolutely certain, type <b>\"<%= magicWord %>\"</b> into the text box below.",
|
||||
"deleteLocalAccountText": "<b>Are you sure?</b> This action is permanent. Deleting your account will remove all of your data, and it cannot be recovered. Gems will not be refunded.<br><br>Please allow up to 24 hours for account deletion to complete, and up to 30 days for analytics data to be removed if you opted in. Once complete, you'll be able to register for a new Habitica account using your previous login information.<br><br>To continue, type your password below.",
|
||||
"deleteSocialAccountText": "<b>Are you sure?</b> This action is permanent. Deleting your account will remove all of your data, and it cannot be recovered. Gems will not be refunded.<br><br>Please allow up to 24 hours for account deletion to complete, and up to 30 days for analytics data to be removed if you opted in. Once complete, you'll be able to register for a new Habitica account using your previous login information.<br><br>To continue, type <%= magicWord %> below.",
|
||||
"API": "API",
|
||||
"APICopied": "API token copied to clipboard.",
|
||||
"APITokenTitle": "API Token",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"brokenChaLink": "Broken Challenge Link",
|
||||
"keepIt": "Keep It",
|
||||
"removeIt": "Remove It",
|
||||
"brokenChallenge": "Broken Challenge Link",
|
||||
"brokenChallenge": "Broken Challenge Link: this task was part of a challenge, but the challenge (or group) has been deleted. What to do with the orphan tasks?",
|
||||
"challengeCompleted": "This challenge has been completed, and the winner was <span class=\"badge\"><%= user %></span>! What to do with the orphan tasks?",
|
||||
"unsubChallenge": "Broken Challenge Link: this task was part of a challenge, but you have unsubscribed from the challenge. What to do with the orphan tasks?",
|
||||
"challenges": "Challenges",
|
||||
|
||||
@@ -114,8 +114,8 @@
|
||||
"unallocated": "Unallocated Stat Points",
|
||||
"autoAllocation": "Automatic Allocation",
|
||||
"autoAllocationPop": "Places Points into Stats according to your preferences, when you level up.",
|
||||
"evenAllocation": "Distribute evenly",
|
||||
"evenAllocationPop": "Assigns the same number of points to each attribute",
|
||||
"evenAllocation": "Distribute Stat Points evenly",
|
||||
"evenAllocationPop": "Assigns the same number of Points to each Stat.",
|
||||
"classAllocation": "Distribute based on class",
|
||||
"classAllocationPop": "Assigns more points to the attributes important to your class",
|
||||
"taskAllocation": "Distribute based on task activity",
|
||||
@@ -123,7 +123,7 @@
|
||||
"distributePoints": "Distribute Unallocated Points",
|
||||
"distributePointsPop": "Assigns all unallocated Stat Points according to the selected allocation scheme.",
|
||||
"warriorText": "Warriors score more and better \"critical hits\", which randomly give bonus Gold, Experience, and drop chance for scoring a task. They also deal heavy damage to boss monsters. Play a Warrior if you find motivation from unpredictable jackpot-style rewards, or want to dish out the hurt in boss Quests!",
|
||||
"wizardText": "Mages learn swiftly, gaining Experience and Levels faster than other classes. They also get a great deal of Mana for using special abilities. Play a Mage if you enjoy the tactical game aspects of Habitica, or if you are strongly motivated by levelling up and unlocking advanced features!",
|
||||
"wizardText": "Mages learn swiftly, gaining Experience and Levels faster than other classes. They also get a great deal of Mana for using special abilities. Play a Mage if you enjoy the tactical game aspects of Habitica, or if you are strongly motivated by leveling up and unlocking advanced features!",
|
||||
"mageText": "Mages learn swiftly, gaining Experience and Levels faster than other classes. They also get a great deal of Mana for using special abilities. Play a Mage if you enjoy the tactical game aspects of Habitica, or if you are strongly motivated by levelling up and unlocking advanced features!",
|
||||
"rogueText": "Rogues love to accumulate wealth, gaining more Gold than anyone else, and are adept at finding random items. Their iconic Stealth ability lets them duck the consequences of missed Dailies. Play a Rogue if you find strong motivation from Rewards and Achievements, striving for loot and badges!",
|
||||
"healerText": "Healers stand impervious against harm, and extend that protection to others. Missed Dailies and bad Habits don't faze them much, and they have ways to recover Health from failure. Play a Healer if you enjoy assisting others in your Party, or if the idea of cheating Death through hard work inspires you!",
|
||||
@@ -178,7 +178,7 @@
|
||||
"mainHand": "Main-Hand",
|
||||
"offHand": "Off-Hand",
|
||||
"statPoints": "Stat Points",
|
||||
"pts": "PTS",
|
||||
"pts": "pts",
|
||||
"chatCastSpellUser": "<%= username %> casts <%= spell %> on <%= target %>.",
|
||||
"chatCastSpellParty": "<%= username %> casts <%= spell %> for the party.",
|
||||
"purchasePetItemConfirm": "This purchase would exceed the number of items you need to hatch all possible <%= itemText %> pets. Are you sure?",
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
"subscriptionDetail48": "Are there any changes to other subscription benefits, like Mystery Gear Sets?",
|
||||
"subscriptionDetail480": "These changes only affect Mystic Hourglasses and subscriber Gems. All other benefits will remain the same.",
|
||||
"subscriptionPara2": "If you have any questions not covered by the answers above, you can always contact our team at <%= mailto %>.",
|
||||
"contentAnswer01": "<strong>Grand Galas are being extended</strong> to be active throughout the whole season, along with all their Class gear, Avatar Customisations, and other goodies.",
|
||||
"contentAnswer01": "<strong>Grand Galas are being extended</strong> to be active throughout the whole season, along with all their Class gear, Avatar Customizations, and other goodies.",
|
||||
"contentAnswer10": "Habitica has been around since 2013 (wow!) and over time we’ve released thousands of items players can collect. This can be overwhelming, especially for new players. We want to be sure that we showcase everything we have to offer, and that excellent items released earlier in our history aren’t overlooked.",
|
||||
"contentAnswer11": "When new players join between Grand Galas they are often unaware of these events and miss out on the fun. We want to be sure all new players can join in on our seasonal festivities no matter when they choose to start their journeys.",
|
||||
"subscriptionDetail00": "All subscribers, including those with gifted subscriptions, will receive 1 Mystic Hourglass at the start of each month they have subscriber benefits.",
|
||||
@@ -192,7 +192,7 @@
|
||||
"faqQuestion63": "How do unassigned tasks work?",
|
||||
"webFaqAnswer63": "Unassigned tasks can be completed by any member. For example, taking out the trash. Whoever takes out the trash can complete the unassigned task and it will show as completed for everyone.",
|
||||
"faqQuestion64": "How does the synchronised day reset work?",
|
||||
"webFaqAnswer64": "Shared tasks will reset at the same time for everyone to keep the shared task board in sync. This time is visible on the shared task board and is determined by the Group Plan leader’s day start time. Because shared tasks reset automatically, you will not get a chance to complete yesterday’s uncompleted shared Dailies when you check in the next morning.\n\nShared Dailies will not do damage if they are missed, however they will degrade in colour to help visualise progress.",
|
||||
"webFaqAnswer64": "Shared tasks will reset at the same time for everyone to keep the shared task board in sync. This time is visible on the shared task board and is determined by the Group Plan leader’s day start time. Because shared tasks reset automatically, you will not get a chance to complete yesterday’s uncompleted shared Dailies when you check in the next morning.\n\nShared Dailies will not do damage if they are missed, however they will degrade in colour to help visualize progress.",
|
||||
"webFaqAnswer65": "While the mobile apps don’t fully support all Group Plans functionality yet, you can still complete shared tasks from the iOS and Android apps!\n\nOn Android, you can tap your Display Name at the top of the screen when viewing your tasks to switch to your shared task board. From there you can view members, access your chat, and create, complete, or assign tasks.\n\nYou can also switch on a preference to copy shared tasks to your personal task board so you can complete all your tasks from one place.\n\nTo do this on the mobile apps:\n * Open Settings and switch on “Copy shared tasks”\n\nTo do this on Habitica’s website:\n * Navigate to your Group Plan and switch on the “Copy tasks” toggle on the shared task board",
|
||||
"faqQuestion66": "What’s the difference between a Group Plan’s shared tasks and Challenge tasks?",
|
||||
"webFaqAnswer66": "Group Plan shared task boards are more dynamic than Challenges, in that they can constantly be updated and interacted with. Challenges are great if you have one set of tasks to send out to many people.\n\nGroup Plans are also a paid feature, while Challenges are available free to everyone.\n\nYou cannot assign specific tasks in Challenges, and Challenges do not have a shared day reset. In general, Challenges offer less control and direct interaction.",
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
"invalidReqParams": "Invalid request parameters.",
|
||||
"memberIdRequired": "\"member\" must be a valid UUID.",
|
||||
"heroIdRequired": "\"heroId\" must be a valid UUID.",
|
||||
"cannotFulfillReq": "This email address is already in use. You can try logging in or use a different email to register. If you need help, reach out to admin@habitica.com.",
|
||||
"cannotFulfillReq": "Please enter a valid email address. Email admin@habitica.com if this error persists.",
|
||||
"modelNotFound": "This model does not exist.",
|
||||
"signUpWithSocial": "Continue with <%= social %>",
|
||||
"loginWithSocial": "Log in with <%= social %>",
|
||||
@@ -163,7 +163,7 @@
|
||||
"schoolAndWork": "School and Work",
|
||||
"schoolAndWorkDesc": "Whether you're preparing a report for your teacher or your boss, it's easy to keep track of your progress as you tackle your toughest tasks.",
|
||||
"muchmuchMore": "And much, much more!",
|
||||
"muchmuchMoreDesc": "Our fully customisable task list means that you can shape Habitica to fit your personal goals. Work on creative projects, emphasise self-care, or pursue a different dream -- it's all up to you.",
|
||||
"muchmuchMoreDesc": "Our fully customizable task list means that you can shape Habitica to fit your personal goals. Work on creative projects, emphasise self-care, or pursue a different dream -- it's all up to you.",
|
||||
"levelUpAnywhere": "Level Up Anywhere",
|
||||
"levelUpAnywhereDesc": "Our mobile apps make it simple to keep track of your tasks on-the-go. Accomplish your goals with a single tap, no matter where you are.",
|
||||
"joinMany": "Join over <%= userCountInMillions %> million people having fun while accomplishing their goals!",
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
"weaponSpecialSummer2015MageNotes": "Hidden power glimmers in the jewels of this staff. Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition 2015 Summer Gear.",
|
||||
"weaponSpecialSummer2015HealerText": "Wand of the Waves",
|
||||
"weaponSpecialSummer2015HealerNotes": "Cures seasickness and sea sickness! Increases Intelligence by <%= int %>. Limited Edition 2015 Summer Gear.",
|
||||
"weaponSpecialFall2015RogueText": "Bat-tle Axe",
|
||||
"weaponSpecialFall2015RogueText": "Bat-tle Ax",
|
||||
"weaponSpecialFall2015RogueNotes": "Fearsome To Do's cower before the flapping of this axe. Increases Strength by <%= str %>. Limited Edition 2015 Autumn Gear.",
|
||||
"weaponSpecialFall2015WarriorText": "Wooden Plank",
|
||||
"weaponSpecialFall2015WarriorNotes": "Great for elevating things in cornfields and/or smacking tasks. Increases Strength by <%= str %>. Limited Edition 2015 Autumn Gear.",
|
||||
@@ -246,7 +246,7 @@
|
||||
"weaponSpecialWinter2018WarriorText": "Holiday Bow Hammer",
|
||||
"weaponSpecialWinter2018WarriorNotes": "The sparkly appearance of this bright weapon will dazzle your enemies as you swing it! Increases Strength by <%= str %>. Limited Edition 2017-2018 Winter Gear.",
|
||||
"weaponSpecialWinter2018MageText": "Holiday Confetti",
|
||||
"weaponSpecialWinter2018MageNotes": "Magic—and glitter—is in the air! Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition 2017-2018 Winter Gear.",
|
||||
"weaponSpecialWinter2018MageNotes": "Magic--and glitter--is in the air! Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition 2017-2018 Winter Gear.",
|
||||
"weaponSpecialWinter2018HealerText": "Mistletoe Wand",
|
||||
"weaponSpecialWinter2018HealerNotes": "This mistletoe ball is sure to enchant and delight passersby! Increases Intelligence by <%= int %>. Limited Edition 2017-2018 Winter Gear.",
|
||||
"weaponSpecialSpring2018RogueText": "Buoyant Bullrush",
|
||||
@@ -326,7 +326,7 @@
|
||||
"weaponArmoireBasicLongbowText": "Basic Longbow",
|
||||
"weaponArmoireBasicLongbowNotes": "A serviceable hand-me-down bow. Increases Strength by <%= str %>. Enchanted Armoire: Basic Archer Set (Item 1 of 3).",
|
||||
"weaponArmoireHabiticanDiplomaText": "Habitican Diploma",
|
||||
"weaponArmoireHabiticanDiplomaNotes": "A certificate of significant achievement—well done! Increases Intelligence by <%= int %>. Enchanted Armoire: Graduate Set (Item 1 of 3).",
|
||||
"weaponArmoireHabiticanDiplomaNotes": "A certificate of significant achievement -- well done! Increases Intelligence by <%= int %>. Enchanted Armoire: Graduate Set (Item 1 of 3).",
|
||||
"weaponArmoireSandySpadeText": "Sandy Spade",
|
||||
"weaponArmoireSandySpadeNotes": "A tool for digging, as well as flicking sand into the eyes of enemy monsters. Increases Strength by <%= str %>. Enchanted Armoire: Seaside Set (Item 1 of 3).",
|
||||
"weaponArmoireCannonText": "Cannon",
|
||||
@@ -806,7 +806,7 @@
|
||||
"armorArmoireCoverallsOfBookbindingText": "Coveralls of Bookbinding",
|
||||
"armorArmoireCoverallsOfBookbindingNotes": "Everything you need in a set of coveralls, including pockets for everything. A pair of goggles, loose change, a golden ring... Increases Constitution by <%= con %> and Perception by <%= per %>. Enchanted Armoire: Bookbinder Set (Item 2 of 4).",
|
||||
"armorArmoireRobeOfSpadesText": "Robe of Spades",
|
||||
"armorArmoireRobeOfSpadesNotes": "These luxuriant robes conceal hidden pockets for treasures or weapons—your choice! Increases Strength by <%= str %>. Enchanted Armoire: Ace of Spades Set (Item 2 of 3).",
|
||||
"armorArmoireRobeOfSpadesNotes": "These luxuriant robes conceal hidden pockets for treasures or weapons--your choice! Increases Strength by <%= str %>. Enchanted Armoire: Ace of Spades Set (Item 2 of 3).",
|
||||
"armorArmoireSoftBlueSuitText": "Soft Blue Suit",
|
||||
"armorArmoireSoftBlueSuitNotes": "Blue is a calming colour. So calming, some even wear this soft outfit to sleep... zZz. Increases Intelligence by <%= int %> and Perception by <%= per %>. Enchanted Armoire: Blue Loungewear Set (Item 2 of 3).",
|
||||
"armorArmoireSoftGreenSuitText": "Soft Green Suit",
|
||||
@@ -876,7 +876,7 @@
|
||||
"headSpecialLunarWarriorHelmText": "Lunar Warrior Helm",
|
||||
"headSpecialLunarWarriorHelmNotes": "The power of the moon will strengthen you in battle! Increases Strength and Intelligence by <%= attrs %> each.",
|
||||
"headSpecialMammothRiderHelmText": "Mammoth Rider Helm",
|
||||
"headSpecialMammothRiderHelmNotes": "Don't let its fluffiness fool you—this hat will grant you piercing powers of perception! Increases Perception by <%= per %>.",
|
||||
"headSpecialMammothRiderHelmNotes": "Don't let its fluffiness fool you--this hat will grant you piercing powers of perception! Increases Perception by <%= per %>.",
|
||||
"headSpecialPageHelmText": "Page Helm",
|
||||
"headSpecialPageHelmNotes": "Chainmail: for the stylish AND the practical. Increases Perception by <%= per %>.",
|
||||
"headSpecialRoguishRainbowMessengerHoodText": "Roguish Rainbow Messenger Hood",
|
||||
@@ -960,7 +960,7 @@
|
||||
"headSpecialFall2015RogueText": "Bat-tle Wings",
|
||||
"headSpecialFall2015RogueNotes": "Echolocate your enemies with this powerful helm! Increases Perception by <%= per %>. Limited Edition 2015 Autumn Gear.",
|
||||
"headSpecialFall2015WarriorText": "Scarecrow Hat",
|
||||
"headSpecialFall2015WarriorNotes": "Everyone would want this hat—if they only had a brain. Increases Strength by <%= str %>. Limited Edition 2015 Autumn Gear.",
|
||||
"headSpecialFall2015WarriorNotes": "Everyone would want this hat--if they only had a brain. Increases Strength by <%= str %>. Limited Edition 2015 Autumn Gear.",
|
||||
"headSpecialFall2015MageText": "Stitched Hat",
|
||||
"headSpecialFall2015MageNotes": "Every stitch in this hat augments its power. Increases Perception by <%= per %>. Limited Edition 2015 Autumn Gear.",
|
||||
"headSpecialFall2015HealerText": "Hat of Frog",
|
||||
@@ -1435,7 +1435,7 @@
|
||||
"shieldArmoireMushroomDruidShieldText": "Mushroom Druid Shield",
|
||||
"shieldArmoireMushroomDruidShieldNotes": "Though made from a mushroom, there's nothing mushy about this tough shield! Increases Constitution by <%= con %> and Strength by <%= str %>. Enchanted Armoire: Mushroom Druid Set (Item 3 of 3).",
|
||||
"shieldArmoireFestivalParasolText": "Festival Parasol",
|
||||
"shieldArmoireFestivalParasolNotes": "This lightweight parasol will shield you from the glare—whether it's from the sun or from dark red Dailies! Increases Constitution by <%= con %>. Enchanted Armoire: Festival Attire Set (Item 2 of 3).",
|
||||
"shieldArmoireFestivalParasolNotes": "This lightweight parasol will shield you from the glare--whether it's from the sun or from dark red Dailies! Increases Constitution by <%= con %>. Enchanted Armoire: Festival Attire Set (Item 2 of 3).",
|
||||
"shieldArmoireVikingShieldText": "Viking Shield",
|
||||
"shieldArmoireVikingShieldNotes": "This sturdy shield of wood and hide can stand up to the most daunting of foes. Increases Perception by <%= per %> and Intelligence by <%= int %>. Enchanted Armoire: Viking Set (Item 3 of 3).",
|
||||
"shieldArmoireSwanFeatherFanText": "Swan Feather Fan",
|
||||
@@ -2042,7 +2042,7 @@
|
||||
"weaponSpecialSpring2020WarriorText": "Sharpened Wing",
|
||||
"weaponSpecialSpring2020RogueNotes": "You'll strike so fast it'll look even MORE blue! Increases Strength by <%= str %>. Limited Edition 2020 Spring Gear.",
|
||||
"weaponSpecialSpring2020RogueText": "Lazurite Blade",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -2092,7 +2092,7 @@
|
||||
"shieldSpecialSummer2020WarriorText": "Huge Trout Scale",
|
||||
"armorSpecialSummer2020WarriorNotes": "You'll be the bright fish in a dull stream, with these dazzling scales! Increases Constitution by <%= con %>. Limited Edition 2020 Summer Gear.",
|
||||
"armorSpecialSummer2020WarriorText": "Rainbow Trout Tail",
|
||||
"armorSpecialSummer2020RogueNotes": "A crocodile makes the perfect Rogue, waiting for the perfect moment to strike. Borrow their skills—and their explosive speed. Increases Perception by <%= per %>. Limited Edition 2020 Summer Gear.",
|
||||
"armorSpecialSummer2020RogueNotes": "A crocodile makes the perfect Rogue, waiting for the perfect moment to strike. Borrow their skills--and their explosive speed. Increases Perception by <%= per %>. Limited Edition 2020 Summer Gear.",
|
||||
"armorSpecialSummer2020RogueText": "Crocodile Disguise",
|
||||
"weaponSpecialSummer2020HealerNotes": "As the currents wear away sharp edges, so shall your magic soften your friends' pain. Increases Intelligence by <%= int %>. Limited Edition 2020 Summer Gear.",
|
||||
"weaponSpecialSummer2020HealerText": "Frosted Glass Rod",
|
||||
@@ -2278,7 +2278,7 @@
|
||||
"weaponSpecialFall2021RogueText": "Dripping Goo",
|
||||
"weaponSpecialFall2021RogueNotes": "What on Earth did you get into? When people say Rogues have sticky fingers, this is not what they mean! Increases Strength by <%= str %>. Limited Edition 2021 Autumn Gear.",
|
||||
"weaponSpecialFall2021WarriorText": "Horse Rider's Axe",
|
||||
"weaponSpecialFall2021WarriorNotes": "This stylised, single-bladed axe is ideal for chopping... pumpkins! Increases Strength by <%= str %>. Limited Edition 2021 Autumn Gear.",
|
||||
"weaponSpecialFall2021WarriorNotes": "This stylized, single-bladed axe is ideal for chopping... pumpkins! Increases Strength by <%= str %>. Limited Edition 2021 Autumn Gear.",
|
||||
"weaponSpecialFall2021MageText": "Staff of Pure Thought",
|
||||
"weaponSpecialSummer2022RogueNotes": "If you're in a pinch, don't hesitate to show these fearsome claws! Increases Strength by <%= str %>. Limited Edition 2022 Summer Gear.",
|
||||
"weaponSpecialSummer2022RogueText": "Crab Claw",
|
||||
@@ -2435,7 +2435,7 @@
|
||||
"armorSpecialSummer2022MageNotes": "When wearing this armour, you will glide easily through your work like the manta ray glides through water. Increases Intelligence by <%= int %>. Limited Edition 2022 Summer Gear.",
|
||||
"armorSpecialFall2022RogueNotes": "Whether you’re swimming, sneaking, or wrestling, you will be safe in this armour. Increases Perception by <%= per %>. Limited Edition 2022 Autumn Gear.",
|
||||
"armorSpecialSpring2023WarriorText": "Hummingbird Armour",
|
||||
"weaponArmoireRidingBroomNotes": "Run all your most magical errands on this fine broom—or, just take it for a joyride around the neighbourhood. Whee! Increases Strength by <%= str %> and Intelligence by <%= int %>. Enchanted Armoire: Spooky Sorcery Set (Item 1 of 3)",
|
||||
"weaponArmoireRidingBroomNotes": "Run all your most magical errands on this fine broom--or, just take it for a joyride around the neighbourhood. Whee! Increases Strength by <%= str %> and Intelligence by <%= int %>. Enchanted Armoire: Spooky Sorcery Set (Item 1 of 3)",
|
||||
"armorSpecialSummer2021WarriorText": "Finny Armour",
|
||||
"armorSpecialSummer2023WarriorNotes": "Goldfish Warriors actually have excellent memories because they always keep their Dailies and To Do's organised in lists. Increases Constitution by <%= con %>. Limited Edition 2023 Summer Gear.",
|
||||
"armorSpecialFall2023RogueNotes": "You were lured with the promise of a nice hot soak... Joke's on you! Increases Perception by <%= per %>. Limited Edition 2023 Autumn Gear.",
|
||||
@@ -2557,18 +2557,5 @@
|
||||
"weaponMystery202212Text": "Glacial Wand",
|
||||
"weaponSpecialWinter2024MageNotes": "Thanks to a generous, magical narwhal that sensed your great abilities, you have been gifted a tusk that lets you sense changes happening around you. Increases Intelligence by <%= int %>. Limited Edition Winter 2023-2024 Gear.",
|
||||
"weaponSpecialSpring2024RogueNotes": "Challenges that are as hard as ice can be sliced into smaller pieces. Increases Strength by <%= str %>. Limited Edition Spring 2024 Gear.",
|
||||
"backMystery202602Notes": "These fluffy tails are the colour of cherry blossoms, a reminder that spring is on the way! Confers no benefit. February 2026 Subscriber Item.",
|
||||
"armorSpecialSpring2026RogueText": "Birch Bark Armour",
|
||||
"armorSpecialSpring2026WarriorText": "Frog Armour",
|
||||
"armorMystery202512Text": "Cookie Champion Armour",
|
||||
"shieldArmoireSoftWhitePillowNotes": "The organised warrior packs a pillow for any expedition. Protect yourself from overlooked obligations… even while you nap. Increases Intelligence and Perception by <%= attrs %> each. Enchanted Armoire: White Loungewear Set (Item 3 of 3)",
|
||||
"headSpecialSummer2025MageNotes": "All will be mesmerised by the way your fins move with the currents. Increases Perception by <%= per %>. Limited Edition Summer 2025 Gear.",
|
||||
"headSpecialSpring2025HealerNotes": "This flower symbolises birth, love, and new beginnings! It also provides a beautiful scent you can enjoy as you work on your tasks. Increases Intelligence by <%= int %>. Limited Edition Spring 2025 Gear.",
|
||||
"armorArmoireFancyPirateSuitNotes": "Wear this fine jacket well as you organise your ship’s library or talk it through as a crew. Increases Constitution and Intelligence by <%= attrs %> each. Enchanted Armoire: Fancy Pirate Set (Item 1 of 3).",
|
||||
"armorArmoireJewelersApronNotes": "This heavy-duty apron is just the thing to wear when you feel creative. Best of all, there are dozens of small pockets to hold everything you need. Increases Intelligence by <%= int %>. Enchanted Armoire: Jeweller Set (Item 1 of 4).",
|
||||
"shieldArmoireJewelersPliersText": "Jeweller's Pliers",
|
||||
"shieldArmoireJewelersPliersNotes": "They cut, twist, pinch, and more. This tool can help you create whatever you can imagine. Increases Strength by <%= str %>. Enchanted Armoire: Jeweller Set (Item 3 of 4).",
|
||||
"weaponArmoireFinelyCutGemNotes": "What a find! This stunning, precision-cut gem will be the prize of your collection. And it might contain some special magic, just waiting for you to tap into it. Increases Constitution by <%= con %>. Enchanted Armoire: Jeweller Set (Item 4 of 4).",
|
||||
"eyewearArmoireJewelersEyeLoupeText": "Jeweller's Eye Loupe",
|
||||
"eyewearArmoireJewelersEyeLoupeNotes": "This eye loupe magnifies what you’re working on so you can see absolutely every detail. Increases Perception by <%= per %>. Enchanted Armoire: Jeweller Set (Item 2 of 4)."
|
||||
"backMystery202602Notes": "These fluffy tails are the colour of cherry blossoms, a reminder that spring is on the way! Confers no benefit. February 2026 Subscriber Item."
|
||||
}
|
||||
|
||||
@@ -263,24 +263,24 @@
|
||||
"summer2024SeaAnemoneMageSet": "Sea Anemone Set (Mage)",
|
||||
"summer2024SeaSnailHealerSet": "Sea Snail Set (Healer)",
|
||||
"summer2024NudibranchRogueSet": "Nudibranch Set (Rogue)",
|
||||
"winter2025AuroraMageSet": "Aurora Set (Mage)",
|
||||
"winter2025SnowRogueSet": "Snow Set (Rogue)",
|
||||
"spring2025SunshineWarriorSet": "Sunshine Set (Warrior)",
|
||||
"spring2025CrystalPointRogueSet": "Crystal Point Set (Rogue)",
|
||||
"spring2025PlumeriaHealerSet": "Plumeria Set (Healer)",
|
||||
"spring2025MantisMageSet": "Mantis Set (Mage)",
|
||||
"winter2025AuroraMageSet": "Aurora Mage Set",
|
||||
"winter2025SnowRogueSet": "Snow Rogue Set",
|
||||
"spring2025SunshineWarriorSet": "Sunshine Warrior Set",
|
||||
"spring2025CrystalPointRogueSet": "Crystal Point Rogue Set",
|
||||
"spring2025PlumeriaHealerSet": "Plumeria Healer Set",
|
||||
"spring2025MantisMageSet": "Mantis Mage Set",
|
||||
"fall2024FieryImpWarriorSet": "Fiery Imp Set (Warrior)",
|
||||
"fall2024UnderworldSorcerorMageSet": "Underworld Sorceror Set (Mage)",
|
||||
"fall2024SpaceInvaderHealerSet": "Space Invader Set (Healer)",
|
||||
"fall2024BlackCatRogueSet": "Black Cat Set (Rogue)",
|
||||
"winter2025MooseWarriorSet": "Moose Set (Warrior)",
|
||||
"winter2025StringLightsHealerSet": "String Lights Set (Healer)",
|
||||
"fall2025SasquatchWarriorSet": "Sasquatch Set (Warrior)",
|
||||
"fall2025SkeletonRogueSet": "Skeleton Set (Rogue)",
|
||||
"fall2025KoboldHealerSet": "Kobold Set (Healer)",
|
||||
"fall2025MaskedGhostMageSet": "Masked Ghost Set (Mage)",
|
||||
"summer2025ScallopWarriorSet": "Scallop Set (Warrior)",
|
||||
"summer2025SquidRogueSet": "Squid Set (Rogue)",
|
||||
"summer2025SeaAngelHealerSet": "Sea Angel Set (Healer)",
|
||||
"summer2025FairyWrasseMageSet": "Fairy Wrasse Set (Mage)"
|
||||
"winter2025MooseWarriorSet": "Moose Warrior Set",
|
||||
"winter2025StringLightsHealerSet": "String Lights Healer Set",
|
||||
"fall2025SasquatchWarriorSet": "Sasquatch Warrior Set",
|
||||
"fall2025SkeletonRogueSet": "Skeleton Rogue Set",
|
||||
"fall2025KoboldHealerSet": "Kobold Healer Set",
|
||||
"fall2025MaskedGhostMageSet": "Masked Ghost Mage Set",
|
||||
"summer2025ScallopWarriorSet": "Scallop Warrior Set",
|
||||
"summer2025SquidRogueSet": "Squid Rogue Set",
|
||||
"summer2025SeaAngelHealerSet": "Sea Angel Healer Set",
|
||||
"summer2025FairyWrasseMageSet": "Fairy Wrasse Mage Set"
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
"questTRexUndeadBoss": "Skeletal Tyrannosaur",
|
||||
"questTRexUndeadRageTitle": "Skeleton Healing",
|
||||
"questTRexUndeadRageDescription": "This bar fills when you don't complete your Dailies. When it is full, the Skeletal Tyrannosaur will heal 30% of its remaining health!",
|
||||
"questTRexUndeadRageEffect": "Skeletal Tyrannosaur uses SKELETON HEALING!\n\nThe monster lets forth an unearthly roar, and some of its damaged bones knit back together!",
|
||||
"questTRexUndeadRageEffect": "`Skeletal Tyrannosaur uses SKELETON HEALING!`\n\nThe monster lets forth an unearthly roar, and some of its damaged bones knit back together!",
|
||||
"questTRexDropTRexEgg": "Tyrannosaur (Egg)",
|
||||
"questTRexUnlockText": "Unlocks Tyrannosaur Eggs for purchase in the Market",
|
||||
"questRockText": "Escape the Cave Creature",
|
||||
@@ -241,7 +241,7 @@
|
||||
"questDilatoryDistress2Boss": "Water Skull Swarm",
|
||||
"questDilatoryDistress2RageTitle": "Swarm Respawn",
|
||||
"questDilatoryDistress2RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Water Skull Swarm will heal 30% of its remaining health!",
|
||||
"questDilatoryDistress2RageEffect": "Water Skull Swarm uses SWARM RESPAWN!\n\nEmboldened by their victories, more skulls pour forth from the crevasse, bolstering the swarm!",
|
||||
"questDilatoryDistress2RageEffect": "`Water Skull Swarm uses SWARM RESPAWN!`\n\nEmboldened by their victories, more skulls pour forth from the crevasse, bolstering the swarm!",
|
||||
"questDilatoryDistress2DropSkeletonPotion": "Skeleton Hatching Potion",
|
||||
"questDilatoryDistress2DropCottonCandyBluePotion": "Candyfloss Blue Hatching Potion",
|
||||
"questDilatoryDistress2DropHeadgear": "Fire Coral Circlet (Headgear)",
|
||||
@@ -253,7 +253,7 @@
|
||||
"questDilatoryDistress3DropWeapon": "Trident of Crashing Tides (Weapon)",
|
||||
"questDilatoryDistress3DropShield": "Moonpearl Shield (Off-Hand Item)",
|
||||
"questCheetahText": "Such a Cheetah",
|
||||
"questCheetahNotes": "As you hike across the Sloensteadi Savannah with your friends @PainterProphet, @tivaquinn, @Unruly Hyena, and @Crawford, you're startled to see a Cheetah screeching past with a new Habitican clamped in its jaws. Under the Cheetah's scorching paws, tasks burn away as though complete—before anyone has the chance to actually finish them! The Habitican sees you and yells, \"Please help me! This Cheetah is making me level too quickly, but I'm not getting anything done. I want to slow down and enjoy the game. Make it stop!\" You fondly remember your own fledgling days, and know that you have to help the newbie by stopping the Cheetah!",
|
||||
"questCheetahNotes": "As you hike across the Sloensteadi Savannah with your friends @PainterProphet, @tivaquinn, @Unruly Hyena, and @Crawford, you're startled to see a Cheetah screeching past with a new Habitican clamped in its jaws. Under the Cheetah's scorching paws, tasks burn away as though complete -- before anyone has the chance to actually finish them! The Habitican sees you and yells, \"Please help me! This Cheetah is making me level too quickly, but I'm not getting anything done. I want to slow down and enjoy the game. Make it stop!\" You fondly remember your own fledgling days, and know that you have to help the newbie by stopping the Cheetah!",
|
||||
"questCheetahCompletion": "The new Habitican is breathing heavily after the wild ride, but thanks you and your friends for your help. \"I'm glad that Cheetah won't be able to grab anyone else. It did leave some Cheetah eggs for us, so maybe we can raise them into more trustworthy pets!\"",
|
||||
"questCheetahBoss": "Cheetah",
|
||||
"questCheetahDropCheetahEgg": "Cheetah (Egg)",
|
||||
@@ -277,7 +277,7 @@
|
||||
"questBurnoutBossRageSeasonalShop": "`Burnout uses EXHAUST STRIKE!`\n\nAhh!!! Our incomplete Dailies have fed the flames of Burnout, and now it has enough energy to strike again! It lets loose a gout of spectral flame that sears the Seasonal Shop. You're horrified to see that the cheery Seasonal Sorceress has been transformed into a drooping Exhaust Spirit.\n\nWe have to rescue our NPCs! Hurry, Habiticans, complete your tasks and defeat Burnout before it strikes for a third time!",
|
||||
"questBurnoutBossRageTavern": "`Burnout uses EXHAUST STRIKE!`\n\nMany Habiticans have been hiding from Burnout in the Tavern, but no longer! With a screeching howl, Burnout rakes the Tavern with its white-hot hands. As the Tavern patrons flee, Daniel is caught in Burnout's grip, and transforms into an Exhaust Spirit right in front of you!\n\nThis hot-headed horror has gone on for too long. Don't give up... we're so close to vanquishing Burnout for once and for all!",
|
||||
"questFrogText": "Swamp of the Clutter Frog",
|
||||
"questFrogNotes": "As you and your friends are slogging through the Swamps of Stagnation, @starsystemic points at a large sign. \"Stay on the path—if you can.\"<br><br>\"Surely that isn't hard!\" @RosemonkeyCT says. \"It's broad and clear.\"<br><br>But as you continue, you notice that path is gradually overtaken by the muck of the swamp, laced with bits of strange blue debris and clutter, until it's impossible to proceed.<br><br>As you look around, wondering how it got this messy, @Jon Arjinborn shouts, \"Look out!\" An angry frog leaps from the sludge, clad in dirty laundry and lit by blue fire. You will have to overcome this poisonous Clutter Frog to progress!",
|
||||
"questFrogNotes": "As you and your friends are slogging through the Swamps of Stagnation, @starsystemic points at a large sign. \"Stay on the path -- if you can.\"<br><br>\"Surely that isn't hard!\" @RosemonkeyCT says. \"It's broad and clear.\"<br><br>But as you continue, you notice that path is gradually overtaken by the muck of the swamp, laced with bits of strange blue debris and clutter, until it's impossible to proceed.<br><br>As you look around, wondering how it got this messy, @Jon Arjinborn shouts, \"Look out!\" An angry frog leaps from the sludge, clad in dirty laundry and lit by blue fire. You will have to overcome this poisonous Clutter Frog to progress!",
|
||||
"questFrogCompletion": "The frog cowers back into the muck, defeated. As it slinks away, the blue slime fades, leaving the way ahead clear.<br><br>Sitting in the middle of the path are three pristine eggs. \"You can even see the tiny tadpoles through the clear casing!\" @Breadstrings says. \"Here, you should take them.\"",
|
||||
"questFrogBoss": "Clutter Frog",
|
||||
"questFrogDropFrogEgg": "Frog (Egg)",
|
||||
@@ -313,7 +313,7 @@
|
||||
"questSnailDropSnailEgg": "Snail (Egg)",
|
||||
"questSnailUnlockText": "Unlocks Snail Eggs for purchase in the Market",
|
||||
"questBewilderText": "The Be-Wilder",
|
||||
"questBewilderNotes": "The party begins like any other.<br><br>The appetisers are excellent, the music is swinging, and even the dancing elephants have become routine. Habiticans laugh and frolic amid the overflowing floral centrepieces, happy to have a distraction from their least-favourite tasks, and the April Fool whirls among them, eagerly providing an amusing trick here and a witty twist there.<br><br>As the Mistiflying clock tower strikes midnight, the April Fool leaps onto the stage to give a speech.<br><br>“Friends! Enemies! Tolerant acquaintances! Lend me your ears.” The crowd chuckles as animal ears sprout from their heads, and they pose with their new accessories.<br><br>“As you know,” the Fool continues, “my confusing illusions usually only last a single day. But I’m pleased to announce that I’ve discovered a shortcut that will guarantee us non-stop fun, without having to deal with the pesky weight of our responsibilities. Charming Habiticans, meet my magical new friend... the Be-Wilder!”<br><br>Lemoness pales suddenly, dropping her hors d'oeuvres. “Wait! Don’t trust—”<br><br>But suddenly mists are pouring into the room, glittering and thick, and they swirl around the April Fool, coalescing into cloudy feathers and a stretching neck. The crowd is speechless as a monstrous bird unfolds before them, its wings shimmering with illusions. It lets out a horrible screeching laugh.<br><br>“Oh, it has been ages since a Habitican has been foolish enough to summon me! How wonderful it feels, to have a tangible form at last.”<br><br>Buzzing in terror, the magic bees of Mistiflying flee the floating city, which sags from the sky. One by one, the brilliant spring flowers wither up and wisp away.<br><br>“My dearest friends, why so alarmed?” crows the Be-Wilder, beating its wings. “There’s no need to toil for your rewards any more. I’ll just give you all the things that you desire!”<br><br>A rain of coins pours from the sky, hammering into the ground with brutal force, and the crowd screams and flees for cover. “Is this a joke?” Baconsaur shouts, as the gold smashes through windows and shatters roof shingles.<br><br>PainterProphet ducks as lightning bolts crackle overhead, and fog blots out the sun. “No! This time, I don’t think it is!”<br><br>Quickly, Habiticans, don’t let this World Boss distract us from our goals! Stay focused on the tasks that you need to complete so we can rescue Mistiflying—and hopefully, ourselves.",
|
||||
"questBewilderNotes": "The party begins like any other.<br><br>The appetisers are excellent, the music is swinging, and even the dancing elephants have become routine. Habiticans laugh and frolic amid the overflowing floral centrepieces, happy to have a distraction from their least-favourite tasks, and the April Fool whirls among them, eagerly providing an amusing trick here and a witty twist there.<br><br>As the Mistiflying clock tower strikes midnight, the April Fool leaps onto the stage to give a speech.<br><br>“Friends! Enemies! Tolerant acquaintances! Lend me your ears.” The crowd chuckles as animal ears sprout from their heads, and they pose with their new accessories.<br><br>“As you know,” the Fool continues, “my confusing illusions usually only last a single day. But I’m pleased to announce that I’ve discovered a shortcut that will guarantee us non-stop fun, without having to deal with the pesky weight of our responsibilities. Charming Habiticans, meet my magical new friend... the Be-Wilder!”<br><br>Lemoness pales suddenly, dropping her hors d'oeuvres. “Wait! Don’t trust--”<br><br>But suddenly mists are pouring into the room, glittering and thick, and they swirl around the April Fool, coalescing into cloudy feathers and a stretching neck. The crowd is speechless as a monstrous bird unfolds before them, its wings shimmering with illusions. It lets out a horrible screeching laugh.<br><br>“Oh, it has been ages since a Habitican has been foolish enough to summon me! How wonderful it feels, to have a tangible form at last.”<br><br>Buzzing in terror, the magic bees of Mistiflying flee the floating city, which sags from the sky. One by one, the brilliant spring flowers wither up and wisp away.<br><br>“My dearest friends, why so alarmed?” crows the Be-Wilder, beating its wings. “There’s no need to toil for your rewards any more. I’ll just give you all the things that you desire!”<br><br>A rain of coins pours from the sky, hammering into the ground with brutal force, and the crowd screams and flees for cover. “Is this a joke?” Baconsaur shouts, as the gold smashes through windows and shatters roof shingles.<br><br>PainterProphet ducks as lightning bolts crackle overhead, and fog blots out the sun. “No! This time, I don’t think it is!”<br><br>Quickly, Habiticans, don’t let this World Boss distract us from our goals! Stay focused on the tasks that you need to complete so we can rescue Mistiflying -- and hopefully, ourselves.",
|
||||
"questBewilderCompletion": "<strong>The Be-Wilder is DEFEATED!</strong><br><br>We've done it! The Be-Wilder lets out a ululating cry as it twists in the air, shedding feathers like falling rain. Slowly, gradually, it coils into a cloud of sparkling mist. As the newly-revealed sun pierces the fog, it burns away, revealing the coughing, mercifully human forms of Bailey, Matt, Alex.... and the April Fool himself.<br><br><strong>Mistiflying is saved!</strong><br><br>The April Fool has enough shame to look a bit sheepish. “Oh, hm,” he says. “Perhaps I got a little…. carried away.”<br><br>The crowd mutters. Sodden flowers wash up on pavements. Somewhere in the distance, a roof collapses with a spectacular splash.<br><br>“Er, yes,” the April Fool says. “That is. What I meant to say was, I’m dreadfully sorry.” He heaves a sigh. “I suppose it can’t all be fun and games, after all. It might not hurt to focus occasionally. Maybe I’ll get a head start on next year’s pranking.”<br><br>Redphoenix coughs meaningfully.<br><br>“I mean, get a head start on this year’s spring cleaning!” the April Fool says. “Nothing to fear, I’ll have Habit City in spit-shape soon. Luckily nobody is better than I at dual-wielding mops.”<br><br>Encouraged, the marching band starts up.<br><br>It isn’t long before all is back to normal in Habit City. Plus, now that the Be-Wilder has evaporated, the magical bees of Mistiflying bustle back to work, and soon the flowers are blooming and the city is floating once more.<br><br>As Habiticans cuddle the magical fuzzy bees, the April Fool’s eyes light up. “Oho, I’ve had a thought! Why don’t you all keep some of these fuzzy Bee Pets and Mounts? It’s a gift that perfectly symbolises the balance between hard work and sweet rewards, if I’m going to get all boring and allegorical on you.” He winks. “Besides, they don’t have stingers! Fool’s honour.”",
|
||||
"questBewilderCompletionChat": "`The Be-Wilder is DEFEATED!`\n\nWe've done it! The Be-Wilder lets out a ululating cry as it twists in the air, shedding feathers like falling rain. Slowly, gradually, it coils into a cloud of sparkling mist. As the newly-revealed sun pierces the fog, it burns away, revealing the coughing, mercifully human forms of Bailey, Matt, Alex.... and the April Fool himself.\n\n`Mistiflying is saved!`\n\nThe April Fool has enough shame to look a bit sheepish. “Oh, hm,” he says. “Perhaps I got a little…. carried away.”\n\nThe crowd mutters. Sodden flowers wash up on pavements. Somewhere in the distance, a roof collapses with a spectacular splash.\n\n“Er, yes,” the April Fool says. “That is. What I meant to say was, I’m dreadfully sorry.” He heaves a sigh. “I suppose it can’t all be fun and games, after all. It might not hurt to focus occasionally. Maybe I’ll get a head start on next year’s pranking.”\n\nRedphoenix coughs meaningfully.\n\n“I mean, get a head start on this year’s spring cleaning!” the April Fool says. “Nothing to fear, I’ll have Habit City in spit-shape soon. Luckily nobody is better than I at dual-wielding mops.”\n\nEncouraged, the marching band starts up.\n\nIt isn’t long before all is back to normal in Habit City. Plus, now that the Be-Wilder has evaporated, the magical bees of Mistiflying bustle back to work, and soon the flowers are blooming and the city is floating once more.\n\nAs Habiticans cuddle the magical fuzzy bees, the April Fool’s eyes light up. “Oho, I’ve had a thought! Why don’t you all keep some of these fuzzy Bee Pets and Mounts? It’s a gift that perfectly symbolises the balance between hard work and sweet rewards, if I’m going to get all boring and allegorical on you.” He winks. “Besides, they don’t have stingers! Fool’s honour.”",
|
||||
"questBewilderBossRageTitle": "Beguilement Strike",
|
||||
@@ -343,7 +343,7 @@
|
||||
"questAxolotlUnlockText": "Unlocks Axolotl Eggs for purchase in the Market",
|
||||
"questAxolotlRageTitle": "Axolotl Regeneration",
|
||||
"questAxolotlRageDescription": "This bar fills when you don't complete your Dailies. When it is full, the Magical Axolotl will heal 30% of its remaining health!",
|
||||
"questAxolotlRageEffect": "Magical Axolotl uses AXOLOTL REGENERATION!\n\nA curtain of colourful bubbles obscures the monster for a moment, and when it clears, some of its wounds have vanished!",
|
||||
"questAxolotlRageEffect": "`Magical Axolotl uses AXOLOTL REGENERATION!`\n\n`A curtain of colourful bubbles obscures the monster for a moment, and when it clears, some of its wounds have vanished!`",
|
||||
"questTurtleText": "Guide the Turtle",
|
||||
"questTurtleNotes": "Help! This giant sea turtle cannot find her way to her nesting beach. She returns there every year to lay her eggs, but this year Inkomplete Bay is filled with toxic Task Flotsam made of red Dailies and unchecked To Do's. \"She's thrashing in a panic!\" @JessicaChase says.<br><br>@UncommonCriminal nods. \"It's because her guiding senses are fogged and confused.\"<br><br>@Scarabsi grabs your arm. \"Can you help clear the Task Flotsam blocking her path? It may be hazardous, but we have to help her!\"",
|
||||
"questTurtleCompletion": "Your valiant work has cleared the waters for our sea turtle to find her beach. You, @Bambin, and @JaizakAripaik watch as she buries her brood of eggs deep in the sand so they can grow and hatch into hundreds of little sea turtles. Ever the lady, she gives you three eggs each, asking that you feed and nurture them so one day they become big sea turtles themselves.",
|
||||
@@ -357,7 +357,7 @@
|
||||
"questArmadilloDropArmadilloEgg": "Armadillo (Egg)",
|
||||
"questArmadilloUnlockText": "Unlocks Armadillo Eggs for purchase in the Market",
|
||||
"questCowText": "The Mootant Cow",
|
||||
"questCowNotes": "It’s been a long, hot day at Sparring Farms, and there is nothing more you want than a long sip of water and some sleep. You're standing there daydreaming when @Soloana suddenly screams, \"Everyone run! The prize cow has mootated!\"<br><br>@eevachu gulps. \"It must be our bad habits that infected it.\"<br><br>\"Quick!\" @Feralem Tau says. \"Let’s do something before the udder cows mootate, too.\"<br><br>You’ve herd enough. No more daydreaming—it's time to get those bad habits under control!",
|
||||
"questCowNotes": "It’s been a long, hot day at Sparring Farms, and there is nothing more you want than a long sip of water and some sleep. You're standing there daydreaming when @Soloana suddenly screams, \"Everyone run! The prize cow has mootated!\"<br><br>@eevachu gulps. \"It must be our bad habits that infected it.\"<br><br>\"Quick!\" @Feralem Tau says. \"Let’s do something before the udder cows mootate, too.\"<br><br>You’ve herd enough. No more daydreaming -- it's time to get those bad habits under control!",
|
||||
"questCowCompletion": "You milk your good habits for all they are worth until the cow reverts to its original form. The cow looks over at you with her pretty brown eyes and nudges over three eggs.<br><br>@fuzzytrees laughs and hands you the eggs, \"Maybe it still is mootated if there are baby cows in these eggs. But I trust you to stick to your good habits when you raise them!\"",
|
||||
"questCowBoss": "Mootant Cow",
|
||||
"questCowDropCowEgg": "Cow (Egg)",
|
||||
@@ -375,7 +375,7 @@
|
||||
"questTaskwoodsTerror1Boss": "Fire Skull Swarm",
|
||||
"questTaskwoodsTerror1RageTitle": "Swarm Respawn",
|
||||
"questTaskwoodsTerror1RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Fire Skull Swarm will heal 30% of its remaining health!",
|
||||
"questTaskwoodsTerror1RageEffect": "Fire Skull Swarm uses SWARM RESPAWN!\n\nEmboldened by their victories, more skulls swirl around you in a gout of flame!",
|
||||
"questTaskwoodsTerror1RageEffect": "`Fire Skull Swarm uses SWARM RESPAWN!`\n\nEmboldened by their victories, more skulls swirl around you in a gout of flame!",
|
||||
"questTaskwoodsTerror1DropSkeletonPotion": "Skeleton Hatching Potion",
|
||||
"questTaskwoodsTerror1DropRedPotion": "Red Hatching Potion",
|
||||
"questTaskwoodsTerror1DropHeadgear": "Pyromancer's Turban (Headgear)",
|
||||
@@ -387,7 +387,7 @@
|
||||
"questTaskwoodsTerror2CollectDryads": "Dryads",
|
||||
"questTaskwoodsTerror2DropArmor": "Pyromancer's Robes (Armour)",
|
||||
"questTaskwoodsTerror3Text": "Terror in the Taskwoods, Part 3: Jacko of the Lantern",
|
||||
"questTaskwoodsTerror3Notes": "Ready for battle, your group marches to the heart of the forest, where the renegade spirit is trying to destroy an ancient apple tree surrounded by fruitful berry bushes. His pumpkin-like head radiates a terrible light wherever it turns, and in his left hand he holds a long rod, with a lantern hanging from its tip. Instead of fire or flame, however, the lantern contains a dark crystal that chills you to the very bone.<br><br>The Joyful Reaper raises a bony hand to her mouth. \"That's—that's Jacko, the Lantern Spirit! But he's a helpful harvest ghost who guides our farmers. What could possibly drive the dear soul to act this way?\"<br><br>\"I don't know,\" says @bridgetteempress. \"But it looks like that 'dear soul' is about to attack us!\"",
|
||||
"questTaskwoodsTerror3Notes": "Ready for battle, your group marches to the heart of the forest, where the renegade spirit is trying to destroy an ancient apple tree surrounded by fruitful berry bushes. His pumpkin-like head radiates a terrible light wherever it turns, and in his left hand he holds a long rod, with a lantern hanging from its tip. Instead of fire or flame, however, the lantern contains a dark crystal that chills you to the very bone.<br><br>The Joyful Reaper raises a bony hand to her mouth. \"That's -- that's Jacko, the Lantern Spirit! But he's a helpful harvest ghost who guides our farmers. What could possibly drive the dear soul to act this way?\"<br><br>\"I don't know,\" says @bridgetteempress. \"But it looks like that 'dear soul' is about to attack us!\"",
|
||||
"questTaskwoodsTerror3Completion": "After a long battle, you manage to land a well-aimed blow at the lantern that Jacko carries, and the crystal within shatters. Jacko suddenly snaps back to his senses and bursts into glowing tears. \"Oh, my beautiful forest! What have I done?!\" he wails. His tears extinguish the remaining fires, and the apple tree and wild berries are saved.<br><br>After you help him relax, he explains, \"I met this charming lady named Tzina, and she gave me this glowing crystal as a gift. At her urging, I put it in my lantern... but that's the last thing I recall.\" He turns to you with a golden smile. \"Perhaps you should take it for safekeeping while I help the wild orchards to regrow.\"",
|
||||
"questTaskwoodsTerror3Boss": "Jacko of the Lantern",
|
||||
"questTaskwoodsTerror3DropStrawberry": "Strawberry (Food)",
|
||||
@@ -425,24 +425,24 @@
|
||||
"questSlothDropSlothEgg": "Sloth (Egg)",
|
||||
"questSlothUnlockText": "Unlocks Sloth Eggs for purchase in the Market",
|
||||
"questTriceratopsText": "The Trampling Triceratops",
|
||||
"questTriceratopsNotes": "The snow-capped Stoïkalm Volcanoes are always bustling with hikers and sight-seers. One tourist, @plumilla, calls over a crowd. \"Look! I enchanted the ground to glow so that we can play field games on it for our outdoor activity Dailies!\" Sure enough, the ground is swirling with glowing red patterns. Even some of the prehistoric pets from the area come over to play.<br><br>Suddenly, there's a loud snap—a curious Triceratops has stepped on @plumilla's wand! It's engulfed in a burst of magic energy, and the ground starts shaking and growing hot. The Triceratops' eyes shine red, and it roars and begins to stampede!<br><br>\"That's not good,\" calls @McCoyly, pointing in the distance. Each magic-fuelled stomp is causing the volcanoes to erupt, and the glowing ground is turning to lava beneath the dinosaur's feet! Quickly, you must hold off the Trampling Triceratops until someone can reverse the spell!",
|
||||
"questTriceratopsNotes": "The snow-capped Stoïkalm Volcanoes are always bustling with hikers and sight-seers. One tourist, @plumilla, calls over a crowd. \"Look! I enchanted the ground to glow so that we can play field games on it for our outdoor activity Dailies!\" Sure enough, the ground is swirling with glowing red patterns. Even some of the prehistoric pets from the area come over to play.<br><br>Suddenly, there's a loud snap -- a curious Triceratops has stepped on @plumilla's wand! It's engulfed in a burst of magic energy, and the ground starts shaking and growing hot. The Triceratops' eyes shine red, and it roars and begins to stampede!<br><br>\"That's not good,\" calls @McCoyly, pointing in the distance. Each magic-fueled stomp is causing the volcanoes to erupt, and the glowing ground is turning to lava beneath the dinosaur's feet! Quickly, you must hold off the Trampling Triceratops until someone can reverse the spell!",
|
||||
"questTriceratopsCompletion": "With quick thinking, you herd the creature towards the soothing Stoïkalm Steppes so that @*~Seraphina~* and @PainterProphet can reverse the lava spell without distraction. The calming aura of the Steppes takes effect, and the Triceratops curls up as the volcanoes go dormant once more. @PainterProphet passes you some eggs that were rescued from the lava. \"Without you, we wouldn't have been able to concentrate to stop the eruptions. Give these pets a good home.\"",
|
||||
"questTriceratopsBoss": "Trampling Triceratops",
|
||||
"questTriceratopsDropTriceratopsEgg": "Triceratops (Egg)",
|
||||
"questTriceratopsUnlockText": "Unlocks Triceratops Eggs for purchase in the Market",
|
||||
"questGroupStoikalmCalamity": "Stoïkalm Calamity",
|
||||
"questStoikalmCalamity1Text": "Stoïkalm Calamity, Part 1: Earthen Enemies",
|
||||
"questStoikalmCalamity1Notes": "A terse missive arrives from @Kiwibot, and the frost-crusted scroll chills your heart as well as your fingertips. \"Visiting Stoïkalm Steppes—monsters bursting from earth—send help!\" You gather your party and ride north, but as soon as you venture down from the mountains, the snow beneath your feet explodes and gruesomely grinning skulls surround you!<br><br>Suddenly, a spear sails past, burying itself in a skull that was burrowing through the snow in an attempt to catch you unawares. A tall woman in finely-crafted armour gallops into the fray on the back of a mastodon, her long braid swinging as she yanks the spear unceremoniously from the crushed beast. It's time to fight off these foes with the help of Lady Glaciate, the leader of the Mammoth Riders!",
|
||||
"questStoikalmCalamity1Notes": "A terse missive arrives from @Kiwibot, and the frost-crusted scroll chills your heart as well as your fingertips. \"Visiting Stoïkalm Steppes -- monsters bursting from earth -- send help!\" You gather your party and ride north, but as soon as you venture down from the mountains, the snow beneath your feet explodes and gruesomely grinning skulls surround you!<br><br>Suddenly, a spear sails past, burying itself in a skull that was burrowing through the snow in an attempt to catch you unawares. A tall woman in finely-crafted armour gallops into the fray on the back of a mastodon, her long braid swinging as she yanks the spear unceremoniously from the crushed beast. It's time to fight off these foes with the help of Lady Glaciate, the leader of the Mammoth Riders!",
|
||||
"questStoikalmCalamity1Completion": "As you deliver a final blow to the skulls, they dissipate in a puff of magic. \"The dratted swarm may be gone,\" Lady Glaciate says, \"but we have bigger problems. Follow me.\" She tosses you a cloak to protect you from the chill air, and you ride off after her.",
|
||||
"questStoikalmCalamity1Boss": "Earth Skull Swarm",
|
||||
"questStoikalmCalamity1RageTitle": "Swarm Respawn",
|
||||
"questStoikalmCalamity1RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Earth Skull Swarm will heal 30% of its remaining health!",
|
||||
"questStoikalmCalamity1RageEffect": "Earth Skull Swarm uses SWARM RESPAWN!\n\nMore skulls break free from the ground, their teeth chattering in the cold!",
|
||||
"questStoikalmCalamity1RageEffect": "`Earth Skull Swarm uses SWARM RESPAWN!`\n\nMore skulls break free from the ground, their teeth chattering in the cold!",
|
||||
"questStoikalmCalamity1DropSkeletonPotion": "Skeleton Hatching Potion",
|
||||
"questStoikalmCalamity1DropDesertPotion": "Desert Hatching Potion",
|
||||
"questStoikalmCalamity1DropArmor": "Mammoth Rider Armour",
|
||||
"questStoikalmCalamity2Text": "Stoïkalm Calamity, Part 2: Seek the Icicle Caverns",
|
||||
"questStoikalmCalamity2Notes": "The stately hall of the Mammoth Riders is an austere masterpiece of architecture, but it is also entirely empty. There's no furniture, the weapons are missing, and even the columns were picked clean of their inlays.<br><br>\"Those skulls scoured the place,\" Lady Glaciate says, and there is a blizzard brewing in her tone. \"Humiliating. Not a soul is to mention this to the April Fool, or I will never hear the end of it.\"<br><br>\"How mysterious!\" says @Beffymaroo. \"But where did they—\"<br><br>\"The icicle drake caverns.\" Lady Glaciate gestures at shining coins spilled in the snow outside. \"Sloppy.\"<br><br>\"But aren't icicle drakes honourable creatures with their own treasure hoards?\" @Beffymaroo asks. \"Why would they possibly—\"<br><br>\"Mind control,\" says Lady Glaciate, utterly unfazed. \"Or something equally melodramatic and inconvenient.\" She begins to stride from the hall. \"Why are you just standing there?\"<br><br>Quickly, go follow the trail of Icicle Coins!",
|
||||
"questStoikalmCalamity2Notes": "The stately hall of the Mammoth Riders is an austere masterpiece of architecture, but it is also entirely empty. There's no furniture, the weapons are missing, and even the columns were picked clean of their inlays.<br><br>\"Those skulls scoured the place,\" Lady Glaciate says, and there is a blizzard brewing in her tone. \"Humiliating. Not a soul is to mention this to the April Fool, or I will never hear the end of it.\"<br><br>\"How mysterious!\" says @Beffymaroo. \"But where did they--\"<br><br>\"The icicle drake caverns.\" Lady Glaciate gestures at shining coins spilled in the snow outside. \"Sloppy.\"<br><br>\"But aren't icicle drakes honorable creatures with their own treasure hoards?\" @Beffymaroo asks. \"Why would they possibly--\"<br><br>\"Mind control,\" says Lady Glaciate, utterly unfazed. \"Or something equally melodramatic and inconvenient.\" She begins to stride from the hall. \"Why are you just standing there?\"<br><br>Quickly, go follow the trail of Icicle Coins!",
|
||||
"questStoikalmCalamity2Completion": "The Icicle Coins lead you straight to the buried entrance of a cleverly hidden cavern. Though the weather outside is calm and lovely, with the sunlight sparkling across the expanse of snow, there is a howling within like a fierce winter wind. Lady Glaciate grimaces and hands you a Mammoth Rider helm. \"Wear this,\" she says. \"You'll need it.\"",
|
||||
"questStoikalmCalamity2CollectIcicleCoins": "Icicle Coins",
|
||||
"questStoikalmCalamity2DropHeadgear": "Mammoth Rider Helm (Headgear)",
|
||||
@@ -454,19 +454,19 @@
|
||||
"questStoikalmCalamity3DropShield": "Mammoth Rider's Horn (Off-Hand Item)",
|
||||
"questStoikalmCalamity3DropWeapon": "Mammoth Rider Spear (Weapon)",
|
||||
"questGuineaPigText": "The Guinea Pig Gang",
|
||||
"questGuineaPigNotes": "You're casually strolling through Habit City's famous Market when @Pandah waves you down. \"Hey, check these out!\" They're holding up a brown and beige egg you don't recognise.<br><br>Alexander the Merchant frowns at it. \"I don't remember putting that out. I wonder where it came—\" A small paw cuts him off.<br><br>\"Guinea all your gold, merchant!\" squeaks a tiny voice brimming with evil.<br><br>\"Oh no, the egg was a distraction!\" @mewrose exclaims. \"It's the gritty, greedy Guinea Pig Gang! They never do their Dailies, so they constantly steal gold to buy health potions.\"<br><br>\"Robbing the Market?\" says @emmavig. \"Not on our watch!\" Without further prompting, you leap to Alexander's aid.",
|
||||
"questGuineaPigCompletion": "\"We submit!\" The Guinea Pig Gang Boss waves his paws at you, fluffy head hanging in shame. From underneath his hat falls a list, and @snazzyorange quickly swipes it for evidence. \"Wait a minute,\" you say. \"It's no wonder you've been getting hurt! You've got way too many Dailies. You don't need health potions—you just need help organising.\"<br><br>\"Really?\" squeaks the Guinea Pig Gang Boss. \"We've robbed so many people because of this! Please take our eggs as an apology for our crooked ways.\"",
|
||||
"questGuineaPigNotes": "You're casually strolling through Habit City's famous Market when @Pandah waves you down. \"Hey, check these out!\" They're holding up a brown and beige egg you don't recognise.<br><br>Alexander the Merchant frowns at it. \"I don't remember putting that out. I wonder where it came--\" A small paw cuts him off.<br><br>\"Guinea all your gold, merchant!\" squeaks a tiny voice brimming with evil.<br><br>\"Oh no, the egg was a distraction!\" @mewrose exclaims. \"It's the gritty, greedy Guinea Pig Gang! They never do their Dailies, so they constantly steal gold to buy health potions.\"<br><br>\"Robbing the Market?\" says @emmavig. \"Not on our watch!\" Without further prompting, you leap to Alexander's aid.",
|
||||
"questGuineaPigCompletion": "\"We submit!\" The Guinea Pig Gang Boss waves his paws at you, fluffy head hanging in shame. From underneath his hat falls a list, and @snazzyorange quickly swipes it for evidence. \"Wait a minute,\" you say. \"It's no wonder you've been getting hurt! You've got way too many Dailies. You don't need health potions -- you just need help organising.\"<br><br>\"Really?\" squeaks the Guinea Pig Gang Boss. \"We've robbed so many people because of this! Please take our eggs as an apology for our crooked ways.\"",
|
||||
"questGuineaPigBoss": "Guinea Pig Gang",
|
||||
"questGuineaPigDropGuineaPigEgg": "Guinea Pig (Egg)",
|
||||
"questGuineaPigUnlockText": "Unlocks Guinea Pig Eggs for purchase in the Market",
|
||||
"questPeacockText": "The Push-and-Pull Peacock",
|
||||
"questPeacockNotes": "You trek through the Taskwoods, wondering which of the enticing new goals you should pick. As you go deeper into the forest, you realise that you're not alone in your indecision. \"I could learn a new language, or go to the gym...\" @Cecily Perez mutters. \"I could sleep more,\" muses @Lilith of Alfheim, \"or spend time with my friends...\" It looks like @PainterProphet, @Pfeffernusse, and @Draayder are equally paralysed by the overwhelming options.<br><br>You realise that these ever-more-demanding feelings aren't really your own... you've stumbled straight into the trap of the pernicious Push-and-Pull Peacock! Before you can run, it leaps from the bushes. With each head pulling you in conflicting directions, you start to feel burnout overcoming you. You can't defeat both foes at once, so you only have one option—concentrate on the nearest task to fight back!",
|
||||
"questPeacockNotes": "You trek through the Taskwoods, wondering which of the enticing new goals you should pick. As you go deeper into the forest, you realise that you're not alone in your indecision. \"I could learn a new language, or go to the gym...\" @Cecily Perez mutters. \"I could sleep more,\" muses @Lilith of Alfheim, \"or spend time with my friends...\" It looks like @PainterProphet, @Pfeffernusse, and @Draayder are equally paralysed by the overwhelming options.<br><br>You realise that these ever-more-demanding feelings aren't really your own... you've stumbled straight into the trap of the pernicious Push-and-Pull Peacock! Before you can run, it leaps from the bushes. With each head pulling you in conflicting directions, you start to feel burnout overcoming you. You can't defeat both foes at once, so you only have one option -- concentrate on the nearest task to fight back!",
|
||||
"questPeacockCompletion": "The Push-and-Pull Peacock is caught off guard by your sudden conviction. Defeated by your single-minded drive, its heads merge back into one, revealing the most beautiful creature you've ever seen. \"Thank you,\" the peacock says. \"I’ve spent so long pulling myself in different directions that I lost sight of what I truly wanted. Please accept these eggs as a token of my gratitude.\"",
|
||||
"questPeacockBoss": "Push-and-Pull Peacock",
|
||||
"questPeacockDropPeacockEgg": "Peacock (Egg)",
|
||||
"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!",
|
||||
"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 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)",
|
||||
@@ -478,7 +478,7 @@
|
||||
"questMayhemMistiflying1Boss": "Air Skull Swarm",
|
||||
"questMayhemMistiflying1RageTitle": "Swarm Respawn",
|
||||
"questMayhemMistiflying1RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Air Skull Swarm will heal 30% of its remaining health!",
|
||||
"questMayhemMistiflying1RageEffect": "Air Skull Swarm uses SWARM RESPAWN!\n\nEmboldened by their victories, more skulls come whirling out of the clouds!",
|
||||
"questMayhemMistiflying1RageEffect": "`Air Skull Swarm uses SWARM RESPAWN!`\n\nEmboldened by their victories, more skulls come whirling out of the clouds!",
|
||||
"questMayhemMistiflying1DropSkeletonPotion": "Skeleton Hatching Potion",
|
||||
"questMayhemMistiflying1DropWhitePotion": "White Hatching Potion",
|
||||
"questMayhemMistiflying1DropArmor": "Roguish Rainbow Messenger Robes (Armour)",
|
||||
@@ -535,7 +535,7 @@
|
||||
"questLostMasterclasser3Boss": "Void Skull Swarm",
|
||||
"questLostMasterclasser3RageTitle": "Swarm Respawn",
|
||||
"questLostMasterclasser3RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Void Skull Swarm will heal 30% of its remaining health!",
|
||||
"questLostMasterclasser3RageEffect": "Void Skull Swarm uses SWARM RESPAWN!\n\nEmboldened by their victories, more skulls scream down from the heavens, bolstering the swarm!",
|
||||
"questLostMasterclasser3RageEffect": "`Void Skull Swarm uses SWARM RESPAWN!`\n\nEmboldened by their victories, more skulls scream down from the heavens, bolstering the swarm!",
|
||||
"questLostMasterclasser3DropBodyAccessory": "Aether Amulet (Body Accessory)",
|
||||
"questLostMasterclasser3DropBasePotion": "Base Hatching Potion",
|
||||
"questLostMasterclasser3DropGoldenPotion": "Golden Hatching Potion",
|
||||
@@ -548,7 +548,7 @@
|
||||
"questLostMasterclasser4Boss": "Anti'zinnya",
|
||||
"questLostMasterclasser4RageTitle": "Siphoning Void",
|
||||
"questLostMasterclasser4RageDescription": "Siphoning Void: This bar fills when you don't complete your Dailies. When it is full, Anti'zinnya will remove the party's Mana!",
|
||||
"questLostMasterclasser4RageEffect": "Anti'zinnya uses SIPHONING VOID! In a twisted inversion of the Ethereal Surge spell, you feel your magic drain away into the darkness!",
|
||||
"questLostMasterclasser4RageEffect": "`Anti'zinnya uses SIPHONING VOID!` In a twisted inversion of the Ethereal Surge spell, you feel your magic drain away into the darkness!",
|
||||
"questLostMasterclasser4DropBackAccessory": "Aether Cloak (Back Accessory)",
|
||||
"questLostMasterclasser4DropWeapon": "Aether Crystals (Two-Handed Weapon)",
|
||||
"questLostMasterclasser4DropMount": "Invisible Aether Mount",
|
||||
@@ -612,7 +612,7 @@
|
||||
"questSeaSerpentDropSeaSerpentEgg": "Sea Serpent (Egg)",
|
||||
"questSeaSerpentUnlockText": "Unlocks Sea Serpent Eggs for purchase in the Market",
|
||||
"questKangarooText": "Kangaroo Catastrophe",
|
||||
"questKangarooNotes": "Maybe you should have finished that last task… you know, the one you keep avoiding, even though it always comes back around? But @Mewrose and @LilithofAlfheim invited you and @stefalupagus to see a rare kangaroo troop hopping through the Sloensteadi Savannah; how could you say no?! As the troop comes into view, something hits you on the back of the head with a mighty <em>whack!</em><br><br>Shaking the stars from your vision, you pick up the responsible object—a dark red boomerang, with the very task you continually push back etched into its surface. A quick glance around confirms the rest of your party met a similar fate. One larger kangaroo looks at you with a smug grin, like she’s daring you to face her and that dreaded task once and for all!",
|
||||
"questKangarooNotes": "Maybe you should have finished that last task… you know, the one you keep avoiding, even though it always comes back around? But @Mewrose and @LilithofAlfheim invited you and @stefalupagus to see a rare kangaroo troop hopping through the Sloensteadi Savannah; how could you say no?! As the troop comes into view, something hits you on the back of the head with a mighty <em>whack!</em><br><br>Shaking the stars from your vision, you pick up the responsible object--a dark red boomerang, with the very task you continually push back etched into its surface. A quick glance around confirms the rest of your party met a similar fate. One larger kangaroo looks at you with a smug grin, like she’s daring you to face her and that dreaded task once and for all!",
|
||||
"questKangarooCompletion": "“NOW!” You signal your party to throw the boomerangs back at the kangaroo. The beast hops further away with each hit until she flees, leaving nothing more than a dark red cloud of dust, a few eggs, and some gold coins.<br><br>@Mewrose walks forward to where the kangaroo once stood. “Hey, where did the boomerangs go?”<br><br>“They probably dissolved into dust, making that dark red cloud, when we finished our respective tasks,” @stefalupagus speculates.<br><br>@LilithofAlfheim squints at the horizon. “Is that another kangaroo troop heading our way?”<br><br>You all break into a run back to Habit City. Better to face your difficult tasks than take another lump to the back of the head!",
|
||||
"questKangarooBoss": "Catastrophic Kangaroo",
|
||||
"questKangarooDropKangarooEgg": "Kangaroo (Egg)",
|
||||
@@ -672,7 +672,7 @@
|
||||
"delightfulDinosNotes": "Contains Quests to obtain Triceratops, T-Rex, and Pterodactyl Pet eggs: The Trampling Triceratops, The Dinosaur Unearthed, and The Pterror-dactyl.",
|
||||
"questAmberText": "The Amber Alliance",
|
||||
"questAmberNotes": "You’re sitting in the Tavern with @beffymaroo and @-Tyr- when @Vikte bursts through the door and excitedly tells you about the rumours of another type of Magic Hatching Potion hidden in the Taskwoods. Having completed your Dailies, the three of you immediately agree to help @Vikte on their search. After all, what’s the harm in a little adventure?<br><br>After walking through the Taskwoods for hours, you’re beginning to regret joining such a wild chase. You’re about to head home, when you hear a surprised yelp and turn to see a huge lizard with shiny amber scales coiled around a tree, clutching @Vikte in her claws. @beffymaroo reaches for her sword.<br><br>“Wait!” cries @-Tyr-. “It’s the Trerezin! She’s not dangerous, just dangerously clingy!”",
|
||||
"questAmberCompletion": "“Trerezin?” @-Tyr- says calmly. “Could you let @Vikte go? I don’t think they’re enjoying being so high up.”<br><br>The Trerezin’s amber skin blushes crimson and she gently lowers @Vikte to the ground. “My apologies! It’s been so long since I’ve had any guests that I’ve forgotten my manners!” She slithers forward to greet you properly before disappearing into her treehouse, and returning with an armful of Amber Hatching Potions as thank-you gifts!<br><br>“Magic Potions!” @Vikte gasps.<br><br>“Oh, these old things?” The Trerezin's tongue flickers as she thinks. “How about this? I’ll give you this whole stack if you promise to visit me every so often...”<br><br>And so you leave the Taskwoods, excited to tell everyone about the new potions—and your new friend!",
|
||||
"questAmberCompletion": "“Trerezin?” @-Tyr- says calmly. “Could you let @Vikte go? I don’t think they’re enjoying being so high up.”<br><br>The Trerezin’s amber skin blushes crimson and she gently lowers @Vikte to the ground. “My apologies! It’s been so long since I’ve had any guests that I’ve forgotten my manners!” She slithers forward to greet you properly before disappearing into her treehouse, and returning with an armful of Amber Hatching Potions as thank-you gifts!<br><br>“Magic Potions!” @Vikte gasps.<br><br>“Oh, these old things?” The Trerezin's tongue flickers as she thinks. “How about this? I’ll give you this whole stack if you promise to visit me every so often...”<br><br>And so you leave the Taskwoods, excited to tell everyone about the new potions--and your new friend!",
|
||||
"questAmberBoss": "Trerezin",
|
||||
"questAmberDropAmberPotion": "Amber Hatching Potion",
|
||||
"questAmberUnlockText": "Unlocks Amber Hatching Potions for purchase in the Market",
|
||||
@@ -686,12 +686,12 @@
|
||||
"questRubyText": "Ruby Rapport",
|
||||
"questWaffleRageTitle": "Maple Mire",
|
||||
"questWaffleBoss": "Awful Waffle",
|
||||
"questWaffleCompletion": "Battered and buttered but triumphant, you savour 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 colour—new Hatching Potions! He tosses them into your arms. “All that adventure has given me an appetite. Who wants to join me for breakfast?”",
|
||||
"questWaffleCompletion": "Battered and buttered but triumphant, you savour 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 colour--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! Pending damage reduced.",
|
||||
"questWaffleRageEffect": "`Awful Waffle uses MAPLE MIRE!` Sticky sappy syrup slows your swings and spells! Pending damage reduced.",
|
||||
"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!",
|
||||
"jungleBuddiesNotes": "Contains Quests to obtain Monkey, Treeling, and Sloth Pet eggs: Monstrous Mandrill and the Mischief Monkeys, The Tangle Tree, and The Somnolent Sloth.",
|
||||
"jungleBuddiesText": "Jungle Buddies Quest Bundle",
|
||||
@@ -701,7 +701,7 @@
|
||||
"questFluoriteCompletion": "As you do battle, the crystal creature seems more and more distracted by the light show you are creating. “So shiny…” it mutters.<br><br>“Of course!” @nirbhao exclaims. “It must be a fluorite elemental. All they want is light to let them glow. Let’s help it shine.”<br><br>The elemental giggles happily and glows all the brighter as you light up torches and motes of magic. It’s so glad to be shining again that it leads you to a rich deposit of fluorite crystals.<br><br>“This is the perfect ingredient for a new hatching potion,” says @nirbhao. “One which will make our pets as bright as our new fluorescent friend.”",
|
||||
"questFluoriteNotes": "Unusual minerals are in high demand these days, so you and a few friends have trekked deep into the mines of the Meandering Mountains, in search of exciting ores. It’s a long and boring expedition, until @-Tyr- stumbles over a large rock, sitting right in the middle of the tunnel.<br><br>“This should help brighten things up,” says @nirbhao, before conjuring up an orb of light.<br><br>A warm brightness fills the tunnel, but something odd starts happening to that large rock. Feeding on the magical light, it begins to glow with fluorescent blues, greens and purples. Then it rears upright into a vaguely humanoid shape, complete with glowing red eyes fixed right on you! You jump into action with flashing spells and shining weapons.",
|
||||
"questFluoriteText": "A Bright Fluorite Fright",
|
||||
"questWindupNotes": "Habit City is seldom quiet, but you weren’t prepared for the cacophony of creaks, squeaks and screams escaping Good Timekeeping, Habitica’s finest clockwork emporium. You sigh—you just wanted your watch fixed. The proprietor, known only as “Great and Powerful”, tumbles out the door, pursued by a clanking copper colossus!<br><br>“Ki-! Ki-! Ki!” it clangs, arms smashing up and down. Its gears grind and screech in protest.<br><br>“My robot Clankton has gone mad! It’s trying to kill me!” the supposedly Powerful one shrieks.<br><br>Even with a broken watch, you can tell when it’s time to fight. You leap forward to defend the panicking watchmaker. @Vikte and @a_diamond also step up to help!<br><br>“Ki-! Ki-! Ki-!” Clankton chants with each blow. “Mew!”<br><br>Wait, was that mechanical mewling amidst the murderous monotone?",
|
||||
"questWindupNotes": "Habit City is seldom quiet, but you weren’t prepared for the cacophony of creaks, squeaks and screams escaping Good Timekeeping, Habitica’s finest clockwork emporium. You sigh--you just wanted your watch fixed. The proprietor, known only as “Great and Powerful”, tumbles out the door, pursued by a clanking copper colossus!<br><br>“Ki-! Ki-! Ki!” it clangs, arms smashing up and down. Its gears grind and screech in protest.<br><br>“My robot Clankton has gone mad! It’s trying to kill me!” the supposedly Powerful one shrieks.<br><br>Even with a broken watch, you can tell when it’s time to fight. You leap forward to defend the panicking watchmaker. @Vikte and @a_diamond also step up to help!<br><br>“Ki-! Ki-! Ki-!” Clankton chants with each blow. “Mew!”<br><br>Wait, was that mechanical mewling amidst the murderous monotone?",
|
||||
"questWindupText": "A Whirl with a Wind-Up Warrior",
|
||||
"questBlackPearlUnlockText": "Unlocks Black Pearl Hatching Potions for purchase in the Market",
|
||||
"questBlackPearlDropBlackPearlPotion": "Black Pearl Hatching Potion",
|
||||
@@ -739,7 +739,7 @@
|
||||
"questStoneText": "A Maze of Moss",
|
||||
"questSolarSystemUnlockText": "Unlocks Solar System Hatching Potions for purchase in the Market",
|
||||
"questSolarSystemText": "A Voyage of Cosmic Concentration",
|
||||
"questSolarSystemNotes": "Your party is travelling through the cosmos, seeing the sights in a fantastical airship designed by talented space engineer @gawrone. Its meditationite propulsion relies on the calm of your Party to stay on course.<br><br>Up ahead in the clouds of sparkling galaxies, you spot an ominously pulsing star. “Keep your focus,” warns @beffymaroo. “If we get too distracted when we’re passing that nova, the pull between the star and our ship may veer us off course!”<br><br>As you near the star, pulses of strange energy come toward the ship.<br><br>“They’re Diversionoids, thought creatures trying to get us lost,” says @SabreCat. “If we can let them flow by without carrying us away, we should be able to stay pointed toward our goal!”",
|
||||
"questSolarSystemNotes": "Your party is traveling through the cosmos, seeing the sights in a fantastical airship designed by talented space engineer @gawrone. Its meditationite propulsion relies on the calm of your Party to stay on course.<br><br>Up ahead in the clouds of sparkling galaxies, you spot an ominously pulsing star. “Keep your focus,” warns @beffymaroo. “If we get too distracted when we’re passing that nova, the pull between the star and our ship may veer us off course!”<br><br>As you near the star, pulses of strange energy come toward the ship.<br><br>“They’re Diversionoids, thought creatures trying to get us lost,” says @SabreCat. “If we can let them flow by without carrying us away, we should be able to stay pointed toward our goal!”",
|
||||
"questStoneCollectMarsRunes": "Mars Runes",
|
||||
"questSolarSystemCompletion": "Through careful practice, you and the crew manage to keep the Diversionoids from sweeping you overboard, just by noticing and acknowledging them without letting them take over. As you pass safely by the pulsing star, @gawrone notices a cluster of floating bottles and pulls them aboard. Each appears to contain a tiny solar system!<br><br>“Well, looks like our hard work has brought us some fine rewards!” says @beffymaroo. “Let’s see what celestial wonders might appear if we hatch pet eggs with these new potions.”",
|
||||
"questStoneDropMossyStonePotion": "Mossy Stone Hatching Potion",
|
||||
@@ -760,7 +760,7 @@
|
||||
"questVirtualPetCompletion": "Some careful button pushing seems to have fulfilled the virtual pet’s mysterious needs, and finally it has quieted down and appears content.<br><br>Suddenly in a burst of confetti, the April Fool appears with a basket full of strange potions emitting soft beeps.<br><br>“What timing, April Fool,” @Beffymaroo says with a wry smile. “I suspect this large beeping fellow is an acquaintance of yours.”<br><br>“Uh, yes,” the Fool says, sheepishly. “So sorry about that, and thank you both for taking care of Wotchimon! Take these potions in the way of thanks, they can bring your Virtual pets back anytime you like!”<br><br>You’re not 100% sure you’re on board with all the beeping, but they’re sure cute so it’s worth a shot!",
|
||||
"questVirtualPetBoss": "Wotchimon",
|
||||
"questVirtualPetRageTitle": "The Beepening",
|
||||
"questVirtualPetRageEffect": "Wotchimon uses Bothersome Beep! Wotchimon sounds a bothersome beep, and its happiness bar suddenly disappears! Pending damage reduced.",
|
||||
"questVirtualPetRageEffect": "`Wotchimon uses Bothersome Beep!` Wotchimon sounds a bothersome beep, and its happiness bar suddenly disappears! Pending damage reduced.",
|
||||
"questVirtualPetDropVirtualPetPotion": "Virtual Pet Hatching Potion",
|
||||
"questVirtualPetUnlockText": "Unlocks Virtual Pet Hatching Potion for purchase in the Market",
|
||||
"questVirtualPetRageDescription": "This bar fills when you don't complete your Dailies. When it is full, the Wotchimon will take away some of your party's pending damage!",
|
||||
@@ -769,7 +769,7 @@
|
||||
"questPinkMarbleBoss": "Cupido",
|
||||
"questPinkMarbleRageTitle": "Pink Punch",
|
||||
"questPinkMarbleRageDescription": "This bar fills when you don't complete your Dailies. When it is full, Cupido will take away some of your party's pending damage!",
|
||||
"questPinkMarbleRageEffect": "Cupido uses Pink Punch! That wasn't affectionate at all! Your partymates are taken aback. Pending damage reduced.",
|
||||
"questPinkMarbleRageEffect": "`Cupido uses Pink Punch!` That wasn't affectionate at all! Your partymates are taken aback. Pending damage reduced.",
|
||||
"questPinkMarbleDropPinkMarblePotion": "Pink Marble Hatching Potion",
|
||||
"questPinkMarbleText": "Calm the Corrupted Cupid",
|
||||
"questGiraffeText": "The Gear-affe",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"rebirthNew": "Rebirth: New Adventure Available!",
|
||||
"rebirthUnlock": "You've unlocked Rebirth! This special Market item allows you to begin a new game at level 1 while keeping your tasks, achievements, pets, and more. Use it to breathe new life into Habitica if you feel you've achieved it all, or to experience new features with the fresh eyes of a beginning character!",
|
||||
"rebirthAchievement": "You've used the Orb of Rebirth <strong><%= number %></strong> times, and your highest level reached is <strong><%= level %></strong>.",
|
||||
"rebirthAchievement100": "You've used the Orb of Rebirth <strong><%= number %></strong> times, and your highest level reached is <strong>100</strong> or higher.",
|
||||
"rebirthAchievement": "You've begun a new adventure! This is Rebirth <%= number %> for you, and the highest Level you've attained is <%= level %>. To stack this Achievement, begin your next new adventure when you've reached an even higher Level!",
|
||||
"rebirthAchievement100": "You've begun a new adventure! This is Rebirth <%= number %> for you, and the highest Level you've attained is 100 or higher. To stack this Achievement, begin your next new adventure when you've reached at least 100!",
|
||||
"rebirthBegan": "Began a New Adventure",
|
||||
"rebirthText": "Began <%= rebirths %> New Adventures",
|
||||
"rebirthOrb": "Used an Orb of Rebirth to start over after attaining Level <%= level %>.",
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"onwards": "¡Adelante!",
|
||||
"levelup": "Al cumplir tus objetivos en la vida real, ¡has subido de nivel y has recuperado toda tu salud!",
|
||||
"reachedLevel": "Has Alcanzado el Nivel <%= level %>",
|
||||
"achievementLostMasterclasser": "Completista de Aventuras: Serie Arquimaestra",
|
||||
"achievementLostMasterclasser": "Completador de Aventuras: Serie Arquimaestra",
|
||||
"achievementLostMasterclasserText": "¡Ha completado las dieciséis misiones en la Serie Arquimaestra y resuelto el misterio de la Arquimaestra Perdida!",
|
||||
"achievementLostMasterclasserModalText": "¡Completaste las dieciséis misiones en la Serie Arquimaestra y resolviste el misterio de la Arquimaestra Perdida!",
|
||||
"achievementLostMasterclasserModalText": "¡Completaste las dieciséis misiones en la Serie Maestro de Clases y resolviste el misterio de la Arquimaestra Perdida!",
|
||||
"achievementMindOverMatter": "La mente sobre la materia",
|
||||
"achievementMindOverMatterText": "Ha completado las misiones de la Roca, el Limo y el Hilo.",
|
||||
"achievementMindOverMatterModalText": "¡Completaste las misiones de mascotas de la Roca, el Limo y el Hilo!",
|
||||
@@ -24,106 +24,106 @@
|
||||
"achievementAridAuthorityText": "Por domar todas las Monturas Desérticas.",
|
||||
"achievementAridAuthority": "Autoridad Árida",
|
||||
"achievementDustDevilModalText": "¡Conseguiste todas las Mascotas Desérticas!",
|
||||
"achievementDustDevilText": "Ha conseguido todas las Mascotas Desérticas.",
|
||||
"achievementDustDevilText": "Por conseguir todas las Mascotas Desérticas.",
|
||||
"achievementDustDevil": "Demonio de Polvo",
|
||||
"achievementMonsterMagus": "Monstruo Magus",
|
||||
"achievementUndeadUndertakerModalText": "¡Has domado todas las Monturas Zombi!",
|
||||
"achievementUndeadUndertakerText": "Ha domado todas las Monturas Zombi.",
|
||||
"achievementUndeadUndertakerText": "Por domar todas las Monturas Zombie.",
|
||||
"achievementUndeadUndertaker": "Sepulturero de Muertos Vivientes",
|
||||
"achievementMonsterMagusModalText": "¡Has conseguido todas las Mascotas Zombi!",
|
||||
"achievementMonsterMagusText": "Ha conseguido todas las Mascotas Zombi.",
|
||||
"achievementMonsterMagusText": "Por conseguir todas las Mascotas Zombi.",
|
||||
"achievementPartyOn": "¡Tu equipo llegó a 4 miembros!",
|
||||
"achievementPartyUp": "¡Formaste un equipo con alguien más!",
|
||||
"achievementPearlyProModalText": "¡Has domado todas las Monturas Blancas!",
|
||||
"achievementPearlyProText": "Ha domado todas las Monturas Blancas.",
|
||||
"achievementPearlyProModalText": "¡Has domado todas las Mascotas Blancas!",
|
||||
"achievementPearlyProText": "Ha domado Todas las Mascotas Blancas.",
|
||||
"achievementPrimedForPaintingModalText": "¡Has conseguido todas las Mascotas Blancas!",
|
||||
"achievementPrimedForPaintingText": "Ha conseguido todas las Mascotas Blancas.",
|
||||
"achievementPrimedForPainting": "Preparado para Pintar",
|
||||
"achievementPrimedForPainting": "Preparado para pintar",
|
||||
"hideAchievements": "Ocultar <%= category %>",
|
||||
"showAllAchievements": "Mostrar Todos <%= category %>",
|
||||
"onboardingCompleteDesc": "Has ganado <strong>5 Logros</strong> y <strong class=\"gold-amount\">100 de Oro</strong> por completar la lista.",
|
||||
"showAllAchievements": "Mostrar todo <%= category %>",
|
||||
"onboardingCompleteDesc": "Has ganado <strong>5 logros</strong> y <strong class=\"gold-amount\">100 de oro</strong> por completar la lista.",
|
||||
"earnedAchievement": "¡Has conseguido un logro!",
|
||||
"viewAchievements": "Ver Logros",
|
||||
"letsGetStarted": "¡Comencemos!",
|
||||
"onboardingProgress": "<%= percentage %> % de progreso",
|
||||
"gettingStartedDesc": "¡Completa estas tareas de introducción y ganarás <strong>5 Logros</strong> y <strong class=\"gold-amount\">100 de Oro</strong> una vez hayas terminado!",
|
||||
"gettingStartedDesc": "¡Completa estas tareas de incorporación y ganarás <strong>5 logros</strong> y <strong class=\"gold-amount\">100 de oro</strong> una vez hayas terminado!",
|
||||
"achievementCreatedTaskText": "Creó su primera tarea.",
|
||||
"achievementCreatedTask": "Crea tu primera tarea",
|
||||
"achievementFedPet": "Alimenta a una Mascota",
|
||||
"achievementFedPetModalText": "Hay muchos tipos diferentes de alimentos, pero las Mascotas pueden ser selectivas",
|
||||
"achievementFedPet": "Alimenta a una mascota",
|
||||
"achievementFedPetModalText": "Hay muchos tipos diferentes de alimentos, pero las mascotas pueden ser exigentes",
|
||||
"achievementHatchedPetModalText": "Dirígete a tu inventario y prueba a combinar una poción de eclosión y un huevo",
|
||||
"achievementCompletedTaskText": "Ha completado su primera tarea.",
|
||||
"achievementPurchasedEquipmentModalText": "El Equipamiento es una forma de personalizar tu avatar y mejorar tus Estadísticas",
|
||||
"achievementPurchasedEquipment": "Compra una pieza de Equipamiento",
|
||||
"achievementCompletedTaskModalText": "Marca como completada cualquier tarea para conseguir recompensas",
|
||||
"achievementCompletedTaskText": "Has completado tu primera tarea.",
|
||||
"achievementPurchasedEquipmentModalText": "El equipamiento es una forma de personalizar tu avatar y mejorar tus estadísticas",
|
||||
"achievementPurchasedEquipment": "Compra una pieza de equipamiento",
|
||||
"achievementCompletedTaskModalText": "Marca alguna de tus tareas para conseguir recompensas",
|
||||
"achievementCompletedTask": "Completa una tarea",
|
||||
"achievementCreatedTaskModalText": "Añade una tarea para algo que te gustaría cumplir esta semana",
|
||||
"achievementFedPetText": "Alimentó a su primera mascota.",
|
||||
"achievementPurchasedEquipmentText": "Adquirió su primera pieza de equipamento.",
|
||||
"achievementPurchasedEquipmentText": "Adquirió su primera pieza de equipo.",
|
||||
"achievementHatchedPetText": "Eclosionó su primera mascota.",
|
||||
"achievementHatchedPet": "Eclosiona una Mascota",
|
||||
"achievementHatchedPet": "Eclosiona una mascota",
|
||||
"achievementPearlyPro": "Coleccionista de Perlas",
|
||||
"achievementTickledPinkModalText": "¡Has conseguido todas las mascotas de Algodón de Azúcar Rosa!",
|
||||
"achievementTickledPinkText": "Ha conseguido todas las mascotas de Algodón de Azúcar Rosa.",
|
||||
"achievementTickledPinkModalText": "¡Has conseguido todas las mascotas de algodón de azúcar rosa!",
|
||||
"achievementTickledPinkText": "Ha conseguido todas las mascotas de algodón de azúcar rosa.",
|
||||
"achievementTickledPink": "cosquillas rosa",
|
||||
"foundNewItemsCTA": "¡Dirígete a tu Inventario e intenta combinar tu nueva poción de eclosión y un huevo!",
|
||||
"foundNewItemsCTA": "¡Dirígete a tu Inventario e intenta combinar tu nueva poción para incubar y un huevo!",
|
||||
"foundNewItemsExplanation": "Completar tareas te da la oportunidad de encontrar objetos, como Huevos, Pociones de Eclosión y Alimento para Mascotas.",
|
||||
"foundNewItems": "¡Encontraste nuevos artículos!",
|
||||
"onboardingCompleteDescSmall": "¡Si quieres aún más, mira tus Logros y empieza a coleccionarlos!",
|
||||
"yourProgress": "Tu Progreso",
|
||||
"onboardingComplete": "¡Has completado tus tareas de introducción!",
|
||||
"achievementRosyOutlookModalText": "¡Has domado todas las Monturas de Algodón de Azúcar Rosa!",
|
||||
"achievementRosyOutlookText": "Ha domado todas las Monturas de Algodón de Azúcar Rosa.",
|
||||
"achievementRosyOutlook": "Perspectiva Optimista",
|
||||
"achievementBareNecessities": "Necesidades Básicas",
|
||||
"achievementBugBonanzaModalText": "¡Has completado las misiones de las mascotas Escarabajo, Mariposa, Caracol y Araña!",
|
||||
"achievementBugBonanzaText": "Ha completado las misiones de las mascotas Escarabajo, Mariposa, Caracol y Araña.",
|
||||
"achievementBareNecessitiesModalText": "¡Has completado las misiones de las mascotas Mono, Perezoso y Brote!",
|
||||
"achievementBareNecessitiesText": "Ha completado las misiones de las mascotas Mono, Perezoso y Brote.",
|
||||
"onboardingComplete": "¡Has completado tus tareas de incorporación!",
|
||||
"achievementRosyOutlookModalText": "¡Has domado todas las monturas de algodón de azúcar rosa!",
|
||||
"achievementRosyOutlookText": "Ha domado todas las monturas de algodón de azúcar rosa.",
|
||||
"achievementRosyOutlook": "Perspectiva optimista",
|
||||
"achievementBareNecessities": "Necesidades básicas",
|
||||
"achievementBugBonanzaModalText": "¡Has completado las misiones de las mascotas escarabajo, mariposa, caracol y araña!",
|
||||
"achievementBugBonanzaText": "Ha completado las misiones de las mascotas escarabajo, mariposa, caracol y araña.",
|
||||
"achievementBareNecessitiesModalText": "¡Has completado las misiones de las mascotas mono, perezoso y brote!",
|
||||
"achievementBareNecessitiesText": "Ha completado las misiones de las mascotas mono, perezoso y brote.",
|
||||
"achievementBugBonanza": "Bonanza insectil",
|
||||
"achievementFreshwaterFriendsModalText": "¡Has completado las misiones de las mascotas Ajolote, Rana e Hipopótamo!",
|
||||
"achievementFreshwaterFriendsText": "Ha completado las misiones de las mascotas Ajolote, Rana, e Hipopótamo.",
|
||||
"achievementFreshwaterFriends": "Amigos de Agua Dulce",
|
||||
"achievementFreshwaterFriendsModalText": "¡Has completado las misiones de las mascotas ajolote, rana e hipopótamo!",
|
||||
"achievementFreshwaterFriendsText": "Ha completado las misiones de las mascotas ajolote, rana, e hipopótamo.",
|
||||
"achievementFreshwaterFriends": "Amigos de agua dulce",
|
||||
"achievementAllThatGlittersModalText": "¡Has domado todas las Monturas Doradas!",
|
||||
"achievementAllThatGlittersText": "Ha domado todas las Monturas Doradas.",
|
||||
"achievementGoodAsGoldModalText": "¡Has conseguido todas las Mascotas Doradas!",
|
||||
"achievementGoodAsGoldText": "Ha conseguido todas las Mascotas Doradas.",
|
||||
"achievementGoodAsGold": "Corazón de Oro",
|
||||
"achievementGoodAsGoldText": "Ha conseguido todas las Mascotas doradas.",
|
||||
"achievementGoodAsGold": "Más Bueno que el Pan",
|
||||
"yourRewards": "Tus Recompensas",
|
||||
"achievementAllThatGlitters": "Todo lo que Brilla",
|
||||
"achievementBoneCollector": "Coleccionista de Huesos",
|
||||
"achievementBoneCollectorText": "Ha conseguido todas las Mascotas Esqueleto.",
|
||||
"achievementAllThatGlitters": "Todo lo que brilla",
|
||||
"achievementBoneCollector": "Coleccionista de huesos",
|
||||
"achievementBoneCollectorText": "Ha conseguido todas las Mascotas esqueléticas.",
|
||||
"achievementSeeingRed": "Rojo de Ira",
|
||||
"achievementSkeletonCrewModalText": "¡Has domado todas las Monturas Esqueleto!",
|
||||
"achievementSkeletonCrewText": "Ha domado todas las Monturas Esqueleto.",
|
||||
"achievementSkeletonCrew": "Ejército Huesudo",
|
||||
"achievementBoneCollectorModalText": "¡Has conseguido todas las Mascotas Esqueleto!",
|
||||
"achievementSeeingRedText": "Ha conseguido todas las Mascotas Rojas.",
|
||||
"achievementSeeingRedModalText": "¡Has conseguido todas las Mascotas Rojas!",
|
||||
"achievementRedLetterDayModalText": "¡Has domado todas las Monturas Rojas!",
|
||||
"achievementRedLetterDayText": "Ha domado todas las Monturas Rojas.",
|
||||
"achievementSkeletonCrewModalText": "¡Has domado todas las monturas esqueléticas!",
|
||||
"achievementSkeletonCrewText": "Ha domado todas las monturas esqueléticas.",
|
||||
"achievementSkeletonCrew": "Equipo esquelético",
|
||||
"achievementBoneCollectorModalText": "¡Has conseguido todas las mascotas esqueléticas!",
|
||||
"achievementSeeingRedText": "Ha conseguido todas las Mascotas rojas.",
|
||||
"achievementSeeingRedModalText": "¡Has conseguido todas las mascotas rojas!",
|
||||
"achievementRedLetterDayModalText": "¡Has domado todas las monturas rojas!",
|
||||
"achievementRedLetterDayText": "Ha domado todas las monturas rojas.",
|
||||
"achievementRedLetterDay": "Día señalado",
|
||||
"achievementLegendaryBestiaryModalText": "¡Has conseguido todas las mascotas míticas!",
|
||||
"achievementLegendaryBestiaryText": "¡Ha eclosionado todos los colores estándar de todas las mascotas míticas: Dragón, Cerdo Volador, Grifo, Serpiente Marina y Unicornio!",
|
||||
"achievementLegendaryBestiary": "Bestiario Legendario",
|
||||
"achievementLegendaryBestiary": "Bestiario legendario",
|
||||
"achievementSeasonalSpecialistModalText": "¡Completaste todas las misiones estacionales!",
|
||||
"achievementSeasonalSpecialistText": "Ha completado todas las misiones estacionales de Primavera e Invierno: ¡Búsqueda de Huevos, Santa Trampero, y Encuentra al Cachorro!",
|
||||
"achievementSeasonalSpecialist": "Especialista Estacional",
|
||||
"achievementVioletsAreBlue": "Las Violetas son Azules",
|
||||
"achievementVioletsAreBlue": "Las violetas son azules",
|
||||
"achievementWildBlueYonderModalText": "¡Has domado todas las monturas de Algodon de Azúcar Azul!",
|
||||
"achievementWildBlueYonderText": "Ha domado todas las monturas de Algodon de Azúcar Azul.",
|
||||
"achievementWildBlueYonder": "La Salvaje y Azul Lejanía",
|
||||
"achievementWildBlueYonder": "La salvaje y azul lejanía",
|
||||
"achievementVioletsAreBlueModalText": "¡Has conseguido todas las mascotas de Algodon de Azúcar Azul!",
|
||||
"achievementVioletsAreBlueText": "Ha conseguido todas las mascotas de Algodon de Azúcar Azul.",
|
||||
"achievementDomesticatedText": "¡Ha eclosionado todos los colores de mascotas domésticas: Hurón, Cobaya, Gallo, Cerdo Volador, Rata, Conejito, Caballo y Vaca!",
|
||||
"achievementDomesticated": "I-A-I-A-O",
|
||||
"achievementDomesticatedText": "¡Has criado todos los colores de mascotas domésticas: hurón, cobaya, gallo, cerdo volador, rata, conejito, caballo y vaca!",
|
||||
"achievementDomesticated": "E-I-E-I-O",
|
||||
"achievementDomesticatedModalText": "¡Has conseguido todas las mascotas domesticadas!",
|
||||
"achievementShadyCustomerModalText": "¡Has conseguido todas las Mascotas Sombrías!",
|
||||
"achievementShadeOfItAll": "A la Sombra de Todo",
|
||||
"achievementShadeOfItAllText": "Ha domado todas las Monturas Sombrías.",
|
||||
"achievementShadeOfItAllModalText": "¡Has domado todas las Monturas Sombrías!",
|
||||
"achievementShadyCustomerText": "Ha conseguido todas las Mascotas Sombrías.",
|
||||
"achievementShadyCustomer": "Cliente Sombrío",
|
||||
"achievementShadyCustomerModalText": "¡Has conseguido todas las mascotas sombrías!",
|
||||
"achievementShadeOfItAll": "La sombra de todo ello",
|
||||
"achievementShadeOfItAllText": "Ha domado todas las monturas sombrías.",
|
||||
"achievementShadeOfItAllModalText": "¡Has domado todas las monturas sombrías!",
|
||||
"achievementShadyCustomerText": "Ha conseguido todas las mascotas sombrías.",
|
||||
"achievementShadyCustomer": "Cliente sombrío",
|
||||
"achievementZodiacZookeeper": "Cuidador del Zodiaco",
|
||||
"achievementZodiacZookeeperText": "¡Ha eclosionado todas las mascotas del zodíaco de color básico: Rata, Vaca, Conejo, Serpiente, Caballo, Oveja, Mono, Gallo, Lobo, Tigre, Cerdo Volador y Dragón!",
|
||||
"achievementZodiacZookeeperModalText": "¡Has conseguido todas las mascotas del zodíaco!",
|
||||
@@ -131,39 +131,39 @@
|
||||
"achievementBirdsOfAFeatherModalText": "¡Has conseguido todas las mascotas voladoras!",
|
||||
"achievementBirdsOfAFeather": "Aves de Pluma",
|
||||
"achievementReptacularRumbleModalText": "¡Has coleccionado todas las mascotas reptiles!",
|
||||
"achievementReptacularRumbleText": "¡Ha eclosionado todos los colores estándar de las mascotas reptiles: Caimán, Pterodáctilo, Serpiente, Triceratops, Tortuga, Tiranosaurio Rex y Velociraptor!",
|
||||
"achievementGroupsBeta2022": "Probador Beta Interactivo",
|
||||
"achievementGroupsBeta2022Text": "Tú y tu grupo proporcionaron comentarios invaluables para ayudar a probar Habitica.",
|
||||
"achievementReptacularRumbleText": "!Has incubado todos los colores estándar de las mascotas reptiles: caimán, pterodáctilo, serpiente, triceratops, tortuga, tiranosaurio rex y velociraptor!",
|
||||
"achievementGroupsBeta2022": "Probador Beta interactivo",
|
||||
"achievementGroupsBeta2022Text": "Tú y tu grupo habéis proporcionado comentarios invaluables para ayudar a probar Habitica.",
|
||||
"achievementGroupsBeta2022ModalText": "¡Usted y sus grupos ayudaron a Habitica probando y proporcionando comentarios!",
|
||||
"achievementReptacularRumble": "Rumble reptacular",
|
||||
"achievementWoodlandWizard": "Mago del Bosque",
|
||||
"achievementWoodlandWizardModalText": "¡Has conseguido todas las mascotas del bosque!",
|
||||
"achievementWoodlandWizardText": "¡Ha eclosionado todos los colores estándar de las criaturas del bosque: Tejón, Oso, Ciervo, Zorro, Rana, Erizo, Búho, Caracol, Ardilla y Brote!",
|
||||
"achievementBoneToPick": "Un Hueso Duro de Roer",
|
||||
"achievementBoneToPickText": "¡Ha eclosionado todas las Mascotas Esqueleto, Clásicas y de Misión!",
|
||||
"achievementWoodlandWizard": "Mago del bosque",
|
||||
"achievementWoodlandWizardModalText": "¡Has recogido todas las mascotas del bosque!",
|
||||
"achievementWoodlandWizardText": "Ha incubado todos los colores estándar de las criaturas del bosque: Tejón, Oso, Ciervo, Zorro, Rana, Erizo, Búho, Caracol, Ardilla y Treeling!",
|
||||
"achievementBoneToPick": "Un hueso duro de roer",
|
||||
"achievementBoneToPickText": "¡Ha eclosionado todas las mascotas Clásicas y de Misión Esqueléticas!",
|
||||
"achievementPolarPro": "Experto Polar",
|
||||
"achievementPolarProModalText": "¡Has conseguido todas las Mascotas Polares!",
|
||||
"achievementBoneToPickModalText": "¡Has coleccionado todas las Mascotas Esqueleto, Clásicas y de Misión!",
|
||||
"achievementPolarProModalText": "¡Has coleccionado todas las mascotas Polares!",
|
||||
"achievementBoneToPickModalText": "¡Has coleccionado todas las mascotas clásicas y de misiones esqueléticas!",
|
||||
"achievementPolarProText": "¡Ha eclosionado todos los colores estándar para mascotas Polares: Osos, Zorros, Pinguinos, Ballenas y Lobos!",
|
||||
"achievementPlantParent": "Cuidador de Plantas",
|
||||
"achievementPlantParentText": "¡Ha eclosionado todos los colores estándar de mascotas vegetales: Cactus y Brote!",
|
||||
"achievementPlantParentModalText": "¡Has conseguido todas las Mascotas Planta!",
|
||||
"achievementPlantParentText": "¡Ha eclosionado todos los colores estándar de mascotas vegetales: Cactus y Árbolito!",
|
||||
"achievementPlantParentModalText": "¡Has coleccionado todas las Mascotas Planta!",
|
||||
"achievementDinosaurDynasty": "Dinastía de Dinosaurios",
|
||||
"achievementDinosaurDynastyModalText": "¡Has conseguido todas las mascotas ave y dinosaurio!",
|
||||
"achievementDinosaurDynastyText": "Ha eclosionado todos los colores estándar de mascotas ave y dinosaurio: Halcón, Búho, Loro, Pavo Real, Pingüino, Gallo, Pterodáctilo, Tiranosaurio rex, Triceratops y Velociraptor!",
|
||||
"achievementDinosaurDynastyModalText": "¡Has recogido todas las mascotas de pájaros y dinosaurios!",
|
||||
"achievementDinosaurDynastyText": "Ha eclosionado todos los colores estándar de mascotas, de aves y dinosaurios: halcón, búho, loro, pavo real, pingüino, gallo, pterodáctilo, tiranosaurio rex, triceratops y velociraptor!",
|
||||
"achievementRoughRider": "Jinete tosco",
|
||||
"achievementRoughRiderText": "¡Ha eclosionado todos los colores estándar de mascotas y monturas incómodas: Cactus, Erizo y Piedra!",
|
||||
"achievementBonelessBossModalText": "¡Has conseguido todas las mascotas invertebradas!",
|
||||
"achievementBonelessBossText": "¡Ha eclosionado todos los colores estándar de mascotas invertebradas: Escarabajo, Mariposa, Calamar, Nudibranquio, Pulpo, Caracol y Araña!",
|
||||
"achievementBonelessBoss": "Jefe Deshuesado",
|
||||
"achievementDuneBuddyText": "¡Ha eclosionado todos los colores estándar de mascotas de clima desértico: Armadillo, Cactus, Zorro, Rana, Serpiente y Araña!",
|
||||
"achievementBonelessBossModalText": "¡Tienes todas las mascotas invertebradas en tu colección!",
|
||||
"achievementBonelessBossText": "Ha eclosionado todos los colores estándar de mascotas invertebradas: escarabajo, mariposa calamar, nudibranquio, pulpo, caracol y araña!",
|
||||
"achievementBonelessBoss": "Jefe deshuesado",
|
||||
"achievementDuneBuddyText": "¡Ha eclosionado todos los colores estándar de mascotas de clima desértico: armadillo, cactus, zorro, rana, serpiente y araña!",
|
||||
"achievementDuneBuddy": "Amigo de médano",
|
||||
"achievementDuneBuddyModalText": "¡Has conseguido todas las mascotas de desierto!",
|
||||
"achievementRoughRiderModalText": "¡Has conseguido todos los colores básicos de mascotas y monturas incómodas!",
|
||||
"achievementRodentRuler": "Rey Roedor",
|
||||
"achievementRodentRulerText": "¡Ha eclosionado todos los colores estándar de mascotas roedores: Conejillo de Indias, Rata y Ardilla!",
|
||||
"achievementRodentRulerModalText": "¡Has conseguido todos las mascotas roedores!",
|
||||
"achievementCats": "Señora de los Gatos",
|
||||
"achievementCatsText": "¡Ha eclosionado todos los colores estándar de mascotas felinas: Guepardo, León, Tigre Dientes de Sable y Tigre!",
|
||||
"achievementRoughRiderModalText": "¡Has conseguido todos los colores básicos de las mascotas y monturas incómodas!",
|
||||
"achievementRodentRuler": "Gobernante Roedor",
|
||||
"achievementRodentRulerText": "¡Ha eclosionado todos los colores estándar de las mascotas roedores: Conejillo de Indias, Rata y Ardilla!",
|
||||
"achievementRodentRulerModalText": "¡Has conseguido todos las roedores mascota!",
|
||||
"achievementCats": "Pastor de Gatos",
|
||||
"achievementCatsText": "¡Ha eclosionado todos los colores estándar de las mascotas felinas: Guepardo, León, Tigre Dientes de Sable y Tigre!",
|
||||
"achievementCatsModalText": "¡Has conseguido todas las mascotas felinas!"
|
||||
}
|
||||
|
||||
@@ -1,203 +1,203 @@
|
||||
{
|
||||
"backgrounds": "Fondos",
|
||||
"background": "Fondo",
|
||||
"backgroundShop": "Tienda de Fondos",
|
||||
"noBackground": "Ningún Fondo Seleccionado",
|
||||
"backgroundShop": "Tienda de fondos",
|
||||
"noBackground": "Ningún fondo seleccionado",
|
||||
"backgrounds062014": "1.ª serie: publicada en junio de 2014",
|
||||
"backgroundBeachText": "Playa",
|
||||
"backgroundBeachNotes": "Relájate en una cálida playa.",
|
||||
"backgroundFairyRingText": "Anillo de Hadas",
|
||||
"backgroundFairyRingText": "Anillo de hadas",
|
||||
"backgroundFairyRingNotes": "Baila en un anillo de hadas.",
|
||||
"backgroundForestText": "Bosque",
|
||||
"backgroundForestNotes": "Pasea por un bosque estival.",
|
||||
"backgrounds072014": "2.ª serie: publicada en julio de 2014",
|
||||
"backgroundCoralReefText": "Arrecife de Coral",
|
||||
"backgroundCoralReefText": "Arrecife de coral",
|
||||
"backgroundCoralReefNotes": "Nada en un arrecife de coral.",
|
||||
"backgroundOpenWatersText": "Aguas Abiertas",
|
||||
"backgroundOpenWatersText": "Aguas abiertas",
|
||||
"backgroundOpenWatersNotes": "Disfruta de las aguas abiertas.",
|
||||
"backgroundSeafarerShipText": "Bajel de Marineros",
|
||||
"backgroundSeafarerShipNotes": "Navega a bordo de un Barco Marinero.",
|
||||
"backgroundSeafarerShipText": "Bajel de marineros",
|
||||
"backgroundSeafarerShipNotes": "Navega a bordo de un barco marinero.",
|
||||
"backgrounds082014": "3.ª serie: publicada en agosto de 2014",
|
||||
"backgroundCloudsText": "Nubes",
|
||||
"backgroundCloudsNotes": "Planea entre las nubes.",
|
||||
"backgroundDustyCanyonsText": "Cañón Polvoriento",
|
||||
"backgroundDustyCanyonsText": "Cañón polvoriento",
|
||||
"backgroundDustyCanyonsNotes": "Pasea por un cañón polvoriento.",
|
||||
"backgroundVolcanoText": "Volcán",
|
||||
"backgroundVolcanoNotes": "Entra en calor dentro de un volcán.",
|
||||
"backgrounds092014": "4.ª serie: publicada en septiembre de 2014",
|
||||
"backgroundThunderstormText": "Tormenta Eléctrica",
|
||||
"backgroundThunderstormText": "Tormenta eléctrica",
|
||||
"backgroundThunderstormNotes": "Conduce rayos en la tormenta eléctrica.",
|
||||
"backgroundAutumnForestText": "Bosque Otoñal",
|
||||
"backgroundAutumnForestText": "Bosque otoñal",
|
||||
"backgroundAutumnForestNotes": "Pasea por un bosque otoñal.",
|
||||
"backgroundHarvestFieldsText": "Campos de Cultivo",
|
||||
"backgroundHarvestFieldsText": "Campos de cultivo",
|
||||
"backgroundHarvestFieldsNotes": "Labra tus campos de cultivo.",
|
||||
"backgrounds102014": "5.ª serie: publicada en octubre de 2014",
|
||||
"backgroundGraveyardText": "Cementerio",
|
||||
"backgroundGraveyardNotes": "Visita un espeluznante cementerio.",
|
||||
"backgroundHauntedHouseText": "Casa Encantada",
|
||||
"backgroundHauntedHouseText": "Casa encantada",
|
||||
"backgroundHauntedHouseNotes": "Entra a hurtadillas en una casa encantada.",
|
||||
"backgroundPumpkinPatchText": "Campo de Calabazas",
|
||||
"backgroundPumpkinPatchNotes": "Talla tus calabazas en este campo de calabazas.",
|
||||
"backgroundPumpkinPatchText": "Terreno de calabazas",
|
||||
"backgroundPumpkinPatchNotes": "Talla tu calabaza en este terreno.",
|
||||
"backgrounds112014": "6.ª serie: publicada en noviembre de 2014",
|
||||
"backgroundHarvestFeastText": "Festín de la Cosecha",
|
||||
"backgroundHarvestFeastNotes": "Disfruta del Banquete de la Cosecha.",
|
||||
"backgroundStarrySkiesText": "Cielos Estrellados",
|
||||
"backgroundHarvestFeastText": "Festín de la cosecha",
|
||||
"backgroundHarvestFeastNotes": "Disfruta del banquete de la cosecha.",
|
||||
"backgroundStarrySkiesText": "Cielos estrellados",
|
||||
"backgroundStarrySkiesNotes": "Contempla los cielos repletos de estrellas.",
|
||||
"backgroundSunsetMeadowText": "Atardecer en la Pradera",
|
||||
"backgroundSunsetMeadowText": "Atardecer en la pradera",
|
||||
"backgroundSunsetMeadowNotes": "Admira un atardecer en la pradera.",
|
||||
"backgrounds122014": "7.ª serie: publicada en diciembre de 2014",
|
||||
"backgroundIcebergText": "Témpano de Hielo",
|
||||
"backgroundIcebergNotes": "Flota a la deriva sobre un Témpano de Hielo.",
|
||||
"backgroundTwinklyLightsText": "Luces Brillantes de Invierno",
|
||||
"backgroundIcebergText": "Iceberg",
|
||||
"backgroundIcebergNotes": "Flota a la deriva sobre un iceberg.",
|
||||
"backgroundTwinklyLightsText": "Luces brillantes de invierno",
|
||||
"backgroundTwinklyLightsNotes": "Camina entre árboles engalanados con luces festivas.",
|
||||
"backgroundSouthPoleText": "Polo Sur",
|
||||
"backgroundSouthPoleNotes": "Visita el gélido Polo Sur.",
|
||||
"backgrounds012015": "8.ª serie: publicada en enero de 2015",
|
||||
"backgroundIceCaveText": "Cueva de Hielo",
|
||||
"backgroundIceCaveNotes": "Desciende y adéntrate en una Cueva de Hielo.",
|
||||
"backgroundFrigidPeakText": "Cima Glacial",
|
||||
"backgroundIceCaveText": "Cueva de hielo",
|
||||
"backgroundIceCaveNotes": "Desciende y adéntrate en una cueva de hielo.",
|
||||
"backgroundFrigidPeakText": "Cima glacial",
|
||||
"backgroundFrigidPeakNotes": "Escala una cima glacial.",
|
||||
"backgroundSnowyPinesText": "Pinos Nevados",
|
||||
"backgroundSnowyPinesText": "Pinos nevados",
|
||||
"backgroundSnowyPinesNotes": "Refúgiate entre pinos nevados.",
|
||||
"backgrounds022015": "9.ª serie: publicada en febrero de 2015",
|
||||
"backgroundBlacksmithyText": "Forja",
|
||||
"backgroundBlacksmithyNotes": "Trabaja en la forja.",
|
||||
"backgroundCrystalCaveText": "Cueva de Cristal",
|
||||
"backgroundCrystalCaveText": "Cueva de cristal",
|
||||
"backgroundCrystalCaveNotes": "Explora una cueva de cristal.",
|
||||
"backgroundDistantCastleText": "Castillo Distante",
|
||||
"backgroundDistantCastleText": "Castillo distante",
|
||||
"backgroundDistantCastleNotes": "Defiende un castillo distante.",
|
||||
"backgrounds032015": "10.ª serie: publicada en marzo de 2015",
|
||||
"backgroundSpringRainText": "Lluvia Primaveral",
|
||||
"backgroundSpringRainText": "Lluvia primaveral",
|
||||
"backgroundSpringRainNotes": "Baila bajo la lluvia de primavera.",
|
||||
"backgroundStainedGlassText": "Vidriera",
|
||||
"backgroundStainedGlassNotes": "Contempla las vidrieras.",
|
||||
"backgroundRollingHillsText": "Colinas Ondulantes",
|
||||
"backgroundRollingHillsText": "Colinas ondulantes",
|
||||
"backgroundRollingHillsNotes": "Corretea por las colinas ondulantes.",
|
||||
"backgrounds042015": "11.ª serie: publicada en abril de 2015",
|
||||
"backgroundCherryTreesText": "Cerezos",
|
||||
"backgroundCherryTreesNotes": "Admira los cerezos en flor.",
|
||||
"backgroundFloralMeadowText": "Prado Floreciente",
|
||||
"backgroundFloralMeadowText": "Prado floreciente",
|
||||
"backgroundFloralMeadowNotes": "Ve de pícnic a un prado floreciente.",
|
||||
"backgroundGumdropLandText": "País de las Gominolas",
|
||||
"backgroundGumdropLandNotes": "Mordisquea el paisaje del País de las Gominolas.",
|
||||
"backgrounds052015": "12.ª serie: publicada en mayo de 2015",
|
||||
"backgroundMarbleTempleText": "Templo de Mármol",
|
||||
"backgroundMarbleTempleText": "Templo de mármol",
|
||||
"backgroundMarbleTempleNotes": "Posa delante de un templo de mármol.",
|
||||
"backgroundMountainLakeText": "Lago de Montaña",
|
||||
"backgroundMountainLakeText": "Lago de montaña",
|
||||
"backgroundMountainLakeNotes": "Atrévete a sumergir la punta del pie en este lago de montaña.",
|
||||
"backgroundPagodasText": "Pagodas",
|
||||
"backgroundPagodasNotes": "Sube a lo alto de las Pagodas.",
|
||||
"backgroundPagodasNotes": "Sube a lo alto de las pagodas.",
|
||||
"backgrounds062015": "13.ª serie: publicada en junio de 2015",
|
||||
"backgroundDriftingRaftText": "Balsa a la Deriva",
|
||||
"backgroundDriftingRaftText": "Balsa a la deriva",
|
||||
"backgroundDriftingRaftNotes": "Rema sobre una balsa a la deriva.",
|
||||
"backgroundShimmeryBubblesText": "Burbujas Relucientes",
|
||||
"backgroundShimmeryBubblesText": "Burbujas relucientes",
|
||||
"backgroundShimmeryBubblesNotes": "Flota a través de un mar de burbujas relucientes.",
|
||||
"backgroundIslandWaterfallsText": "Cascadas Isleñas",
|
||||
"backgroundIslandWaterfallsText": "Cascadas isleñas",
|
||||
"backgroundIslandWaterfallsNotes": "Haz un pícnic junto a las cascadas de esta isla.",
|
||||
"backgrounds072015": "14.ª serie: publicada en julio de 2015",
|
||||
"backgroundDilatoryRuinsText": "Ruinas de Dilatoria",
|
||||
"backgroundDilatoryRuinsNotes": "Sumérgete en las ruinas de Dilatoria.",
|
||||
"backgroundGiantWaveText": "Ola Gigante",
|
||||
"backgroundGiantWaveText": "Ola gigante",
|
||||
"backgroundGiantWaveNotes": "¡Surfea una ola gigante!",
|
||||
"backgroundSunkenShipText": "Barco Hundido",
|
||||
"backgroundSunkenShipText": "Barco hundido",
|
||||
"backgroundSunkenShipNotes": "Explora un barco hundido.",
|
||||
"backgrounds082015": "15.ª serie: publicada en agosto de 2015",
|
||||
"backgroundPyramidsText": "Pirámides",
|
||||
"backgroundPyramidsNotes": "Admira las pirámides.",
|
||||
"backgroundSunsetSavannahText": "Ocaso en la Sabana",
|
||||
"backgroundSunsetSavannahText": "Ocaso en la sabana",
|
||||
"backgroundSunsetSavannahNotes": "Acecha a tus presas al atardecer en la sabana.",
|
||||
"backgroundTwinklyPartyLightsText": "Luces Parpadeantes de Fiesta",
|
||||
"backgroundTwinklyPartyLightsText": "Luces parpadeantes de fiesta",
|
||||
"backgroundTwinklyPartyLightsNotes": "¡Baila bajo las luces festivas centelleantes!",
|
||||
"backgrounds092015": "16.ª serie: publicada en septiembre de 2015",
|
||||
"backgroundMarketText": "Mercado de Habitica",
|
||||
"backgroundMarketNotes": "Compra en el Mercado de Habitica.",
|
||||
"backgroundMarketNotes": "Compra en el mercado de Habitica.",
|
||||
"backgroundStableText": "Establo de Habitica",
|
||||
"backgroundStableNotes": "Cuida a tus monturas en el Establo de Habitica.",
|
||||
"backgroundStableNotes": "Cuida a tus monturas en el establo de Habitica.",
|
||||
"backgroundTavernText": "Taberna de Habitica",
|
||||
"backgroundTavernNotes": "Visita la Taberna de Habitica.",
|
||||
"backgrounds102015": "17.ª serie: publicada en octubre de 2015",
|
||||
"backgroundHarvestMoonText": "Luna de Cosecha",
|
||||
"backgroundHarvestMoonText": "Luna de cosecha",
|
||||
"backgroundHarvestMoonNotes": "Ríete a carcajadas bajo la luna de cosecha.",
|
||||
"backgroundSlimySwampText": "Pantano Lodoso",
|
||||
"backgroundSlimySwampText": "Pantano lodoso",
|
||||
"backgroundSlimySwampNotes": "Cruza con esfuerzo el pantano lodoso.",
|
||||
"backgroundSwarmingDarknessText": "Criaturas de la Oscuridad",
|
||||
"backgroundSwarmingDarknessText": "Criaturas de la oscuridad",
|
||||
"backgroundSwarmingDarknessNotes": "Tiembla entre las criaturas de la oscuridad.",
|
||||
"backgrounds112015": "18.ª serie: publicada en noviembre de 2015",
|
||||
"backgroundFloatingIslandsText": "Islas Flotantes",
|
||||
"backgroundFloatingIslandsText": "Islas flotantes",
|
||||
"backgroundFloatingIslandsNotes": "Salta entre las islas flotantes.",
|
||||
"backgroundNightDunesText": "Dunas Nocturnas",
|
||||
"backgroundNightDunesText": "Dunas nocturnas",
|
||||
"backgroundNightDunesNotes": "Camina tranquilamente por las dunas nocturnas.",
|
||||
"backgroundSunsetOasisText": "Oasis al Atardecer",
|
||||
"backgroundSunsetOasisText": "Oasis al atardecer",
|
||||
"backgroundSunsetOasisNotes": "Disfruta del oasis al atardecer.",
|
||||
"backgrounds122015": "19.ª serie: publicada en diciembre de 2015",
|
||||
"backgroundAlpineSlopesText": "Laderas Alpinas",
|
||||
"backgroundAlpineSlopesText": "Laderas alpinas",
|
||||
"backgroundAlpineSlopesNotes": "Esquía en las laderas alpinas.",
|
||||
"backgroundSnowySunriseText": "Amanecer Nevado",
|
||||
"backgroundSnowySunriseText": "Amanecer nevado",
|
||||
"backgroundSnowySunriseNotes": "Contempla el amanecer nevado.",
|
||||
"backgroundWinterTownText": "Pueblo Invernal",
|
||||
"backgroundWinterTownText": "Pueblo invernal",
|
||||
"backgroundWinterTownNotes": "Camina deprisa por el pueblo invernal.",
|
||||
"backgrounds012016": "20.ª serie: publicada en enero de 2016",
|
||||
"backgroundFrozenLakeText": "Lago Congelado",
|
||||
"backgroundFrozenLakeText": "Lago congelado",
|
||||
"backgroundFrozenLakeNotes": "Patina sobre un lago congelado.",
|
||||
"backgroundSnowmanArmyText": "Ejército de Muñecos de Nieve",
|
||||
"backgroundSnowmanArmyText": "Ejército de muñecos de nieve",
|
||||
"backgroundSnowmanArmyNotes": "Lidera un ejército de muñecos de nieve.",
|
||||
"backgroundWinterNightText": "Noche de Invierno",
|
||||
"backgroundWinterNightText": "Noche de invierno",
|
||||
"backgroundWinterNightNotes": "Mira las estrellas de una noche de invierno.",
|
||||
"backgrounds022016": "21.ª serie: publicada en febrero de 2016",
|
||||
"backgroundBambooForestText": "Bosque de Bambú",
|
||||
"backgroundBambooForestText": "Bosque de bambú",
|
||||
"backgroundBambooForestNotes": "Pasea por el bosque de bambú.",
|
||||
"backgroundCozyLibraryText": "Biblioteca Acogedora",
|
||||
"backgroundCozyLibraryText": "Biblioteca acogedora",
|
||||
"backgroundCozyLibraryNotes": "Lee en esta acogedora biblioteca.",
|
||||
"backgroundGrandStaircaseText": "Gran Escalinata",
|
||||
"backgroundGrandStaircaseText": "Gran escalinata",
|
||||
"backgroundGrandStaircaseNotes": "Desciende por la gran escalinata.",
|
||||
"backgrounds032016": "22.ª serie: publicada en marzo de 2016",
|
||||
"backgroundDeepMineText": "Mina Profunda",
|
||||
"backgroundDeepMineText": "Mina profunda",
|
||||
"backgroundDeepMineNotes": "Encuentra metales preciosos en esta mina profunda.",
|
||||
"backgroundRainforestText": "Selva Tropical",
|
||||
"backgroundRainforestText": "Selva tropical",
|
||||
"backgroundRainforestNotes": "Adéntrate en la selva tropical.",
|
||||
"backgroundStoneCircleText": "Crómlech",
|
||||
"backgroundStoneCircleNotes": "Lanza hechizos en este crómlech.",
|
||||
"backgrounds042016": "23.ª serie: publicada en abril de 2016",
|
||||
"backgroundArcheryRangeText": "Campo de Tiro con Arco",
|
||||
"backgroundArcheryRangeText": "Campo de tiro con arco",
|
||||
"backgroundArcheryRangeNotes": "Practica en este campo de tiro con arco.",
|
||||
"backgroundGiantFlowersText": "Flores Gigantes",
|
||||
"backgroundGiantFlowersText": "Flores gigantes",
|
||||
"backgroundGiantFlowersNotes": "Diviértete sobre estas gigantescas flores.",
|
||||
"backgroundRainbowsEndText": "Final del Arcoíris",
|
||||
"backgroundRainbowsEndText": "Final del arcoíris",
|
||||
"backgroundRainbowsEndNotes": "Encuentra oro al final del arcoíris.",
|
||||
"backgrounds052016": "24.ª serie: publicada en mayo de 2016",
|
||||
"backgroundBeehiveText": "Colmena",
|
||||
"backgroundBeehiveNotes": "Zumba y baila en una colmena.",
|
||||
"backgroundGazeboText": "Kiosko",
|
||||
"backgroundGazeboNotes": "Pelea en un kiosko.",
|
||||
"backgroundTreeRootsText": "Raíces de Árbol",
|
||||
"backgroundTreeRootsText": "Raíces de árbol",
|
||||
"backgroundTreeRootsNotes": "Explora las raíces del árbol.",
|
||||
"backgrounds062016": "25.ª serie: publicada en junio de 2016",
|
||||
"backgroundLighthouseShoreText": "Costa con Faro",
|
||||
"backgroundLighthouseShoreText": "Costa con faro",
|
||||
"backgroundLighthouseShoreNotes": "Pasea por la costa junto al faro.",
|
||||
"backgroundLilypadText": "Nenúfar",
|
||||
"backgroundLilypadNotes": "Salta sobre un nenúfar.",
|
||||
"backgroundWaterfallRockText": "Roca de Cascada",
|
||||
"backgroundWaterfallRockText": "Roca de cascada",
|
||||
"backgroundWaterfallRockNotes": "Chapotea junto a la roca de la cascada.",
|
||||
"backgrounds072016": "26.ª serie: publicada en julio de 2016",
|
||||
"backgroundAquariumText": "Acuario",
|
||||
"backgroundAquariumNotes": "Sube y baja dentro de este acuario.",
|
||||
"backgroundDeepSeaText": "Mar Profundo",
|
||||
"backgroundDeepSeaText": "Profundidades del mar",
|
||||
"backgroundDeepSeaNotes": "Bucea hasta las profundidades del mar.",
|
||||
"backgroundDilatoryCastleText": "Castillo de Dilatoria",
|
||||
"backgroundDilatoryCastleNotes": "Nada junto al Castillo de Dilatoria.",
|
||||
"backgrounds082016": "27.ª serie: publicada en agosto de 2016",
|
||||
"backgroundIdyllicCabinText": "Cabaña Idílica",
|
||||
"backgroundIdyllicCabinNotes": "Retírate a una cabaña idílica.",
|
||||
"backgroundMountainPyramidText": "Pirámide de Montaña",
|
||||
"backgroundIdyllicCabinText": "Cabaña bucólica",
|
||||
"backgroundIdyllicCabinNotes": "Retírate a una cabaña bucólica.",
|
||||
"backgroundMountainPyramidText": "Pirámide de montaña",
|
||||
"backgroundMountainPyramidNotes": "Sube los incontables peldaños de esta pirámide de montaña.",
|
||||
"backgroundStormyShipText": "Barco Tormentoso",
|
||||
"backgroundStormyShipText": "Barco tormentoso",
|
||||
"backgroundStormyShipNotes": "Agárrate fuerte contra viento y marea a bordo de un barco tormentoso.",
|
||||
"backgrounds092016": "28.ª serie: publicada en septiembre de 2016",
|
||||
"backgroundCornfieldsText": "Campos de Maíz",
|
||||
"backgroundCornfieldsText": "Campos de maíz",
|
||||
"backgroundCornfieldsNotes": "Disfruta de un bonito día en los campos de maíz.",
|
||||
"backgroundFarmhouseText": "Casa de Granja",
|
||||
"backgroundFarmhouseText": "Casa de granja",
|
||||
"backgroundFarmhouseNotes": "Saluda a los animales de camino a la casa de la granja.",
|
||||
"backgroundOrchardText": "Huerto de Árboles Frutales",
|
||||
"backgroundOrchardText": "Huerto de árboles frutales",
|
||||
"backgroundOrchardNotes": "Coge fruta madura en el huerto.",
|
||||
"backgrounds102016": "29.ª serie: publicada en octubre de 2016",
|
||||
"backgroundSpiderWebText": "Telaraña",
|
||||
@@ -207,14 +207,14 @@
|
||||
"backgroundRainyCityText": "Ciudad Lluviosa",
|
||||
"backgroundRainyCityNotes": "Chapotea a través de una Ciudad Lluviosa.",
|
||||
"backgrounds112016": "30.ª serie: publicada en noviembre de 2016",
|
||||
"backgroundMidnightCloudsText": "Nubes de Medianoche",
|
||||
"backgroundMidnightCloudsNotes": "Vuela a través de las nubes de medianoche.",
|
||||
"backgroundStormyRooftopsText": "Techos Tormentosos",
|
||||
"backgroundStormyRooftopsNotes": "Deslízate a través de los techos tormentosos.",
|
||||
"backgroundMidnightCloudsText": "Media noche nublada",
|
||||
"backgroundMidnightCloudsNotes": "Vuela a través de la Media noche nublada.",
|
||||
"backgroundStormyRooftopsText": "Tempestuoso Techo",
|
||||
"backgroundStormyRooftopsNotes": "Deslízate a través del Tempestuoso Techo.",
|
||||
"backgroundWindyAutumnText": "Otoño Ventoso",
|
||||
"backgroundWindyAutumnNotes": "Persigue las hojas durante el otoño ventoso.",
|
||||
"backgroundWindyAutumnNotes": "Caza las hojas durante el Otoño Ventoso.",
|
||||
"incentiveBackgrounds": "Fondos Estándar",
|
||||
"backgroundVioletText": "Violeta",
|
||||
"backgroundVioletText": "violeta",
|
||||
"backgroundVioletNotes": "Un vibrante fondo violeta.",
|
||||
"backgroundBlueText": "Azul",
|
||||
"backgroundBlueNotes": "Un fondo básico azul.",
|
||||
@@ -228,16 +228,16 @@
|
||||
"backgroundYellowNotes": "Un agradable fondo amarillo.",
|
||||
"backgrounds122016": "31.ª serie: publicada en diciembre de 2016",
|
||||
"backgroundShimmeringIcePrismText": "Prismas de Hielo Relucientes",
|
||||
"backgroundShimmeringIcePrismNotes": "Baila junto a los prismas de hielo relucientes.",
|
||||
"backgroundShimmeringIcePrismNotes": "Baila junto a los Prismas de Hielo Relucientes.",
|
||||
"backgroundWinterFireworksText": "Fuegos Artificiales de Invierno",
|
||||
"backgroundWinterFireworksNotes": "Lanza los Fuegos Artificiales de Invierno.",
|
||||
"backgroundWinterStorefrontText": "Tienda Invernal",
|
||||
"backgroundWinterStorefrontNotes": "Compra regalos en la Tienda Invernal.",
|
||||
"backgrounds012017": "32.ª serie: publicada en enero de 2017",
|
||||
"backgroundBlizzardText": "Tormenta de Nieve",
|
||||
"backgroundBlizzardNotes": "Enfréntate a una terrible tormenta de nieve.",
|
||||
"backgroundBlizzardNotes": "Enfréntate a una terrible Tormenta de Nieve.",
|
||||
"backgroundSparklingSnowflakeText": "Copo de Nieve Chispeante",
|
||||
"backgroundSparklingSnowflakeNotes": "Deslízate en un copo de nieve chispeante.",
|
||||
"backgroundSparklingSnowflakeNotes": "Deslízate en un Copo de Nieve Chispeante.",
|
||||
"backgroundStoikalmVolcanoesText": "Volcanes Stoïkalm",
|
||||
"backgroundStoikalmVolcanoesNotes": "Explora los Volcanes Stoïkalm.",
|
||||
"backgrounds022017": "33.ª serie: publicada en febrero de 2017",
|
||||
@@ -246,10 +246,10 @@
|
||||
"backgroundTreasureRoomText": "Sala del Tesoro",
|
||||
"backgroundTreasureRoomNotes": "Disfruta de la riqueza de la Sala del Tesoro.",
|
||||
"backgroundWeddingArchText": "Arco de Boda",
|
||||
"backgroundWeddingArchNotes": "Posa bajo el arco de boda.",
|
||||
"backgroundWeddingArchNotes": "Posa bajo el Arco de Boda.",
|
||||
"backgrounds032017": "34.ª serie: publicada en marzo de 2017",
|
||||
"backgroundMagicBeanstalkText": "Tallo de Judía Mágico",
|
||||
"backgroundMagicBeanstalkNotes": "Sube por un tallo de judía mágico.",
|
||||
"backgroundMagicBeanstalkNotes": "Sube por un Tallo de Judía Mágico.",
|
||||
"backgroundMeanderingCaveText": "Cuerva Serpenteante",
|
||||
"backgroundMeanderingCaveNotes": "Explora la Cueva Serpenteante.",
|
||||
"backgroundMistiflyingCircusText": "Desconcertante Circo Volador",
|
||||
@@ -260,47 +260,47 @@
|
||||
"backgroundGiantBirdhouseText": "Casa de Pájaros Gigante",
|
||||
"backgroundGiantBirdhouseNotes": "Posarse sobre la Casa de Pájaros Gigante.",
|
||||
"backgroundMistShroudedMountainText": "Montaña Envuelta en Niebla",
|
||||
"backgroundMistShroudedMountainNotes": "Escala la montaña envuelta en niebla.",
|
||||
"backgroundMistShroudedMountainNotes": "Escala la Montaña Envuelta en Niebla.",
|
||||
"backgrounds052017": "36.ª serie: publicada en mayo de 2017",
|
||||
"backgroundGuardianStatuesText": "Estatuas Guardianes",
|
||||
"backgroundGuardianStatuesNotes": "Quédate en vigilia frente a Estatuas Guardianes.",
|
||||
"backgroundHabitCityStreetsText": "Calles de la Ciudad de los Hábitos",
|
||||
"backgroundHabitCityStreetsNotes": "Explora las Calles de la Ciudad de los Hábitos.",
|
||||
"backgroundOnATreeBranchText": "Sobre Una Rama de un Árbol",
|
||||
"backgroundOnATreeBranchNotes": "Pósate sobre una rama de un árbol.",
|
||||
"backgroundOnATreeBranchNotes": "Pósate sobre una Rama de un Árbol.",
|
||||
"backgrounds062017": "37.ª serie: publicada en junio de 2017",
|
||||
"backgroundBuriedTreasureText": "Tesoro Enterrado",
|
||||
"backgroundBuriedTreasureNotes": "Desentierra el tesoro enterrado.",
|
||||
"backgroundBuriedTreasureNotes": "Desentierra el Tesoro Enterrado.",
|
||||
"backgroundOceanSunriseText": "Amanecer Oceánico",
|
||||
"backgroundOceanSunriseNotes": "Admira el amanecer oceánico.",
|
||||
"backgroundOceanSunriseNotes": "Admira el Amanecer Oceánico.",
|
||||
"backgroundSandcastleText": "Castillo de Arena",
|
||||
"backgroundSandcastleNotes": "Rige sobre el castillo de arena.",
|
||||
"backgroundSandcastleNotes": "Rige sobre el Castillo de Arena.",
|
||||
"backgrounds072017": "38.ª serie: publicada en julio de 2017",
|
||||
"backgroundGiantSeashellText": "Concha Gigante",
|
||||
"backgroundGiantSeashellNotes": "Reposa en la concha gigante.",
|
||||
"backgroundGiantSeashellNotes": "Reposa en la Concha Gigante.",
|
||||
"backgroundKelpForestText": "Bosque de Algas Marinas",
|
||||
"backgroundKelpForestNotes": "Nada a través del bosque de algas marinas.",
|
||||
"backgroundKelpForestNotes": "Nada a través del Bosque de Algas Marinas.",
|
||||
"backgroundMidnightLakeText": "Lago Medianoche",
|
||||
"backgroundMidnightLakeNotes": "Descansa junto al Lago Medianoche.",
|
||||
"backgrounds082017": "39.ª serie: publicada en agosto de 2017",
|
||||
"backgroundBackOfGiantBeastText": "Espalda de una Bestia Gigante",
|
||||
"backgroundBackOfGiantBeastNotes": "Cabalga en la espalda de una bestia gigante.",
|
||||
"backgroundBackOfGiantBeastNotes": "Cabalga en la Espalda de una Bestia Gigante.",
|
||||
"backgroundDesertDunesText": "Dunas del Desierto",
|
||||
"backgroundDesertDunesNotes": "Explora valientemente las dunas del desierto.",
|
||||
"backgroundDesertDunesNotes": "Explora valientemente las Dunas del Desierto.",
|
||||
"backgroundSummerFireworksText": "Fuegos Artificiales de Verano",
|
||||
"backgroundSummerFireworksNotes": "¡Celebra el Día del Nombramiento de Habitica con Fuegos Artificiales de Verano!",
|
||||
"backgrounds092017": "40.ª serie: publicada en septiembre de 2017",
|
||||
"backgroundBesideWellText": "Al lado de un Pozo",
|
||||
"backgroundBesideWellNotes": "Paseo junto a un pozo.",
|
||||
"backgroundBesideWellNotes": "Paseo junto a un Pozo.",
|
||||
"backgroundGardenShedText": "Cobertizo de Jardín",
|
||||
"backgroundGardenShedNotes": "Trabaja en un cobertizo de jardín.",
|
||||
"backgroundGardenShedNotes": "Trabajo en un Cobertizo de Jardín.",
|
||||
"backgroundPixelistsWorkshopText": "Taller de Pixelist",
|
||||
"backgroundPixelistsWorkshopNotes": "Crea obras maestras en el Taller de Pixelist.",
|
||||
"backgrounds102017": "41.ª serie: publicada en octubre de 2017",
|
||||
"backgroundMagicalCandlesText": "Velas Mágicas",
|
||||
"backgroundMagicalCandlesNotes": "Déjate acariciar por la luz de velas mágicas.",
|
||||
"backgroundMagicalCandlesNotes": "Déjate acariciar por la luz de Velas Mágicas.",
|
||||
"backgroundSpookyHotelText": "Hotel Escalofriante",
|
||||
"backgroundSpookyHotelNotes": "Infíltrate por el vestíbulo de un hotel escalofriante.",
|
||||
"backgroundSpookyHotelNotes": "Infíltrate por el vestíbulo de un Hotel Escalofriante.",
|
||||
"backgroundTarPitsText": "Pozos de Alquitrán",
|
||||
"backgroundTarPitsNotes": "Pasa de puntillas por los Pozos de Alquitrán.",
|
||||
"backgrounds112017": "42.ª serie: publicada en noviembre de 2017",
|
||||
@@ -314,16 +314,16 @@
|
||||
"backgroundCrosscountrySkiTrailText": "Ruta de Ski Todo-Terreno",
|
||||
"backgroundCrosscountrySkiTrailNotes": "Planea sobre la Ruta de Ski Todo-Terreno.",
|
||||
"backgroundStarryWinterNightText": "Noche de Invierno Estrellada",
|
||||
"backgroundStarryWinterNightNotes": "Admira una noche de invierno estrellada.",
|
||||
"backgroundStarryWinterNightNotes": "Noche de Invierno Estrellada.",
|
||||
"backgroundToymakersWorkshopText": "Taller del Fabricante de Juguetes",
|
||||
"backgroundToymakersWorkshopNotes": "Disfrutar las maravillas del Taller del Fabricante de Juguetes.",
|
||||
"backgrounds012018": "44.ª serie: publicada en enero de 2018",
|
||||
"backgroundAuroraText": "Aurora",
|
||||
"backgroundAuroraNotes": "Disfruta del brillo invernal de la aurora.",
|
||||
"backgroundAuroraNotes": "Disfrutar del brillo invernal de la Aurora.",
|
||||
"backgroundDrivingASleighText": "Trineo",
|
||||
"backgroundDrivingASleighNotes": "Conduce un trineo sobre terreno nevado.",
|
||||
"backgroundDrivingASleighNotes": "Conduce un Trineo sobre terreno nevado.",
|
||||
"backgroundFlyingOverIcySteppesText": "Estepas heladas",
|
||||
"backgroundFlyingOverIcySteppesNotes": "Vuela sobre las estepas heladas.",
|
||||
"backgroundFlyingOverIcySteppesNotes": "Volar sobre las Estepas Heladas.",
|
||||
"backgrounds022018": "45.ª serie: publicada en febrero de 2018",
|
||||
"backgroundChessboardLandText": "Tierra de Tablero de Ajedrez",
|
||||
"backgroundChessboardLandNotes": "Juega una partida en la Tierra de Tablero de Ajedrez.",
|
||||
@@ -333,16 +333,16 @@
|
||||
"backgroundRoseGardenNotes": "Pasear por el Jardín de Rosas.",
|
||||
"backgrounds032018": "45.ª serie: publicada en marzo de 2018",
|
||||
"backgroundGorgeousGreenhouseText": "Espléndido Invernadero",
|
||||
"backgroundGorgeousGreenhouseNotes": "Camina entre la flora que se cuida en un espléndido invernadero.",
|
||||
"backgroundGorgeousGreenhouseNotes": "Camina entre la flora que se cuida en el Espléndido Invernadero.",
|
||||
"backgroundElegantBalconyText": "Elegante Balcón",
|
||||
"backgroundElegantBalconyNotes": "Admira el paisaje desde un elegante balcón.",
|
||||
"backgroundElegantBalconyNotes": "Admira el paisaje desde un Elegante Balcón.",
|
||||
"backgroundDrivingACoachText": "Conduciendo una Carroza",
|
||||
"backgroundDrivingACoachNotes": "Disfruta conducir una carroza por campos de flores.",
|
||||
"backgroundDrivingACoachNotes": "Disfruta Conducir una Carroza por campos de flores.",
|
||||
"backgrounds042018": "47.ª serie: publicada en abril de 2018",
|
||||
"backgroundTulipGardenText": "Jardín de Tulipanes",
|
||||
"backgroundTulipGardenNotes": "Pasa de puntillas por un jardín de tulipanes.",
|
||||
"backgroundTulipGardenNotes": "Pasa de puntillas por un Jardín de Tulipanes.",
|
||||
"backgroundFlyingOverWildflowerFieldText": "Campo de Flores silvestres",
|
||||
"backgroundFlyingOverWildflowerFieldNotes": "Elévate por encima de un campo de flores silvestres.",
|
||||
"backgroundFlyingOverWildflowerFieldNotes": "Elévate por encima de un Campo de Flores silvestres.",
|
||||
"backgroundFlyingOverAncientForestText": "Bosque Antiguo",
|
||||
"backgroundFlyingOverAncientForestNotes": "Vuela por encima de las copas de los árboles de un bosque antiguo.",
|
||||
"backgrounds052018": "48.ª serie: publicada en mayo de 2018",
|
||||
@@ -355,19 +355,19 @@
|
||||
"backgrounds062018": "49.ª serie: publicada en junio de 2018",
|
||||
"backgroundDocksText": "Muelle",
|
||||
"backgroundDocksNotes": "Pescado de lo alto del muelle.",
|
||||
"backgroundRowboatText": "Bote de Remos",
|
||||
"backgroundRowboatText": "Bote de remos",
|
||||
"backgroundRowboatNotes": "Canta en un bote de remos.",
|
||||
"backgroundPirateFlagText": "Bandera Pirata",
|
||||
"backgroundPirateFlagText": "Bandera pirata",
|
||||
"backgroundPirateFlagNotes": "Cuelga una temible bandera pirata.",
|
||||
"backgrounds072018": "50.ª serie: publicada en julio de 2018",
|
||||
"backgroundDarkDeepText": "Oscuras Profundidades",
|
||||
"backgroundDarkDeepText": "Oscuras profundidades",
|
||||
"backgroundDarkDeepNotes": "Nada en las oscuras profundidades entre bichos bioluminiscentes.",
|
||||
"backgroundDilatoryCityText": "Ciudad de Dilatoria",
|
||||
"backgroundDilatoryCityNotes": "Deambula a través de la ciudad submarina de Dilatoria.",
|
||||
"backgroundTidePoolText": "Poza de Marea",
|
||||
"backgroundTidePoolText": "Poza de marea",
|
||||
"backgroundTidePoolNotes": "Observa la vida del océano cerca de una poza de marea.",
|
||||
"backgrounds082018": "51.ª serie: publicada en agosto de 2018",
|
||||
"backgroundTrainingGroundsText": "Campos de Entrenamiento",
|
||||
"backgroundTrainingGroundsText": "Campos de entrenamiento",
|
||||
"backgroundTrainingGroundsNotes": "Practica en los campos de entrenamiento.",
|
||||
"backgroundFlyingOverRockyCanyonText": "Cañón Rocoso",
|
||||
"backgroundFlyingOverRockyCanyonNotes": "Contempla un escenario que quita el aliento mientras vuelas sobre un Cañón rocoso.",
|
||||
@@ -375,9 +375,9 @@
|
||||
"backgroundBridgeNotes": "Cruza un Puente encantador.",
|
||||
"backgrounds092018": "52.ª serie: publicada en septiembre de 2018",
|
||||
"backgroundApplePickingText": "Colecta de Manzanas",
|
||||
"backgroundApplePickingNotes": "Ve a recolectar manzanas y trae a casa unas cuantas.",
|
||||
"backgroundApplePickingNotes": "Ve a Recolectar Manzanas y trae a casa unas cuantas.",
|
||||
"backgroundGiantBookText": "Libro Gigante",
|
||||
"backgroundGiantBookNotes": "Lee mientras paseas por las páginas de un libro gigante.",
|
||||
"backgroundGiantBookNotes": "Lee mientras paseas por las páginas de un Libro Gigante.",
|
||||
"backgroundCozyBarnText": "Granero Confortable",
|
||||
"backgroundCozyBarnNotes": "Relájate con tus mascotas y monturas en su Confortable Granero.",
|
||||
"backgrounds102018": "53.ª serie: publicada en octubre de 2018",
|
||||
@@ -930,16 +930,5 @@
|
||||
"backgroundWinterDesertWithSaguarosNotes": "Exalta tus sentidos entre los Cactus en Desierto Invernal.",
|
||||
"backgrounds022026": "CONJUNTO 141: Publicado en Febrero 2026",
|
||||
"backgroundElegantPalaceText": "Palacio Elegante",
|
||||
"backgroundElegantPalaceNotes": "Quédate obnubilado por las coloridas salas del Palacio Elegante.",
|
||||
"backgrounds032026": "142.ª serie: publicada en marzo de 2026",
|
||||
"backgroundWaterfallWithRainbowText": "Cascada y Arcoíris",
|
||||
"backgroundWaterfallWithRainbowNotes": "Admira la belleza cautivadora de una cascada con un arcoíris.",
|
||||
"backgrounds042026": "143.ª serie: publicada en abril de 2026",
|
||||
"backgroundRidingACometText": "Montando un Cometa",
|
||||
"backgroundRidingACometNotes": "¡Viaja a través del espacio mientras montas un cometa!",
|
||||
"backgrounds052026": "144.ª serie: publicada en mayo de 2026",
|
||||
"backgroundElvenCitadelText": "Ciudadela Élfica",
|
||||
"backgroundElvenCitadelNotes": "Toma un pintoresto recorrido en una ciudadela élfica.",
|
||||
"backgroundOnAStrangePlanetText": "En un Extraño Planeta",
|
||||
"backgroundOnAStrangePlanetNotes": "Aventúrate allá donde ningun Habiticano ha viajado antes: hacia un extraño planeta."
|
||||
"backgroundElegantPalaceNotes": "Quédate obnubilado por las coloridas salas del Palacio Elegante."
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user