mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 22:57:21 +01:00
Purchase API Refactoring: Market Gear (#10010)
* convert buyGear to buyMarketGearOperation + tests * move NotImplementedError
This commit is contained in:
@@ -4,14 +4,20 @@ import sinon from 'sinon'; // eslint-disable-line no-shadow
|
|||||||
import {
|
import {
|
||||||
generateUser,
|
generateUser,
|
||||||
} from '../../../helpers/common.helper';
|
} from '../../../helpers/common.helper';
|
||||||
import buyGear from '../../../../website/common/script/ops/buy/buyGear';
|
import {BuyMarketGearOperation} from '../../../../website/common/script/ops/buy/buyMarketGear';
|
||||||
import shared from '../../../../website/common/script';
|
import shared from '../../../../website/common/script';
|
||||||
import {
|
import {
|
||||||
BadRequest, NotAuthorized, NotFound,
|
BadRequest, NotAuthorized, NotFound,
|
||||||
} from '../../../../website/common/script/libs/errors';
|
} from '../../../../website/common/script/libs/errors';
|
||||||
import i18n from '../../../../website/common/script/i18n';
|
import i18n from '../../../../website/common/script/i18n';
|
||||||
|
|
||||||
describe('shared.ops.buyGear', () => {
|
function buyGear (user, req, analytics) {
|
||||||
|
let buyOp = new BuyMarketGearOperation(user, req, analytics);
|
||||||
|
|
||||||
|
return buyOp.purchase();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('shared.ops.buyMarketGear', () => {
|
||||||
let user;
|
let user;
|
||||||
let analytics = {track () {}};
|
let analytics = {track () {}};
|
||||||
|
|
||||||
@@ -111,6 +117,31 @@ describe('shared.ops.buyGear', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not buy equipment of different class', (done) => {
|
||||||
|
user.stats.gp = 82;
|
||||||
|
user.stats.class = 'warrior';
|
||||||
|
|
||||||
|
try {
|
||||||
|
buyGear(user, {params: {key: 'weapon_special_winter2018Rogue'}});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('cannotBuyItem'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not buy equipment in bulk', (done) => {
|
||||||
|
user.stats.gp = 82;
|
||||||
|
|
||||||
|
try {
|
||||||
|
buyGear(user, {params: {key: 'armor_warrior_1'}, quantity: 3});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
|
expect(err.message).to.equal(i18n.t('messageNotAbleToBuyInBulk'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// TODO after user.ops.equip is done
|
// TODO after user.ops.equip is done
|
||||||
xit('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', () => {
|
xit('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', () => {
|
||||||
user.stats.gp = 100;
|
user.stats.gp = 100;
|
||||||
@@ -40,3 +40,12 @@ export class NotFound extends CustomError {
|
|||||||
this.message = customMessage || 'Not found.';
|
this.message = customMessage || 'Not found.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class NotImplementedError extends CustomError {
|
||||||
|
constructor (str) {
|
||||||
|
super();
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
|
||||||
|
this.message = `Method: '${str}' not implemented`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
127
website/common/script/ops/buy/abstractBuyOperation.js
Normal file
127
website/common/script/ops/buy/abstractBuyOperation.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import i18n from '../../i18n';
|
||||||
|
import {
|
||||||
|
NotAuthorized, NotImplementedError,
|
||||||
|
} from '../../libs/errors';
|
||||||
|
import _merge from 'lodash/merge';
|
||||||
|
import _get from 'lodash/get';
|
||||||
|
|
||||||
|
export class AbstractBuyOperation {
|
||||||
|
/**
|
||||||
|
* @param {User} user - the User-Object
|
||||||
|
* @param {Request} req - the Request-Object
|
||||||
|
* @param {analytics} analytics
|
||||||
|
*/
|
||||||
|
constructor (user, req, analytics) {
|
||||||
|
this.user = user;
|
||||||
|
this.req = req || {};
|
||||||
|
this.analytics = analytics;
|
||||||
|
|
||||||
|
this.quantity = _get(req, 'quantity', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut to get the translated string without passing `req.language`
|
||||||
|
* @param {String} key - translation key
|
||||||
|
* @param {*=} params
|
||||||
|
* @returns {*|string}
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
i18n (key, params = {}) {
|
||||||
|
return i18n.t.apply(null, [...arguments, this.req.language]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the Operation allows purchasing items by quantity
|
||||||
|
* @returns Boolean
|
||||||
|
*/
|
||||||
|
multiplePurchaseAllowed () {
|
||||||
|
throw new NotImplementedError('multiplePurchaseAllowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method is called to save the params as class-fields in order to access them
|
||||||
|
*/
|
||||||
|
extractAndValidateParams () {
|
||||||
|
throw new NotImplementedError('extractAndValidateParams');
|
||||||
|
}
|
||||||
|
|
||||||
|
executeChanges () {
|
||||||
|
throw new NotImplementedError('executeChanges');
|
||||||
|
}
|
||||||
|
|
||||||
|
analyticsData () {
|
||||||
|
throw new NotImplementedError('sendToAnalytics');
|
||||||
|
}
|
||||||
|
|
||||||
|
purchase () {
|
||||||
|
if (!this.multiplePurchaseAllowed() && this.quantity > 1) {
|
||||||
|
throw new NotAuthorized(this.i18n('messageNotAbleToBuyInBulk'));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.extractAndValidateParams(this.user, this.req);
|
||||||
|
|
||||||
|
let resultObj = this.executeChanges(this.user, this.item, this.req);
|
||||||
|
|
||||||
|
if (this.analytics) {
|
||||||
|
this.sendToAnalytics(this.analyticsData());
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendToAnalytics (additionalData = {}) {
|
||||||
|
// spread-operator produces an "unexpected token" error
|
||||||
|
let analyticsData = _merge(additionalData, {
|
||||||
|
// ...additionalData,
|
||||||
|
uuid: this.user._id,
|
||||||
|
category: 'behavior',
|
||||||
|
headers: this.req.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.multiplePurchaseAllowed()) {
|
||||||
|
analyticsData.quantityPurchased = this.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.analytics.track('acquire item', analyticsData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AbstractGoldItemOperation extends AbstractBuyOperation {
|
||||||
|
constructor (user, req, analytics) {
|
||||||
|
super(user, req, analytics);
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemValue (item) {
|
||||||
|
return item.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
canUserPurchase (user, item) {
|
||||||
|
this.item = item;
|
||||||
|
let itemValue = this.getItemValue(item);
|
||||||
|
|
||||||
|
let userGold = user.stats.gp;
|
||||||
|
|
||||||
|
if (userGold < itemValue * this.quantity) {
|
||||||
|
throw new NotAuthorized(this.i18n('messageNotEnoughGold'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.canOwn && !item.canOwn(user)) {
|
||||||
|
throw new NotAuthorized(this.i18n('cannotBuyItem'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
substractCurrency (user, item, quantity = 1) {
|
||||||
|
let itemValue = this.getItemValue(item);
|
||||||
|
|
||||||
|
user.stats.gp -= itemValue * quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
analyticsData () {
|
||||||
|
return {
|
||||||
|
itemKey: this.item.key,
|
||||||
|
itemType: 'Market',
|
||||||
|
acquireMethod: 'Gold',
|
||||||
|
goldCost: this.getItemValue(this.item),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
} from '../../libs/errors';
|
} from '../../libs/errors';
|
||||||
import buyHealthPotion from './buyHealthPotion';
|
import buyHealthPotion from './buyHealthPotion';
|
||||||
import buyArmoire from './buyArmoire';
|
import buyArmoire from './buyArmoire';
|
||||||
import buyGear from './buyGear';
|
import {BuyMarketGearOperation} from './buyMarketGear';
|
||||||
import buyMysterySet from './buyMysterySet';
|
import buyMysterySet from './buyMysterySet';
|
||||||
import buyQuest from './buyQuest';
|
import buyQuest from './buyQuest';
|
||||||
import buySpecialSpell from './buySpecialSpell';
|
import buySpecialSpell from './buySpecialSpell';
|
||||||
@@ -58,9 +58,12 @@ module.exports = function buy (user, req = {}, analytics) {
|
|||||||
case 'special':
|
case 'special':
|
||||||
buyRes = buySpecialSpell(user, req, analytics);
|
buyRes = buySpecialSpell(user, req, analytics);
|
||||||
break;
|
break;
|
||||||
default:
|
default: {
|
||||||
buyRes = buyGear(user, req, analytics);
|
const buyOp = new BuyMarketGearOperation(user, req, analytics);
|
||||||
|
|
||||||
|
buyRes = buyOp.purchase();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return buyRes;
|
return buyRes;
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
import content from '../../content/index';
|
|
||||||
import i18n from '../../i18n';
|
|
||||||
import get from 'lodash/get';
|
|
||||||
import pick from 'lodash/pick';
|
|
||||||
import splitWhitespace from '../../libs/splitWhitespace';
|
|
||||||
import {
|
|
||||||
BadRequest,
|
|
||||||
NotAuthorized,
|
|
||||||
NotFound,
|
|
||||||
} from '../../libs/errors';
|
|
||||||
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));
|
|
||||||
|
|
||||||
let item = content.gear.flat[key];
|
|
||||||
|
|
||||||
if (!item) throw new NotFound(i18n.t('itemNotFound', {key}, req.language));
|
|
||||||
|
|
||||||
if (user.stats.gp < item.value) {
|
|
||||||
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.canOwn && !item.canOwn(user)) {
|
|
||||||
throw new NotAuthorized(i18n.t('cannotBuyItem', req.language));
|
|
||||||
}
|
|
||||||
|
|
||||||
let message;
|
|
||||||
|
|
||||||
if (user.items.gear.owned[item.key]) {
|
|
||||||
throw new NotAuthorized(i18n.t('equipmentAlreadyOwned', req.language));
|
|
||||||
}
|
|
||||||
|
|
||||||
let itemIndex = Number(item.index);
|
|
||||||
|
|
||||||
if (Number.isInteger(itemIndex) && content.classes.includes(item.klass)) {
|
|
||||||
let previousLevelGear = key.replace(/[0-9]/, itemIndex - 1);
|
|
||||||
let hasPreviousLevelGear = user.items.gear.owned[previousLevelGear];
|
|
||||||
let checkIndexToType = itemIndex > (item.type === 'weapon' || item.type === 'shield' && item.klass === 'rogue' ? 0 : 1);
|
|
||||||
|
|
||||||
if (checkIndexToType && !hasPreviousLevelGear) {
|
|
||||||
throw new NotAuthorized(i18n.t('previousGearNotOwned', req.language));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.preferences.autoEquip) {
|
|
||||||
user.items.gear.equipped[item.type] = item.key;
|
|
||||||
message = handleTwoHanded(user, item, undefined, req);
|
|
||||||
}
|
|
||||||
|
|
||||||
removePinnedGearAddPossibleNewOnes(user, `gear.flat.${item.key}`, item.key);
|
|
||||||
|
|
||||||
if (item.last) ultimateGear(user);
|
|
||||||
|
|
||||||
user.stats.gp -= item.value;
|
|
||||||
|
|
||||||
if (!message) {
|
|
||||||
message = i18n.t('messageBought', {
|
|
||||||
itemText: item.text(req.language),
|
|
||||||
}, req.language);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (analytics) {
|
|
||||||
analytics.track('acquire item', {
|
|
||||||
uuid: user._id,
|
|
||||||
itemKey: key,
|
|
||||||
acquireMethod: 'Gold',
|
|
||||||
goldCost: item.value,
|
|
||||||
category: 'behavior',
|
|
||||||
headers: req.headers,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
pick(user, splitWhitespace('items achievements stats flags pinnedItems')),
|
|
||||||
message,
|
|
||||||
];
|
|
||||||
};
|
|
||||||
78
website/common/script/ops/buy/buyMarketGear.js
Normal file
78
website/common/script/ops/buy/buyMarketGear.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import content from '../../content/index';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
import pick from 'lodash/pick';
|
||||||
|
import splitWhitespace from '../../libs/splitWhitespace';
|
||||||
|
import {
|
||||||
|
BadRequest,
|
||||||
|
NotAuthorized,
|
||||||
|
NotFound,
|
||||||
|
} from '../../libs/errors';
|
||||||
|
import handleTwoHanded from '../../fns/handleTwoHanded';
|
||||||
|
import ultimateGear from '../../fns/ultimateGear';
|
||||||
|
|
||||||
|
import {removePinnedGearAddPossibleNewOnes} from '../pinnedGearUtils';
|
||||||
|
|
||||||
|
import { AbstractGoldItemOperation } from './abstractBuyOperation';
|
||||||
|
|
||||||
|
export class BuyMarketGearOperation extends AbstractGoldItemOperation {
|
||||||
|
constructor (user, req, analytics) {
|
||||||
|
super(user, req, analytics);
|
||||||
|
}
|
||||||
|
|
||||||
|
multiplePurchaseAllowed () {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
extractAndValidateParams (user, req) {
|
||||||
|
let key = this.key = get(req, 'params.key');
|
||||||
|
if (!key) throw new BadRequest(this.i18n('missingKeyParam'));
|
||||||
|
|
||||||
|
let item = content.gear.flat[key];
|
||||||
|
|
||||||
|
if (!item) throw new NotFound(this.i18n('itemNotFound', {key}));
|
||||||
|
|
||||||
|
this.canUserPurchase(user, item);
|
||||||
|
|
||||||
|
if (user.items.gear.owned[item.key]) {
|
||||||
|
throw new NotAuthorized(this.i18n('equipmentAlreadyOwned'));
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemIndex = Number(item.index);
|
||||||
|
|
||||||
|
if (Number.isInteger(itemIndex) && content.classes.includes(item.klass)) {
|
||||||
|
let previousLevelGear = key.replace(/[0-9]/, itemIndex - 1);
|
||||||
|
let hasPreviousLevelGear = user.items.gear.owned[previousLevelGear];
|
||||||
|
let checkIndexToType = itemIndex > (item.type === 'weapon' || item.type === 'shield' && item.klass === 'rogue' ? 0 : 1);
|
||||||
|
|
||||||
|
if (checkIndexToType && !hasPreviousLevelGear) {
|
||||||
|
throw new NotAuthorized(this.i18n('previousGearNotOwned'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
executeChanges (user, item, req) {
|
||||||
|
let message;
|
||||||
|
|
||||||
|
if (user.preferences.autoEquip) {
|
||||||
|
user.items.gear.equipped[item.type] = item.key;
|
||||||
|
message = handleTwoHanded(user, item, undefined, req);
|
||||||
|
}
|
||||||
|
|
||||||
|
removePinnedGearAddPossibleNewOnes(user, `gear.flat.${item.key}`, item.key);
|
||||||
|
|
||||||
|
if (item.last) ultimateGear(user);
|
||||||
|
|
||||||
|
this.substractCurrency(user, item);
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
message = this.i18n('messageBought', {
|
||||||
|
itemText: item.text(req.language),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
pick(user, splitWhitespace('items achievements stats flags pinnedItems')),
|
||||||
|
message,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user