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
|
||||
|
||||
it('buys a hourglass pet', async () => {
|
||||
it('buys an hourglass pet', async () => {
|
||||
let response = await user.post('/user/purchase-hourglass/pets/MantisShrimp-Base');
|
||||
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.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 = [
|
||||
'gear', 'backgrounds', 'mystery_set', 'card',
|
||||
'rebirth_orb', 'fortify', 'armoire', 'keys',
|
||||
'debuffPotion',
|
||||
'debuffPotion', 'pets', 'mounts',
|
||||
];
|
||||
|
||||
export default {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
strong {{ $t('howManyToBuy') }}
|
||||
.box
|
||||
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 }}
|
||||
|
||||
button.btn.btn-primary(
|
||||
@@ -44,6 +44,7 @@
|
||||
div.clearfix(slot="modal-footer")
|
||||
span.balance.float-left {{ $t('yourBalance') }}
|
||||
balanceInfo(
|
||||
:withHourglass="priceType === 'hourglasses'",
|
||||
:currencyNeeded="priceType",
|
||||
:amountNeeded="item.value"
|
||||
).float-right
|
||||
@@ -202,6 +203,7 @@
|
||||
import svgGem from 'assets/svg/gem.svg';
|
||||
import svgPin from 'assets/svg/pin.svg';
|
||||
import svgExperience from 'assets/svg/experience.svg';
|
||||
import svgHourglasses from 'assets/svg/hourglass.svg';
|
||||
|
||||
import BalanceInfo from '../balanceInfo.vue';
|
||||
import currencyMixin from '../_currencyMixin';
|
||||
@@ -229,6 +231,7 @@
|
||||
gem: svgGem,
|
||||
pin: svgPin,
|
||||
experience: svgExperience,
|
||||
hourglass: svgHourglasses,
|
||||
}),
|
||||
|
||||
isPinned: false,
|
||||
@@ -258,6 +261,11 @@
|
||||
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: {
|
||||
onChange ($event) {
|
||||
|
||||
@@ -68,9 +68,13 @@
|
||||
:emptyItem="false",
|
||||
@click="selectItemToBuy(ctx.item)"
|
||||
)
|
||||
span(slot="popoverContent", slot-scope="ctx")
|
||||
span(slot="popoverContent", slot-scope="ctx", v-if="category !== 'quests'")
|
||||
div
|
||||
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")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
@@ -79,6 +83,18 @@
|
||||
@click.prevent.stop="togglePinned(ctx.item)"
|
||||
)
|
||||
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>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -225,8 +241,10 @@
|
||||
import ItemRows from 'client/components/ui/itemRows';
|
||||
import toggleSwitch from 'client/components/ui/toggleSwitch';
|
||||
import Avatar from 'client/components/avatar';
|
||||
import QuestInfo from '../quests/questInfo.vue';
|
||||
|
||||
import BuyModal from '../buyModal.vue';
|
||||
import BuyQuestModal from '../quests/buyQuestModal.vue';
|
||||
|
||||
import svgPin from 'assets/svg/pin.svg';
|
||||
import svgHourglass from 'assets/svg/hourglass.svg';
|
||||
@@ -250,9 +268,11 @@
|
||||
CountBadge,
|
||||
ItemRows,
|
||||
toggleSwitch,
|
||||
QuestInfo,
|
||||
|
||||
Avatar,
|
||||
BuyModal,
|
||||
BuyQuestModal,
|
||||
},
|
||||
watch: {
|
||||
searchText: _throttle(function throttleSearch () {
|
||||
@@ -274,6 +294,8 @@
|
||||
sortItemsBy: ['AZ', 'sortByNumber'],
|
||||
selectedSortItemsBy: 'AZ',
|
||||
|
||||
selectedItemToBuy: null,
|
||||
|
||||
hidePinned: false,
|
||||
|
||||
backgroundUpdate: new Date(),
|
||||
@@ -303,11 +325,11 @@
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
|
||||
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) => {
|
||||
return c.identifier !== 'mounts' && c.identifier !== 'pets';
|
||||
return c.identifier !== 'mounts' && c.identifier !== 'pets' && c.identifier !== 'quests';
|
||||
});
|
||||
|
||||
let setCategory = {
|
||||
@@ -375,7 +397,18 @@
|
||||
return _groupBy(entries, 'group');
|
||||
},
|
||||
selectItemToBuy (item) {
|
||||
this.$root.$emit('buyModal::showItem', item);
|
||||
if (item.purchaseType === 'quests') {
|
||||
this.selectedItemToBuy = item;
|
||||
|
||||
this.$root.$emit('bv::show::modal', 'buy-quest-modal');
|
||||
} else {
|
||||
this.$root.$emit('buyModal::showItem', item);
|
||||
}
|
||||
},
|
||||
resetItemToBuy ($event) {
|
||||
if (!$event) {
|
||||
this.selectedItemToBuy = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
created () {
|
||||
|
||||
@@ -112,12 +112,13 @@ export function purchaseMysterySet (store, params) {
|
||||
}
|
||||
|
||||
export function purchaseHourglassItem (store, params) {
|
||||
const quantity = params.quantity || 1;
|
||||
const user = store.state.user.data;
|
||||
let opResult = hourglassPurchaseOp(user, {params});
|
||||
let opResult = hourglassPurchaseOp(user, {params, quantity});
|
||||
|
||||
return {
|
||||
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",
|
||||
"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) %>.",
|
||||
|
||||
"hatchingPotionBase": "Base",
|
||||
|
||||
@@ -763,5 +763,14 @@
|
||||
"questSilverCollectMoonRunes": "Moon Runes",
|
||||
"questSilverCollectSilverIngots": "Silver Ingots",
|
||||
"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'),
|
||||
canBuy: hasQuestAchievementFunction('dolphin'),
|
||||
},
|
||||
Robot: {
|
||||
text: t('questEggRobotText'),
|
||||
mountText: t('questEggRobotMountText'),
|
||||
adjective: t('questEggRobotAdjective'),
|
||||
canBuy: hasQuestAchievementFunction('robot'),
|
||||
},
|
||||
};
|
||||
|
||||
applyEggDefaults(drops, {
|
||||
|
||||
@@ -3458,6 +3458,50 @@ let quests = {
|
||||
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) => {
|
||||
|
||||
@@ -132,7 +132,6 @@ module.exports = function getItemInfo (user, type, item, officialPinnedItems, la
|
||||
notes: item.notes(language),
|
||||
group: item.group,
|
||||
value: item.goldValue ? item.goldValue : item.value,
|
||||
currency: item.goldValue ? 'gold' : 'gems',
|
||||
locked,
|
||||
previous: content.quests[item.previous] ? content.quests[item.previous].text(language) : null,
|
||||
unlockCondition: item.unlockCondition,
|
||||
@@ -150,6 +149,13 @@ module.exports = function getItemInfo (user, type, item, officialPinnedItems, la
|
||||
path: `quests.${item.key}`,
|
||||
pinType: 'quests',
|
||||
};
|
||||
if (item.goldValue) {
|
||||
itemInfo.currency = 'gold';
|
||||
} else if (item.category === 'timeTravelers') {
|
||||
itemInfo.currency = 'hourglasses';
|
||||
} else {
|
||||
itemInfo.currency = 'gems';
|
||||
}
|
||||
|
||||
break;
|
||||
case 'timeTravelers':
|
||||
|
||||
@@ -333,6 +333,20 @@ shops.getTimeTravelersCategories = function getTimeTravelersCategories (user, la
|
||||
let stable = {pets: 'Pet-', mounts: 'Mount_Icon_'};
|
||||
|
||||
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) {
|
||||
if (stable.hasOwnProperty(type)) {
|
||||
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
|
||||
|
||||
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');
|
||||
const hourglass = options.hourglass;
|
||||
const quantity = options.quantity;
|
||||
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
|
||||
|
||||
// @TODO: Slowly remove the need for key and use type instead
|
||||
@@ -54,9 +56,13 @@ module.exports = function buy (user, req = {}, analytics) {
|
||||
break;
|
||||
}
|
||||
case 'quests': {
|
||||
const buyOp = new BuyQuestWithGemOperation(user, req, analytics);
|
||||
if (hourglass) {
|
||||
buyRes = hourglassPurchase(user, req, analytics, quantity);
|
||||
} else {
|
||||
const buyOp = new BuyQuestWithGemOperation(user, req, analytics);
|
||||
|
||||
buyRes = buyOp.purchase();
|
||||
buyRes = buyOp.purchase();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'eggs':
|
||||
|
||||
@@ -9,39 +9,52 @@ import {
|
||||
} from '../../libs/errors';
|
||||
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');
|
||||
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
|
||||
|
||||
let type = get(req, 'params.type');
|
||||
if (!type) throw new BadRequest(errorMessage('missingTypeParam'));
|
||||
|
||||
if (!content.timeTravelStable[type]) {
|
||||
throw new NotAuthorized(i18n.t('typeNotAllowedHourglass', {allowedTypes: keys(content.timeTravelStable).toString()}, req.language));
|
||||
}
|
||||
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 (!includes(keys(content.timeTravelStable[type]), key)) {
|
||||
throw new NotAuthorized(i18n.t('notAllowedHourglass', 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.items[type][key]) {
|
||||
throw new NotAuthorized(i18n.t(`${type}AlreadyOwned`, req.language));
|
||||
}
|
||||
if (user.markModified) user.markModified('items.quests');
|
||||
} else {
|
||||
if (!content.timeTravelStable[type]) {
|
||||
throw new NotAuthorized(i18n.t('typeNotAllowedHourglass', {allowedTypes: keys(content.timeTravelStable).toString()}, req.language));
|
||||
}
|
||||
|
||||
if (user.purchased.plan.consecutive.trinkets <= 0) {
|
||||
throw new NotAuthorized(i18n.t('notEnoughHourglasses', req.language));
|
||||
}
|
||||
if (!includes(keys(content.timeTravelStable[type]), key)) {
|
||||
throw new NotAuthorized(i18n.t('notAllowedHourglass', req.language));
|
||||
}
|
||||
|
||||
user.purchased.plan.consecutive.trinkets--;
|
||||
if (user.items[type][key]) {
|
||||
throw new NotAuthorized(i18n.t(`${type}AlreadyOwned`, req.language));
|
||||
}
|
||||
|
||||
if (type === 'pets') {
|
||||
user.items.pets[key] = 5;
|
||||
if (user.markModified) user.markModified('items.pets');
|
||||
}
|
||||
if (user.purchased.plan.consecutive.trinkets <= 0) {
|
||||
throw new NotAuthorized(i18n.t('notEnoughHourglasses', req.language));
|
||||
}
|
||||
|
||||
if (type === 'mounts') {
|
||||
user.items.mounts[key] = true;
|
||||
if (user.markModified) user.markModified('items.mounts');
|
||||
user.purchased.plan.consecutive.trinkets--;
|
||||
|
||||
if (type === 'pets') {
|
||||
user.items.pets[key] = 5;
|
||||
if (user.markModified) user.markModified('items.pets');
|
||||
}
|
||||
|
||||
if (type === 'mounts') {
|
||||
user.items.mounts[key] = true;
|
||||
if (user.markModified) user.markModified('items.mounts');
|
||||
}
|
||||
}
|
||||
|
||||
if (analytics) {
|
||||
|
||||
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 = {};
|
||||
|
||||
// @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
|
||||
bailey: false,
|
||||
};
|
||||
@@ -30,14 +30,14 @@ api.getNews = {
|
||||
<div class="mr-3 ${baileyClass}"></div>
|
||||
<div class="media-body">
|
||||
<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>
|
||||
<hr/>
|
||||
<div class="promo_mystery_201908 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>
|
||||
<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>
|
||||
<div class="small mb-3">by beffymaroo</div>
|
||||
<div class="quest_robot center-block"></div>
|
||||
<h3>Special Time Travelers' Pet Quest: Mysterious Mechanical Marvels!</h3>
|
||||
<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, Rev, artemie, McCoyly, FolleMente, elyons1, QuartzFox, and SabreCat</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -983,12 +983,15 @@ api.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 (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.purchasedPlanConsecutive user.purchased.plan.consecutive
|
||||
* @apiSuccess {String} message Success message
|
||||
*
|
||||
* @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 {BadRequest} Quantity Quantity to purchase must be a number.
|
||||
* @apiError {NotFound} Type Type invalid.
|
||||
*
|
||||
* @apiErrorExample {json}
|
||||
@@ -1000,7 +1003,9 @@ api.userPurchaseHourglass = {
|
||||
url: '/user/purchase-hourglass/:type/:key',
|
||||
async handler (req, res) {
|
||||
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();
|
||||
res.respond(200, ...purchaseHourglassRes);
|
||||
},
|
||||
|
||||