mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-04-09 05:21:02 -05:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54f57445da | ||
|
|
95ef2b1789 | ||
|
|
4d32977e5c | ||
|
|
7fe2504906 | ||
|
|
b74cee3d21 | ||
|
|
5af7733150 | ||
|
|
824bf62e0a | ||
|
|
ac24a5dddd | ||
|
|
9111f59da4 | ||
|
|
6a550b34df | ||
|
|
0c973b1cf0 | ||
|
|
4170ef5e79 | ||
|
|
506275c30e | ||
|
|
6838b7d0a6 |
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"version": "4.4.4",
|
||||
"version": "4.5.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.4.4",
|
||||
"version": "4.5.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@slack/client": "^3.8.1",
|
||||
|
||||
@@ -98,4 +98,24 @@ describe('POST /user/purchase/:type/:key', () => {
|
||||
await members[0].sync();
|
||||
expect(members[0].balance).to.equal(oldBalance);
|
||||
});
|
||||
|
||||
describe('bulk purchasing', () => {
|
||||
it('purchases a gem item', async () => {
|
||||
await user.post(`/user/purchase/${type}/${key}`, {quantity: 2});
|
||||
await user.sync();
|
||||
|
||||
expect(user.items[type][key]).to.equal(2);
|
||||
});
|
||||
|
||||
it('can convert gold to gems if subscribed', async () => {
|
||||
let oldBalance = user.balance;
|
||||
await user.update({
|
||||
'purchased.plan.customerId': 'group-plan',
|
||||
'stats.gp': 1000,
|
||||
});
|
||||
await user.post('/user/purchase/gems/gem', {quantity: 2});
|
||||
await user.sync();
|
||||
expect(user.balance).to.equal(oldBalance + 0.50);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import shared from '../../../../../website/common/script';
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import shared from '../../../../../../website/common/script';
|
||||
|
||||
let content = shared.content;
|
||||
|
||||
@@ -82,4 +82,19 @@ describe('POST /user/buy/:key', () => {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
});
|
||||
|
||||
it('allows for bulk purchases', async () => {
|
||||
await user.update({
|
||||
'stats.gp': 400,
|
||||
'stats.hp': 20,
|
||||
});
|
||||
|
||||
let potion = content.potion;
|
||||
let res = await user.post('/user/buy/potion', {quantity: 2});
|
||||
await user.sync();
|
||||
|
||||
expect(user.stats.hp).to.equal(50);
|
||||
expect(res.data).to.eql(user.stats);
|
||||
expect(res.message).to.equal(t('messageBought', {itemText: potion.text()}));
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /user/buy-armoire', () => {
|
||||
let user;
|
||||
@@ -3,7 +3,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /user/buy-gear/:key', () => {
|
||||
let user;
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import shared from '../../../../../website/common/script';
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import shared from '../../../../../../website/common/script';
|
||||
|
||||
let content = shared.content;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /user/buy-mystery-set/:key', () => {
|
||||
let user;
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import shared from '../../../../../website/common/script';
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import shared from '../../../../../../website/common/script';
|
||||
|
||||
let content = shared.content;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import shared from '../../../../../website/common/script';
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import shared from '../../../../../../website/common/script';
|
||||
|
||||
let content = shared.content;
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
generateNext,
|
||||
} from '../../../../helpers/api-unit.helper';
|
||||
import responseMiddleware from '../../../../../website/server/middlewares/response';
|
||||
import packageInfo from '../../../../../package.json';
|
||||
|
||||
describe('response middleware', () => {
|
||||
let res, req, next;
|
||||
@@ -34,6 +35,7 @@ describe('response middleware', () => {
|
||||
data: {field: 1},
|
||||
notifications: [],
|
||||
userV: res.locals.user._v,
|
||||
appVersion: packageInfo.version,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,6 +53,7 @@ describe('response middleware', () => {
|
||||
message: 'hello',
|
||||
notifications: [],
|
||||
userV: res.locals.user._v,
|
||||
appVersion: packageInfo.version,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,6 +70,7 @@ describe('response middleware', () => {
|
||||
data: {field: 1},
|
||||
notifications: [],
|
||||
userV: res.locals.user._v,
|
||||
appVersion: packageInfo.version,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,6 +85,7 @@ describe('response middleware', () => {
|
||||
data: {field: 1},
|
||||
notifications: [],
|
||||
userV: 0,
|
||||
appVersion: packageInfo.version,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,6 +109,7 @@ describe('response middleware', () => {
|
||||
},
|
||||
],
|
||||
userV: res.locals.user._v,
|
||||
appVersion: packageInfo.version,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import buy from '../../../website/common/script/ops/buy';
|
||||
import {
|
||||
BadRequest,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
|
||||
describe('shared.ops.buy', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser({
|
||||
items: {
|
||||
gear: {
|
||||
owned: {
|
||||
weapon_warrior_0: true,
|
||||
},
|
||||
equipped: {
|
||||
weapon_warrior_0: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
stats: { gp: 200 },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when key is not provided', (done) => {
|
||||
try {
|
||||
buy(user);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('missingKeyParam'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('recovers 15 hp', () => {
|
||||
user.stats.hp = 30;
|
||||
buy(user, {params: {key: 'potion'}});
|
||||
expect(user.stats.hp).to.eql(45);
|
||||
});
|
||||
|
||||
it('adds equipment to inventory', () => {
|
||||
user.stats.gp = 31;
|
||||
buy(user, {params: {key: 'armor_warrior_1'}});
|
||||
expect(user.items.gear.owned).to.eql({
|
||||
weapon_warrior_0: true,
|
||||
armor_warrior_1: true,
|
||||
eyewear_special_blackTopFrame: true,
|
||||
eyewear_special_blueTopFrame: true,
|
||||
eyewear_special_greenTopFrame: true,
|
||||
eyewear_special_pinkTopFrame: true,
|
||||
eyewear_special_redTopFrame: true,
|
||||
eyewear_special_whiteTopFrame: true,
|
||||
eyewear_special_yellowTopFrame: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
124
test/common/ops/buy/buy.js
Normal file
124
test/common/ops/buy/buy.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/* eslint-disable camelcase */
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/common.helper';
|
||||
import buy from '../../../../website/common/script/ops/buy';
|
||||
import {
|
||||
BadRequest,
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import content from '../../../../website/common/script/content/index';
|
||||
|
||||
describe('shared.ops.buy', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser({
|
||||
items: {
|
||||
gear: {
|
||||
owned: {
|
||||
weapon_warrior_0: true,
|
||||
},
|
||||
equipped: {
|
||||
weapon_warrior_0: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
stats: { gp: 200 },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when key is not provided', (done) => {
|
||||
try {
|
||||
buy(user);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('missingKeyParam'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('recovers 15 hp', () => {
|
||||
user.stats.hp = 30;
|
||||
buy(user, {params: {key: 'potion'}});
|
||||
expect(user.stats.hp).to.eql(45);
|
||||
});
|
||||
|
||||
it('adds equipment to inventory', () => {
|
||||
user.stats.gp = 31;
|
||||
|
||||
buy(user, {params: {key: 'armor_warrior_1'}});
|
||||
|
||||
expect(user.items.gear.owned).to.eql({
|
||||
weapon_warrior_0: true,
|
||||
armor_warrior_1: true,
|
||||
eyewear_special_blackTopFrame: true,
|
||||
eyewear_special_blueTopFrame: true,
|
||||
eyewear_special_greenTopFrame: true,
|
||||
eyewear_special_pinkTopFrame: true,
|
||||
eyewear_special_redTopFrame: true,
|
||||
eyewear_special_whiteTopFrame: true,
|
||||
eyewear_special_yellowTopFrame: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('buys Steampunk Accessories Set', () => {
|
||||
user.purchased.plan.consecutive.trinkets = 1;
|
||||
|
||||
buy(user, {
|
||||
params: {
|
||||
key: '301404',
|
||||
},
|
||||
type: 'mystery',
|
||||
});
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.items.gear.owned).to.have.property('weapon_warrior_0', true);
|
||||
expect(user.items.gear.owned).to.have.property('weapon_mystery_301404', true);
|
||||
expect(user.items.gear.owned).to.have.property('armor_mystery_301404', true);
|
||||
expect(user.items.gear.owned).to.have.property('head_mystery_301404', true);
|
||||
expect(user.items.gear.owned).to.have.property('eyewear_mystery_301404', true);
|
||||
});
|
||||
|
||||
it('buys a Quest scroll', () => {
|
||||
user.stats.gp = 205;
|
||||
|
||||
buy(user, {
|
||||
params: {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
type: 'quest',
|
||||
});
|
||||
|
||||
expect(user.items.quests).to.eql({dilatoryDistress1: 1});
|
||||
expect(user.stats.gp).to.equal(5);
|
||||
});
|
||||
|
||||
it('buys a special item', () => {
|
||||
user.stats.gp = 11;
|
||||
let item = content.special.thankyou;
|
||||
|
||||
let [data, message] = buy(user, {
|
||||
params: {
|
||||
key: 'thankyou',
|
||||
},
|
||||
type: 'special',
|
||||
});
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.thankyou).to.equal(1);
|
||||
expect(data).to.eql({
|
||||
items: user.items,
|
||||
stats: user.stats,
|
||||
});
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
});
|
||||
|
||||
it('allows for bulk purchases', () => {
|
||||
user.stats.hp = 30;
|
||||
buy(user, {params: {key: 'potion'}, quantity: 2});
|
||||
expect(user.stats.hp).to.eql(50);
|
||||
});
|
||||
});
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import count from '../../../website/common/script/count';
|
||||
import buyArmoire from '../../../website/common/script/ops/buyArmoire';
|
||||
import randomVal from '../../../website/common/script/libs/randomVal';
|
||||
import content from '../../../website/common/script/content/index';
|
||||
} from '../../../helpers/common.helper';
|
||||
import count from '../../../../website/common/script/count';
|
||||
import buyArmoire from '../../../../website/common/script/ops/buyArmoire';
|
||||
import randomVal from '../../../../website/common/script/libs/randomVal';
|
||||
import content from '../../../../website/common/script/content/index';
|
||||
import {
|
||||
NotAuthorized,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
|
||||
function getFullArmoire () {
|
||||
let fullArmoire = {};
|
||||
@@ -3,13 +3,13 @@
|
||||
import sinon from 'sinon'; // eslint-disable-line no-shadow
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import buyGear from '../../../website/common/script/ops/buyGear';
|
||||
import shared from '../../../website/common/script';
|
||||
} from '../../../helpers/common.helper';
|
||||
import buyGear from '../../../../website/common/script/ops/buyGear';
|
||||
import shared from '../../../../website/common/script';
|
||||
import {
|
||||
NotAuthorized,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
|
||||
describe('shared.ops.buyGear', () => {
|
||||
let user;
|
||||
@@ -1,12 +1,12 @@
|
||||
/* eslint-disable camelcase */
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import buyHealthPotion from '../../../website/common/script/ops/buyHealthPotion';
|
||||
} from '../../../helpers/common.helper';
|
||||
import buyHealthPotion from '../../../../website/common/script/ops/buyHealthPotion';
|
||||
import {
|
||||
NotAuthorized,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
|
||||
describe('shared.ops.buyHealthPotion', () => {
|
||||
let user;
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import buyMysterySet from '../../../website/common/script/ops/buyMysterySet';
|
||||
} from '../../../helpers/common.helper';
|
||||
import buyMysterySet from '../../../../website/common/script/ops/buyMysterySet';
|
||||
import {
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
|
||||
describe('shared.ops.buyMysterySet', () => {
|
||||
let user;
|
||||
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import buyQuest from '../../../website/common/script/ops/buyQuest';
|
||||
} from '../../../helpers/common.helper';
|
||||
import buyQuest from '../../../../website/common/script/ops/buyQuest';
|
||||
import {
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
|
||||
describe('shared.ops.buyQuest', () => {
|
||||
let user;
|
||||
@@ -1,14 +1,14 @@
|
||||
import buySpecialSpell from '../../../website/common/script/ops/buySpecialSpell';
|
||||
import buySpecialSpell from '../../../../website/common/script/ops/buySpecialSpell';
|
||||
import {
|
||||
BadRequest,
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import content from '../../../website/common/script/content/index';
|
||||
} from '../../../helpers/common.helper';
|
||||
import content from '../../../../website/common/script/content/index';
|
||||
|
||||
describe('shared.ops.buySpecialSpell', () => {
|
||||
let user;
|
||||
@@ -138,6 +138,7 @@ describe('shared.ops.purchase', () => {
|
||||
user.balance = userGemAmount;
|
||||
user.stats.gp = goldPoints;
|
||||
user.purchased.plan.gemsBought = 0;
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
});
|
||||
|
||||
it('purchases gems', () => {
|
||||
@@ -226,4 +227,39 @@ describe('shared.ops.purchase', () => {
|
||||
clock.restore();
|
||||
});
|
||||
});
|
||||
|
||||
context('bulk purchase', () => {
|
||||
let userGemAmount = 10;
|
||||
|
||||
before(() => {
|
||||
user.balance = userGemAmount;
|
||||
user.stats.gp = goldPoints;
|
||||
user.purchased.plan.gemsBought = 0;
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
});
|
||||
|
||||
it('makes bulk purchases of gems', () => {
|
||||
let [, message] = purchase(user, {
|
||||
params: {type: 'gems', key: 'gem'},
|
||||
quantity: 2,
|
||||
});
|
||||
|
||||
expect(message).to.equal(i18n.t('plusOneGem'));
|
||||
expect(user.balance).to.equal(userGemAmount + 0.50);
|
||||
expect(user.purchased.plan.gemsBought).to.equal(2);
|
||||
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate * 2);
|
||||
});
|
||||
|
||||
it('makes bulk purchases of eggs', () => {
|
||||
let type = 'eggs';
|
||||
let key = 'TigerCub';
|
||||
|
||||
purchase(user, {
|
||||
params: {type, key},
|
||||
quantity: 2,
|
||||
});
|
||||
|
||||
expect(user.items[type][key]).to.equal(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -172,7 +172,7 @@ export default {
|
||||
return Promise.resolve(error);
|
||||
}
|
||||
|
||||
this.$store.state.notificationStore.push({
|
||||
this.$store.dispatch('snackbars:add', {
|
||||
title: 'Habitica',
|
||||
text: error.response.data.message,
|
||||
type: 'error',
|
||||
@@ -191,6 +191,7 @@ export default {
|
||||
|
||||
const isApiCall = url.indexOf('api/v3') !== -1;
|
||||
const userV = response.data && response.data.userV;
|
||||
const isCron = url.indexOf('/api/v3/cron') === 0 && method === 'post';
|
||||
|
||||
if (this.isUserLoaded && isApiCall && userV) {
|
||||
const oldUserV = this.user._v;
|
||||
@@ -202,7 +203,6 @@ export default {
|
||||
// exclude chat seen requests because with real time chat they would be too many
|
||||
const isChatSeen = url.indexOf('/chat/seen') !== -1 && method === 'post';
|
||||
// exclude POST /api/v3/cron because the user is synced automatically after cron runs
|
||||
const isCron = url.indexOf('/api/v3/cron') === 0 && method === 'post';
|
||||
|
||||
// Something has changed on the user object that was not tracked here, sync the user
|
||||
if (userV - oldUserV > 1 && !isCron && !isChatSeen && !isUserSync && !isTasksSync) {
|
||||
@@ -213,6 +213,21 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the client is updated
|
||||
// const serverAppVersion = response.data.appVersion;
|
||||
// let serverAppVersionState = this.$store.state.serverAppVersion;
|
||||
// let deniedUpdate = this.$store.state.deniedUpdate;
|
||||
// if (isApiCall && !serverAppVersionState) {
|
||||
// this.$store.state.serverAppVersion = serverAppVersion;
|
||||
// } else if (isApiCall && serverAppVersionState !== serverAppVersion && !deniedUpdate || isCron) {
|
||||
// // For reload on cron
|
||||
// if (isCron || confirm(this.$t('habiticaHasUpdated'))) {
|
||||
// location.reload(true);
|
||||
// } else {
|
||||
// this.$store.state.deniedUpdate = true;
|
||||
// }
|
||||
// }
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
.promo_bundle_witchyFamiliars {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -142px 0px;
|
||||
width: 141px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_fall_customizations {
|
||||
.scene_positivity {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px 0px;
|
||||
width: 141px;
|
||||
height: 588px;
|
||||
}
|
||||
.scene_raking_leaves {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -284px 0px;
|
||||
width: 246px;
|
||||
height: 198px;
|
||||
width: 531px;
|
||||
height: 243px;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// import axios from 'axios';
|
||||
import bModal from 'bootstrap-vue/lib/components/modal';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import markdown from 'client/directives/markdown';
|
||||
@@ -38,6 +39,16 @@
|
||||
directives: {
|
||||
markdown,
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('show::modal', async (modalId) => {
|
||||
if (modalId !== 'new-stuff') return;
|
||||
// Request the lastest news, but not locally incase they don't refresh
|
||||
// let response = await axios.get('/static/new-stuff');
|
||||
});
|
||||
},
|
||||
destroyed () {
|
||||
this.$root.$off('show::modal');
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('hide::modal', 'new-stuff');
|
||||
|
||||
@@ -58,8 +58,11 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
.gem-lock(v-if='item.locked')
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 2
|
||||
.col-12.text-center
|
||||
button.btn.btn-secondary.purchase-all(v-if='!userOwnsSet("shirt", specialShirtKeys)', @click='unlock(`shirt.${specialShirtKeys.join(",shirt.")}`)') {{ $t('purchaseAll') }}
|
||||
.col-12.text-center(v-if='!userOwnsSet("shirt", specialShirtKeys)')
|
||||
.gem-lock
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 5
|
||||
button.btn.btn-secondary.purchase-all(@click='unlock(`shirt.${specialShirtKeys.join(",shirt.")}`)') {{ $t('purchaseAll') }}
|
||||
#skin.section.customize-section(v-if='activeTopPage === "skin"')
|
||||
.row.sub-menu.col-6.offset-3.text-center
|
||||
.col-6.offset-3.text-center.sub-menu-item(:class='{active: activeSubPage === "color"}')
|
||||
@@ -77,18 +80,18 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
.gem-lock(v-if='option.locked')
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 2
|
||||
.col-12.text-center
|
||||
button.btn.btn-secondary.purchase-all(v-if='!hideSet(set) && !userOwnsSet("skin", set.keys)', @click='unlock(`skin.${set.keys.join(",skin.")}`)') {{ $t('purchaseAll') }}
|
||||
.col-12.text-center(v-if='!hideSet(set) && !userOwnsSet("skin", set.keys)')
|
||||
.gem-lock
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 5
|
||||
button.btn.btn-secondary.purchase-all(@click='unlock(`skin.${set.keys.join(",skin.")}`)') {{ $t('purchaseAll') }}
|
||||
#hair.section.customize-section(v-if='activeTopPage === "hair"')
|
||||
.row.sub-menu.text-center
|
||||
.col-3.offset-1.text-center.sub-menu-item(@click='changeSubPage("color")', :class='{active: activeSubPage === "color"}')
|
||||
.row.col-12.sub-menu.text-center
|
||||
.col-3.text-center.sub-menu-item(@click='changeSubPage("color")', :class='{active: activeSubPage === "color"}')
|
||||
strong(v-once) {{$t('color')}}
|
||||
.col-4.text-center.sub-menu-item(@click='changeSubPage("bangs")', :class='{active: activeSubPage === "bangs"}')
|
||||
.col-3.text-center.sub-menu-item(@click='changeSubPage("bangs")', :class='{active: activeSubPage === "bangs"}')
|
||||
strong(v-once) {{$t('bangs')}}
|
||||
.col-3.text-center.sub-menu-item(@click='changeSubPage("ponytail")', :class='{active: activeSubPage === "ponytail"}')
|
||||
strong(v-once) {{$t('ponytail')}}
|
||||
.row.sub-menu.text-center
|
||||
.col-3.offset-3.text-center.sub-menu-item(@click='changeSubPage("style")', :class='{active: activeSubPage === "style"}', v-if='editing')
|
||||
.col-3.text-center.sub-menu-item(@click='changeSubPage("style")', :class='{active: activeSubPage === "style"}', v-if='editing')
|
||||
strong(v-once) {{$t('style')}}
|
||||
.col-3.text-center.sub-menu-item(@click='changeSubPage("facialhair")', :class='{active: activeSubPage === "facialhair"}', v-if='editing')
|
||||
strong(v-once) {{$t('facialhair')}}
|
||||
@@ -104,8 +107,11 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
.gem-lock(v-if='option.locked')
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 2
|
||||
.col-12.text-center
|
||||
button.btn.btn-secondary.purchase-all(v-if='!hideSet(set) && !userOwnsSet("hair", set.keys, "color")', @click='unlock(`hair.color.${set.keys.join(",hair.color.")}`)') {{ $t('purchaseAll') }}
|
||||
.col-12.text-center(v-if='!hideSet(set) && !userOwnsSet("hair", set.keys, "color")')
|
||||
.gem-lock
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 5
|
||||
button.btn.btn-secondary.purchase-all(@click='unlock(`hair.color.${set.keys.join(",hair.color.")}`)') {{ $t('purchaseAll') }}
|
||||
#style.row(v-if='activeSubPage === "style"')
|
||||
.col-12.customize-options(v-if='editing')
|
||||
.head_0.option(@click='set({"preferences.hair.base": 0})', :class="[{ active: user.preferences.hair.base === 0 }, 'hair_base_0_' + user.preferences.hair.color]")
|
||||
@@ -115,8 +121,11 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
.gem-lock(v-if='option.locked')
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 2
|
||||
.col-12.text-center
|
||||
button.btn.btn-secondary.purchase-all(v-if='!userOwnsSet("hair", baseHair3Keys, "base")', @click='unlock(`hair.base.${baseHair3Keys.join(",hair.base.")}`)') {{ $t('purchaseAll') }}
|
||||
.col-12.text-center(v-if='!userOwnsSet("hair", baseHair3Keys, "base")')
|
||||
.gem-lock
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 5
|
||||
button.btn.btn-secondary.purchase-all(@click='unlock(`hair.base.${baseHair3Keys.join(",hair.base.")}`)') {{ $t('purchaseAll') }}
|
||||
.col-12.customize-options(v-if='editing')
|
||||
.option(v-for='option in baseHair4',
|
||||
:class='{active: option.active, locked: option.locked}')
|
||||
@@ -124,16 +133,11 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
.gem-lock(v-if='option.locked')
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 2
|
||||
.col-12.text-center
|
||||
button.btn.btn-secondary.purchase-all(v-if='!userOwnsSet("hair", baseHair4Keys, "base")', @click='unlock(`hair.base.${baseHair4Keys.join(",hair.base.")}`)') {{ $t('purchaseAll') }}
|
||||
#bangs.row(v-if='activeSubPage === "bangs"')
|
||||
.col-12.customize-options
|
||||
.head_0.option(@click='set({"preferences.hair.bangs": 0})',
|
||||
:class="[{ active: user.preferences.hair.bangs === 0 }, 'hair_bangs_0_' + user.preferences.hair.color]")
|
||||
.option(v-for='option in ["1", "2", "3", "4"]',
|
||||
:class='{active: user.preferences.hair.bangs === option}')
|
||||
.bangs.sprite.customize-option(:class="`hair_bangs_${option}_${user.preferences.hair.color}`", @click='set({"preferences.hair.bangs": option})')
|
||||
#base-hair.row(v-if='activeSubPage === "ponytail"')
|
||||
.col-12.text-center(v-if='!userOwnsSet("hair", baseHair4Keys, "base")')
|
||||
.gem-lock
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 5
|
||||
button.btn.btn-secondary.purchase-all(@click='unlock(`hair.base.${baseHair4Keys.join(",hair.base.")}`)') {{ $t('purchaseAll') }}
|
||||
.col-12.customize-options
|
||||
.head_0.option(@click='set({"preferences.hair.base": 0})', :class="[{ active: user.preferences.hair.base === 0 }, 'hair_base_0_' + user.preferences.hair.color]")
|
||||
.option(v-for='option in baseHair1',
|
||||
@@ -146,8 +150,18 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
.gem-lock(v-if='option.locked')
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 2
|
||||
.col-12.text-center
|
||||
button.btn.btn-secondary.purchase-all(v-if='!userOwnsSet("hair", baseHair2Keys, "base")', @click='unlock(`hair.base.${baseHair2Keys.join(",hair.base.")}`)') {{ $t('purchaseAll') }}
|
||||
.col-12.text-center(v-if='!userOwnsSet("hair", baseHair2Keys, "base")')
|
||||
.gem-lock
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 5
|
||||
button.btn.btn-secondary.purchase-all(@click='unlock(`hair.base.${baseHair2Keys.join(",hair.base.")}`)') {{ $t('purchaseAll') }}
|
||||
#bangs.row(v-if='activeSubPage === "bangs"')
|
||||
.col-12.customize-options
|
||||
.head_0.option(@click='set({"preferences.hair.bangs": 0})',
|
||||
:class="[{ active: user.preferences.hair.bangs === 0 }, 'hair_bangs_0_' + user.preferences.hair.color]")
|
||||
.option(v-for='option in ["1", "2", "3", "4"]',
|
||||
:class='{active: user.preferences.hair.bangs === option}')
|
||||
.bangs.sprite.customize-option(:class="`hair_bangs_${option}_${user.preferences.hair.color}`", @click='set({"preferences.hair.bangs": option})')
|
||||
#facialhair.row(v-if='activeSubPage === "facialhair"')
|
||||
.col-12.customize-options(v-if='editing')
|
||||
.head_0.option(@click='set({"preferences.hair.beard": 0})', :class="[{ active: user.preferences.hair.beard === 0 }, 'hair_base_0_' + user.preferences.hair.color]")
|
||||
@@ -157,8 +171,11 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
.gem-lock(v-if='option.locked')
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 2
|
||||
.col-12.text-center
|
||||
button.btn.btn-secondary.purchase-all(v-if='!userOwnsSet("hair", baseHair5Keys, "beard")', @click='unlock(`hair.beard.${baseHair5Keys.join(",hair.beard.")}`)') {{ $t('purchaseAll') }}
|
||||
.col-12.text-center(v-if='!userOwnsSet("hair", baseHair5Keys, "beard")')
|
||||
.gem-lock
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 5
|
||||
button.btn.btn-secondary.purchase-all(@click='unlock(`hair.beard.${baseHair5Keys.join(",hair.beard.")}`)') {{ $t('purchaseAll') }}
|
||||
.col-12.customize-options(v-if='editing')
|
||||
.option(v-for='option in baseHair6',
|
||||
:class='{active: option.active, locked: option.locked}')
|
||||
@@ -166,8 +183,11 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
.gem-lock(v-if='option.locked')
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 2
|
||||
.col-12.text-center
|
||||
button.btn.btn-secondary.purchase-all(v-if='!userOwnsSet("hair", baseHair6Keys, "mustache")', @click='unlock(`hair.mustache.${baseHair6Keys.join(",hair.mustache.")}`)') {{ $t('purchaseAll') }}
|
||||
.col-12.text-center(v-if='!userOwnsSet("hair", baseHair6Keys, "mustache")')
|
||||
.gem-lock
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 5
|
||||
button.btn.btn-secondary.purchase-all(@click='unlock(`hair.mustache.${baseHair6Keys.join(",hair.mustache.")}`)') {{ $t('purchaseAll') }}
|
||||
#extra.section.container.customize-section(v-if='activeTopPage === "extra"')
|
||||
.row.sub-menu
|
||||
.col-3.offset-1.text-center.sub-menu-item(@click='changeSubPage("glasses")', :class='{active: activeSubPage === "glasses"}')
|
||||
@@ -191,8 +211,11 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
.gem-lock(v-if='option.locked')
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 2
|
||||
.col-12.text-center
|
||||
button.btn.btn-secondary.purchase-all(v-if='!animalEarsOwned', @click='unlock(animalEarsUnlockString)') {{ $t('purchaseAll') }}
|
||||
.col-12.text-center(v-if='!animalEarsOwned')
|
||||
.gem-lock
|
||||
.svg-icon.gem(v-html='icons.gem')
|
||||
span 5
|
||||
button.btn.btn-secondary.purchase-all(@click='unlock(animalEarsUnlockString)') {{ $t('purchaseAll') }}
|
||||
#wheelchairs.row(v-if='activeSubPage === "wheelchair"')
|
||||
.col-12.customize-options
|
||||
.option(@click='set({"preferences.chair": "none"})', :class='{active: user.preferences.chair === "none"}')
|
||||
@@ -531,28 +554,35 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
margin-bottom: .5em;
|
||||
margin-right: .5em;
|
||||
|
||||
.gem-lock {
|
||||
.svg-icon {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #24cc8f;
|
||||
font-weight: bold;
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.svg-icon, span {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.sprite.customize-option {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.text-center .gem-lock {
|
||||
display: inline-block;
|
||||
margin-right: 1em;
|
||||
margin-bottom: 1.6em;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.gem-lock {
|
||||
.svg-icon {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #24cc8f;
|
||||
font-weight: bold;
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.svg-icon, span {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.option.active {
|
||||
border: 4px solid $purple-200;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -42,16 +42,11 @@ div.item-with-icon.item-notifications.dropdown
|
||||
span {{message.name}}
|
||||
span.clear-button(@click='clearMessages(message.key)', :popover="$t('clear')",
|
||||
popover-placement='right', popover-trigger='mouseenter', popover-append-to-body='true') Clear
|
||||
a.dropdown-item(v-for='(notification, index) in user.groupNotifications', @click='viewGroupApprovalNotification(notification, index, true)')
|
||||
a.dropdown-item(v-for='notification in groupNotifications', :key='notification.id')
|
||||
span(:class="groupApprovalNotificationIcon(notification)")
|
||||
span
|
||||
| {{notification.data.message}}
|
||||
a.dropdown-item(@click='viewGroupApprovalNotification(notification, $index)',
|
||||
:popover="$t('clear')",
|
||||
popover-placement='right',
|
||||
popover-trigger='mouseenter',
|
||||
popover-append-to-body='true')
|
||||
span.glyphicon.glyphicon-remove-circle
|
||||
span {{notification.data.message}}
|
||||
span.clear-button(@click='viewGroupApprovalNotification(notification)', :popover="$t('clear')",
|
||||
popover-placement='right', popover-trigger='mouseenter', popover-append-to-body='true') Clear
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@@ -159,6 +154,7 @@ div.item-with-icon.item-notifications.dropdown
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import map from 'lodash/map';
|
||||
|
||||
@@ -194,6 +190,9 @@ export default {
|
||||
}
|
||||
return userNewMessages;
|
||||
},
|
||||
groupNotifications () {
|
||||
return this.$store.state.groupNotifications;
|
||||
},
|
||||
notificationsCount () {
|
||||
let count = 0;
|
||||
|
||||
@@ -217,6 +216,8 @@ export default {
|
||||
count += Object.keys(this.userNewMessages).length;
|
||||
}
|
||||
|
||||
count += this.groupNotifications.length;
|
||||
|
||||
return count;
|
||||
},
|
||||
},
|
||||
@@ -236,8 +237,8 @@ export default {
|
||||
return unallocatedValue;
|
||||
} else if (!isEmpty(user.newMessages)) {
|
||||
return messageValue;
|
||||
} else if (!isEmpty(user.groupNotifications)) {
|
||||
let groupNotificationTypes = map(user.groupNotifications, 'type');
|
||||
} else if (!isEmpty(this.groupNotifications)) {
|
||||
let groupNotificationTypes = map(this.groupNotifications, 'type');
|
||||
if (groupNotificationTypes.indexOf('GROUP_TASK_APPROVAL') !== -1) {
|
||||
return groupApprovalRequested;
|
||||
} else if (groupNotificationTypes.indexOf('GROUP_TASK_APPROVED') !== -1) {
|
||||
@@ -303,11 +304,14 @@ export default {
|
||||
hasNoNotifications () {
|
||||
return this.selectNotificationValue(false, false, false, false, false, true, false, false);
|
||||
},
|
||||
viewGroupApprovalNotification (notification, index, navigate) {
|
||||
// @TODO: USe notifications: User.readNotification(notification.id);
|
||||
this.user.groupNotifications.splice(index, 1);
|
||||
return navigate; // @TODO: remove
|
||||
// @TODO: this.$router.go if (navigate) go('options.social.guilds.detail', {gid: notification.data.groupId});
|
||||
viewGroupApprovalNotification (notification) {
|
||||
this.$store.state.groupNotifications = this.groupNotifications.filter(groupNotif => {
|
||||
return groupNotif.id !== notification.id;
|
||||
});
|
||||
|
||||
axios.post('/api/v3/notifications/read', {
|
||||
notificationIds: [notification.id],
|
||||
});
|
||||
},
|
||||
groupApprovalNotificationIcon (notification) {
|
||||
if (notification.type === 'GROUP_TASK_APPROVAL') {
|
||||
|
||||
@@ -276,8 +276,8 @@ export default {
|
||||
if (after === before || after === false) return;
|
||||
this.$root.$emit('show::modal', 'armoire-empty');
|
||||
},
|
||||
questCompleted (after) {
|
||||
if (!after) return;
|
||||
questCompleted () {
|
||||
if (!this.questCompleted) return;
|
||||
this.$root.$emit('show::modal', 'quest-completed');
|
||||
},
|
||||
invitedToQuest (after) {
|
||||
@@ -291,6 +291,26 @@ export default {
|
||||
this.$store.dispatch('user:fetch'),
|
||||
this.$store.dispatch('tasks:fetchUserTasks'),
|
||||
]).then(() => {
|
||||
this.checkUserAchievements();
|
||||
|
||||
// @TODO: This is a timeout to ensure dom is loaded
|
||||
window.setTimeout(() => {
|
||||
this.initTour();
|
||||
if (this.user.flags.tour.intro === this.TOUR_END || !this.user.flags.welcomed) return;
|
||||
this.goto('intro', 0);
|
||||
}, 2000);
|
||||
|
||||
this.runYesterDailies();
|
||||
|
||||
// Do not remove the event listener as it's live for the entire app lifetime
|
||||
document.addEventListener('mousemove', () => this.checkNextCron());
|
||||
document.addEventListener('touchstart', () => this.checkNextCron());
|
||||
document.addEventListener('mousedown', () => this.checkNextCron());
|
||||
document.addEventListener('keydown', () => this.checkNextCron());
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
checkUserAchievements () {
|
||||
// List of prompts for user on changes. Sounds like we may need a refactor here, but it is clean for now
|
||||
if (this.user.flags.newStuff) {
|
||||
this.$root.$emit('show::modal', 'new-stuff');
|
||||
@@ -313,24 +333,7 @@ export default {
|
||||
if (this.userClassSelect) {
|
||||
this.$root.$emit('show::modal', 'choose-class');
|
||||
}
|
||||
|
||||
// @TODO: This is a timeout to ensure dom is loaded
|
||||
window.setTimeout(() => {
|
||||
this.initTour();
|
||||
if (this.user.flags.tour.intro === this.TOUR_END || !this.user.flags.welcomed) return;
|
||||
this.goto('intro', 0);
|
||||
}, 2000);
|
||||
|
||||
this.runYesterDailies();
|
||||
|
||||
// Do not remove the event listener as it's live for the entire app lifetime
|
||||
document.addEventListener('mousemove', () => this.checkNextCron());
|
||||
document.addEventListener('touchstart', () => this.checkNextCron());
|
||||
document.addEventListener('mousedown', () => this.checkNextCron());
|
||||
document.addEventListener('keydown', () => this.checkNextCron());
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
showLevelUpNotifications (newlevel) {
|
||||
this.lvl();
|
||||
this.playSound('Level_Up');
|
||||
@@ -417,8 +420,7 @@ export default {
|
||||
this.scheduleNextCron();
|
||||
},
|
||||
transferGroupNotification (notification) {
|
||||
if (!this.user.groupNotifications) this.user.groupNotifications = [];
|
||||
this.user.groupNotifications.push(notification);
|
||||
this.$store.state.groupNotifications.push(notification);
|
||||
},
|
||||
async handleUserNotifications (after) {
|
||||
if (!after || after.length === 0 || !Array.isArray(after)) return;
|
||||
@@ -426,7 +428,7 @@ export default {
|
||||
let notificationsToRead = [];
|
||||
let scoreTaskNotification = [];
|
||||
|
||||
this.user.groupNotifications = []; // Flush group notifictions
|
||||
this.$store.state.groupNotifications = []; // Flush group notifictions
|
||||
|
||||
after.forEach((notification) => {
|
||||
if (this.lastShownNotifications.indexOf(notification.id) !== -1) {
|
||||
@@ -578,6 +580,8 @@ export default {
|
||||
}
|
||||
|
||||
this.user.notifications = []; // reset the notifications
|
||||
|
||||
this.checkUserAchievements();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
b-modal#amazon-payment(title="Amazon", :hide-footer="true", size='md')
|
||||
h2.text-center Continue with Amazon
|
||||
#AmazonPayButton
|
||||
| {{amazonPayments}}
|
||||
| {{amazonLoggedIn}}
|
||||
#AmazonPayWallet(v-if="amazonLoggedIn", style="width: 400px; height: 228px;")
|
||||
#AmazonPayRecurring(v-if="amazonLoggedIn && amazonPayments.type === 'subscription'",
|
||||
style="width: 400px; height: 140px;")
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
.row.standard-page
|
||||
.col-6
|
||||
h2 {{ $t('API') }}
|
||||
small {{ $t('APIText') }}
|
||||
p {{ $t('APIText') }}
|
||||
|
||||
.section
|
||||
h6 {{ $t('userId') }}
|
||||
pre.prettyprint {{user.id}}
|
||||
h6 {{ $t('APIToken') }}
|
||||
pre.prettyprint {{apiToken}}
|
||||
small(v-html='$t("APITokenWarning", { hrefTechAssistanceEmail })')
|
||||
.d-flex.align-items-center.mb-3
|
||||
button.btn.btn-secondary(
|
||||
@click="showApiToken = !showApiToken"
|
||||
) {{ $t(`${showApiToken ? 'hide' : 'show'}APIToken`) }}
|
||||
pre.prettyprint.ml-4.mb-0(v-if="showApiToken") {{apiToken}}
|
||||
p(v-html='$t("APITokenWarning", { hrefTechAssistanceEmail })')
|
||||
|
||||
.section
|
||||
h3 {{ $t('thirdPartyApps') }}
|
||||
@@ -78,6 +82,7 @@ export default {
|
||||
url: '',
|
||||
},
|
||||
hrefTechAssistanceEmail: `<a href="mailto:${TECH_ASSISTANCE_EMAIL}">${TECH_ASSISTANCE_EMAIL}</a>`,
|
||||
showApiToken: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -115,7 +115,8 @@
|
||||
div
|
||||
ul.list-inline
|
||||
li(v-for='network in SOCIAL_AUTH_NETWORKS')
|
||||
button.btn.btn-primary(v-if='!user.auth[network.key].id', @click='socialLogin(network.key, user)') {{ $t('registerWithSocial', {network: network.name}) }}
|
||||
// @TODO this is broken
|
||||
button.btn.btn-primary(v-if='!user.auth[network.key].id', @click='socialAuth(network.key, user)') {{ $t('registerWithSocial', {network: network.name}) }}
|
||||
button.btn.btn-primary(disabled='disabled', v-if='!hasBackupAuthOption(network.key) && user.auth[network.key].id') {{ $t('registeredWithSocial', {network: network.name}) }}
|
||||
button.btn.btn-danger(@click='deleteSocialAuth(network.key)', v-if='hasBackupAuthOption(network.key) && user.auth[network.key].id') {{ $t('detachSocial', {network: network.name}) }}
|
||||
hr
|
||||
@@ -377,7 +378,7 @@ export default {
|
||||
auth,
|
||||
});
|
||||
|
||||
this.$router.go('/tasks');
|
||||
window.location.href = '/';
|
||||
},
|
||||
async changeClassForUser (confirmationNeeded) {
|
||||
if (confirmationNeeded && !confirm(this.$t('changeClassConfirmCost'))) return;
|
||||
|
||||
@@ -41,25 +41,32 @@
|
||||
:item="item"
|
||||
)
|
||||
|
||||
div(:class="{'notEnough': !this.enoughCurrency(getPriceClass(), item.value)}")
|
||||
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons[getPriceClass()]")
|
||||
span.value(:class="getPriceClass()") {{ item.value }}
|
||||
.purchase-amount(:class="{'notEnough': !this.enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}")
|
||||
.how-many-to-buy(v-if='item.purchaseType !== "gear"')
|
||||
strong {{ $t('howManyToBuy') }}
|
||||
div(v-if='item.purchaseType !== "gear"')
|
||||
.box
|
||||
input(type='number', min='0', v-model='selectedAmountToBuy')
|
||||
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons[getPriceClass()]")
|
||||
span.value(:class="getPriceClass()") {{ item.value }}
|
||||
|
||||
.gems-left(v-if='item.key === "gem"')
|
||||
strong(v-if='gemsLeft > 0') {{ gemsLeft }} {{ $t('gemsRemaining') }}
|
||||
strong(v-if='gemsLeft === 0') {{ $t('maxBuyGems') }}
|
||||
|
||||
div(v-if='attemptingToPurchaseMoreGemsThanAreLeft')
|
||||
| {{$t('notEnoughGemsToBuy')}}
|
||||
|
||||
button.btn.btn-primary(
|
||||
@click="purchaseGems()",
|
||||
v-if="getPriceClass() === 'gems' && !this.enoughCurrency(getPriceClass(), item.value)"
|
||||
v-if="getPriceClass() === 'gems' && !this.enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)"
|
||||
) {{ $t('purchaseGems') }}
|
||||
|
||||
button.btn.btn-primary(
|
||||
@click="buyItem()",
|
||||
v-else,
|
||||
:disabled='item.key === "gem" && gemsLeft === 0',
|
||||
:class="{'notEnough': !preventHealthPotion || !this.enoughCurrency(getPriceClass(), item.value)}"
|
||||
:disabled='item.key === "gem" && gemsLeft === 0 || attemptingToPurchaseMoreGemsThanAreLeft',
|
||||
:class="{'notEnough': !preventHealthPotion || !this.enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}"
|
||||
) {{ $t('buyNow') }}
|
||||
|
||||
div.limitedTime(v-if="item.event")
|
||||
@@ -101,6 +108,37 @@
|
||||
width: 282px;
|
||||
}
|
||||
|
||||
.purchase-amount {
|
||||
margin-top: 24px;
|
||||
|
||||
.how-many-to-buy {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: inline-block;
|
||||
width: 74px;
|
||||
height: 40px;
|
||||
border-radius: 2px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
margin-right: 24px;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
input::-webkit-contacts-auto-fill-button {
|
||||
visibility: hidden;
|
||||
display: none !important;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-text {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
@@ -217,6 +255,8 @@
|
||||
|
||||
<script>
|
||||
import bModal from 'bootstrap-vue/lib/components/modal';
|
||||
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
|
||||
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import spellsMixin from 'client/mixins/spells';
|
||||
import planGemLimits from 'common/script/libs/planGemLimits';
|
||||
@@ -248,6 +288,8 @@
|
||||
mixins: [currencyMixin, notifications, spellsMixin, buyMixin],
|
||||
components: {
|
||||
bModal,
|
||||
bDropdown,
|
||||
bDropdownItem,
|
||||
BalanceInfo,
|
||||
EquipmentAttributesGrid,
|
||||
Item,
|
||||
@@ -264,6 +306,7 @@
|
||||
clock: svgClock,
|
||||
}),
|
||||
|
||||
selectedAmountToBuy: 1,
|
||||
isPinned: false,
|
||||
};
|
||||
},
|
||||
@@ -306,10 +349,15 @@
|
||||
if (!this.user.purchased.plan) return 0;
|
||||
return planGemLimits.convCap + this.user.purchased.plan.consecutive.gemCapExtra - this.user.purchased.plan.gemsBought;
|
||||
},
|
||||
attemptingToPurchaseMoreGemsThanAreLeft () {
|
||||
if (this.item && this.item.key && this.item.key === 'gem' && this.selectedAmountToBuy > this.gemsLeft) return true;
|
||||
return false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
item: function itemChanged () {
|
||||
this.isPinned = this.item && this.item.pinned;
|
||||
this.selectedAmountToBuy = 1;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -320,7 +368,7 @@
|
||||
if (this.item.cast) {
|
||||
this.castStart(this.item);
|
||||
} else if (this.genericPurchase) {
|
||||
this.makeGenericPurchase(this.item);
|
||||
this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
|
||||
this.purchased(this.item.text);
|
||||
}
|
||||
|
||||
|
||||
@@ -355,7 +355,7 @@
|
||||
|
||||
}
|
||||
|
||||
.gems-left {
|
||||
.market .gems-left {
|
||||
position: absolute;
|
||||
right: -.5em;
|
||||
top: -.5em;
|
||||
@@ -739,11 +739,6 @@ export default {
|
||||
}
|
||||
},
|
||||
itemSelected (item) {
|
||||
if (item.purchaseType !== 'gear' && this.$store.state.recentlyPurchased[item.key]) {
|
||||
this.makeGenericPurchase(item);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$root.$emit('buyModal::showItem', item);
|
||||
},
|
||||
featuredItemSelected (item) {
|
||||
|
||||
@@ -18,20 +18,23 @@
|
||||
div.inner-content
|
||||
questDialogContent(:item="item")
|
||||
|
||||
div
|
||||
.purchase-amount
|
||||
.how-many-to-buy
|
||||
strong {{ $t('howManyToBuy') }}
|
||||
.box
|
||||
input(type='number', min='0', v-model='selectedAmountToBuy')
|
||||
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="(priceType === 'gems') ? icons.gem : icons.gold")
|
||||
span.value(:class="priceType") {{ item.value }}
|
||||
|
||||
button.btn.btn-primary(
|
||||
@click="purchaseGems()",
|
||||
v-if="priceType === 'gems' && !this.enoughCurrency(priceType, item.value)"
|
||||
v-if="priceType === 'gems' && !this.enoughCurrency(priceType, item.value * selectedAmountToBuy)"
|
||||
) {{ $t('purchaseGems') }}
|
||||
|
||||
|
||||
button.btn.btn-primary(
|
||||
@click="buyItem()",
|
||||
v-else,
|
||||
:class="{'notEnough': !this.enoughCurrency(priceType, item.value)}"
|
||||
:class="{'notEnough': !this.enoughCurrency(priceType, item.value * selectedAmountToBuy)}"
|
||||
) {{ $t('buyNow') }}
|
||||
|
||||
div.right-sidebar(v-if="item.drop")
|
||||
@@ -52,9 +55,12 @@
|
||||
#buy-quest-modal {
|
||||
@include centeredModal();
|
||||
|
||||
.modal-dialog {
|
||||
margin-top: 25em;
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
max-height: 80vh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
@@ -167,6 +173,37 @@
|
||||
pointer-events: none;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.purchase-amount {
|
||||
margin-top: 24px;
|
||||
|
||||
.how-many-to-buy {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: inline-block;
|
||||
width: 74px;
|
||||
height: 40px;
|
||||
border-radius: 2px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
margin-right: 24px;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
input::-webkit-contacts-auto-fill-button {
|
||||
visibility: hidden;
|
||||
display: none !important;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -210,6 +247,7 @@
|
||||
}),
|
||||
|
||||
isPinned: false,
|
||||
selectedAmountToBuy: 1,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -238,10 +276,11 @@
|
||||
},
|
||||
methods: {
|
||||
onChange ($event) {
|
||||
this.selectedAmountToBuy = 1;
|
||||
this.$emit('change', $event);
|
||||
},
|
||||
buyItem () {
|
||||
this.makeGenericPurchase(this.item, 'buyQuestModal');
|
||||
this.makeGenericPurchase(this.item, 'buyQuestModal', this.selectedAmountToBuy);
|
||||
this.purchased(this.item.text);
|
||||
this.hideDialog();
|
||||
},
|
||||
|
||||
@@ -477,11 +477,6 @@ export default {
|
||||
|
||||
this.selectedItemToBuy = item;
|
||||
|
||||
if (this.$store.state.recentlyPurchased[item.key]) {
|
||||
this.makeGenericPurchase(item);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$root.$emit('show::modal', 'buy-quest-modal');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -40,6 +40,10 @@
|
||||
span.rectangle
|
||||
span.text(v-once) {{ $t('featuredset', { name: seasonal.featured.text }) }}
|
||||
span.rectangle
|
||||
div.featured-label.with-border(v-else)
|
||||
span.rectangle
|
||||
span.text(v-once) {{ $t('featuredItems') }}
|
||||
span.rectangle
|
||||
|
||||
div.items.margin-center
|
||||
shopItem(
|
||||
@@ -264,6 +268,11 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.margin-center {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import {mapState} from 'client/libs/store';
|
||||
@@ -493,12 +502,6 @@
|
||||
},
|
||||
itemSelected (item) {
|
||||
if (item.locked) return;
|
||||
|
||||
if (this.$store.state.recentlyPurchased[item.key]) {
|
||||
this.makeGenericPurchase(item);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$root.$emit('buyModal::showItem', item);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -135,9 +135,7 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
show () {
|
||||
setTimeout(() => {
|
||||
this.$store.state.notificationStore.splice(0, 1);
|
||||
}, 1000);
|
||||
this.$store.dispatch('snackbars:remove', this.notification);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template lang="pug">
|
||||
.notifications
|
||||
div(v-for='notification in notifications')
|
||||
div(v-for='notification in notifications', :key='notification.uuid')
|
||||
notification(:notification='notification')
|
||||
</template>
|
||||
|
||||
|
||||
@@ -4,23 +4,13 @@
|
||||
.align-self-center.right-margin(:class='baileyClass')
|
||||
.media-body
|
||||
h1.align-self-center(v-markdown='$t("newStuff")')
|
||||
h2 10/16/2017 - TASK PAGE UPDATES AND FINAL MASTERCLASSER QUEST LINE
|
||||
h2 10/20/2017 - USE CASE SPOTLIGHT: CULTIVATING POSITIVITY
|
||||
hr
|
||||
h3 Requested Updates to Task Page
|
||||
p(v-markdown='"We\'re happy to announce that we\'ve pushed some of the most-requested updates from our [feedback form](https://docs.google.com/forms/d/e/1FAIpQLScpeCeoTbmNPotBvEWtAFeVlD6g83KEN_YlwD_GHB4yHJbZww/viewform): the return of quick-add tasks above each column, a more compact header and task page footer, and the ability to refresh the page using the new sync button in the upper-right! Thank you very much to everyone who left us thoughtful comments. We\'re still working on implementing some of the advanced options like push to top/bottom, the ability to batch-add tasks, and manual task attribute allocation, but you\'ll be seeing them soon."')
|
||||
p We know that this has been a disruptive experience for many of you, and we're very sorry about that. This was a much tougher rollout than we anticipated, and we've been so grateful for your patience, your thoughtful feedback, and your supportive messages. Our small team has been working nights and weekends since July to bring you the upgrade and fix the bugs that resulted, but we only have two full-time web developers and a small handful of part-timers, so things don't always move as quickly as we'd like. It's been beyond exhausting, but we are determined to get everything running smoothly again.
|
||||
p The good news is that a large part of this redesign was rewriting the entirety of the front-end code from scratch, so we've managed to completely get rid of the outdated, cobbled-together code that was preventing us from adding new features or quickly fixing bugs. Being trapped between the old and new code was particularly rough for us, because it meant that all our development was frozen and we were cut off from our awesome open-source contributors. Now that we've launched, we're able to actually move forward without being paralyzed by the constraints of the old site. Our new design has also enabled us to build a foundation for features that people have been requesting from the beginning, like batch-buying items, new Group Plan upgrades, quest enhancements, and more. This has been a tough time of transition, but we can't wait to show you everything that we have planned.
|
||||
p(v-markdown='"Thank you again for sticking with us -- and if you have feedback about the redesign, please let us know via our [feedback form](https://docs.google.com/forms/d/e/1FAIpQLScpeCeoTbmNPotBvEWtAFeVlD6g83KEN_YlwD_GHB4yHJbZww/viewform)! We\'ve been reading every single comment that you send in and incorporating your requests into our roadmap going forward. We\'re incredibly lucky to have such passionate users -- you guys mean the world to us."')
|
||||
p Look out for more updates soon!
|
||||
.media
|
||||
.media-body
|
||||
h3 Mystery of the Masterclassers Quest Line
|
||||
p(v-markdown='"There\'s a new set of gold-purchasable quests available in the [Quest Shop](https://habitica.com/shops/quests): the Mystery of the Masterclassers Quest-Line! After some of Habitica\'s denizens are possessed by mysterious and malevolent objects, the four Masterclassers have come to you for aid in their quest to find and defeat the enigmatic foe behind these dire deeds. Can you help them save Habitica? If you can defeat the most powerful foe in the game so far, you\'ll earn the exclusive Aether Equipment Set... and a rare Mount like none other."')
|
||||
.quest_lostMasterclasser4.left-margin
|
||||
p The Mystery of the Masterclassers is the culmination of the story told in the gold-purchasable quests, so you need to complete all four of those previous quest-lines in order to unlock it. None of the gold-purchasable questlines are limited-edition, so you have plenty of time to save up!
|
||||
.small by SabreCat, Beffymaroo, and Lemoness
|
||||
.small Writing by Lemoness
|
||||
.small Art by AnnDeLune, Beffymaroo, Katy133, Tuqjoi, Kiwibot, stefalupagus, and tricksy.fox
|
||||
h3 Use Case Spotlight on Positivity
|
||||
p(v-markdown='"This month\'s [Use Case Spotlight](https://habitica.wordpress.com/2017/10/17/use-case-spotlight-cultivating-positivity/) is about Cultivating Positivity! It features a number of great suggestions submitted by Habiticans in the [Use Case Spotlights Guild](https://habitica.com/groups/guild/1d3a10bf-60aa-4806-a38b-82d1084a59e6). We hope it helps any of you who might be looking to build and maintain a more positive outlook!"')
|
||||
p Plus, we're collecting user submissions for the next spotlight! How do you use Habitica to manage Money Matters? We’ll be featuring player-submitted examples in Use Case Spotlights on the Habitica Blog next month, so post your suggestions in the Use Case Spotlight Guild now. We look forward to learning more about how you use Habitica to improve your life and get things done!
|
||||
.small by Beffymaroo
|
||||
.scene_positivity.center-block
|
||||
br
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template lang="pug">
|
||||
.tasks-column(:class='type')
|
||||
b-modal(ref="editTaskModal")
|
||||
buy-quest-modal(:item="selectedItemToBuy || {}",
|
||||
:priceType="selectedItemToBuy ? selectedItemToBuy.currency : ''",
|
||||
:withPin="true",
|
||||
@change="resetItemToBuy($event)"
|
||||
v-if='type === "reward"')
|
||||
.d-flex
|
||||
h2.tasks-column-title(v-once) {{ $t(types[type].label) }}
|
||||
.filters.d-flex.justify-content-end
|
||||
@@ -11,11 +16,11 @@
|
||||
) {{ $t(filter.label) }}
|
||||
.tasks-list(ref="tasksWrapper")
|
||||
input.quick-add(
|
||||
v-if="isUser", :placeholder="quickAddPlaceholder",
|
||||
v-if="isUser", :placeholder="quickAddPlaceholder",
|
||||
v-model="quickAddText", @keyup.enter="quickAdd",
|
||||
ref="quickAdd",
|
||||
)
|
||||
.sortable-tasks(ref="tasksList", v-sortable='', @onsort='sorted')
|
||||
.sortable-tasks(ref="tasksList", v-sortable='activeFilters[type].label !== "scheduled"', @onsort='sorted', data-sortableId)
|
||||
task(
|
||||
v-for="task in taskList",
|
||||
:key="task.id", :task="task",
|
||||
@@ -200,6 +205,7 @@ import sortable from 'client/directives/sortable.directive';
|
||||
import buyMixin from 'client/mixins/buy';
|
||||
import { mapState, mapActions } from 'client/libs/store';
|
||||
import shopItem from '../shops/shopItem';
|
||||
import BuyQuestModal from 'client/components/shops/quests/buyQuestModal.vue';
|
||||
|
||||
import { shouldDo } from 'common/script/cron';
|
||||
import inAppRewards from 'common/script/libs/inAppRewards';
|
||||
@@ -215,6 +221,7 @@ export default {
|
||||
mixins: [buyMixin],
|
||||
components: {
|
||||
Task,
|
||||
BuyQuestModal,
|
||||
bModal,
|
||||
shopItem,
|
||||
},
|
||||
@@ -278,6 +285,8 @@ export default {
|
||||
|
||||
forceRefresh: new Date(),
|
||||
quickAddText: '',
|
||||
|
||||
selectedItemToBuy: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -288,8 +297,25 @@ export default {
|
||||
}),
|
||||
taskList () {
|
||||
// @TODO: This should not default to user's tasks. It should require that you pass options in
|
||||
if (this.taskListOverride) return this.taskListOverride;
|
||||
return this.tasks[`${this.type}s`];
|
||||
const filter = this.activeFilters[this.type];
|
||||
|
||||
let taskList = this.tasks[`${this.type}s`];
|
||||
if (this.taskListOverride) taskList = this.taskListOverride;
|
||||
|
||||
if (taskList.length > 0 && ['scheduled', 'due'].indexOf(filter.label) === -1) {
|
||||
let taskListSorted = this.$store.dispatch('tasks:order', [
|
||||
taskList,
|
||||
this.user.tasksOrder,
|
||||
]);
|
||||
|
||||
taskList = taskListSorted[`${this.type}s`];
|
||||
}
|
||||
|
||||
if (filter.sort) {
|
||||
taskList = sortBy(taskList, filter.sort);
|
||||
}
|
||||
|
||||
return taskList;
|
||||
},
|
||||
inAppRewards () {
|
||||
let watchRefresh = this.forceRefresh; // eslint-disable-line
|
||||
@@ -365,19 +391,25 @@ export default {
|
||||
loadCompletedTodos: 'tasks:fetchCompletedTodos',
|
||||
createTask: 'tasks:create',
|
||||
}),
|
||||
sorted (data) {
|
||||
async sorted (data) {
|
||||
const filteredList = this.taskList.filter(this.activeFilters[this.type].filter);
|
||||
const sorting = this.taskList;
|
||||
const taskIdToMove = this.taskList[data.oldIndex]._id;
|
||||
const taskIdToMove = filteredList[data.oldIndex]._id;
|
||||
|
||||
// Server
|
||||
const taskIdToReplace = filteredList[data.newIndex];
|
||||
const newIndexOnServer = this.taskList.findIndex(taskId => taskId === taskIdToReplace);
|
||||
let newOrder = await this.$store.dispatch('tasks:move', {
|
||||
taskId: taskIdToMove,
|
||||
position: newIndexOnServer,
|
||||
});
|
||||
this.user.tasksOrder[`${this.type}s`] = newOrder;
|
||||
|
||||
// Client
|
||||
if (sorting) {
|
||||
const deleted = sorting.splice(data.oldIndex, 1);
|
||||
sorting.splice(data.newIndex, 0, deleted[0]);
|
||||
}
|
||||
|
||||
this.$store.dispatch('tasks:move', {
|
||||
taskId: taskIdToMove,
|
||||
position: data.newIndex,
|
||||
});
|
||||
},
|
||||
quickAdd () {
|
||||
const task = taskDefaults({type: this.type, text: this.quickAddText});
|
||||
@@ -393,10 +425,6 @@ export default {
|
||||
this.loadCompletedTodos();
|
||||
}
|
||||
this.activeFilters[type] = filter;
|
||||
|
||||
if (filter.sort) {
|
||||
this.tasks[`${type}s`] = sortBy(this.tasks[`${type}s`], filter.sort);
|
||||
}
|
||||
},
|
||||
setColumnBackgroundVisibility () {
|
||||
this.$nextTick(() => {
|
||||
@@ -455,6 +483,8 @@ export default {
|
||||
}
|
||||
},
|
||||
openBuyDialog (rewardItem) {
|
||||
if (rewardItem.locked) return;
|
||||
|
||||
// Buy armoire and health potions immediately
|
||||
let itemsToPurchaseImmediately = ['potion', 'armoire'];
|
||||
if (itemsToPurchaseImmediately.indexOf(rewardItem.key) !== -1) {
|
||||
@@ -463,10 +493,21 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rewardItem.purchaseType === 'quests') {
|
||||
this.selectedItemToBuy = rewardItem;
|
||||
this.$root.$emit('show::modal', 'buy-quest-modal');
|
||||
return;
|
||||
}
|
||||
|
||||
if (rewardItem.purchaseType !== 'gear' || !rewardItem.locked) {
|
||||
this.$emit('openBuyDialog', rewardItem);
|
||||
}
|
||||
},
|
||||
resetItemToBuy ($event) {
|
||||
if (!$event) {
|
||||
this.selectedItemToBuy = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,23 +1,45 @@
|
||||
import Sortable from 'sortablejs';
|
||||
import uuid from 'uuid';
|
||||
|
||||
let emit = (vnode, eventName, data) => {
|
||||
let handlers = vnode.data && vnode.data.on ||
|
||||
vnode.componentOptions && vnode.componentOptions.listeners;
|
||||
let emit = (vNode, eventName, data) => {
|
||||
let handlers = vNode.data && vNode.data.on ||
|
||||
vNode.componentOptions && vNode.componentOptions.listeners;
|
||||
|
||||
if (handlers && handlers[eventName]) {
|
||||
handlers[eventName].fns(data);
|
||||
}
|
||||
};
|
||||
|
||||
let sortableReferences = {};
|
||||
|
||||
function createSortable (el, vNode) {
|
||||
let sortableRef = Sortable.create(el, {
|
||||
onSort: (evt) => {
|
||||
emit(vNode, 'onsort', {
|
||||
oldIndex: evt.oldIndex,
|
||||
newIndex: evt.newIndex,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
let uniqueId = uuid();
|
||||
sortableReferences[uniqueId] = sortableRef;
|
||||
el.dataset.sortableId = uniqueId;
|
||||
}
|
||||
|
||||
export default {
|
||||
bind (el, binding, vnode) {
|
||||
Sortable.create(el, {
|
||||
onSort: (evt) => {
|
||||
emit(vnode, 'onsort', {
|
||||
oldIndex: evt.oldIndex,
|
||||
newIndex: evt.newIndex,
|
||||
});
|
||||
},
|
||||
});
|
||||
bind (el, binding, vNode) {
|
||||
createSortable(el, vNode);
|
||||
},
|
||||
unbind (el) {
|
||||
if (sortableReferences[el.dataset.sortableId]) sortableReferences[el.dataset.sortableId].destroy();
|
||||
},
|
||||
update (el, vNode) {
|
||||
if (sortableReferences[el.dataset.sortableId] && !vNode.value) {
|
||||
sortableReferences[el.dataset.sortableId].destroy();
|
||||
delete sortableReferences[el.dataset.sortableId];
|
||||
return;
|
||||
}
|
||||
if (!sortableReferences[el.dataset.sortableId]) createSortable(el, vNode);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
export default {
|
||||
methods: {
|
||||
makeGenericPurchase (item, type = 'buyModal') {
|
||||
makeGenericPurchase (item, type = 'buyModal', quantity = 1) {
|
||||
this.$store.dispatch('shops:genericPurchase', {
|
||||
pinType: item.pinType,
|
||||
type: item.purchaseType,
|
||||
key: item.key,
|
||||
currency: item.currency,
|
||||
quantity,
|
||||
});
|
||||
|
||||
if (item.purchaseType !== 'gear') {
|
||||
this.$store.state.recentlyPurchased[item.key] = true;
|
||||
}
|
||||
|
||||
this.$root.$emit('playSound', 'Reward');
|
||||
|
||||
if (type !== 'buyModal') {
|
||||
|
||||
@@ -65,7 +65,7 @@ export default {
|
||||
return round(number, nDigits);
|
||||
},
|
||||
notify (html, type, icon, sign) {
|
||||
this.notifications.push({
|
||||
this.$store.dispatch('snackbars:add', {
|
||||
title: '',
|
||||
text: html,
|
||||
type,
|
||||
|
||||
@@ -14,6 +14,7 @@ import * as notifications from './notifications';
|
||||
import * as tags from './tags';
|
||||
import * as hall from './hall';
|
||||
import * as shops from './shops';
|
||||
import * as snackbars from './snackbars';
|
||||
|
||||
// Actions should be named as 'actionName' and can be accessed as 'namespace:actionName'
|
||||
// Example: fetch in user.js -> 'user:fetch'
|
||||
@@ -33,6 +34,7 @@ const actions = flattenAndNamespace({
|
||||
tags,
|
||||
hall,
|
||||
shops,
|
||||
snackbars,
|
||||
});
|
||||
|
||||
export default actions;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import axios from 'axios';
|
||||
import buyOp from 'common/script/ops/buy';
|
||||
import buyQuestOp from 'common/script/ops/buyQuest';
|
||||
import purchaseOp from 'common/script/ops/purchaseWithSpell';
|
||||
import buyMysterySetOp from 'common/script/ops/buyMysterySet';
|
||||
import hourglassPurchaseOp from 'common/script/ops/hourglassPurchase';
|
||||
import sellOp from 'common/script/ops/sell';
|
||||
import unlockOp from 'common/script/ops/unlock';
|
||||
import buyArmoire from 'common/script/ops/buyArmoire';
|
||||
import rerollOp from 'common/script/ops/reroll';
|
||||
import { getDropClass } from 'client/libs/notifications';
|
||||
|
||||
// @TODO: Purchase means gems and buy means gold. That wording is misused below, but we should also change
|
||||
// the generic buy functions to something else. Or have a Gold Vendor and Gem Vendor, etc
|
||||
|
||||
export function buyItem (store, params) {
|
||||
const quantity = params.quantity || 1;
|
||||
const user = store.state.user.data;
|
||||
let opResult = buyOp(user, {params});
|
||||
let opResult = buyOp(user, {params, quantity});
|
||||
|
||||
return {
|
||||
result: opResult,
|
||||
@@ -21,32 +22,77 @@ export function buyItem (store, params) {
|
||||
}
|
||||
|
||||
export function buyQuestItem (store, params) {
|
||||
const quantity = params.quantity || 1;
|
||||
const user = store.state.user.data;
|
||||
let opResult = buyQuestOp(user, {params});
|
||||
let opResult = buyOp(user, {
|
||||
params,
|
||||
type: 'quest',
|
||||
quantity,
|
||||
});
|
||||
|
||||
return {
|
||||
result: opResult,
|
||||
httpCall: axios.post(`/api/v3/user/buy-quest/${params.key}`),
|
||||
httpCall: axios.post(`/api/v3/user/buy/${params.key}`, {type: 'quest'}),
|
||||
};
|
||||
}
|
||||
|
||||
async function buyArmoire (store, params) {
|
||||
const quantity = params.quantity || 1;
|
||||
|
||||
let buyResult = buyOp(store.state.user.data, {
|
||||
params: {
|
||||
key: 'armoire',
|
||||
},
|
||||
type: 'armoire',
|
||||
quantity,
|
||||
});
|
||||
|
||||
// We need the server result because armoir has random item in the result
|
||||
let result = await axios.post('/api/v3/user/buy/armoire', {
|
||||
type: 'armoire',
|
||||
quantity,
|
||||
});
|
||||
buyResult = result.data.data;
|
||||
|
||||
if (buyResult) {
|
||||
const resData = buyResult;
|
||||
const item = resData.armoire;
|
||||
|
||||
const isExperience = item.type === 'experience';
|
||||
|
||||
if (item.type === 'gear') {
|
||||
store.state.user.data.items.gear.owned[item.dropKey] = true;
|
||||
}
|
||||
|
||||
// @TODO: We might need to abstract notifications to library rather than mixin
|
||||
store.dispatch('snackbars:add', {
|
||||
title: '',
|
||||
text: isExperience ? item.value : item.dropText,
|
||||
type: isExperience ? 'xp' : 'drop',
|
||||
icon: isExperience ? null : getDropClass({type: item.type, key: item.dropKey}),
|
||||
timeout: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function purchase (store, params) {
|
||||
const quantity = params.quantity || 1;
|
||||
const user = store.state.user.data;
|
||||
let opResult = purchaseOp(user, {params});
|
||||
let opResult = purchaseOp(user, {params, quantity});
|
||||
|
||||
return {
|
||||
result: opResult,
|
||||
httpCall: axios.post(`/api/v3/user/purchase/${params.type}/${params.key}`),
|
||||
httpCall: axios.post(`/api/v3/user/purchase/${params.type}/${params.key}`, {quantity}),
|
||||
};
|
||||
}
|
||||
|
||||
export function purchaseMysterySet (store, params) {
|
||||
const user = store.state.user.data;
|
||||
let opResult = buyMysterySetOp(user, {params, noConfirm: true});
|
||||
let opResult = buyOp(user, {params, noConfirm: true, type: 'mystery'});
|
||||
|
||||
return {
|
||||
result: opResult,
|
||||
httpCall: axios.post(`/api/v3/user/buy-mystery-set/${params.key}`),
|
||||
httpCall: axios.post(`/api/v3/user/buy/${params.key}`, {type: 'mystery'}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,32 +121,7 @@ export async function genericPurchase (store, params) {
|
||||
case 'mystery_set':
|
||||
return purchaseMysterySet(store, params);
|
||||
case 'armoire': // eslint-disable-line
|
||||
let buyResult = buyArmoire(store.state.user.data);
|
||||
|
||||
// We need the server result because armoir has random item in the result
|
||||
let result = await axios.post('/api/v3/user/buy-armoire');
|
||||
buyResult = result.data.data;
|
||||
|
||||
if (buyResult) {
|
||||
const resData = buyResult;
|
||||
const item = resData.armoire;
|
||||
|
||||
const isExperience = item.type === 'experience';
|
||||
|
||||
if (item.type === 'gear') {
|
||||
store.state.user.data.items.gear.owned[item.dropKey] = true;
|
||||
}
|
||||
|
||||
// @TODO: We might need to abstract notifications to library rather than mixin
|
||||
store.state.notificationStore.push({
|
||||
title: '',
|
||||
text: isExperience ? item.value : item.dropText,
|
||||
type: isExperience ? 'xp' : 'drop',
|
||||
icon: isExperience ? null : getDropClass({type: item.type, key: item.dropKey}),
|
||||
timeout: true,
|
||||
});
|
||||
}
|
||||
|
||||
await buyArmoire(store, params);
|
||||
return;
|
||||
case 'fortify': {
|
||||
let rerollResult = rerollOp(store.state.user.data);
|
||||
@@ -134,9 +155,5 @@ export async function genericPurchase (store, params) {
|
||||
export function sellItems (store, params) {
|
||||
const user = store.state.user.data;
|
||||
sellOp(user, {params, query: {amount: params.amount}});
|
||||
axios
|
||||
.post(`/api/v3/user/sell/${params.type}/${params.key}?amount=${params.amount}`);
|
||||
// TODO
|
||||
// .then((res) => console.log('equip', res))
|
||||
// .catch((err) => console.error('equip', err));
|
||||
axios.post(`/api/v3/user/sell/${params.type}/${params.key}?amount=${params.amount}`);
|
||||
}
|
||||
|
||||
13
website/client/store/actions/snackbars.js
Normal file
13
website/client/store/actions/snackbars.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import uuid from 'uuid';
|
||||
|
||||
export function add (store, payload) {
|
||||
let notification = Object.assign({}, payload);
|
||||
notification.uuid = uuid();
|
||||
store.state.notificationStore.push(notification);
|
||||
}
|
||||
|
||||
export function remove (store, payload) {
|
||||
store.state.notificationStore = store.state.notificationStore.filter(notification => {
|
||||
return notification.uuid !== payload.uuid;
|
||||
});
|
||||
}
|
||||
@@ -55,6 +55,8 @@ export default function () {
|
||||
actions,
|
||||
getters,
|
||||
state: {
|
||||
serverAppVersion: '',
|
||||
deniedUpdate: false,
|
||||
title: 'Habitica',
|
||||
isUserLoggedIn,
|
||||
isUserLoaded: false, // Means the user and the user's tasks are ready
|
||||
@@ -134,8 +136,8 @@ export default function () {
|
||||
modalStack: [],
|
||||
brokenChallengeTask: {},
|
||||
equipmentDrawerOpen: true,
|
||||
recentlyPurchased: {},
|
||||
groupPlans: [],
|
||||
groupNotifications: [],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -280,5 +280,7 @@
|
||||
"emptyMessagesLine1": "You don't have any messages",
|
||||
"emptyMessagesLine2": "Send a message to start a conversation!",
|
||||
"letsgo": "Let's Go!",
|
||||
"selected": "Selected"
|
||||
"selected": "Selected",
|
||||
"howManyToBuy": "How many would you like to buy?",
|
||||
"habiticaHasUpdated": "There is a new version of Habitica. Would you like to refresh to get the latest updates?"
|
||||
}
|
||||
|
||||
@@ -623,7 +623,7 @@
|
||||
|
||||
"questLostMasterclasser4Text": "The Mystery of the Masterclassers, Part 4: The Lost Masterclasser",
|
||||
"questLostMasterclasser4Notes": "You surface from the portal, but you’re still suspended in a strange, shifting netherworld. “That was bold,” says a cold voice. “I have to admit, I hadn’t planned for a direct confrontation yet.” A woman rises from the churning whirlpool of darkness. “Welcome to the Realm of Void.”<br><br>You try to fight back your rising nausea. “Are you Zinnya?” you ask.<br><br>“That old name for a young idealist,” she says, mouth twisting, and the world writhes beneath you. “No. If anything, you should call me the Anti’zinnya now, given all that I have done and undone.”<br><br>Suddenly, the portal reopens behind you, and as the four Masterclassers burst out, bolting towards you, Anti’zinnya’s eyes flash with hatred. “I see that my pathetic replacements have managed to follow you.”<br><br>You stare. “Replacements?”<br><br>“As the Master Aethermancer, I was the first Masterclasser — the only Masterclasser. These four are a mockery, each possessing only a fragment of what I once had! I commanded every spell and learned every skill. I shaped your very world to my whim — until the traitorous aether itself collapsed under the weight of my talents and my perfectly reasonable expectations. I have been trapped for millennia in this resulting void, recuperating. Imagine my disgust when I learned how my legacy had been corrupted.” She lets out a low, echoing laugh. “My plan was to destroy their domains before destroying them, but I suppose the order is irrelevant.” With a burst of uncanny strength, she charges forward, and the Realm of Void explodes into chaos.",
|
||||
"questLostMasterclasser4Completion": "Under the onslaught of your final attack, the Lost Masterclasser screams in frustration, her body flickering into translucence. The thrashing void stills around her as she slumps forward, and for a moment, she seems to change, becoming younger, calmer, with an expression of peace upon her face… but then everything melts away with scarcely a whisper, and you’re kneeling once more in the desert sand.<br><br>“It seems that we have much to learn about our own history,” King Manta says, staring at the broken ruins. “After the Master Aethermancer grew overwhelmed and lost control of her abilities, the outpouring of void must have leached the life from the entire land. Everything probably became deserts like this.”<br><br>“No wonder the ancients who founded Habitica stressed a balance of productivity and wellness,” the Joyful Reaper murmurs. “Rebuilding their world would have been a daunting task requiring considerable hard work, but they would have wanted to prevent such a catastrophe from happening again.”<br><br>“Oho, look at those formerly possessed items!” says the April Fool. Sure enough, all of them shimmer with a pale, glimmering translucence from the final burst of aether released when you laid Anti’zinnya’s spirit to rest. “What a dazzling effect. I must take notes.”<br><br>“The concentrated remnants of aether in this area probably caused caused these animals to go invisible, too,” says Lady Glaciate, scratching a patch of emptiness behind the ears. You feel an unseen fluffy head nudge your hand, and suspect that you’ll have to do some explaining at the Stables back home. As you look at the ruins one last time, you spot all that remains of the first Masterclasser: her shimmering cloak. Lifting it onto your shoulders, you head back to Habit City, pondering everything that you have learned.<br><br>",
|
||||
"questLostMasterclasser4Completion": "Under the onslaught of your final attack, the Lost Masterclasser screams in frustration, her body flickering into translucence. The thrashing void stills around her as she slumps forward, and for a moment, she seems to change, becoming younger, calmer, with an expression of peace upon her face… but then everything melts away with scarcely a whisper, and you’re kneeling once more in the desert sand.<br><br>“It seems that we have much to learn about our own history,” King Manta says, staring at the broken ruins. “After the Master Aethermancer grew overwhelmed and lost control of her abilities, the outpouring of void must have leached the life from the entire land. Everything probably became deserts like this.”<br><br>“No wonder the ancients who founded Habitica stressed a balance of productivity and wellness,” the Joyful Reaper murmurs. “Rebuilding their world would have been a daunting task requiring considerable hard work, but they would have wanted to prevent such a catastrophe from happening again.”<br><br>“Oho, look at those formerly possessed items!” says the April Fool. Sure enough, all of them shimmer with a pale, glimmering translucence from the final burst of aether released when you laid Anti’zinnya’s spirit to rest. “What a dazzling effect. I must take notes.”<br><br>“The concentrated remnants of aether in this area probably caused these animals to go invisible, too,” says Lady Glaciate, scratching a patch of emptiness behind the ears. You feel an unseen fluffy head nudge your hand, and suspect that you’ll have to do some explaining at the Stables back home. As you look at the ruins one last time, you spot all that remains of the first Masterclasser: her shimmering cloak. Lifting it onto your shoulders, you head back to Habit City, pondering everything that you have learned.<br><br>",
|
||||
"questLostMasterclasser4Boss": "Anti'zinnya",
|
||||
"questLostMasterclasser4RageTitle": "Siphoning Void",
|
||||
"questLostMasterclasser4RageDescription": "Siphoning Void: This bar fills when you don't complete your Dailies. When it is full, Anti'zinnya will remove the party's Mana!",
|
||||
|
||||
@@ -72,7 +72,9 @@
|
||||
"APIv3": "API v3",
|
||||
"APIText": "Copy these for use in third party applications. However, think of your API Token like a password, and do not share it publicly. You may occasionally be asked for your User ID, but never post your API Token where others can see it, including on Github.",
|
||||
"APIToken": "API Token (this is a password - see warning above!)",
|
||||
"APITokenWarning": "If you need a new API Token (e.g., if you accidentally shared it), email <%= hrefTechAssistanceEmail %> with your User ID and current Token. Once it is reset you will need to re-authorize everything by logging out of the website and mobile app and by providing the new Token to any other Habitica tools that you use.",
|
||||
"showAPIToken": "Show API Token",
|
||||
"hideAPIToken": "Hide API Token",
|
||||
"APITokenWarning": "If you need a new API Token (e.g., if you accidentally shared it), email <%= hrefTechAssistanceEmail %> with your User ID and current Token. Once it is reset you will need to re-authorize everything by logging out of the website and mobile app and by providing the new Token to any other Habitica tools that you use.",
|
||||
"thirdPartyApps": "Third Party Apps",
|
||||
"dataToolDesc": "A webpage that shows you certain information from your Habitica account, such as statistics about your tasks, equipment, and skills.",
|
||||
"beeminder": "Beeminder",
|
||||
|
||||
@@ -204,5 +204,6 @@
|
||||
"subscriptionAlreadySubscribed1": "To see your subscription details and cancel, renew, or change your subscription, please go to <a href='/user/settings/subscription'>User icon > Settings > Subscription</a>.",
|
||||
"purchaseAll": "Purchase All",
|
||||
"gemsPurchaseNote": "Subscribers can buy gems for gold in the Market! For easy access, you can also pin the gem to your Rewards column.",
|
||||
"gemsRemaining": "gems remaining"
|
||||
"gemsRemaining": "gems remaining",
|
||||
"notEnoughGemsToBuy": "You are unable to buy that amount of gems"
|
||||
}
|
||||
|
||||
@@ -6,18 +6,45 @@ import {
|
||||
import buyHealthPotion from './buyHealthPotion';
|
||||
import buyArmoire from './buyArmoire';
|
||||
import buyGear from './buyGear';
|
||||
import buyMysterySet from './buyMysterySet';
|
||||
import buyQuest from './buyQuest';
|
||||
import buySpecialSpell from './buySpecialSpell';
|
||||
|
||||
// @TODO: remove the req option style. Dependency on express structure is an anti-pattern
|
||||
// We should either have more parms or a set structure validated by a Type checker
|
||||
|
||||
// @TODO: when we are sure buy is the only function used, let's move the buy files to a folder
|
||||
|
||||
module.exports = function buy (user, req = {}, analytics) {
|
||||
let key = get(req, 'params.key');
|
||||
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
|
||||
|
||||
// @TODO: Slowly remove the need for key and use type instead
|
||||
// This should evenutally be the 'factory' function with vendor classes
|
||||
let type = get(req, 'type');
|
||||
if (!type) type = key;
|
||||
|
||||
// @TODO: For now, builk purchasing is here, but we should probably have a parent vendor
|
||||
// class that calls the factory and handles larger operations. If there is more than just bulk
|
||||
let quantity = 1;
|
||||
if (req.quantity) quantity = req.quantity;
|
||||
|
||||
let buyRes;
|
||||
if (key === 'potion') {
|
||||
buyRes = buyHealthPotion(user, req, analytics);
|
||||
} else if (key === 'armoire') {
|
||||
buyRes = buyArmoire(user, req, analytics);
|
||||
} else {
|
||||
buyRes = buyGear(user, req, analytics);
|
||||
|
||||
for (let i = 0; i < quantity; i += 1) {
|
||||
if (type === 'potion') {
|
||||
buyRes = buyHealthPotion(user, req, analytics);
|
||||
} else if (type === 'armoire') {
|
||||
buyRes = buyArmoire(user, req, analytics);
|
||||
} else if (type === 'mystery') {
|
||||
buyRes = buyMysterySet(user, req, analytics);
|
||||
} else if (type === 'quest') {
|
||||
buyRes = buyQuest(user, req, analytics);
|
||||
} else if (type === 'special') {
|
||||
buyRes = buySpecialSpell(user, req, analytics);
|
||||
} else {
|
||||
buyRes = buyGear(user, req, analytics);
|
||||
}
|
||||
}
|
||||
|
||||
return buyRes;
|
||||
|
||||
@@ -14,68 +14,52 @@ import {
|
||||
import { removeItemByPath } from './pinnedGearUtils';
|
||||
import getItemInfo from '../libs/getItemInfo';
|
||||
|
||||
module.exports = function purchase (user, req = {}, analytics) {
|
||||
let type = get(req.params, 'type');
|
||||
let key = get(req.params, 'key');
|
||||
function buyGems (user, analytics, req, key) {
|
||||
let convRate = planGemLimits.convRate;
|
||||
let convCap = planGemLimits.convCap;
|
||||
convCap += user.purchased.plan.consecutive.gemCapExtra;
|
||||
|
||||
// Some groups limit their members ability to obtain gems
|
||||
// The check is async so it's done on the server (in server/controllers/api-v3/user#purchase)
|
||||
// only and not on the client,
|
||||
// resulting in a purchase that will seem successful until the request hit the server.
|
||||
if (!user.purchased || !user.purchased.plan || !user.purchased.plan.customerId) {
|
||||
throw new NotAuthorized(i18n.t('mustSubscribeToPurchaseGems', req.language));
|
||||
}
|
||||
|
||||
if (user.stats.gp < convRate) {
|
||||
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
|
||||
}
|
||||
|
||||
if (user.purchased.plan.gemsBought >= convCap) {
|
||||
throw new NotAuthorized(i18n.t('reachedGoldToGemCap', {convCap}, req.language));
|
||||
}
|
||||
|
||||
user.balance += 0.25;
|
||||
user.purchased.plan.gemsBought++;
|
||||
user.stats.gp -= convRate;
|
||||
|
||||
if (analytics) {
|
||||
analytics.track('purchase gems', {
|
||||
uuid: user._id,
|
||||
itemKey: key,
|
||||
acquireMethod: 'Gold',
|
||||
goldCost: convRate,
|
||||
category: 'behavior',
|
||||
headers: req.headers,
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
pick(user, splitWhitespace('stats balance')),
|
||||
i18n.t('plusOneGem', req.language),
|
||||
];
|
||||
}
|
||||
|
||||
function getItemAndPrice (user, type, key, req) {
|
||||
let item;
|
||||
let price;
|
||||
|
||||
if (!type) {
|
||||
throw new BadRequest(i18n.t('typeRequired', req.language));
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
throw new BadRequest(i18n.t('keyRequired', req.language));
|
||||
}
|
||||
|
||||
if (type === 'gems' && key === 'gem') {
|
||||
let convRate = planGemLimits.convRate;
|
||||
let convCap = planGemLimits.convCap;
|
||||
convCap += user.purchased.plan.consecutive.gemCapExtra;
|
||||
|
||||
// Some groups limit their members ability to obtain gems
|
||||
// The check is async so it's done on the server (in server/controllers/api-v3/user#purchase)
|
||||
// only and not on the client,
|
||||
// resulting in a purchase that will seem successful until the request hit the server.
|
||||
|
||||
if (!user.purchased || !user.purchased.plan || !user.purchased.plan.customerId) {
|
||||
throw new NotAuthorized(i18n.t('mustSubscribeToPurchaseGems', req.language));
|
||||
}
|
||||
|
||||
if (user.stats.gp < convRate) {
|
||||
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
|
||||
}
|
||||
|
||||
if (user.purchased.plan.gemsBought >= convCap) {
|
||||
throw new NotAuthorized(i18n.t('reachedGoldToGemCap', {convCap}, req.language));
|
||||
}
|
||||
|
||||
user.balance += 0.25;
|
||||
user.purchased.plan.gemsBought++;
|
||||
user.stats.gp -= convRate;
|
||||
|
||||
if (analytics) {
|
||||
analytics.track('purchase gems', {
|
||||
uuid: user._id,
|
||||
itemKey: key,
|
||||
acquireMethod: 'Gold',
|
||||
goldCost: convRate,
|
||||
category: 'behavior',
|
||||
headers: req.headers,
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
pick(user, splitWhitespace('stats balance')),
|
||||
i18n.t('plusOneGem', req.language),
|
||||
];
|
||||
}
|
||||
|
||||
let acceptedTypes = ['eggs', 'hatchingPotions', 'food', 'quests', 'gear', 'bundles'];
|
||||
if (acceptedTypes.indexOf(type) === -1) {
|
||||
throw new NotFound(i18n.t('notAccteptedType', req.language));
|
||||
}
|
||||
|
||||
if (type === 'gear') {
|
||||
item = content.gear.flat[key];
|
||||
|
||||
@@ -98,17 +82,10 @@ module.exports = function purchase (user, req = {}, analytics) {
|
||||
price = item.value / 4;
|
||||
}
|
||||
|
||||
if (!item.canBuy(user)) {
|
||||
throw new NotAuthorized(i18n.t('messageNotAvailable', req.language));
|
||||
}
|
||||
|
||||
if (!user.balance || user.balance < price) {
|
||||
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
|
||||
}
|
||||
|
||||
let itemInfo = getItemInfo(user, type, item);
|
||||
removeItemByPath(user, itemInfo.path);
|
||||
return {item, price};
|
||||
}
|
||||
|
||||
function purchaseItem (user, item, price, type, key) {
|
||||
user.balance -= price;
|
||||
|
||||
if (type === 'gear') {
|
||||
@@ -127,6 +104,50 @@ module.exports = function purchase (user, req = {}, analytics) {
|
||||
}
|
||||
user.items[type][key]++;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function purchase (user, req = {}, analytics) {
|
||||
let type = get(req.params, 'type');
|
||||
let key = get(req.params, 'key');
|
||||
let quantity = req.quantity || 1;
|
||||
|
||||
if (!type) {
|
||||
throw new BadRequest(i18n.t('typeRequired', req.language));
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
throw new BadRequest(i18n.t('keyRequired', req.language));
|
||||
}
|
||||
|
||||
if (type === 'gems' && key === 'gem') {
|
||||
let gemResponse;
|
||||
for (let i = 0; i < quantity; i += 1) {
|
||||
gemResponse = buyGems(user, analytics, req, key);
|
||||
}
|
||||
return gemResponse;
|
||||
}
|
||||
|
||||
let acceptedTypes = ['eggs', 'hatchingPotions', 'food', 'quests', 'gear', 'bundles'];
|
||||
if (acceptedTypes.indexOf(type) === -1) {
|
||||
throw new NotFound(i18n.t('notAccteptedType', req.language));
|
||||
}
|
||||
|
||||
let {price, item} = getItemAndPrice(user, type, key, req);
|
||||
|
||||
if (!item.canBuy(user)) {
|
||||
throw new NotAuthorized(i18n.t('messageNotAvailable', req.language));
|
||||
}
|
||||
|
||||
if (!user.balance || user.balance < price) {
|
||||
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
|
||||
}
|
||||
|
||||
let itemInfo = getItemInfo(user, type, item);
|
||||
removeItemByPath(user, itemInfo.path);
|
||||
|
||||
for (let i = 0; i < quantity; i += 1) {
|
||||
purchaseItem(user, item, price, type, key);
|
||||
}
|
||||
|
||||
if (analytics) {
|
||||
analytics.track('acquire item', {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import buySpecialSpellOp from './buySpecialSpell';
|
||||
import buy from './buy';
|
||||
import purchaseOp from './purchase';
|
||||
import get from 'lodash/get';
|
||||
|
||||
module.exports = function purchaseWithSpell (user, req = {}, analytics) {
|
||||
const type = get(req.params, 'type');
|
||||
|
||||
return type === 'spells' ? buySpecialSpellOp(user, req) : purchaseOp(user, req, analytics);
|
||||
// Set up type for buy function - different than the above type.
|
||||
req.type = 'special';
|
||||
|
||||
return type === 'spells' ? buy(user, req, analytics) : purchaseOp(user, req, analytics);
|
||||
};
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.6 KiB |
BIN
website/raw_sprites/spritesmith_large/scene_positivity.png
Normal file
BIN
website/raw_sprites/spritesmith_large/scene_positivity.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -874,11 +874,21 @@ api.buy = {
|
||||
let buyRes;
|
||||
let specialKeys = ['snowball', 'spookySparkles', 'shinySeed', 'seafoam'];
|
||||
|
||||
// @TODO: Remove this when mobile passes type in body
|
||||
let type = req.params.key;
|
||||
if (specialKeys.indexOf(req.params.key) !== -1) {
|
||||
buyRes = common.ops.buySpecialSpell(user, req);
|
||||
} else {
|
||||
buyRes = common.ops.buy(user, req, res.analytics);
|
||||
type = 'special';
|
||||
}
|
||||
req.type = type;
|
||||
|
||||
// @TODO: right now common follow express structure, but we should decouple the dependency
|
||||
if (req.body.type) req.type = req.body.type;
|
||||
|
||||
let quantity = 1;
|
||||
if (req.body.quantity) quantity = req.body.quantity;
|
||||
req.quantity = quantity;
|
||||
|
||||
buyRes = common.ops.buy(user, req, res.analytics);
|
||||
|
||||
await user.save();
|
||||
res.respond(200, ...buyRes);
|
||||
@@ -926,7 +936,7 @@ api.buyGear = {
|
||||
url: '/user/buy-gear/:key',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let buyGearRes = common.ops.buyGear(user, req, res.analytics);
|
||||
let buyGearRes = common.ops.buy(user, req, res.analytics);
|
||||
await user.save();
|
||||
res.respond(200, ...buyGearRes);
|
||||
},
|
||||
@@ -966,7 +976,9 @@ api.buyArmoire = {
|
||||
url: '/user/buy-armoire',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let buyArmoireResponse = common.ops.buyArmoire(user, req, res.analytics);
|
||||
req.type = 'armoire';
|
||||
req.params.key = 'armoire';
|
||||
let buyArmoireResponse = common.ops.buy(user, req, res.analytics);
|
||||
await user.save();
|
||||
res.respond(200, ...buyArmoireResponse);
|
||||
},
|
||||
@@ -1004,7 +1016,9 @@ api.buyHealthPotion = {
|
||||
url: '/user/buy-health-potion',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let buyHealthPotionResponse = common.ops.buyHealthPotion(user, req, res.analytics);
|
||||
req.type = 'potion';
|
||||
req.params.key = 'potion';
|
||||
let buyHealthPotionResponse = common.ops.buy(user, req, res.analytics);
|
||||
await user.save();
|
||||
res.respond(200, ...buyHealthPotionResponse);
|
||||
},
|
||||
@@ -1044,7 +1058,8 @@ api.buyMysterySet = {
|
||||
url: '/user/buy-mystery-set/:key',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let buyMysterySetRes = common.ops.buyMysterySet(user, req, res.analytics);
|
||||
req.type = 'mystery';
|
||||
let buyMysterySetRes = common.ops.buy(user, req, res.analytics);
|
||||
await user.save();
|
||||
res.respond(200, ...buyMysterySetRes);
|
||||
},
|
||||
@@ -1084,7 +1099,8 @@ api.buyQuest = {
|
||||
url: '/user/buy-quest/:key',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let buyQuestRes = common.ops.buyQuest(user, req, res.analytics);
|
||||
req.type = 'quest';
|
||||
let buyQuestRes = common.ops.buy(user, req, res.analytics);
|
||||
await user.save();
|
||||
res.respond(200, ...buyQuestRes);
|
||||
},
|
||||
@@ -1123,7 +1139,8 @@ api.buySpecialSpell = {
|
||||
url: '/user/buy-special-spell/:key',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let buySpecialSpellRes = common.ops.buySpecialSpell(user, req);
|
||||
req.type = 'special';
|
||||
let buySpecialSpellRes = common.ops.buy(user, req);
|
||||
await user.save();
|
||||
res.respond(200, ...buySpecialSpellRes);
|
||||
},
|
||||
@@ -1337,6 +1354,11 @@ api.purchase = {
|
||||
if (!canGetGems) throw new NotAuthorized(res.t('groupPolicyCannotGetGems'));
|
||||
}
|
||||
|
||||
// Req is currently used as options. Slighly confusing, but this will solve that for now.
|
||||
let quantity = 1;
|
||||
if (req.body.quantity) quantity = req.body.quantity;
|
||||
req.quantity = quantity;
|
||||
|
||||
let purchaseRes = common.ops.purchaseWithSpell(user, req, res.analytics);
|
||||
await user.save();
|
||||
res.respond(200, ...purchaseRes);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import packageInfo from '../../../package.json';
|
||||
|
||||
module.exports = function responseHandler (req, res, next) {
|
||||
// Only used for successful responses
|
||||
res.respond = function respond (status = 200, data = {}, message) {
|
||||
@@ -15,6 +17,8 @@ module.exports = function responseHandler (req, res, next) {
|
||||
response.userV = user._v;
|
||||
}
|
||||
|
||||
response.appVersion = packageInfo.version;
|
||||
|
||||
res.status(status).json(response);
|
||||
};
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 13 KiB |
Reference in New Issue
Block a user