Compare commits

..

63 Commits

Author SHA1 Message Date
Sabe Jones e173b7784c 4.60.0 2018-09-04 21:30:00 +00:00
Sabe Jones c3db59aae8 chore(i18n): update locales 2018-09-04 21:29:10 +00:00
Sabe Jones 44e063c035 chore(sprites): compile 2018-09-04 16:24:12 -05:00
Sabe Jones 4e2c08cfed feat(content): Armoire and Backgrounds 201809 2018-09-04 16:24:04 -05:00
Sabe Jones a9340ee60f 4.59.2 2018-08-31 16:02:35 -05:00
Sabe Jones c8d874d28a Revert "Show accurate XP gain in notification on level up (#10590)"
This reverts commit 1f7dd421d4.
2018-08-31 16:02:14 -05:00
Sabe Jones 32a22f1545 Revert "Check user version before adding notifications (#10628)"
This reverts commit 0002148326.
2018-08-31 16:00:31 -05:00
Sabe Jones 1545685a5b 4.59.1 2018-08-30 19:00:15 +00:00
Sabe Jones 410355c3f1 chore(i18n): update locales 2018-08-30 18:59:45 +00:00
Sabe Jones ac27cabf6a chore(news): Bailey 2018-08-30 13:56:36 -05:00
Sabe Jones 972631e7ac Merge branch 'release' into develop 2018-08-29 20:26:39 +00:00
Sabe Jones d27ed7c406 4.59.0 2018-08-29 20:25:50 +00:00
Sabe Jones 031783b1d7 chore(i18n): update locales 2018-08-29 20:25:24 +00:00
Sabe Jones 318aa7cbd9 chore(sprites): compile 2018-08-29 15:20:36 -05:00
Sabe Jones f802a41f75 feat(content): Animal Tails 2018-08-29 15:20:09 -05:00
Alys 1d597039ca prevent Quest progress message in Party chat when user is Resting in the Inn (#10636)
* prevent quest progress message in party chat when user is resting in the inn

* improve comment

* update tests now that the test group includes a new member (sleeping quest participant)

* adjust a test to fix lint failure (and make the test better)

* fix order of element assignments in test array
2018-08-28 15:04:16 +02:00
Keith Holliday 0002148326 Check user version before adding notifications (#10628) 2018-08-26 15:13:36 -05:00
Keith Holliday d198e23de6 Fixed editing categories (#10627) 2018-08-25 11:15:47 -05:00
Keith Holliday 4f4e141806 Added prize back after deleting challenge (#10631) 2018-08-25 11:15:00 -05:00
Keith Holliday 05e8d6f032 Cleaned mp displaed in fixed value (#10626) 2018-08-25 11:14:41 -05:00
Matteo Pagliazzi 39847893d2 remove use mobile apps banner (#10634) 2018-08-25 15:02:44 +02:00
Forrest Hatfield eb99b709e0 Allow login buttons to expand vertically - fixes #9861 (#10622)
* Allow login buttons to expand vertically

* whitespace matching
2018-08-24 16:38:42 -05:00
Alex Figueroa 862b3453f8 Fix members modal showing stale data (#10619)
Resolves: #10544
2018-08-24 16:00:49 -05:00
Jacob Frericks 7f48853d32 Fixing misspelling and inconsistent punctuation in the api doc (#10617) 2018-08-24 15:48:51 -05:00
Rene Cordier 5c4f763bb1 Fix lostMasterclasser achievement issue (#10616) 2018-08-24 15:23:43 -05:00
Forrest Hatfield bc9401b2f7 Added smartbanner code to suggest iphone/android apps for mobile users - fixes #9901 (#10604)
* Added smartbanner code to suggest iphone/android apps for mobile users

* Installed smartbanner.js as a module and imported css through app.vue

* Changed the logos to use the ones in the existing presskit directory and fixed the import line for the smartbanner component

* Changed smartbanner import to a src include for css and updated js import
2018-08-24 15:08:34 -05:00
negue 6fb9030b96 reload completed tasks after resync is finished - always reload completed tasks (#10614) 2018-08-24 15:04:59 -05:00
Sabe Jones ba307af963 Correct timing on updating Group Plan member quantities (#10589)
* fix(groups): correct timing on updating member quantities

* fix(groups): don't run group cancellation check if we're in invite flow

* fix(groups): update leader when memberCount is 1

* fix(groups): move leader update back--unrelated to group plans fix
2018-08-24 14:57:05 -05:00
Sabe Jones cf4b920a67 4.58.0 2018-08-23 20:13:15 +00:00
Sabe Jones b0ff35a8f1 chore(i18n): update locales 2018-08-23 20:04:40 +00:00
Sabe Jones 85b4c7825e chore(sprites): compile 2018-08-23 14:57:28 -05:00
Sabe Jones 5b7ea8ec5c feat(content): Mystery Items Aug 2018 2018-08-23 14:57:11 -05:00
Sabe Jones 5cfd0c863e Merge branch 'release' into develop 2018-08-22 16:46:44 +00:00
Sabe Jones 10c6244c0c 4.57.4 2018-08-22 16:46:14 +00:00
Sabe Jones 20e65be8bf chore(i18n): update locales 2018-08-22 16:45:38 +00:00
Sabe Jones 8bac324ba7 fix(content): September end date for Ember Potions 2018-08-22 11:42:38 -05:00
Matteo Pagliazzi 2ee0288aaa fix stripe sub cancellation test 2018-08-22 14:36:20 +02:00
Sabe Jones b7ef4c50b2 Merge branch 'release' into develop 2018-08-21 18:32:52 +00:00
Sabe Jones 52be9c750f 4.57.3 2018-08-21 18:32:30 +00:00
Sabe Jones b0200026aa chore(i18n): update locales 2018-08-21 18:32:13 +00:00
Sabe Jones e6c8b977c8 feat(content): enable Ember Potions 2018-08-21 13:29:52 -05:00
Sabe Jones c78b5ecf7c Analytics: More / improved tracking (#10608)
* WIP(analytics): add / improve tracking

* fix(groups): revert attempt at tracking on group model

* fix(analytics): track questing based on user data

* each buy-operation now has a getItemType method - typo getItemKey - removed unneeded overrides
2018-08-20 14:13:22 -05:00
Sabe Jones f27e9b02d8 Merge branch 'release' into develop 2018-08-20 14:16:36 +00:00
Sabe Jones c06c19ca41 4.57.2 2018-08-20 14:16:01 +00:00
Sabe Jones d5d894b8a9 chore(i18n): update locales 2018-08-20 14:15:23 +00:00
Sabe Jones 7bd4e6a5a9 Merge branch 'remove-auth-with-url' into release 2018-08-20 09:12:59 -05:00
Matteo Pagliazzi f13eed5663 fix inbox modal header 2018-08-20 15:40:31 +02:00
Keith Holliday a9a2fe6314 Fixed mp rounding (#10599)
* Fixed mp rounding

* Fixed toFixed rounding
2018-08-18 21:08:56 -05:00
Keith Holliday d6514bce8b Fixed inbox id after add (#10609) 2018-08-18 21:08:32 -05:00
Alys 603fc8c4dd combine cron's Resting in the Inn code with non-sleeping code - fixes #5232 etc (#10577)
* remove commented-out code for purging PMs - no longer needed

https://github.com/HabitRPG/habitica/issues/7940#issuecomment-406489506

* adjust comments

* move cron code when sleeping / resting back into main body of cron code

* rename tests to use consistent terminology for sleeping

* add tests for cron when user is sleeping

* move sleeping tests to same place as non-sleeping test

This matches how the code has sleeping and non-sleeping code mingled.

* replace a broken test with new tests

The deleted test wasn't working correctly. The check that the user's
health hadn't decreased would have worked even if the user wasn't
sleeping because the Daily had been marked completed.
The new tests test both no damage from incomplete Dailies and
that Dailies are reset.

* add tests for Perfect Day buff and rename existing tests for consistent terminology

* remove old test code
2018-08-18 12:47:07 +02:00
Sabe Jones 3c602351f9 Merge branch 'release' into develop 2018-08-18 03:32:39 +00:00
Keith Holliday 6aa204c3f5 Fixed concurrency issues with push devices (#10598)
* Fixed concurrency issues with push devices

* Fixed push notificaiton response and model adding
2018-08-17 07:01:41 -05:00
Keith Holliday eaaa5ad7f3 Added amoire food to user immediately (#10596)
* Added amoire food to user immediately

* Fixed user item set
2018-08-17 06:29:32 -05:00
Keith Holliday 54468ff499 Added existence check (#10595) 2018-08-17 06:20:45 -05:00
Brian Fenton 53405aa586 Handleless wheelchair options (#10572)
* removing duplicate keys

* adding chair assets and wiring them to customize screen

* adding customization data for new wheelchair types

* removing an unused locale key and moving the code style override closer to the affected area

* explicitly re-enabilng linting rule

* adding button-sized chair assets

* updating assets to new resolution

* moving chair keys into component data
2018-08-17 12:23:43 +02:00
Rene Cordier 7630c02e13 Fixing healing light not being castable when user full hp (#10603)
* Fixing healing light not being castable on server and client sides when user has already full health

Adding integration test for spell cast of healing light when full health

Adding test for heal cast if user has full health

* Fixing ESLint syntax in the spells test files
2018-08-17 12:06:58 +02:00
Isabelle Lavandero ec444384f4 Add error notification for deleted user (#10600)
* snackbar notification for deleted user

* check for 404

* localize text
2018-08-17 12:02:43 +02:00
Isabelle Lavandero cce9b33844 Filter dailies by due/not due in group plan and challenge page (#10582)
* sort by isDue works but only on refresh

* update isDue for new tasks

* apply correct filter to challenge page
2018-08-17 11:58:43 +02:00
Matteo Pagliazzi b977d42402 fix hall of heroes 2018-08-17 11:52:15 +02:00
Matteo Pagliazzi 2672cbd790 fix stripe cance 2018-08-17 11:12:48 +02:00
Keith Holliday b7ca5be6ee Closed modal when removing challenge task (#10597) 2018-08-16 16:54:57 -05:00
Keith Holliday 5ae89761b0 Prevent tour from displaying twice (#10594)
* Prevent tour from displaying twice

* Removed forced and prevent overlay click
2018-08-16 16:53:37 -05:00
Matteo Pagliazzi 696121fb24 remove auth with url 2018-08-15 10:40:25 +02:00
403 changed files with 32792 additions and 30960 deletions
+2 -2
View File
@@ -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 });
+2 -2
View File
@@ -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) {
+5615 -5612
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -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",
+152 -77
View File
@@ -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);
+110 -16
View File
@@ -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;
});
});
+38
View File
@@ -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();
}
});
});
+2 -1
View File
@@ -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;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 549 KiB

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 KiB

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 KiB

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

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');
},
},
+18 -17
View File
@@ -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
+64 -45
View File
@@ -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;
+3 -2
View File
@@ -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');
+1 -7
View File
@@ -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 = {};
+6 -4
View File
@@ -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,
+18 -28
View File
@@ -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');
+11
View File
@@ -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">
+1 -1
View File
@@ -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)}`;
}
+1
View File
@@ -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';
+8 -16
View File
@@ -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);
},
+8 -1
View File
@@ -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', {
+4 -6
View File
@@ -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,
};
+14 -1
View File
@@ -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;
+4 -2
View File
@@ -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;
}
+6
View File
@@ -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
+2 -13
View File
@@ -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?
}
});
}
}
+8 -1
View File
@@ -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."
}
+1 -2
View File
@@ -329,6 +329,5 @@
"signup": "Регистриране",
"getStarted": "Първи стъпки",
"mobileApps": "Мобилни приложения",
"learnMore": "Научете повече",
"useMobileApps": "Хабитика не работи добре в браузър за мобилно устройство. Препоръчваме Ви да свалите мобилното ни приложение."
"learnMore": "Научете повече"
}
+29
View File
@@ -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": "Няма предмет за очи",
+1
View File
@@ -132,6 +132,7 @@
"dateEndJune": "14 юни",
"dateEndJuly": "31 юли",
"dateEndAugust": "31 август",
"dateEndSeptember": "21 септември",
"dateEndOctober": "31 октомври",
"dateEndNovember": "30 ноември",
"dateEndJanuary": "31 януари",
+2 -1
View File
@@ -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": "Изтънчен паунов комплект",
+8 -1
View File
@@ -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."
}
+1 -2
View File
@@ -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"
}
+29
View File
@@ -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",
+1
View File
@@ -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",
+2 -1
View File
@@ -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",
+8 -1
View File
@@ -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."
}

Some files were not shown because too many files have changed in this diff Show More