mirror of
https://github.com/sudoxnym/habitica.git
synced 2026-07-13 15:38:59 +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() {
|
context('all other quests', function() {
|
||||||
it('sends quest object', function(done) {
|
it('sends quest object', function(done) {
|
||||||
questsService.buyQuest('whale')
|
questsService.buyQuest('whale')
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,19 @@ describe('shops', () => {
|
||||||
|
|
||||||
it('items contain required fields', () => {
|
it('items contain required fields', () => {
|
||||||
_.each(shopCategories, (category) => {
|
_.each(shopCategories, (category) => {
|
||||||
_.each(category.items, (item) => {
|
if (category.identifier === 'bundle') {
|
||||||
_.each(['key', 'text', 'notes', 'value', 'currency', 'locked', 'purchaseType', 'boss', 'class', 'collect', 'drop', 'unlockCondition', 'lvl'], (key) => {
|
_.each(category.items, (item) => {
|
||||||
expect(_.has(item, key)).to.eql(true);
|
_.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 {
|
import {
|
||||||
generateUser,
|
generateUser,
|
||||||
} from '../../helpers/common.helper';
|
} from '../../helpers/common.helper';
|
||||||
|
import forEach from 'lodash/forEach';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
describe('shared.ops.purchase', () => {
|
describe('shared.ops.purchase', () => {
|
||||||
const SEASONAL_FOOD = 'Meat';
|
const SEASONAL_FOOD = 'Meat';
|
||||||
|
|
@ -200,5 +202,28 @@ describe('shared.ops.purchase', () => {
|
||||||
|
|
||||||
expect(user.items.gear.owned[key]).to.be.true;
|
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) {
|
function buyQuest(quest) {
|
||||||
return $q(function(resolve, reject) {
|
return $q(function(resolve, reject) {
|
||||||
var item = Content.quests[quest];
|
var item = Content.quests[quest] || Content.bundles[quest];
|
||||||
|
|
||||||
var preventQuestModal = _preventQuestModal(item);
|
var preventQuestModal = _preventQuestModal(item);
|
||||||
if (preventQuestModal) {
|
if (preventQuestModal) {
|
||||||
|
|
@ -73,7 +73,10 @@ angular.module('habitrpg')
|
||||||
function questPopover(quest) {
|
function questPopover(quest) {
|
||||||
// The popover gets parsed as markdown (hence the double \n for line breaks
|
// The popover gets parsed as markdown (hence the double \n for line breaks
|
||||||
var text = '';
|
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('bossHP') + ':** ' + quest.boss.hp + '\n\n';
|
||||||
text += '**' + window.env.t('bossStrength') + ':** ' + quest.boss.str + '\n\n';
|
text += '**' + window.env.t('bossStrength') + ':** ' + quest.boss.str + '\n\n';
|
||||||
} else if(quest.collect) {
|
} 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 += '**' + window.env.t('collect') + ':** ' + quest.collect[key].count + ' ' + quest.collect[key].text() + '\n\n';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
text += '---\n\n';
|
if (quest.drop) {
|
||||||
text += '**' + window.env.t('rewardsAllParticipants') + ':**\n\n';
|
text += '---\n\n';
|
||||||
var participantRewards = _.reject(quest.drop.items, 'onlyOwner');
|
text += '**' + window.env.t('rewardsAllParticipants') + ':**\n\n';
|
||||||
if(participantRewards.length > 0) {
|
var participantRewards = _.reject(quest.drop.items, 'onlyOwner');
|
||||||
_.each(participantRewards, function(item) {
|
if(participantRewards.length > 0) {
|
||||||
text += item.text() + '\n\n';
|
_.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.exp)
|
||||||
if(quest.drop.gp)
|
text += quest.drop.exp + ' ' + window.env.t('experience') + '\n\n';
|
||||||
text += quest.drop.gp + ' ' + window.env.t('gold') + '\n\n';
|
if (quest.drop.gp)
|
||||||
|
text += quest.drop.gp + ' ' + window.env.t('gold') + '\n\n';
|
||||||
|
|
||||||
var ownerRewards = _.filter(quest.drop.items, 'onlyOwner');
|
var ownerRewards = _.filter(quest.drop.items, 'onlyOwner');
|
||||||
if(ownerRewards.length > 0) {
|
if (ownerRewards.length > 0) {
|
||||||
text += '**' + window.env.t('rewardsQuestOwner') + ':**\n\n';
|
text += '**' + window.env.t('rewardsQuestOwner') + ':**\n\n';
|
||||||
_.each(ownerRewards, function(item){
|
_.each(ownerRewards, function(item){
|
||||||
text += item.text() + '\n\n';
|
text += item.text() + '\n\n';
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
|
|
@ -108,7 +113,7 @@ angular.module('habitrpg')
|
||||||
|
|
||||||
function showQuest(quest) {
|
function showQuest(quest) {
|
||||||
return $q(function(resolve, reject) {
|
return $q(function(resolve, reject) {
|
||||||
var item = Content.quests[quest];
|
var item = Content.quests[quest];
|
||||||
|
|
||||||
var preventQuestModal = _preventQuestModal(item);
|
var preventQuestModal = _preventQuestModal(item);
|
||||||
if (preventQuestModal) {
|
if (preventQuestModal) {
|
||||||
|
|
|
||||||
|
|
@ -106,5 +106,6 @@
|
||||||
"spring2017SneakyBunnySet": "Sneaky Bunny (Rogue)",
|
"spring2017SneakyBunnySet": "Sneaky Bunny (Rogue)",
|
||||||
"eventAvailability": "Available for purchase until <%= date(locale) %>.",
|
"eventAvailability": "Available for purchase until <%= date(locale) %>.",
|
||||||
"dateEndApril": "April 19",
|
"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!",
|
"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!",
|
"loginIncentiveQuestObtained": "You earned this quest by checking in to Habitica on <%= count %> different days!",
|
||||||
"loginReward": "<%= count %> Check-ins",
|
"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",
|
"questMayhemMistiflying3Boss": "The Wind-Worker",
|
||||||
"questMayhemMistiflying3DropPinkCottonCandy": "Pink Cotton Candy (Food)",
|
"questMayhemMistiflying3DropPinkCottonCandy": "Pink Cotton Candy (Food)",
|
||||||
"questMayhemMistiflying3DropShield": "Roguish Rainbow Message (Shield-Hand Weapon)",
|
"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 },
|
quests: { localeKey: 'quest', isEquipment: false },
|
||||||
food: { localeKey: 'foodText', isEquipment: false },
|
food: { localeKey: 'foodText', isEquipment: false },
|
||||||
Saddle: { localeKey: 'foodSaddleText', isEquipment: false },
|
Saddle: { localeKey: 'foodSaddleText', isEquipment: false },
|
||||||
|
bundles: { localeKey: 'discountBundle', isEquipment: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const USER_CAN_OWN_QUEST_CATEGORIES = [
|
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) {
|
shops.getQuestShopCategories = function getQuestShopCategories (user, language) {
|
||||||
let categories = [];
|
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 => {
|
each(content.userCanOwnQuestCategories, type => {
|
||||||
let category = {
|
let category = {
|
||||||
identifier: type,
|
identifier: type,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import content from '../content/index';
|
||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import pick from 'lodash/pick';
|
import pick from 'lodash/pick';
|
||||||
|
import forEach from 'lodash/forEach';
|
||||||
import splitWhitespace from '../libs/splitWhitespace';
|
import splitWhitespace from '../libs/splitWhitespace';
|
||||||
import planGemLimits from '../libs/planGemLimits';
|
import planGemLimits from '../libs/planGemLimits';
|
||||||
import {
|
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) {
|
if (acceptedTypes.indexOf(type) === -1) {
|
||||||
throw new NotFound(i18n.t('notAccteptedType', req.language));
|
throw new NotFound(i18n.t('notAccteptedType', req.language));
|
||||||
}
|
}
|
||||||
|
|
@ -101,6 +102,14 @@ module.exports = function purchase (user, req = {}, analytics) {
|
||||||
|
|
||||||
if (type === 'gear') {
|
if (type === 'gear') {
|
||||||
user.items.gear.owned[key] = true;
|
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 {
|
} else {
|
||||||
if (!user.items[type][key] || user.items[type][key] < 0) {
|
if (!user.items[type][key] || user.items[type][key] < 0) {
|
||||||
user.items[type][key] = 0;
|
user.items[type][key] = 0;
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ mixin questInfo
|
||||||
p(ng-repeat='(k,v) in ::selectedQuest.collect')
|
p(ng-repeat='(k,v) in ::selectedQuest.collect')
|
||||||
| {{:: env.t('collectionItems', { number: selectedQuest.collect[k].count, items: selectedQuest.collect[k].text() })}}
|
| {{:: env.t('collectionItems', { number: selectedQuest.collect[k].count, items: selectedQuest.collect[k].text() })}}
|
||||||
div(ng-bind-html='::selectedQuest.notes()')
|
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')
|
script(type='text/ng-template', id='modals/questCompleted.html')
|
||||||
.modal-header
|
.modal-header
|
||||||
|
|
@ -61,7 +61,8 @@ script(type='text/ng-template', id='modals/buyQuest.html')
|
||||||
+questInfo
|
+questInfo
|
||||||
.modal-footer
|
.modal-footer
|
||||||
button.btn.btn-default(ng-click='closeQuest(); $close()')=env.t('neverMind')
|
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')
|
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')
|
script(type='text/ng-template', id='modals/questInvitation.html')
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue