mirror of
https://github.com/sudoxnym/habitica.git
synced 2026-04-14 19:56:23 +00:00
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:
parent
0af1203832
commit
e6f605f23a
14 changed files with 2932 additions and 2745 deletions
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
2724
website/common/script/content/quests.js
Normal file
2724
website/common/script/content/quests.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Reference in a new issue