Discount Bundled Quests (#8731)

* refactor(content): split quests file

* feat(purchases): sell bundled quests

* fix(style): address linting errors

* test(bundles): shop and purchase tests

* fix(test): remove only

* test(bundles): check balance deduction

* docs(content): comment bundle structure

* fix(test): account for cumulative balance
This commit is contained in:
Sabe Jones 2017-05-17 20:36:34 -05:00 committed by GitHub
parent 0af1203832
commit e6f605f23a
14 changed files with 2932 additions and 2745 deletions

View file

@ -206,6 +206,20 @@ describe('Quests Service', function() {
});
});
context('quest bundles', function() {
it('sends bundle object', function(done) {
questsService.buyQuest('featheredFriends')
.then(function(res) {
expect(res).to.eql(content.bundles.featheredFriends);
expect(window.alert).to.not.be.called;
expect(rejectSpy).to.not.be.called;
done();
}, rejectSpy);
scope.$apply();
});
});
context('all other quests', function() {
it('sends quest object', function(done) {
questsService.buyQuest('whale')

View file

@ -47,11 +47,19 @@ describe('shops', () => {
it('items contain required fields', () => {
_.each(shopCategories, (category) => {
_.each(category.items, (item) => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], (key) => {
expect(_.has(item, key)).to.eql(true);
if (category.identifier === 'bundle') {
_.each(category.items, (item) => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'purchaseType', 'class'], (key) => {
expect(_.has(item, key)).to.eql(true);
});
});
});
} else {
_.each(category.items, (item) => {
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], (key) => {
expect(_.has(item, key)).to.eql(true);
});
});
}
});
});
});

View file

@ -9,6 +9,8 @@ import i18n from '../../../website/common/script/i18n';
import {
generateUser,
} from '../../helpers/common.helper';
import forEach from 'lodash/forEach';
import moment from 'moment';
describe('shared.ops.purchase', () => {
const SEASONAL_FOOD = 'Meat';
@ -200,5 +202,28 @@ describe('shared.ops.purchase', () => {
expect(user.items.gear.owned[key]).to.be.true;
});
it('purchases quest bundles', () => {
let startingBalance = user.balance;
let clock = sandbox.useFakeTimers(moment('2017-05-20').valueOf());
let type = 'bundles';
let key = 'featheredFriends';
let price = 1.75;
let questList = [
'falcon',
'harpy',
'owl',
];
purchase(user, {params: {type, key}});
forEach(questList, (bundledKey) => {
expect(user.items.quests[bundledKey]).to.equal(1);
});
expect(user.balance).to.equal(startingBalance - price);
clock.restore();
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -39,7 +39,7 @@ angular.module('habitrpg')
function buyQuest(quest) {
return $q(function(resolve, reject) {
var item = Content.quests[quest];
var item = Content.quests[quest] || Content.bundles[quest];
var preventQuestModal = _preventQuestModal(item);
if (preventQuestModal) {
@ -73,7 +73,10 @@ angular.module('habitrpg')
function questPopover(quest) {
// The popover gets parsed as markdown (hence the double \n for line breaks
var text = '';
if(quest.boss) {
if (quest.purchaseType === 'bundles') {
text += quest.notes;
}
if (quest.boss) {
text += '**' + window.env.t('bossHP') + ':** ' + quest.boss.hp + '\n\n';
text += '**' + window.env.t('bossStrength') + ':** ' + quest.boss.str + '\n\n';
} else if(quest.collect) {
@ -82,25 +85,27 @@ angular.module('habitrpg')
text += '**' + window.env.t('collect') + ':** ' + quest.collect[key].count + ' ' + quest.collect[key].text() + '\n\n';
}
}
text += '---\n\n';
text += '**' + window.env.t('rewardsAllParticipants') + ':**\n\n';
var participantRewards = _.reject(quest.drop.items, 'onlyOwner');
if(participantRewards.length > 0) {
_.each(participantRewards, function(item) {
text += item.text() + '\n\n';
});
}
if(quest.drop.exp)
text += quest.drop.exp + ' ' + window.env.t('experience') + '\n\n';
if(quest.drop.gp)
text += quest.drop.gp + ' ' + window.env.t('gold') + '\n\n';
if (quest.drop) {
text += '---\n\n';
text += '**' + window.env.t('rewardsAllParticipants') + ':**\n\n';
var participantRewards = _.reject(quest.drop.items, 'onlyOwner');
if(participantRewards.length > 0) {
_.each(participantRewards, function(item) {
text += item.text() + '\n\n';
});
}
if (quest.drop.exp)
text += quest.drop.exp + ' ' + window.env.t('experience') + '\n\n';
if (quest.drop.gp)
text += quest.drop.gp + ' ' + window.env.t('gold') + '\n\n';
var ownerRewards = _.filter(quest.drop.items, 'onlyOwner');
if(ownerRewards.length > 0) {
text += '**' + window.env.t('rewardsQuestOwner') + ':**\n\n';
_.each(ownerRewards, function(item){
text += item.text() + '\n\n';
});
var ownerRewards = _.filter(quest.drop.items, 'onlyOwner');
if (ownerRewards.length > 0) {
text += '**' + window.env.t('rewardsQuestOwner') + ':**\n\n';
_.each(ownerRewards, function(item){
text += item.text() + '\n\n';
});
}
}
return text;
@ -108,7 +113,7 @@ angular.module('habitrpg')
function showQuest(quest) {
return $q(function(resolve, reject) {
var item = Content.quests[quest];
var item = Content.quests[quest];
var preventQuestModal = _preventQuestModal(item);
if (preventQuestModal) {

View file

@ -106,5 +106,6 @@
"spring2017SneakyBunnySet": "Sneaky Bunny (Rogue)",
"eventAvailability": "Available for purchase until <%= date(locale) %>.",
"dateEndApril": "April 19",
"dateEndMay": "May 17"
"dateEndMay": "May 17",
"discountBundle": "bundle"
}

View file

@ -112,5 +112,7 @@
"loginIncentiveQuest": "To earn this quest, check in to Habitica on <%= count %> different days!",
"loginIncentiveQuestObtained": "You earned this quest by checking in to Habitica on <%= count %> different days!",
"loginReward": "<%= count %> Check-ins",
"createAccountQuest": "You received this quest when you joined Habitica! If a friend joins, they'll get one too."
"createAccountQuest": "You received this quest when you joined Habitica! If a friend joins, they'll get one too.",
"questBundles": "Discounted Quest Bundles",
"buyQuestBundle": "Buy Quest Bundle"
}

View file

@ -553,5 +553,8 @@
"questMayhemMistiflying3Boss": "The Wind-Worker",
"questMayhemMistiflying3DropPinkCottonCandy": "Pink Cotton Candy (Food)",
"questMayhemMistiflying3DropShield": "Roguish Rainbow Message (Shield-Hand Weapon)",
"questMayhemMistiflying3DropWeapon": "Roguish Rainbow Message (Weapon)"
"questMayhemMistiflying3DropWeapon": "Roguish Rainbow Message (Weapon)",
"featheredFriendsText": "Feathered Friends Quest Bundle",
"featheredFriendsNotes": "Contains 'Help! Harpy!,' 'The Night-Owl,' and 'The Birds of Preycrastination.' Available until May 31."
}

View file

@ -56,6 +56,7 @@ export const ITEM_LIST = {
quests: { localeKey: 'quest', isEquipment: false },
food: { localeKey: 'foodText', isEquipment: false },
Saddle: { localeKey: 'foodSaddleText', isEquipment: false },
bundles: { localeKey: 'discountBundle', isEquipment: false },
};
export const USER_CAN_OWN_QUEST_CATEGORIES = [

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -114,6 +114,79 @@ shops.getMarketCategories = function getMarket (user, language) {
shops.getQuestShopCategories = function getQuestShopCategories (user, language) {
let categories = [];
/*
* ---------------------------------------------------------------
* Quest Bundles
* ---------------------------------------------------------------
*
* These appear in the Content index.js as follows:
* {
* bundleName: {
* key: 'bundleName',
* text: t('bundleNameText'),
* notes: t('bundleNameNotes'),
* bundleKeys: [
* 'quest1',
* 'quest2',
* 'quest3',
* ],
* canBuy () {
* return true when bundle is available for purchase;
* },
* type: 'quests',
* value: 7,
* },
* secondBundleName: {
* ...
* },
* }
*
* After filtering and mapping, the Shop will produce:
*
* [
* {
* identifier: 'bundle',
* text: 'i18ned string for bundles category',
* items: [
* {
* key: 'bundleName',
* text: 'i18ned string for bundle title',
* notes: 'i18ned string for bundle description',
* value: 7,
* currency: 'gems',
* class: 'quest_bundle_bundleName',
* purchaseType: 'bundles',
* },
* { second bundle },
* ],
* },
* { main quest category 1 },
* ...
* ]
*
*/
let bundleCategory = {
identifier: 'bundle',
text: i18n.t('questBundles', 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',
};
}));
categories.push(bundleCategory);
each(content.userCanOwnQuestCategories, type => {
let category = {
identifier: type,

View file

@ -2,6 +2,7 @@ import content from '../content/index';
import i18n from '../i18n';
import get from 'lodash/get';
import pick from 'lodash/pick';
import forEach from 'lodash/forEach';
import splitWhitespace from '../libs/splitWhitespace';
import planGemLimits from '../libs/planGemLimits';
import {
@ -62,7 +63,7 @@ module.exports = function purchase (user, req = {}, analytics) {
];
}
let acceptedTypes = ['eggs', 'hatchingPotions', 'food', 'quests', 'gear'];
let acceptedTypes = ['eggs', 'hatchingPotions', 'food', 'quests', 'gear', 'bundles'];
if (acceptedTypes.indexOf(type) === -1) {
throw new NotFound(i18n.t('notAccteptedType', req.language));
}
@ -101,6 +102,14 @@ module.exports = function purchase (user, req = {}, analytics) {
if (type === 'gear') {
user.items.gear.owned[key] = true;
} else if (type === 'bundles') {
let subType = item.type;
forEach(item.bundleKeys, function addBundledItems (bundledKey) {
if (!user.items[subType][bundledKey] || user.items[subType][key] < 0) {
user.items[subType][bundledKey] = 0;
}
user.items[subType][bundledKey]++;
});
} else {
if (!user.items[type][key] || user.items[type][key] < 0) {
user.items[type][key] = 0;

View file

@ -20,7 +20,7 @@ mixin questInfo
p(ng-repeat='(k,v) in ::selectedQuest.collect')
| {{:: env.t('collectionItems', { number: selectedQuest.collect[k].count, items: selectedQuest.collect[k].text() })}}
div(ng-bind-html='::selectedQuest.notes()')
quest-rewards(key='{{::selectedQuest.key}}', header-participant=env.t('rewardsAllParticipants'), header-quest-owner=env.t('rewardsQuestOwner'))
quest-rewards(ng-if='::selectedQuest.drop', key='{{::selectedQuest.key}}', header-participant=env.t('rewardsAllParticipants'), header-quest-owner=env.t('rewardsQuestOwner'))
script(type='text/ng-template', id='modals/questCompleted.html')
.modal-header
@ -61,7 +61,8 @@ script(type='text/ng-template', id='modals/buyQuest.html')
+questInfo
.modal-footer
button.btn.btn-default(ng-click='closeQuest(); $close()')=env.t('neverMind')
button.btn.btn-primary(ng-if='::selectedQuest.category !== "gold"', ng-click='purchase("quests", quest); closeQuest(); $close()')=env.t('buyQuest') + ': {{::selectedQuest.value}} ' + env.t('gems')
button.btn.btn-primary(ng-if='::selectedQuest.bundleKeys', ng-click='purchase("bundles", selectedQuest); closeQuest(); $close()')=env.t('buyQuestBundle') + ': {{::selectedQuest.value}} ' + env.t('gems')
button.btn.btn-primary(ng-if='::selectedQuest.category && selectedQuest.category !== "gold"', ng-click='purchase("quests", selectedQuest); closeQuest(); $close()')=env.t('buyQuest') + ': {{::selectedQuest.value}} ' + env.t('gems')
button.btn.btn-primary(ng-if='::selectedQuest.category === "gold"', ng-click='User.buyQuest({params:{key:selectedQuest.key}}); closeQuest(); $close()')=env.t('buyQuest') + ': {{::selectedQuest.goldValue}} ' + env.t('gold')
script(type='text/ng-template', id='modals/questInvitation.html')