From ff72706cae39e2725f60b433c5c715d4c7ba3bd4 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Wed, 16 Mar 2016 16:25:46 +0100 Subject: [PATCH] wip(shared): port buy ops and linked fns --- common/locales/en/api-v3.json | 7 +- common/script/fns/handleTwoHanded.js | 19 ++-- common/script/fns/predictableRandom.js | 23 ++-- common/script/fns/randomVal.js | 14 +-- common/script/fns/ultimateGear.js | 32 +++--- common/script/ops/buy.js | 146 ++++++++++++++----------- common/script/ops/buyMysterySet.js | 68 ++++++------ common/script/ops/buyQuest.js | 73 ++++++------- common/script/ops/buySpecialSpell.js | 42 +++---- tasks/gulp-eslint.js | 8 -- website/src/models/user.js | 10 +- 11 files changed, 227 insertions(+), 215 deletions(-) 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, },