[WIP] client/inventory/stable (#8737)

* Stable: basic layout (based on equipment)

* extract item popover-content as component

# Conflicts:
#	website/client/components/inventory/item.vue

* extract item-content as slot - list standard pets with image

* dynamic list petGroups in sidebar / content - with selected / open filter

* itemContentClass for pets

* fix filter - extract filter labels

* Filter: Hide Missing

* fix position of pets

* sort by: A-Z, Color, Hatchable

* refactor animalList - created the list once per type and cache it - sort now works before viewing one or all pets

* custom petItem to show the progress

* list specialPets - rename petGroup to animalGroup (more generic)

* equip a pet

* filter animals by input

* count pets

* list mounts

* hatchable pet popover

* hatchable pet popover #2

* PixelPaw Opacity for not owned and not hatchable pets - change item background for unowned pets

* Hold to hatch the pet - cleanup

* add food drawer + countBadge - special mounts - hide un-owned special animals - fixes

* Sliding Drawer: Buttons to scroll left/right

* Drag&Drop: food on pets

* fix hold to hatch - use mouseleave event

* 'Show All' / 'Show Less' - Animals

* Matts Image + Popover + use image width as sidebar width (removed col-2 / col-10)

* fixes: colors, v-once, drawer-errorMessage, strings

* drawerSlider - split items to pages / add divider / add first item of the next page - ResizeDirective

* ResizeDirective - throttle emits by `v-resize="value"` - fix drawer left padding

* show animals by available content width

* change sortBy button / label

* fix pet colors / backgrounds

* DragDropDirective - grabbing cursor

* remove browser specific prefixes

* fix lint issues

* show welcome dialog

* change translationkey (noFood, already exists)
This commit is contained in:
negue
2017-06-23 13:24:10 +02:00
committed by Matteo Pagliazzi
parent 7954763738
commit 187a9f5f19
18 changed files with 1269 additions and 145 deletions

View File

@@ -0,0 +1,9 @@
[draggable] {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
/* (Optional) Apply a "closed-hand" cursor during drag operation. */
[draggable]:active {
cursor: grabbing;
}

View File

@@ -20,4 +20,5 @@
@import './popover'; @import './popover';
@import './item'; @import './item';
@import './stats'; @import './stats';
@import './icon'; @import './icon';
@import './dragdrop';

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="20" viewBox="0 0 13 20">
<path fill="none" fill-rule="evenodd" stroke="#A5A1AC" stroke-width="4" d="M2 2l8 8-8 8"/>
</svg>

After

Width:  |  Height:  |  Size: 186 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="20" viewBox="0 0 13 20">
<path fill="none" fill-rule="evenodd" stroke="#A5A1AC" stroke-width="4" d="M11 2l-8 8 8 8"/>
</svg>

After

Width:  |  Height:  |  Size: 188 B

View File

@@ -91,8 +91,7 @@
} }
.drawer-slider { .drawer-slider {
padding: 12px 0 0 24px; padding: 12px 0 0 0;
margin-left: -24px;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
white-space: nowrap; white-space: nowrap;

View File

@@ -16,13 +16,16 @@ b-popover(
.item-wrapper .item-wrapper
.item .item
slot(name="itemBadge", :item="item") slot(name="itemBadge", :item="item")
span.item-content(:class="itemContentClass") span.item-content(
:class="itemContentClass",
:draggable="draggable",
@dragstart="onDrag"
)
span.item-label(v-if="label") {{ label }} span.item-label(v-if="label") {{ label }}
</template> </template>
<script> <script>
import bPopover from 'bootstrap-vue/lib/components/popover'; import bPopover from 'bootstrap-vue/lib/components/popover';
import { mapState } from 'client/libs/store';
export default { export default {
components: { components: {
@@ -46,16 +49,22 @@ export default {
type: String, type: String,
default: 'bottom', default: 'bottom',
}, },
}, draggable: {
computed: { type: Boolean,
...mapState({ default: false,
ATTRIBUTES: 'constants.ATTRIBUTES', },
}),
}, },
methods: { methods: {
click () { click () {
this.$emit('click', this.item); this.$emit('click', this.item);
}, },
onDrag (ev) {
if (this.draggable) {
this.$emit('onDrag', ev);
} else {
ev.preventDefault();
}
},
}, },
}; };
</script> </script>

View File

@@ -1,131 +0,0 @@
<template lang="pug">
.row
.col-2.standard-sidebar
.form-group
input.form-control(type="text", :placeholder="$t('search')")
.form
h3(v-once) {{ $t('filter') }}
.form-group
.form-check
label.form-check-label(v-once)
input.form-check-input(type="checkbox")
strong {{ $t('pets') }}
.form-check.nested-field
label.form-check-label(v-once)
input.form-check-input(type="checkbox")
span {{ $t('hatchingPotions') }}
.form-check.nested-field
label.form-check-label(v-once)
input.form-check-input(type="checkbox")
span {{ $t('quest') }}
.form-check.nested-field
label.form-check-label(v-once)
input.form-check-input(type="checkbox")
span {{ $t('special') }}
.form-group
.form-check
label.form-check-label(v-once)
input.form-check-input(type="checkbox")
strong {{ $t('mounts') }}
.form-check.nested-field
label.form-check-label(v-once)
input.form-check-input(type="checkbox")
span {{ $t('hatchingPotions') }}
.form-check.nested-field
label.form-check-label(v-once)
input.form-check-input(type="checkbox")
span {{ $t('quest') }}
.form-check.nested-field
label.form-check-label(v-once)
input.form-check-input(type="checkbox")
span {{ $t('special') }}
.col-10.standard-page
h4 Pets
.inventory-item-container(v-for="pet in dropPets")
.PixelPaw
.btn.btn-secondary.d-block(@click="open.dropPets = !open.dropPets") {{ open.dropPets ? 'Close' : 'Open' }}
h2 Magic Potions Pets
.inventory-item-container(v-for="pet in magicPets")
.PixelPaw
.btn.btn-secondary.d-block(@click="open.magicPets = !open.magicPets") {{ open.magicPets ? 'Close' : 'Open' }}
h2 Quest Pets
.inventory-item-container(v-for="pet in questPets")
.PixelPaw
.btn.btn-secondary.d-block(@click="open.questPets = !open.questPets") {{ open.questPets ? 'Close' : 'Open' }}
h2 Rare Pets
.inventory-item-container(v-for="pet in rarePets")
.PixelPaw
.btn.btn-secondary.d-block(@click="open.rarePets = !open.rarePets") {{ open.rarePets ? 'Close' : 'Open' }}
h2 Mounts
h2 Quest Mounts
h2 Rare Mounts
</template>
<style lang="scss">
.inventory-item-container {
padding: 20px;
border: 1px solid;
display: inline-block;
}
</style>
<script>
import { mapState } from 'client/libs/store';
import each from 'lodash/each';
// TODO Normalize special pets and mounts
// import Store from 'client/store';
// import deepFreeze from 'client/libs/deepFreeze';
// const specialMounts =
export default {
data () {
return {
open: {
dropPets: false,
magicPets: false,
questPets: false,
rarePets: false,
},
};
},
computed: {
...mapState(['content']),
dropPets () {
return this.listAnimals('pet', this.content.dropEggs, this.content.dropHatchingPotions, this.open.dropPets);
},
magicPets () {
return this.listAnimals('pet', this.content.dropEggs, this.content.premiumHatchingPotions, this.open.magicPets);
},
questPets () {
return this.listAnimals('pet', this.content.questEggs, this.content.dropHatchingPotions, this.open.questPets);
},
rarePets () {
return this.listAnimals('pet', this.content.dropEggs, this.content.dropHatchingPotions, this.open.rarePets);
},
},
methods: {
listAnimals (type, eggSource, potionSource, isOpen = false) {
let animals = [];
let iteration = 0;
each(eggSource, (egg) => {
if (iteration === 1 && !isOpen) return false;
iteration++;
each(potionSource, (potion) => {
let animalKey = `${egg.key}-${potion.key}`;
animals.push(this.content[`${type}Info`][animalKey].text());
});
});
return animals;
},
},
};
</script>

View File

@@ -0,0 +1,33 @@
<template lang="pug">
span.badge.badge-pill.badge-item.badge-count(
v-if="show",
) {{ count }}
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
.badge-count {
right: -9px;
color: $white;
background: $orange-100;
padding: 4.5px 6px;
min-width: 24px;
height: 24px;
box-shadow: 0 1px 1px 0 rgba($black, 0.12);
}
</style>
<script>
export default {
props: {
show: {
type: Boolean,
},
count: {
type: Number,
},
},
};
</script>

View File

@@ -0,0 +1,174 @@
<template lang="pug">
div.slider-root(
v-bind:class="{'scrollButtonsVisible': scrollButtonsVisible}",
)
div.slider-button-area.left-button(
v-if="scrollButtonsVisible",
@mousedown.left="lastPage"
)
a.slider-button
.svg-icon(v-html="icons.previous")
div.slider-button-area.right-button(
v-if="scrollButtonsVisible",
@mousedown.left="nextPage"
)
a.slider-button
.svg-icon(v-html="icons.next")
// 120 = width of the left/right buttons
div.sliding-content(v-resize="500", @resized="currentWidth = $event.width - 120")
.items.items-one-line
template(v-for="item in pages[currentPage]")
div.vertical-divider(v-if="item.ofNextPage")
slot(
name="item",
:item="item",
)
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
$buttonAreaWidth: 60;
.slider-root {
position: relative;
}
.slider-button {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #ffffff;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
position: absolute;
top: calc((100% - 40px) / 2);
.svg-icon {
color: #a5a1ac;
margin: auto 0;
position: absolute;
top: calc((100% - 12px) / 2);
width: 8px;
height: 16px;
right: 16px;
}
}
.scrollButtonsVisible {
.sliding-content {
overflow: hidden;
}
}
.slider-button-area {
width: $buttonAreaWidth+px;
height: 100%;
position: absolute;
z-index: 2;
&.left-button {
left: 0;
}
&.right-button {
right: 0;
}
}
.sliding-content .items {
padding-top: 10px;
margin-left: $buttonAreaWidth+ px;
& > div:last-of-type {
margin-right: $buttonAreaWidth + 20px;
}
}
.vertical-divider {
height: 92px;
width: 1px;
background: #34313a;
margin-bottom: 8px;
}
</style>
<script>
import previous from 'assets/svg/previous.svg';
import next from 'assets/svg/next.svg';
import ResizeDirective from 'client/directives/resize.directive';
import _chunk from 'lodash/chunk';
export default {
directives: {
resize: ResizeDirective,
},
data () {
return {
icons: Object.freeze({
previous,
next,
}),
currentWidth: 0,
currentPage: 0,
};
},
computed: {
pages () {
return _chunk(this.items, this.itemsPerPage() - 1).map((content, index, array) => {
let resultData = [...content];
if (array[index + 1]) {
resultData.push({
...array[index + 1][0],
ofNextPage: true,
});
}
return resultData;
});
},
},
methods: {
lastPage () {
if (this.currentPage > 0) {
this.currentPage--;
} else {
this.currentPage = this.pages.length - 1;
}
},
nextPage () {
if (this.currentPage < this.pages.length - 1) {
this.currentPage++;
} else {
this.currentPage = 0;
}
},
itemsPerPage () {
return Math.floor(this.currentWidth / (this.itemWidth + this.itemMargin));
},
},
props: {
scrollButtonsVisible: {
type: Boolean,
default: true,
},
items: {
type: Array,
},
itemWidth: {
type: Number,
},
itemMargin: {
type: Number,
},
},
};
</script>

View File

@@ -0,0 +1,48 @@
<template lang="pug">
b-popover(
:triggers="['hover']",
:placement="'top'",
)
span(slot="content")
h4.popover-content-title {{ item.text() }}
div.popover-content-text(v-html="item.notes()")
.item-wrapper
.item
countBadge(
:show="true",
:count="itemCount"
)
span.item-content(
:class="'Pet_Food_'+item.key",
v-drag.food="item.key"
)
</template>
<script>
import bPopover from 'bootstrap-vue/lib/components/popover';
import DragDropDirective from 'client/directives/dragdrop.directive';
import CountBadge from './countBadge';
export default {
components: {
bPopover,
CountBadge,
},
directives: {
drag: DragDropDirective,
},
props: {
item: {
type: Object,
},
itemCount: {
type: Number,
},
itemContentClass: {
type: String,
},
},
};
</script>

View File

@@ -0,0 +1,721 @@
<template lang="pug">
.row.stable
.standard-sidebar
div
b-popover(
:triggers="['hover']",
:placement="'right'"
)
span(slot="content")
h4.popover-content-title(v-once) {{ $t('mattBoch') }}
.popover-content-text(v-once) {{ $t('mattBochText1') }}
div.npc_matt
.form-group
input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
.form
h2(v-once) {{ $t('filter') }}
h3(v-once) {{ $t('pets') }}
.form-group
.form-check(
v-for="petGroup in petGroups",
v-if="viewOptions[petGroup.key].animalCount != 0",
:key="petGroup.key"
)
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", v-model="viewOptions[petGroup.key].selected")
span.custom-control-indicator
span.custom-control-description(v-once) {{ petGroup.label }}
h3(v-once) {{ $t('mounts') }}
.form-group
.form-check(
v-for="mountGroup in mountGroups",
v-if="viewOptions[mountGroup.key].animalCount != 0",
:key="mountGroup.key"
)
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", v-model="viewOptions[mountGroup.key].selected")
span.custom-control-indicator
span.custom-control-description(v-once) {{ mountGroup.label }}
div.form-group.clearfix
h3.float-left Hide Missing
toggle-switch.float-right.hideMissing(
:label="''",
:checked="hideMissing",
@change="updateHideMissing"
)
.standard-page(v-resize="500", @resized="availableContentWidth = $event.width - 48")
.clearfix
h1.float-left.mb-0.page-header(v-once) {{ $t('stable') }}
div.float-right
span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="$t(selectedSortBy)", right=true)
b-dropdown-item(
v-for="sort in sortByItems",
@click="selectedSortBy = sort",
:active="selectedSortBy === sort",
:key="sort"
) {{ $t(sort) }}
h2
| {{ $t('pets') }}
|
span.badge.badge-pill.badge-default {{countOwnedAnimals(petGroups[0], 'pet')}}
div(
v-for="petGroup in petGroups",
v-if="viewOptions[petGroup.key].selected",
:key="petGroup.key"
)
h4(v-if="viewOptions[petGroup.key].animalCount != 0") {{ petGroup.label }}
div.items
div(
v-for="pet in pets(petGroup, viewOptions[petGroup.key].open, hideMissing, selectedSortBy, searchTextThrottled, availableContentWidth)",
:key="pet.key",
v-drag.drop.food="pet.key",
@dragover="onDragOver($event, pet)",
@dropped="onDrop($event, pet)",
)
petItem(
:item="pet",
:itemContentClass="getPetItemClass(pet)",
:popoverPosition="'top'",
:progress="pet.progress",
:emptyItem="!pet.isOwned()",
:showPopover="pet.isOwned() || pet.isHatchable()",
@hatchPet="hatchPet",
)
span(slot="popoverContent")
div(v-if="pet.isOwned()")
h4.popover-content-title {{ pet.name }}
div.hatchablePopover(v-else-if="pet.isHatchable()")
h4.popover-content-title {{ pet.name }}
div.popover-content-text(v-html="$t('haveHatchablePet', { potion: pet.potionName, egg: pet.eggName })")
div.potionEggGroup
div.potionEggBackground
div(:class="'Pet_HatchingPotion_'+pet.potionKey")
div.potionEggBackground
div(:class="'Pet_Egg_'+pet.eggKey")
template(slot="itemBadge", scope="ctx")
starBadge(
:selected="ctx.item.key === currentPet",
:show="ctx.item.isOwned()",
@click="selectPet(ctx.item)",
)
.btn.btn-show-more(
@click="viewOptions[petGroup.key].open = !viewOptions[petGroup.key].open",
v-if="viewOptions[petGroup.key].animalCount != 0"
) {{ $t(viewOptions[petGroup.key].open ? 'showLessAnimals' : 'showAllAnimals', { color: petGroup.label, type: $t('pets')}) }}
h2
| {{ $t('mounts') }}
|
span.badge.badge-pill.badge-default {{countOwnedAnimals(mountGroups[0], 'mount')}}
div(
v-for="mountGroup in mountGroups",
v-if="viewOptions[mountGroup.key].selected",
:key="mountGroup.key"
)
h4(v-if="viewOptions[mountGroup.key].animalCount != 0") {{ mountGroup.label }}
div.items
item(
v-for="mount in mounts(mountGroup, viewOptions[mountGroup.key].open, hideMissing, selectedSortBy, searchTextThrottled, availableContentWidth)",
:item="mount",
:itemContentClass="mount.isOwned() ? ('Mount_Icon_' + mount.key) : 'PixelPaw greyedOut'",
:key="mount.key",
:popoverPosition="'top'"
)
span(slot="popoverContent")
h4.popover-content-title {{ mount.name }}
template(slot="itemBadge", scope="ctx")
starBadge(
:selected="ctx.item.key === currentMount",
:show="mount.isOwned()",
@click="selectMount(ctx.item)",
)
.btn.btn-show-more(
@click="viewOptions[mountGroup.key].open = !viewOptions[mountGroup.key].open",
v-if="viewOptions[mountGroup.key].animalCount != 0"
) {{ $t(viewOptions[mountGroup.key].open ? 'showLessAnimals' : 'showAllAnimals', { color: mountGroup.label, type: $t('mounts')}) }}
drawer(
:title="$t('quickInventory')",
:errorMessage="(!hasDrawerTabItems(selectedDrawerTab)) ? $t('noFoodAvailable') : null"
)
div(slot="drawer-header")
.drawer-tab-container
.drawer-tab.text-right
a.drawer-tab-text(
@click="selectedDrawerTab = 0",
:class="{'drawer-tab-text-active': selectedDrawerTab === 0}",
) {{ drawerTabs[0].label }}
.clearfix
.drawer-tab.float-left
a.drawer-tab-text(
@click="selectedDrawerTab = 1",
:class="{ 'drawer-tab-text-active': selectedDrawerTab === 1 }",
) {{ drawerTabs[1].label }}
b-popover(
:triggers="['hover']",
:placement="'top'"
)
span(slot="content")
.popover-content-text Test Popover
div.float-right What does my pet like to eat?
drawer-slider(
:items="drawerTabs[selectedDrawerTab].items",
:scrollButtonsVisible="hasDrawerTabItems(selectedDrawerTab)",
slot="drawer-slider",
:itemWidth=94,
:itemMargin=24,
)
template(slot="item", scope="ctx")
foodItem(
:item="ctx.item",
:itemCount="userItems.food[ctx.item.key]",
)
b-modal#welcome-modal(
:ok-only="true",
:ok-title="$t('gotIt')",
:visible="!hideDialog",
:hide-header="true"
)
div.content
div.npc_matt
h1.page-header(v-once) {{ $t('welcomeStable') }}
div.content-text(v-once) {{ $t('welcomeStableText') }}
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
.inventory-item-container {
padding: 20px;
border: 1px solid;
display: inline-block;
}
.stable .item .item-content.Pet {
position: absolute;
top: -28px;
}
.toggle-switch-container.hideMissing {
margin-top: 0;
}
.hatchablePopover {
width: 180px
}
.potionEggGroup {
margin: 0 auto;
}
.potionEggBackground {
display: inline-flex;
align-items: center;
width: 64px;
height: 64px;
border-radius: 2px;
background-color: #4e4a57;
&:first-child {
margin-right: 24px;
}
& div {
margin: 0 auto;
}
}
.GreyedOut {
opacity: 0.3;
}
.item.item-empty {
width: 94px;
height: 92px;
border-radius: 2px;
background-color: #edecee;
}
.npc_matt {
margin-bottom: 17px;
}
.stable {
.standard-page {
flex: 1;
}
.drawer-container {
// 3% padding + 252px sidebar width
left: calc(3% + 252px) !important;
}
}
.drawer-slider .items {
height: 114px;
}
div#welcome-modal {
display: flex;
justify-content: center;
flex-direction: column;
header, footer {
border: 0;
}
.npc_matt {
margin: 0 auto 21px auto;
}
.content {
text-align: center;
// the modal already has 15px padding
margin-left: 33px;
margin-right: 33px;
margin-top: 25px;
}
.content-text {
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: normal;
line-height: 1.43;
width: 400px;
}
.modal-footer {
justify-content: center;
}
}
.modal-backdrop.fade.show {
background-color: $purple-50;
opacity: 0.9;
}
</style>
<script>
import {mapState} from 'client/libs/store';
import bDropdown from 'bootstrap-vue/lib/components/dropdown';
import bDropdownItem from 'bootstrap-vue/lib/components/dropdown-item';
import bPopover from 'bootstrap-vue/lib/components/popover';
import bModal from 'bootstrap-vue/lib/components/modal';
import _each from 'lodash/each';
import _sortBy from 'lodash/sortBy';
import _take from 'lodash/take';
import _filter from 'lodash/filter';
import _drop from 'lodash/drop';
import _flatMap from 'lodash/flatMap';
import _throttle from 'lodash/throttle';
import Item from '../item';
import PetItem from './petItem';
import FoodItem from './foodItem';
import Drawer from 'client/components/inventory/drawer';
import toggleSwitch from 'client/components/ui/toggleSwitch';
import StarBadge from 'client/components/inventory/starBadge';
import CountBadge from './countBadge';
import DrawerSlider from './drawerSlider';
import ResizeDirective from 'client/directives/resize.directive';
import DragDropDirective from 'client/directives/dragdrop.directive';
// TODO Normalize special pets and mounts
// import Store from 'client/store';
// import deepFreeze from 'client/libs/deepFreeze';
// const specialMounts =
export default {
components: {
PetItem,
Item,
FoodItem,
Drawer,
bDropdown,
bDropdownItem,
bPopover,
bModal,
toggleSwitch,
StarBadge,
CountBadge,
DrawerSlider,
},
directives: {
resize: ResizeDirective,
drag: DragDropDirective,
},
data () {
return {
viewOptions: {},
hideMissing: false,
searchText: null,
searchTextThrottled: '',
// sort has the translation-keys as values
selectedSortBy: 'standard',
sortByItems: [
'standard',
'AZ',
'sortByColor',
'sortByHatchable',
],
selectedDrawerTab: 0,
availableContentWidth: 0,
};
},
watch: {
searchText: _throttle(function throttleSearch () {
let search = this.searchText.toLowerCase();
this.searchTextThrottled = search;
}, 250),
},
computed: {
...mapState({
content: 'content',
currentPet: 'user.data.items.currentPet',
currentMount: 'user.data.items.currentMount',
userItems: 'user.data.items',
hideDialog: 'user.data.flags.tutorial.common.mounts',
}),
petGroups () {
let petGroups = [
{
label: this.$t('filterByStandard'),
key: 'standardPets',
petSource: {
eggs: this.content.dropEggs,
potions: this.content.dropHatchingPotions,
},
},
{
label: this.$t('filterByMagicPotion'),
key: 'magicPets',
petSource: {
eggs: this.content.dropEggs,
potions: this.content.premiumHatchingPotions,
},
},
{
label: this.$t('filterByQuest'),
key: 'questPets',
petSource: {
eggs: this.content.questEggs,
potions: this.content.dropHatchingPotions,
},
},
{
label: this.$t('special'),
key: 'specialPets',
petSource: {
special: this.content.specialPets,
},
alwaysHideMissing: true,
},
];
petGroups.map((petGroup) => {
this.$set(this.viewOptions, petGroup.key, {
selected: true,
open: false,
animalCount: 0,
});
});
return petGroups;
},
mountGroups () {
let mountGroups = [
{
label: this.$t('filterByStandard'),
key: 'standardMounts',
petSource: {
eggs: this.content.dropEggs,
potions: this.content.dropHatchingPotions,
},
},
{
label: this.$t('filterByMagicPotion'),
key: 'magicMounts',
petSource: {
eggs: this.content.dropEggs,
potions: this.content.premiumHatchingPotions,
},
},
{
label: this.$t('filterByQuest'),
key: 'questMounts',
petSource: {
eggs: this.content.questEggs,
potions: this.content.dropHatchingPotions,
},
},
{
label: this.$t('special'),
key: 'specialMounts',
petSource: {
special: this.content.specialMounts,
},
alwaysHideMissing: true,
},
];
mountGroups.map((mountGroup) => {
this.$set(this.viewOptions, mountGroup.key, {
selected: true,
open: false,
animalCount: 0,
});
});
return mountGroups;
},
drawerTabs () {
return [
{
label: this.$t('food'),
items: _filter(this.content.food, f => {
return f.key !== 'Saddle' && this.userItems.food[f.key];
}),
},
{
label: this.$t('special'),
items: _filter(this.content.food, f => {
return f.key === 'Saddle' && this.userItems.food[f.key];
}),
},
];
},
},
methods: {
getAnimalList (animalGroup, type) {
let key = animalGroup.key;
this.cachedAnimalList = this.cachedAnimalList || {};
if (this.cachedAnimalList[key]) {
return this.cachedAnimalList[key];
}
let animals = [];
let userItems = this.userItems;
switch (key) {
case 'specialPets':
case 'specialMounts': {
_each(animalGroup.petSource.special, (value, specialKey) => {
let eggKey = specialKey.split('-')[0];
let potionKey = specialKey.split('-')[1];
animals.push({
key: specialKey,
eggKey,
potionKey,
pet: this.content[`${type}Info`][specialKey].text(),
isOwned () {
return [`${type}s`][this.key] > 0;
},
isHatchable () {
return false;
},
});
});
break;
}
default: {
_each(animalGroup.petSource.eggs, (egg) => {
_each(animalGroup.petSource.potions, (potion) => {
let animalKey = `${egg.key}-${potion.key}`;
animals.push({
key: animalKey,
eggKey: egg.key,
eggName: egg.text(),
potionKey: potion.key,
potionName: potion.text(),
name: this.content[`${type}Info`][animalKey].text(),
isOwned () {
return userItems[`${type}s`][animalKey] > 0;
},
isHatchable () {
return userItems.eggs[egg.key] > 0 && userItems.hatchingPotions[potion.key] > 0;
},
});
});
});
}
}
this.cachedAnimalList[key] = animals;
return animals;
},
listAnimals (animalGroup, type, isOpen, hideMissing, sort, searchText, availableSpace) {
let animals = this.getAnimalList(animalGroup, type);
let isPetList = type === 'pet';
let withProgress = isPetList && animalGroup.key !== 'specialPets';
// 1. Filter
if (hideMissing || animalGroup.alwaysHideMissing) {
animals = _filter(animals, (a) => {
return a.isOwned();
});
}
if (searchText && searchText !== '') {
animals = _filter(animals, (a) => {
return a.name.toLowerCase().indexOf(searchText) !== -1;
});
}
// 2. Sort
switch (sort) {
case 'AZ':
animals = _sortBy(animals, ['pet']);
break;
case 'sortByColor':
animals = _sortBy(animals, ['potionName']);
break;
case 'sortByHatchable': {
if (isPetList) {
let sortFunc = (i) => {
return i.isHatchable() ? 0 : 1;
};
animals = _sortBy(animals, [sortFunc]);
}
break;
}
}
let animalRows = [];
let itemsPerRow = Math.floor(availableSpace / (94 + 24));
let rowsToShow = isOpen ? Math.ceil(animals.length / itemsPerRow) : 1;
for (let i = 0; i < rowsToShow; i++) {
let skipped = _drop(animals, i * itemsPerRow);
let row = _take(skipped, itemsPerRow);
let rowWithProgressData = withProgress ? _flatMap(row, (a) => {
let progress = this.userItems[`${type}s`][a.key];
return {
...a,
progress,
};
}) : row;
animalRows.push(...rowWithProgressData);
}
this.viewOptions[animalGroup.key].animalCount = animals.length;
return animalRows;
},
countOwnedAnimals (animalGroup, type) {
let animals = this.getAnimalList(animalGroup, type);
let countAll = animals.length;
let countOwned = _filter(animals, (a) => {
return a.isOwned();
});
return `${countOwned.length}/${countAll}`;
},
pets (animalGroup, showAll, hideMissing, sortBy, searchText, availableSpace) {
return this.listAnimals(animalGroup, 'pet', showAll, hideMissing, sortBy, searchText, availableSpace);
},
mounts (animalGroup, showAll, hideMissing, sortBy, searchText, availableSpace) {
return this.listAnimals(animalGroup, 'mount', showAll, hideMissing, sortBy, searchText, availableSpace);
},
getPetItemClass (pet) {
if (pet.isOwned()) {
return `Pet Pet-${pet.key}`;
}
if (pet.isHatchable()) {
return 'PixelPaw';
}
return 'GreyedOut PixelPaw';
},
hasDrawerTabItems (index) {
return this.drawerTabs && this.drawerTabs[index].items.length !== 0;
},
// Actions
updateHideMissing (newVal) {
this.hideMissing = newVal;
},
selectPet (item) {
this.$store.dispatch('common:equip', {key: item.key, type: 'pet'});
},
selectMount (item) {
this.$store.dispatch('common:equip', {key: item.key, type: 'mount'});
},
hatchPet (pet) {
this.$store.dispatch('common:hatch', {egg: pet.eggKey, hatchingPotion: pet.potionKey});
},
onDragOver (ev, pet) {
if (this.userItems.mounts[pet.key]) {
ev.dropable = false;
}
},
onDrop (ev, pet) {
this.$store.dispatch('common:feed', {pet: pet.key, food: ev.draggingKey});
},
},
};
</script>

View File

@@ -0,0 +1,117 @@
<template lang="pug">
b-popover(
:triggers="[showPopover?'hover':'']",
:placement="popoverPosition",
)
span(slot="content")
slot(name="popoverContent", :item="item")
.item-wrapper
.item(
:class="{'item-empty': emptyItem}",
@mouseup="holdStop",
@mouseleave="holdStop",
@mousedown.left="holdStart"
)
slot(name="itemBadge", :item="item")
span.item-content(:class="itemContentClass")
span.pet-progress-background(v-if="progress > 0")
div.pet-progress-bar(v-bind:style="{width: 100 * progress/50 + '%' }")
span.pet-progress-background(v-if="holdProgress > 0")
div.pet-progress-bar.hold(v-bind:style="{width: 100 * holdProgress/5 + '%' }")
span.item-label(v-if="label") {{ label }}
</template>
<style lang="scss">
.pet-progress-background {
width: 62px;
height: 4px;
background-color: #e1e0e3;
position: absolute;
bottom: 4px;
left: calc((100% - 62px) / 2);
}
.pet-progress-bar {
height: 4px;
background-color: #24cc8f;
}
.pet-progress-bar.hold {
background-color: #54c3cc;
}
</style>
<script>
import bPopover from 'bootstrap-vue/lib/components/popover';
import {mapState} from 'client/libs/store';
export default {
components: {
bPopover,
},
props: {
item: {
type: Object,
},
itemContentClass: {
type: String,
},
label: {
type: String,
},
progress: {
type: Number,
default: -1,
},
emptyItem: {
type: Boolean,
default: false,
},
popoverPosition: {
type: String,
default: 'bottom',
},
showPopover: {
type: Boolean,
default: true,
},
},
data () {
return {
holdProgress: -1,
};
},
computed: {
...mapState({
ATTRIBUTES: 'constants.ATTRIBUTES',
}),
},
methods: {
holdStart () {
let pet = this.item;
if (pet.isOwned() || !pet.isHatchable()) {
return;
}
this.holdProgress = 1;
this.currentHoldingTimer = setInterval(() => {
if (this.holdProgress === 5) {
this.holdStop();
this.$emit('hatchPet', pet);
}
this.holdProgress += 1;
}, 1000);
},
holdStop () {
if (this.currentHoldingTimer) {
clearInterval(this.currentHoldingTimer);
this.holdProgress = -1;
}
},
},
};
</script>

View File

@@ -0,0 +1,8 @@
// https://stackoverflow.com/a/40720172/1298154
export const emit = (vnode, emitName, data) => {
let handlers = vnode.data && vnode.data.on || vnode.componentOptions && vnode.componentOptions.listeners;
if (handlers && handlers[emitName]) {
handlers[emitName].fns(data);
}
};

View File

@@ -0,0 +1,63 @@
import {emit} from './directive.common';
import _keys from 'lodash/keys';
import _without from 'lodash/without';
/**
* DRAG_GROUP is a static custom value
* KEY_OF_ITEM
*
* v-drag.DRAG_GROUP="KEY_OF_ITEM"
* v-drag.drop.DRAG_GROUP="KEY_OF_ITEM" @dropped="callback" @dragover="optional"
*/
const DROPPED_EVENT_NAME = 'dropped';
const DRAGOVER_EVENT_NAME = 'dragover';
export default {
bind (el, binding, vnode) {
el.isDropHandler = binding.modifiers.drop === true;
el.dragGroup = _without(_keys(binding.modifiers), 'drop')[0];
el.key = binding.value;
if (!el.isDropHandler) {
el.draggable = true;
el.handleDrag = (ev) => {
ev.dataTransfer.setData('KEY', binding.value);
};
el.addEventListener('dragstart', el.handleDrag);
} else {
el.handleDragOver = (ev) => {
let dragOverEventData = {
dropable: true,
draggingKey: ev.dataTransfer.getData('KEY'),
};
emit(vnode, DRAGOVER_EVENT_NAME, dragOverEventData);
if (dragOverEventData.dropable) {
ev.preventDefault();
}
};
el.handleDrop = (ev) => {
let dropEventData = {
draggingKey: ev.dataTransfer.getData('KEY'),
};
emit(vnode, DROPPED_EVENT_NAME, dropEventData);
};
el.addEventListener('dragover', el.handleDragOver);
el.addEventListener('drop', el.handleDrop);
}
},
unbind (el) {
if (!el.isDropHandler) {
el.removeEventListener('drag', el.handleDrag);
} else {
el.removeEventListener('dragover', el.handleDragOver);
el.removeEventListener('drop', el.handleDrop);
}
},
};

View File

@@ -0,0 +1,31 @@
import Vue from 'vue';
import _throttle from 'lodash/throttle';
import { emit } from './directive.common';
/**
* v-resize="throttleTimeout", @resized="callback()"
*/
const EVENT_NAME = 'resized';
export default {
bind (el, binding, vnode) {
el.handleWindowResize = _throttle(() => {
emit(vnode, EVENT_NAME, {
width: el.clientWidth,
height: el.clientHeight,
});
}, binding.value);
window.addEventListener('resize', el.handleWindowResize);
// send the first width
Vue.nextTick(el.handleWindowResize);
},
unbind (el) {
window.removeEventListener('resize', el.handleWindowResize);
},
};

View File

@@ -19,7 +19,7 @@ import UserTasks from './components/userTasks';
const InventoryContainer = () => import(/* webpackChunkName: "inventory" */'./components/inventory/index'); const InventoryContainer = () => import(/* webpackChunkName: "inventory" */'./components/inventory/index');
const ItemsPage = () => import(/* webpackChunkName: "inventory" */'./components/inventory/items/index'); const ItemsPage = () => import(/* webpackChunkName: "inventory" */'./components/inventory/items/index');
const EquipmentPage = () => import(/* webpackChunkName: "inventory" */'./components/inventory/equipment/index'); const EquipmentPage = () => import(/* webpackChunkName: "inventory" */'./components/inventory/equipment/index');
const StablePage = () => import(/* webpackChunkName: "inventory" */'./components/inventory/stable'); const StablePage = () => import(/* webpackChunkName: "inventory" */'./components/inventory/stable/index');
// Social // Social
const InboxPage = () => import(/* webpackChunkName: "inbox" */ './components/social/inbox/index'); const InboxPage = () => import(/* webpackChunkName: "inbox" */ './components/social/inbox/index');

View File

@@ -1,5 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import equipOp from 'common/script/ops/equip'; import equipOp from 'common/script/ops/equip';
import hatchOp from 'common/script/ops/hatch';
import feedOp from 'common/script/ops/feed';
export function equip (store, params) { export function equip (store, params) {
const user = store.state.user.data; const user = store.state.user.data;
@@ -9,4 +11,24 @@ export function equip (store, params) {
// TODO // TODO
// .then((res) => console.log('equip', res)) // .then((res) => console.log('equip', res))
// .catch((err) => console.error('equip', err)); // .catch((err) => console.error('equip', err));
} }
export function hatch (store, params) {
const user = store.state.user.data;
hatchOp(user, {params});
axios
.post(`/api/v3/user/hatch/${params.egg}/${params.hatchingPotion}`);
// TODO
// .then((res) => console.log('equip', res))
// .catch((err) => console.error('equip', err));
}
export function feed (store, params) {
const user = store.state.user.data;
feedOp(user, {params});
axios
.post(`/api/v3/user/feed/${params.pet}/${params.food}`);
// TODO
// .then((res) => console.log('equip', res))
// .catch((err) => console.error('equip', err));
}

View File

@@ -1,5 +1,4 @@
{ {
"viewParty": "View Party",
"shops": "Shops", "shops": "Shops",
"faq": "FAQ", "faq": "FAQ",
"costumePopoverText": "Select \"Use Costume\" to equip items to your avatar without affecting the stats from your Battle Gear! This means that you can dress up your avatar in whatever outfit you like while still having your best Battle Gear equipped.", "costumePopoverText": "Select \"Use Costume\" to equip items to your avatar without affecting the stats from your Battle Gear! This means that you can dress up your avatar in whatever outfit you like while still having your best Battle Gear equipped.",
@@ -25,6 +24,14 @@
"groupBy2": "Group By", "groupBy2": "Group By",
"quantity": "Quantity", "quantity": "Quantity",
"AZ": "A-Z", "AZ": "A-Z",
"costumeDisabled": "You have disabled your costume.",
"filterByStandard": "Standard",
"filterByMagicPotion": "Magin Potion",
"filterByQuest": "Quest",
"standard": "Standard",
"sortByColor": "Color",
"sortByHatchable": "Hatchable",
"haveHatchablePet": "You have a <%= potion %> and <%= egg %> to hatch this pet! <b>Click and hold</b> the paw print to hatch.",
"editAvatar": "Edit Avatar", "editAvatar": "Edit Avatar",
"sort": "Sort", "sort": "Sort",
"memberCount": "Member Count", "memberCount": "Member Count",
@@ -69,5 +76,12 @@
"sendMessage": "Send Message", "sendMessage": "Send Message",
"removeManager2": "Remove Manager", "removeManager2": "Remove Manager",
"promoteToLeader": "Promote to Leader", "promoteToLeader": "Promote to Leader",
"inviteFriendsParty": "Inviting friends to your party will grant you an exclusive <br/> Quest Scroll to battle the Basi-List together!" "inviteFriendsParty": "Inviting friends to your party will grant you an exclusive <br/> Quest Scroll to battle the Basi-List together!",
"showAllAnimals": "Show All <%= color %> <%= type %>",
"showLessAnimals": "Show Less <%= color %> <%= type %>",
"quickInventory": "Quick Inventory",
"noFoodAvailable": "You don't have any food.",
"gotIt": "Got it!",
"welcomeStable": "Welcome to the Stable!",
"welcomeStableText": "I'm Matt, the Beast Master. Starting at level 3, you can hatch Pets from Eggs by using Potions you find! When you hatch a Pet from your Inventory, it will appear here! Click a Pet's to add it to your avatar. Feed them with the Food you find in your Inventory after level 3, and they'll grow into hardy Mounts."
} }