Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e173b7784c | |||
| c3db59aae8 | |||
| 44e063c035 | |||
| 4e2c08cfed | |||
| a9340ee60f | |||
| c8d874d28a | |||
| 32a22f1545 | |||
| 1545685a5b | |||
| 410355c3f1 | |||
| ac27cabf6a | |||
| 972631e7ac | |||
| d27ed7c406 | |||
| 031783b1d7 | |||
| 318aa7cbd9 | |||
| f802a41f75 | |||
| 1d597039ca | |||
| 0002148326 | |||
| d198e23de6 | |||
| 4f4e141806 | |||
| 05e8d6f032 | |||
| 39847893d2 | |||
| eb99b709e0 | |||
| 862b3453f8 | |||
| 7f48853d32 | |||
| 5c4f763bb1 | |||
| bc9401b2f7 | |||
| 6fb9030b96 | |||
| ba307af963 | |||
| cf4b920a67 | |||
| b0ff35a8f1 | |||
| 85b4c7825e | |||
| 5b7ea8ec5c | |||
| 5cfd0c863e | |||
| 10c6244c0c | |||
| 20e65be8bf | |||
| 8bac324ba7 | |||
| 2ee0288aaa | |||
| b7ef4c50b2 | |||
| 52be9c750f | |||
| b0200026aa | |||
| e6c8b977c8 | |||
| c78b5ecf7c | |||
| f27e9b02d8 | |||
| c06c19ca41 | |||
| d5d894b8a9 | |||
| 7bd4e6a5a9 | |||
| f13eed5663 | |||
| a9a2fe6314 | |||
| d6514bce8b | |||
| 603fc8c4dd | |||
| 3c602351f9 | |||
| 6aa204c3f5 | |||
| eaaa5ad7f3 | |||
| 54468ff499 | |||
| 53405aa586 | |||
| 7630c02e13 | |||
| ec444384f4 | |||
| cce9b33844 | |||
| b977d42402 | |||
| 2672cbd790 | |||
| b7ca5be6ee | |||
| 5ae89761b0 | |||
| 696121fb24 |
@@ -1,14 +1,14 @@
|
||||
import monk from 'monk';
|
||||
import nconf from 'nconf';
|
||||
|
||||
const migrationName = 'mystery-items-201807.js'; // Update per month
|
||||
const migrationName = 'mystery-items-201808.js'; // Update per month
|
||||
const authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
|
||||
|
||||
/*
|
||||
* Award this month's mystery items to subscribers
|
||||
*/
|
||||
const MYSTERY_ITEMS = ['armor_mystery_201807', 'head_mystery_201807'];
|
||||
const MYSTERY_ITEMS = ['armor_mystery_201808', 'head_mystery_201808'];
|
||||
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
|
||||
|
||||
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
let migrationName = '20180801_takeThis.js'; // Update per month
|
||||
let migrationName = '20180904_takeThis.js'; // Update per month
|
||||
let authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
|
||||
|
||||
@@ -15,7 +15,7 @@ function processUsers (lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
let query = {
|
||||
migration: {$ne: migrationName},
|
||||
challenges: {$in: ['081f8912-3526-47d5-984f-f71bbeec77fc']}, // Update per month
|
||||
challenges: {$in: ['1044ec0c-4a85-48c5-9f36-d51c0c62c7d3']}, // Update per month
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.57.1",
|
||||
"version": "4.60.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@slack/client": "^3.8.1",
|
||||
@@ -9,9 +9,9 @@
|
||||
"amazon-payments": "^0.2.7",
|
||||
"amplitude": "^3.5.0",
|
||||
"apidoc": "^0.17.5",
|
||||
"apn": "^2.2.0",
|
||||
"autoprefixer": "^8.5.0",
|
||||
"aws-sdk": "^2.239.1",
|
||||
"apn": "^2.2.0",
|
||||
"axios": "^0.18.0",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-core": "^6.26.3",
|
||||
@@ -83,6 +83,7 @@
|
||||
"rimraf": "^2.4.3",
|
||||
"sass-loader": "^7.0.0",
|
||||
"shelljs": "^0.8.2",
|
||||
"smartbanner.js": "^1.9.1",
|
||||
"stripe": "^5.9.0",
|
||||
"superagent": "^3.8.3",
|
||||
"svg-inline-loader": "^0.8.0",
|
||||
|
||||
@@ -65,6 +65,12 @@ describe('cron', () => {
|
||||
expect(analytics.track.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('calls analytics when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(analytics.track.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
describe('end of the month perks', () => {
|
||||
beforeEach(() => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
@@ -655,76 +661,6 @@ describe('cron', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('user is sleeping', () => {
|
||||
beforeEach(() => {
|
||||
user.preferences.sleep = true;
|
||||
});
|
||||
|
||||
it('calls analytics', () => {
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(analytics.track.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('clears user buffs', () => {
|
||||
user.stats.buffs = {
|
||||
str: 1,
|
||||
int: 1,
|
||||
per: 1,
|
||||
con: 1,
|
||||
stealth: 1,
|
||||
streaks: true,
|
||||
};
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
expect(user.stats.buffs.int).to.equal(0);
|
||||
expect(user.stats.buffs.per).to.equal(0);
|
||||
expect(user.stats.buffs.con).to.equal(0);
|
||||
expect(user.stats.buffs.stealth).to.equal(0);
|
||||
expect(user.stats.buffs.streaks).to.be.false;
|
||||
});
|
||||
|
||||
it('resets all dailies without damaging user', () => {
|
||||
let daily = {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: new Date(),
|
||||
};
|
||||
|
||||
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
|
||||
tasksByType.dailys.push(task);
|
||||
tasksByType.dailys[0].completed = true;
|
||||
|
||||
let healthBefore = user.stats.hp;
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||
expect(user.stats.hp).to.equal(healthBefore);
|
||||
});
|
||||
|
||||
it('sets isDue for daily', () => {
|
||||
let daily = {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
frequency: 'daily',
|
||||
everyX: 5,
|
||||
startDate: new Date(),
|
||||
};
|
||||
|
||||
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
|
||||
tasksByType.dailys.push(task);
|
||||
tasksByType.dailys[0].completed = true;
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(tasksByType.dailys[0].isDue).to.be.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe('todos', () => {
|
||||
beforeEach(() => {
|
||||
let todo = {
|
||||
@@ -846,6 +782,15 @@ describe('cron', () => {
|
||||
expect(tasksByType.dailys[0].isDue).to.be.false;
|
||||
});
|
||||
|
||||
it('computes isDue when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
tasksByType.dailys[0].frequency = 'daily';
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().toDate();
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(tasksByType.dailys[0].isDue).to.exist;
|
||||
});
|
||||
|
||||
it('computes nextDue', () => {
|
||||
tasksByType.dailys[0].frequency = 'daily';
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
@@ -865,6 +810,13 @@ describe('cron', () => {
|
||||
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||
});
|
||||
|
||||
it('should set tasks completed to false when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||
});
|
||||
|
||||
it('should reset task checklist for completed dailys', () => {
|
||||
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
|
||||
tasksByType.dailys[0].completed = true;
|
||||
@@ -872,6 +824,14 @@ describe('cron', () => {
|
||||
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
||||
});
|
||||
|
||||
it('should reset task checklist for completed dailys when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
|
||||
tasksByType.dailys[0].completed = true;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
||||
});
|
||||
|
||||
it('should reset task checklist for dailys with scheduled misses', () => {
|
||||
daysMissed = 10;
|
||||
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
|
||||
@@ -884,12 +844,19 @@ describe('cron', () => {
|
||||
daysMissed = 1;
|
||||
let hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.stats.hp).to.be.lessThan(hpBefore);
|
||||
});
|
||||
|
||||
it('should not do damage for missing a daily when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
daysMissed = 1;
|
||||
let hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.stats.hp).to.equal(hpBefore);
|
||||
});
|
||||
|
||||
it('should not do damage for missing a daily when CRON_SAFE_MODE is set', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
|
||||
let cronOverride = requireAgain(pathToCronLib).cron;
|
||||
@@ -930,7 +897,7 @@ describe('cron', () => {
|
||||
expect(hpDifferenceOfPartiallyIncompleteDaily).to.be.lessThan(hpDifferenceOfFullyIncompleteDaily);
|
||||
});
|
||||
|
||||
it('should decrement quest progress down for missing a daily', () => {
|
||||
it('should decrement quest.progress.down for missing a daily', () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
|
||||
@@ -939,6 +906,16 @@ describe('cron', () => {
|
||||
expect(progress.down).to.equal(-1);
|
||||
});
|
||||
|
||||
it('should not decrement quest.progress.down for missing a daily when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
|
||||
let progress = cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(progress.down).to.equal(0);
|
||||
});
|
||||
|
||||
it('should do damage for only yesterday\'s dailies', () => {
|
||||
daysMissed = 3;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
@@ -1017,7 +994,7 @@ describe('cron', () => {
|
||||
expect(tasksByType.habits[0].counterDown).to.equal(0);
|
||||
});
|
||||
|
||||
it('should reset habit counters even if user is resting in the Inn', () => {
|
||||
it('should reset habit counters even if user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
tasksByType.habits[0].counterUp = 1;
|
||||
tasksByType.habits[0].counterDown = 1;
|
||||
@@ -1278,7 +1255,23 @@ describe('cron', () => {
|
||||
expect(user.achievements.perfect).to.equal(0);
|
||||
});
|
||||
|
||||
it('increments user buffs if all (at least 1) due dailies were completed', () => {
|
||||
it('gives perfect day buff if all (at least 1) due dailies were completed', () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
|
||||
let previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
|
||||
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
|
||||
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
|
||||
});
|
||||
|
||||
it('gives perfect day buff if all (at least 1) due dailies were completed when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
@@ -1317,6 +1310,31 @@ describe('cron', () => {
|
||||
expect(user.stats.buffs.streaks).to.be.false;
|
||||
});
|
||||
|
||||
it('clears buffs if user does not have a perfect day (no due dailys) when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).add({days: 1});
|
||||
|
||||
user.stats.buffs = {
|
||||
str: 1,
|
||||
int: 1,
|
||||
per: 1,
|
||||
con: 1,
|
||||
stealth: 0,
|
||||
streaks: true,
|
||||
};
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
expect(user.stats.buffs.int).to.equal(0);
|
||||
expect(user.stats.buffs.per).to.equal(0);
|
||||
expect(user.stats.buffs.con).to.equal(0);
|
||||
expect(user.stats.buffs.stealth).to.equal(0);
|
||||
expect(user.stats.buffs.streaks).to.be.false;
|
||||
});
|
||||
|
||||
it('clears buffs if user does not have a perfect day (at least one due daily not completed)', () => {
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = false;
|
||||
@@ -1341,7 +1359,50 @@ describe('cron', () => {
|
||||
expect(user.stats.buffs.streaks).to.be.false;
|
||||
});
|
||||
|
||||
it('still grants a perfect day when CRON_SAFE_MODE is set', () => {
|
||||
it('clears buffs if user does not have a perfect day (at least one due daily not completed) when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = false;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
|
||||
user.stats.buffs = {
|
||||
str: 1,
|
||||
int: 1,
|
||||
per: 1,
|
||||
con: 1,
|
||||
stealth: 0,
|
||||
streaks: true,
|
||||
};
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
expect(user.stats.buffs.int).to.equal(0);
|
||||
expect(user.stats.buffs.per).to.equal(0);
|
||||
expect(user.stats.buffs.con).to.equal(0);
|
||||
expect(user.stats.buffs.stealth).to.equal(0);
|
||||
expect(user.stats.buffs.streaks).to.be.false;
|
||||
});
|
||||
|
||||
it('always grants a perfect day buff when CRON_SAFE_MODE is set', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
|
||||
let cronOverride = requireAgain(pathToCronLib).cron;
|
||||
daysMissed = 1;
|
||||
tasksByType.dailys[0].completed = false;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||
|
||||
let previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
cronOverride({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
|
||||
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
|
||||
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
|
||||
});
|
||||
|
||||
it('always grants a perfect day buff when CRON_SAFE_MODE is set when user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
|
||||
let cronOverride = requireAgain(pathToCronLib).cron;
|
||||
daysMissed = 1;
|
||||
@@ -1373,6 +1434,20 @@ describe('cron', () => {
|
||||
common.statsComputed.restore();
|
||||
});
|
||||
|
||||
it('should not add mp to user when user is sleeping', () => {
|
||||
const statsComputedRes = common.statsComputed(user);
|
||||
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
||||
|
||||
user.preferences.sleep = true;
|
||||
let mpBefore = user.stats.mp;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, {maxMP: 100}));
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.stats.mp).to.equal(mpBefore);
|
||||
|
||||
common.statsComputed.restore();
|
||||
});
|
||||
|
||||
it('set user\'s mp to statsComputed.maxMP when user.stats.mp is greater', () => {
|
||||
const statsComputedRes = common.statsComputed(user);
|
||||
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
||||
@@ -1568,7 +1643,7 @@ describe('cron', () => {
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
});
|
||||
|
||||
it('increments loginIncentives by 1 even if user has Dailies paused', () => {
|
||||
it('increments loginIncentives by 1 even if user is sleeping', () => {
|
||||
user.preferences.sleep = true;
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
|
||||
@@ -20,7 +20,7 @@ import { TAVERN_ID } from '../../../../website/common/script/';
|
||||
import shared from '../../../../website/common';
|
||||
|
||||
describe('Group Model', () => {
|
||||
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
|
||||
let party, questLeader, participatingMember, sleepingParticipatingMember, nonParticipatingMember, undecidedMember;
|
||||
|
||||
beforeEach(async () => {
|
||||
sandbox.stub(email, 'sendTxn');
|
||||
@@ -48,6 +48,11 @@ describe('Group Model', () => {
|
||||
party: { _id: party._id },
|
||||
profile: { name: 'Participating Member' },
|
||||
});
|
||||
sleepingParticipatingMember = new User({
|
||||
party: { _id: party._id },
|
||||
profile: { name: 'Sleeping Participating Member' },
|
||||
preferences: { sleep: true },
|
||||
});
|
||||
nonParticipatingMember = new User({
|
||||
party: { _id: party._id },
|
||||
profile: { name: 'Non-Participating Member' },
|
||||
@@ -61,6 +66,7 @@ describe('Group Model', () => {
|
||||
party.save(),
|
||||
questLeader.save(),
|
||||
participatingMember.save(),
|
||||
sleepingParticipatingMember.save(),
|
||||
nonParticipatingMember.save(),
|
||||
undecidedMember.save(),
|
||||
]);
|
||||
@@ -80,6 +86,7 @@ describe('Group Model', () => {
|
||||
party.quest.members = {
|
||||
[questLeader._id]: true,
|
||||
[participatingMember._id]: true,
|
||||
[sleepingParticipatingMember._id]: true,
|
||||
[nonParticipatingMember._id]: false,
|
||||
[undecidedMember._id]: null,
|
||||
};
|
||||
@@ -175,6 +182,34 @@ describe('Group Model', () => {
|
||||
expect(party._processBossQuest).to.not.be.called;
|
||||
expect(Group.prototype._processCollectionQuest).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('does not call _processBossQuest when user is resting in the inn', async () => {
|
||||
party.quest.key = 'whale';
|
||||
|
||||
await party.startQuest(questLeader);
|
||||
await party.save();
|
||||
|
||||
await Group.processQuestProgress(sleepingParticipatingMember, progress);
|
||||
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(party._processBossQuest).to.not.be.called;
|
||||
expect(party._processCollectionQuest).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not call _processCollectionQuest when user is resting in the inn', async () => {
|
||||
party.quest.key = 'evilsanta2';
|
||||
|
||||
await party.startQuest(questLeader);
|
||||
await party.save();
|
||||
|
||||
await Group.processQuestProgress(sleepingParticipatingMember, progress);
|
||||
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(party._processBossQuest).to.not.be.called;
|
||||
expect(party._processCollectionQuest).to.not.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
context('Boss Quests', () => {
|
||||
@@ -216,17 +251,20 @@ describe('Group Model', () => {
|
||||
let [
|
||||
updatedLeader,
|
||||
updatedParticipatingMember,
|
||||
updatedSleepingParticipatingMember,
|
||||
updatedNonParticipatingMember,
|
||||
updatedUndecidedMember,
|
||||
] = await Promise.all([
|
||||
User.findById(questLeader._id),
|
||||
User.findById(participatingMember._id),
|
||||
User.findById(sleepingParticipatingMember._id),
|
||||
User.findById(nonParticipatingMember._id),
|
||||
User.findById(undecidedMember._id),
|
||||
]);
|
||||
|
||||
expect(updatedLeader.stats.hp).to.eql(42.5);
|
||||
expect(updatedParticipatingMember.stats.hp).to.eql(42.5);
|
||||
expect(updatedSleepingParticipatingMember.stats.hp).to.eql(42.5);
|
||||
expect(updatedNonParticipatingMember.stats.hp).to.eql(50);
|
||||
expect(updatedUndecidedMember.stats.hp).to.eql(50);
|
||||
});
|
||||
@@ -236,6 +274,7 @@ describe('Group Model', () => {
|
||||
party.quest.members = {
|
||||
[questLeader._id]: true,
|
||||
[participatingMember._id]: true,
|
||||
[sleepingParticipatingMember._id]: true,
|
||||
[nonParticipatingMember._id]: false,
|
||||
[undecidedMember._id]: null,
|
||||
};
|
||||
@@ -248,17 +287,20 @@ describe('Group Model', () => {
|
||||
let [
|
||||
updatedLeader,
|
||||
updatedParticipatingMember,
|
||||
updatedSleepingParticipatingMember,
|
||||
updatedNonParticipatingMember,
|
||||
updatedUndecidedMember,
|
||||
] = await Promise.all([
|
||||
User.findById(questLeader._id),
|
||||
User.findById(participatingMember._id),
|
||||
User.findById(sleepingParticipatingMember._id),
|
||||
User.findById(nonParticipatingMember._id),
|
||||
User.findById(undecidedMember._id),
|
||||
]);
|
||||
|
||||
expect(updatedLeader.stats.hp).to.eql(42.5);
|
||||
expect(updatedParticipatingMember.stats.hp).to.eql(42.5);
|
||||
expect(updatedSleepingParticipatingMember.stats.hp).to.eql(42.5);
|
||||
expect(updatedNonParticipatingMember.stats.hp).to.eql(50);
|
||||
expect(updatedUndecidedMember.stats.hp).to.eql(50);
|
||||
});
|
||||
@@ -497,9 +539,11 @@ describe('Group Model', () => {
|
||||
let [
|
||||
updatedLeader,
|
||||
updatedParticipatingMember,
|
||||
updatedSleepingParticipatingMember,
|
||||
] = await Promise.all([
|
||||
User.findById(questLeader._id),
|
||||
User.findById(participatingMember._id),
|
||||
User.findById(sleepingParticipatingMember._id),
|
||||
]);
|
||||
|
||||
expect(updatedLeader.achievements.quests[party.quest.key]).to.eql(1);
|
||||
@@ -508,6 +552,9 @@ describe('Group Model', () => {
|
||||
expect(updatedParticipatingMember.achievements.quests[party.quest.key]).to.eql(1);
|
||||
expect(updatedParticipatingMember.stats.exp).to.be.greaterThan(0);
|
||||
expect(updatedParticipatingMember.stats.gp).to.be.greaterThan(0);
|
||||
expect(updatedSleepingParticipatingMember.achievements.quests[party.quest.key]).to.eql(1);
|
||||
expect(updatedSleepingParticipatingMember.stats.exp).to.be.greaterThan(0);
|
||||
expect(updatedSleepingParticipatingMember.stats.gp).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -647,6 +694,7 @@ describe('Group Model', () => {
|
||||
it('returns an array of members whose quest status set to true', () => {
|
||||
party.quest.members = {
|
||||
[participatingMember._id]: true,
|
||||
[sleepingParticipatingMember._id]: true,
|
||||
[questLeader._id]: true,
|
||||
[nonParticipatingMember._id]: false,
|
||||
[undecidedMember._id]: null,
|
||||
@@ -654,6 +702,7 @@ describe('Group Model', () => {
|
||||
|
||||
expect(party.getParticipatingQuestMembers()).to.eql([
|
||||
participatingMember._id,
|
||||
sleepingParticipatingMember._id,
|
||||
questLeader._id,
|
||||
]);
|
||||
});
|
||||
@@ -756,11 +805,12 @@ describe('Group Model', () => {
|
||||
it('removes user from group quest', async () => {
|
||||
party.quest.members = {
|
||||
[participatingMember._id]: true,
|
||||
[sleepingParticipatingMember._id]: true,
|
||||
[questLeader._id]: true,
|
||||
[nonParticipatingMember._id]: false,
|
||||
[undecidedMember._id]: null,
|
||||
};
|
||||
party.memberCount = 4;
|
||||
party.memberCount = 5;
|
||||
await party.save();
|
||||
|
||||
await party.leave(participatingMember);
|
||||
@@ -768,6 +818,7 @@ describe('Group Model', () => {
|
||||
party = await Group.findOne({_id: party._id});
|
||||
expect(party.quest.members).to.eql({
|
||||
[questLeader._id]: true,
|
||||
[sleepingParticipatingMember._id]: true,
|
||||
[nonParticipatingMember._id]: false,
|
||||
[undecidedMember._id]: null,
|
||||
});
|
||||
@@ -775,6 +826,7 @@ describe('Group Model', () => {
|
||||
|
||||
it('deletes a private party when the last member leaves', async () => {
|
||||
await party.leave(participatingMember);
|
||||
await party.leave(sleepingParticipatingMember);
|
||||
await party.leave(questLeader);
|
||||
await party.leave(nonParticipatingMember);
|
||||
await party.leave(undecidedMember);
|
||||
@@ -846,6 +898,7 @@ describe('Group Model', () => {
|
||||
party.privacy = 'public';
|
||||
|
||||
await party.leave(participatingMember);
|
||||
await party.leave(sleepingParticipatingMember);
|
||||
await party.leave(questLeader);
|
||||
await party.leave(nonParticipatingMember);
|
||||
await party.leave(undecidedMember);
|
||||
@@ -1074,6 +1127,7 @@ describe('Group Model', () => {
|
||||
party.quest.members = {
|
||||
[questLeader._id]: true,
|
||||
[participatingMember._id]: true,
|
||||
[sleepingParticipatingMember._id]: true,
|
||||
[nonParticipatingMember._id]: false,
|
||||
[undecidedMember._id]: null,
|
||||
};
|
||||
@@ -1130,6 +1184,7 @@ describe('Group Model', () => {
|
||||
let expectedQuestMembers = {};
|
||||
expectedQuestMembers[questLeader._id] = true;
|
||||
expectedQuestMembers[participatingMember._id] = true;
|
||||
expectedQuestMembers[sleepingParticipatingMember._id] = true;
|
||||
|
||||
expect(party.quest.members).to.eql(expectedQuestMembers);
|
||||
});
|
||||
@@ -1148,12 +1203,18 @@ describe('Group Model', () => {
|
||||
|
||||
questLeader = await User.findById(questLeader._id);
|
||||
participatingMember = await User.findById(participatingMember._id);
|
||||
sleepingParticipatingMember = await User.findById(sleepingParticipatingMember._id);
|
||||
|
||||
expect(participatingMember.party.quest.key).to.eql('whale');
|
||||
expect(participatingMember.party.quest.progress.down).to.eql(0);
|
||||
expect(participatingMember.party.quest.progress.collectedItems).to.eql(0);
|
||||
expect(participatingMember.party.quest.completed).to.eql(null);
|
||||
|
||||
expect(sleepingParticipatingMember.party.quest.key).to.eql('whale');
|
||||
expect(sleepingParticipatingMember.party.quest.progress.down).to.eql(0);
|
||||
expect(sleepingParticipatingMember.party.quest.progress.collectedItems).to.eql(0);
|
||||
expect(sleepingParticipatingMember.party.quest.completed).to.eql(null);
|
||||
|
||||
expect(questLeader.party.quest.key).to.eql('whale');
|
||||
expect(questLeader.party.quest.progress.down).to.eql(0);
|
||||
expect(questLeader.party.quest.progress.collectedItems).to.eql(0);
|
||||
@@ -1172,9 +1233,11 @@ describe('Group Model', () => {
|
||||
|
||||
it('sends email to participating members that quest has started', async () => {
|
||||
participatingMember.preferences.emailNotifications.questStarted = true;
|
||||
sleepingParticipatingMember.preferences.emailNotifications.questStarted = true;
|
||||
questLeader.preferences.emailNotifications.questStarted = true;
|
||||
await Promise.all([
|
||||
participatingMember.save(),
|
||||
sleepingParticipatingMember.save(),
|
||||
questLeader.save(),
|
||||
]);
|
||||
|
||||
@@ -1187,8 +1250,9 @@ describe('Group Model', () => {
|
||||
let memberIds = _.map(email.sendTxn.args[0][0], '_id');
|
||||
let typeOfEmail = email.sendTxn.args[0][1];
|
||||
|
||||
expect(memberIds).to.have.a.lengthOf(2);
|
||||
expect(memberIds).to.have.a.lengthOf(3);
|
||||
expect(memberIds).to.include(participatingMember._id);
|
||||
expect(memberIds).to.include(sleepingParticipatingMember._id);
|
||||
expect(memberIds).to.include(questLeader._id);
|
||||
expect(typeOfEmail).to.eql('quest-started');
|
||||
});
|
||||
@@ -1202,6 +1266,13 @@ describe('Group Model', () => {
|
||||
questStarted: true,
|
||||
},
|
||||
}];
|
||||
sleepingParticipatingMember.webhooks = [{
|
||||
type: 'questActivity',
|
||||
url: 'http://someurl.com',
|
||||
options: {
|
||||
questStarted: true,
|
||||
},
|
||||
}];
|
||||
questLeader.webhooks = [{
|
||||
type: 'questActivity',
|
||||
url: 'http://someurl.com',
|
||||
@@ -1210,13 +1281,13 @@ describe('Group Model', () => {
|
||||
},
|
||||
}];
|
||||
|
||||
await Promise.all([participatingMember.save(), questLeader.save()]);
|
||||
await Promise.all([participatingMember.save(), sleepingParticipatingMember.save(), questLeader.save()]);
|
||||
|
||||
await party.startQuest(nonParticipatingMember);
|
||||
|
||||
await sleep(0.5);
|
||||
|
||||
expect(questActivityWebhook.send).to.be.calledTwice; // for 2 participating members
|
||||
expect(questActivityWebhook.send).to.be.calledThrice; // for 3 participating members
|
||||
|
||||
let args = questActivityWebhook.send.args[0];
|
||||
let webhooks = args[0].webhooks;
|
||||
@@ -1226,6 +1297,8 @@ describe('Group Model', () => {
|
||||
expect(webhooks).to.have.a.lengthOf(1);
|
||||
if (webhookOwner === questLeader._id) {
|
||||
expect(webhooks[0].id).to.eql(questLeader.webhooks[0].id);
|
||||
} else if (webhookOwner === sleepingParticipatingMember._id) {
|
||||
expect(webhooks[0].id).to.eql(sleepingParticipatingMember.webhooks[0].id);
|
||||
} else {
|
||||
expect(webhooks[0].id).to.eql(participatingMember.webhooks[0].id);
|
||||
}
|
||||
@@ -1236,9 +1309,11 @@ describe('Group Model', () => {
|
||||
|
||||
it('sends email only to members who have not opted out', async () => {
|
||||
participatingMember.preferences.emailNotifications.questStarted = false;
|
||||
sleepingParticipatingMember.preferences.emailNotifications.questStarted = false;
|
||||
questLeader.preferences.emailNotifications.questStarted = true;
|
||||
await Promise.all([
|
||||
participatingMember.save(),
|
||||
sleepingParticipatingMember.save(),
|
||||
questLeader.save(),
|
||||
]);
|
||||
|
||||
@@ -1252,14 +1327,17 @@ describe('Group Model', () => {
|
||||
|
||||
expect(memberIds).to.have.a.lengthOf(1);
|
||||
expect(memberIds).to.not.include(participatingMember._id);
|
||||
expect(memberIds).to.not.include(sleepingParticipatingMember._id);
|
||||
expect(memberIds).to.include(questLeader._id);
|
||||
});
|
||||
|
||||
it('does not send email to initiating member', async () => {
|
||||
participatingMember.preferences.emailNotifications.questStarted = true;
|
||||
sleepingParticipatingMember.preferences.emailNotifications.questStarted = true;
|
||||
questLeader.preferences.emailNotifications.questStarted = true;
|
||||
await Promise.all([
|
||||
participatingMember.save(),
|
||||
sleepingParticipatingMember.save(),
|
||||
questLeader.save(),
|
||||
]);
|
||||
|
||||
@@ -1271,8 +1349,9 @@ describe('Group Model', () => {
|
||||
|
||||
let memberIds = _.map(email.sendTxn.args[0][0], '_id');
|
||||
|
||||
expect(memberIds).to.have.a.lengthOf(1);
|
||||
expect(memberIds).to.have.a.lengthOf(2);
|
||||
expect(memberIds).to.not.include(participatingMember._id);
|
||||
expect(memberIds).to.include(sleepingParticipatingMember._id);
|
||||
expect(memberIds).to.include(questLeader._id);
|
||||
});
|
||||
|
||||
@@ -1281,7 +1360,7 @@ describe('Group Model', () => {
|
||||
|
||||
await party.startQuest(nonParticipatingMember);
|
||||
|
||||
let members = [questLeader._id, participatingMember._id];
|
||||
let members = [questLeader._id, participatingMember._id, sleepingParticipatingMember._id];
|
||||
|
||||
expect(User.update).to.be.calledWith(
|
||||
{ _id: { $in: members } },
|
||||
@@ -1346,6 +1425,7 @@ describe('Group Model', () => {
|
||||
party.quest.members = {
|
||||
[questLeader._id]: true,
|
||||
[participatingMember._id]: true,
|
||||
[sleepingParticipatingMember._id]: true,
|
||||
[nonParticipatingMember._id]: false,
|
||||
[undecidedMember._id]: null,
|
||||
};
|
||||
@@ -1368,7 +1448,7 @@ describe('Group Model', () => {
|
||||
|
||||
await party.finishQuest(quest);
|
||||
|
||||
expect(User.update).to.be.calledTwice;
|
||||
expect(User.update).to.be.calledThrice;
|
||||
});
|
||||
|
||||
it('stops retrying when a successful update has occurred', async () => {
|
||||
@@ -1378,7 +1458,7 @@ describe('Group Model', () => {
|
||||
|
||||
await party.finishQuest(quest);
|
||||
|
||||
expect(User.update).to.be.calledThrice;
|
||||
expect(User.update.callCount).to.equal(4);
|
||||
});
|
||||
|
||||
it('retries failed updates at most five times per user', async () => {
|
||||
@@ -1386,7 +1466,7 @@ describe('Group Model', () => {
|
||||
|
||||
await expect(party.finishQuest(quest)).to.eventually.be.rejected;
|
||||
|
||||
expect(User.update.callCount).to.eql(10);
|
||||
expect(User.update.callCount).to.eql(15); // for 3 users
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1396,17 +1476,19 @@ describe('Group Model', () => {
|
||||
let [
|
||||
updatedLeader,
|
||||
updatedParticipatingMember,
|
||||
updatedSleepingParticipatingMember,
|
||||
] = await Promise.all([
|
||||
User.findById(questLeader._id),
|
||||
User.findById(participatingMember._id),
|
||||
User.findById(sleepingParticipatingMember._id),
|
||||
]);
|
||||
|
||||
expect(updatedLeader.achievements.quests[quest.key]).to.eql(1);
|
||||
expect(updatedParticipatingMember.achievements.quests[quest.key]).to.eql(1);
|
||||
expect(updatedSleepingParticipatingMember.achievements.quests[quest.key]).to.eql(1);
|
||||
});
|
||||
|
||||
// Disable test, it fails on TravisCI, but only there
|
||||
xit('gives out super awesome Masterclasser achievement to the deserving', async () => {
|
||||
it('gives out super awesome Masterclasser achievement to the deserving', async () => {
|
||||
quest = questScrolls.lostMasterclasser4;
|
||||
party.quest.key = quest.key;
|
||||
|
||||
@@ -1433,17 +1515,19 @@ describe('Group Model', () => {
|
||||
let [
|
||||
updatedLeader,
|
||||
updatedParticipatingMember,
|
||||
updatedSleepingParticipatingMember,
|
||||
] = await Promise.all([
|
||||
User.findById(questLeader._id).exec(),
|
||||
User.findById(participatingMember._id).exec(),
|
||||
User.findById(sleepingParticipatingMember._id).exec(),
|
||||
]);
|
||||
|
||||
expect(updatedLeader.achievements.lostMasterclasser).to.eql(true);
|
||||
expect(updatedParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
|
||||
expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
|
||||
});
|
||||
|
||||
// Disable test, it fails on TravisCI, but only there
|
||||
xit('gives out super awesome Masterclasser achievement when quests done out of order', async () => {
|
||||
it('gives out super awesome Masterclasser achievement when quests done out of order', async () => {
|
||||
quest = questScrolls.lostMasterclasser1;
|
||||
party.quest.key = quest.key;
|
||||
|
||||
@@ -1470,13 +1554,16 @@ describe('Group Model', () => {
|
||||
let [
|
||||
updatedLeader,
|
||||
updatedParticipatingMember,
|
||||
updatedSleepingParticipatingMember,
|
||||
] = await Promise.all([
|
||||
User.findById(questLeader._id).exec(),
|
||||
User.findById(participatingMember._id).exec(),
|
||||
User.findById(sleepingParticipatingMember._id).exec(),
|
||||
]);
|
||||
|
||||
expect(updatedLeader.achievements.lostMasterclasser).to.eql(true);
|
||||
expect(updatedParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
|
||||
expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
|
||||
});
|
||||
|
||||
it('gives xp and gold', async () => {
|
||||
@@ -1485,15 +1572,19 @@ describe('Group Model', () => {
|
||||
let [
|
||||
updatedLeader,
|
||||
updatedParticipatingMember,
|
||||
updatedSleepingParticipatingMember,
|
||||
] = await Promise.all([
|
||||
User.findById(questLeader._id),
|
||||
User.findById(participatingMember._id),
|
||||
User.findById(sleepingParticipatingMember._id),
|
||||
]);
|
||||
|
||||
expect(updatedLeader.stats.exp).to.eql(quest.drop.exp);
|
||||
expect(updatedLeader.stats.gp).to.eql(quest.drop.gp);
|
||||
expect(updatedParticipatingMember.stats.exp).to.eql(quest.drop.exp);
|
||||
expect(updatedParticipatingMember.stats.gp).to.eql(quest.drop.gp);
|
||||
expect(updatedSleepingParticipatingMember.stats.exp).to.eql(quest.drop.exp);
|
||||
expect(updatedSleepingParticipatingMember.stats.gp).to.eql(quest.drop.gp);
|
||||
});
|
||||
|
||||
context('drops', () => {
|
||||
@@ -1593,13 +1684,16 @@ describe('Group Model', () => {
|
||||
sandbox.spy(User, 'update');
|
||||
await party.finishQuest(quest);
|
||||
|
||||
expect(User.update).to.be.calledTwice;
|
||||
expect(User.update).to.be.calledThrice;
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
_id: questLeader._id,
|
||||
});
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
_id: participatingMember._id,
|
||||
});
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
_id: sleepingParticipatingMember._id,
|
||||
});
|
||||
});
|
||||
|
||||
it('sets user quest object to a clean state', async () => {
|
||||
@@ -1632,7 +1726,7 @@ describe('Group Model', () => {
|
||||
},
|
||||
}];
|
||||
|
||||
await Promise.all([participatingMember.save(), questLeader.save()]);
|
||||
await Promise.all([participatingMember.save(), sleepingParticipatingMember.save(), questLeader.save()]);
|
||||
|
||||
await party.finishQuest(quest);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
|
||||
describe('payments - stripe - #subscribeCancel', () => {
|
||||
let endpoint = '/stripe/subscribe/cancel?redirect=none';
|
||||
let endpoint = '/stripe/subscribe/cancel?noRedirect=true';
|
||||
let user, group, stripeCancelSubscriptionStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -58,6 +58,21 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if use Healing Light spell with full health', async () => {
|
||||
await user.update({
|
||||
'stats.class': 'healer',
|
||||
'stats.lvl': 11,
|
||||
'stats.hp': 50,
|
||||
'stats.mp': 200,
|
||||
});
|
||||
await expect(user.post('/user/class/cast/heal'))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('messageHealthAlreadyMax'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error if spell.lvl > user.level', async () => {
|
||||
await user.update({'stats.mp': 200, 'stats.class': 'wizard'});
|
||||
await expect(user.post('/user/class/cast/earth'))
|
||||
|
||||
@@ -50,11 +50,24 @@ describe('POST /user/push-devices', () => {
|
||||
});
|
||||
|
||||
it('adds a push device to the user', async () => {
|
||||
let response = await user.post('/user/push-devices', {type, regId});
|
||||
const response = await user.post('/user/push-devices', {type, regId});
|
||||
await user.sync();
|
||||
|
||||
expect(response.message).to.equal(t('pushDeviceAdded'));
|
||||
expect(response.data[0].type).to.equal(type);
|
||||
expect(response.data[0].regId).to.equal(regId);
|
||||
expect(user.pushDevices[0].type).to.equal(type);
|
||||
expect(user.pushDevices[0].regId).to.equal(regId);
|
||||
});
|
||||
|
||||
it('removes a push device to the user', async () => {
|
||||
await user.post('/user/push-devices', {type, regId});
|
||||
|
||||
const response = await user.del(`/user/push-devices/${regId}`);
|
||||
await user.sync();
|
||||
|
||||
expect(response.message).to.equal(t('pushDeviceRemoved'));
|
||||
expect(response.data[0]).to.not.exist;
|
||||
expect(user.pushDevices[0]).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import spells from '../../../website/common/script/content/spells';
|
||||
import {
|
||||
NotAuthorized,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
|
||||
// TODO complete the test suite...
|
||||
|
||||
describe('shared.ops.spells', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
});
|
||||
|
||||
it('returns an error when healer tries to cast Healing Light with full health', (done) => {
|
||||
user.stats.class = 'healer';
|
||||
user.stats.lvl = 11;
|
||||
user.stats.hp = 50;
|
||||
user.stats.mp = 200;
|
||||
|
||||
let spell = spells.healer.heal;
|
||||
|
||||
try {
|
||||
spell.cast(user);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('messageHealthAlreadyMax'));
|
||||
expect(user.stats.hp).to.eql(50);
|
||||
expect(user.stats.mp).to.eql(200);
|
||||
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -103,7 +103,7 @@ div
|
||||
|
||||
<style lang='scss'>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
|
||||
/* @TODO: The modal-open class is not being removed. Let's try this for now */
|
||||
.modal {
|
||||
overflow-y: scroll !important;
|
||||
@@ -659,3 +659,4 @@ export default {
|
||||
<style src="assets/css/sprites/spritesmith-main-21.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-22.css"></style>
|
||||
<style src="assets/css/sprites.css"></style>
|
||||
<style src="smartbanner.js/dist/smartbanner.min.css"></style>
|
||||
|
||||
@@ -1,42 +1,54 @@
|
||||
.promo_armoire_backgrounds_201808 {
|
||||
.promo_animal_tails {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -817px 0px;
|
||||
background-position: -421px 0px;
|
||||
width: 141px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_armoire_backgrounds_201809 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -563px 0px;
|
||||
width: 141px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_ember_potions {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -705px 0px;
|
||||
width: 141px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_ios {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -441px 0px;
|
||||
background-position: 0px -337px;
|
||||
width: 375px;
|
||||
height: 361px;
|
||||
}
|
||||
.promo_kangaroo {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -327px;
|
||||
background-position: 0px 0px;
|
||||
width: 420px;
|
||||
height: 336px;
|
||||
}
|
||||
.promo_mystery_201807 {
|
||||
.promo_mystery_201808 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -817px -442px;
|
||||
width: 114px;
|
||||
height: 120px;
|
||||
background-position: -847px -462px;
|
||||
width: 78px;
|
||||
height: 81px;
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -817px -563px;
|
||||
background-position: -847px -392px;
|
||||
width: 96px;
|
||||
height: 69px;
|
||||
}
|
||||
.promo_unconventional_armor {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -441px -362px;
|
||||
background-position: -847px -211px;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
.scene_tavern {
|
||||
.scene_perfect_day {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px 0px;
|
||||
width: 440px;
|
||||
height: 326px;
|
||||
background-position: -847px 0px;
|
||||
width: 210px;
|
||||
height: 210px;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 549 KiB After Width: | Height: | Size: 551 KiB |
|
Before Width: | Height: | Size: 449 KiB After Width: | Height: | Size: 463 KiB |
|
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 343 KiB After Width: | Height: | Size: 348 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 117 KiB |
@@ -6,21 +6,21 @@ b-modal#login-incentives(:title="data.message", size='md', :hide-footer="true")
|
||||
.row.reward-row
|
||||
.col-12
|
||||
avatar.avatar(:member='user', :avatarOnly='true', :withBackground='true')
|
||||
.text-center.col-12
|
||||
.text-center.col-12(v-if='nextReward')
|
||||
.reward-wrap(v-if="!data.rewardText")
|
||||
div(v-if="nextReward.rewardKey.length === 1", :class="nextReward.rewardKey[0]")
|
||||
.reward(v-for="reward in nextReward.rewardKey", v-if="nextReward.rewardKey.length > 1", :class='reward')
|
||||
.reward-wrap(v-if="data.rewardText")
|
||||
div(v-if="data.rewardKey.length === 1", :class="data.rewardKey[0]")
|
||||
.reward(v-for="reward in data.rewardKey", v-if="data.rewardKey.length > 1", :class='reward')
|
||||
.col-12.text-center(v-if="data.nextRewardAt")
|
||||
.col-12.text-center(v-if="data && data.nextRewardAt")
|
||||
h4 {{ $t('countLeft', {count: data.nextRewardAt - user.loginIncentives}) }}
|
||||
.row
|
||||
.col-12.text-center(v-if='data.rewardText')
|
||||
p {{ $t('earnedRewardForDevotion', {reward: data.rewardText}) }}
|
||||
.col-12.text-center
|
||||
p {{ $t('incentivesDescription') }}
|
||||
.col-12.text-center(v-if="data.nextRewardAt")
|
||||
.col-12.text-center(v-if="data && data.nextRewardAt")
|
||||
h3 {{ $t('nextRewardUnlocksIn', {numberOfCheckinsLeft: data.nextRewardAt - user.loginIncentives}) }}
|
||||
.modal-footer
|
||||
.col-12.text-center
|
||||
@@ -70,8 +70,9 @@ export default {
|
||||
user: 'user.data',
|
||||
}),
|
||||
nextReward () {
|
||||
let nextRewardKey = this.loginIncentives[this.user.loginIncentives].nextRewardAt;
|
||||
let nextReward = this.loginIncentives[nextRewardKey];
|
||||
if (!this.loginIncentives[this.user.loginIncentives]) return;
|
||||
const nextRewardKey = this.loginIncentives[this.user.loginIncentives].nextRewardAt;
|
||||
const nextReward = this.loginIncentives[nextRewardKey];
|
||||
return nextReward;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -49,6 +49,8 @@
|
||||
|
||||
.social-button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
white-space: inherit;
|
||||
text-align: center;
|
||||
|
||||
.text {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
.col-12.col-md-6
|
||||
.btn.btn-secondary.social-button(@click='socialAuth("google")')
|
||||
.svg-icon.social-icon(v-html="icons.googleIcon")
|
||||
span {{registering ? $t('signUpWithSocial', {social: 'Google'}) : $t('loginWithSocial', {social: 'Google'})}}
|
||||
.text {{registering ? $t('signUpWithSocial', {social: 'Google'}) : $t('loginWithSocial', {social: 'Google'})}}
|
||||
.form-group(v-if='registering')
|
||||
label(for='usernameInput', v-once) {{$t('username')}}
|
||||
input#usernameInput.form-control(type='text', :placeholder='$t("usernamePlaceholder")', v-model='username')
|
||||
@@ -207,6 +207,8 @@
|
||||
|
||||
.social-button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
white-space: inherit;
|
||||
text-align: center;
|
||||
|
||||
.text {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
.row
|
||||
challenge-modal(v-on:updatedChallenge='updatedChallenge')
|
||||
leave-challenge-modal(:challengeId='challenge._id')
|
||||
close-challenge-modal(:members='members', :challengeId='challenge._id')
|
||||
close-challenge-modal(:members='members', :challengeId='challenge._id', :prize='challenge.prize')
|
||||
challenge-member-progress-modal(:challengeId='challenge._id')
|
||||
.col-12.col-md-8.standard-page
|
||||
.row
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
div.category-wrap(@click.prevent="toggleCategorySelect")
|
||||
span.category-select(v-if='workingChallenge.categories.length === 0') {{$t('none')}}
|
||||
.category-label(v-for='category in workingChallenge.categories') {{$t(categoriesHashByKey[category])}}
|
||||
.category-box(v-if="showCategorySelect")
|
||||
.category-box(v-if="showCategorySelect && creating")
|
||||
.form-check(
|
||||
v-for="group in categoryOptions",
|
||||
:key="group.key",
|
||||
|
||||
@@ -74,7 +74,7 @@ div
|
||||
import memberSearchDropdown from 'client/components/members/memberSearchDropdown';
|
||||
|
||||
export default {
|
||||
props: ['challengeId', 'members'],
|
||||
props: ['challengeId', 'members', 'prize'],
|
||||
components: {
|
||||
memberSearchDropdown,
|
||||
},
|
||||
@@ -102,7 +102,10 @@ export default {
|
||||
},
|
||||
async deleteChallenge () {
|
||||
if (!confirm('Are you sure you want to delete this challenge?')) return;
|
||||
this.challenge = await this.$store.dispatch('challenges:deleteChallenge', {challengeId: this.challengeId});
|
||||
this.challenge = await this.$store.dispatch('challenges:deleteChallenge', {
|
||||
challengeId: this.challengeId,
|
||||
prize: this.prize,
|
||||
});
|
||||
this.$router.push('/challenges/myChallenges');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,23 +14,24 @@ div
|
||||
p.time(v-b-tooltip="", :title="msg.timestamp | date") {{msg.timestamp | timeAgo}}
|
||||
.text(v-markdown='msg.text')
|
||||
hr
|
||||
.action(@click='like()', v-if='!inbox && msg.likes', :class='{active: msg.likes[user._id]}')
|
||||
.svg-icon(v-html="icons.like")
|
||||
span(v-if='!msg.likes[user._id]') {{ $t('like') }}
|
||||
span(v-if='msg.likes[user._id]') {{ $t('liked') }}
|
||||
span.action(v-if='!inbox', @click='copyAsTodo(msg)')
|
||||
.svg-icon(v-html="icons.copy")
|
||||
| {{$t('copyAsTodo')}}
|
||||
span.action(v-if='!inbox && user.flags.communityGuidelinesAccepted && msg.uuid !== "system"', @click='report(msg)')
|
||||
.svg-icon(v-html="icons.report")
|
||||
| {{$t('report')}}
|
||||
// @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys
|
||||
span.action(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove()')
|
||||
.svg-icon(v-html="icons.delete")
|
||||
| {{$t('delete')}}
|
||||
span.action.float-right.liked(v-if='likeCount > 0')
|
||||
.svg-icon(v-html="icons.liked")
|
||||
| + {{ likeCount }}
|
||||
div(v-if='msg.id')
|
||||
.action(@click='like()', v-if='!inbox && msg.likes', :class='{active: msg.likes[user._id]}')
|
||||
.svg-icon(v-html="icons.like")
|
||||
span(v-if='!msg.likes[user._id]') {{ $t('like') }}
|
||||
span(v-if='msg.likes[user._id]') {{ $t('liked') }}
|
||||
span.action(v-if='!inbox', @click='copyAsTodo(msg)')
|
||||
.svg-icon(v-html="icons.copy")
|
||||
| {{$t('copyAsTodo')}}
|
||||
span.action(v-if='!inbox && user.flags.communityGuidelinesAccepted && msg.uuid !== "system"', @click='report(msg)')
|
||||
.svg-icon(v-html="icons.report")
|
||||
| {{$t('report')}}
|
||||
// @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys
|
||||
span.action(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove()')
|
||||
.svg-icon(v-html="icons.delete")
|
||||
| {{$t('delete')}}
|
||||
span.action.float-right.liked(v-if='likeCount > 0')
|
||||
.svg-icon(v-html="icons.liked")
|
||||
| + {{ likeCount }}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -202,8 +202,17 @@ export default {
|
||||
|
||||
if (!profile._id) {
|
||||
const result = await this.$store.dispatch('members:fetchMember', { memberId });
|
||||
this.cachedProfileData[memberId] = result.data.data;
|
||||
profile = result.data.data;
|
||||
if (result.response && result.response.status === 404) {
|
||||
return this.$store.dispatch('snackbars:add', {
|
||||
title: 'Habitica',
|
||||
text: this.$t('messageDeletedUser'),
|
||||
type: 'error',
|
||||
timeout: false,
|
||||
});
|
||||
} else {
|
||||
this.cachedProfileData[memberId] = result.data.data;
|
||||
profile = result.data.data;
|
||||
}
|
||||
}
|
||||
|
||||
// Open the modal only if the data is available
|
||||
|
||||
@@ -193,8 +193,10 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
.col-3.text-center.sub-menu-item(@click='changeSubPage("flower")', :class='{active: activeSubPage === "flower"}')
|
||||
strong(v-once) {{$t('accent')}}
|
||||
.row.sub-menu(v-if='editing')
|
||||
.col-4.offset-2.text-center.sub-menu-item(@click='changeSubPage("ears")' :class='{active: activeSubPage === "ears"}')
|
||||
.col-4.text-center.sub-menu-item(@click='changeSubPage("ears")' :class='{active: activeSubPage === "ears"}')
|
||||
strong(v-once) {{$t('animalEars')}}
|
||||
.col-4.text-center.sub-menu-item(@click='changeSubPage("tails")' :class='{active: activeSubPage === "tails"}')
|
||||
strong(v-once) {{$t('animalTails')}}
|
||||
.col-4.text-center.sub-menu-item(@click='changeSubPage("headband")' :class='{active: activeSubPage === "headband"}')
|
||||
strong(v-once) {{$t('headband')}}
|
||||
#glasses.row(v-if='activeSubPage === "glasses"')
|
||||
@@ -203,17 +205,30 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
.sprite.customize-option(:class="`eyewear_special_${option.key}`", @click='option.click')
|
||||
#animal-ears.row(v-if='activeSubPage === "ears"')
|
||||
.section.col-12.customize-options
|
||||
.option(v-for='option in animalEars',
|
||||
.option(v-for='option in animalItems("headAccessory")',
|
||||
:class='{active: option.active, locked: option.locked}')
|
||||
.sprite.customize-option(:class="`headAccessory_special_${option.key}`", @click='option.click')
|
||||
.gem-lock(v-if='option.locked')
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 2
|
||||
.col-12.text-center(v-if='!animalEarsOwned')
|
||||
.col-12.text-center(v-if='!animalItemsOwned("headAccessory")')
|
||||
.gem-lock
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 5
|
||||
button.btn.btn-secondary.purchase-all(@click='unlock(animalEarsUnlockString)') {{ $t('purchaseAll') }}
|
||||
button.btn.btn-secondary.purchase-all(@click='unlock(animalItemsUnlockString("headAccessory"))') {{ $t('purchaseAll') }}
|
||||
#animal-tails.row(v-if='activeSubPage === "tails"')
|
||||
.section.col-12.customize-options
|
||||
.option(v-for='option in animalItems("back")',
|
||||
:class='{active: option.active, locked: option.locked}')
|
||||
.sprite.customize-option(:class="`icon_back_special_${option.key}`", @click='option.click')
|
||||
.gem-lock(v-if='option.locked')
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 2
|
||||
.col-12.text-center(v-if='!animalItemsOwned("back")')
|
||||
.gem-lock
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 5
|
||||
button.btn.btn-secondary.purchase-all(@click='unlock(animalItemsUnlockString("back"))') {{ $t('purchaseAll') }}
|
||||
#headband.row(v-if='activeSubPage === "headband"')
|
||||
.col-12.customize-options
|
||||
.option(v-for='option in headbands', :class='{active: option.active}')
|
||||
@@ -222,7 +237,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
.col-12.customize-options
|
||||
.option(@click='set({"preferences.chair": "none"})', :class='{active: user.preferences.chair === "none"}')
|
||||
| None
|
||||
.option(v-for='option in ["black", "blue", "green", "pink", "red", "yellow"]',
|
||||
.option(v-for='option in chairKeys',
|
||||
:class='{active: user.preferences.chair === option}')
|
||||
.chair.sprite.customize-option(:class="`button_chair_${option}`", @click='set({"preferences.chair": option})')
|
||||
#flowers.row(v-if='activeSubPage === "flower"')
|
||||
@@ -856,7 +871,7 @@ import isPinned from 'common/script/libs/isPinned';
|
||||
const skinsBySet = groupBy(appearance.skin, 'set.key');
|
||||
const hairColorBySet = groupBy(appearance.hair.color, 'set.key');
|
||||
|
||||
let tasksByCategory = {
|
||||
const tasksByCategory = {
|
||||
work: [
|
||||
{
|
||||
type: 'habit',
|
||||
@@ -1013,7 +1028,11 @@ export default {
|
||||
baseHair4Keys: [15, 16, 17, 18, 19, 20],
|
||||
baseHair5Keys: [1, 2],
|
||||
baseHair6Keys: [1, 2, 3],
|
||||
animalEarsKeys: ['bearEars', 'cactusEars', 'foxEars', 'lionEars', 'pandaEars', 'pigEars', 'tigerEars', 'wolfEars'],
|
||||
animalItemKeys: {
|
||||
back: ['bearTail', 'cactusTail', 'foxTail', 'lionTail', 'pandaTail', 'pigTail', 'tigerTail', 'wolfTail'],
|
||||
headAccessory: ['bearEars', 'cactusEars', 'foxEars', 'lionEars', 'pandaEars', 'pigEars', 'tigerEars', 'wolfEars'],
|
||||
},
|
||||
chairKeys: ['black', 'blue', 'green', 'pink', 'red', 'yellow', 'handleless_black', 'handleless_blue', 'handleless_green', 'handleless_pink', 'handleless_red', 'handleless_yellow'],
|
||||
icons: Object.freeze({
|
||||
logoPurple,
|
||||
bodyIcon,
|
||||
@@ -1074,44 +1093,6 @@ export default {
|
||||
});
|
||||
return options;
|
||||
},
|
||||
animalEarsUnlockString () {
|
||||
let animalItemKeys = this.animalEarsKeys.map(key => {
|
||||
return `items.gear.owned.headAccessory_special_${key}`;
|
||||
});
|
||||
|
||||
return animalItemKeys.join(',');
|
||||
},
|
||||
animalEarsOwned () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
|
||||
let own = true;
|
||||
this.animalEarsKeys.forEach(key => {
|
||||
if (!this.user.items.gear.owned[`headAccessory_special_${key}`]) own = false;
|
||||
});
|
||||
return own;
|
||||
},
|
||||
animalEars () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.animalEarsKeys;
|
||||
let options = keys.map(key => {
|
||||
let newKey = `headAccessory_special_${key}`;
|
||||
let userPurchased = this.user.items.gear.owned[newKey];
|
||||
let locked = !userPurchased;
|
||||
|
||||
let option = {};
|
||||
option.key = key;
|
||||
option.active = this.user.preferences.costume ? this.user.items.gear.costume.headAccessory === newKey : this.user.items.gear.equipped.headAccessory === newKey;
|
||||
option.locked = locked;
|
||||
option.click = () => {
|
||||
let type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||
return locked ? this.unlock(`items.gear.owned.${newKey}`) : this.equip(newKey, type);
|
||||
};
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
},
|
||||
specialShirts () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
@@ -1549,6 +1530,44 @@ export default {
|
||||
backgroundPurchased () {
|
||||
this.backgroundUpdate = new Date();
|
||||
},
|
||||
animalItemsUnlockString (category) {
|
||||
const keys = this.animalItemKeys[category].map(key => {
|
||||
return `items.gear.owned.${category}_special_${key}`;
|
||||
});
|
||||
|
||||
return keys.join(',');
|
||||
},
|
||||
animalItemsOwned (category) {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
|
||||
let own = true;
|
||||
this.animalItemKeys[category].forEach(key => {
|
||||
if (!this.user.items.gear.owned[`${category}_special_${key}`]) own = false;
|
||||
});
|
||||
return own;
|
||||
},
|
||||
animalItems (category) {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let keys = this.animalItemKeys[category];
|
||||
let options = keys.map(key => {
|
||||
let newKey = `${category}_special_${key}`;
|
||||
let userPurchased = this.user.items.gear.owned[newKey];
|
||||
let locked = !userPurchased;
|
||||
|
||||
let option = {};
|
||||
option.key = key;
|
||||
option.active = this.user.preferences.costume ? this.user.items.gear.costume[category] === newKey : this.user.items.gear.equipped[category] === newKey;
|
||||
option.locked = locked;
|
||||
option.click = () => {
|
||||
let type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||
return locked ? this.unlock(`items.gear.owned.${newKey}`) : this.equip(newKey, type);
|
||||
};
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -338,6 +338,9 @@ export default {
|
||||
// @TOOD: We might not need this since groupId is computed now
|
||||
this.getMembers();
|
||||
},
|
||||
challengeId () {
|
||||
this.getMembers();
|
||||
},
|
||||
group () {
|
||||
this.getMembers();
|
||||
},
|
||||
@@ -377,9 +380,7 @@ export default {
|
||||
this.invites = invites;
|
||||
}
|
||||
|
||||
if (this.$store.state.memberModalOptions.viewingMembers.length > 0) {
|
||||
this.members = this.$store.state.memberModalOptions.viewingMembers;
|
||||
}
|
||||
this.members = this.$store.state.memberModalOptions.viewingMembers;
|
||||
},
|
||||
async clickMember (uid, forceShow) {
|
||||
let user = this.$store.state.user.data;
|
||||
|
||||
@@ -35,26 +35,11 @@ div
|
||||
span.small-text(v-html="$t('inviteFriendsParty')")
|
||||
br
|
||||
button.btn.btn-primary(@click='createOrInviteParty()') {{ user.party._id ? $t('inviteFriends') : $t('startAParty') }}
|
||||
a.useMobileApp(v-if="isAndroidMobile()", v-once, href="https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica") {{ $t('useMobileApps') }}
|
||||
a.useMobileApp(v-if="isIOSMobile()", v-once, href="https://itunes.apple.com/us/app/habitica-gamified-task-manager/id994882113?mt=8") {{ $t('useMobileApps') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.useMobileApp {
|
||||
background: red;
|
||||
color: white;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
margin: 10px 5px 0 0;
|
||||
height: 64px;
|
||||
text-align: center;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#app-header {
|
||||
padding-left: 24px;
|
||||
padding-top: 9px;
|
||||
@@ -155,12 +140,6 @@ export default {
|
||||
...mapActions({
|
||||
getPartyMembers: 'party:getMembers',
|
||||
}),
|
||||
isAndroidMobile () {
|
||||
return navigator.userAgent.match(/Android/i);
|
||||
},
|
||||
isIOSMobile () {
|
||||
return navigator.userAgent.match(/iPhone|iPad|iPod/i);
|
||||
},
|
||||
expandMember (memberId) {
|
||||
if (this.expandedMember === memberId) {
|
||||
this.expandedMember = null;
|
||||
|
||||
@@ -396,12 +396,13 @@ export default {
|
||||
toggleUserDropdown () {
|
||||
this.isUserDropdownOpen = !this.isUserDropdownOpen;
|
||||
},
|
||||
sync () {
|
||||
async sync () {
|
||||
this.$root.$emit('habitica::resync-requested');
|
||||
return Promise.all([
|
||||
await Promise.all([
|
||||
this.$store.dispatch('user:fetch', {forceLoad: true}),
|
||||
this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true}),
|
||||
]);
|
||||
this.$root.$emit('habitica::resync-completed');
|
||||
},
|
||||
async getUserGroupPlans () {
|
||||
this.$store.state.groupPlans = await this.$store.dispatch('guilds:getGroupPlans');
|
||||
|
||||
@@ -88,7 +88,6 @@ import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
import { toNextLevel } from '../../common/script/statHelpers';
|
||||
import { shouldDo } from '../../common/script/cron';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import notifications from 'client/mixins/notifications';
|
||||
@@ -223,12 +222,7 @@ export default {
|
||||
userExp (after, before) {
|
||||
if (after === before) return;
|
||||
if (this.user.stats.lvl === 0) return;
|
||||
|
||||
let exp = after - before;
|
||||
if (exp < -50) { // recalculate exp if user level up
|
||||
exp = toNextLevel(this.user.stats.lvl - 1) - before + after;
|
||||
}
|
||||
this.exp(exp);
|
||||
this.exp(after - before);
|
||||
},
|
||||
userGp (after, before) {
|
||||
if (after === before) return;
|
||||
|
||||
@@ -40,7 +40,7 @@ export default {
|
||||
...mapState({user: 'user.data', credentials: 'credentials'}),
|
||||
getCodesUrl () {
|
||||
if (!this.user) return '';
|
||||
return `/api/v4/coupons?_id=${this.user._id}&apiToken=${this.credentials.API_TOKEN}`;
|
||||
return '/api/v4/coupons';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -83,6 +83,7 @@ export default {
|
||||
removeTask () {
|
||||
if (!confirm('Are you sure you want to delete this task?')) return;
|
||||
this.destroyTask(this.brokenChallengeTask);
|
||||
this.close();
|
||||
},
|
||||
close () {
|
||||
this.$store.state.brokenChallengeTask = {};
|
||||
|
||||
@@ -362,7 +362,7 @@ export default {
|
||||
type: this.type,
|
||||
filterType: this.activeFilter.label,
|
||||
}) :
|
||||
this.filterByCompleted(this.taskListOverride, this.activeFilter.label);
|
||||
this.filterByLabel(this.taskListOverride, this.activeFilter.label);
|
||||
|
||||
let taggedList = this.filterByTagList(filteredTaskList, this.selectedTags);
|
||||
let searchedList = this.filterBySearchText(taggedList, this.searchText);
|
||||
@@ -449,9 +449,9 @@ export default {
|
||||
});
|
||||
|
||||
if (this.type !== 'todo') return;
|
||||
this.$root.$on('habitica::resync-requested', () => {
|
||||
this.$root.$on('habitica::resync-completed', () => {
|
||||
if (this.activeFilter.label !== 'complete2') return;
|
||||
this.loadCompletedTodos(true);
|
||||
this.loadCompletedTodos();
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
@@ -598,10 +598,12 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
filterByCompleted (taskList, filter) {
|
||||
filterByLabel (taskList, filter) {
|
||||
if (!taskList) return [];
|
||||
return taskList.filter(task => {
|
||||
if (filter === 'complete2') return task.completed;
|
||||
if (filter === 'due') return task.isDue;
|
||||
if (filter === 'notDue') return !task.isDue;
|
||||
return !task.completed;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -923,17 +923,18 @@ export default {
|
||||
|
||||
if (this.purpose === 'create') {
|
||||
if (this.challengeId) {
|
||||
this.$store.dispatch('tasks:createChallengeTasks', {
|
||||
const response = await this.$store.dispatch('tasks:createChallengeTasks', {
|
||||
challengeId: this.challengeId,
|
||||
tasks: [this.task],
|
||||
});
|
||||
Object.assign(this.task, response);
|
||||
this.$emit('taskCreated', this.task);
|
||||
} else if (this.groupId) {
|
||||
await this.$store.dispatch('tasks:createGroupTasks', {
|
||||
const response = await this.$store.dispatch('tasks:createGroupTasks', {
|
||||
groupId: this.groupId,
|
||||
tasks: [this.task],
|
||||
});
|
||||
|
||||
Object.assign(this.task, response);
|
||||
let promises = this.assignedMembers.map(memberId => {
|
||||
return this.$store.dispatch('tasks:assignTask', {
|
||||
taskId: this.task._id,
|
||||
|
||||
@@ -7,21 +7,17 @@
|
||||
.col-2
|
||||
.svg-icon.envelope(v-html="icons.messageIcon")
|
||||
.col-6
|
||||
h2.text-center(v-once) {{$t('messages')}}
|
||||
// @TODO: Implement this after we fix username bug
|
||||
// .col-2.offset-1
|
||||
// button.btn.btn-secondary(@click='toggleClick()') +
|
||||
.col-4.offset-4
|
||||
.svg-icon.close(v-html="icons.svgClose", @click='close()')
|
||||
h2.text-center(v-once) {{ $t('messages') }}
|
||||
.col-4.offset-3
|
||||
toggle-switch.float-right(
|
||||
:label="optTextSet.switchDescription",
|
||||
:checked="!this.user.inbox.optOut"
|
||||
:hoverText="optTextSet.popoverText",
|
||||
@change="toggleOpt()"
|
||||
)
|
||||
// .col-8.to-form(v-if='displayCreate')
|
||||
// strong To:
|
||||
// b-form-input
|
||||
.col-1
|
||||
.close
|
||||
span.svg-icon.inline.icon-10(aria-hidden="true", v-html="icons.svgClose", @click="close()")
|
||||
.row
|
||||
.col-4.sidebar
|
||||
.search-section
|
||||
@@ -48,9 +44,6 @@
|
||||
.pm-disabled-caption.text-center(v-if="user.inbox.optOut && selectedConversation.key")
|
||||
h4 {{$t('PMDisabledCaptionTitle')}}
|
||||
p {{$t('PMDisabledCaptionText')}}
|
||||
|
||||
// @TODO: Implement new message header here when we fix the above
|
||||
|
||||
.new-message-row(v-if='selectedConversation.key && !user.flags.chatRevoked')
|
||||
textarea(
|
||||
v-model='newMessage',
|
||||
@@ -79,14 +72,6 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close {
|
||||
margin-top: .5em;
|
||||
width: 15px;
|
||||
position: absolute;
|
||||
top: -1.9em;
|
||||
right: 0.3em;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: $gray-700;
|
||||
min-height: 600px;
|
||||
@@ -211,6 +196,7 @@ import moment from 'moment';
|
||||
import filter from 'lodash/filter';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import styleHelper from 'client/mixins/styleHelper';
|
||||
import toggleSwitch from 'client/components/ui/toggleSwitch';
|
||||
@@ -386,15 +372,10 @@ export default {
|
||||
sendPrivateMessage () {
|
||||
if (!this.newMessage) return;
|
||||
|
||||
let convoFound = this.conversations.find((conversation) => {
|
||||
const convoFound = this.conversations.find((conversation) => {
|
||||
return conversation.key === this.selectedConversation.key;
|
||||
});
|
||||
|
||||
this.$store.dispatch('members:sendPrivateMessage', {
|
||||
toUserId: this.selectedConversation.key,
|
||||
message: this.newMessage,
|
||||
});
|
||||
|
||||
convoFound.messages.push({
|
||||
text: this.newMessage,
|
||||
timestamp: new Date(),
|
||||
@@ -408,13 +389,22 @@ export default {
|
||||
convoFound.lastMessageText = this.newMessage;
|
||||
convoFound.date = new Date();
|
||||
|
||||
this.newMessage = '';
|
||||
|
||||
Vue.nextTick(() => {
|
||||
if (!this.$refs.chatscroll) return;
|
||||
let chatscroll = this.$refs.chatscroll.$el;
|
||||
chatscroll.scrollTop = chatscroll.scrollHeight;
|
||||
});
|
||||
|
||||
this.$store.dispatch('members:sendPrivateMessage', {
|
||||
toUserId: this.selectedConversation.key,
|
||||
message: this.newMessage,
|
||||
}).then(response => {
|
||||
const newMessage = response.data.data.message;
|
||||
const messageIndex = findIndex(convoFound.messages, msg => !msg.id);
|
||||
convoFound.messages.splice(convoFound.messages.length - 1, messageIndex, newMessage);
|
||||
});
|
||||
|
||||
this.newMessage = '';
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'inbox-modal');
|
||||
|
||||
@@ -6,6 +6,17 @@
|
||||
<title>Habitica - Gamify Your Life</title>
|
||||
<meta name="description" content="Habitica is a free habit and productivity app that treats your real life like a game. Habitica can help you achieve your goals to become healthy and happy.">
|
||||
<meta name="keywords" content="Habits,Goals,Todo,Gamification,Health,Fitness,School,Work">
|
||||
<meta name="smartbanner:title" content="Habitica">
|
||||
<meta name="smartbanner:author" content="HabitRPG, Inc.">
|
||||
<meta name="smartbanner:price" content="FREE">
|
||||
<meta name="smartbanner:price-suffix-apple" content=" - On the App Store">
|
||||
<meta name="smartbanner:price-suffix-google" content=" - In Google Play">
|
||||
<meta name="smartbanner:icon-apple" content="/static/presskit/Logo/iOS.png">
|
||||
<meta name="smartbanner:icon-google" content="/static/presskit/Logo/Android.png">
|
||||
<meta name="smartbanner:button" content="VIEW">
|
||||
<meta name="smartbanner:button-url-apple" content="https://itunes.apple.com/us/app/habitica-gamified-taskmanager/id994882113">
|
||||
<meta name="smartbanner:button-url-google" content="https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica">
|
||||
<meta name="smartbanner:enabled-platforms" content="android,ios">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700,700i|Roboto:400,400i,700,700i" rel="stylesheet">
|
||||
<link rel="shortcut icon" sizes="48x48" href="/static/icons/favicon.ico">
|
||||
<link rel="shortcut icon" sizes="192x192" href="/static/icons/favicon_192x192.png">
|
||||
|
||||
@@ -47,6 +47,6 @@ export function round (number, nDigits) {
|
||||
}
|
||||
|
||||
export function getXPMessage (val) {
|
||||
if (val < -50) return; // don't show when they multi-level up (resetting their exp)
|
||||
if (val < -50) return; // don't show when they level up (resetting their exp)
|
||||
return `${getSign(val)} ${round(val)}`;
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import getStore from './store';
|
||||
import StoreModule from './libs/store';
|
||||
import './filters/registerGlobals';
|
||||
import i18n from './libs/i18n';
|
||||
import 'smartbanner.js/dist/smartbanner.js';
|
||||
|
||||
import BootstrapVue from 'bootstrap-vue';
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import times from 'lodash/times';
|
||||
import Intro from 'intro.js/';
|
||||
import introjs from 'intro.js';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
|
||||
let showingTour = false;
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
@@ -13,6 +15,8 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
$route () {
|
||||
if (showingTour) return;
|
||||
showingTour = true;
|
||||
this.routeChange();
|
||||
},
|
||||
},
|
||||
@@ -168,13 +172,15 @@ export default {
|
||||
});
|
||||
|
||||
// @TODO: Do we always need to initialize here?
|
||||
let intro = Intro.introJs();
|
||||
const intro = introjs();
|
||||
intro.setOptions({
|
||||
exitOnOverlayClick: false,
|
||||
steps: opts.steps,
|
||||
doneLabel: this.$t('letsgo'),
|
||||
});
|
||||
intro.start();
|
||||
intro.oncomplete(() => {
|
||||
showingTour = false;
|
||||
this.markTourComplete(chapter);
|
||||
});
|
||||
},
|
||||
@@ -187,19 +193,6 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// if (true) { // -2 indicates complete
|
||||
// if (chapter === 'intro') {
|
||||
// // Manually show bunny scroll reward
|
||||
// // let rewardData = {
|
||||
// // reward: [Shared.content.quests.dustbunnies],
|
||||
// // rewardKey: ['inventory_quest_scroll_dustbunnies'],
|
||||
// // rewardText: Shared.content.quests.dustbunnies.text(),
|
||||
// // message: this.$t('checkinEarned'),
|
||||
// // nextRewardAt: 1,
|
||||
// // };
|
||||
// // @TODO: Notification.showLoginIncentive(this.user, rewardData, Social.loadWidgets);
|
||||
// }
|
||||
|
||||
// Mark tour complete
|
||||
ups[`flags.tour.${chapter}`] = -2; // @TODO: Move magic numbers to enum
|
||||
|
||||
@@ -211,7 +204,6 @@ export default {
|
||||
eventValue: lastKnownStep,
|
||||
complete: true,
|
||||
});
|
||||
// }
|
||||
|
||||
this.$store.dispatch('user:set', ups);
|
||||
},
|
||||
|
||||
@@ -2,6 +2,12 @@ import habiticaMarkdown from 'habitica-markdown';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import { getDropClass, getXPMessage, getSign, round } from 'client/libs/notifications';
|
||||
|
||||
// See https://stackoverflow.com/questions/4187146/truncate-number-to-two-decimal-places-without-rounding
|
||||
function toFixedWithoutRounding (num, fixed) {
|
||||
const re = new RegExp(`^-?\\d+(?:\.\\d{0,${(fixed || -1)}})?`);
|
||||
return num.toString().match(re)[0];
|
||||
}
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState({notifications: 'notificationStore'}),
|
||||
@@ -46,7 +52,8 @@ export default {
|
||||
this.notify(parsedMarkdown, 'info');
|
||||
},
|
||||
mp (val) {
|
||||
this.notify(`${this.sign(val)} ${this.round(val)}`, 'mp', 'glyphicon glyphicon-fire', this.sign(val));
|
||||
const cleanMp = `${val}`.replace('-', '').replace('+', '');
|
||||
this.notify(`${this.sign(val)} ${toFixedWithoutRounding(cleanMp, 1)}`, 'mp', 'glyphicon glyphicon-fire', this.sign(val));
|
||||
},
|
||||
purchased (itemName) {
|
||||
this.text(this.$t('purchasedItem', {
|
||||
|
||||
@@ -13,10 +13,10 @@ export default {
|
||||
...mapState(['credentials']),
|
||||
// @TODO refactor into one single computed property
|
||||
paypalCheckoutLink () {
|
||||
return `/paypal/checkout?_id=${this.credentials.API_ID}&apiToken=${this.credentials.API_TOKEN}`;
|
||||
return '/paypal/checkout';
|
||||
},
|
||||
paypalSubscriptionLink () {
|
||||
return `/paypal/subscribe?_id=${this.credentials.API_ID}&apiToken=${this.credentials.API_TOKEN}&sub=${this.subscriptionPlan}`;
|
||||
return `/paypal/subscribe?sub=${this.subscriptionPlan}`;
|
||||
},
|
||||
paypalPurchaseLink () {
|
||||
if (!this.subscription) {
|
||||
@@ -26,7 +26,7 @@ export default {
|
||||
}
|
||||
let couponString = '';
|
||||
if (this.subscription.coupon) couponString = `&coupon=${this.subscription.coupon}`;
|
||||
return `/paypal/subscribe?_id=${this.credentials.API_ID}&apiToken=${this.credentials.API_TOKEN}&sub=${this.subscription.key}${couponString}`;
|
||||
return `/paypal/subscribe?sub=${this.subscription.key}${couponString}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -39,7 +39,7 @@ export default {
|
||||
if (!this.checkGemAmount(data)) return;
|
||||
|
||||
let gift = this.encodeGift(data.giftedTo, data.gift);
|
||||
const url = `/paypal/checkout?_id=${this.credentials.API_ID}&apiToken=${this.credentials.API_TOKEN}&gift=${gift}`;
|
||||
const url = `/paypal/checkout?gift=${gift}`;
|
||||
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
@@ -210,8 +210,6 @@ export default {
|
||||
}
|
||||
|
||||
let queryParams = {
|
||||
_id: this.user._id,
|
||||
apiToken: this.credentials.API_TOKEN,
|
||||
noRedirect: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -85,7 +85,20 @@ export default {
|
||||
|
||||
// the selected member doesn't have the flags property which sets `cardReceived`
|
||||
if (spell.pinType !== 'card') {
|
||||
spell.cast(this.user, target);
|
||||
try {
|
||||
spell.cast(this.user, target);
|
||||
} catch (e) {
|
||||
if (!e.request) {
|
||||
this.$store.dispatch('snackbars:add', {
|
||||
title: '',
|
||||
text: e.message,
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let targetId = target ? target._id : null;
|
||||
|
||||
@@ -84,8 +84,10 @@ export async function updateChallenge (store, payload) {
|
||||
}
|
||||
|
||||
export async function deleteChallenge (store, payload) {
|
||||
let response = await axios.delete(`/api/v4/challenges/${payload.challengeId}`);
|
||||
|
||||
const response = await axios.delete(`/api/v4/challenges/${payload.challengeId}`);
|
||||
if (payload.prize) {
|
||||
store.state.user.data.balance += payload.prize / 4;
|
||||
}
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,12 @@ async function buyArmoire (store, params) {
|
||||
if (item.type === 'gear') {
|
||||
store.state.user.data.items.gear.owned[item.dropKey] = true;
|
||||
}
|
||||
|
||||
if (item.type === 'food') {
|
||||
if (!store.state.user.data.items.food[item.dropKey]) store.state.user.data.items.food[item.dropKey] = 0;
|
||||
store.state.user.data.items.food[item.dropKey] += 1;
|
||||
}
|
||||
|
||||
store.state.user.data.stats.gp -= armoire.value;
|
||||
|
||||
// @TODO: We might need to abstract notifications to library rather than mixin
|
||||
|
||||
@@ -19,13 +19,13 @@ export function fetchUserTasks (store, options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchCompletedTodos (store, forceLoad = false) {
|
||||
export async function fetchCompletedTodos (store) {
|
||||
// Wait for the user to be loaded before deserializing
|
||||
// because user.tasksOrder is necessary
|
||||
await store.dispatch('tasks:fetchUserTasks');
|
||||
|
||||
const loadStatus = store.state.completedTodosStatus;
|
||||
if (loadStatus === 'NOT_LOADED' || forceLoad) {
|
||||
if (loadStatus !== 'LOADING') {
|
||||
store.state.completedTodosStatus = 'LOADING';
|
||||
|
||||
const response = await axios.get('/api/v4/tasks/user?type=completedTodos');
|
||||
@@ -36,17 +36,6 @@ export async function fetchCompletedTodos (store, forceLoad = false) {
|
||||
tasks.todos.push(...completedTodos);
|
||||
|
||||
store.state.completedTodosStatus = 'LOADED';
|
||||
} else if (status === 'LOADED') {
|
||||
return;
|
||||
} else if (loadStatus === 'LOADING') {
|
||||
const watcher = store.watch(state => state.completedTodosStatus, (newLoadingStatus) => {
|
||||
watcher(); // remove the watcher
|
||||
if (newLoadingStatus === 'LOADED') {
|
||||
return;
|
||||
} else {
|
||||
throw new Error(); // TODO add reason?
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -373,5 +373,12 @@
|
||||
"backgroundFlyingOverRockyCanyonText": "Каменист каньон",
|
||||
"backgroundFlyingOverRockyCanyonNotes": "Погледнете отвисоко, докато прелитате над каменист каньон.",
|
||||
"backgroundBridgeText": "Мост",
|
||||
"backgroundBridgeNotes": "Преминете по страхотен мост"
|
||||
"backgroundBridgeNotes": "Преминете по страхотен мост",
|
||||
"backgrounds092018": "SET 52: Released September 2018",
|
||||
"backgroundApplePickingText": "Apple Picking",
|
||||
"backgroundApplePickingNotes": "Go Apple Picking and bring home a bushel.",
|
||||
"backgroundGiantBookText": "Giant Book",
|
||||
"backgroundGiantBookNotes": "Read as you walk through the pages of a Giant Book.",
|
||||
"backgroundCozyBarnText": "Cozy Barn",
|
||||
"backgroundCozyBarnNotes": "Relax with your pets and mounts in their Cozy Barn."
|
||||
}
|
||||
@@ -329,6 +329,5 @@
|
||||
"signup": "Регистриране",
|
||||
"getStarted": "Първи стъпки",
|
||||
"mobileApps": "Мобилни приложения",
|
||||
"learnMore": "Научете повече",
|
||||
"useMobileApps": "Хабитика не работи добре в браузър за мобилно устройство. Препоръчваме Ви да свалите мобилното ни приложение."
|
||||
"learnMore": "Научете повече"
|
||||
}
|
||||
@@ -350,6 +350,8 @@
|
||||
"weaponArmoirePoisonedGobletNotes": "Използвайте това, за да изградите имунитет срещу различни невъобразимо опасни отрови. Увеличава интелигентността с <%= int %>. Омагьосан гардероб: комплект „Пиратска принцеса“ (предмет 3 от 4).",
|
||||
"weaponArmoireJeweledArcherBowText": "Инкрустиран лък за стрелец",
|
||||
"weaponArmoireJeweledArcherBowNotes": "Този лък от злато и скъпоценни камъни ще изпрати стрелите Ви към целите им с невероятна скорост. Увеличава интелигентността с <%= int %>. Омагьосан гардероб: Инкрустиран стрелкови комплект (предмет 3 от 3).",
|
||||
"weaponArmoireNeedleOfBookbindingText": "Needle of Bookbinding",
|
||||
"weaponArmoireNeedleOfBookbindingNotes": "You'd be surprised at how tough books can be. This needle can pierce right to the heart of your chores. Increases Strength by <%= str %>. Enchanted Armoire: Bookbinder Set (Item 3 of 4).",
|
||||
"armor": "броня",
|
||||
"armorCapitalized": "Броня",
|
||||
"armorBase0Text": "Обикновени дрехи",
|
||||
@@ -660,6 +662,8 @@
|
||||
"armorMystery201806Notes": "По тази игрива опашка има светещи петна, които ще осветят пътя Ви в дълбините. Не променя показателите. Предмет за абонати: юни 2018 г.",
|
||||
"armorMystery201807Text": "Опашка на морски змей",
|
||||
"armorMystery201807Notes": "Тази могъща опашка ще Ви изстреля с невероятна скорост през морските дълбини! Не променя показателите. Предмет за абонати: юли 2018 г.",
|
||||
"armorMystery201808Text": "Броня на дракон от лава",
|
||||
"armorMystery201808Notes": "Тази броня е направена от отчупени люспи на неуловимия (и изключително горещ) дракон от лава. Не променя показателите. Предмет за абонати: август 2018 г.",
|
||||
"armorMystery301404Text": "Изтънчен костюм",
|
||||
"armorMystery301404Notes": "Спретнат и елегантен! Не променя показателите. Предмет за абонати: февруари 3015 г.",
|
||||
"armorMystery301703Text": "Изтънчена паунова рокля",
|
||||
@@ -754,6 +758,8 @@
|
||||
"armorArmoirePiraticalPrincessGownNotes": "В тази луксозна дреха има много джобове за криене на оръжия и плячка! Увеличава усета с <%= per %>. Омагьосан гардероб: комплект „Пиратска принцеса“ (предмет 2 от 4).",
|
||||
"armorArmoireJeweledArcherArmorText": "Инкрустирана броня за стрелец",
|
||||
"armorArmoireJeweledArcherArmorNotes": "Тази прецизно изработена броня ще Ви защити от стрели и блуждаещи червени ежедневни задачи! Увеличава якостта с <%= con %>. Омагьосан гардероб: Инкрустиран стрелкови комплект (предмет 2 от 3).",
|
||||
"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).",
|
||||
"headgear": "шлем",
|
||||
"headgearCapitalized": "Защита за главата",
|
||||
"headBase0Text": "Няма защита за главата",
|
||||
@@ -1070,6 +1076,8 @@
|
||||
"headMystery201806Notes": "Хипнотизиращата светлина, закачена отгоре на този шлем, ще привика всички морски същества на Ваша страна. Не променя показателите. Предмет за абонати: февруари 2018 г.",
|
||||
"headMystery201807Text": "Шлем на морски змей",
|
||||
"headMystery201807Notes": "Здравите люспи на този шлем ще Ви защитят от всеки морски враг. Не променя показателите. Предмет за абонати: юли 2018 г.",
|
||||
"headMystery201808Text": "Качулка на дракон от лава",
|
||||
"headMystery201808Notes": "Светещите рога на тази качулка ще Ви осветят пътя през подземните пещери. Не променя показателите. Предмет за абонати: август 2018 г.",
|
||||
"headMystery301404Text": "Украсен цилиндър",
|
||||
"headMystery301404Notes": "Украсен цилиндър за най-изтънчените и високопоставени членове на обществото. Не променя показателите. Предмет за абонати: януари 3015 г.",
|
||||
"headMystery301405Text": "Обикновен цилиндър",
|
||||
@@ -1390,10 +1398,13 @@
|
||||
"shieldArmoireFancyBlownGlassVaseNotes": "Каква страхотна ваза сътворихте! Какво има вътре? Увеличава интелигентността с <%= int %>. Омагьосан гардероб: комплект „Стъклодухач“ (предмет 4 от 4).",
|
||||
"shieldArmoirePiraticalSkullShieldText": "Пиратски щит от череп",
|
||||
"shieldArmoirePiraticalSkullShieldNotes": "Този омагьосан щит шепне къде са местата, където враговете Ви са заровили тайните си съкровища. Слушайте внимателно! Увеличава усета и интелигентността с по <%= attrs %>. Омагьосан гардероб: комплект „Пиратска принцеса“ (предмет 4 от 4).",
|
||||
"shieldArmoireUnfinishedTomeText": "Unfinished Tome",
|
||||
"shieldArmoireUnfinishedTomeNotes": "You simply can't procrastinate when you're holding this! The binding needs to be finished so people can read the book! Increases Intelligence by <%= int %>. Enchanted Armoire: Bookbinder Set (Item 4 of 4).",
|
||||
"back": "Аксесоар за гръб",
|
||||
"backCapitalized": "Аксесоар за гръб",
|
||||
"backBase0Text": "Няма аксесоар за гръб",
|
||||
"backBase0Notes": "Няма аксесоар за гръб.",
|
||||
"animalTails": "Животински опашки",
|
||||
"backMystery201402Text": "Златни крила",
|
||||
"backMystery201402Notes": "Перата на тези сияйни крила блестят на слънце. Не променя показателите. Предмет за абонати: февруари 2014 г.",
|
||||
"backMystery201404Text": "Крила на здрачна пеперуда",
|
||||
@@ -1438,6 +1449,22 @@
|
||||
"backSpecialAetherCloakNotes": "Тази мантия някога е принадлежала на самата Изгубената класова повелителка. Увеличава усета с <%= per %>.",
|
||||
"backSpecialTurkeyTailBaseText": "Пуешка опашка",
|
||||
"backSpecialTurkeyTailBaseNotes": "Носете гордо пуешката си опашка, докато празнувате! Не променя показателите.",
|
||||
"backBearTailText": "Опашка на мечка",
|
||||
"backBearTailNotes": "С тази опашка приличате на смела мечка! Не променя показателите.",
|
||||
"backCactusTailText": "Опашка на кактус",
|
||||
"backCactusTailNotes": "С тази опашка приличате на бодлив кактус! Не променя показателите.",
|
||||
"backFoxTailText": "Опашка на лисица",
|
||||
"backFoxTailNotes": "С тази опашка приличате на хитра лисица! Не променя показателите.",
|
||||
"backLionTailText": "Опашка на лъв",
|
||||
"backLionTailNotes": "С тази опашка приличате на величествен лъв! Не променя показателите.",
|
||||
"backPandaTailText": "Опашка на панда",
|
||||
"backPandaTailNotes": "С тази опашка приличате на любезна панда! Не променя показателите.",
|
||||
"backPigTailText": "Опашка на прасе",
|
||||
"backPigTailNotes": "С тази опашка приличате на странно прасе! Не променя показателите.",
|
||||
"backTigerTailText": "Опашка на тигър",
|
||||
"backTigerTailNotes": "С тази опашка приличате на свиреп тигър! Не променя показателите.",
|
||||
"backWolfTailText": "Опашка на лъв",
|
||||
"backWolfTailNotes": "С тази опашка приличате на предан вълк! Не променя показателите.",
|
||||
"body": "Аксесоар за тяло",
|
||||
"bodyCapitalized": "Аксесоар за тяло",
|
||||
"bodyBase0Text": "Няма аксесоар за тяло",
|
||||
@@ -1560,6 +1587,8 @@
|
||||
"headAccessoryMystery301405Notes": "„Очилата се слагат на очите“ — казват хората. „Никому не са нужни защитни очила, които могат да се носят само на главата“ — казват пак те. Ха! Показахте на всички, че това не е вярно! Не променя показателите. Предмет за абонати: август 3015 г.",
|
||||
"headAccessoryArmoireComicalArrowText": "Забавна стрела",
|
||||
"headAccessoryArmoireComicalArrowNotes": "Този странен предмет е създаден, за да предизвиква смях! Увеличава силата с <%= str %>. Омагьосан гардероб: независим предмет.",
|
||||
"headAccessoryArmoireGogglesOfBookbindingText": "Goggles of Bookbinding",
|
||||
"headAccessoryArmoireGogglesOfBookbindingNotes": "These goggles will help you zero in on any task, large or small! Increases Perception by <%= per %>. Enchanted Armoire: Bookbinder Set (Item 1 of 4).",
|
||||
"eyewear": "Предмет за очи",
|
||||
"eyewearCapitalized": "Предмет за очи",
|
||||
"eyewearBase0Text": "Няма предмет за очи",
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
"dateEndJune": "14 юни",
|
||||
"dateEndJuly": "31 юли",
|
||||
"dateEndAugust": "31 август",
|
||||
"dateEndSeptember": "21 септември",
|
||||
"dateEndOctober": "31 октомври",
|
||||
"dateEndNovember": "30 ноември",
|
||||
"dateEndJanuary": "31 януари",
|
||||
|
||||
@@ -61,5 +61,6 @@
|
||||
"messageNotAbleToBuyInBulk": "Не може да се закупи повече от един брой от този предмет.",
|
||||
"notificationsRequired": "Идентификаторите на известията са задължителни.",
|
||||
"unallocatedStatsPoints": "Имате <span class=\"notification-bold-blue\"><%= points %> неразпределени показателни точки</span>",
|
||||
"beginningOfConversation": "Това е началото на разговора Ви с <%= userName %>. Запомнете да спазвате добрия тон, да уважавате другия и да следвате Обществените правила!"
|
||||
"beginningOfConversation": "Това е началото на разговора Ви с <%= userName %>. Запомнете да спазвате добрия тон, да уважавате другия и да следвате Обществените правила!",
|
||||
"messageDeletedUser": "Съжаляваме, но този потребител е изтрил профила си."
|
||||
}
|
||||
@@ -146,6 +146,7 @@
|
||||
"mysterySet201805": "Феноменален паунов комплект",
|
||||
"mysterySet201806": "Комплект на морския дявол",
|
||||
"mysterySet201807": "Комплект на морския змей",
|
||||
"mysterySet201808": "Комплект на дракон от лава",
|
||||
"mysterySet301404": "Стандартен изтънчен комплект",
|
||||
"mysterySet301405": "Комплект изтънчени принадлежности",
|
||||
"mysterySet301703": "Изтънчен паунов комплект",
|
||||
|
||||
@@ -373,5 +373,12 @@
|
||||
"backgroundFlyingOverRockyCanyonText": "Rocky Canyon",
|
||||
"backgroundFlyingOverRockyCanyonNotes": "Look down into a breathtaking scene as you fly over a Rocky Canyon.",
|
||||
"backgroundBridgeText": "Bridge",
|
||||
"backgroundBridgeNotes": "Cross a charming Bridge."
|
||||
"backgroundBridgeNotes": "Cross a charming Bridge.",
|
||||
"backgrounds092018": "SET 52: Released September 2018",
|
||||
"backgroundApplePickingText": "Apple Picking",
|
||||
"backgroundApplePickingNotes": "Go Apple Picking and bring home a bushel.",
|
||||
"backgroundGiantBookText": "Giant Book",
|
||||
"backgroundGiantBookNotes": "Read as you walk through the pages of a Giant Book.",
|
||||
"backgroundCozyBarnText": "Cozy Barn",
|
||||
"backgroundCozyBarnNotes": "Relax with your pets and mounts in their Cozy Barn."
|
||||
}
|
||||
@@ -329,6 +329,5 @@
|
||||
"signup": "Zaregistruj se",
|
||||
"getStarted": "Začni",
|
||||
"mobileApps": "Mobilní aplikace",
|
||||
"learnMore": "Zjisti více",
|
||||
"useMobileApps": "Habitica není optimalizována pro mobilní prohlížeče. Doporučujeme si stáhnout naší mobilní aplikaci."
|
||||
"learnMore": "Zjisti více"
|
||||
}
|
||||
@@ -350,6 +350,8 @@
|
||||
"weaponArmoirePoisonedGobletNotes": "Use this to build your resistance to iocane powder and other inconceivably dangerous poisons. Increases Intelligence by <%= int %>. Enchanted Armoire: Piratical Princess Set (Item 3 of 4).",
|
||||
"weaponArmoireJeweledArcherBowText": "Jeweled Archer Bow",
|
||||
"weaponArmoireJeweledArcherBowNotes": "This bow of gold and gems will send your arrows to their targets at incredible speed. Increases Intelligence by <%= int %>. Enchanted Armoire: Jeweled Archer Set (Item 3 of 3).",
|
||||
"weaponArmoireNeedleOfBookbindingText": "Needle of Bookbinding",
|
||||
"weaponArmoireNeedleOfBookbindingNotes": "You'd be surprised at how tough books can be. This needle can pierce right to the heart of your chores. Increases Strength by <%= str %>. Enchanted Armoire: Bookbinder Set (Item 3 of 4).",
|
||||
"armor": "zbroj",
|
||||
"armorCapitalized": "Zbroj",
|
||||
"armorBase0Text": "Obyčejné oblečení",
|
||||
@@ -660,6 +662,8 @@
|
||||
"armorMystery201806Notes": "This sinuous tail features glowing spots to light your way through the deep. Confers no benefit. June 2018 Subscriber Item.",
|
||||
"armorMystery201807Text": "Sea Serpent Tail",
|
||||
"armorMystery201807Notes": "This powerful tail will propel you through the sea at incredible speeds! Confers no benefit. July 2018 Subscriber Item.",
|
||||
"armorMystery201808Text": "Lava Dragon Armor",
|
||||
"armorMystery201808Notes": "This armor is made from the shed scales of the elusive (and extremely warm) Lava Dragon. Confers no benefit. August 2018 Subscriber Item.",
|
||||
"armorMystery301404Text": "Steampunk oblek",
|
||||
"armorMystery301404Notes": "Elegantní a fešácký, joj! Nepřináší žádný benefit. Předmět pro předplatitele únor 3015.",
|
||||
"armorMystery301703Text": "Steampunk Peacock Gown",
|
||||
@@ -754,6 +758,8 @@
|
||||
"armorArmoirePiraticalPrincessGownNotes": "This luxuriant garment has many pockets for concealing weapons and loot! Increases Perception by <%= per %>. Enchanted Armoire: Piratical Princess Set (Item 2 of 4).",
|
||||
"armorArmoireJeweledArcherArmorText": "Jeweled Archer Armor",
|
||||
"armorArmoireJeweledArcherArmorNotes": "This finely crafted armor will protect you from projectiles or errant red Dailies! Increases Constitution by <%= con %>. Enchanted Armoire: Jeweled Archer Set (Item 2 of 3).",
|
||||
"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).",
|
||||
"headgear": "helm",
|
||||
"headgearCapitalized": "Headgear",
|
||||
"headBase0Text": "No Headgear",
|
||||
@@ -1070,6 +1076,8 @@
|
||||
"headMystery201806Notes": "The mesmerizing light atop this helm will call all the creatures of the sea to your side. We urge you to use your glowy powers of attraction for good! Confers no benefit. June 2018 Subscriber Item.",
|
||||
"headMystery201807Text": "Sea Serpent Helm",
|
||||
"headMystery201807Notes": "The strong scales on this helm will protect you from any manner of oceanic foe. Confers no benefit. July 2018 Subscriber Item.",
|
||||
"headMystery201808Text": "Lava Dragon Cowl",
|
||||
"headMystery201808Notes": "The glowing horns on this cowl will light your way through underground caverns. Confers no benefit. August 2018 Subscriber Item.",
|
||||
"headMystery301404Text": "Fešný cylindr",
|
||||
"headMystery301404Notes": "Fešný cylindr pro ty největší džentlmeny. Předmět pro předplatitele leden 2015. Nepřináší žádný benefit.",
|
||||
"headMystery301405Text": "Obyčejný cylindr",
|
||||
@@ -1390,10 +1398,13 @@
|
||||
"shieldArmoireFancyBlownGlassVaseNotes": "What a fancy vase you've made! What will you put inside? Increases Intelligence by <%= int %>. Enchanted Armoire: Glassblower Set (Item 4 of 4).",
|
||||
"shieldArmoirePiraticalSkullShieldText": "Piratical Skull Shield",
|
||||
"shieldArmoirePiraticalSkullShieldNotes": "This enchanted shield will whisper the secret locations of your enemies' treasures- listen closely! Increases Perception and Intelligence by <%= attrs %> each. Enchanted Armoire: Piratical Princess Set (Item 4 of 4).",
|
||||
"shieldArmoireUnfinishedTomeText": "Unfinished Tome",
|
||||
"shieldArmoireUnfinishedTomeNotes": "You simply can't procrastinate when you're holding this! The binding needs to be finished so people can read the book! Increases Intelligence by <%= int %>. Enchanted Armoire: Bookbinder Set (Item 4 of 4).",
|
||||
"back": "Příslušenství na záda",
|
||||
"backCapitalized": "Back Accessory",
|
||||
"backBase0Text": "Bez příslušenství na zádech",
|
||||
"backBase0Notes": "Bez příslušenství na zádech.",
|
||||
"animalTails": "Animal Tails",
|
||||
"backMystery201402Text": "Zlatá křídla",
|
||||
"backMystery201402Notes": "Tato lesklá křídla mají pera, která se třpytí na slunci! Nepřináší žádný benefit. Výbava pro předplatitele únor 2014",
|
||||
"backMystery201404Text": "Měsíční motýlí křídla",
|
||||
@@ -1438,6 +1449,22 @@
|
||||
"backSpecialAetherCloakNotes": "This cloak once belonged to the Lost Masterclasser herself. Increases Perception by <%= per %>.",
|
||||
"backSpecialTurkeyTailBaseText": "Turkey Tail",
|
||||
"backSpecialTurkeyTailBaseNotes": "Wear your noble Turkey Tail with pride while you celebrate! Confers no benefit.",
|
||||
"backBearTailText": "Bear Tail",
|
||||
"backBearTailNotes": "This tail makes you look like a brave bear! Confers no benefit.",
|
||||
"backCactusTailText": "Cactus Tail",
|
||||
"backCactusTailNotes": "This tail makes you look like a prickly cactus! Confers no benefit.",
|
||||
"backFoxTailText": "Fox Tail",
|
||||
"backFoxTailNotes": "This tail makes you look like a wily fox! Confers no benefit.",
|
||||
"backLionTailText": "Lion Tail",
|
||||
"backLionTailNotes": "This tail makes you look like a regal lion! Confers no benefit.",
|
||||
"backPandaTailText": "Panda Tail",
|
||||
"backPandaTailNotes": "This tail makes you look like a gentle panda! Confers no benefit.",
|
||||
"backPigTailText": "Pig Tail",
|
||||
"backPigTailNotes": "This tail makes you look like a whimsical pig! Confers no benefit.",
|
||||
"backTigerTailText": "Tiger Tail",
|
||||
"backTigerTailNotes": "This tail makes you look like a fierce tiger! Confers no benefit.",
|
||||
"backWolfTailText": "Wolf Tail",
|
||||
"backWolfTailNotes": "This tail makes you look like a loyal wolf! Confers no benefit.",
|
||||
"body": "Příslušenství na tělo",
|
||||
"bodyCapitalized": "Body Accessory",
|
||||
"bodyBase0Text": "Žádné doplňky",
|
||||
@@ -1560,6 +1587,8 @@
|
||||
"headAccessoryMystery301405Notes": "\"Brýle jsou na oči,\" říkali. \"Nikdo nechce nosit brýle na čele,\" říkali. Ha! Teď jsi jim to natřel! Nepřináší žádný benefit. Předmět pro předplatitele srpen 3015.",
|
||||
"headAccessoryArmoireComicalArrowText": "Komický šíp",
|
||||
"headAccessoryArmoireComicalArrowNotes": "This whimsical item sure is good for a laugh! Increases Strength by <%= str %>. Enchanted Armoire: Independent Item.",
|
||||
"headAccessoryArmoireGogglesOfBookbindingText": "Goggles of Bookbinding",
|
||||
"headAccessoryArmoireGogglesOfBookbindingNotes": "These goggles will help you zero in on any task, large or small! Increases Perception by <%= per %>. Enchanted Armoire: Bookbinder Set (Item 1 of 4).",
|
||||
"eyewear": "Brýle",
|
||||
"eyewearCapitalized": "Brýle",
|
||||
"eyewearBase0Text": "Žádné vybavení pro oči",
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
"dateEndJune": "Červen 14",
|
||||
"dateEndJuly": "July 31",
|
||||
"dateEndAugust": "Srpen 31",
|
||||
"dateEndSeptember": "September 21",
|
||||
"dateEndOctober": "Říjen 31",
|
||||
"dateEndNovember": "Listopad 30",
|
||||
"dateEndJanuary": "Leden 31",
|
||||
|
||||
@@ -61,5 +61,6 @@
|
||||
"messageNotAbleToBuyInBulk": "This item cannot be purchased in quantities above 1.",
|
||||
"notificationsRequired": "Id upozornění je potřeba.",
|
||||
"unallocatedStatsPoints": "Máš <span class=\"notification-bold-blue\"><%= points %>nepřidělených vlastnostních bodů</span>",
|
||||
"beginningOfConversation": "Toto je začátek tvé konverzace s uživatelem <%= userName %>. Nezapomeň být milý, ucitvý a drž se směrnic komunity!"
|
||||
"beginningOfConversation": "Toto je začátek tvé konverzace s uživatelem <%= userName %>. Nezapomeň být milý, ucitvý a drž se směrnic komunity!",
|
||||
"messageDeletedUser": "Sorry, this user has deleted their account."
|
||||
}
|
||||
@@ -146,6 +146,7 @@
|
||||
"mysterySet201805": "Phenomenal Peacock Set",
|
||||
"mysterySet201806": "Alluring Anglerfish Set",
|
||||
"mysterySet201807": "Sea Serpent Set",
|
||||
"mysterySet201808": "Lava Dragon Set",
|
||||
"mysterySet301404": "Standardní steampunkový set",
|
||||
"mysterySet301405": "Set steampunkových doplňků",
|
||||
"mysterySet301703": "Peacock Steampunk Set",
|
||||
|
||||
@@ -373,5 +373,12 @@
|
||||
"backgroundFlyingOverRockyCanyonText": "Rocky Canyon",
|
||||
"backgroundFlyingOverRockyCanyonNotes": "Look down into a breathtaking scene as you fly over a Rocky Canyon.",
|
||||
"backgroundBridgeText": "Bridge",
|
||||
"backgroundBridgeNotes": "Cross a charming Bridge."
|
||||
"backgroundBridgeNotes": "Cross a charming Bridge.",
|
||||
"backgrounds092018": "SET 52: Released September 2018",
|
||||
"backgroundApplePickingText": "Apple Picking",
|
||||
"backgroundApplePickingNotes": "Go Apple Picking and bring home a bushel.",
|
||||
"backgroundGiantBookText": "Giant Book",
|
||||
"backgroundGiantBookNotes": "Read as you walk through the pages of a Giant Book.",
|
||||
"backgroundCozyBarnText": "Cozy Barn",
|
||||
"backgroundCozyBarnNotes": "Relax with your pets and mounts in their Cozy Barn."
|
||||
}
|
||||