diff --git a/common/locales/en/npc.json b/common/locales/en/npc.json
index 55ad9377c4..fa93c62ec7 100644
--- a/common/locales/en/npc.json
+++ b/common/locales/en/npc.json
@@ -11,6 +11,7 @@
"danielText2Broken": "Oh... If you are participating in a boss quest, the boss will still damage you for your party mates' missed Dailies... Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn...",
"alexander": "Alexander the Merchant",
"welcomeMarket": "Welcome to the Market! Buy hard-to-find eggs and potions! Sell your extras! Commission useful services! Come see what we have to offer.",
+ "welcomeMarketMobile": "Welcome to the Market! Buy hard-to-find eggs and potions! Come see what we have to offer.",
"displayItemForGold": "Do you want to sell a <%= itemType %>?",
"displayEggForGold": "Do you want to sell a <%= itemType %> Egg?",
"displayPotionForGold": "Do you want to sell a <%= itemType %> Potion?",
@@ -20,6 +21,7 @@
"justin": "Justin",
"ian": "Ian",
"ianText": "Welcome to the Quest Shop! Here you can use Quest Scrolls to battle monsters with your friends. Be sure to check out our fine array of Quest Scrolls for purchase on the right!",
+ "ianTextMobile": "Welcome to the Quest Shop! Be sure to check out our fine array of Quest Scrolls for purchase!",
"ianBrokenText": "Welcome to the Quest Shop... Here you can use Quest Scrolls to battle monsters with your friends... Be sure to check out our fine array of Quest Scrolls for purchase on the right...",
"missingKeyParam": "\"req.params.key\" is required.",
diff --git a/common/locales/en/subscriber.json b/common/locales/en/subscriber.json
index a0354f41cc..264b6e107c 100644
--- a/common/locales/en/subscriber.json
+++ b/common/locales/en/subscriber.json
@@ -73,6 +73,7 @@
"timeTravelersTitleNoSub": "<%= linkStartTyler %>Tyler<%= linkEnd %> and <%= linkStartVicky %>Vicky<%= linkEnd %>",
"timeTravelersTitle": "Mysterious Time Travelers",
"timeTravelersPopoverNoSub": "You'll need a Mystic Hourglass to summon the mysterious Time Travelers! <%= linkStart %>Subscribers<%= linkEnd %> earn one Mystic Hourglass for every three months of consecutive subscribing. Come back when you have a Mystic Hourglass, and the Time Travelers will fetch you a rare pet, mount, or Subscriber Item Set from the past... or maybe even the future.",
+ "timeTravelersPopoverNoSubMobile": "You'll need a Mystic Hourglass to summon the mysterious Time Travelers! Subscribers earn one Mystic Hourglass for every three months of consecutive subscribing. Come back when you have a Mystic Hourglass, and the Time Travelers will fetch you a rare pet, mount, or Subscriber Item Set from the past... or maybe even the future.",
"timeTravelersPopover": "We see you have a Mystic Hourglass, so we will happily travel back in time for you! Please choose the pet, mount, or Mystery Item Set you would like. You can see a list of the past item sets here! If those don't satisfy you, perhaps you'd be interested in one of our fashionably futuristic Steampunk Item Sets?",
"timeTravelersAlreadyOwned": "Congratulations! You already own everything the Time Travelers currently offer. Thanks for supporting the site!",
"mysticHourglassPopover": "A Mystic Hourglass allows you to purchase certain limited-time items, such as monthly Mystery Item Sets and awards from world bosses, from the past!",
diff --git a/common/script/index.js b/common/script/index.js
index 112c06a597..8f32dcb456 100644
--- a/common/script/index.js
+++ b/common/script/index.js
@@ -94,6 +94,9 @@ api.count = count;
import statsComputed from './libs/statsComputed';
api.statsComputed = statsComputed;
+import shops from './libs/shops';
+api.shops = shops;
+
import autoAllocate from './fns/autoAllocate';
import crit from './fns/crit';
import handleTwoHanded from './fns/handleTwoHanded';
diff --git a/common/script/libs/shops.js b/common/script/libs/shops.js
new file mode 100644
index 0000000000..a911729cef
--- /dev/null
+++ b/common/script/libs/shops.js
@@ -0,0 +1,243 @@
+import _ from 'lodash';
+import content from '../content/index';
+import i18n from '../../../common/script/i18n';
+
+let shops = {};
+
+function lockQuest (quest, user) {
+ if (quest.lvl && user.stats.lvl < quest.lvl) return true;
+ if (user.achievements.quests) return quest.previous && !user.achievements.quests[quest.previous];
+ return quest.previous;
+}
+
+shops.getMarketCategories = function getMarket (user, language) {
+ let categories = [];
+ let eggsCategory = {
+ identifier: 'eggs',
+ text: i18n.t('eggs', language),
+ notes: i18n.t('dropsExplanation', language),
+ };
+
+ eggsCategory.items = _(content.questEggs)
+ .values()
+ .filter(egg => egg.canBuy(user))
+ .concat(_.values(content.dropEggs))
+ .map(egg => {
+ return {
+ key: egg.key,
+ text: i18n.t('egg', {eggType: egg.text()}, language),
+ notes: egg.notes(language),
+ value: egg.value,
+ class: `Pet_Egg_${egg.key}`,
+ locked: false,
+ currency: 'gems',
+ purchaseType: 'eggs',
+ };
+ }).sortBy('key').value();
+ categories.push(eggsCategory);
+
+ let hatchingPotionsCategory = {
+ identifier: 'hatchingPotions',
+ text: i18n.t('hatchingPotions', language),
+ notes: i18n.t('dropsExplanation', language),
+ };
+ hatchingPotionsCategory.items = _(content.hatchingPotions)
+ .values()
+ .filter(hp => !hp.limited)
+ .map(hatchingPotion => {
+ return {
+ key: hatchingPotion.key,
+ text: hatchingPotion.text(language),
+ notes: hatchingPotion.notes(language),
+ class: `Pet_HatchingPotion_${hatchingPotion.key}`,
+ value: hatchingPotion.value,
+ locked: false,
+ currency: 'gems',
+ purchaseType: 'hatchingpotions',
+ };
+ }).sortBy('key').value();
+ categories.push(hatchingPotionsCategory);
+
+ let foodCategory = {
+ identifier: 'food',
+ text: i18n.t('food', language),
+ notes: i18n.t('dropsExplanation', language),
+ };
+ foodCategory.items = _(content.food)
+ .values()
+ .filter(food => food.canDrop || food.key === 'Saddle')
+ .map(foodItem => {
+ return {
+ key: foodItem.key,
+ text: foodItem.text(language),
+ notes: foodItem.notes(language),
+ class: `Pet_Food_${foodItem.key}`,
+ value: foodItem.value,
+ locked: false,
+ currency: 'gems',
+ purchaseType: 'food',
+ };
+ }).sortBy('key').value();
+ categories.push(foodCategory);
+
+ return categories;
+};
+
+shops.getQuestShopCategories = function getQuestShopCategories (user, language) {
+ let categories = [];
+
+ _.each(content.userCanOwnQuestCategories, type => {
+ let category = {
+ identifier: type,
+ text: i18n.t(`${type}Quests`, language),
+ };
+
+ category.items = _(content.questsByLevel)
+ .filter(quest => quest.canBuy(user) && quest.category === type)
+ .map(quest => {
+ let locked = lockQuest(quest, user);
+ return {
+ key: quest.key,
+ text: quest.text(language),
+ notes: quest.notes(language),
+ value: quest.goldValue ? quest.goldValue : quest.value,
+ currency: quest.goldValue ? 'gold' : 'gems',
+ locked,
+ unlockCondition: quest.unlockCondition,
+ drop: quest.drop,
+ boss: quest.boss,
+ collect: quest.collect,
+ lvl: quest.lvl,
+ class: locked ? `inventory_quest_scroll_${quest.key}_locked` : `inventory_quest_scroll_${quest.key}`,
+ purchaseType: 'quests',
+ };
+ }).value();
+
+ categories.push(category);
+ });
+
+ return categories;
+};
+
+shops.getTimeTravelersCategories = function getTimeTravelersCategories (user, language) {
+ let categories = [];
+ let stable = {pets: 'Pet-', mounts: 'Mount_Head_'};
+ for (let type in stable) {
+ if (stable.hasOwnProperty(type)) {
+ let category = {
+ identifier: type,
+ text: i18n.t(type, language),
+ items: [],
+ };
+
+ for (let key in content.timeTravelStable[type]) {
+ if (content.timeTravelStable[type].hasOwnProperty(key)) {
+ if (!user.items[type][key]) {
+ let item = {
+ key,
+ text: content.timeTravelStable[type][key](language),
+ class: stable[type] + key,
+ type,
+ purchaseType: type,
+ value: 1,
+ notes: '',
+ locked: false,
+ currency: 'hourglasses',
+ };
+ category.items.push(item);
+ }
+ }
+ }
+ if (category.items.length > 0) {
+ categories.push(category);
+ }
+ }
+ }
+
+ let sets = content.timeTravelerStore(user.items.gear.owned);
+ for (let setKey in sets) {
+ if (sets.hasOwnProperty(setKey)) {
+ let set = sets[setKey];
+ let category = {
+ identifier: set.key,
+ text: set.text(language),
+ purchaseAll: true,
+ };
+
+ category.items = _.map(set.items, item => {
+ return {
+ key: item.key,
+ text: item.text(language),
+ notes: item.notes(language),
+ type: item.type,
+ purchaseType: 'gear',
+ value: 1,
+ locked: false,
+ currency: 'hourglasses',
+ class: `shop_${item.key}`,
+ };
+ });
+ if (category.items.length > 0) {
+ categories.push(category);
+ }
+ }
+ }
+
+ return categories;
+};
+
+// To switch seasons/available inventory, edit the availableSets object to whatever should be sold.
+// let availableSets = {
+// setKey: i18n.t('setTranslationString', language),
+// };
+shops.getSeasonalShopCategories = function getSeasonalShopCategories (user, language) {
+ let availableSets = {
+ summerWarrior: i18n.t('daringSwashbucklerSet', language),
+ summerMage: i18n.t('emeraldMermageSet', language),
+ summerHealer: i18n.t('reefSeahealerSet', language),
+ summerRogue: i18n.t('roguishPirateSet', language),
+ summer2015Warrior: i18n.t('sunfishWarriorSet', language),
+ summer2015Mage: i18n.t('shipSoothsayerSet', language),
+ summer2015Healer: i18n.t('strappingSailorSet', language),
+ summer2015Rogue: i18n.t('reefRenegadeSet', language),
+ };
+
+ let categories = [];
+
+ let flatGearArray = _.toArray(content.gear.flat);
+
+ for (let key in availableSets) {
+ if (availableSets.hasOwnProperty(key)) {
+ let category = {
+ identifier: key,
+ text: availableSets[key],
+ };
+
+ category.items = _(flatGearArray).filter((gear) => {
+ if (gear.index !== key) {
+ return false;
+ }
+ return user.items.gear.owned[gear.key] !== true;
+ }).where({index: key}).map(gear => {
+ return {
+ key: gear.key,
+ text: gear.text(language),
+ notes: gear.notes(language),
+ value: 1,
+ type: gear.type,
+ specialClass: gear.specialClass,
+ locked: false,
+ currency: 'gems',
+ purchaseType: 'gear',
+ };
+ }).value();
+ if (category.items.length > 0) {
+ categories.push(category);
+ }
+ }
+ }
+
+ return categories;
+};
+
+module.exports = shops;
diff --git a/test/api/v3/integration/shops/GET-shops_market.test.js b/test/api/v3/integration/shops/GET-shops_market.test.js
new file mode 100644
index 0000000000..02162f0d5c
--- /dev/null
+++ b/test/api/v3/integration/shops/GET-shops_market.test.js
@@ -0,0 +1,28 @@
+import {
+ generateUser,
+ translate as t,
+} from '../../../../helpers/api-integration/v3';
+
+describe('GET /shops/market', () => {
+ let user;
+
+ beforeEach(async () => {
+ user = await generateUser();
+ });
+
+ it('returns a valid shop object', async () => {
+ let shop = await user.get('/shops/market');
+
+ expect(shop.identifier).to.equal('market');
+ expect(shop.text).to.eql(t('market'));
+ expect(shop.notes).to.eql(t('welcomeMarketMobile'));
+ expect(shop.imageName).to.be.a('string');
+ expect(shop.categories).to.be.an('array');
+
+ let categories = shop.categories.map(cat => cat.identifier);
+
+ expect(categories).to.include('eggs');
+ expect(categories).to.include('hatchingPotions');
+ expect(categories).to.include('food');
+ });
+});
diff --git a/test/api/v3/integration/shops/GET-shops_quests.test.js b/test/api/v3/integration/shops/GET-shops_quests.test.js
new file mode 100644
index 0000000000..e4546fb4b5
--- /dev/null
+++ b/test/api/v3/integration/shops/GET-shops_quests.test.js
@@ -0,0 +1,28 @@
+import {
+ generateUser,
+ translate as t,
+} from '../../../../helpers/api-integration/v3';
+
+describe('GET /shops/quests', () => {
+ let user;
+
+ beforeEach(async () => {
+ user = await generateUser();
+ });
+
+ it('returns a valid shop object', async () => {
+ let shop = await user.get('/shops/quests');
+
+ expect(shop.identifier).to.equal('questShop');
+ expect(shop.text).to.eql(t('quests'));
+ expect(shop.notes).to.eql(t('ianTextMobile'));
+ expect(shop.imageName).to.be.a('string');
+ expect(shop.categories).to.be.an('array');
+
+ let categories = shop.categories.map(cat => cat.identifier);
+
+ expect(categories).to.include('unlockable');
+ expect(categories).to.include('gold');
+ expect(categories).to.include('pet');
+ });
+});
diff --git a/test/api/v3/integration/shops/GET-shops_seasonal.test.js b/test/api/v3/integration/shops/GET-shops_seasonal.test.js
new file mode 100644
index 0000000000..87f97ac111
--- /dev/null
+++ b/test/api/v3/integration/shops/GET-shops_seasonal.test.js
@@ -0,0 +1,22 @@
+import {
+ generateUser,
+ translate as t,
+} from '../../../../helpers/api-integration/v3';
+
+describe('GET /shops/seasonal', () => {
+ let user;
+
+ beforeEach(async () => {
+ user = await generateUser();
+ });
+
+ it('returns a valid shop object', async () => {
+ let shop = await user.get('/shops/seasonal');
+
+ expect(shop.identifier).to.equal('seasonalShop');
+ expect(shop.text).to.eql(t('seasonalShop'));
+ expect(shop.notes).to.eql(t('seasonalShopSummerText'));
+ expect(shop.imageName).to.be.a('string');
+ expect(shop.categories).to.be.an('array');
+ });
+});
diff --git a/test/api/v3/integration/shops/GET-shops_time_travelers.test.js b/test/api/v3/integration/shops/GET-shops_time_travelers.test.js
new file mode 100644
index 0000000000..5ae901f657
--- /dev/null
+++ b/test/api/v3/integration/shops/GET-shops_time_travelers.test.js
@@ -0,0 +1,98 @@
+import {
+ generateUser,
+ translate as t,
+} from '../../../../helpers/api-integration/v3';
+
+describe('GET /shops/time-travelers', () => {
+ let user;
+
+ beforeEach(async () => {
+ user = await generateUser();
+ });
+
+ it('returns a valid shop object', async () => {
+ let shop = await user.get('/shops/time-travelers');
+
+ expect(shop.identifier).to.equal('timeTravelersShop');
+ expect(shop.text).to.eql(t('timeTravelers'));
+ expect(shop.notes).to.be.a('string');
+ expect(shop.imageName).to.be.a('string');
+ expect(shop.categories).to.be.an('array');
+
+ let categories = shop.categories.map(cat => cat.identifier);
+
+ expect(categories).to.include('pets');
+ expect(categories).to.include('mounts');
+ expect(categories).to.include('201606');
+
+ let mammothPet = shop.categories
+ .find(cat => cat.identifier === 'pets')
+ .items
+ .find(pet => pet.key === 'Mammoth-Base');
+ let mantisShrimp = shop.categories
+ .find(cat => cat.identifier === 'mounts')
+ .items
+ .find(pet => pet.key === 'MantisShrimp-Base');
+
+ expect(mammothPet).to.exist;
+ expect(mantisShrimp).to.exist;
+ });
+
+ it('returns active shop notes and imageName if user has trinkets', async () => {
+ await user.update({
+ 'purchased.plan.consecutive.trinkets': 1,
+ });
+
+ let shop = await user.get('/shops/time-travelers');
+
+ expect(shop.notes).to.eql(t('timeTravelersPopover'));
+ expect(shop.imageName).to.eql('npc_timetravelers_active');
+ });
+
+ it('returns inactive shop notes and imageName if user has trinkets', async () => {
+ let shop = await user.get('/shops/time-travelers');
+
+ expect(shop.notes).to.eql(t('timeTravelersPopoverNoSubMobile'));
+ expect(shop.imageName).to.eql('npc_timetravelers');
+ });
+
+ it('does not return mystery sets that are already owned', async () => {
+ await user.update({
+ 'items.gear.owned': {
+ head_mystery_201606: true, // eslint-disable-line camelcase
+ armor_mystery_201606: true, // eslint-disable-line camelcase
+ },
+ });
+
+ let shop = await user.get('/shops/time-travelers');
+
+ let categories = shop.categories.map(cat => cat.identifier);
+
+ expect(categories).to.not.include('201606');
+ });
+
+ it('does not return pets and mounts that user already owns', async () => {
+ await user.update({
+ 'items.mounts': {
+ 'MantisShrimp-Base': true,
+ },
+ 'items.pets': {
+ 'Mammoth-Base': 5,
+ },
+ });
+
+ let shop = await user.get('/shops/time-travelers');
+
+ let mammothPet = shop.categories
+ .find(cat => cat.identifier === 'pets')
+ .items
+ .find(pet => pet.key === 'Mammoth-Base');
+ let mantisShrimp = shop.categories
+ .find(cat => cat.identifier === 'mounts')
+ .items
+ .find(pet => pet.key === 'MantisShrimp-Base');
+
+ expect(mammothPet).to.not.exist;
+ expect(mantisShrimp).to.not.exist;
+ });
+});
diff --git a/test/common/libs/shops.js b/test/common/libs/shops.js
new file mode 100644
index 0000000000..16fce945b1
--- /dev/null
+++ b/test/common/libs/shops.js
@@ -0,0 +1,84 @@
+import shared from '../../../common';
+import {
+ generateUser,
+} from '../../helpers/common.helper';
+
+describe('shops', () => {
+ let user = generateUser();
+
+ describe('market', () => {
+ let shopCategories = shared.shops.getMarketCategories(user);
+
+ it('contains at least the 3 default categories', () => {
+ expect(shopCategories.length).to.be.greaterThan(2);
+ });
+
+ it('does not contain an empty category', () => {
+ _.each(shopCategories, (category) => {
+ expect(category.items.length).to.be.greaterThan(0);
+ });
+ });
+
+ it('items contain required fields', () => {
+ _.each(shopCategories, (category) => {
+ _.each(category.items, (item) => {
+ expect(item).to.have.all.keys(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'class']);
+ });
+ });
+ });
+ });
+
+ describe('questShop', () => {
+ let shopCategories = shared.shops.getQuestShopCategories(user);
+
+ it('does not contain an empty category', () => {
+ _.each(shopCategories, (category) => {
+ expect(category.items.length).to.be.greaterThan(0);
+ });
+ });
+
+ it('items contain required fields', () => {
+ _.each(shopCategories, (category) => {
+ _.each(category.items, (item) => {
+ expect(item).to.have.all.keys('key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl');
+ });
+ });
+ });
+ });
+
+ describe('timeTravelers', () => {
+ let shopCategories = shared.shops.getTimeTravelersCategories(user);
+
+ it('does not contain an empty category', () => {
+ _.each(shopCategories, (category) => {
+ expect(category.items.length).to.be.greaterThan(0);
+ });
+ });
+
+ it('items contain required fields', () => {
+ _.each(shopCategories, (category) => {
+ _.each(category.items, (item) => {
+ expect(item).to.have.all.keys('key', 'text', 'value', 'currency', 'locked', 'purchaseType', 'class', 'notes', 'class');
+ });
+ });
+ });
+ });
+
+ describe('seasonalShop', () => {
+ let shopCategories = shared.shops.getSeasonalShopCategories(user);
+
+ it('does not contain an empty category', () => {
+ _.each(shopCategories, (category) => {
+ expect(category.items.length).to.be.greaterThan(0);
+ });
+ });
+
+ it('items contain required fields', () => {
+ _.each(shopCategories, (category) => {
+ _.each(category.items, (item) => {
+ expect(item).to.have.all.keys('key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'specialClass', 'type');
+ });
+ });
+ });
+ });
+});
diff --git a/website/client/js/controllers/inventoryCtrl.js b/website/client/js/controllers/inventoryCtrl.js
index 64c222cb32..84b4ed8d14 100644
--- a/website/client/js/controllers/inventoryCtrl.js
+++ b/website/client/js/controllers/inventoryCtrl.js
@@ -352,6 +352,11 @@ habitrpg.controller("InventoryCtrl",
User.hourglassPurchase({params:{type:type,key:key}});
};
+ $scope.marketShopCategories = Shared.shops.getMarketCategories(user);
+ $scope.questShopCategories = Shared.shops.getQuestShopCategories(user);
+ $scope.timeTravelersCategories = Shared.shops.getTimeTravelersCategories(user);
+ $scope.seasonalShopCategories = Shared.shops.getSeasonalShopCategories(user);
+
function _updateDropAnimalCount(items) {
$scope.petCount = Shared.count.beastMasterProgress(items.pets);
$scope.mountCount = Shared.count.mountMasterProgress(items.mounts);
diff --git a/website/server/controllers/api-v3/shops.js b/website/server/controllers/api-v3/shops.js
new file mode 100644
index 0000000000..5dc8ce615b
--- /dev/null
+++ b/website/server/controllers/api-v3/shops.js
@@ -0,0 +1,123 @@
+import { authWithHeaders } from '../../middlewares/api-v3/auth';
+import shops from '../../../../common/script/libs/shops';
+
+let api = {};
+
+/**
+ * @apiIgnore
+ * @api {get} /api/v3/shops/market get the available items for the market
+ * @apiVersion 3.0.0
+ * @apiName GetMarketItems
+ * @apiGroup Shops
+ *
+ * @apiSuccess {Object} data List of push devices
+ * @apiSuccess {string} message Success message
+ */
+api.getMarketItems = {
+ method: 'GET',
+ url: '/shops/market',
+ middlewares: [authWithHeaders()],
+ async handler (req, res) {
+ let user = res.locals.user;
+
+ let resObject = {
+ identifier: 'market',
+ text: res.t('market'),
+ notes: res.t('welcomeMarketMobile'),
+ imageName: 'npc_alex',
+ categories: shops.getMarketCategories(user, req.language),
+ };
+
+ res.respond(200, resObject);
+ },
+};
+
+/**
+ * @apiIgnore
+ * @api {get} /api/v3/shops/quests get the available items for the quests shop
+ * @apiVersion 3.0.0
+ * @apiName GetQuestShopItems
+ * @apiGroup Shops
+ *
+ * @apiSuccess {Object} data List of push devices
+ * @apiSuccess {string} message Success message
+ */
+api.getQuestShopItems = {
+ method: 'GET',
+ url: '/shops/quests',
+ middlewares: [authWithHeaders()],
+ async handler (req, res) {
+ let user = res.locals.user;
+
+ let resObject = {
+ identifier: 'questShop',
+ text: res.t('quests'),
+ notes: res.t('ianTextMobile'),
+ imageName: 'npc_ian',
+ categories: shops.getQuestShopCategories(user, req.language),
+ };
+
+ res.respond(200, resObject);
+ },
+};
+
+/**
+ * @apiIgnore
+ * @api {get} /api/v3/shops/time-travelers get the available items for the time travelers shop
+ * @apiVersion 3.0.0
+ * @apiName GetTimeTravelersShopItems
+ * @apiGroup Shops
+ *
+ * @apiSuccess {Object} data List of push devices
+ * @apiSuccess {string} message Success message
+ */
+api.getTimeTravelerShopItems = {
+ method: 'GET',
+ url: '/shops/time-travelers',
+ middlewares: [authWithHeaders()],
+ async handler (req, res) {
+ let user = res.locals.user;
+ let hasTrinkets = user.purchased.plan.consecutive.trinkets > 0;
+
+ let resObject = {
+ identifier: 'timeTravelersShop',
+ text: res.t('timeTravelers'),
+ notes: hasTrinkets ? res.t('timeTravelersPopover') : res.t('timeTravelersPopoverNoSubMobile'),
+ imageName: hasTrinkets ? 'npc_timetravelers_active' : 'npc_timetravelers',
+ categories: shops.getTimeTravelersCategories(user, req.language),
+ };
+
+ res.respond(200, resObject);
+ },
+};
+
+/**
+ * @apiIgnore
+ * @api {get} /api/v3/shops/seasonal get the available items for the seasonal shop
+ * @apiVersion 3.0.0
+ * @apiName GetSeasonalShopItems
+ * @apiGroup Shops
+ *
+ * @apiSuccess {Object} data List of push devices
+ * @apiSuccess {string} message Success message
+ */
+api.getSeasonalShopItems = {
+ method: 'GET',
+ url: '/shops/seasonal',
+ middlewares: [authWithHeaders()],
+ async handler (req, res) {
+ let user = res.locals.user;
+
+ let resObject = {
+ identifier: 'seasonalShop',
+ text: res.t('seasonalShop'),
+ notes: res.t('seasonalShopSummerText'),
+ imageName: 'seasonalshop_open',
+ categories: shops.getSeasonalShopCategories(user, req.language),
+ };
+
+ res.respond(200, resObject);
+ },
+};
+
+module.exports = api;
diff --git a/website/views/options/inventory/drops.jade b/website/views/options/inventory/drops.jade
index 67c2b80d4e..4604bcdf9f 100644
--- a/website/views/options/inventory/drops.jade
+++ b/website/views/options/inventory/drops.jade
@@ -110,61 +110,19 @@
button.btn.btn-primary.btn-block(ng-show='selectedFood', ng-click='sellInventory()')=env.t('sellForGold', {item: "{{selectedFood.text()}}", gold: "{{selectedFood.value}}"})
menu.inventory-list(type='list')
- li.customize-menu
- menu.pets-menu(label=env.t('eggs'))
- p.muted!=env.t('dropsExplanation')
+ li.customize-menu(ng-repeat='category in marketShopCategories')
+ menu.pets-menu(label='{{category.text}}')
+ p.muted(ng-bind-html='category.notes')
- div(ng-repeat='egg in Content.eggs', ng-if='egg.canBuy(user)')
- button.customize-option(class='Pet_Egg_{{::egg.key}}',
- popover='{{::egg.notes()}}', popover-append-to-body='true',
- popover-title!=env.t("egg", {eggType: "{{::egg.text()}}"}),
+ div(ng-repeat='item in category.items')
+ button.customize-option(class='{{item.class}}',
+ popover='{{item.notes}}', popover-append-to-body='true',
+ popover-title!='{{item.text}}',
popover-trigger='mouseenter', popover-placement='top',
- ng-click='purchase("eggs", egg)')
- p {{::egg.value}}
- span.Pet_Currency_Gem1x.inline-gems
-
- li.customize-menu
- menu.pets-menu(label=env.t('hatchingPotions'))
- p.muted!=env.t('dropsExplanation')
- div(ng-repeat='pot in Content.hatchingPotions', ng-if='!pot.premium')
- button.customize-option(class='Pet_HatchingPotion_{{::pot.key}}',
- popover='{{::pot.notes()}}', popover-append-to-body='true',
- popover-title!=env.t("potion", {potionType: "{{::pot.text()}}"}),
- popover-trigger='mouseenter', popover-placement='top',
- ng-click='purchase("hatchingPotions", pot)')
- p
- | {{::pot.value}}
- span.Pet_Currency_Gem1x.inline-gems
-
- // li.customize-menu
- menu.pets-menu!=env.t('magicHatchingPotions') + " - " + env.t('springEventAvailability')
- p.muted=env.t('premiumPotionNoDropExplanation')
- div(ng-repeat='pot in Content.hatchingPotions', ng-if='pot.premium && pot.canBuy(user)')
- button.customize-option(class='Pet_HatchingPotion_{{::pot.key}}',
- popover='{{::pot.notes()}} {{::pot.addlNotes()}}', popover-append-to-body='true',
- popover-title!=env.t("potion", {potionType: "{{::pot.text()}}"}),
- popover-trigger='mouseenter', popover-placement='top',
- ng-click='purchase("hatchingPotions", pot)')
- p
- | {{::pot.value}}
- span.Pet_Currency_Gem1x.inline-gems
-
- li.customize-menu
- menu.pets-menu(label=env.t('food'))
- p.muted!=env.t('dropsExplanation')
- div(ng-repeat='food in Content.food', ng-if='food.canBuy(user)')
- button.customize-option(class='Pet_Food_{{::food.key}}',
- popover='{{::food.notes()}}', popover-title='{{::food.text()}}',
- popover-trigger='mouseenter', popover-placement='top',
- popover-append-to-body='true',
- ng-click='purchase("food", food)')
- p
- | {{::food.value}}
- span.Pet_Currency_Gem1x.inline-gems
-
- li.customize-menu
- menu.pets-menu(label=env.t('quests'))
- p=env.t('whereAreMyQuests')
+ ng-click='purchase(category.identifier, item)')
+ p {{item.value}}
+ span.Pet_Currency_Gem1x.inline-gems(ng-if='item.currency === "gems"')
+ span(class='shop_gold', ng-if='item.currency === "gold"')
li.customize-menu
menu.pets-menu(label=env.t('special'))
diff --git a/website/views/options/inventory/quests.jade b/website/views/options/inventory/quests.jade
index 53a8dfa3e4..b53a3c6422 100644
--- a/website/views/options/inventory/quests.jade
+++ b/website/views/options/inventory/quests.jade
@@ -18,20 +18,20 @@ include ../../shared/mixins
.col-md-6.border-left
li.customize-menu
h3.equipment-title=env.t('questsForSale')
- div(ng-repeat='type in Content.userCanOwnQuestCategories')
- menu.pets-menu(label='{{env.t(type + "Quests")}}')
- div(ng-repeat='quest in Content.questsByLevel', ng-if='quest.canBuy(user) && quest.category === type')
- button.customize-option(ng-class='lockQuest(quest) ? "inventory_quest_scroll_locked inventory_quest_scroll_{{::quest.key}}_locked locked" : "inventory_quest_scroll inventory_quest_scroll_{{::quest.key}}"',
- data-popover-html="{{::lockQuest(quest,true) ? env.t('scrollsPre') : questPopover(quest) | markdown}}",
- popover-title='{{::quest.text()}}', popover-append-to-body="true",
+ div(ng-repeat='category in questShopCategories')
+ menu.pets-menu(label='{{category.text}}')
+ div(ng-repeat='quest in category.items')
+ button.customize-option(ng-class='quest.class',
+ data-popover-html="{{quest.locked ? env.t('scrollsPre') : questPopover(quest) | markdown}}",
+ popover-title='{{quest.text}}', popover-append-to-body="true",
popover-trigger='mouseenter', ng-click='buyQuest(quest.key)')
p(ng-if='quest.unlockCondition')
| {{::quest.unlockCondition.text()}}
- p(ng-if='!quest.unlockCondition && quest.category !== "gold" && !lockQuest(quest)')
+ p(ng-if='!quest.unlockCondition && category.identifier !== "gold" && !quest.locked')
| {{::quest.value}}
span.Pet_Currency_Gem1x.inline-gems
- p(ng-if='quest.category === "gold" && !lockQuest(quest)')
- | {{::quest.goldValue}}
+ p(ng-if='category.identifier === "gold" && !quest.locked')
+ | {{::quest.value}}
span.shop_gold
- p(ng-if='quest.lvl && lockQuest(quest)')=env.t('level')
+ p(ng-if='quest.lvl && quest.locked')=env.t('level')
| {{::quest.lvl}}
diff --git a/website/views/options/inventory/seasonal-shop.jade b/website/views/options/inventory/seasonal-shop.jade
index 2c1ab34a81..cb3b683921 100644
--- a/website/views/options/inventory/seasonal-shop.jade
+++ b/website/views/options/inventory/seasonal-shop.jade
@@ -11,15 +11,6 @@
.well(ng-if='User.user.achievements.rebirths > 0')=env.t('seasonalShopRebirth')
li.customize-menu.inventory-gear
- // menu.pets-menu(label=env.t('quests'))
- div(ng-repeat='quest in ::getSeasonalShopQuests()')
- button.customize-option(ng-class='(quest.previous && !user.achievements.quests[quest.previous]) ? "inventory_quest_scroll_locked inventory_quest_scroll_{{::quest.key}}_locked locked" : "inventory_quest_scroll inventory_quest_scroll_{{::quest.key}}"'
- data-popover-html="{{::quest.previous && !user.achievements.quests[quest.previous] ? env.t('scrollsPre') : questPopover(quest) | markdown}}",
- popover-append-to-body='true', popover-title='{{::quest.text()}}',
- popover-trigger='mouseenter', popover-placement='right',
- ng-click='buyQuest(quest.key)')
- p {{::quest.value}}
- span.Pet_Currency_Gem1x.inline-gems
menu.pets-menu(label=env.t('seasonalItems'))
div
button.customize-option(class='inventory_special_seafoam',
@@ -30,24 +21,12 @@
ng-click='purchase("special", Content.spells.special.seafoam)')
p {{::Content.spells.special.seafoam.value}}
span(class='shop_gold')
- // div
- button.customize-option(class='Pet_HatchingPotion_Peppermint',
- popover='{{::Content.hatchingPotions.Peppermint.notes()}}',
- popover-title!=env.t("potion", {potionType: "{{::Content.hatchingPotions.Peppermint.text()}}"}),
- popover-trigger='mouseenter', popover-placement='right',
- popover-append-to-body='true',
- ng-click='purchase("hatchingPotions", Content.hatchingPotions.Peppermint)')
- p {{::Content.hatchingPotions.Peppermint.value}}
- span.Pet_Currency_Gem1x.inline-gems
- // div
- button.customize-option(popover='{{::Content.spells.special.nye.notes()}}', popover-title='{{::Content.spells.special.nye.text()}}', popover-trigger='mouseenter', popover-placement='right', popover-append-to-body='true', ng-click='castStart(Content.spells.special.nye)', class='inventory_special_nye')
- p {{Content.spells.special.nye.value}}
- span(class='shop_gold')
- menu.pets-menu(label='{{::label}}', ng-repeat='(set,label) in ::{summerWarrior:env.t("daringSwashbucklerSet"), summerMage:env.t("emeraldMermageSet"), summerHealer:env.t("reefSeahealerSet"), summerRogue:env.t("roguishPirateSet"), summer2015Warrior:env.t("sunfishWarriorSet"), summer2015Mage:env.t("shipSoothsayerSet"), summer2015Healer:env.t("strappingSailorSet"), summer2015Rogue:env.t("reefRenegadeSet")}')
- div(ng-repeat='item in ::getSeasonalShopArray(set)',
- ng-class="{transparent: user.items.gear.owned[item.key] !== undefined}")
- button.customize-option(class='shop_{{::item.key}}',
- popover='{{::item.notes()}}', popover-title='{{::item.text()}}',
+ menu.pets-menu(label='{{category.text}}', ng-repeat='category in seasonalShopCategories')
+ div(ng-repeat='item in category.items',
+ ng-class="{transparent: user.items.gear.owned[item.key] !== undefined}",
+ ng-if='!user.items.gear.owned[item.key]')
+ button.customize-option(class='shop_{{item.key}}',
+ popover='{{item.notes}}', popover-title='{{item.text}}',
popover-trigger='mouseenter', popover-placement='right',
popover-append-to-body='true',
ng-click='purchase(item.type,item)')
diff --git a/website/views/options/inventory/time-travelers.jade b/website/views/options/inventory/time-travelers.jade
index be0c3ea7dd..80b3a35e7e 100644
--- a/website/views/options/inventory/time-travelers.jade
+++ b/website/views/options/inventory/time-travelers.jade
@@ -20,19 +20,10 @@
.row
.col-md-12
li.customize-menu.inventory-gear
- each prepend, type in {pets:'Pet-', mounts:'Mount_Head_'}
- menu.pets-menu(label=env.t('#{type}'), ng-if='!hasAllTimeTravelerItemsOfType("#{type}")')
- div(ng-repeat='(item, text) in Content.timeTravelStable["#{type}"]', style='margin-top:0')
- button.pet-button(class='#{prepend}{{::item}}', style='margin-top:0',
- ng-if='!user.items["#{type}"][item]',
- popover='{{::text()}}', popover-trigger='mouseenter',
- popover-placement='right', popover-append-to-body='true',
- ng-click='clickTimeTravelItem("#{type}",item)')
- li.customize-menu.inventory-gear
- menu.pets-menu(label='{{::set.text()}}', ng-repeat='set in Content.timeTravelerStore(user.items.gear.owned)')
- div(ng-repeat='item in set.items')
- button.customize-option(class='shop_{{::item.key}}',
- popover='{{::item.notes()}}', popover-title='{{::item.text()}}',
+ menu.pets-menu(label='{{::category.text}}', ng-repeat='category in timeTravelersCategories')
+ div(ng-repeat='item in category.items', ng-if='category.identifier === "pets" || category.identifier === "mounts" ? !user.items[category.identifier][item.key] : !user.items.gear.owned[item.key]')
+ button.customize-option(class='{{item.class ? item.class : "shop_"+item.key}}',
+ popover='{{item.notes}}', popover-title='{{item.text}}',
popover-trigger='mouseenter', popover-placement='right',
popover-append-to-body='true',
- ng-click='User.buyMysterySet({params:{key:set.key}})')
+ ng-click='category.purchaseAll ? User.buyMysterySet({params:{key:category.identifier}}) : clickTimeTravelItem(category.identifier,item.key)')