Hourglass Quest (#11325)
* feat(content): Hourglass Quest * fix(hourglasses): NaN from undefined * fix(quests): sanity check for negative scrolls * fix(hourglasses): don't show quantity selection for binary items * fix(route): validate number, use body not params * test(timetrav): add quest tests
@@ -14,7 +14,7 @@ describe('POST /user/purchase-hourglass/:type/:key', () => {
|
|||||||
|
|
||||||
// More tests in common code unit tests
|
// More tests in common code unit tests
|
||||||
|
|
||||||
it('buys a hourglass pet', async () => {
|
it('buys an hourglass pet', async () => {
|
||||||
let response = await user.post('/user/purchase-hourglass/pets/MantisShrimp-Base');
|
let response = await user.post('/user/purchase-hourglass/pets/MantisShrimp-Base');
|
||||||
await user.sync();
|
await user.sync();
|
||||||
|
|
||||||
@@ -22,4 +22,22 @@ describe('POST /user/purchase-hourglass/:type/:key', () => {
|
|||||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
expect(user.items.pets['MantisShrimp-Base']).to.eql(5);
|
expect(user.items.pets['MantisShrimp-Base']).to.eql(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('buys an hourglass quest', async () => {
|
||||||
|
let response = await user.post('/user/purchase-hourglass/quests/robot');
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
expect(response.message).to.eql(t('hourglassPurchase'));
|
||||||
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||||
|
expect(user.items.quests.robot).to.eql(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('buys multiple hourglass quests', async () => {
|
||||||
|
let response = await user.post('/user/purchase-hourglass/quests/robot', {quantity: 2});
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
expect(response.message).to.eql(t('hourglassPurchase'));
|
||||||
|
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||||
|
expect(user.items.quests.robot).to.eql(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -306,7 +306,7 @@
|
|||||||
const hideAmountSelectionForPurchaseTypes = [
|
const hideAmountSelectionForPurchaseTypes = [
|
||||||
'gear', 'backgrounds', 'mystery_set', 'card',
|
'gear', 'backgrounds', 'mystery_set', 'card',
|
||||||
'rebirth_orb', 'fortify', 'armoire', 'keys',
|
'rebirth_orb', 'fortify', 'armoire', 'keys',
|
||||||
'debuffPotion',
|
'debuffPotion', 'pets', 'mounts',
|
||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
strong {{ $t('howManyToBuy') }}
|
strong {{ $t('howManyToBuy') }}
|
||||||
.box
|
.box
|
||||||
input(type='number', min='0', step='1', v-model.number='selectedAmountToBuy')
|
input(type='number', min='0', step='1', v-model.number='selectedAmountToBuy')
|
||||||
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="(priceType === 'gems') ? icons.gem : icons.gold")
|
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="currencyIcon")
|
||||||
span.value(:class="priceType") {{ item.value }}
|
span.value(:class="priceType") {{ item.value }}
|
||||||
|
|
||||||
button.btn.btn-primary(
|
button.btn.btn-primary(
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
div.clearfix(slot="modal-footer")
|
div.clearfix(slot="modal-footer")
|
||||||
span.balance.float-left {{ $t('yourBalance') }}
|
span.balance.float-left {{ $t('yourBalance') }}
|
||||||
balanceInfo(
|
balanceInfo(
|
||||||
|
:withHourglass="priceType === 'hourglasses'",
|
||||||
:currencyNeeded="priceType",
|
:currencyNeeded="priceType",
|
||||||
:amountNeeded="item.value"
|
:amountNeeded="item.value"
|
||||||
).float-right
|
).float-right
|
||||||
@@ -202,6 +203,7 @@
|
|||||||
import svgGem from 'assets/svg/gem.svg';
|
import svgGem from 'assets/svg/gem.svg';
|
||||||
import svgPin from 'assets/svg/pin.svg';
|
import svgPin from 'assets/svg/pin.svg';
|
||||||
import svgExperience from 'assets/svg/experience.svg';
|
import svgExperience from 'assets/svg/experience.svg';
|
||||||
|
import svgHourglasses from 'assets/svg/hourglass.svg';
|
||||||
|
|
||||||
import BalanceInfo from '../balanceInfo.vue';
|
import BalanceInfo from '../balanceInfo.vue';
|
||||||
import currencyMixin from '../_currencyMixin';
|
import currencyMixin from '../_currencyMixin';
|
||||||
@@ -229,6 +231,7 @@
|
|||||||
gem: svgGem,
|
gem: svgGem,
|
||||||
pin: svgPin,
|
pin: svgPin,
|
||||||
experience: svgExperience,
|
experience: svgExperience,
|
||||||
|
hourglass: svgHourglasses,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
isPinned: false,
|
isPinned: false,
|
||||||
@@ -258,6 +261,11 @@
|
|||||||
return this.item.notes;
|
return this.item.notes;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
currencyIcon () {
|
||||||
|
if (this.priceType === 'gold') return this.icons.gold;
|
||||||
|
if (this.priceType === 'hourglasses') return this.icons.hourglass;
|
||||||
|
return this.icons.gem;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onChange ($event) {
|
onChange ($event) {
|
||||||
|
|||||||
@@ -68,9 +68,13 @@
|
|||||||
:emptyItem="false",
|
:emptyItem="false",
|
||||||
@click="selectItemToBuy(ctx.item)"
|
@click="selectItemToBuy(ctx.item)"
|
||||||
)
|
)
|
||||||
span(slot="popoverContent", slot-scope="ctx")
|
span(slot="popoverContent", slot-scope="ctx", v-if="category !== 'quests'")
|
||||||
div
|
div
|
||||||
h4.popover-content-title {{ ctx.item.text }}
|
h4.popover-content-title {{ ctx.item.text }}
|
||||||
|
span(slot="popoverContent", slot-scope="ctx", v-if="category === 'quests'")
|
||||||
|
div.questPopover
|
||||||
|
h4.popover-content-title {{ item.text }}
|
||||||
|
questInfo(:quest="item")
|
||||||
|
|
||||||
template(slot="itemBadge", slot-scope="ctx")
|
template(slot="itemBadge", slot-scope="ctx")
|
||||||
span.badge.badge-pill.badge-item.badge-svg(
|
span.badge.badge-pill.badge-item.badge-svg(
|
||||||
@@ -79,6 +83,18 @@
|
|||||||
@click.prevent.stop="togglePinned(ctx.item)"
|
@click.prevent.stop="togglePinned(ctx.item)"
|
||||||
)
|
)
|
||||||
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
|
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
|
||||||
|
buyQuestModal(
|
||||||
|
:item="selectedItemToBuy || {}",
|
||||||
|
:priceType="selectedItemToBuy ? selectedItemToBuy.currency : ''",
|
||||||
|
:withPin="true",
|
||||||
|
@change="resetItemToBuy($event)",
|
||||||
|
)
|
||||||
|
template(slot="item", slot-scope="ctx")
|
||||||
|
item.flat(
|
||||||
|
:item="ctx.item",
|
||||||
|
:itemContentClass="ctx.item.class",
|
||||||
|
:showPopover="false"
|
||||||
|
)
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@@ -225,8 +241,10 @@
|
|||||||
import ItemRows from 'client/components/ui/itemRows';
|
import ItemRows from 'client/components/ui/itemRows';
|
||||||
import toggleSwitch from 'client/components/ui/toggleSwitch';
|
import toggleSwitch from 'client/components/ui/toggleSwitch';
|
||||||
import Avatar from 'client/components/avatar';
|
import Avatar from 'client/components/avatar';
|
||||||
|
import QuestInfo from '../quests/questInfo.vue';
|
||||||
|
|
||||||
import BuyModal from '../buyModal.vue';
|
import BuyModal from '../buyModal.vue';
|
||||||
|
import BuyQuestModal from '../quests/buyQuestModal.vue';
|
||||||
|
|
||||||
import svgPin from 'assets/svg/pin.svg';
|
import svgPin from 'assets/svg/pin.svg';
|
||||||
import svgHourglass from 'assets/svg/hourglass.svg';
|
import svgHourglass from 'assets/svg/hourglass.svg';
|
||||||
@@ -250,9 +268,11 @@
|
|||||||
CountBadge,
|
CountBadge,
|
||||||
ItemRows,
|
ItemRows,
|
||||||
toggleSwitch,
|
toggleSwitch,
|
||||||
|
QuestInfo,
|
||||||
|
|
||||||
Avatar,
|
Avatar,
|
||||||
BuyModal,
|
BuyModal,
|
||||||
|
BuyQuestModal,
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
searchText: _throttle(function throttleSearch () {
|
searchText: _throttle(function throttleSearch () {
|
||||||
@@ -274,6 +294,8 @@
|
|||||||
sortItemsBy: ['AZ', 'sortByNumber'],
|
sortItemsBy: ['AZ', 'sortByNumber'],
|
||||||
selectedSortItemsBy: 'AZ',
|
selectedSortItemsBy: 'AZ',
|
||||||
|
|
||||||
|
selectedItemToBuy: null,
|
||||||
|
|
||||||
hidePinned: false,
|
hidePinned: false,
|
||||||
|
|
||||||
backgroundUpdate: new Date(),
|
backgroundUpdate: new Date(),
|
||||||
@@ -303,11 +325,11 @@
|
|||||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||||
|
|
||||||
let normalGroups = _filter(apiCategories, (c) => {
|
let normalGroups = _filter(apiCategories, (c) => {
|
||||||
return c.identifier === 'mounts' || c.identifier === 'pets';
|
return c.identifier === 'mounts' || c.identifier === 'pets' || c.identifier === 'quests';
|
||||||
});
|
});
|
||||||
|
|
||||||
let setGroups = _filter(apiCategories, (c) => {
|
let setGroups = _filter(apiCategories, (c) => {
|
||||||
return c.identifier !== 'mounts' && c.identifier !== 'pets';
|
return c.identifier !== 'mounts' && c.identifier !== 'pets' && c.identifier !== 'quests';
|
||||||
});
|
});
|
||||||
|
|
||||||
let setCategory = {
|
let setCategory = {
|
||||||
@@ -375,7 +397,18 @@
|
|||||||
return _groupBy(entries, 'group');
|
return _groupBy(entries, 'group');
|
||||||
},
|
},
|
||||||
selectItemToBuy (item) {
|
selectItemToBuy (item) {
|
||||||
|
if (item.purchaseType === 'quests') {
|
||||||
|
this.selectedItemToBuy = item;
|
||||||
|
|
||||||
|
this.$root.$emit('bv::show::modal', 'buy-quest-modal');
|
||||||
|
} else {
|
||||||
this.$root.$emit('buyModal::showItem', item);
|
this.$root.$emit('buyModal::showItem', item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetItemToBuy ($event) {
|
||||||
|
if (!$event) {
|
||||||
|
this.selectedItemToBuy = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
|||||||
@@ -112,12 +112,13 @@ export function purchaseMysterySet (store, params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function purchaseHourglassItem (store, params) {
|
export function purchaseHourglassItem (store, params) {
|
||||||
|
const quantity = params.quantity || 1;
|
||||||
const user = store.state.user.data;
|
const user = store.state.user.data;
|
||||||
let opResult = hourglassPurchaseOp(user, {params});
|
let opResult = hourglassPurchaseOp(user, {params, quantity});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: opResult,
|
result: opResult,
|
||||||
httpCall: axios.post(`/api/v4/user/purchase-hourglass/${params.type}/${params.key}`),
|
httpCall: axios.post(`/api/v4/user/purchase-hourglass/${params.type}/${params.key}`, {quantity}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -253,6 +253,10 @@
|
|||||||
"questEggDolphinMountText": "Dolphin",
|
"questEggDolphinMountText": "Dolphin",
|
||||||
"questEggDolphinAdjective": "a chipper",
|
"questEggDolphinAdjective": "a chipper",
|
||||||
|
|
||||||
|
"questEggRobotText": "Robot",
|
||||||
|
"questEggRobotMountText": "Robot",
|
||||||
|
"questEggRobotAdjective": "a futuristic",
|
||||||
|
|
||||||
"eggNotes": "Find a hatching potion to pour on this egg, and it will hatch into <%= eggAdjective(locale) %> <%= eggText(locale) %>.",
|
"eggNotes": "Find a hatching potion to pour on this egg, and it will hatch into <%= eggAdjective(locale) %> <%= eggText(locale) %>.",
|
||||||
|
|
||||||
"hatchingPotionBase": "Base",
|
"hatchingPotionBase": "Base",
|
||||||
|
|||||||
@@ -763,5 +763,14 @@
|
|||||||
"questSilverCollectMoonRunes": "Moon Runes",
|
"questSilverCollectMoonRunes": "Moon Runes",
|
||||||
"questSilverCollectSilverIngots": "Silver Ingots",
|
"questSilverCollectSilverIngots": "Silver Ingots",
|
||||||
"questSilverDropSilverPotion": "Silver Hatching Potion",
|
"questSilverDropSilverPotion": "Silver Hatching Potion",
|
||||||
"questSilverUnlockText": "Unlocks purchasable Silver hatching potions in the Market"
|
"questSilverUnlockText": "Unlocks purchasable Silver hatching potions in the Market",
|
||||||
|
|
||||||
|
"questRobotText": "Mysterious Mechanical Marvels!",
|
||||||
|
"questRobotNotes": "At Max Capacity labs, @Rev is putting the finishing touches on their newest invention, a robotic Accountability Buddy, when a strange metal vehicle suddenly appears in a plume of smoke, inches from the robot’s Fluctuation Detector! Its occupants, two strange figures dressed in silver, emerge and remove their space helmets, revealing themselves as @FolleMente and @McCoyly.<br><br>“I hypothesize that there was an anomaly in our productivity implementation,” @FolleMente says sheepishly.<br><br>@McCoyly crosses her arms. “That means they neglected to complete their Dailies, which I postulate led to the disintegration of our Productivity Stabilizer. It’s an essential component to time travel that needs consistency to work properly. Our accomplishments power our movement through time and space! I don’t have time to explain further, @Rev. You’ll discover it in 37 years, or perhaps your allies the Mysterious Time Travelers can fill you in. For now, can you help us fix our time machine?”",
|
||||||
|
"questRobotCompletion": "As @Rev and the Accountability Buddy place the last bolt in place, the time machine buzzes to life. @FolleMente and @McCoyly jump aboard. “Thanks for the assist! We’ll see you in the future! By the way, these should help you with your next invention!” With that, the time travelers disappear, but left behind in the wreckage of the old Productivity Stabilizer are three clockwork eggs. Perhaps these will be the crucial components for a new production line of Accountability Buddies!",
|
||||||
|
"questRobotCollectBolts": "Bolts",
|
||||||
|
"questRobotCollectGears": "Gears",
|
||||||
|
"questRobotCollectSprings": "Springs",
|
||||||
|
"questRobotDropRobotEgg": "Robot (Egg)",
|
||||||
|
"questRobotUnlockText": "Unlocks purchasable Robot Eggs in the Market"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -386,6 +386,12 @@ let quests = {
|
|||||||
adjective: t('questEggDolphinAdjective'),
|
adjective: t('questEggDolphinAdjective'),
|
||||||
canBuy: hasQuestAchievementFunction('dolphin'),
|
canBuy: hasQuestAchievementFunction('dolphin'),
|
||||||
},
|
},
|
||||||
|
Robot: {
|
||||||
|
text: t('questEggRobotText'),
|
||||||
|
mountText: t('questEggRobotMountText'),
|
||||||
|
adjective: t('questEggRobotAdjective'),
|
||||||
|
canBuy: hasQuestAchievementFunction('robot'),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
applyEggDefaults(drops, {
|
applyEggDefaults(drops, {
|
||||||
|
|||||||
@@ -3458,6 +3458,50 @@ let quests = {
|
|||||||
unlock: t('questSilverUnlockText'),
|
unlock: t('questSilverUnlockText'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
robot: {
|
||||||
|
text: t('questRobotText'),
|
||||||
|
notes: t('questRobotNotes'),
|
||||||
|
completion: t('questRobotCompletion'),
|
||||||
|
value: 1,
|
||||||
|
category: 'timeTravelers',
|
||||||
|
canBuy () {
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
collect: {
|
||||||
|
bolt: {
|
||||||
|
text: t('questRobotCollectBolts'),
|
||||||
|
count: 15,
|
||||||
|
},
|
||||||
|
gear: {
|
||||||
|
text: t('questRobotCollectGears'),
|
||||||
|
count: 10,
|
||||||
|
},
|
||||||
|
spring: {
|
||||||
|
text: t('questRobotCollectSprings'),
|
||||||
|
count: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
drop: {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'eggs',
|
||||||
|
key: 'Robot',
|
||||||
|
text: t('questRobotDropRobotEgg'),
|
||||||
|
}, {
|
||||||
|
type: 'eggs',
|
||||||
|
key: 'Robot',
|
||||||
|
text: t('questRobotDropRobotEgg'),
|
||||||
|
}, {
|
||||||
|
type: 'eggs',
|
||||||
|
key: 'Robot',
|
||||||
|
text: t('questRobotDropRobotEgg'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
gp: 40,
|
||||||
|
exp: 75,
|
||||||
|
unlock: t('questRobotUnlockText'),
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
each(quests, (v, key) => {
|
each(quests, (v, key) => {
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ module.exports = function getItemInfo (user, type, item, officialPinnedItems, la
|
|||||||
notes: item.notes(language),
|
notes: item.notes(language),
|
||||||
group: item.group,
|
group: item.group,
|
||||||
value: item.goldValue ? item.goldValue : item.value,
|
value: item.goldValue ? item.goldValue : item.value,
|
||||||
currency: item.goldValue ? 'gold' : 'gems',
|
|
||||||
locked,
|
locked,
|
||||||
previous: content.quests[item.previous] ? content.quests[item.previous].text(language) : null,
|
previous: content.quests[item.previous] ? content.quests[item.previous].text(language) : null,
|
||||||
unlockCondition: item.unlockCondition,
|
unlockCondition: item.unlockCondition,
|
||||||
@@ -150,6 +149,13 @@ module.exports = function getItemInfo (user, type, item, officialPinnedItems, la
|
|||||||
path: `quests.${item.key}`,
|
path: `quests.${item.key}`,
|
||||||
pinType: 'quests',
|
pinType: 'quests',
|
||||||
};
|
};
|
||||||
|
if (item.goldValue) {
|
||||||
|
itemInfo.currency = 'gold';
|
||||||
|
} else if (item.category === 'timeTravelers') {
|
||||||
|
itemInfo.currency = 'hourglasses';
|
||||||
|
} else {
|
||||||
|
itemInfo.currency = 'gems';
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'timeTravelers':
|
case 'timeTravelers':
|
||||||
|
|||||||
@@ -333,6 +333,20 @@ shops.getTimeTravelersCategories = function getTimeTravelersCategories (user, la
|
|||||||
let stable = {pets: 'Pet-', mounts: 'Mount_Icon_'};
|
let stable = {pets: 'Pet-', mounts: 'Mount_Icon_'};
|
||||||
|
|
||||||
let officialPinnedItems = getOfficialPinnedItems(user);
|
let officialPinnedItems = getOfficialPinnedItems(user);
|
||||||
|
|
||||||
|
let questCategory = {
|
||||||
|
identifier: 'quests',
|
||||||
|
text: i18n.t('quests', language),
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
for (let key in content.quests) {
|
||||||
|
if (content.quests[key].category === 'timeTravelers') {
|
||||||
|
let item = getItemInfo(user, 'quests', content.quests[key], officialPinnedItems, language);
|
||||||
|
questCategory.items.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
categories.push(questCategory);
|
||||||
|
|
||||||
for (let type in stable) {
|
for (let type in stable) {
|
||||||
if (stable.hasOwnProperty(type)) {
|
if (stable.hasOwnProperty(type)) {
|
||||||
let category = {
|
let category = {
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ import {BuyQuestWithGemOperation} from './buyQuestGem';
|
|||||||
|
|
||||||
// @TODO: when we are sure buy is the only function used, let's move the buy files to a folder
|
// @TODO: when we are sure buy is the only function used, let's move the buy files to a folder
|
||||||
|
|
||||||
module.exports = function buy (user, req = {}, analytics) {
|
module.exports = function buy (user, req = {}, analytics, options = {quantity: 1, hourglass: false}) {
|
||||||
let key = get(req, 'params.key');
|
let key = get(req, 'params.key');
|
||||||
|
const hourglass = options.hourglass;
|
||||||
|
const quantity = options.quantity;
|
||||||
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
|
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
|
||||||
|
|
||||||
// @TODO: Slowly remove the need for key and use type instead
|
// @TODO: Slowly remove the need for key and use type instead
|
||||||
@@ -54,9 +56,13 @@ module.exports = function buy (user, req = {}, analytics) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'quests': {
|
case 'quests': {
|
||||||
|
if (hourglass) {
|
||||||
|
buyRes = hourglassPurchase(user, req, analytics, quantity);
|
||||||
|
} else {
|
||||||
const buyOp = new BuyQuestWithGemOperation(user, req, analytics);
|
const buyOp = new BuyQuestWithGemOperation(user, req, analytics);
|
||||||
|
|
||||||
buyRes = buyOp.purchase();
|
buyRes = buyOp.purchase();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'eggs':
|
case 'eggs':
|
||||||
|
|||||||
@@ -9,13 +9,25 @@ import {
|
|||||||
} from '../../libs/errors';
|
} from '../../libs/errors';
|
||||||
import errorMessage from '../../libs/errorMessage';
|
import errorMessage from '../../libs/errorMessage';
|
||||||
|
|
||||||
module.exports = function purchaseHourglass (user, req = {}, analytics) {
|
module.exports = function purchaseHourglass (user, req = {}, analytics, quantity = 1) {
|
||||||
let key = get(req, 'params.key');
|
let key = get(req, 'params.key');
|
||||||
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
|
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
|
||||||
|
|
||||||
let type = get(req, 'params.type');
|
let type = get(req, 'params.type');
|
||||||
if (!type) throw new BadRequest(errorMessage('missingTypeParam'));
|
if (!type) throw new BadRequest(errorMessage('missingTypeParam'));
|
||||||
|
|
||||||
|
if (type === 'quests') {
|
||||||
|
if (!content.quests[key] || content.quests[key].category !== 'timeTravelers') throw new NotAuthorized(i18n.t('notAllowedHourglass', req.language));
|
||||||
|
if (user.purchased.plan.consecutive.trinkets < quantity) {
|
||||||
|
throw new NotAuthorized(i18n.t('notEnoughHourglasses', req.language));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.items.quests[key] || user.items.quests[key] < 0) user.items.quests[key] = 0;
|
||||||
|
user.items.quests[key] += quantity;
|
||||||
|
user.purchased.plan.consecutive.trinkets -= quantity;
|
||||||
|
|
||||||
|
if (user.markModified) user.markModified('items.quests');
|
||||||
|
} else {
|
||||||
if (!content.timeTravelStable[type]) {
|
if (!content.timeTravelStable[type]) {
|
||||||
throw new NotAuthorized(i18n.t('typeNotAllowedHourglass', {allowedTypes: keys(content.timeTravelStable).toString()}, req.language));
|
throw new NotAuthorized(i18n.t('typeNotAllowedHourglass', {allowedTypes: keys(content.timeTravelStable).toString()}, req.language));
|
||||||
}
|
}
|
||||||
@@ -43,6 +55,7 @@ module.exports = function purchaseHourglass (user, req = {}, analytics) {
|
|||||||
user.items.mounts[key] = true;
|
user.items.mounts[key] = true;
|
||||||
if (user.markModified) user.markModified('items.mounts');
|
if (user.markModified) user.markModified('items.mounts');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (analytics) {
|
if (analytics) {
|
||||||
analytics.track('acquire item', {
|
analytics.track('acquire item', {
|
||||||
|
|||||||
BIN
website/raw_sprites/spritesmith/quests/bosses/quest_robot.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 427 B |
|
After Width: | Height: | Size: 547 B |
|
After Width: | Height: | Size: 376 B |
|
After Width: | Height: | Size: 1.2 KiB |
BIN
website/raw_sprites/spritesmith/stable/eggs/Pet_Egg_Robot.png
Normal file
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 302 B |
|
After Width: | Height: | Size: 334 B |
|
After Width: | Height: | Size: 336 B |
|
After Width: | Height: | Size: 315 B |
|
After Width: | Height: | Size: 329 B |
|
After Width: | Height: | Size: 329 B |
|
After Width: | Height: | Size: 319 B |
|
After Width: | Height: | Size: 772 B |
|
After Width: | Height: | Size: 329 B |
|
After Width: | Height: | Size: 364 B |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1016 B |
|
After Width: | Height: | Size: 1003 B |
|
After Width: | Height: | Size: 1021 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 890 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 485 B |
|
After Width: | Height: | Size: 506 B |
|
After Width: | Height: | Size: 506 B |
|
After Width: | Height: | Size: 486 B |
|
After Width: | Height: | Size: 499 B |
|
After Width: | Height: | Size: 486 B |
|
After Width: | Height: | Size: 458 B |
|
After Width: | Height: | Size: 489 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 676 B |
BIN
website/raw_sprites/spritesmith/stable/pets/Pet-Robot-Base.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
BIN
website/raw_sprites/spritesmith/stable/pets/Pet-Robot-Desert.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
website/raw_sprites/spritesmith/stable/pets/Pet-Robot-Golden.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
website/raw_sprites/spritesmith/stable/pets/Pet-Robot-Red.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
website/raw_sprites/spritesmith/stable/pets/Pet-Robot-Shade.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
BIN
website/raw_sprites/spritesmith/stable/pets/Pet-Robot-White.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
website/raw_sprites/spritesmith/stable/pets/Pet-Robot-Zombie.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
@@ -3,7 +3,7 @@ import { authWithHeaders } from '../../middlewares/auth';
|
|||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
// @TODO export this const, cannot export it from here because only routes are exported from controllers
|
// @TODO export this const, cannot export it from here because only routes are exported from controllers
|
||||||
const LAST_ANNOUNCEMENT_TITLE = 'AUGUST SUBSCRIBER ITEMS REVEALED!';
|
const LAST_ANNOUNCEMENT_TITLE = 'SPECIAL TIME TRAVELERS QUEST!';
|
||||||
const worldDmg = { // @TODO
|
const worldDmg = { // @TODO
|
||||||
bailey: false,
|
bailey: false,
|
||||||
};
|
};
|
||||||
@@ -30,14 +30,14 @@ api.getNews = {
|
|||||||
<div class="mr-3 ${baileyClass}"></div>
|
<div class="mr-3 ${baileyClass}"></div>
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<h1 class="align-self-center">${res.t('newStuff')}</h1>
|
<h1 class="align-self-center">${res.t('newStuff')}</h1>
|
||||||
<h2>8/27/2019 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
|
<h2>8/22/2019 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr/>
|
<hr/>
|
||||||
<div class="promo_mystery_201908 center-block"></div>
|
<div class="quest_robot center-block"></div>
|
||||||
<p>The August Subscriber Item Set has been revealed, and it could be the GOAT: <a href='/user/settings/subscription'>the Footloose Faun Item Set</a>! You only have until August 31 to receive the item set when you subscribe. If you're already an active subscriber, reload the site and then head to Inventory > Items to claim your gear!</p>
|
<h3>Special Time Travelers' Pet Quest: Mysterious Mechanical Marvels!</h3>
|
||||||
<p>Subscribers also receive the ability to buy Gems for Gold -- the longer you subscribe, the more Gems you can buy per month! There are other perks as well, such as longer access to uncompressed data and a cute Jackalope pet. Best of all, subscriptions let us keep Habitica running. Thank you very much for your support -- it means a lot to us.</p>
|
<p>Hello Habiticans! We've released a brand-new quest in the Time Travelers' shop! It will be available at the cost of one <a href='https://habitica.fandom.com/wiki/Mystic_Hourglass' target='_blank'>Mystic Hourglass</a>, and is not limited, so you can buy it anytime you like, and as many times as you like. Get "<a href='/shops/time'>Mysterious Mechanical Marvels</a>", and earn some futuristic Robot pets by completing your real-life tasks!</p>
|
||||||
<div class="small mb-3">by beffymaroo</div>
|
<div class="small mb-3">by Beffymaroo, Rev, artemie, McCoyly, FolleMente, elyons1, QuartzFox, and SabreCat</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -983,12 +983,15 @@ api.purchase = {
|
|||||||
* @apiParam (Path) {String="pets","mounts"} type The type of item to purchase
|
* @apiParam (Path) {String="pets","mounts"} type The type of item to purchase
|
||||||
* @apiParam (Path) {String} key Ex: {Phoenix-Base}. The key for the mount/pet
|
* @apiParam (Path) {String} key Ex: {Phoenix-Base}. The key for the mount/pet
|
||||||
*
|
*
|
||||||
|
* @apiParam (Body) {Integer} [quantity=1] Count of items to buy. Defaults to 1 and is ignored for items where quantity is irrelevant.
|
||||||
|
*
|
||||||
* @apiSuccess {Object} data.items user.items
|
* @apiSuccess {Object} data.items user.items
|
||||||
* @apiSuccess {Object} data.purchasedPlanConsecutive user.purchased.plan.consecutive
|
* @apiSuccess {Object} data.purchasedPlanConsecutive user.purchased.plan.consecutive
|
||||||
* @apiSuccess {String} message Success message
|
* @apiSuccess {String} message Success message
|
||||||
*
|
*
|
||||||
* @apiError {NotAuthorized} NotAvailable Item is not available to be purchased or is not valid.
|
* @apiError {NotAuthorized} NotAvailable Item is not available to be purchased or is not valid.
|
||||||
* @apiError {NotAuthorized} Hourglasses User does not have enough Mystic Hourglasses.
|
* @apiError {NotAuthorized} Hourglasses User does not have enough Mystic Hourglasses.
|
||||||
|
* @apiError {BadRequest} Quantity Quantity to purchase must be a number.
|
||||||
* @apiError {NotFound} Type Type invalid.
|
* @apiError {NotFound} Type Type invalid.
|
||||||
*
|
*
|
||||||
* @apiErrorExample {json}
|
* @apiErrorExample {json}
|
||||||
@@ -1000,7 +1003,9 @@ api.userPurchaseHourglass = {
|
|||||||
url: '/user/purchase-hourglass/:type/:key',
|
url: '/user/purchase-hourglass/:type/:key',
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let purchaseHourglassRes = common.ops.buy(user, req, res.analytics);
|
const quantity = req.body.quantity || 1;
|
||||||
|
if (quantity < 1 || !Number.isInteger(quantity)) throw new BadRequest(res.t('invalidQuantity'), req.language);
|
||||||
|
let purchaseHourglassRes = common.ops.buy(user, req, res.analytics, {quantity, hourglass: true});
|
||||||
await user.save();
|
await user.save();
|
||||||
res.respond(200, ...purchaseHourglassRes);
|
res.respond(200, ...purchaseHourglassRes);
|
||||||
},
|
},
|
||||||
|
|||||||