mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 22:57:21 +01:00
Merge branch 'develop' into release
This commit is contained in:
@@ -6,6 +6,9 @@ export const MAX_INCENTIVES = 500;
|
||||
|
||||
export const TAVERN_ID = '00000000-0000-4000-A000-000000000000';
|
||||
export const LARGE_GROUP_COUNT_MESSAGE_CUTOFF = 5000;
|
||||
export const MAX_SUMMARY_SIZE_FOR_GUILDS = 250;
|
||||
export const MAX_SUMMARY_SIZE_FOR_CHALLENGES = 250;
|
||||
export const MIN_SHORTNAME_SIZE_FOR_CHALLENGES = 3;
|
||||
|
||||
export const SUPPORTED_SOCIAL_NETWORKS = [
|
||||
{key: 'facebook', name: 'Facebook'},
|
||||
|
||||
@@ -598,10 +598,20 @@ let backgrounds = {
|
||||
};
|
||||
/* eslint-enable quote-props */
|
||||
|
||||
forOwn(backgrounds, function prefillBackgroundSet (value) {
|
||||
forOwn(value, function prefillBackground (bgObject) {
|
||||
bgObject.price = 7;
|
||||
let flat = {};
|
||||
|
||||
forOwn(backgrounds, function prefillBackgroundSet (backgroundsInSet, set) {
|
||||
forOwn(backgroundsInSet, function prefillBackground (background, bgKey) {
|
||||
background.key = bgKey;
|
||||
background.set = set;
|
||||
background.price = 7;
|
||||
|
||||
flat[bgKey] = background;
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = backgrounds;
|
||||
module.exports = {
|
||||
tree: backgrounds,
|
||||
flat,
|
||||
};
|
||||
|
||||
|
||||
@@ -32,6 +32,34 @@ export const EVENTS = {
|
||||
fall2017: { start: '2017-09-21', end: '2017-11-02' },
|
||||
};
|
||||
|
||||
export const SEASONAL_SETS = {
|
||||
fall: [
|
||||
// fall 2014
|
||||
'vampireSmiterSet',
|
||||
'monsterOfScienceSet',
|
||||
'witchyWizardSet',
|
||||
'mummyMedicSet',
|
||||
|
||||
// fall 2015
|
||||
'battleRogueSet',
|
||||
'scarecrowWarriorSet',
|
||||
'stitchWitchSet',
|
||||
'potionerSet',
|
||||
|
||||
// fall 2016
|
||||
'fall2016BlackWidowSet',
|
||||
'fall2016SwampThingSet',
|
||||
'fall2016WickedSorcererSet',
|
||||
'fall2016GorgonHealerSet',
|
||||
|
||||
// fall 2017
|
||||
'fall2017TrickOrTreatSet',
|
||||
'fall2017HabitoweenSet',
|
||||
'fall2017MasqueradeSet',
|
||||
'fall2017HauntedHouseSet',
|
||||
],
|
||||
};
|
||||
|
||||
export const GEAR_TYPES = [
|
||||
'weapon',
|
||||
'armor',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import defaults from 'lodash/defaults';
|
||||
import each from 'lodash/each';
|
||||
import includes from 'lodash/includes';
|
||||
import moment from 'moment';
|
||||
import t from './translation';
|
||||
|
||||
@@ -25,7 +24,7 @@ import {
|
||||
} from './quests';
|
||||
|
||||
import appearances from './appearance';
|
||||
import backgrounds from './appearance/backgrounds.js';
|
||||
import backgrounds from './appearance/backgrounds';
|
||||
import spells from './spells';
|
||||
import subscriptionBlocks from './subscriptionBlocks';
|
||||
import faq from './faq';
|
||||
@@ -33,6 +32,8 @@ import timeTravelers from './time-travelers';
|
||||
|
||||
import loginIncentives from './loginIncentives';
|
||||
|
||||
import officialPinnedItems from './officialPinnedItems';
|
||||
|
||||
api.achievements = achievements;
|
||||
|
||||
api.quests = quests;
|
||||
@@ -45,9 +46,13 @@ api.gear = gear;
|
||||
api.spells = spells;
|
||||
api.subscriptionBlocks = subscriptionBlocks;
|
||||
|
||||
api.audioThemes = ['danielTheBard', 'gokulTheme', 'luneFoxTheme', 'wattsTheme', 'rosstavoTheme', 'dewinTheme', 'airuTheme', 'beatscribeNesTheme', 'arashiTheme'];
|
||||
|
||||
api.mystery = timeTravelers.mystery;
|
||||
api.timeTravelerStore = timeTravelers.timeTravelerStore;
|
||||
|
||||
api.officialPinnedItems = officialPinnedItems;
|
||||
|
||||
/*
|
||||
---------------------------------------------------------------
|
||||
Discounted Item Bundles
|
||||
@@ -68,6 +73,7 @@ api.bundles = {
|
||||
return moment().isBetween('2017-05-16', '2017-05-31');
|
||||
},
|
||||
type: 'quests',
|
||||
class: 'quest_bundle_featheredFriends',
|
||||
value: 7,
|
||||
},
|
||||
splashyPals: {
|
||||
@@ -83,6 +89,7 @@ api.bundles = {
|
||||
return moment().isBetween('2017-07-11', '2017-08-02');
|
||||
},
|
||||
type: 'quests',
|
||||
class: 'quest_bundle_splashyPals',
|
||||
value: 7,
|
||||
},
|
||||
farmFriends: {
|
||||
@@ -127,8 +134,8 @@ api.armoire = {
|
||||
},
|
||||
value: 100,
|
||||
key: 'armoire',
|
||||
canOwn (u) {
|
||||
return includes(u.achievements.ultimateGearSets, true);
|
||||
canOwn () {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -533,7 +540,8 @@ each(api.food, (food, key) => {
|
||||
|
||||
api.appearances = appearances;
|
||||
|
||||
api.backgrounds = backgrounds;
|
||||
api.backgrounds = backgrounds.tree;
|
||||
api.backgroundsFlat = backgrounds.flat;
|
||||
|
||||
api.userDefaults = {
|
||||
habits: [
|
||||
|
||||
@@ -204,6 +204,7 @@ let mysterySets = {
|
||||
each(mysterySets, (value, key) => {
|
||||
value.key = key;
|
||||
value.text = t(`mysterySet${key}`);
|
||||
value.class = `shop_set_mystery_${key}`;
|
||||
});
|
||||
|
||||
module.exports = mysterySets;
|
||||
|
||||
4
website/common/script/content/officialPinnedItems.js
Normal file
4
website/common/script/content/officialPinnedItems.js
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
// { path: '', type: '', canShow?: (user) => boolean }
|
||||
|
||||
export default [];
|
||||
@@ -1054,6 +1054,7 @@ let quests = {
|
||||
basilist: {
|
||||
text: t('questBasilistText'),
|
||||
notes: t('questBasilistNotes'),
|
||||
group: 'questGroupEarnable',
|
||||
completion: t('questBasilistCompletion'),
|
||||
value: 4,
|
||||
category: 'unlockable',
|
||||
@@ -2224,6 +2225,7 @@ let quests = {
|
||||
dustbunnies: {
|
||||
text: t('questDustBunniesText'),
|
||||
notes: t('questDustBunniesNotes'),
|
||||
group: 'questGroupEarnable',
|
||||
completion: t('questDustBunniesCompletion'),
|
||||
value: 4,
|
||||
category: 'unlockable',
|
||||
|
||||
@@ -1,19 +1,44 @@
|
||||
const featuredItems = {
|
||||
market: [
|
||||
'head_armoire_vikingHelm',
|
||||
'weapon_special_1',
|
||||
'shield_special_0',
|
||||
'armor_warrior_5',
|
||||
],
|
||||
quests: [
|
||||
'dilatoryDistress1',
|
||||
'dilatoryDistress2',
|
||||
'dilatoryDistress3',
|
||||
],
|
||||
seasonal: 'summerMage',
|
||||
timeTravelers: [
|
||||
market: [
|
||||
{
|
||||
type: 'armoire',
|
||||
path: 'armoire',
|
||||
},
|
||||
{
|
||||
type: 'hatchingPotions',
|
||||
path: 'hatchingPotions.Golden',
|
||||
},
|
||||
{
|
||||
type: 'food',
|
||||
path: 'food.Saddle',
|
||||
},
|
||||
{
|
||||
type: 'card',
|
||||
path: 'cardTypes.greeting',
|
||||
},
|
||||
],
|
||||
quests: [
|
||||
{
|
||||
type: 'quests',
|
||||
path: 'quests.gryphon',
|
||||
},
|
||||
{
|
||||
type: 'quests',
|
||||
path: 'quests.dilatoryDistress1',
|
||||
},
|
||||
{
|
||||
type: 'quests',
|
||||
path: 'quests.nudibranch',
|
||||
},
|
||||
{
|
||||
type: 'quests',
|
||||
path: 'quests.taskwoodsTerror1',
|
||||
},
|
||||
],
|
||||
seasonal: 'summerMage',
|
||||
timeTravelers: [
|
||||
// TODO
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
export default featuredItems;
|
||||
|
||||
@@ -2,6 +2,8 @@ import t from './translation';
|
||||
import each from 'lodash/each';
|
||||
import { NotAuthorized } from '../libs/errors';
|
||||
import statsComputed from '../libs/statsComputed';
|
||||
import crit from '../fns/crit';
|
||||
import updateStats from '../fns/updateStats';
|
||||
|
||||
/*
|
||||
---------------------------------------------------------------
|
||||
@@ -15,7 +17,7 @@ import statsComputed from '../libs/statsComputed';
|
||||
|
||||
* {cast}: the function that's run to perform the ability's action. This is pretty slick - because this is exported to the
|
||||
web, this function can be performed on the client and on the server. `user` param is self (needed for determining your
|
||||
own stats for effectiveness of cast), and `target` param is one of [task, party, user]. In the case of `self` spells,
|
||||
own stats for effectiveness of cast), and `target` param is one of [task, party, user]. In the case of `self` skills,
|
||||
you act on `user` instead of `target`. You can trust these are the correct objects, as long as the `target` attr of the
|
||||
spell is correct. Take a look at habitrpg/website/server/models/user.js and habitrpg/website/server/models/task.js for what attributes are
|
||||
available on each model. Note `task.value` is its "redness". If party is passed in, it's an array of users,
|
||||
@@ -29,8 +31,8 @@ function diminishingReturns (bonus, max, halfway) {
|
||||
return max * (bonus / (bonus + halfway));
|
||||
}
|
||||
|
||||
function calculateBonus (value, stat, crit = 1, statScale = 0.5) {
|
||||
return (value < 0 ? 1 : value + 1) + stat * statScale * crit;
|
||||
function calculateBonus (value, stat, critVal = 1, statScale = 0.5) {
|
||||
return (value < 0 ? 1 : value + 1) + stat * statScale * critVal;
|
||||
}
|
||||
|
||||
let spells = {};
|
||||
@@ -43,12 +45,12 @@ spells.wizard = {
|
||||
target: 'task',
|
||||
notes: t('spellWizardFireballNotes'),
|
||||
cast (user, target, req) {
|
||||
let bonus = statsComputed(user).int * user.fns.crit('per');
|
||||
let bonus = statsComputed(user).int * crit.crit(user, 'per');
|
||||
bonus *= Math.ceil((target.value < 0 ? 1 : target.value + 1) * 0.075);
|
||||
user.stats.exp += diminishingReturns(bonus, 75);
|
||||
if (!user.party.quest.progress.up) user.party.quest.progress.up = 0;
|
||||
user.party.quest.progress.up += Math.ceil(statsComputed(user).int * 0.1);
|
||||
user.fns.updateStats(user.stats, req);
|
||||
updateStats(user, user.stats, req);
|
||||
},
|
||||
},
|
||||
mpheal: { // Ethereal Surge
|
||||
@@ -100,7 +102,7 @@ spells.warrior = {
|
||||
target: 'task',
|
||||
notes: t('spellWarriorSmashNotes'),
|
||||
cast (user, target) {
|
||||
let bonus = statsComputed(user).str * user.fns.crit('con');
|
||||
let bonus = statsComputed(user).str * crit.crit(user, 'con');
|
||||
target.value += diminishingReturns(bonus, 2.5, 35);
|
||||
if (!user.party.quest.progress.up) user.party.quest.progress.up = 0;
|
||||
user.party.quest.progress.up += diminishingReturns(bonus, 55, 70);
|
||||
@@ -167,11 +169,11 @@ spells.rogue = {
|
||||
target: 'task',
|
||||
notes: t('spellRogueBackStabNotes'),
|
||||
cast (user, target, req) {
|
||||
let _crit = user.fns.crit('str', 0.3);
|
||||
let _crit = crit.crit(user, 'str', 0.3);
|
||||
let bonus = calculateBonus(target.value, statsComputed(user).str, _crit);
|
||||
user.stats.exp += diminishingReturns(bonus, 75, 50);
|
||||
user.stats.gp += diminishingReturns(bonus, 18, 75);
|
||||
user.fns.updateStats(user.stats, req);
|
||||
updateStats(user, user.stats, req);
|
||||
},
|
||||
},
|
||||
toolsOfTrade: { // Tools of the Trade
|
||||
|
||||
@@ -69,6 +69,7 @@ let specialPets = {
|
||||
'JackOLantern-Ghost': 'ghostJackolantern',
|
||||
'Jackalope-RoyalPurple': 'royalPurpleJackalope',
|
||||
'Orca-Base': 'orca',
|
||||
'Bear-Veteran': 'veteranBear',
|
||||
};
|
||||
|
||||
let specialMounts = {
|
||||
|
||||
@@ -25,6 +25,9 @@ import {
|
||||
MAX_INCENTIVES,
|
||||
TAVERN_ID,
|
||||
LARGE_GROUP_COUNT_MESSAGE_CUTOFF,
|
||||
MAX_SUMMARY_SIZE_FOR_GUILDS,
|
||||
MAX_SUMMARY_SIZE_FOR_CHALLENGES,
|
||||
MIN_SHORTNAME_SIZE_FOR_CHALLENGES,
|
||||
SUPPORTED_SOCIAL_NETWORKS,
|
||||
GUILDS_PER_PAGE,
|
||||
PARTY_LIMIT_MEMBERS,
|
||||
@@ -33,6 +36,9 @@ import {
|
||||
api.constants = {
|
||||
MAX_INCENTIVES,
|
||||
LARGE_GROUP_COUNT_MESSAGE_CUTOFF,
|
||||
MAX_SUMMARY_SIZE_FOR_GUILDS,
|
||||
MAX_SUMMARY_SIZE_FOR_CHALLENGES,
|
||||
MIN_SHORTNAME_SIZE_FOR_CHALLENGES,
|
||||
SUPPORTED_SOCIAL_NETWORKS,
|
||||
GUILDS_PER_PAGE,
|
||||
PARTY_LIMIT_MEMBERS,
|
||||
@@ -64,6 +70,9 @@ api.preenTodos = preenTodos;
|
||||
import updateStore from './libs/updateStore';
|
||||
api.updateStore = updateStore;
|
||||
|
||||
import inAppRewards from './libs/inAppRewards';
|
||||
api.inAppRewards = inAppRewards;
|
||||
|
||||
import uuid from './libs/uuid';
|
||||
api.uuid = uuid;
|
||||
|
||||
@@ -161,6 +170,7 @@ import deletePM from './ops/deletePM';
|
||||
import reroll from './ops/reroll';
|
||||
import reset from './ops/reset';
|
||||
import markPmsRead from './ops/markPMSRead';
|
||||
import pinnedGearUtils from './ops/pinnedGearUtils';
|
||||
|
||||
api.ops = {
|
||||
scoreTask,
|
||||
@@ -198,6 +208,7 @@ api.ops = {
|
||||
reroll,
|
||||
reset,
|
||||
markPmsRead,
|
||||
pinnedGearUtils,
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
326
website/common/script/libs/getItemInfo.js
Normal file
326
website/common/script/libs/getItemInfo.js
Normal file
@@ -0,0 +1,326 @@
|
||||
import i18n from '../i18n';
|
||||
import content from '../content/index';
|
||||
import { BadRequest } from './errors';
|
||||
import count from '../count';
|
||||
|
||||
import isPinned from './isPinned';
|
||||
import getOfficialPinnedItems from './getOfficialPinnedItems';
|
||||
|
||||
import _mapValues from 'lodash/mapValues';
|
||||
|
||||
function lockQuest (quest, user) {
|
||||
if (quest.lvl && user.stats.lvl < quest.lvl) return true;
|
||||
if (quest.unlockCondition && (quest.key === 'moon1' || quest.key === 'moon2' || quest.key === 'moon3')) {
|
||||
return user.loginIncentives < quest.unlockCondition.incentiveThreshold;
|
||||
}
|
||||
if (user.achievements.quests) return quest.previous && !user.achievements.quests[quest.previous];
|
||||
return quest.previous;
|
||||
}
|
||||
|
||||
function isItemSuggested (officialPinnedItems, itemInfo) {
|
||||
return officialPinnedItems.findIndex(officialItem => {
|
||||
return officialItem.type === itemInfo.pinType && officialItem.path === itemInfo.path;
|
||||
}) > -1;
|
||||
}
|
||||
|
||||
function getDefaultGearProps (item, language) {
|
||||
return {
|
||||
key: item.key,
|
||||
text: item.text(language),
|
||||
notes: item.notes(language),
|
||||
type: item.type,
|
||||
specialClass: item.specialClass,
|
||||
locked: false,
|
||||
purchaseType: 'gear',
|
||||
class: `shop_${item.key}`,
|
||||
path: `gear.flat.${item.key}`,
|
||||
str: item.str,
|
||||
int: item.int,
|
||||
per: item.per,
|
||||
con: item.con,
|
||||
klass: item.klass,
|
||||
event: item.event,
|
||||
set: item.set,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = function getItemInfo (user, type, item, officialPinnedItems, language = 'en') {
|
||||
if (officialPinnedItems === undefined) {
|
||||
officialPinnedItems = getOfficialPinnedItems(user);
|
||||
}
|
||||
|
||||
let itemInfo;
|
||||
|
||||
switch (type) {
|
||||
case 'eggs':
|
||||
itemInfo = {
|
||||
key: item.key,
|
||||
text: i18n.t('egg', {eggType: item.text(language)}, language),
|
||||
notes: item.notes(language),
|
||||
value: item.value,
|
||||
class: `Pet_Egg_${item.key}`,
|
||||
locked: false,
|
||||
currency: 'gems',
|
||||
purchaseType: 'eggs',
|
||||
path: `eggs.${item.key}`,
|
||||
pinType: 'eggs',
|
||||
};
|
||||
break;
|
||||
case 'hatchingPotions':
|
||||
itemInfo = {
|
||||
key: item.key,
|
||||
text: i18n.t('potion', {potionType: item.text(language)}),
|
||||
notes: item.notes(language),
|
||||
class: `Pet_HatchingPotion_${item.key}`,
|
||||
value: item.value,
|
||||
locked: false,
|
||||
currency: 'gems',
|
||||
purchaseType: 'hatchingPotions',
|
||||
path: `hatchingPotions.${item.key}`,
|
||||
pinType: 'hatchingPotions',
|
||||
};
|
||||
break;
|
||||
case 'premiumHatchingPotion':
|
||||
itemInfo = {
|
||||
key: item.key,
|
||||
text: i18n.t('potion', {potionType: item.text(language)}),
|
||||
notes: `${item.notes(language)} ${item._addlNotes(language)}`,
|
||||
class: `Pet_HatchingPotion_${item.key}`,
|
||||
value: item.value,
|
||||
locked: false,
|
||||
currency: 'gems',
|
||||
purchaseType: 'hatchingPotions',
|
||||
path: `premiumHatchingPotions.${item.key}`,
|
||||
pinType: 'premiumHatchingPotion',
|
||||
};
|
||||
break;
|
||||
case 'food':
|
||||
itemInfo = {
|
||||
key: item.key,
|
||||
text: item.text(language),
|
||||
notes: item.notes(language),
|
||||
class: `Pet_Food_${item.key}`,
|
||||
value: item.value,
|
||||
locked: false,
|
||||
currency: 'gems',
|
||||
purchaseType: 'food',
|
||||
path: `food.${item.key}`,
|
||||
pinType: 'food',
|
||||
};
|
||||
break;
|
||||
case 'bundles':
|
||||
itemInfo = {
|
||||
key: item.key,
|
||||
text: item.text(language),
|
||||
notes: item.notes(language),
|
||||
value: item.value,
|
||||
currency: 'gems',
|
||||
class: `quest_bundle_${item.key}`,
|
||||
purchaseType: 'bundles',
|
||||
path: `bundles.${item.key}`,
|
||||
pinType: 'bundles',
|
||||
};
|
||||
break;
|
||||
case 'quests': // eslint-disable-line no-case-declarations
|
||||
const locked = lockQuest(item, user);
|
||||
|
||||
itemInfo = {
|
||||
key: item.key,
|
||||
text: item.text(language),
|
||||
notes: item.notes(language),
|
||||
group: item.group,
|
||||
value: item.goldValue ? item.goldValue : item.value,
|
||||
currency: item.goldValue ? 'gold' : 'gems',
|
||||
locked,
|
||||
unlockCondition: item.unlockCondition,
|
||||
drop: item.drop,
|
||||
boss: item.boss,
|
||||
collect: item.collect ? _mapValues(item.collect, (o) => {
|
||||
return {
|
||||
count: o.count,
|
||||
text: o.text(),
|
||||
};
|
||||
}) : undefined,
|
||||
lvl: item.lvl,
|
||||
class: locked ? `inventory_quest_scroll_${item.key}_locked` : `inventory_quest_scroll_${item.key}`,
|
||||
purchaseType: 'quests',
|
||||
path: `quests.${item.key}`,
|
||||
pinType: 'quests',
|
||||
};
|
||||
|
||||
break;
|
||||
case 'timeTravelers':
|
||||
// TODO
|
||||
itemInfo = {};
|
||||
break;
|
||||
case 'seasonalSpell':
|
||||
itemInfo = {
|
||||
key: item.key,
|
||||
text: item.text(language),
|
||||
notes: item.notes(language),
|
||||
value: item.value,
|
||||
type: 'special',
|
||||
currency: 'gold',
|
||||
locked: false,
|
||||
purchaseType: 'spells',
|
||||
class: `inventory_special_${item.key}`,
|
||||
path: `spells.special.${item.key}`,
|
||||
pinType: 'seasonalSpell',
|
||||
};
|
||||
break;
|
||||
case 'seasonalQuest':
|
||||
itemInfo = {
|
||||
key: item.key,
|
||||
text: item.text(language),
|
||||
notes: item.notes(language),
|
||||
value: item.value,
|
||||
type: 'quests',
|
||||
currency: 'gems',
|
||||
locked: false,
|
||||
drop: item.drop,
|
||||
boss: item.boss,
|
||||
collect: item.collect,
|
||||
class: `inventory_quest_scroll_${item.key}`,
|
||||
purchaseType: 'quests',
|
||||
path: `quests.${item.key}`,
|
||||
pinType: 'seasonalQuest',
|
||||
};
|
||||
break;
|
||||
case 'gear':
|
||||
// spread operator not available
|
||||
itemInfo = Object.assign(getDefaultGearProps(item, language), {
|
||||
value: item.twoHanded ? 2 : 1,
|
||||
currency: 'gems',
|
||||
pinType: 'gear',
|
||||
});
|
||||
break;
|
||||
case 'marketGear':
|
||||
itemInfo = Object.assign(getDefaultGearProps(item, language), {
|
||||
value: item.value,
|
||||
currency: 'gold',
|
||||
pinType: 'marketGear',
|
||||
canOwn: item.canOwn,
|
||||
});
|
||||
break;
|
||||
case 'background':
|
||||
itemInfo = {
|
||||
key: item.key,
|
||||
text: item.text(language),
|
||||
notes: item.notes(language),
|
||||
class: `icon_background_${item.key}`,
|
||||
value: item.price,
|
||||
currency: item.currency || 'gems',
|
||||
purchaseType: 'backgrounds',
|
||||
path: `backgrounds.${item.set}.${item.key}`,
|
||||
pinType: 'background',
|
||||
};
|
||||
break;
|
||||
case 'mystery_set':
|
||||
itemInfo = {
|
||||
key: item.key,
|
||||
text: item.text(language),
|
||||
value: 1,
|
||||
currency: 'hourglasses',
|
||||
purchaseType: 'mystery_set',
|
||||
class: `shop_set_mystery_${item.key}`,
|
||||
path: `mystery.${item.key}`,
|
||||
pinType: 'mystery_set',
|
||||
};
|
||||
break;
|
||||
case 'potion':
|
||||
itemInfo = {
|
||||
key: item.key,
|
||||
text: item.text(language),
|
||||
notes: item.notes(language),
|
||||
value: item.value,
|
||||
currency: 'gold',
|
||||
purchaseType: 'potions',
|
||||
class: `shop_${item.key}`,
|
||||
path: 'potion',
|
||||
pinType: 'potion',
|
||||
};
|
||||
break;
|
||||
case 'armoire':
|
||||
itemInfo = {
|
||||
key: item.key,
|
||||
text: item.text(language),
|
||||
notes: item.notes(user, count.remainingGearInSet(user.items.gear.owned, 'armoire')), // TODO count
|
||||
value: item.value,
|
||||
currency: 'gold',
|
||||
purchaseType: 'armoire',
|
||||
class: `shop_${item.key}`,
|
||||
path: 'armoire',
|
||||
pinType: 'armoire',
|
||||
};
|
||||
break;
|
||||
case 'card': {
|
||||
let spellInfo = content.spells.special[item.key];
|
||||
|
||||
itemInfo = {
|
||||
key: item.key,
|
||||
purchaseType: 'card',
|
||||
class: `inventory_special_${item.key}`,
|
||||
text: spellInfo.text(),
|
||||
notes: spellInfo.notes(),
|
||||
value: spellInfo.value,
|
||||
currency: 'gold',
|
||||
path: `cardTypes.${item.key}`,
|
||||
pinType: 'card',
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'gem': {
|
||||
itemInfo = {
|
||||
key: 'gem',
|
||||
purchaseType: 'gems',
|
||||
class: 'gem',
|
||||
text: i18n.t('subGemName'),
|
||||
notes: i18n.t('subGemPop'),
|
||||
value: 20,
|
||||
currency: 'gold',
|
||||
path: 'special.gems',
|
||||
pinType: 'gem',
|
||||
locked: !user.purchased.plan.customerId,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'rebirth_orb': {
|
||||
itemInfo = {
|
||||
key: 'rebirth_orb',
|
||||
purchaseType: 'rebirth_orb',
|
||||
class: 'rebirth_orb',
|
||||
text: i18n.t('rebirthName'),
|
||||
notes: i18n.t('rebirthPop'),
|
||||
value: user.stats.lvl < 100 ? 6 : 0,
|
||||
currency: 'gems',
|
||||
path: 'special.rebirth_orb',
|
||||
pinType: 'rebirth_orb',
|
||||
locked: !user.flags.rebirthEnabled,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'fortify': {
|
||||
itemInfo = {
|
||||
key: 'fortify',
|
||||
purchaseType: 'fortify',
|
||||
class: 'inventory_special_fortify',
|
||||
text: i18n.t('fortifyName'),
|
||||
notes: i18n.t('fortifyPop'),
|
||||
value: 4,
|
||||
currency: 'gems',
|
||||
path: 'special.fortify',
|
||||
pinType: 'fortify',
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (itemInfo) {
|
||||
itemInfo.isSuggested = isItemSuggested(officialPinnedItems, itemInfo);
|
||||
itemInfo.pinned = isPinned(user, itemInfo);
|
||||
} else {
|
||||
throw new BadRequest(i18n.t('wrongItemType', {type}, language));
|
||||
}
|
||||
|
||||
return itemInfo;
|
||||
};
|
||||
28
website/common/script/libs/getOfficialPinnedItems.js
Normal file
28
website/common/script/libs/getOfficialPinnedItems.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import content from '../content/index';
|
||||
import SeasonalShopConfig from '../libs/shops-seasonal.config';
|
||||
import toArray from 'lodash/toArray';
|
||||
|
||||
const officialPinnedItems = content.officialPinnedItems;
|
||||
|
||||
let flatGearArray = toArray(content.gear.flat);
|
||||
|
||||
module.exports = function getOfficialPinnedItems (user) {
|
||||
let officialItemsArray = [...officialPinnedItems];
|
||||
|
||||
if (SeasonalShopConfig.pinnedSets && Boolean(user) && user.stats.class) {
|
||||
let setToAdd = SeasonalShopConfig.pinnedSets[user.stats.class];
|
||||
|
||||
// pinnedSets == current seasonal class set are always gold purchaseable
|
||||
|
||||
flatGearArray.filter((gear) => {
|
||||
return user.items.gear.owned[gear.key] === undefined && gear.set === setToAdd;
|
||||
}).map((gear) => {
|
||||
officialItemsArray.push({
|
||||
type: 'marketGear',
|
||||
path: `gear.flat.${gear.key}`,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return officialItemsArray;
|
||||
};
|
||||
25
website/common/script/libs/inAppRewards.js
Normal file
25
website/common/script/libs/inAppRewards.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import content from '../content/index';
|
||||
import get from 'lodash/get';
|
||||
import getItemInfo from './getItemInfo';
|
||||
import shops from './shops';
|
||||
import getOfficialPinnedItems from './getOfficialPinnedItems';
|
||||
|
||||
|
||||
module.exports = function getPinnedItems (user) {
|
||||
let officialPinnedItems = getOfficialPinnedItems(user);
|
||||
|
||||
const officialPinnedItemsNotUnpinned = officialPinnedItems.filter(officialPin => {
|
||||
const isUnpinned = user.unpinnedItems.findIndex(unpinned => unpinned.path === officialPin.path) > -1;
|
||||
return !isUnpinned;
|
||||
});
|
||||
|
||||
const pinnedItems = officialPinnedItemsNotUnpinned.concat(user.pinnedItems);
|
||||
|
||||
let items = pinnedItems.map(({type, path}) => {
|
||||
return getItemInfo(user, type, get(content, path), officialPinnedItems);
|
||||
});
|
||||
|
||||
shops.checkMarketGearLocked(user, items);
|
||||
|
||||
return items;
|
||||
};
|
||||
14
website/common/script/libs/isPinned.js
Normal file
14
website/common/script/libs/isPinned.js
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
module.exports = function isPinned (user, item, checkOfficialPinnedItems /* getOfficialPinnedItems */) {
|
||||
if (user === null)
|
||||
return false;
|
||||
|
||||
const isPinnedOfficial = checkOfficialPinnedItems !== undefined && checkOfficialPinnedItems.findIndex(pinned => pinned.path === item.path) > -1;
|
||||
const isItemUnpinned = user.unpinnedItems !== undefined && user.unpinnedItems.findIndex(unpinned => unpinned.path === item.path) > -1;
|
||||
const isItemPinned = user.pinnedItems !== undefined && user.pinnedItems.findIndex(pinned => pinned.path === item.path) > -1;
|
||||
|
||||
if (isPinnedOfficial && !isItemUnpinned)
|
||||
return true;
|
||||
|
||||
return isItemPinned;
|
||||
};
|
||||
32
website/common/script/libs/shops-seasonal.config.js
Normal file
32
website/common/script/libs/shops-seasonal.config.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { SEASONAL_SETS} from '../content/constants';
|
||||
|
||||
module.exports = {
|
||||
// opened: false,
|
||||
opened: true,
|
||||
|
||||
// used for the seasonalShop.notes
|
||||
// currentSeason: 'Closed',
|
||||
currentSeason: 'Fall',
|
||||
|
||||
dateRange: { start: '2017-09-21', end: '2017-10-31' },
|
||||
|
||||
availableSets: [
|
||||
...SEASONAL_SETS.fall,
|
||||
],
|
||||
|
||||
pinnedSets: {
|
||||
warrior: 'fall2017HabitoweenSet',
|
||||
wizard: 'fall2017MasqueradeSet',
|
||||
rogue: 'fall2017TrickOrTreatSet',
|
||||
healer: 'fall2017HauntedHouseSet',
|
||||
},
|
||||
|
||||
availableSpells: [
|
||||
// 'spookySparkles',
|
||||
],
|
||||
|
||||
availableQuests: [
|
||||
],
|
||||
|
||||
featuredSet: 'battleRogueSet',
|
||||
};
|
||||
@@ -1,26 +1,45 @@
|
||||
import values from 'lodash/values';
|
||||
import map from 'lodash/map';
|
||||
import keys from 'lodash/keys';
|
||||
import get from 'lodash/get';
|
||||
import each from 'lodash/each';
|
||||
import filter from 'lodash/filter';
|
||||
import eachRight from 'lodash/eachRight';
|
||||
import toArray from 'lodash/toArray';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import content from '../content/index';
|
||||
import i18n from '../i18n';
|
||||
import getItemInfo from './getItemInfo';
|
||||
import updateStore from './updateStore';
|
||||
import seasonalShopConfig from './shops-seasonal.config';
|
||||
import featuredItems from '../content/shop-featuredItems';
|
||||
|
||||
import getOfficialPinnedItems from './getOfficialPinnedItems';
|
||||
|
||||
let shops = {};
|
||||
|
||||
function lockQuest (quest, user) {
|
||||
if (quest.lvl && user.stats.lvl < quest.lvl) return true;
|
||||
if (quest.unlockCondition && (quest.key === 'moon1' || quest.key === 'moon2' || quest.key === 'moon3')) {
|
||||
return user.loginIncentives < quest.unlockCondition.incentiveThreshold;
|
||||
}
|
||||
if (user.achievements.quests) return quest.previous && !user.achievements.quests[quest.previous];
|
||||
return quest.previous;
|
||||
}
|
||||
/* Market */
|
||||
|
||||
shops.getMarketShop = function getMarketShop (user, language) {
|
||||
return {
|
||||
identifier: 'market',
|
||||
text: i18n.t('market'),
|
||||
notes: i18n.t('welcomeMarketMobile'),
|
||||
imageName: 'npc_alex',
|
||||
categories: shops.getMarketCategories(user, language),
|
||||
featured: {
|
||||
text: i18n.t('featuredItems'),
|
||||
items: featuredItems.market.map(i => {
|
||||
return getItemInfo(user, i.type, get(content, i.path));
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
shops.getMarketCategories = function getMarket (user, language) {
|
||||
let officialPinnedItems = getOfficialPinnedItems(user);
|
||||
|
||||
let categories = [];
|
||||
let eggsCategory = {
|
||||
identifier: 'eggs',
|
||||
@@ -32,16 +51,7 @@ shops.getMarketCategories = function getMarket (user, language) {
|
||||
.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',
|
||||
};
|
||||
return getItemInfo(user, 'eggs', egg, officialPinnedItems, language);
|
||||
}), 'key');
|
||||
categories.push(eggsCategory);
|
||||
|
||||
@@ -53,16 +63,7 @@ shops.getMarketCategories = function getMarket (user, language) {
|
||||
hatchingPotionsCategory.items = sortBy(values(content.hatchingPotions)
|
||||
.filter(hp => !hp.limited)
|
||||
.map(hatchingPotion => {
|
||||
return {
|
||||
key: hatchingPotion.key,
|
||||
text: i18n.t('potion', {potionType: hatchingPotion.text(language)}),
|
||||
notes: hatchingPotion.notes(language),
|
||||
class: `Pet_HatchingPotion_${hatchingPotion.key}`,
|
||||
value: hatchingPotion.value,
|
||||
locked: false,
|
||||
currency: 'gems',
|
||||
purchaseType: 'hatchingPotions',
|
||||
};
|
||||
return getItemInfo(user, 'hatchingPotions', hatchingPotion, officialPinnedItems, language);
|
||||
}), 'key');
|
||||
categories.push(hatchingPotionsCategory);
|
||||
|
||||
@@ -74,16 +75,7 @@ shops.getMarketCategories = function getMarket (user, language) {
|
||||
premiumHatchingPotionsCategory.items = sortBy(values(content.hatchingPotions)
|
||||
.filter(hp => hp.limited && hp.canBuy())
|
||||
.map(premiumHatchingPotion => {
|
||||
return {
|
||||
key: premiumHatchingPotion.key,
|
||||
text: i18n.t('potion', {potionType: premiumHatchingPotion.text(language)}),
|
||||
notes: `${premiumHatchingPotion.notes(language)} ${premiumHatchingPotion._addlNotes(language)}`,
|
||||
class: `Pet_HatchingPotion_${premiumHatchingPotion.key}`,
|
||||
value: premiumHatchingPotion.value,
|
||||
locked: false,
|
||||
currency: 'gems',
|
||||
purchaseType: 'hatchingPotions',
|
||||
};
|
||||
return getItemInfo(user, 'premiumHatchingPotion', premiumHatchingPotion, officialPinnedItems, language);
|
||||
}), 'key');
|
||||
if (premiumHatchingPotionsCategory.items.length > 0) {
|
||||
categories.push(premiumHatchingPotionsCategory);
|
||||
@@ -97,24 +89,106 @@ shops.getMarketCategories = function getMarket (user, language) {
|
||||
foodCategory.items = sortBy(values(content.food)
|
||||
.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',
|
||||
};
|
||||
return getItemInfo(user, 'food', foodItem, officialPinnedItems, language);
|
||||
}), 'key');
|
||||
categories.push(foodCategory);
|
||||
|
||||
return categories;
|
||||
};
|
||||
|
||||
function getClassName (classType, language) {
|
||||
if (classType === 'wizard') {
|
||||
return i18n.t('mage', language);
|
||||
} else {
|
||||
return i18n.t(classType, language);
|
||||
}
|
||||
}
|
||||
|
||||
shops.checkMarketGearLocked = function checkMarketGearLocked (user, items) {
|
||||
let result = filter(items, ['pinType', 'marketGear']);
|
||||
|
||||
let availableGear = map(updateStore(user), (item) => getItemInfo(user, 'marketGear', item).path);
|
||||
|
||||
for (let gear of result) {
|
||||
if (gear.klass !== user.stats.class) {
|
||||
gear.locked = true;
|
||||
}
|
||||
|
||||
if (!gear.locked && !availableGear.includes(gear.path)) {
|
||||
gear.locked = true;
|
||||
}
|
||||
|
||||
// @TODO: I'm not sure what the logic for locking is supposed to be
|
||||
// But, I am pretty sure if we pin an armoire item, it needs to be unlocked
|
||||
if (gear.klass === 'armoire') {
|
||||
gear.locked = false;
|
||||
}
|
||||
|
||||
if (Boolean(gear.specialClass) && Boolean(gear.set)) {
|
||||
let currentSet = gear.set === seasonalShopConfig.pinnedSets[gear.specialClass];
|
||||
|
||||
gear.locked = currentSet && user.stats.class !== gear.specialClass;
|
||||
}
|
||||
|
||||
if (gear.canOwn) {
|
||||
gear.locked = !gear.canOwn(user);
|
||||
}
|
||||
|
||||
|
||||
let itemOwned = user.items.gear.owned[gear.key];
|
||||
|
||||
if (itemOwned === false) {
|
||||
gear.locked = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
shops.getMarketGearCategories = function getMarketGear (user, language) {
|
||||
let categories = [];
|
||||
let officialPinnedItems = getOfficialPinnedItems(user);
|
||||
|
||||
for (let classType of content.classes) {
|
||||
let category = {
|
||||
identifier: classType,
|
||||
text: getClassName(classType, language),
|
||||
};
|
||||
|
||||
let result = filter(content.gear.flat, ['klass', classType]);
|
||||
category.items = map(result, (e) => {
|
||||
let newItem = getItemInfo(user, 'marketGear', e, officialPinnedItems);
|
||||
|
||||
return newItem;
|
||||
});
|
||||
|
||||
shops.checkMarketGearLocked(user, category.items);
|
||||
|
||||
categories.push(category);
|
||||
}
|
||||
|
||||
return categories;
|
||||
};
|
||||
|
||||
/* Quests */
|
||||
|
||||
shops.getQuestShop = function getQuestShop (user, language) {
|
||||
return {
|
||||
identifier: 'questShop',
|
||||
text: i18n.t('quests'),
|
||||
notes: i18n.t('ianTextMobile'),
|
||||
imageName: 'npc_ian',
|
||||
categories: shops.getQuestShopCategories(user, language),
|
||||
featured: {
|
||||
text: i18n.t('featuredQuests'),
|
||||
items: featuredItems.quests.map(i => {
|
||||
return getItemInfo(user, i.type, get(content, i.path));
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
shops.getQuestShopCategories = function getQuestShopCategories (user, language) {
|
||||
let categories = [];
|
||||
let officialPinnedItems = getOfficialPinnedItems(user);
|
||||
|
||||
/*
|
||||
* ---------------------------------------------------------------
|
||||
@@ -178,15 +252,7 @@ shops.getQuestShopCategories = function getQuestShopCategories (user, language)
|
||||
bundleCategory.items = sortBy(values(content.bundles)
|
||||
.filter(bundle => bundle.type === 'quests' && bundle.canBuy())
|
||||
.map(bundle => {
|
||||
return {
|
||||
key: bundle.key,
|
||||
text: bundle.text(language),
|
||||
notes: bundle.notes(language),
|
||||
value: bundle.value,
|
||||
currency: 'gems',
|
||||
class: `quest_bundle_${bundle.key}`,
|
||||
purchaseType: 'bundles',
|
||||
};
|
||||
return getItemInfo(user, 'bundles', bundle, officialPinnedItems, language);
|
||||
}));
|
||||
|
||||
if (bundleCategory.items.length > 0) {
|
||||
@@ -202,23 +268,7 @@ shops.getQuestShopCategories = function getQuestShopCategories (user, 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),
|
||||
group: quest.group,
|
||||
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_locked inventory_quest_scroll_${quest.key}_locked` : `inventory_quest_scroll inventory_quest_scroll_${quest.key}`,
|
||||
purchaseType: 'quests',
|
||||
};
|
||||
return getItemInfo(user, 'quests', quest, officialPinnedItems, language);
|
||||
});
|
||||
|
||||
categories.push(category);
|
||||
@@ -227,6 +277,21 @@ shops.getQuestShopCategories = function getQuestShopCategories (user, language)
|
||||
return categories;
|
||||
};
|
||||
|
||||
/* Time Travelers */
|
||||
|
||||
shops.getTimeTravelersShop = function getTimeTravelersShop (user, language) {
|
||||
let hasTrinkets = user.purchased.plan.consecutive.trinkets > 0;
|
||||
|
||||
return {
|
||||
identifier: 'timeTravelersShop',
|
||||
text: i18n.t('timeTravelers'),
|
||||
opened: hasTrinkets,
|
||||
notes: hasTrinkets ? i18n.t('timeTravelersPopover') : i18n.t('timeTravelersPopoverNoSubMobile'),
|
||||
imageName: hasTrinkets ? 'npc_timetravelers_active' : 'npc_timetravelers',
|
||||
categories: shops.getTimeTravelersCategories(user, language),
|
||||
};
|
||||
};
|
||||
|
||||
shops.getTimeTravelersCategories = function getTimeTravelersCategories (user, language) {
|
||||
let categories = [];
|
||||
let stable = {pets: 'Pet-', mounts: 'Mount_Icon_'};
|
||||
@@ -251,6 +316,7 @@ shops.getTimeTravelersCategories = function getTimeTravelersCategories (user, la
|
||||
notes: '',
|
||||
locked: false,
|
||||
currency: 'hourglasses',
|
||||
pinType: 'IGNORE',
|
||||
};
|
||||
category.items.push(item);
|
||||
}
|
||||
@@ -269,6 +335,8 @@ shops.getTimeTravelersCategories = function getTimeTravelersCategories (user, la
|
||||
let category = {
|
||||
identifier: set.key,
|
||||
text: set.text(language),
|
||||
path: `mystery.${set.key}`,
|
||||
pinType: 'mystery_set',
|
||||
purchaseAll: true,
|
||||
};
|
||||
|
||||
@@ -283,6 +351,7 @@ shops.getTimeTravelersCategories = function getTimeTravelersCategories (user, la
|
||||
locked: false,
|
||||
currency: 'hourglasses',
|
||||
class: `shop_${item.key}`,
|
||||
pinKey: `timeTravelers!gear.flat.${item.key}`,
|
||||
};
|
||||
});
|
||||
if (category.items.length > 0) {
|
||||
@@ -294,36 +363,64 @@ shops.getTimeTravelersCategories = function getTimeTravelersCategories (user, la
|
||||
return categories;
|
||||
};
|
||||
|
||||
|
||||
/* Seasonal */
|
||||
|
||||
let flatGearArray = toArray(content.gear.flat);
|
||||
|
||||
shops.getSeasonalGearBySet = function getSeasonalGearBySet (user, set, officialPinnedItems, language, ignoreAlreadyOwned = false) {
|
||||
return flatGearArray.filter((gear) => {
|
||||
if (!ignoreAlreadyOwned && user.items.gear.owned[gear.key] !== undefined)
|
||||
return false;
|
||||
|
||||
return gear.set === set;
|
||||
}).map(gear => {
|
||||
let currentSet = gear.set === seasonalShopConfig.pinnedSets[gear.specialClass];
|
||||
|
||||
// only the current season set can be purchased by gold
|
||||
let itemInfo = getItemInfo(null, currentSet ? 'marketGear' : 'gear', gear, officialPinnedItems, language);
|
||||
itemInfo.locked = currentSet && user.stats.class !== gear.specialClass;
|
||||
|
||||
return itemInfo;
|
||||
});
|
||||
};
|
||||
|
||||
shops.getSeasonalShop = function getSeasonalShop (user, language) {
|
||||
let officialPinnedItems = getOfficialPinnedItems(user);
|
||||
|
||||
let resObject = {
|
||||
identifier: 'seasonalShop',
|
||||
text: i18n.t('seasonalShop'),
|
||||
notes: i18n.t(`seasonalShop${seasonalShopConfig.currentSeason}Text`),
|
||||
imageName: seasonalShopConfig.opened ? 'seasonalshop_open' : 'seasonalshop_closed',
|
||||
opened: seasonalShopConfig.opened,
|
||||
categories: this.getSeasonalShopCategories(user, language),
|
||||
featured: {
|
||||
text: i18n.t(seasonalShopConfig.featuredSet),
|
||||
items: shops.getSeasonalGearBySet(user, seasonalShopConfig.featuredSet, officialPinnedItems, language, true),
|
||||
},
|
||||
};
|
||||
|
||||
return resObject;
|
||||
};
|
||||
|
||||
// To switch seasons/available inventory, edit the AVAILABLE_SETS object to whatever should be sold.
|
||||
// let AVAILABLE_SETS = {
|
||||
// setKey: i18n.t('setTranslationString', language),
|
||||
// };
|
||||
shops.getSeasonalShopCategories = function getSeasonalShopCategories (user, language) {
|
||||
const AVAILABLE_SETS = {
|
||||
fallHealer: i18n.t('mummyMedicSet', language),
|
||||
fall2015Healer: i18n.t('potionerSet', language),
|
||||
fall2016Healer: i18n.t('fall2016GorgonHealerSet', language),
|
||||
fallMage: i18n.t('witchyWizardSet', language),
|
||||
fall2015Mage: i18n.t('stitchWitchSet', language),
|
||||
fall2016Mage: i18n.t('fall2016WickedSorcererSet', language),
|
||||
fallRogue: i18n.t('vampireSmiterSet', language),
|
||||
fall2015Rogue: i18n.t('battleRogueSet', language),
|
||||
fall2016Rogue: i18n.t('fall2016BlackWidowSet', language),
|
||||
fallWarrior: i18n.t('monsterOfScienceSet', language),
|
||||
fall2015Warrior: i18n.t('scarecrowWarriorSet', language),
|
||||
fall2016Warrior: i18n.t('fall2016SwampThingSet', language),
|
||||
};
|
||||
let officialPinnedItems = getOfficialPinnedItems(user);
|
||||
|
||||
const AVAILABLE_SPELLS = [
|
||||
...seasonalShopConfig.availableSpells,
|
||||
];
|
||||
|
||||
const AVAILABLE_QUESTS = [
|
||||
...seasonalShopConfig.availableQuests,
|
||||
];
|
||||
|
||||
let categories = [];
|
||||
|
||||
let flatGearArray = toArray(content.gear.flat);
|
||||
|
||||
let spells = pickBy(content.spells.special, (spell, key) => {
|
||||
return AVAILABLE_SPELLS.indexOf(key) !== -1;
|
||||
});
|
||||
@@ -334,18 +431,8 @@ shops.getSeasonalShopCategories = function getSeasonalShopCategories (user, lang
|
||||
text: i18n.t('seasonalItems', language),
|
||||
};
|
||||
|
||||
category.items = map(spells, (spell, key) => {
|
||||
return {
|
||||
key,
|
||||
text: spell.text(language),
|
||||
notes: spell.notes(language),
|
||||
value: spell.value,
|
||||
type: 'special',
|
||||
currency: 'gold',
|
||||
locked: false,
|
||||
purchaseType: 'spells',
|
||||
class: `inventory_special_${key}`,
|
||||
};
|
||||
category.items = map(spells, (spell) => {
|
||||
return getItemInfo(user, 'seasonalSpell', spell, officialPinnedItems, language);
|
||||
});
|
||||
|
||||
categories.push(category);
|
||||
@@ -361,54 +448,27 @@ shops.getSeasonalShopCategories = function getSeasonalShopCategories (user, lang
|
||||
text: i18n.t('quests', language),
|
||||
};
|
||||
|
||||
category.items = map(quests, (quest, key) => {
|
||||
return {
|
||||
key,
|
||||
text: quest.text(language),
|
||||
notes: quest.notes(language),
|
||||
value: quest.value,
|
||||
type: 'quests',
|
||||
currency: 'gems',
|
||||
locked: false,
|
||||
drop: quest.drop,
|
||||
boss: quest.boss,
|
||||
collect: quest.collect,
|
||||
class: `inventory_quest_scroll_${key}`,
|
||||
purchaseType: 'quests',
|
||||
};
|
||||
category.items = map(quests, (quest) => {
|
||||
return getItemInfo(user, 'seasonalQuest', quest, language);
|
||||
});
|
||||
|
||||
categories.push(category);
|
||||
}
|
||||
|
||||
for (let key in AVAILABLE_SETS) {
|
||||
if (AVAILABLE_SETS.hasOwnProperty(key)) {
|
||||
let category = {
|
||||
identifier: key,
|
||||
text: AVAILABLE_SETS[key],
|
||||
};
|
||||
for (let set of seasonalShopConfig.availableSets) {
|
||||
let category = {
|
||||
identifier: set,
|
||||
text: i18n.t(set),
|
||||
};
|
||||
|
||||
category.items = flatGearArray.filter((gear) => {
|
||||
return user.items.gear.owned[gear.key] === undefined && gear.index === key;
|
||||
}).map(gear => {
|
||||
return {
|
||||
key: gear.key,
|
||||
text: gear.text(language),
|
||||
notes: gear.notes(language),
|
||||
value: gear.twoHanded ? 2 : 1,
|
||||
type: gear.type,
|
||||
specialClass: gear.specialClass,
|
||||
locked: false,
|
||||
currency: 'gems',
|
||||
purchaseType: 'gear',
|
||||
class: `shop_${gear.key}`,
|
||||
};
|
||||
});
|
||||
category.items = shops.getSeasonalGearBySet(user, set, officialPinnedItems, language, false);
|
||||
|
||||
if (category.items.length > 0) {
|
||||
category.specialClass = category.items[0].specialClass;
|
||||
categories.push(category);
|
||||
}
|
||||
if (category.items.length > 0) {
|
||||
let item = category.items[0];
|
||||
|
||||
category.specialClass = item.specialClass;
|
||||
category.event = item.event;
|
||||
categories.push(category);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,6 +477,7 @@ shops.getSeasonalShopCategories = function getSeasonalShopCategories (user, lang
|
||||
|
||||
shops.getBackgroundShopSets = function getBackgroundShopSets (language) {
|
||||
let sets = [];
|
||||
let officialPinnedItems = getOfficialPinnedItems();
|
||||
|
||||
eachRight(content.backgrounds, (group, key) => {
|
||||
let set = {
|
||||
@@ -424,15 +485,8 @@ shops.getBackgroundShopSets = function getBackgroundShopSets (language) {
|
||||
text: i18n.t(key, language),
|
||||
};
|
||||
|
||||
set.items = map(group, (background, bgKey) => {
|
||||
return {
|
||||
key: bgKey,
|
||||
text: background.text(language),
|
||||
notes: background.notes(language),
|
||||
value: background.price,
|
||||
currency: background.currency || 'gems',
|
||||
purchaseType: 'backgrounds',
|
||||
};
|
||||
set.items = map(group, (background) => {
|
||||
return getItemInfo(null, 'background', background, officialPinnedItems, language);
|
||||
});
|
||||
|
||||
sets.push(set);
|
||||
|
||||
@@ -16,11 +16,15 @@ function equipmentStatBonusComputed (stat, user) {
|
||||
let equippedKeys = values(!equipped.toObject ? equipped : equipped.toObject());
|
||||
|
||||
each(equippedKeys, (equippedItem) => {
|
||||
let equipmentStat = gear[equippedItem][stat];
|
||||
let classBonusMultiplier = gear[equippedItem].klass === user.stats.class ||
|
||||
gear[equippedItem].specialClass === user.stats.class ? 0.5 : 0;
|
||||
gearBonus += equipmentStat;
|
||||
classBonus += equipmentStat * classBonusMultiplier;
|
||||
let item = gear[equippedItem];
|
||||
|
||||
if (item) {
|
||||
let equipmentStat = item[stat];
|
||||
let classBonusMultiplier = item.klass === user.stats.class ||
|
||||
item.specialClass === user.stats.class ? 0.5 : 0;
|
||||
gearBonus += equipmentStat;
|
||||
classBonus += equipmentStat * classBonusMultiplier;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -25,7 +25,13 @@ module.exports = function taskDefaults (task = {}) {
|
||||
challenge: {
|
||||
shortName: 'None',
|
||||
},
|
||||
yesterDaily: true,
|
||||
group: {
|
||||
approval: {
|
||||
required: false,
|
||||
approved: false,
|
||||
requested: false,
|
||||
},
|
||||
},
|
||||
reminders: [],
|
||||
attribute: 'str',
|
||||
createdAt: new Date(), // TODO these are going to be overwritten by the server...
|
||||
@@ -73,6 +79,9 @@ module.exports = function taskDefaults (task = {}) {
|
||||
startDate: moment().startOf('day').toDate(),
|
||||
everyX: 1,
|
||||
frequency: 'weekly',
|
||||
daysOfMonth: [],
|
||||
weeksOfMonth: [],
|
||||
yesterDaily: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import reduce from 'lodash/reduce';
|
||||
import content from '../content/index';
|
||||
|
||||
// Return the list of gear items available for purchase
|
||||
// TODO: Remove updateStore once the new client is live
|
||||
|
||||
let sortOrder = reduce(content.gearTypes, (accumulator, val, key) => {
|
||||
accumulator[val] = key;
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
import handleTwoHanded from '../fns/handleTwoHanded';
|
||||
import ultimateGear from '../fns/ultimateGear';
|
||||
|
||||
import { removePinnedGearAddPossibleNewOnes } from './pinnedGearUtils';
|
||||
|
||||
module.exports = function buyGear (user, req = {}, analytics) {
|
||||
let key = get(req, 'params.key');
|
||||
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
|
||||
@@ -38,7 +40,8 @@ module.exports = function buyGear (user, req = {}, analytics) {
|
||||
message = handleTwoHanded(user, item, undefined, req);
|
||||
}
|
||||
|
||||
user.items.gear.owned[item.key] = true;
|
||||
|
||||
removePinnedGearAddPossibleNewOnes(user, `gear.flat.${item.key}`, item.key);
|
||||
|
||||
if (item.last) ultimateGear(user);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ module.exports = function buyMysterySet (user, req = {}, analytics) {
|
||||
throw new NotFound(i18n.t('mysterySetNotFound', req.language));
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && window.confirm) { // TODO move to client
|
||||
if (typeof window !== 'undefined' && !req.noConfirm && window.confirm) { // TODO move to client
|
||||
if (!window.confirm(i18n.t('hourglassBuyEquipSetConfirm'))) return;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,11 @@ import {
|
||||
NotAuthorized,
|
||||
BadRequest,
|
||||
} from '../libs/errors';
|
||||
import { removePinnedGearByClass, removePinnedItemsByOwnedGear, addPinnedGearByClass } from './pinnedGearUtils';
|
||||
|
||||
function resetClass (user, req = {}) {
|
||||
removePinnedGearByClass(user);
|
||||
|
||||
if (user.preferences.disableClasses) {
|
||||
user.preferences.disableClasses = false;
|
||||
user.preferences.autoAllocate = false;
|
||||
@@ -41,9 +44,13 @@ module.exports = function changeClass (user, req = {}, analytics) {
|
||||
user.stats.class = klass;
|
||||
user.flags.classSelected = true;
|
||||
|
||||
addPinnedGearByClass(user);
|
||||
|
||||
user.items.gear.owned[`weapon_${klass}_0`] = true;
|
||||
if (klass === 'rogue') user.items.gear.owned[`shield_${klass}_0`] = true;
|
||||
|
||||
removePinnedItemsByOwnedGear(user);
|
||||
|
||||
if (analytics) {
|
||||
analytics.track('change class', {
|
||||
uuid: user._id,
|
||||
|
||||
@@ -40,6 +40,7 @@ import readCard from './readCard';
|
||||
import openMysteryItem from './openMysteryItem';
|
||||
import scoreTask from './scoreTask';
|
||||
import markPmsRead from './markPMSRead';
|
||||
import * as pinnedGearUtils from './pinnedGearUtils';
|
||||
|
||||
module.exports = {
|
||||
sleep,
|
||||
@@ -84,4 +85,5 @@ module.exports = {
|
||||
openMysteryItem,
|
||||
scoreTask,
|
||||
markPmsRead,
|
||||
pinnedGearUtils,
|
||||
};
|
||||
|
||||
183
website/common/script/ops/pinnedGearUtils.js
Normal file
183
website/common/script/ops/pinnedGearUtils.js
Normal file
@@ -0,0 +1,183 @@
|
||||
import content from '../content/index';
|
||||
import getItemInfo from '../libs/getItemInfo';
|
||||
import { BadRequest } from '../libs/errors';
|
||||
import i18n from '../i18n';
|
||||
import isPinned from '../libs/isPinned';
|
||||
import getOfficialPinnedItems from '../libs/getOfficialPinnedItems';
|
||||
|
||||
import get from 'lodash/get';
|
||||
import each from 'lodash/each';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import lodashFind from 'lodash/find';
|
||||
import reduce from 'lodash/reduce';
|
||||
|
||||
let sortOrder = reduce(content.gearTypes, (accumulator, val, key) => {
|
||||
accumulator[val] = key;
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
function selectGearToPin (user) {
|
||||
let changes = [];
|
||||
|
||||
each(content.gearTypes, (type) => {
|
||||
let found = lodashFind(content.gear.tree[type][user.stats.class], (item) => {
|
||||
return !user.items.gear.owned[item.key];
|
||||
});
|
||||
|
||||
if (found) changes.push(found);
|
||||
});
|
||||
|
||||
return sortBy(changes, (change) => sortOrder[change.type]);
|
||||
}
|
||||
|
||||
|
||||
function addPinnedGear (user, type, path) {
|
||||
const foundIndex = user.pinnedItems.findIndex(pinnedItem => {
|
||||
return pinnedItem.path === path;
|
||||
});
|
||||
|
||||
if (foundIndex === -1) {
|
||||
user.pinnedItems.push({
|
||||
type,
|
||||
path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addPinnedGearByClass (user) {
|
||||
let newPinnedItems = selectGearToPin(user);
|
||||
|
||||
for (let item of newPinnedItems) {
|
||||
let itemInfo = getItemInfo(user, 'marketGear', item);
|
||||
|
||||
addPinnedGear(user, itemInfo.pinType, itemInfo.path);
|
||||
}
|
||||
}
|
||||
|
||||
function removeItemByPath (user, path) {
|
||||
const foundIndex = user.pinnedItems.findIndex(pinnedItem => {
|
||||
return pinnedItem.path === path;
|
||||
});
|
||||
|
||||
if (foundIndex >= 0) {
|
||||
user.pinnedItems.splice(foundIndex, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function removePinnedGearByClass (user) {
|
||||
let currentPinnedItems = selectGearToPin(user);
|
||||
|
||||
for (let item of currentPinnedItems) {
|
||||
let itemInfo = getItemInfo(user, 'marketGear', item);
|
||||
|
||||
removeItemByPath(user, itemInfo.path);
|
||||
}
|
||||
}
|
||||
|
||||
function removePinnedGearAddPossibleNewOnes (user, itemPath, newItemKey) {
|
||||
let currentPinnedItems = selectGearToPin(user);
|
||||
let removeAndAddAllItems = false;
|
||||
|
||||
for (let item of currentPinnedItems) {
|
||||
let itemInfo = getItemInfo(user, 'marketGear', item);
|
||||
|
||||
if (itemInfo.path === itemPath) {
|
||||
removeAndAddAllItems = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
removeItemByPath(user, itemPath);
|
||||
|
||||
if (removeAndAddAllItems) {
|
||||
// an item of the users current "new" gear was bought
|
||||
// remove the old pinned gear items and add the new gear back
|
||||
removePinnedGearByClass(user);
|
||||
user.items.gear.owned[newItemKey] = true;
|
||||
addPinnedGearByClass(user);
|
||||
} else {
|
||||
// just change the new gear to owned
|
||||
user.items.gear.owned[newItemKey] = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* removes all pinned gear that the user already owns (like class starter gear which has been pinned before)
|
||||
* @param user
|
||||
*/
|
||||
function removePinnedItemsByOwnedGear (user) {
|
||||
each(user.items.gear.owned, (bool, key) => {
|
||||
if (bool) {
|
||||
removeItemByPath(user, `gear.flat.${key}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const PATHS_WITHOUT_ITEM = ['special.gems', 'special.rebirth_orb', 'special.fortify'];
|
||||
|
||||
/**
|
||||
* @returns {boolean} TRUE added the item / FALSE removed it
|
||||
*/
|
||||
function togglePinnedItem (user, {item, type, path}, req = {}) {
|
||||
let arrayToChange;
|
||||
let officialPinnedItems = getOfficialPinnedItems(user);
|
||||
|
||||
if (!path) {
|
||||
// If path isn't passed it means an item was passed
|
||||
path = getItemInfo(user, type, item, officialPinnedItems, req.language).path;
|
||||
} else {
|
||||
if (!item) {
|
||||
item = get(content, path);
|
||||
}
|
||||
|
||||
if (!item && PATHS_WITHOUT_ITEM.indexOf(path) === -1) {
|
||||
// path not exists in our content structure
|
||||
|
||||
throw new BadRequest(i18n.t('wrongItemPath', {path}, req.language));
|
||||
}
|
||||
|
||||
// check if item exists & valid to be pinned
|
||||
getItemInfo(user, type, item, officialPinnedItems, req.language);
|
||||
}
|
||||
|
||||
|
||||
if (path === 'armoire' || path === 'potion') {
|
||||
throw new BadRequest(i18n.t('cannotUnpinArmoirPotion', req.language));
|
||||
}
|
||||
|
||||
let isOfficialPinned = officialPinnedItems.find(officialPinnedItem => {
|
||||
return officialPinnedItem.path === path;
|
||||
}) !== undefined;
|
||||
|
||||
if (isOfficialPinned) {
|
||||
arrayToChange = user.unpinnedItems;
|
||||
} else {
|
||||
arrayToChange = user.pinnedItems;
|
||||
}
|
||||
|
||||
const foundIndex = arrayToChange.findIndex(pinnedItem => {
|
||||
return pinnedItem.path === path;
|
||||
});
|
||||
|
||||
if (foundIndex >= 0) {
|
||||
arrayToChange.splice(foundIndex, 1);
|
||||
return isOfficialPinned;
|
||||
} else {
|
||||
arrayToChange.push({path, type});
|
||||
return !isOfficialPinned;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addPinnedGearByClass,
|
||||
addPinnedGear,
|
||||
removePinnedGearByClass,
|
||||
removePinnedGearAddPossibleNewOnes,
|
||||
removePinnedItemsByOwnedGear,
|
||||
togglePinnedItem,
|
||||
removeItemByPath,
|
||||
isPinned,
|
||||
};
|
||||
@@ -11,6 +11,9 @@ import {
|
||||
BadRequest,
|
||||
} from '../libs/errors';
|
||||
|
||||
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');
|
||||
@@ -103,6 +106,9 @@ module.exports = function purchase (user, req = {}, analytics) {
|
||||
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
|
||||
}
|
||||
|
||||
let itemInfo = getItemInfo(user, type, item);
|
||||
removeItemByPath(user, itemInfo.path);
|
||||
|
||||
user.balance -= price;
|
||||
|
||||
if (type === 'gear') {
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
NotAuthorized,
|
||||
} from '../libs/errors';
|
||||
import equip from './equip';
|
||||
import { removePinnedGearByClass } from './pinnedGearUtils';
|
||||
|
||||
|
||||
const USERSTATSLIST = ['per', 'int', 'con', 'str', 'points', 'gp', 'exp', 'mp'];
|
||||
|
||||
@@ -46,6 +48,8 @@ module.exports = function rebirth (user, tasks = [], req = {}, analytics) {
|
||||
}
|
||||
});
|
||||
|
||||
removePinnedGearByClass(user);
|
||||
|
||||
let stats = user.stats;
|
||||
stats.buffs = {};
|
||||
stats.hp = 50;
|
||||
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
import randomVal from '../libs/randomVal';
|
||||
import predictableRandom from '../fns/predictableRandom';
|
||||
|
||||
import { removePinnedGearByClass, addPinnedGearByClass, addPinnedGear } from './pinnedGearUtils';
|
||||
import getItemInfo from '../libs/getItemInfo';
|
||||
|
||||
module.exports = function revive (user, req = {}, analytics) {
|
||||
if (user.stats.hp > 0) {
|
||||
throw new NotAuthorized(i18n.t('cannotRevive', req.language));
|
||||
@@ -81,8 +84,15 @@ module.exports = function revive (user, req = {}, analytics) {
|
||||
let item = content.gear.flat[lostItem];
|
||||
|
||||
if (item) {
|
||||
removePinnedGearByClass(user);
|
||||
|
||||
user.items.gear.owned[lostItem] = false;
|
||||
|
||||
addPinnedGearByClass(user);
|
||||
|
||||
let itemInfo = getItemInfo(user, 'marketGear', item);
|
||||
addPinnedGear(user, itemInfo.pinType, itemInfo.path);
|
||||
|
||||
if (user.items.gear.equipped[item.type] === lostItem) {
|
||||
user.items.gear.equipped[item.type] = `${item.type}_base_0`;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
BadRequest,
|
||||
} from '../libs/errors';
|
||||
|
||||
import { removeItemByPath } from './pinnedGearUtils';
|
||||
import getItemInfo from '../libs/getItemInfo';
|
||||
import content from '../content/index';
|
||||
|
||||
// If item is already purchased -> equip it
|
||||
// Otherwise unlock it
|
||||
module.exports = function unlock (user, req = {}, analytics) {
|
||||
@@ -75,10 +79,11 @@ module.exports = function unlock (user, req = {}, analytics) {
|
||||
setWith(user, `purchased.${pathPart}`, true, Object);
|
||||
});
|
||||
} else {
|
||||
let split = path.split('.');
|
||||
let value = split.pop();
|
||||
let key = split.join('.');
|
||||
|
||||
if (alreadyOwns) { // eslint-disable-line no-lonely-if
|
||||
let split = path.split('.');
|
||||
let value = split.pop();
|
||||
let key = split.join('.');
|
||||
if (key === 'background' && value === user.preferences.background) {
|
||||
value = '';
|
||||
}
|
||||
@@ -88,6 +93,13 @@ module.exports = function unlock (user, req = {}, analytics) {
|
||||
} else {
|
||||
// Using Object so path[1] won't create an array but an object {path: {1: value}}
|
||||
setWith(user, `purchased.${path}`, true, Object);
|
||||
|
||||
// @TODO: Test and check test coverage
|
||||
if (isBackground) {
|
||||
let backgroundContent = content.backgroundsFlat[value];
|
||||
let itemInfo = getItemInfo(user, 'background', backgroundContent);
|
||||
removeItemByPath(user, itemInfo.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user