diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json
index 9f48532913..abbce91745 100644
--- a/common/locales/en/api-v3.json
+++ b/common/locales/en/api-v3.json
@@ -77,6 +77,7 @@
"guildQuestsNotSupported": "Guilds cannot be invited on quests.",
"questNotFound": "Quest \"<%= key %>\" not found.",
"questNotOwned": "You don't own that quest scroll.",
+ "questNotGoldPurchasable": "Quest \"<%= key %>\" is not a Gold-purchasable quest.",
"questLevelTooHigh": "You must be level <%= level %> to begin this quest.",
"questAlreadyUnderway": "Your party is already on a quest. Try again when the current quest has ended.",
"questAlreadyAccepted": "You already accepted the quest invitation.",
@@ -102,5 +103,9 @@
"spellNotOwned": "You don't own this spell.",
"spellLevelTooHigh": "You must be level <%= level %> to use this spell.",
"invalidAttribute": "\"<%= attr %>\" is not a valid attribute.",
- "notEnoughAttrPoints": "You don't have enough attribute points."
+ "notEnoughAttrPoints": "You don't have enough attribute points.",
+ "missingKeyParam": "\"req.params.key\" is required.",
+ "mysterySetNotFound": "Mystery set not found, or set already owned",
+ "itemNotFound": "Item \"<%= key %>\" not found.",
+ "cannoyBuyItem": "You can't buy this item"
}
diff --git a/common/script/fns/handleTwoHanded.js b/common/script/fns/handleTwoHanded.js
index c700346988..44537a1cac 100644
--- a/common/script/fns/handleTwoHanded.js
+++ b/common/script/fns/handleTwoHanded.js
@@ -1,24 +1,23 @@
import content from '../content/index';
import i18n from '../i18n';
-module.exports = function(user, item, type, req) {
- var message, currentWeapon, currentShield;
- if (type == null) {
- type = 'equipped';
- }
- currentShield = content.gear.flat[user.items.gear[type].shield];
- currentWeapon = content.gear.flat[user.items.gear[type].weapon];
+module.exports = function handleTwoHanded (user, item, type = 'equipped', req) {
+ let currentShield = content.gear.flat[user.items.gear[type].shield];
+ let currentWeapon = content.gear.flat[user.items.gear[type].weapon];
- if (item.type === "shield" && (currentWeapon ? currentWeapon.twoHanded : false)) {
+ let message;
+
+ if (item.type === 'shield' && (currentWeapon ? currentWeapon.twoHanded : false)) {
user.items.gear[type].weapon = 'weapon_base_0';
message = i18n.t('messageTwoHandedUnequip', {
twoHandedText: currentWeapon.text(req.language), offHandedText: item.text(req.language),
}, req.language);
- } else if (item.twoHanded && (currentShield && user.items.gear[type].shield != "shield_base_0")) {
- user.items.gear[type].shield = "shield_base_0";
+ } else if (item.twoHanded && (currentShield && user.items.gear[type].shield !== 'shield_base_0')) {
+ user.items.gear[type].shield = 'shield_base_0';
message = i18n.t('messageTwoHandedEquip', {
twoHandedText: item.text(req.language), offHandedText: currentShield.text(req.language),
}, req.language);
}
+
return message;
};
diff --git a/common/script/fns/predictableRandom.js b/common/script/fns/predictableRandom.js
index 64c8153746..373e015500 100644
--- a/common/script/fns/predictableRandom.js
+++ b/common/script/fns/predictableRandom.js
@@ -1,20 +1,19 @@
import _ from 'lodash';
-/*
-Because the same op needs to be performed on the client and the server (critical hits, item drops, etc),
-we need things to be "random", but technically predictable so that they don't go out-of-sync
- */
-module.exports = function(user, seed) {
- var x;
+// Because the same op needs to be performed on the client and the server (critical hits, item drops, etc),
+// we need things to be "random", but technically predictable so that they don't go out-of-sync
+
+module.exports = function predictableRandom (user, seed) {
if (!seed || seed === Math.PI) {
- seed = _.reduce(user.stats, (function(m, v) {
- if (_.isNumber(v)) {
- return m + v;
+ seed = _.reduce(user.stats, (accumulator, val) => {
+ if (_.isNumber(val)) {
+ return accumulator + val;
} else {
- return m;
+ return accumulator;
}
- }), 0);
+ }, 0);
}
- x = Math.sin(seed++) * 10000;
+
+ let x = Math.sin(seed++) * 10000;
return x - Math.floor(x);
};
diff --git a/common/script/fns/randomVal.js b/common/script/fns/randomVal.js
index 3c2b5b82e8..2244d04558 100644
--- a/common/script/fns/randomVal.js
+++ b/common/script/fns/randomVal.js
@@ -1,14 +1,12 @@
import _ from 'lodash';
+import predictableRandom from './predictableRandom';
-/*
- Get a random property from an object
- returns random property (the value)
- */
+// Get a random property from an object
+// returns random property (the value)
-module.exports = function(user, obj, options) {
- var array, rand;
- array = (options != null ? options.key : void 0) ? _.keys(obj) : _.values(obj);
- rand = user.fns.predictableRandom(options != null ? options.seed : void 0);
+module.exports = function randomVal (user, obj, options = {}) {
+ let array = options.key ? _.keys(obj) : _.values(obj);
+ let rand = predictableRandom(user, options.seed);
array.sort();
return array[Math.floor(rand * array.length)];
};
diff --git a/common/script/fns/ultimateGear.js b/common/script/fns/ultimateGear.js
index 1333e8cc38..729d11001a 100644
--- a/common/script/fns/ultimateGear.js
+++ b/common/script/fns/ultimateGear.js
@@ -1,33 +1,35 @@
import content from '../content/index';
import _ from 'lodash';
-module.exports = function(user) {
- var base, owned;
- owned = typeof window !== "undefined" && window !== null ? user.items.gear.owned : user.items.gear.owned.toObject();
- if ((base = user.achievements).ultimateGearSets == null) {
- base.ultimateGearSets = {
+module.exports = function ultimateGear (user) {
+ let owned = window ? user.items.gear.owned : user.items.gear.owned.toObject();
+
+ if (!user.achievements.ultimateGearSets) {
+ user.achievements.ultimateGearSets = {
healer: false,
wizard: false,
rogue: false,
- warrior: false
+ warrior: false,
};
}
- content.classes.forEach(function(klass) {
+
+ content.classes.forEach((klass) => {
if (user.achievements.ultimateGearSets[klass] !== true) {
- return user.achievements.ultimateGearSets[klass] = _.reduce(['armor', 'shield', 'head', 'weapon'], function(soFarGood, type) {
- var found;
- found = _.find(content.gear.tree[type][klass], {
- last: true
+ user.achievements.ultimateGearSets[klass] = _.reduce(['armor', 'shield', 'head', 'weapon'], (soFarGood, type) => {
+ let found = _.find(content.gear.tree[type][klass], {
+ last: true,
});
return soFarGood && (!found || owned[found.key] === true);
}, true);
}
});
- if (typeof user.markModified === "function") {
- user.markModified('achievements.ultimateGearSets');
- }
+
+ // TODO
+ if (user.markModified) user.markModified('achievements.ultimateGearSets');
+
if (_.contains(user.achievements.ultimateGearSets, true) && user.flags.armoireEnabled !== true) {
user.flags.armoireEnabled = true;
- return typeof user.markModified === "function" ? user.markModified('flags') : void 0;
}
+
+ return;
};
diff --git a/common/script/ops/buy.js b/common/script/ops/buy.js
index 1686316264..d90d07b0cb 100644
--- a/common/script/ops/buy.js
+++ b/common/script/ops/buy.js
@@ -3,117 +3,133 @@ import i18n from '../i18n';
import _ from 'lodash';
import count from '../count';
import splitWhitespace from '../libs/splitWhitespace';
+import {
+ BadRequest,
+ NotAuthorized,
+ NotFound,
+} from '../libs/errors';
+import predictableRandom from '../fns/predictableRandom';
+import randomVal from '../fns/randomVal';
+import handleTwoHanded from '../fns/handleTwoHanded';
+import ultimateGear from '../fns/ultimateGear';
-module.exports = function(user, req, cb, analytics) {
- var analyticsData, armoireExp, armoireResp, armoireResult, base, buyResp, drop, eligibleEquipment, item, key, message, name;
- key = req.params.key;
- item = key === 'potion' ? content.potion : key === 'armoire' ? content.armoire : content.gear.flat[key];
- if (!item) {
- return typeof cb === "function" ? cb({
- code: 404,
- message: "Item '" + key + " not found (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.js)"
- }) : void 0;
+module.exports = function buy (user, req = {}, analytics) {
+ let key = _.get(req, 'params.key');
+ if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
+
+ let item;
+ if (key === 'potion') {
+ item = content.potion;
+ } else if (key === 'armoire') {
+ item = content.armoire;
+ } else {
+ item = content.gear.flat[key];
}
+ if (!item) throw new NotFound(i18n.t('itemNotFound', {key}, req.language));
+
if (user.stats.gp < item.value) {
- return typeof cb === "function" ? cb({
- code: 401,
- message: i18n.t('messageNotEnoughGold', req.language)
- }) : void 0;
+ throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
- if ((item.canOwn != null) && !item.canOwn(user)) {
- return typeof cb === "function" ? cb({
- code: 401,
- message: "You can't buy this item"
- }) : void 0;
+
+ if (item.canOwn && !item.canOwn(user)) {
+ throw new NotAuthorized(i18n.t('cannoyBuyItem', req.language));
}
- armoireResp = void 0;
+
+ let armoireResp;
+ let armoireResult;
+ let eligibleEquipment;
+ let drop;
+ let message;
+
if (item.key === 'potion') {
user.stats.hp += 15;
if (user.stats.hp > 50) {
user.stats.hp = 50;
}
} else if (item.key === 'armoire') {
- armoireResult = user.fns.predictableRandom(user.stats.gp);
- eligibleEquipment = _.filter(content.gear.flat, (function(i) {
- return i.klass === 'armoire' && !user.items.gear.owned[i.key];
- }));
- if (!_.isEmpty(eligibleEquipment) && (armoireResult < .6 || !user.flags.armoireOpened)) {
+ armoireResult = predictableRandom(user, user.stats.gp);
+ eligibleEquipment = _.filter(content.gear.flat, (eligible) => {
+ return eligible.klass === 'armoire' && !user.items.gear.owned[eligible.key];
+ });
+
+ if (!_.isEmpty(eligibleEquipment) && (armoireResult < 0.6 || !user.flags.armoireOpened)) {
eligibleEquipment.sort();
- drop = user.fns.randomVal(eligibleEquipment);
+ drop = randomVal(user, eligibleEquipment);
+
user.items.gear.owned[drop.key] = true;
user.flags.armoireOpened = true;
message = i18n.t('armoireEquipment', {
- image: '',
- dropText: drop.text(req.language)
+ image: ``,
+ dropText: drop.text(req.language),
}, req.language);
+
if (count.remainingGearInSet(user.items.gear.owned, 'armoire') === 0) {
user.flags.armoireEmpty = true;
}
+
armoireResp = {
- type: "gear",
+ type: 'gear',
dropKey: drop.key,
- dropText: drop.text(req.language)
+ dropText: drop.text(req.language),
};
- } else if ((!_.isEmpty(eligibleEquipment) && armoireResult < .8) || armoireResult < .5) {
- drop = user.fns.randomVal(_.where(content.food, {
- canDrop: true
+ } else if ((!_.isEmpty(eligibleEquipment) && armoireResult < 0.8) || armoireResult < 0.5) { // eslint-disable-line no-extra-parens
+ drop = randomVal(_.where(content.food, {
+ canDrop: true,
}));
- if ((base = user.items.food)[name = drop.key] == null) {
- base[name] = 0;
- }
+ user.items.food[drop.key] = user.items.food[drop.key] || 0;
user.items.food[drop.key] += 1;
+
message = i18n.t('armoireFood', {
- image: '',
+ image: ``,
dropArticle: drop.article,
- dropText: drop.text(req.language)
+ dropText: drop.text(req.language),
}, req.language);
armoireResp = {
- type: "food",
+ type: 'food',
dropKey: drop.key,
dropArticle: drop.article,
- dropText: drop.text(req.language)
+ dropText: drop.text(req.language),
};
} else {
- armoireExp = Math.floor(user.fns.predictableRandom(user.stats.exp) * 40 + 10);
+ let armoireExp = Math.floor(predictableRandom(user, user.stats.exp) * 40 + 10);
user.stats.exp += armoireExp;
message = i18n.t('armoireExp', req.language);
armoireResp = {
- "type": "experience",
- "value": armoireExp
+ type: 'experience',
+ value: armoireExp,
};
}
} else {
if (user.preferences.autoEquip) {
user.items.gear.equipped[item.type] = item.key;
- message = user.fns.handleTwoHanded(item, null, req);
+ message = handleTwoHanded(user, item, null, req);
}
user.items.gear.owned[item.key] = true;
- if (message == null) {
+
+ if (!message) {
message = i18n.t('messageBought', {
- itemText: item.text(req.language)
+ itemText: item.text(req.language),
}, req.language);
}
- if (item.last) {
- user.fns.ultimateGear();
- }
+ if (item.last) ultimateGear(user);
}
+
user.stats.gp -= item.value;
- analyticsData = {
- uuid: user._id,
- itemKey: key,
- acquireMethod: 'Gold',
- goldCost: item.value,
- category: 'behavior'
+ if (analytics) {
+ analytics.track('acquire item', {
+ uuid: user._id,
+ itemKey: key,
+ acquireMethod: 'Gold',
+ goldCost: item.value,
+ category: 'behavior',
+ });
+ }
+
+ let buyResp = _.pick(user, splitWhitespace('items achievements stats flags'));
+ if (armoireResp) buyResp.armoire = armoireResp;
+
+ return {
+ data: buyResp,
+ message,
};
- if (analytics != null) {
- analytics.track('acquire item', analyticsData);
- }
- buyResp = _.pick(user, splitWhitespace('items achievements stats flags'));
- if (armoireResp) {
- buyResp["armoire"] = armoireResp;
- }
- return typeof cb === "function" ? cb({
- code: 200,
- message: message
- }, buyResp) : void 0;
};
diff --git a/common/script/ops/buyMysterySet.js b/common/script/ops/buyMysterySet.js
index 44ccdb9aaf..c43b925358 100644
--- a/common/script/ops/buyMysterySet.js
+++ b/common/script/ops/buyMysterySet.js
@@ -2,42 +2,48 @@ import i18n from '../i18n';
import content from '../content/index';
import _ from 'lodash';
import splitWhitespace from '../libs/splitWhitespace';
+import {
+ BadRequest,
+ NotAuthorized,
+ NotFound,
+} from '../libs/errors';
+
+module.exports = function buyMysterySet (user, req = {}, analytics) {
+ let key = _.get(req, 'params.key');
+ if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
-module.exports = function(user, req, cb, analytics) {
- var mysterySet, ref;
if (!(user.purchased.plan.consecutive.trinkets > 0)) {
- return typeof cb === "function" ? cb({
- code: 401,
- message: i18n.t('notEnoughHourglasses', req.language)
- }) : void 0;
- }
- mysterySet = (ref = content.timeTravelerStore(user.items.gear.owned)) != null ? ref[req.params.key] : void 0;
- if ((typeof window !== "undefined" && window !== null ? window.confirm : void 0) != null) {
- if (!window.confirm(i18n.t('hourglassBuyEquipSetConfirm'))) {
- return;
- }
+ throw new NotAuthorized(i18n.t('notEnoughHourglasses', req.language));
}
+
+ let ref = content.timeTravelerStore(user.items.gear.owned);
+ let mysterySet = ref ? ref[key] : undefined;
+
if (!mysterySet) {
- return typeof cb === "function" ? cb({
- code: 404,
- message: "Mystery set not found, or set already owned"
- }) : void 0;
+ throw new NotFound(i18n.t('mysterySetNotFound', req.language));
}
- _.each(mysterySet.items, function(i) {
- var analyticsData;
- user.items.gear.owned[i.key] = true;
- analyticsData = {
- uuid: user._id,
- itemKey: i.key,
- itemType: 'Subscriber Gear',
- acquireMethod: 'Hourglass',
- category: 'behavior'
- };
- return analytics != null ? analytics.track('acquire item', analyticsData) : void 0;
+
+ if (window && window.confirm) { // TODO move to client
+ if (!window.confirm(i18n.t('hourglassBuyEquipSetConfirm'))) return;
+ }
+
+ _.each(mysterySet.items, item => {
+ user.items.gear.owned[item.key] = true;
+ if (analytics) {
+ analytics.track('acquire item', {
+ uuid: user._id,
+ itemKey: item.key,
+ itemType: 'Subscriber Gear',
+ acquireMethod: 'Hourglass',
+ category: 'behavior',
+ });
+ }
});
+
user.purchased.plan.consecutive.trinkets--;
- return typeof cb === "function" ? cb({
- code: 200,
- message: i18n.t('hourglassPurchaseSet', req.language)
- }, _.pick(user, splitWhitespace('items purchased.plan.consecutive'))) : void 0;
+
+ return {
+ data: _.pick(user, splitWhitespace('items purchased.plan.consecutive')),
+ message: i18n.t('hourglassPurchaseSet', req.language),
+ };
};
diff --git a/common/script/ops/buyQuest.js b/common/script/ops/buyQuest.js
index b7653d43ce..033f2e5620 100644
--- a/common/script/ops/buyQuest.js
+++ b/common/script/ops/buyQuest.js
@@ -1,49 +1,44 @@
import i18n from '../i18n';
import content from '../content/index';
+import {
+ BadRequest,
+ NotAuthorized,
+ NotFound,
+} from '../libs/errors';
+import _ from 'lodash';
+module.exports = function buyQuest (user, req = {}, analytics) {
+ let key = _.get(req, 'params.key');
+ if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
+
+ let item = content.quests[key];
+ if (!item) throw new NotFound(i18n.t('questNotFound', req.language));
-module.exports = function(user, req, cb, analytics) {
- var analyticsData, base, item, key, message, name;
- key = req.params.key;
- item = content.quests[key];
- if (!item) {
- return typeof cb === "function" ? cb({
- code: 404,
- message: "Quest '" + key + " not found (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.js)"
- }) : void 0;
- }
if (!(item.category === 'gold' && item.goldValue)) {
- return typeof cb === "function" ? cb({
- code: 404,
- message: "Quest '" + key + " is not a Gold-purchasable quest (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.js)"
- }) : void 0;
+ throw new NotAuthorized(i18n.t('questNotGoldPurchasable', {key}, req.language));
}
if (user.stats.gp < item.goldValue) {
- return typeof cb === "function" ? cb({
- code: 401,
- message: i18n.t('messageNotEnoughGold', req.language)
- }) : void 0;
+ throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
- message = i18n.t('messageBought', {
- itemText: item.text(req.language)
- }, req.language);
- if ((base = user.items.quests)[name = item.key] == null) {
- base[name] = 0;
- }
- user.items.quests[item.key] += 1;
+
+ user.items.quests[item.key] = user.items.quests[item.key] || 0;
+ user.items.quests[item.key]++;
user.stats.gp -= item.goldValue;
- analyticsData = {
- uuid: user._id,
- itemKey: item.key,
- itemType: 'Market',
- goldCost: item.goldValue,
- acquireMethod: 'Gold',
- category: 'behavior'
- };
- if (analytics != null) {
- analytics.track('acquire item', analyticsData);
+
+ if (analytics) {
+ analytics.track('acquire item', {
+ uuid: user._id,
+ itemKey: item.key,
+ itemType: 'Market',
+ goldCost: item.goldValue,
+ acquireMethod: 'Gold',
+ category: 'behavior',
+ });
}
- return typeof cb === "function" ? cb({
- code: 200,
- message: message
- }, user.items.quests) : void 0;
+
+ return {
+ data: user.items.quests,
+ message: i18n.t('messageBought', {
+ itemText: item.text(req.language),
+ }, req.language),
+ };
};
diff --git a/common/script/ops/buySpecialSpell.js b/common/script/ops/buySpecialSpell.js
index e2a9dc8deb..36ab94e143 100644
--- a/common/script/ops/buySpecialSpell.js
+++ b/common/script/ops/buySpecialSpell.js
@@ -2,30 +2,30 @@ import i18n from '../i18n';
import content from '../content/index';
import _ from 'lodash';
import splitWhitespace from '../libs/splitWhitespace';
+import {
+ BadRequest,
+ NotAuthorized,
+ NotFound,
+} from '../libs/errors';
+
+module.exports = function buySpecialSpell (user, req = {}) {
+ let key = _.get(req, 'params.key');
+ if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
+
+ let item = content.special[key];
+ if (!item) throw new NotFound(i18n.t('spellNotFound', {spellId: key}, req.language));
-module.exports = function(user, req, cb) {
- var base, item, key, message;
- key = req.params.key;
- item = content.special[key];
if (user.stats.gp < item.value) {
- return typeof cb === "function" ? cb({
- code: 401,
- message: i18n.t('messageNotEnoughGold', req.language)
- }) : void 0;
+ throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
}
user.stats.gp -= item.value;
- if ((base = user.items.special)[key] == null) {
- base[key] = 0;
- }
+
user.items.special[key]++;
- if (typeof user.markModified === "function") {
- user.markModified('items.special');
- }
- message = i18n.t('messageBought', {
- itemText: item.text(req.language)
- }, req.language);
- return typeof cb === "function" ? cb({
- code: 200,
- message: message
- }, _.pick(user, splitWhitespace('items stats'))) : void 0;
+
+ return {
+ data: _.pick(user, splitWhitespace('items stats')),
+ message: i18n.t('messageBought', {
+ itemText: item.text(req.language),
+ }, req.language),
+ };
};
diff --git a/tasks/gulp-eslint.js b/tasks/gulp-eslint.js
index 1c5df3f6f8..85c275bb8f 100644
--- a/tasks/gulp-eslint.js
+++ b/tasks/gulp-eslint.js
@@ -21,10 +21,6 @@ const COMMON_FILES = [
'!./common/script/ops/addWebhook.js',
'!./common/script/ops/allocateNow.js',
'!./common/script/ops/blockUser.js',
- '!./common/script/ops/buy.js',
- '!./common/script/ops/buyMysterySet.js',
- '!./common/script/ops/buyQuest.js',
- '!./common/script/ops/buySpecialSpell.js',
'!./common/script/ops/changeClass.js',
'!./common/script/ops/clearCompleted.js',
'!./common/script/ops/clearPMs.js',
@@ -63,13 +59,9 @@ const COMMON_FILES = [
'!./common/script/fns/dotGet.js',
'!./common/script/fns/dotSet.js',
'!./common/script/fns/getItem.js',
- '!./common/script/fns/handleTwoHanded.js',
'!./common/script/fns/nullify.js',
- '!./common/script/fns/predictableRandom.js',
'!./common/script/fns/preenUserHistory.js',
'!./common/script/fns/randomDrop.js',
- '!./common/script/fns/randomVal.js',
- '!./common/script/fns/ultimateGear.js',
'!./common/script/fns/updateStats.js',
'!./common/script/libs/appliedTags.js',
'!./common/script/libs/countExists.js',
diff --git a/website/src/models/user.js b/website/src/models/user.js
index e6bcad4261..12398fa261 100644
--- a/website/src/models/user.js
+++ b/website/src/models/user.js
@@ -263,15 +263,15 @@ export let schema = new Schema({
spookDust: {type: Number, default: 0},
shinySeed: {type: Number, default: 0},
seafoam: {type: Number, default: 0},
- valentine: Number,
+ valentine: {type: Number, default: 0},
valentineReceived: Array, // array of strings, by sender name
- nye: Number,
+ nye: {type: Number, default: 0},
nyeReceived: Array,
- greeting: Number,
+ greeting: {type: Number, default: 0},
greetingReceived: Array,
- thankyou: Number,
+ thankyou: {type: Number, default: 0},
thankyouReceived: Array,
- birthday: Number,
+ birthday: {type: Number, default: 0},
birthdayReceived: Array,
},