#app-header.row(:class='{sticky: user.preferences.stickyHeader}', v-if='showHeader')
create-party-modal
- members-modal(:group='user.party', :hide-badge="true")
+ members-modal(v-if="user.party._id", :group='user.party', :hide-badge="true")
member-details(:member="user", @click="$router.push({name: 'avatar'})")
.view-party(v-if="user.party && user.party._id")
// TODO button should open the party members modal
diff --git a/website/client/components/inventory/stable/index.vue b/website/client/components/inventory/stable/index.vue
index 7e369b9407..0405044aaa 100644
--- a/website/client/components/inventory/stable/index.vue
+++ b/website/client/components/inventory/stable/index.vue
@@ -48,7 +48,7 @@
div.form-group.clearfix
h3.float-left Hide Missing
- toggle-switch.float-right.hideMissing(
+ toggle-switch.float-right.no-margin(
:label="''",
:checked="hideMissing",
@change="updateHideMissing"
@@ -265,10 +265,6 @@
top: -28px;
}
- .toggle-switch-container.hideMissing {
- margin-top: 0;
- }
-
.hatchablePopover {
width: 180px
}
diff --git a/website/client/components/shops/market/balanceInfo.vue b/website/client/components/shops/balanceInfo.vue
similarity index 63%
rename from website/client/components/shops/market/balanceInfo.vue
rename to website/client/components/shops/balanceInfo.vue
index f6e9a90263..a4955c2f7c 100644
--- a/website/client/components/shops/market/balanceInfo.vue
+++ b/website/client/components/shops/balanceInfo.vue
@@ -1,5 +1,8 @@
div
+ .svg-icon(v-if="withHourglass",v-html="icons.hourglasses")
+ span(v-if="withHourglass") {{userHourglasses | roundBigNumber}}
+
.svg-icon(v-html="icons.gem")
span {{userGems | roundBigNumber}}
.svg-icon(v-html="icons.gold")
@@ -32,15 +35,17 @@ span {
diff --git a/website/client/components/shops/market/buyModal.vue b/website/client/components/shops/buyModal.vue
similarity index 86%
rename from website/client/components/shops/market/buyModal.vue
rename to website/client/components/shops/buyModal.vue
index 6002385f72..af788ef7c2 100644
--- a/website/client/components/shops/market/buyModal.vue
+++ b/website/client/components/shops/buyModal.vue
@@ -20,19 +20,19 @@
slot(name="item", :item="item")
h4.title {{ itemText }}
- div.text {{ itemNotes }}
+ div.text(v-html="itemNotes")
slot(name="additionalInfo", :item="item")
div
- span.svg-icon.inline.icon-32(aria-hidden="true", v-html="(priceType === 'gems') ? icons.gem : icons.gold")
- span.value(:class="priceType") {{ item.value }}
+ span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons[getSvgClass()]")
+ span.value(:class="getSvgClass()") {{ item.value }}
button.btn.btn-primary(@click="buyItem()") {{ $t('buyNow') }}
div.clearfix(slot="modal-footer")
span.balance.float-left {{ $t('yourBalance') }}
- balanceInfo.float-right
+ balanceInfo(:withHourglass="getSvgClass() === 'hourglasses'").float-right
@@ -92,6 +92,10 @@
&.gold {
color: $yellow-10
}
+
+ &.hourglasses {
+ color: $blue-10;
+ }
}
button.btn.btn-primary {
@@ -133,6 +137,7 @@
import svgClose from 'assets/svg/close.svg';
import svgGold from 'assets/svg/gold.svg';
import svgGem from 'assets/svg/gem.svg';
+ import svgHourglasses from 'assets/svg/hourglass.svg';
import svgPin from 'assets/svg/pin.svg';
import BalanceInfo from './balanceInfo.vue';
@@ -147,7 +152,8 @@
icons: Object.freeze({
close: svgClose,
gold: svgGold,
- gem: svgGem,
+ gems: svgGem,
+ hourglasses: svgHourglasses,
pin: svgPin,
}),
};
@@ -173,12 +179,19 @@
this.$emit('change', $event);
},
buyItem () {
- this.$store.dispatch('shops:buyItem', {key: this.item.key});
+ this.$emit('buyPressed', this.item);
this.hideDialog();
},
hideDialog () {
this.$root.$emit('hide::modal', 'buy-modal');
},
+ getSvgClass () {
+ if (this.priceType && this.icons[this.priceType]) {
+ return this.priceType;
+ } else {
+ return 'gold';
+ }
+ },
},
props: {
item: {
diff --git a/website/client/components/shops/market/index.vue b/website/client/components/shops/market/index.vue
index e54d98b65e..de8c378395 100644
--- a/website/client/components/shops/market/index.vue
+++ b/website/client/components/shops/market/index.vue
@@ -17,14 +17,14 @@
span.custom-control-description(v-once) {{ category.text }}
div.form-group.clearfix
- h3.float-left Hide locked
- toggle-switch.float-right.hideMissing(
+ h3.float-left(v-once) {{ $t('hideLocked') }}
+ toggle-switch.float-right.no-margin(
:label="''",
v-model="hideLocked",
)
div.form-group.clearfix
- h3.float-left Hide pinned
- toggle-switch.float-right.hideMissing(
+ h3.float-left(v-once) {{ $t('hidePinned') }}
+ toggle-switch.float-right.no-margin(
:label="''",
v-model="hidePinned",
)
@@ -95,8 +95,8 @@
:items="filteredGear(selectedGroupGearByClass, searchTextThrottled, selectedSortGearBy, hideLocked, hidePinned)",
:itemWidth=94,
:itemMargin=24,
- :showAllLabel="$t('showAllEquipment', { classType: getClassName(selectedGroupGearByClass) })",
- :showLessLabel="$t('showLessEquipment', { classType: getClassName(selectedGroupGearByClass) })"
+ :showAllLabel="$t('showAllGeneric', { type: getClassName(selectedGroupGearByClass) + ' '+$t('equipment') })",
+ :showLessLabel="$t('showLessGeneric', { type: getClassName(selectedGroupGearByClass) + ' '+$t('equipment') })"
)
template(slot="item", scope="ctx")
shopItem(
@@ -111,7 +111,6 @@
)
template(slot="popoverContent", scope="ctx")
equipmentAttributesPopover(:item="ctx.item")
- div {{ ctx.item }}
template(slot="itemBadge", scope="ctx")
span.badge.badge-pill.badge-item.badge-svg(
@@ -155,8 +154,7 @@
)
span(slot="popoverContent")
h4.popover-content-title {{ item.text }}
- div {{ item }}
- div {{ userItems[item.purchaseType][item.key] }}
+
template(slot="itemBadge", scope="ctx")
countBadge(
:show="true",
@@ -227,7 +225,8 @@
:item="selectedGearToBuy",
priceType="gold",
:withPin="true",
- @change="resetGearToBuy($event)"
+ @change="resetGearToBuy($event)",
+ @buyPressed="buyGear($event)"
)
template(slot="item", scope="ctx")
div
@@ -244,7 +243,8 @@
buyModal(
:item="selectedItemToBuy",
:priceType="selectedItemToBuy ? selectedItemToBuy.currency : ''",
- @change="resetItemToBuy($event)"
+ @change="resetItemToBuy($event)",
+ @buyPressed="buyItem($event)"
)
template(slot="item", scope="ctx")
item.flat(
@@ -293,48 +293,6 @@
cursor: pointer;
}
- .featuredItems {
- height: 216px;
-
- .background {
- background: url('~assets/images/market/shop_background.png');
-
- background-repeat: repeat-x;
-
- width: 100%;
- height: 216px;
- position: absolute;
-
- top: 0;
- left: 0;
-
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- }
-
- .content {
- display: flex;
- flex-direction: column;
- }
-
- .npc {
- position: absolute;
- left: 0;
- width: 100%;
- height: 216px;
- background: url('~assets/images/market/market_banner_web_alexnpc.png');
- background-repeat: no-repeat;
-
- .featured-label {
- position: absolute;
- bottom: -14px;
- margin: 0;
- left: 80px;
- }
- }
- }
.featured-label {
margin: 24px auto;
@@ -360,6 +318,50 @@
.standard-page {
position: relative;
}
+
+ .featuredItems {
+ height: 216px;
+
+ .background {
+ background: url('~assets/images/shops/shop_background.png');
+
+ background-repeat: repeat-x;
+
+ width: 100%;
+ height: 216px;
+ position: absolute;
+
+ top: 0;
+ left: 0;
+
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .content {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .npc {
+ position: absolute;
+ left: 0;
+ width: 100%;
+ height: 216px;
+ background: url('~assets/images/shops/market_banner_web_alexnpc.png');
+ background-repeat: no-repeat;
+
+ .featured-label {
+ position: absolute;
+ bottom: -14px;
+ margin: 0;
+ left: 80px;
+ }
+ }
+ }
+
}
@@ -380,7 +382,7 @@
import EquipmentAttributesPopover from 'client/components/inventory/equipment/attributesPopover';
import SellModal from './sellModal.vue';
- import BuyModal from './buyModal.vue';
+ import BuyModal from '../buyModal.vue';
import EquipmentAttributesGrid from './equipmentAttributesGrid.vue';
import bPopover from 'bootstrap-vue/lib/components/popover';
@@ -657,9 +659,15 @@ export default {
item.pinned = !isPinned;
this.$store.dispatch(isPinned ? 'shops:unpinGear' : 'shops:pinGear', {key: item.key});
},
+ buyGear (item) {
+ this.$store.dispatch('shops:buyItem', {key: item.key});
+ },
+ buyItem (item) {
+ this.$store.dispatch('shops:purchase', {type: item.purchaseType, key: item.key});
+ },
},
created () {
- this.$store.dispatch('shops:fetch');
+ this.$store.dispatch('shops:fetchMarket');
this.selectedGroupGearByClass = this.userStats.class;
},
diff --git a/website/client/components/shops/market/sellModal.vue b/website/client/components/shops/market/sellModal.vue
index 88b2e4ee01..dbb80e1c5c 100644
--- a/website/client/components/shops/market/sellModal.vue
+++ b/website/client/components/shops/market/sellModal.vue
@@ -122,7 +122,7 @@
import svgGold from 'assets/svg/gold.svg';
import svgGem from 'assets/svg/gem.svg';
- import BalanceInfo from './balanceInfo.vue';
+ import BalanceInfo from '../balanceInfo.vue';
export default {
components: {
diff --git a/website/client/components/shops/quests/buyQuestModal.vue b/website/client/components/shops/quests/buyQuestModal.vue
new file mode 100644
index 0000000000..2f91b924be
--- /dev/null
+++ b/website/client/components/shops/quests/buyQuestModal.vue
@@ -0,0 +1,290 @@
+
+ b-modal#buy-quest-modal(
+ :visible="true",
+ v-if="item != null",
+ :hide-header="true",
+ @change="onChange($event)"
+ )
+ span.badge.badge-pill.badge-dialog(
+ :class="{'item-selected-badge': true}",
+ v-if="withPin"
+ )
+ span.svg-icon.inline.color.icon-10(v-html="icons.pin")
+
+ div.close
+ span.svg-icon.inline.icon-10(aria-hidden="true", v-html="icons.close", @click="hideDialog()")
+
+ div.content(v-if="item != null")
+
+ div.inner-content
+ slot(name="item", :item="item")
+
+ h4.title {{ itemText }}
+ div.text(v-html="itemNotes")
+
+ slot(name="additionalInfo", :item="item")
+
+ div
+ span.svg-icon.inline.icon-32(aria-hidden="true", v-html="(priceType === 'gems') ? icons.gem : icons.gold")
+ span.value(:class="priceType") {{ item.value }}
+
+ button.btn.btn-primary(@click="buyItem()") {{ $t('buyNow') }}
+
+ div.right-sidebar(v-if="item.drop")
+ h3(v-once) {{ $t('rewards') }}
+ div.reward-item
+ span.svg-icon.inline.icon(v-html="icons.experience")
+ span.reward-text {{ $t('amountExperience', { amount: item.drop.exp }) }}
+ div.reward-item(v-if="item.drop.gp != 0")
+ span.svg-icon.inline.icon(v-html="icons.gold")
+ span.reward-text {{ $t('amountGold', { amount: item.drop.gp }) }}
+ div.reward-item(v-for="drop in item.drop.items")
+ span.icon
+ div(:class="getDropIcon(drop)")
+ span.reward-text {{ getDropName(drop) }}
+
+ div.clearfix(slot="modal-footer")
+ span.balance.float-left {{ $t('yourBalance') }}
+ balanceInfo.float-right
+
+
+
+
+
+
diff --git a/website/client/components/shops/quests/index.vue b/website/client/components/shops/quests/index.vue
new file mode 100644
index 0000000000..135d8549db
--- /dev/null
+++ b/website/client/components/shops/quests/index.vue
@@ -0,0 +1,468 @@
+
+ .row.quests
+ .standard-sidebar
+ .form-group
+ input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
+
+ .form
+ h2(v-once) {{ $t('filter') }}
+ .form-group
+ .form-check(
+ v-for="category in categories",
+ :key="category.identifier",
+ )
+ label.custom-control.custom-checkbox
+ input.custom-control-input(type="checkbox", v-model="viewOptions[category.identifier].selected")
+ span.custom-control-indicator
+ span.custom-control-description(v-once) {{ category.text }}
+
+ div.form-group.clearfix
+ h3.float-left(v-once) {{ $t('hideLocked') }}
+ toggle-switch.float-right.no-margin(
+ :label="''",
+ v-model="hideLocked",
+ )
+ div.form-group.clearfix
+ h3.float-left(v-once) {{ $t('hidePinned') }}
+ toggle-switch.float-right.no-margin(
+ :label="''",
+ v-model="hidePinned",
+ )
+ .standard-page
+ div.featuredItems
+ .background
+ div.npc
+ div.featured-label
+ span.rectangle
+ span.text Ian
+ span.rectangle
+ div.content
+ div.featured-label.with-border
+ span.rectangle
+ span.text(v-once) {{ $t('featuredQuests') }}
+ span.rectangle
+
+ div.items.margin-center
+ shopItem(
+ v-for="item in featuredItems",
+ :key="item.key",
+ :item="item",
+ :price="item.goldValue ? item.goldValue : item.value",
+ :priceType="item.goldValue ? 'gold' : 'gem'",
+ :itemContentClass="'inventory_quest_scroll_'+item.key",
+ :emptyItem="false",
+ :popoverPosition="'top'",
+ @click="selectedItemToBuy = item"
+ )
+ template(slot="popoverContent", scope="ctx")
+ div
+ h4.popover-content-title {{ item.text() }}
+ .popover-content-text(v-html="item.notes()")
+
+ h1.mb-0.page-header(v-once) {{ $t('quests') }}
+
+ .clearfix
+ h2.float-left
+ | {{ $t('items') }}
+
+ div.float-right
+ span.dropdown-label {{ $t('sortBy') }}
+ b-dropdown(:text="$t(selectedSortItemsBy)", right=true)
+ b-dropdown-item(
+ v-for="sort in sortItemsBy",
+ @click="selectedSortItemsBy = sort",
+ :active="selectedSortItemsBy === sort",
+ :key="sort"
+ ) {{ $t(sort) }}
+
+
+ div(
+ v-for="category in categories",
+ v-if="viewOptions[category.identifier].selected"
+ )
+ h2 {{ category.text }}
+
+ itemRows(
+ v-if="category.identifier === 'pet'",
+ :items="questItems(category, selectedSortItemsBy, searchTextThrottled, hideLocked, hidePinned)",
+ :itemWidth=94,
+ :itemMargin=24,
+ :showAllLabel="$t('showAllGeneric', { type: category.text })",
+ :showLessLabel="$t('showLessGeneric', { type: category.text })"
+ )
+ template(slot="item", scope="ctx")
+ shopItem(
+ :key="ctx.item.key",
+ :item="ctx.item",
+ :price="ctx.item.value",
+ :priceType="ctx.item.currency",
+ :itemContentClass="ctx.item.class",
+ :emptyItem="false",
+ @click="selectedItemToBuy = ctx.item"
+ )
+ span(slot="popoverContent", scope="ctx")
+ div
+ h4.popover-content-title {{ ctx.item.text }}
+ .popover-content-text(v-html="ctx.item.notes")
+
+ template(slot="itemBadge", scope="ctx")
+ span.badge.badge-pill.badge-item.badge-svg(
+ :class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
+ @click.prevent.stop="togglePinned(ctx.item)"
+ )
+ span.svg-icon.inline.icon-12.color(v-html="icons.pin")
+
+ countBadge(
+ :show="userItems.quests[ctx.item.key] > 0",
+ :count="userItems.quests[ctx.item.key] || 0"
+ )
+
+ div.grouped-parent(v-else-if="category.identifier === 'unlockable' || category.identifier === 'gold'")
+ div.group(v-for="(items, key) in getGrouped(questItems(category, selectedSortItemsBy, searchTextThrottled, hideLocked, hidePinned))")
+ h3 {{ $t(key) }}
+ div.items
+ shopItem(
+ v-for="item in items",
+ :key="item.key",
+ :item="item",
+ :price="item.value",
+ :priceType="item.currency",
+ :itemContentClass="item.class",
+ :emptyItem="false",
+ :popoverPosition="'top'",
+ @click="selectedItemToBuy = item"
+ )
+ span(slot="popoverContent")
+ div
+ h4.popover-content-title {{ item.text }}
+ .popover-content-text(v-html="item.notes")
+
+ template(slot="itemBadge", scope="ctx")
+ span.badge.badge-pill.badge-item.badge-svg(
+ :class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
+ @click.prevent.stop="togglePinned(ctx.item)"
+ )
+ span.svg-icon.inline.icon-12.color(v-html="icons.pin")
+
+ countBadge(
+ :show="userItems.quests[ctx.item.key] > 0",
+ :count="userItems.quests[ctx.item.key] || 0"
+ )
+
+ div.items(v-else)
+ shopItem(
+ v-for="item in questItems(category, selectedSortItemsBy, searchTextThrottled, hideLocked, hidePinned)",
+ :key="item.key",
+ :item="item",
+ :price="item.value",
+ :priceType="item.currency",
+ :itemContentClass="item.class",
+ :emptyItem="false",
+ :popoverPosition="'top'",
+ @click="selectedItemToBuy = item"
+ )
+ span(slot="popoverContent")
+ div
+ h4.popover-content-title {{ item.text }}
+ .popover-content-text(v-html="item.notes")
+
+ template(slot="itemBadge", scope="ctx")
+ span.badge.badge-pill.badge-item.badge-svg(
+ :class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
+ @click.prevent.stop="togglePinned(ctx.item)"
+ )
+ span.svg-icon.inline.icon-12.color(v-html="icons.pin")
+
+ countBadge(
+ :show="userItems.quests[ctx.item.key] > 0",
+ :count="userItems.quests[ctx.item.key] || 0"
+ )
+
+ buyModal(
+ :item="selectedItemToBuy",
+ :priceType="selectedItemToBuy ? selectedItemToBuy.currency : ''",
+ :withPin="true",
+ @change="resetItemToBuy($event)",
+ @buyPressed="buyItem($event)"
+ )
+ template(slot="item", scope="ctx")
+ item.flat(
+ :item="ctx.item",
+ :itemContentClass="ctx.item.class",
+ :showPopover="false"
+ )
+
+
+
+
+
+
diff --git a/website/client/components/shops/seasonal/index.vue b/website/client/components/shops/seasonal/index.vue
new file mode 100644
index 0000000000..07ce9cf990
--- /dev/null
+++ b/website/client/components/shops/seasonal/index.vue
@@ -0,0 +1,496 @@
+
+ .row.seasonal
+ .standard-sidebar
+ .form-group
+ input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
+
+ .form
+ h2(v-once) {{ $t('filter') }}
+ .form-group
+ .form-check(
+ v-for="category in filterCategories",
+ :key="category.key",
+ )
+ label.custom-control.custom-checkbox
+ input.custom-control-input(type="checkbox", v-model="viewOptions[category.key].selected")
+ span.custom-control-indicator
+ span.custom-control-description(v-once) {{ $t(category.localeKey+'Capitalized') }}
+
+ div.form-group.clearfix
+ h3.float-left(v-once) {{ $t('hidePinned') }}
+ toggle-switch.float-right.no-margin(
+ :label="''",
+ v-model="hidePinned",
+ )
+ .standard-page
+ div.featuredItems
+ .background
+ div.npc
+ div.featured-label
+ span.rectangle
+ span.text Leslie
+ span.rectangle
+ div.content
+ div.featured-label.with-border
+ span.rectangle
+ span.text(v-once) {{ $t('featuredset', { name: featuredSet.text }) }}
+ span.rectangle
+
+ div.items.margin-center
+ shopItem(
+ v-for="item in featuredSet.items",
+ :key="item.key",
+ :item="item",
+ :price="item.value",
+ :priceType="item.currency",
+ :itemContentClass="item.class",
+ :emptyItem="false",
+ :popoverPosition="'top'",
+ @click="selectedItemToBuy = item"
+ )
+ template(slot="popoverContent", scope="ctx")
+ div
+ h4.popover-content-title {{ item.text }}
+ .popover-content-text {{ item.notes }}
+
+ h1.mb-0.page-header(v-once) {{ $t('seasonalShop') }}
+
+ .clearfix
+ h2.float-left
+ | {{ $t('classArmor') }}
+
+ div.float-right
+ span.dropdown-label {{ $t('sortBy') }}
+ b-dropdown(:text="$t(selectedSortItemsBy)", right=true)
+ b-dropdown-item(
+ v-for="sort in sortItemsBy",
+ @click="selectedSortItemsBy = sort",
+ :active="selectedSortItemsBy === sort",
+ :key="sort"
+ ) {{ $t(sort) }}
+
+
+ div(
+ v-for="(groupSets, categoryGroup) in getGroupedCategories(categories)",
+ )
+ h3.classgroup
+ span.svg-icon.inline(v-html="icons[categoryGroup]")
+ span.name(:class="categoryGroup") {{ getClassName(categoryGroup) }}
+
+ div.grouped-parent
+ div.group(
+ v-for="category in groupSets"
+ )
+ h3 {{ category.text }}
+ div.items
+ shopItem(
+ v-for="item in seasonalItems(category, selectedSortItemsBy, searchTextThrottled, viewOptions, hidePinned)",
+ :key="item.key",
+ :item="item",
+ :price="item.value",
+ :priceType="item.currency",
+ :itemContentClass="item.class",
+ :emptyItem="false",
+ :popoverPosition="'top'",
+ @click="selectedItemToBuy = item"
+ )
+ span(slot="popoverContent")
+ div
+ h4.popover-content-title {{ item.text }}
+ .popover-content-text {{ item.notes }}
+
+ template(slot="itemBadge", scope="ctx")
+ span.badge.badge-pill.badge-item.badge-svg(
+ :class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
+ @click.prevent.stop="togglePinned(ctx.item)"
+ )
+ span.svg-icon.inline.icon-12.color(v-html="icons.pin")
+
+
+ div.items(v-if="false")
+ shopItem(
+ v-for="item in seasonalItems(category, selectedSortItemsBy, searchTextThrottled, hidePinned)",
+ :key="item.key",
+ :item="item",
+ :price="item.value",
+ :priceType="item.currency",
+ :itemContentClass="item.class",
+ :emptyItem="false",
+ :popoverPosition="'top'",
+ @click="selectedItemToBuy = item"
+ )
+ span(slot="popoverContent")
+ div
+ h4.popover-content-title {{ item.text }}
+ .popover-content-text {{ item.notes }}
+
+ template(slot="itemBadge", scope="ctx")
+ span.badge.badge-pill.badge-item.badge-svg(
+ :class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
+ @click.prevent.stop="togglePinned(ctx.item)"
+ )
+ span.svg-icon.inline.icon-12.color(v-html="icons.pin")
+
+ buyModal(
+ :item="selectedItemToBuy",
+ :priceType="selectedItemToBuy ? selectedItemToBuy.currency : ''",
+ :withPin="true",
+ @change="resetItemToBuy($event)",
+ @buyPressed="buyItem($event)"
+ )
+ template(slot="item", scope="ctx")
+ item.flat(
+ :item="ctx.item",
+ :itemContentClass="ctx.item.class",
+ :showPopover="false"
+ )
+
+
+
+
+
+
diff --git a/website/client/components/shops/shopItem.vue b/website/client/components/shops/shopItem.vue
index 53619cc94b..a70f8fc2e8 100644
--- a/website/client/components/shops/shopItem.vue
+++ b/website/client/components/shops/shopItem.vue
@@ -15,10 +15,11 @@ b-popover(
span.svg-icon.inline.lock(v-if="item.locked" v-html="icons.lock")
- div.image(:class="itemContentClass")
+ div.image
+ div(:class="itemContentClass")
div.price
- span.svg-icon.inline.icon-16(v-html="(priceType === 'gems') ? icons.gem : icons.gold")
+ span.svg-icon.inline.icon-16(v-html="icons[getSvgClass()]")
span.price-label(:class="priceType") {{ price }}
@@ -47,6 +48,11 @@ b-popover(
}
}
+ .image {
+ height: 50px;
+ }
+
+
.price {
.svg-icon {
padding-top: 2px;
@@ -87,6 +93,7 @@ b-popover(
import svgGem from 'assets/svg/gem.svg';
import svgGold from 'assets/svg/gold.svg';
+ import svgHourglasses from 'assets/svg/hourglass.svg';
import svgLock from 'assets/svg/lock.svg';
export default {
@@ -96,9 +103,10 @@ b-popover(
data () {
return {
icons: Object.freeze({
- gem: svgGem,
+ gems: svgGem,
gold: svgGold,
lock: svgLock,
+ hourglasses: svgHourglasses,
}),
};
},
@@ -137,6 +145,13 @@ b-popover(
click () {
this.$emit('click', {});
},
+ getSvgClass () {
+ if (this.priceType && this.icons[this.priceType]) {
+ return this.priceType;
+ } else {
+ return 'gold';
+ }
+ },
},
};
diff --git a/website/client/components/shops/timeTravelers/index.vue b/website/client/components/shops/timeTravelers/index.vue
new file mode 100644
index 0000000000..30d5842e6a
--- /dev/null
+++ b/website/client/components/shops/timeTravelers/index.vue
@@ -0,0 +1,424 @@
+
+ .row.timeTravelers
+ .standard-sidebar
+ .form-group
+ input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
+
+ .form
+ h2(v-once) {{ $t('filter') }}
+ .form-group
+ .form-check(
+ v-for="category in categories",
+ :key="category.identifier",
+ )
+ label.custom-control.custom-checkbox
+ input.custom-control-input(type="checkbox", v-model="viewOptions[category.identifier].selected")
+ span.custom-control-indicator
+ span.custom-control-description(v-once) {{ category.text }}
+
+ div.form-group.clearfix
+ h3.float-left(v-once) {{ $t('hidePinned') }}
+ toggle-switch.float-right.no-margin(
+ :label="''",
+ v-model="hidePinned",
+ )
+ .standard-page
+ div.featuredItems
+ .background
+ div.npc
+ div.featured-label
+ span.rectangle
+ span.text(v-once) {{ timeTravelers.text }}
+ span.rectangle
+ div.content(v-if="false")
+ div.featured-label.with-border
+ span.rectangle
+ span.text(v-once) {{ $t('featuredQuests') }}
+ span.rectangle
+
+ div.items.margin-center
+ shopItem(
+ v-for="item in featuredItems",
+ :key="item.key",
+ :item="item",
+ :price="item.goldValue ? item.goldValue : item.value",
+ :priceType="item.goldValue ? 'gold' : 'gem'",
+ :itemContentClass="'inventory_quest_scroll_'+item.key",
+ :emptyItem="false",
+ :popoverPosition="'top'",
+ @click="selectedItemToBuy = item"
+ )
+ template(slot="popoverContent", scope="ctx")
+ div
+ h4.popover-content-title {{ item.text() }}
+ .popover-content-text {{ item.notes() }}
+
+ h1.mb-0.page-header(v-once) {{ timeTravelers.text }}
+
+ .clearfix
+ div.float-right
+ span.dropdown-label {{ $t('sortBy') }}
+ b-dropdown(:text="$t(selectedSortItemsBy)", right=true)
+ b-dropdown-item(
+ v-for="sort in sortItemsBy",
+ @click="selectedSortItemsBy = sort",
+ :active="selectedSortItemsBy === sort",
+ :key="sort"
+ ) {{ $t(sort) }}
+
+
+ div(
+ v-for="category in categories",
+ v-if="viewOptions[category.identifier].selected",
+ :class="category.identifier"
+ )
+ h2 {{ category.text }}
+
+ itemRows(
+ :items="travelersItems(category, selectedSortItemsBy, searchTextThrottled, hidePinned)",
+ :itemWidth=94,
+ :itemMargin=24,
+ :showAllLabel="$t('showAllGeneric', { type: category.text })",
+ :showLessLabel="$t('showLessGeneric', { type: category.text })"
+ )
+ template(slot="item", scope="ctx")
+ shopItem(
+ :key="ctx.item.key",
+ :item="ctx.item",
+ :price="ctx.item.value",
+ :priceType="ctx.item.currency",
+ :itemContentClass="getItemClass(ctx.item)",
+ :emptyItem="false",
+ @click="selectedItemToBuy = ctx.item"
+ )
+ span(slot="popoverContent", scope="ctx")
+ div
+ h4.popover-content-title {{ ctx.item.text }}
+ .popover-content-text {{ ctx.item.notes }}
+ div {{ ctx.item }}
+
+ template(slot="itemBadge", scope="ctx")
+ span.badge.badge-pill.badge-item.badge-svg(
+ :class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
+ @click.prevent.stop="togglePinned(ctx.item)"
+ )
+ span.svg-icon.inline.icon-12.color(v-html="icons.pin")
+
+ buyModal(
+ :item="selectedItemToBuy",
+ :priceType="selectedItemToBuy ? selectedItemToBuy.currency : ''",
+ :withPin="true",
+ @change="resetItemToBuy($event)",
+ @buyPressed="buyItem($event)"
+ )
+ template(slot="item", scope="ctx")
+ item.flat(
+ :item="ctx.item",
+ :itemContentClass="ctx.item.class",
+ :showPopover="false"
+ )
+
+
+
+
+
+
diff --git a/website/client/router.js b/website/client/router.js
index fbb81bb30b..42eaff6393 100644
--- a/website/client/router.js
+++ b/website/client/router.js
@@ -6,7 +6,6 @@ import getStore from 'client/store';
// TODO Dummy elements used as placeholder until real components are implemented
import ParentPage from './components/parentPage';
-import Page from './components/page';
// Static Pages
const AppPage = () => import(/* webpackChunkName: "static" */'./components/static/app');
@@ -82,6 +81,9 @@ const ChallengeDetail = () => import(/* webpackChunkName: "challenges" */ './com
// Shops
const ShopsContainer = () => import(/* webpackChunkName: "shops" */'./components/shops/index');
const MarketPage = () => import(/* webpackChunkName: "shops-market" */'./components/shops/market/index');
+const QuestsPage = () => import(/* webpackChunkName: "shops-quest" */'./components/shops/quests/index');
+const SeasonalPage = () => import(/* webpackChunkName: "shops-seasonal" */'./components/shops/seasonal/index');
+const TimeTravelersPage = () => import(/* webpackChunkName: "shops-timetravelers" */'./components/shops/timeTravelers/index');
Vue.use(VueRouter);
@@ -115,9 +117,9 @@ const router = new VueRouter({
component: ShopsContainer,
children: [
{ name: 'market', path: 'market', component: MarketPage },
- { name: 'quests', path: 'quests', component: Page },
- { name: 'seasonal', path: 'seasonal', component: Page },
- { name: 'time', path: 'time', component: Page },
+ { name: 'quests', path: 'quests', component: QuestsPage },
+ { name: 'seasonal', path: 'seasonal', component: SeasonalPage },
+ { name: 'time', path: 'time', component: TimeTravelersPage },
],
},
{ name: 'party', path: '/party', component: GuildPage },
diff --git a/website/client/store/actions/shops.js b/website/client/store/actions/shops.js
index 97ca5af0ab..7581f17540 100644
--- a/website/client/store/actions/shops.js
+++ b/website/client/store/actions/shops.js
@@ -1,9 +1,10 @@
import axios from 'axios';
import { loadAsyncResource } from 'client/libs/asyncResource';
import buyOp from 'common/script/ops/buy';
+import purchaseOp from 'common/script/ops/purchaseWithSpell';
import sellOp from 'common/script/ops/sell';
-export function fetch (store, forceLoad = false) { // eslint-disable-line no-shadow
+export function fetchMarket (store, forceLoad = false) { // eslint-disable-line no-shadow
return loadAsyncResource({
store,
path: 'shops.market',
@@ -15,6 +16,43 @@ export function fetch (store, forceLoad = false) { // eslint-disable-line no-sha
});
}
+export function fetchQuests (store, forceLoad = false) { // eslint-disable-line no-shadow
+ return loadAsyncResource({
+ store,
+ path: 'shops.quests',
+ url: '/api/v3/shops/quests',
+ deserialize (response) {
+ return response.data.data;
+ },
+ forceLoad,
+ });
+}
+
+export function fetchSeasonal (store, forceLoad = false) { // eslint-disable-line no-shadow
+ return loadAsyncResource({
+ store,
+ path: 'shops.seasonal',
+ url: '/api/v3/shops/seasonal',
+ deserialize (response) {
+ return response.data.data;
+ },
+ forceLoad,
+ });
+}
+
+export function fetchTimeTravelers (store, forceLoad = false) { // eslint-disable-line no-shadow
+ return loadAsyncResource({
+ store,
+ path: 'shops.time-travelers',
+ url: '/api/v3/shops/time-travelers',
+ deserialize (response) {
+ return response.data.data;
+ },
+ forceLoad,
+ });
+}
+
+
export function buyItem (store, params) {
const user = store.state.user.data;
buyOp(user, {params});
@@ -25,6 +63,16 @@ export function buyItem (store, params) {
// .catch((err) => console.error('equip', err));
}
+export function purchase (store, params) {
+ const user = store.state.user.data;
+ purchaseOp(user, {params});
+ axios
+ .post(`/api/v3/user/purchase/${params.type}/${params.key}`);
+ // TODO
+ // .then((res) => console.log('equip', res))
+ // .catch((err) => console.error('equip', err));
+}
+
export function sellItems (store, params) {
const user = store.state.user.data;
sellOp(user, {params, query: {amount: params.amount}});
diff --git a/website/client/store/getters/shops.js b/website/client/store/getters/shops.js
index 8564ee3b50..f3cf26c0ed 100644
--- a/website/client/store/getters/shops.js
+++ b/website/client/store/getters/shops.js
@@ -1,3 +1,7 @@
export function market (store) {
return store.state.shops.market;
}
+
+export function quests (store) {
+ return store.state.shops.market;
+}
diff --git a/website/client/store/index.js b/website/client/store/index.js
index c9feee5390..04119885e1 100644
--- a/website/client/store/index.js
+++ b/website/client/store/index.js
@@ -45,6 +45,9 @@ export default function () {
},
shops: {
market: asyncResourceFactory(),
+ quests: asyncResourceFactory(),
+ seasonal: asyncResourceFactory(),
+ 'time-travelers': asyncResourceFactory(),
},
myGuilds: [],
publicGuilds: [],
diff --git a/website/common/locales/en/newClient.json b/website/common/locales/en/newClient.json
index 06683bcd9c..f6d1484a5d 100644
--- a/website/common/locales/en/newClient.json
+++ b/website/common/locales/en/newClient.json
@@ -221,17 +221,29 @@
"sortByStr": "Str",
"sortByInt": "Int",
"classEquipment": "Class Equipment",
- "showAllEquipment": "Show All <%= classType %> Equipment",
- "showLessEquipment": "Show Less <%= classType %> Equipment",
+ "showAllGeneric": "Show All <%= type %>",
+ "showLessGeneric": "Show Less <%= type %>",
"howManyToSell": "How many would you like to sell?",
"yourBalance": "Your balance",
"sell": "Sell",
"buyNow": "Buy Now",
"sortByNumber": "Number",
"featuredItems": "Featured Items!",
+ "hideLocked": "Hide locked",
+ "hidePinned": "Hide pinned",
+ "featuredQuests": "Featured Quests!",
+ "amountExperience": "<%= amount %> Experience",
+ "amountGold": "<%= amount %> Gold",
+ "namedHatchingPotion": "<%= type %> Hatching Potion",
"not_participating": "Not Participating",
"owned": "Owned",
"not_owned": "Not Owned",
"participantsTitle": "Participants",
- "shortName": "Short Name"
+ "shortName": "Short Name",
+ "classArmor": "Class Armor",
+ "backCapitalized": "Back Accessory",
+ "bodyCapitalized": "Body Accessory",
+ "eyewearCapitalized": "Eyewear",
+ "featuredset": "Featured Set <%= name %>",
+ "mysterySets": "Mystery Sets"
}
diff --git a/website/common/locales/en/questsContent.json b/website/common/locales/en/questsContent.json
index 8fdb6f95f0..6e08872049 100644
--- a/website/common/locales/en/questsContent.json
+++ b/website/common/locales/en/questsContent.json
@@ -68,6 +68,7 @@
"questSpiderDropSpiderEgg": "Spider (Egg)",
"questSpiderUnlockText": "Unlocks purchasable Spider eggs in the Market",
+ "questGroupVice": "Vice",
"questVice1Text": "Vice, Part 1: Free Yourself of the Dragon's Influence",
"questVice1Notes": "They say there lies a terrible evil in the caverns of Mt. Habitica. A monster whose presence twists the wills of the strong heroes of the land, turning them towards bad habits and laziness! The beast is a grand dragon of immense power and comprised of the shadows themselves: Vice, the treacherous Shadow Wyrm. Brave Habiteers, stand up and defeat this foul beast once and for all, but only if you believe you can stand against its immense power.
Vice Part 1:
How can you expect to fight the beast if it already has control over you? Don't fall victim to laziness and vice! Work hard to fight against the dragon's dark influence and dispel his hold on you!
",
"questVice1Boss": "Vice's Shade",
@@ -86,6 +87,7 @@
"questVice3DropDragonEgg": "Dragon (Egg)",
"questVice3DropShadeHatchingPotion": "Shade Hatching Potion",
+ "questGroupMoonstone": "Recidivate",
"questMoonstone1Text": "Recidivate, Part 1: The Moonstone Chain",
"questMoonstone1Notes": "A terrible affliction has struck Habiticans. Bad Habits thought long-dead are rising back up with a vengeance. Dishes lie unwashed, textbooks linger unread, and procrastination runs rampant!
You track some of your own returning Bad Habits to the Swamps of Stagnation and discover the culprit: the ghostly Necromancer, Recidivate. You rush in, weapons swinging, but they slide through her specter uselessly.
\"Don’t bother,\" she hisses with a dry rasp. \"Without a chain of moonstones, nothing can harm me – and master jeweler @aurakami scattered all the moonstones across Habitica long ago!\" Panting, you retreat... but you know what you must do.",
"questMoonstone1CollectMoonstone": "Moonstones",
@@ -103,6 +105,7 @@
"questMoonstone3DropRottenMeat": "Rotten Meat (Food)",
"questMoonstone3DropZombiePotion": "Zombie Hatching Potion",
+ "questGroupGoldenknight": "The Golden Knight",
"questGoldenknight1Text": "The Golden Knight, Part 1: A Stern Talking-To",
"questGoldenknight1Notes": "The Golden Knight has been getting on poor Habiticans' cases. Didn't do all of your Dailies? Checked off a negative Habit? She will use this as a reason to harass you about how you should follow her example. She is the shining example of a perfect Habitican, and you are naught but a failure. Well, that is not nice at all! Everyone makes mistakes. They should not have to be met with such negativity for it. Perhaps it is time you gather some testimonies from hurt Habiticans and give the Golden Knight a stern talking-to!",
"questGoldenknight1CollectTestimony": "Testimonies",
@@ -151,6 +154,7 @@
"questSeahorseDropSeahorseEgg": "Seahorse (Egg)",
"questSeahorseUnlockText": "Unlocks purchasable Seahorse eggs in the Market",
+ "questGroupAtom": "Attack of the Mundane",
"questAtom1Text": "Attack of the Mundane, Part 1: Dish Disaster!",
"questAtom1Notes": "You reach the shores of Washed-Up Lake for some well-earned relaxation... But the lake is polluted with unwashed dishes! How did this happen? Well, you simply cannot allow the lake to be in this state. There is only one thing you can do: clean the dishes and save your vacation spot! Better find some soap to clean up this mess. A lot of soap...",
"questAtom1CollectSoapBars": "Bars of Soap",
@@ -253,6 +257,7 @@
"questWhaleDropWhaleEgg": "Whale (Egg)",
"questWhaleUnlockText": "Unlocks purchasable Whale eggs in the Market",
+ "questGroupDilatoryDistress": "Dilatory Distress",
"questDilatoryDistress1Text": "Dilatory Distress, Part 1: Message in a Bottle",
"questDilatoryDistress1Notes": "A message in a bottle arrived from the newly rebuilt city of Dilatory! It reads: \"Dear Habiticans, we need your help once again. Our princess has disappeared and the city is under siege by some unknown watery demons! The mantis shrimps are holding the attackers at bay. Please aid us!\" To make the long journey to the sunken city, one must be able to breathe water. Fortunately, the alchemists @Benga and @hazel can make it all possible! You only have to find the proper ingredients.",
"questDilatoryDistress1Completion": "You don the the finned armor and swim to Dilatory as quickly as you can. The merfolk and their mantis shrimp allies have managed to keep the monsters outside the city for the moment, but they are losing. No sooner are you within the castle walls than the horrifying siege descends!",
@@ -412,6 +417,7 @@
"questBeetleDropBeetleEgg": "Beetle (Egg)",
"questBeetleUnlockText": "Unlocks purchasable Beetle eggs in the Market",
+ "questGroupTaskwoodsTerror": "Terror in the Taskwoods",
"questTaskwoodsTerror1Text": "Terror in the Taskwoods, Part 1: The Blaze in the Taskwoods",
"questTaskwoodsTerror1Notes": "You have never seen the Joyful Reaper so agitated. The ruler of the Flourishing Fields lands her skeleton gryphon mount right in the middle of Productivity Plaza and shouts without dismounting. \"Lovely Habiticans, we need your help! Something is starting fires in the Taskwoods, and we still haven't fully recovered from our battle against Burnout. If it's not halted, the flames could engulf all of our wild orchards and berry bushes!\"
You quickly volunteer, and hasten to the Taskwoods. As you creep into Habitica’s biggest fruit-bearing forest, you suddenly hear clanking and cracking voices from far ahead, and catch the faint smell of smoke. Soon enough, a horde of cackling, flaming skull-creatures flies by you, biting off branches and setting the treetops on fire!",
"questTaskwoodsTerror1Completion": "With the help of the Joyful Reaper and the renowned pyromancer @Beffymaroo, you manage to drive back the swarm. In a show of solidarity, Beffymaroo offers you her Pyromancer's Turban as you move deeper into the forest.",
@@ -450,6 +456,7 @@
"questDustBunniesCompletion": "The dust bunnies vanish into a puff of... well, dust. As it clears, you look around. You'd forgotten how nice this place looks when it's clean. You spy a small pile of gold where the dust used to be. Huh, you'd been wondering where that was!",
"questDustBunniesBoss": "Feral Dust Bunnies",
+ "questGroupMoon": "Lunar Battle",
"questMoon1Text": "Lunar Battle, Part 1: Find the Mysterious Shards",
"questMoon1Notes": "Habiticans have been distracted from their tasks by something strange: twisted shards of stone are appearing across the land. Worried, @Starsystemic the Seer summons you to her tower. She says, \"I've been reading alarming omens about these shards, which have been blighting the land and driving hardworking Habiticans to distraction. I can track the source, but first I'll need to examine the shards. Can you bring some to me?\"",
"questMoon1Completion": "@Starsystemic disappears into her tower to examine the shards you gathered. \"This may be more complicated than we feared,\" says @Beffymaroo, her trusted assistant. \"It will take us some time to discover the cause. Keep checking in every day, and when we know more, we'll send you the next quest scroll.\"",
@@ -482,6 +489,7 @@
"questTriceratopsDropTriceratopsEgg": "Triceratops (Egg)",
"questTriceratopsUnlockText": "Unlocks purchasable Triceratops eggs in the Market",
+ "questGroupStoikalmCalamity": "Stoïkalm Calamity",
"questStoikalmCalamity1Text": "Stoïkalm Calamity, Part 1: Earthen Enemies",
"questStoikalmCalamity1Notes": "A terse missive arrives from @Kiwibot, and the frost-crusted scroll chills your heart as well as your fingertips. \"Visiting Stoïkalm Steppes -- monsters bursting from earth -- send help!\" You gather your party and ride north, but as soon as you venture down from the mountains, the snow beneath your feet explodes and gruesomely grinning skulls surround you!
Suddenly, a spear sails past, burying itself in a skull that was burrowing through the snow in an attempt to catch you unawares. A tall woman in finely-crafted armor gallops into the fray on the back of a mastodon, her long braid swinging as she yanks the spear unceremoniously from the crushed beast. It's time to fight off these foes with the help of Lady Glaciate, the leader of the Mammoth Riders!",
"questStoikalmCalamity1Completion": "As you deliver a final blow to the skulls, they dissipate in a puff of magic. \"The dratted swarm may be gone,\" Lady Glaciate says, \"but we have bigger problems. Follow me.\" She tosses you a cloak to protect you from the chill air, and you ride off after her.",
@@ -528,6 +536,7 @@
"questButterflyDropButterflyEgg": "Caterpillar (Egg)",
"questButterflyUnlockText": "Unlocks purchasable Caterpillar eggs in the Market",
+ "questGroupMayhemMistiflying": "Mayhem in Mistiflying",
"questMayhemMistiflying1Text": "Mayhem in Mistiflying, Part 1: In Which Mistiflying Experiences a Dreadful Bother",
"questMayhemMistiflying1Notes": "Although local soothsayers predicted pleasant weather, the afternoon is extremely breezy, so you gratefully follow your friend @Kiwibot into their house to escape the blustery day.
Neither of you expects to find the April Fool lounging at the kitchen table.
“Oh, hello,” he says. “Fancy seeing you here. Please, let me offer you some of this delicious tea.”
“That’s…” @Kiwibot begins. “That’s MY—“
“Yes, yes, of course,” says the April Fool, helping himself to some cookies. “Just thought I’d pop indoors and get a nice reprieve from all the tornado-summoning skulls.” He takes a casual sip from his teacup. “Incidentally, the city of Mistiflying is under attack.”
Horrified, you and your friends race to the Stables and saddle your fastest winged mounts. As you soar towards the floating city, you see that a swarm of chattering, flying skulls are laying siege to the city… and several turn their attentions towards you!",
"questMayhemMistiflying1Completion": "The final skull drops from the sky, a shimmering set of rainbow robes clasped in its jaws, but the steady wind has not slackened. Something else is at play here. And where is that slacking April Fool? You pick up the robes, then swoop into the city.",
diff --git a/website/common/script/content/quests.js b/website/common/script/content/quests.js
index 59ce3eea4b..60316a04db 100644
--- a/website/common/script/content/quests.js
+++ b/website/common/script/content/quests.js
@@ -411,6 +411,7 @@ let quests = {
vice1: {
text: t('questVice1Text'),
notes: t('questVice1Notes'),
+ group: 'questGroupVice',
value: 4,
lvl: 30,
category: 'unlockable',
@@ -435,6 +436,7 @@ let quests = {
vice2: {
text: t('questVice2Text'),
notes: t('questVice2Notes'),
+ group: 'questGroupVice',
value: 4,
lvl: 30,
category: 'unlockable',
@@ -461,6 +463,7 @@ let quests = {
vice3: {
text: t('questVice3Text'),
notes: t('questVice3Notes'),
+ group: 'questGroupVice',
completion: t('questVice3Completion'),
previous: 'vice2',
value: 4,
@@ -661,6 +664,7 @@ let quests = {
atom1: {
text: t('questAtom1Text'),
notes: t('questAtom1Notes'),
+ group: 'questGroupAtom',
value: 4,
lvl: 15,
category: 'unlockable',
@@ -686,6 +690,7 @@ let quests = {
atom2: {
text: t('questAtom2Text'),
notes: t('questAtom2Notes'),
+ group: 'questGroupAtom',
previous: 'atom1',
value: 4,
lvl: 15,
@@ -711,6 +716,7 @@ let quests = {
atom3: {
text: t('questAtom3Text'),
notes: t('questAtom3Notes'),
+ group: 'questGroupAtom',
previous: 'atom2',
completion: t('questAtom3Completion'),
value: 4,
@@ -840,6 +846,7 @@ let quests = {
moonstone1: {
text: t('questMoonstone1Text'),
notes: t('questMoonstone1Notes'),
+ group: 'questGroupMoonstone',
value: 4,
lvl: 60,
category: 'unlockable',
@@ -865,6 +872,7 @@ let quests = {
moonstone2: {
text: t('questMoonstone2Text'),
notes: t('questMoonstone2Notes'),
+ group: 'questGroupMoonstone',
value: 4,
lvl: 60,
previous: 'moonstone1',
@@ -890,6 +898,7 @@ let quests = {
moonstone3: {
text: t('questMoonstone3Text'),
notes: t('questMoonstone3Notes'),
+ group: 'questGroupMoonstone',
completion: t('questMoonstone3Completion'),
previous: 'moonstone2',
value: 4,
@@ -947,6 +956,7 @@ let quests = {
goldenknight1: {
text: t('questGoldenknight1Text'),
notes: t('questGoldenknight1Notes'),
+ group: 'questGroupGoldenknight',
value: 4,
lvl: 40,
category: 'unlockable',
@@ -972,6 +982,7 @@ let quests = {
goldenknight2: {
text: t('questGoldenknight2Text'),
notes: t('questGoldenknight2Notes'),
+ group: 'questGroupGoldenknight',
value: 4,
previous: 'goldenknight1',
lvl: 40,
@@ -997,6 +1008,7 @@ let quests = {
goldenknight3: {
text: t('questGoldenknight3Text'),
notes: t('questGoldenknight3Notes'),
+ group: 'questGroupGoldenknight',
completion: t('questGoldenknight3Completion'),
previous: 'goldenknight2',
value: 4,
@@ -1389,6 +1401,7 @@ let quests = {
dilatoryDistress1: {
text: t('questDilatoryDistress1Text'),
notes: t('questDilatoryDistress1Notes'),
+ group: 'questGroupDilatoryDistress',
completion: t('questDilatoryDistress1Completion'),
value: 4,
goldValue: 200,
@@ -1418,6 +1431,7 @@ let quests = {
dilatoryDistress2: {
text: t('questDilatoryDistress2Text'),
notes: t('questDilatoryDistress2Notes'),
+ group: 'questGroupDilatoryDistress',
completion: t('questDilatoryDistress2Completion'),
previous: 'dilatoryDistress1',
value: 4,
@@ -1457,6 +1471,7 @@ let quests = {
dilatoryDistress3: {
text: t('questDilatoryDistress3Text'),
notes: t('questDilatoryDistress3Notes'),
+ group: 'questGroupDilatoryDistress',
completion: t('questDilatoryDistress3Completion'),
previous: 'dilatoryDistress2',
value: 4,
@@ -2065,6 +2080,7 @@ let quests = {
taskwoodsTerror1: {
text: t('questTaskwoodsTerror1Text'),
notes: t('questTaskwoodsTerror1Notes'),
+ group: 'questGroupTaskwoodsTerror',
completion: t('questTaskwoodsTerror1Completion'),
value: 4,
goldValue: 200,
@@ -2103,6 +2119,7 @@ let quests = {
taskwoodsTerror2: {
text: t('questTaskwoodsTerror2Text'),
notes: t('questTaskwoodsTerror2Notes'),
+ group: 'questGroupTaskwoodsTerror',
completion: t('questTaskwoodsTerror2Completion'),
previous: 'taskwoodsTerror1',
value: 4,
@@ -2137,6 +2154,7 @@ let quests = {
taskwoodsTerror3: {
text: t('questTaskwoodsTerror3Text'),
notes: t('questTaskwoodsTerror3Notes'),
+ group: 'questGroupTaskwoodsTerror',
completion: t('questTaskwoodsTerror3Completion'),
previous: 'taskwoodsTerror2',
value: 4,
@@ -2226,6 +2244,7 @@ let quests = {
moon1: {
text: t('questMoon1Text'),
notes: t('questMoon1Notes'),
+ group: 'questGroupMoon',
completion: t('questMoon1Completion'),
value: 4,
category: 'unlockable',
@@ -2255,6 +2274,7 @@ let quests = {
moon2: {
text: t('questMoon2Text'),
notes: t('questMoon2Notes'),
+ group: 'questGroupMoon',
completion: t('questMoon2Completion'),
previous: 'moon1',
value: 4,
@@ -2284,6 +2304,7 @@ let quests = {
moon3: {
text: t('questMoon3Text'),
notes: t('questMoon3Notes'),
+ group: 'questGroupMoon',
completion: t('questMoon3Completion'),
previous: 'moon2',
value: 4,
@@ -2377,6 +2398,7 @@ let quests = {
stoikalmCalamity1: {
text: t('questStoikalmCalamity1Text'),
notes: t('questStoikalmCalamity1Notes'),
+ group: 'questGroupStoikalmCalamity',
completion: t('questStoikalmCalamity1Completion'),
value: 4,
goldValue: 200,
@@ -2415,6 +2437,7 @@ let quests = {
stoikalmCalamity2: {
text: t('questStoikalmCalamity2Text'),
notes: t('questStoikalmCalamity2Notes'),
+ group: 'questGroupStoikalmCalamity',
completion: t('questStoikalmCalamity2Completion'),
previous: 'stoikalmCalamity1',
value: 4,
@@ -2441,6 +2464,7 @@ let quests = {
stoikalmCalamity3: {
text: t('questStoikalmCalamity3Text'),
notes: t('questStoikalmCalamity3Notes'),
+ group: 'questGroupStoikalmCalamity',
completion: t('questStoikalmCalamity3Completion'),
previous: 'stoikalmCalamity2',
value: 4,
@@ -2578,6 +2602,7 @@ let quests = {
mayhemMistiflying1: {
text: t('questMayhemMistiflying1Text'),
notes: t('questMayhemMistiflying1Notes'),
+ group: 'questGroupMayhemMistiflying',
completion: t('questMayhemMistiflying1Completion'),
value: 4,
goldValue: 200,
@@ -2616,6 +2641,7 @@ let quests = {
mayhemMistiflying2: {
text: t('questMayhemMistiflying2Text'),
notes: t('questMayhemMistiflying2Notes'),
+ group: 'questGroupMayhemMistiflying',
completion: t('questMayhemMistiflying2Completion'),
previous: 'mayhemMistiflying1',
value: 4,
@@ -2650,6 +2676,7 @@ let quests = {
mayhemMistiflying3: {
text: t('questMayhemMistiflying3Text'),
notes: t('questMayhemMistiflying3Notes'),
+ group: 'questGroupMayhemMistiflying',
completion: t('questMayhemMistiflying3Completion'),
previous: 'mayhemMistiflying2',
value: 4,
diff --git a/website/common/script/content/shop-featuredItems.js b/website/common/script/content/shop-featuredItems.js
index 0b05de4bc5..fb6bdd7636 100644
--- a/website/common/script/content/shop-featuredItems.js
+++ b/website/common/script/content/shop-featuredItems.js
@@ -5,6 +5,15 @@ const featuredItems = {
'shield_special_0',
'armor_warrior_5',
],
+ quests: [
+ 'dilatoryDistress1',
+ 'dilatoryDistress2',
+ 'dilatoryDistress3',
+ ],
+ seasonal: 'summerMage',
+ timeTravelers: [
+ // TODO
+ ],
};
export default featuredItems;
diff --git a/website/common/script/index.js b/website/common/script/index.js
index dd5590149a..c5819324c6 100644
--- a/website/common/script/index.js
+++ b/website/common/script/index.js
@@ -143,6 +143,7 @@ import equip from './ops/equip';
import changeClass from './ops/changeClass';
import disableClasses from './ops/disableClasses';
import purchase from './ops/purchase';
+import purchaseWithSpell from './ops/purchaseWithSpell';
import purchaseHourglass from './ops/hourglassPurchase';
import readCard from './ops/readCard';
import openMysteryItem from './ops/openMysteryItem';
@@ -179,6 +180,7 @@ api.ops = {
changeClass,
disableClasses,
purchase,
+ purchaseWithSpell,
purchaseHourglass,
readCard,
openMysteryItem,
diff --git a/website/common/script/libs/shops.js b/website/common/script/libs/shops.js
index e65af41c61..7af8294a17 100644
--- a/website/common/script/libs/shops.js
+++ b/website/common/script/libs/shops.js
@@ -127,6 +127,7 @@ shops.getQuestShopCategories = function getQuestShopCategories (user, language)
* key: 'bundleName',
* text: t('bundleNameText'),
* notes: t('bundleNameNotes'),
+ * group: 'group',
* bundleKeys: [
* 'quest1',
* 'quest2',
@@ -154,6 +155,7 @@ shops.getQuestShopCategories = function getQuestShopCategories (user, language)
* key: 'bundleName',
* text: 'i18ned string for bundle title',
* notes: 'i18ned string for bundle description',
+ * group: 'group',
* value: 7,
* currency: 'gems',
* class: 'quest_bundle_bundleName',
@@ -205,6 +207,7 @@ shops.getQuestShopCategories = function getQuestShopCategories (user, language)
key: quest.key,
text: quest.text(language),
notes: quest.notes(language),
+ group: quest.group,
value: quest.goldValue ? quest.goldValue : quest.value,
currency: quest.goldValue ? 'gold' : 'gems',
locked,
@@ -404,6 +407,7 @@ shops.getSeasonalShopCategories = function getSeasonalShopCategories (user, lang
});
if (category.items.length > 0) {
+ category.specialClass = category.items[0].specialClass;
categories.push(category);
}
}
diff --git a/website/common/script/ops/index.js b/website/common/script/ops/index.js
index d82c9dc55a..e6e5855fc7 100644
--- a/website/common/script/ops/index.js
+++ b/website/common/script/ops/index.js
@@ -18,6 +18,7 @@ import blockUser from './blockUser';
import feed from './feed';
import buySpecialSpell from './buySpecialSpell';
import purchase from './purchase';
+import purchaseWithSpell from './purchaseWithSpell';
import releasePets from './releasePets';
import releaseMounts from './releaseMounts';
import releaseBoth from './releaseBoth';
@@ -61,6 +62,7 @@ module.exports = {
feed,
buySpecialSpell,
purchase,
+ purchaseWithSpell,
releasePets,
releaseMounts,
releaseBoth,
diff --git a/website/common/script/ops/purchaseWithSpell.js b/website/common/script/ops/purchaseWithSpell.js
new file mode 100644
index 0000000000..3c4c71dc9a
--- /dev/null
+++ b/website/common/script/ops/purchaseWithSpell.js
@@ -0,0 +1,9 @@
+import buySpecialSpellOp from './buySpecialSpell';
+import purchaseOp from './purchase';
+import get from 'lodash/get';
+
+module.exports = function purchaseWithSpell (user, req = {}, analytics) {
+ const type = get(req.params, 'type');
+
+ return type === 'spells' ? buySpecialSpellOp(user, req) : purchaseOp(user, req, analytics);
+};
diff --git a/website/server/controllers/api-v3/user.js b/website/server/controllers/api-v3/user.js
index cf66e8c1fd..266fe1d2e4 100644
--- a/website/server/controllers/api-v3/user.js
+++ b/website/server/controllers/api-v3/user.js
@@ -1241,7 +1241,7 @@ api.purchase = {
if (!canGetGems) throw new NotAuthorized(res.t('groupPolicyCannotGetGems'));
}
- let purchaseRes = type === 'spells' ? common.ops.buySpecialSpell(user, req) : common.ops.purchase(user, req, res.analytics);
+ let purchaseRes = common.ops.purchaseWithSpell(user, req, res.analytics);
await user.save();
res.respond(200, ...purchaseRes);
},