mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 21:27:23 +01:00
working code and original tests pass
This commit is contained in:
@@ -9,7 +9,7 @@ describe('shared.ops.unlock', () => {
|
|||||||
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
|
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
|
||||||
const backgroundUnlockPath = 'background.giant_florals';
|
const backgroundUnlockPath = 'background.giant_florals';
|
||||||
const unlockCost = 1.25;
|
const unlockCost = 1.25;
|
||||||
const usersStartingGems = 5;
|
const usersStartingGems = 50 / 4;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
user = generateUser();
|
user = generateUser();
|
||||||
@@ -48,32 +48,50 @@ describe('shared.ops.unlock', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns an error when user already owns a full set', done => {
|
it('returns an error when user already owns a full set', done => {
|
||||||
|
let expectedBalance;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
unlock(user, { query: { path: unlockPath } });
|
unlock(user, { query: { path: unlockPath } });
|
||||||
|
expectedBalance = user.balance;
|
||||||
unlock(user, { query: { path: unlockPath } });
|
unlock(user, { query: { path: unlockPath } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
expect(err.message).to.equal(i18n.t('alreadyUnlocked'));
|
expect(err.message).to.equal(i18n.t('alreadyUnlocked'));
|
||||||
expect(user.balance).to.equal(3.75);
|
expect(user.balance).to.equal(expectedBalance);
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns an error when user already owns a full set of gear', done => {
|
it('returns an error when user already owns a full set of gear', done => {
|
||||||
|
let expectedBalance;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
unlock(user, { query: { path: unlockGearSetPath } });
|
unlock(user, { query: { path: unlockGearSetPath } });
|
||||||
|
expectedBalance = user.balance;
|
||||||
unlock(user, { query: { path: unlockGearSetPath } });
|
unlock(user, { query: { path: unlockGearSetPath } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
expect(err.message).to.equal(i18n.t('alreadyUnlocked'));
|
expect(err.message).to.equal(i18n.t('alreadyUnlocked'));
|
||||||
expect(user.balance).to.equal(3.75);
|
expect(user.balance).to.equal(expectedBalance);
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns an error when user already owns items in a full set and it would be more expensive to buy the entire set', done => {
|
it('returns an error when user already owns items in a full set and it would be more expensive to buy the entire set', done => {
|
||||||
try {
|
try {
|
||||||
unlock(user, { query: { path: unlockPath.split(',').splice(2).join(',') } });
|
// There are 11 shirts in the set, each cost 2 gems, the full set 5 gems
|
||||||
|
// In order for the full purchase not to be worth, we must own 9
|
||||||
|
const partialUnlockPaths = unlockPath.split(',');
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[0] } });
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[1] } });
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[2] } });
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[3] } });
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[4] } });
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[5] } });
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[6] } });
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[7] } });
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[8] } });
|
||||||
|
|
||||||
unlock(user, { query: { path: unlockPath } });
|
unlock(user, { query: { path: unlockPath } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||||
@@ -82,6 +100,22 @@ describe('shared.ops.unlock', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not return an error when user already owns items in a full set and it would not be more expensive to buy the entire set', () => {
|
||||||
|
// There are 11 shirts in the set, each cost 2 gems, the full set 5 gems
|
||||||
|
// In order for the full purchase to be worth, we can own already 8
|
||||||
|
const partialUnlockPaths = unlockPath.split(',');
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[0] } });
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[1] } });
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[2] } });
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[3] } });
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[4] } });
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[5] } });
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[6] } });
|
||||||
|
unlock(user, { query: { path: partialUnlockPaths[7] } });
|
||||||
|
|
||||||
|
unlock(user, { query: { path: unlockPath } });
|
||||||
|
});
|
||||||
|
|
||||||
it('equips an item already owned', () => {
|
it('equips an item already owned', () => {
|
||||||
expect(user.purchased.background.giant_florals).to.not.exist;
|
expect(user.purchased.background.giant_florals).to.not.exist;
|
||||||
|
|
||||||
|
|||||||
@@ -23,16 +23,12 @@ forOwn(backgrounds, (value, key) => {
|
|||||||
|
|
||||||
|
|
||||||
const appearances = {
|
const appearances = {
|
||||||
hair, NO appearances.hair.{bangs|base|beard|color|flower|mustache}[key].set.setPrice and .key
|
hair,
|
||||||
shirt: shirts, appearances.shirt[key].set.setPrice and .key
|
shirt: shirts,
|
||||||
size: sizes, NO, does not have cost
|
size: sizes,
|
||||||
skin: skins, OK, appearances.skin[key].set.setPrice and .key
|
skin: skins,
|
||||||
chair: chairs, NO, does not have cost
|
chair: chairs,
|
||||||
background: reorderedBgs, OK appearances.backgroud[key].set.setPrice and .key
|
background: reorderedBgs,
|
||||||
};
|
};
|
||||||
|
|
||||||
^ check with get(path) after validating name because hair are nested for example
|
|
||||||
^ if item.set exist -> use item.setPrice
|
|
||||||
^ what about other items in set?
|
|
||||||
|
|
||||||
export default appearances;
|
export default appearances;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { removeItemByPath } from './pinnedGearUtils';
|
|||||||
import getItemInfo from '../libs/getItemInfo';
|
import getItemInfo from '../libs/getItemInfo';
|
||||||
import content from '../content/index';
|
import content from '../content/index';
|
||||||
|
|
||||||
|
const incentiveBackgrounds = ['blue', 'green', 'red', 'purple', 'yellow'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Splits `items.gear.owned.headAccessory_wolfEars` into `items.gear.owned`
|
* Splits `items.gear.owned.headAccessory_wolfEars` into `items.gear.owned`
|
||||||
* and `headAccessory_wolfEars`
|
* and `headAccessory_wolfEars`
|
||||||
@@ -23,54 +25,88 @@ function invalidSet () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the type of the set (gear or one of the appareance sets - see content.appearance).
|
* Return an item given its path and the type of set
|
||||||
|
*/
|
||||||
|
function getItemByPath (path, setType) {
|
||||||
|
console.log('getting item by path', path, setType);
|
||||||
|
const itemKey = splitPathItem(path)[1];
|
||||||
|
const item = setType === 'gear'
|
||||||
|
? content.gear.flat[itemKey]
|
||||||
|
: content.appearances[setType][itemKey];
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the type of the set (gear or one of the appareance sets - see content.appearances).
|
||||||
*/
|
*/
|
||||||
function getSetType (firstPath) {
|
function getSetType (firstPath) {
|
||||||
if (firstPath.includes('gear.')) return 'gear';
|
if (firstPath.includes('gear.')) return 'gear';
|
||||||
|
|
||||||
const type = firstPath.split('.')[0];
|
const type = firstPath.split('.')[0];
|
||||||
if (content.appearance[type]) return type;
|
if (content.appearances[type]) return type;
|
||||||
|
|
||||||
return invalidSet();
|
return invalidSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the set of items to unlock.
|
* Return the set of items to unlock given the path of the first item in the set.
|
||||||
*/
|
*/
|
||||||
function getSet (setType, firstPath) {
|
function getSet (setType, firstPath) {
|
||||||
const itemKey = splitPathItem(firstPath);
|
console.log('getting set from type and firstpath', setType, firstPath);
|
||||||
const item = setType === 'gear'
|
const item = getItemByPath(firstPath, setType);
|
||||||
? content.gear.flat[itemKey]
|
console.log('item', item);
|
||||||
: content.appearance[setType][itemKey];
|
|
||||||
|
|
||||||
if (!item) return invalidSet();
|
if (!item) return invalidSet();
|
||||||
|
|
||||||
|
|
||||||
if (setType === 'gear') {
|
if (setType === 'gear') {
|
||||||
// Only animal gear sets are unlockable
|
// Only animal gear sets are unlockable
|
||||||
if (item.gearSet !== 'animal') return invalidSet();
|
if (item.gearSet !== 'animal') return invalidSet();
|
||||||
|
|
||||||
} else {
|
// Since each type of gear has only one purchasable set (the animal set)
|
||||||
return item.set
|
// we get all items with the same type and gearSet === 'animal'
|
||||||
|
const items = [];
|
||||||
|
const paths = [];
|
||||||
|
|
||||||
|
Object.keys(content.gear.tree[item.type][item.klass]).forEach(possibleItemKey => {
|
||||||
|
const possibleItem = content.gear.tree[item.type][item.klass][possibleItemKey];
|
||||||
|
if (possibleItem && possibleItem.gearSet === 'animal') {
|
||||||
|
items.push(possibleItem);
|
||||||
|
paths.push(`items.gear.owned.${possibleItem.key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { items, paths, set: { setPrice: 5 } };
|
||||||
}
|
}
|
||||||
|
console.log('not a gear set');
|
||||||
|
|
||||||
|
const { set } = item;
|
||||||
|
if (!set || set.setPrice === 0) return invalidSet();
|
||||||
|
|
||||||
|
const items = [];
|
||||||
|
const paths = [];
|
||||||
|
|
||||||
|
Object.keys(content.appearances[setType]).forEach(possibleItemKey => {
|
||||||
|
const possibleItem = content.appearances[setType][possibleItemKey];
|
||||||
|
if (possibleItem && possibleItem.set && possibleItem.set.key === set.key) {
|
||||||
|
items.push(possibleItem);
|
||||||
|
paths.push(`${setType}.${possibleItem.key}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { items, paths, set };
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add comment
|
/**
|
||||||
function determineCost (setType, isFullSet, set) {
|
* checks if the user has already unlocked this item
|
||||||
if (isBackground) {
|
*/
|
||||||
return isFullSet ? 3.75 : 1.75;
|
function alreadyUnlocked (user, setType, path) {
|
||||||
}
|
const isGear = setType === 'gear';
|
||||||
return isFullSet ? 1.25 : 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Add comment
|
return isGear
|
||||||
function alreadyUnlocked (user, path) {
|
|
||||||
return isGear(path)
|
|
||||||
? get(user, path) !== undefined
|
? get(user, path) !== undefined
|
||||||
: get(user, `purchased.${path}`);
|
: get(user, `purchased.${path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add comment
|
|
||||||
function setAsObject (target, key, value) {
|
function setAsObject (target, key, value) {
|
||||||
// Using Object so path[1] won't create an array but an object {path: {1: value}}
|
// Using Object so path[1] won't create an array but an object {path: {1: value}}
|
||||||
setWith(target, key, value, Object);
|
setWith(target, key, value, Object);
|
||||||
@@ -83,9 +119,10 @@ function markModified (user, path) {
|
|||||||
if (user.markModified) user.markModified(path);
|
if (user.markModified) user.markModified(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add comment
|
function purchaseItem (path, setType, user) {
|
||||||
function purchaseItem (path, user) {
|
const isGear = setType === 'gear';
|
||||||
if (isGear(path)) {
|
|
||||||
|
if (isGear) {
|
||||||
setAsObject(user, path, true);
|
setAsObject(user, path, true);
|
||||||
const itemName = splitPathItem(path)[1];
|
const itemName = splitPathItem(path)[1];
|
||||||
removeItemByPath(user, `gear.flat.${itemName}`);
|
removeItemByPath(user, `gear.flat.${itemName}`);
|
||||||
@@ -96,7 +133,13 @@ function purchaseItem (path, user) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add comment
|
function getIndividualItemPrice (setType, item) {
|
||||||
|
if (setType === 'gear') return 0.5;
|
||||||
|
|
||||||
|
if (!item.price || item.price === 0) return invalidSet();
|
||||||
|
return item.price / 4;
|
||||||
|
}
|
||||||
|
|
||||||
function buildResponse ({ purchased, preference, items }, ownsAlready, language) {
|
function buildResponse ({ purchased, preference, items }, ownsAlready, language) {
|
||||||
const response = [
|
const response = [
|
||||||
{ purchased, preference, items },
|
{ purchased, preference, items },
|
||||||
@@ -117,26 +160,54 @@ export default function unlock (user, req = {}, analytics) {
|
|||||||
|
|
||||||
const setPaths = path.split(',');
|
const setPaths = path.split(',');
|
||||||
const isFullSet = setPaths.length > 1;
|
const isFullSet = setPaths.length > 1;
|
||||||
// We take the first path and use it to get the set,
|
|
||||||
// The passed paths are not used anymore after this point
|
|
||||||
const firstPath = setPaths[0];
|
const firstPath = setPaths[0];
|
||||||
const setType = getSetType(firstPath);
|
|
||||||
const set = getSet(setType, firstPath);
|
|
||||||
const cost = determineCost(setType, isFullSet, set);
|
|
||||||
|
|
||||||
let unlockedAlready;
|
const setType = getSetType(firstPath);
|
||||||
|
const isBackground = setType === 'background';
|
||||||
|
|
||||||
|
// We take the first path and use it to get the set,
|
||||||
|
// The passed paths are not used anymore after this point for full sets
|
||||||
|
const { set, items, paths } = getSet(setType, firstPath);
|
||||||
|
|
||||||
|
let cost;
|
||||||
|
let unlockedAlready = false;
|
||||||
|
|
||||||
|
console.log('isFullSet', isFullSet, 'setType', setType);
|
||||||
|
|
||||||
if (isFullSet) {
|
if (isFullSet) {
|
||||||
const alreadyUnlockedItems = setPaths.filter(p => alreadyUnlocked(user, p)).length;
|
console.log('fullset', {items}, {paths}, {set});
|
||||||
const totalItems = setPaths.length;
|
cost = set.setPrice / 4;
|
||||||
|
|
||||||
|
// all items in a set have the same price
|
||||||
|
const individualPrice = getIndividualItemPrice(setType, items[0]);
|
||||||
|
|
||||||
|
const alreadyUnlockedItems = paths
|
||||||
|
.filter(itemPath => alreadyUnlocked(user, setType, itemPath)).length;
|
||||||
|
const totalItems = items.length;
|
||||||
|
console.log('totalItems', totalItems, 'alreadyUnlockedItems', alreadyUnlockedItems)
|
||||||
if (alreadyUnlockedItems === totalItems) {
|
if (alreadyUnlockedItems === totalItems) {
|
||||||
throw new NotAuthorized(i18n.t('alreadyUnlocked', req.language));
|
throw new NotAuthorized(i18n.t('alreadyUnlocked', req.language));
|
||||||
// TODO Different pull request
|
} else if ((totalItems - alreadyUnlockedItems) * individualPrice < cost) {
|
||||||
// } else if ((totalItems - alreadyOwnedItems) < 3) {
|
throw new NotAuthorized(i18n.t('alreadyUnlockedPart', req.language));
|
||||||
// throw new NotAuthorized(i18n.t('alreadyUnlockedPart', req.language));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
unlockedAlready = alreadyUnlocked(user, path);
|
const item = getItemByPath(firstPath, setType);
|
||||||
|
if (!item || !items.includes(item) || !paths.includes(firstPath)) {
|
||||||
|
return invalidSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
cost = getIndividualItemPrice(setType, item);
|
||||||
|
|
||||||
|
unlockedAlready = alreadyUnlocked(user, setType, firstPath);
|
||||||
|
|
||||||
|
// Since only an item is being unlocked here,
|
||||||
|
// remove all the other items from the set
|
||||||
|
items.splice(0, items.length);
|
||||||
|
paths.splice(0, paths.length);
|
||||||
|
|
||||||
|
// Only keep the item being unlocked
|
||||||
|
items.push(item);
|
||||||
|
paths.push(firstPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBackground && !unlockedAlready
|
if (isBackground && !unlockedAlready
|
||||||
@@ -149,7 +220,7 @@ export default function unlock (user, req = {}, analytics) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isFullSet) {
|
if (isFullSet) {
|
||||||
setPaths.forEach(pathPart => purchaseItem(pathPart, user));
|
paths.forEach(pathPart => purchaseItem(pathPart, setType, user));
|
||||||
} else {
|
} else {
|
||||||
const [key, value] = splitPathItem(path);
|
const [key, value] = splitPathItem(path);
|
||||||
|
|
||||||
@@ -157,9 +228,8 @@ export default function unlock (user, req = {}, analytics) {
|
|||||||
const unsetBackground = isBackground && value === user.preferences.background;
|
const unsetBackground = isBackground && value === user.preferences.background;
|
||||||
setAsObject(user, `preferences.${key}`, unsetBackground ? '' : value);
|
setAsObject(user, `preferences.${key}`, unsetBackground ? '' : value);
|
||||||
} else {
|
} else {
|
||||||
purchaseItem(path, user);
|
purchaseItem(paths[0], setType, user);
|
||||||
|
|
||||||
// @TODO: Test and check test coverage
|
|
||||||
if (isBackground) {
|
if (isBackground) {
|
||||||
const backgroundContent = content.backgroundsFlat[value];
|
const backgroundContent = content.backgroundsFlat[value];
|
||||||
const itemInfo = getItemInfo(user, 'background', backgroundContent);
|
const itemInfo = getItemInfo(user, 'background', backgroundContent);
|
||||||
|
|||||||
Reference in New Issue
Block a user