mirror of
https://github.com/sudoxnym/habitica.git
synced 2026-04-14 19:56:23 +00:00
WIP(shops): first full test version
This commit is contained in:
parent
7a50b2d2ff
commit
3bf323032c
9 changed files with 258 additions and 474 deletions
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
id="body"
|
||||
class="section customize-section"
|
||||
class="customize-section d-flex flex-column justify-content-between"
|
||||
>
|
||||
<sub-menu
|
||||
class="text-center"
|
||||
|
|
@ -17,17 +17,11 @@
|
|||
</div>
|
||||
<div v-if="activeSubPage === 'shirt'">
|
||||
<customize-options
|
||||
:items="freeShirts"
|
||||
:items="userShirts"
|
||||
:current-value="user.preferences.shirt"
|
||||
/>
|
||||
<customize-options
|
||||
v-if="editing"
|
||||
:items="specialShirts"
|
||||
:current-value="user.preferences.shirt"
|
||||
:full-set="!userOwnsSet('shirt', specialShirtKeys)"
|
||||
@unlock="unlock(`shirt.${specialShirtKeys.join(',shirt.')}`)"
|
||||
/>
|
||||
</div>
|
||||
<customize-banner />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -36,16 +30,14 @@ import appearance from '@/../../common/script/content/appearance';
|
|||
import { subPageMixin } from '../../mixins/subPage';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||
import subMenu from './sub-menu';
|
||||
import customizeBanner from './customize-banner.vue';
|
||||
import customizeOptions from './customize-options';
|
||||
import gem from '@/assets/svg/gem.svg';
|
||||
|
||||
const freeShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price === 0);
|
||||
const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
|
||||
import subMenu from './sub-menu';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
subMenu,
|
||||
customizeBanner,
|
||||
customizeOptions,
|
||||
},
|
||||
mixins: [
|
||||
|
|
@ -58,10 +50,6 @@ export default {
|
|||
],
|
||||
data () {
|
||||
return {
|
||||
specialShirtKeys,
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
id: 'size',
|
||||
|
|
@ -78,25 +66,19 @@ export default {
|
|||
sizes () {
|
||||
return ['slim', 'broad'].map(s => this.mapKeysToFreeOption(s, 'size'));
|
||||
},
|
||||
freeShirts () {
|
||||
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
||||
},
|
||||
specialShirts () {
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.specialShirtKeys;
|
||||
const options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
|
||||
return options;
|
||||
userShirts () {
|
||||
const freeShirts = Object.keys(appearance.shirt)
|
||||
.filter(k => appearance.shirt[k].price === 0)
|
||||
.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
||||
const ownedShirts = Object.keys(this.user.purchased.shirt)
|
||||
.filter(k => this.user.purchased.shirt[k])
|
||||
.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
||||
|
||||
return [...freeShirts, ...ownedShirts];
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.changeSubPage('size');
|
||||
},
|
||||
methods: {
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<div class="bottom-banner">
|
||||
<div class="d-flex justify-content-center align-items-center mt-3">
|
||||
<span
|
||||
class="svg svg-icon sparkles"
|
||||
v-html="icons.sparkles"
|
||||
></span>
|
||||
<strong
|
||||
v-once
|
||||
class="mx-2"
|
||||
> {{ $t('lookingForMore') }}
|
||||
</strong>
|
||||
<span
|
||||
v-once
|
||||
class="svg svg-icon sparkles mirror"
|
||||
v-html="icons.sparkles"
|
||||
></span>
|
||||
</div>
|
||||
<div
|
||||
class="check-link"
|
||||
>
|
||||
<span>Check out the </span>
|
||||
<a href='/shops/customizations'>Customizations Shop</a>
|
||||
<span> for even more ways to customize your avatar!</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.bottom-banner {
|
||||
background: linear-gradient(114.26deg, $purple-300 0%, $purple-200 100%);
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
color: $white;
|
||||
height: 80px;
|
||||
line-height: 24px;
|
||||
|
||||
.check-link, a {
|
||||
color: $purple-600;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.sparkles {
|
||||
width: 32px;
|
||||
|
||||
&.mirror {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import sparkles from '@/assets/svg/sparkles-left.svg';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
sparkles,
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
class="customize-options"
|
||||
:class="{'background-set': fullSet}"
|
||||
>
|
||||
<div
|
||||
v-for="option in items"
|
||||
|
|
@ -28,38 +27,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="option.gemLocked"
|
||||
class="gem-lock"
|
||||
>
|
||||
<div
|
||||
class="svg-icon gem"
|
||||
v-html="icons.gem"
|
||||
></div>
|
||||
<span>{{ option.gem }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="option.goldLocked"
|
||||
class="gold-lock"
|
||||
>
|
||||
<div
|
||||
class="svg-icon gold"
|
||||
v-html="icons.gold"
|
||||
></div>
|
||||
<span>{{ option.gold }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="fullSet"
|
||||
class="purchase-set"
|
||||
@click="unlock()"
|
||||
>
|
||||
<span class="label">{{ $t('purchaseAll') }}</span>
|
||||
<div
|
||||
class="svg-icon gem"
|
||||
v-html="icons.gem"
|
||||
></div>
|
||||
<span class="price">5</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -73,7 +40,7 @@ export default {
|
|||
mixins: [
|
||||
avatarEditorUtilities,
|
||||
],
|
||||
props: ['items', 'currentValue', 'fullSet'],
|
||||
props: ['items', 'currentValue'],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<div
|
||||
id="extra"
|
||||
class="section container customize-section"
|
||||
class="customize-section d-flex flex-column"
|
||||
:class="{ 'justify-content-between': !showEmptySection}"
|
||||
>
|
||||
<sub-menu
|
||||
class="text-center"
|
||||
|
|
@ -20,9 +21,8 @@
|
|||
id="animal-ears"
|
||||
>
|
||||
<customize-options
|
||||
v-if="animalItems('back').length > 1"
|
||||
:items="animalItems('headAccessory')"
|
||||
:full-set="!animalItemsOwned('headAccessory')"
|
||||
@unlock="unlock(animalItemsUnlockString('headAccessory'))"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -30,9 +30,8 @@
|
|||
id="animal-tails"
|
||||
>
|
||||
<customize-options
|
||||
v-if="animalItems('back').length > 1"
|
||||
:items="animalItems('back')"
|
||||
:full-set="!animalItemsOwned('back')"
|
||||
@unlock="unlock(animalItemsUnlockString('back'))"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -53,6 +52,22 @@
|
|||
>
|
||||
<customize-options :items="flowers" />
|
||||
</div>
|
||||
<div
|
||||
v-if="showEmptySection"
|
||||
class="my-5"
|
||||
>
|
||||
<h3
|
||||
v-once
|
||||
> {{ $t('noItemsOwned') }} </h3>
|
||||
<p
|
||||
v-once
|
||||
v-html="$t('visitCustomizationsShop')"
|
||||
class="w-50 mx-auto"
|
||||
></p>
|
||||
</div>
|
||||
<customize-banner
|
||||
v-else
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -61,17 +76,18 @@ import appearance from '@/../../common/script/content/appearance';
|
|||
import { subPageMixin } from '../../mixins/subPage';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||
import subMenu from './sub-menu';
|
||||
import customizeBanner from './customize-banner';
|
||||
import customizeOptions from './customize-options';
|
||||
import gem from '@/assets/svg/gem.svg';
|
||||
import subMenu from './sub-menu';
|
||||
|
||||
const freeShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price === 0);
|
||||
const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
subMenu,
|
||||
customizeBanner,
|
||||
customizeOptions,
|
||||
subMenu,
|
||||
},
|
||||
mixins: [
|
||||
subPageMixin,
|
||||
|
|
@ -89,9 +105,6 @@ export default {
|
|||
},
|
||||
chairKeys: ['none', 'black', 'blue', 'green', 'pink', 'red', 'yellow', 'handleless_black', 'handleless_blue', 'handleless_green', 'handleless_pink', 'handleless_red', 'handleless_yellow'],
|
||||
specialShirtKeys,
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
}),
|
||||
items: [
|
||||
{
|
||||
id: 'size',
|
||||
|
|
@ -178,7 +191,7 @@ export default {
|
|||
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
||||
},
|
||||
specialShirts () {
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.specialShirtKeys;
|
||||
const options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
|
||||
return options;
|
||||
|
|
@ -228,6 +241,16 @@ export default {
|
|||
});
|
||||
return options;
|
||||
},
|
||||
showEmptySection () {
|
||||
switch (this.activeSubPage) {
|
||||
case 'ears':
|
||||
return this.editing && this.animalItems('headAccessory').length === 1;
|
||||
case 'tails':
|
||||
return this.editing && this.animalItems('back').length === 1;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.changeSubPage(this.extraSubMenuItems[0].id);
|
||||
|
|
@ -236,7 +259,7 @@ export default {
|
|||
animalItems (category) {
|
||||
// @TODO: For some resonse when I use $set on the
|
||||
// user purchases object, this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.animalItemKeys[category];
|
||||
|
||||
const noneOption = this.createGearItem(0, category, 'base', category);
|
||||
|
|
@ -248,36 +271,22 @@ export default {
|
|||
for (const key of keys) {
|
||||
const newKey = `${category}_special_${key}`;
|
||||
const userPurchased = this.user.items.gear.owned[newKey];
|
||||
|
||||
const option = {};
|
||||
option.key = key;
|
||||
option.active = this.user.preferences.costume
|
||||
? this.user.items.gear.costume[category] === newKey
|
||||
: this.user.items.gear.equipped[category] === newKey;
|
||||
option.class = `headAccessory_special_${option.key} ${category}`;
|
||||
if (category === 'back') {
|
||||
option.class = `icon_back_special_${option.key} back`;
|
||||
}
|
||||
option.gemLocked = userPurchased === undefined;
|
||||
option.goldLocked = userPurchased === false;
|
||||
if (option.goldLocked) {
|
||||
option.gold = 20;
|
||||
}
|
||||
if (option.gemLocked) {
|
||||
option.gem = 2;
|
||||
}
|
||||
option.locked = option.gemLocked || option.goldLocked;
|
||||
option.click = () => {
|
||||
if (option.gemLocked) {
|
||||
return this.unlock(`items.gear.owned.${newKey}`);
|
||||
} if (option.goldLocked) {
|
||||
return this.buy(newKey);
|
||||
if (userPurchased) {
|
||||
const option = {};
|
||||
option.key = key;
|
||||
option.active = this.user.preferences.costume
|
||||
? this.user.items.gear.costume[category] === newKey
|
||||
: this.user.items.gear.equipped[category] === newKey;
|
||||
option.class = `headAccessory_special_${option.key} ${category}`;
|
||||
if (category === 'back') {
|
||||
option.class = `icon_back_special_${option.key} back`;
|
||||
}
|
||||
const type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||
return this.equip(newKey, type);
|
||||
};
|
||||
|
||||
options.push(option);
|
||||
option.click = () => {
|
||||
const type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||
return this.equip(newKey, type);
|
||||
};
|
||||
options.push(option);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
|
|
@ -287,17 +296,6 @@ export default {
|
|||
|
||||
return keys.join(',');
|
||||
},
|
||||
animalItemsOwned (category) {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
||||
// this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
|
||||
let own = true;
|
||||
this.animalItemKeys[category].forEach(key => {
|
||||
if (this.user.items.gear.owned[`${category}_special_${key}`] === undefined) own = false;
|
||||
});
|
||||
return own;
|
||||
},
|
||||
createGearItem (key, gearType, subGearType, additionalClass) {
|
||||
const newKey = `${gearType}_${subGearType ? `${subGearType}_` : ''}${key}`;
|
||||
const option = {};
|
||||
|
|
@ -339,7 +337,3 @@ export default {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<div
|
||||
id="hair"
|
||||
class="section customize-section"
|
||||
class="customize-section d-flex flex-column"
|
||||
:class="{ 'justify-content-between': !showEmptySection}"
|
||||
>
|
||||
<sub-menu
|
||||
class="text-center"
|
||||
|
|
@ -14,37 +15,9 @@
|
|||
id="hair-color"
|
||||
>
|
||||
<customize-options
|
||||
:items="freeHairColors"
|
||||
:items="userHairColors"
|
||||
:current-value="user.preferences.hair.color"
|
||||
/>
|
||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
||||
<div
|
||||
v-for="set in seasonalHairColors"
|
||||
v-if="editing && set.key !== 'undefined'"
|
||||
:key="set.key"
|
||||
>
|
||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||
<customize-options
|
||||
:items="set.options"
|
||||
:current-value="user.preferences.hair.color"
|
||||
:full-set="!hideSet(set.key) && !userOwnsSet('hair', set.keys, 'color')"
|
||||
@unlock="unlock(`hair.color.${set.keys.join(',hair.color.')}`)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="activeSubPage === 'style'"
|
||||
id="style"
|
||||
>
|
||||
<!-- eslint-disable vue/require-v-for-key NO KEY AVAILABLE HERE -->
|
||||
<div v-for="set in styleSets">
|
||||
<customize-options
|
||||
:items="set.options"
|
||||
:full-set="set.fullSet"
|
||||
@unlock="set.unlock()"
|
||||
/>
|
||||
</div>
|
||||
<!-- eslint-enable vue/require-v-for-key -->
|
||||
</div>
|
||||
<div
|
||||
v-if="activeSubPage === 'bangs'"
|
||||
|
|
@ -55,44 +28,62 @@
|
|||
:current-value="user.preferences.hair.bangs"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="activeSubPage === 'style'"
|
||||
id="style"
|
||||
>
|
||||
<customize-options
|
||||
:items="userHairStyles"
|
||||
:current-value="user.preferences.hair.base"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="activeSubPage === 'facialhair'"
|
||||
id="facialhair"
|
||||
>
|
||||
<customize-options
|
||||
v-if="editing"
|
||||
:items="mustacheList"
|
||||
v-if="editing && userMustaches.length > 1"
|
||||
:items="userMustaches"
|
||||
/>
|
||||
<!-- eslint-disable max-len -->
|
||||
<customize-options
|
||||
v-if="editing"
|
||||
:items="beardList"
|
||||
:full-set="isPurchaseAllNeeded('hair', ['baseHair5', 'baseHair6'], ['mustache', 'beard'])"
|
||||
@unlock="unlock(`hair.mustache.${baseHair5Keys.join(',hair.mustache.')},hair.beard.${baseHair6Keys.join(',hair.beard.')}`)"
|
||||
v-if="editing && userBeards.length > 1"
|
||||
:items="userBeards"
|
||||
/>
|
||||
<!-- eslint-enable max-len -->
|
||||
<div
|
||||
class="my-5"
|
||||
v-if="showEmptySection"
|
||||
>
|
||||
<h3
|
||||
v-once
|
||||
> {{ $t('noItemsOwned') }} </h3>
|
||||
<p
|
||||
v-once
|
||||
v-html="$t('visitCustomizationsShop')"
|
||||
class="w-50 mx-auto"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
<customize-banner
|
||||
v-if="!showEmptySection"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import appearance from '@/../../common/script/content/appearance';
|
||||
import appearanceSets from '@/../../common/script/content/appearance/sets';
|
||||
import { subPageMixin } from '../../mixins/subPage';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||
import subMenu from './sub-menu';
|
||||
import customizeBanner from './customize-banner';
|
||||
import customizeOptions from './customize-options';
|
||||
import gem from '@/assets/svg/gem.svg';
|
||||
|
||||
const hairColorBySet = groupBy(appearance.hair.color, 'set.key');
|
||||
const freeHairColorKeys = hairColorBySet[undefined].map(s => s.key);
|
||||
import subMenu from './sub-menu';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
subMenu,
|
||||
customizeBanner,
|
||||
customizeOptions,
|
||||
subMenu,
|
||||
},
|
||||
mixins: [
|
||||
subPageMixin,
|
||||
|
|
@ -102,20 +93,6 @@ export default {
|
|||
props: [
|
||||
'editing',
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
freeHairColorKeys,
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
}),
|
||||
baseHair1: [1, 3],
|
||||
baseHair2Keys: [2, 4, 5, 6, 7, 8],
|
||||
baseHair3Keys: [9, 10, 11, 12, 13, 14],
|
||||
baseHair4Keys: [15, 16, 17, 18, 19, 20],
|
||||
baseHair5Keys: [1, 2],
|
||||
baseHair6Keys: [1, 2, 3],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hairSubMenuItems () {
|
||||
const items = [
|
||||
|
|
@ -142,91 +119,46 @@ export default {
|
|||
|
||||
return items;
|
||||
},
|
||||
freeHairColors () {
|
||||
return freeHairColorKeys.map(s => this.mapKeysToFreeOption(s, 'hair', 'color'));
|
||||
userHairColors () {
|
||||
const freeHairColors = groupBy(appearance.hair.color, 'set.key')[undefined]
|
||||
.map(s => s.key).map(s => this.mapKeysToFreeOption(s, 'hair', 'color'));
|
||||
const ownedHairColors = Object.keys(this.user.purchased.hair.color || {})
|
||||
.filter(k => this.user.purchased.hair.color[k])
|
||||
.map(h => this.mapKeysToFreeOption(h, 'hair', 'color'));
|
||||
return [...freeHairColors, ...ownedHairColors];
|
||||
},
|
||||
seasonalHairColors () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
||||
// this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
userHairStyles () {
|
||||
const emptyHairStyle = {
|
||||
...this.mapKeysToFreeOption(0, 'hair', 'base'),
|
||||
none: true,
|
||||
};
|
||||
const freeHairStyles = [1, 3].map(s => this.mapKeysToFreeOption(s, 'hair', 'base'));
|
||||
const ownedHairStyles = Object.keys(this.user.purchased.hair.base || {})
|
||||
.filter(k => this.user.purchased.hair.base[k])
|
||||
.map(h => this.mapKeysToFreeOption(h, 'hair', 'base'));
|
||||
return [emptyHairStyle, ...freeHairStyles, ...ownedHairStyles];
|
||||
},
|
||||
userMustaches () {
|
||||
const emptyMustache = {
|
||||
...this.mapKeysToFreeOption(0, 'hair', 'mustache'),
|
||||
none: true,
|
||||
};
|
||||
const ownedMustaches = Object.keys(this.user.purchased.hair.mustache || {})
|
||||
.filter(k => this.user.purchased.hair.mustache[k])
|
||||
.map(h => this.mapKeysToFreeOption(h, 'hair', 'mustache'));
|
||||
|
||||
const seasonalHairColors = [];
|
||||
for (const key of Object.keys(hairColorBySet)) {
|
||||
const set = hairColorBySet[key];
|
||||
return [emptyMustache, ...ownedMustaches];
|
||||
},
|
||||
userBeards () {
|
||||
const emptyBeard = {
|
||||
...this.mapKeysToFreeOption(0, 'hair', 'beard'),
|
||||
none: true,
|
||||
};
|
||||
const ownedBeards = Object.keys(this.user.purchased.hair.beard || {})
|
||||
.filter(k => this.user.purchased.hair.beard[k])
|
||||
.map(h => this.mapKeysToFreeOption(h, 'hair', 'beard'));
|
||||
|
||||
const keys = set.map(item => item.key);
|
||||
|
||||
const options = keys.map(optionKey => {
|
||||
const option = this.mapKeysToOption(optionKey, 'hair', 'color', key);
|
||||
return option;
|
||||
});
|
||||
|
||||
let text = this.$t(key);
|
||||
if (appearanceSets[key] && appearanceSets[key].text) {
|
||||
text = appearanceSets[key].text();
|
||||
}
|
||||
|
||||
const compiledSet = {
|
||||
key,
|
||||
options,
|
||||
keys,
|
||||
text,
|
||||
};
|
||||
seasonalHairColors.push(compiledSet);
|
||||
}
|
||||
|
||||
return seasonalHairColors;
|
||||
},
|
||||
premiumHairColors () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
||||
// this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.premiumHairColorKeys;
|
||||
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'color'));
|
||||
return options;
|
||||
},
|
||||
baseHair2 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
||||
// this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.baseHair2Keys;
|
||||
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'base'));
|
||||
return options;
|
||||
},
|
||||
baseHair3 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
||||
// this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.baseHair3Keys;
|
||||
const options = keys.map(key => {
|
||||
const option = this.mapKeysToOption(key, 'hair', 'base');
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
},
|
||||
baseHair4 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
||||
// this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.baseHair4Keys;
|
||||
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'base'));
|
||||
return options;
|
||||
},
|
||||
baseHair5 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
||||
// this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.baseHair5Keys;
|
||||
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'mustache'));
|
||||
return options;
|
||||
},
|
||||
baseHair6 () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
||||
// this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.baseHair6Keys;
|
||||
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'beard'));
|
||||
return options;
|
||||
return [emptyBeard, ...ownedBeards];
|
||||
},
|
||||
hairBangs () {
|
||||
const none = this.mapKeysToFreeOption(0, 'hair', 'bangs');
|
||||
|
|
@ -236,136 +168,13 @@ export default {
|
|||
|
||||
return [none, ...options];
|
||||
},
|
||||
mustacheList () {
|
||||
const noneOption = this.mapKeysToFreeOption(0, 'hair', 'mustache');
|
||||
noneOption.none = true;
|
||||
|
||||
return [noneOption, ...this.baseHair5];
|
||||
},
|
||||
beardList () {
|
||||
const noneOption = this.mapKeysToFreeOption(0, 'hair', 'beard');
|
||||
noneOption.none = true;
|
||||
|
||||
return [noneOption, ...this.baseHair6];
|
||||
},
|
||||
styleSets () {
|
||||
const sets = [];
|
||||
|
||||
const emptyHairBase = {
|
||||
...this.mapKeysToFreeOption(0, 'hair', 'base'),
|
||||
none: true,
|
||||
};
|
||||
|
||||
sets.push({
|
||||
options: [
|
||||
emptyHairBase,
|
||||
...this.baseHair1.map(key => this.mapKeysToFreeOption(key, 'hair', 'base')),
|
||||
],
|
||||
});
|
||||
|
||||
if (this.editing) {
|
||||
sets.push({
|
||||
fullSet: !this.userOwnsSet('hair', this.baseHair3Keys, 'base'),
|
||||
unlock: () => this.unlock(`hair.base.${this.baseHair3Keys.join(',hair.base.')}`),
|
||||
options: [
|
||||
...this.baseHair3,
|
||||
],
|
||||
});
|
||||
|
||||
sets.push({
|
||||
fullSet: !this.userOwnsSet('hair', this.baseHair4Keys, 'base'),
|
||||
unlock: () => this.unlock(`hair.base.${this.baseHair4Keys.join(',hair.base.')}`),
|
||||
options: [
|
||||
...this.baseHair4,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editing) {
|
||||
sets.push({
|
||||
fullSet: !this.userOwnsSet('hair', this.baseHair2Keys, 'base'),
|
||||
unlock: () => this.unlock(`hair.base.${this.baseHair2Keys.join(',hair.base.')}`),
|
||||
options: [
|
||||
...this.baseHair2,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return sets;
|
||||
showEmptySection () {
|
||||
return this.editing && this.activeSubPage === 'facialhair'
|
||||
&& this.userMustaches.length === 1 && this.userBeards.length === 1;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.changeSubPage('color');
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Allows you to find out whether you need the "Purchase All" button or not.
|
||||
* If there are more than 2 unpurchased items, returns true, otherwise returns false.
|
||||
* @param {string} category - The selected category.
|
||||
* @param {string[]} keySets - The items keySets.
|
||||
* @param {string[]} [types] - The items types (subcategories). Optional.
|
||||
* @returns {boolean} - Determines whether the "Purchase All" button
|
||||
* is needed (true) or not (false).
|
||||
*/
|
||||
isPurchaseAllNeeded (category, keySets, types) {
|
||||
const purchasedItemsLengths = [];
|
||||
// If item types are specified, count them
|
||||
if (types && types.length > 0) {
|
||||
// Types can be undefined, so we must check them.
|
||||
types.forEach(type => {
|
||||
if (this.user.purchased[category][type]) {
|
||||
purchasedItemsLengths
|
||||
.push(Object.keys(this.user.purchased[category][type]).length);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let purchasedItemsCounter = 0;
|
||||
|
||||
// If types are not specified, recursively
|
||||
// search for purchased items in the category
|
||||
const findPurchasedItems = item => {
|
||||
if (typeof item === 'object') {
|
||||
Object.values(item)
|
||||
.forEach(innerItem => {
|
||||
if (typeof innerItem === 'boolean' && innerItem === true) {
|
||||
purchasedItemsCounter += 1;
|
||||
}
|
||||
return findPurchasedItems(innerItem);
|
||||
});
|
||||
}
|
||||
return purchasedItemsCounter;
|
||||
};
|
||||
|
||||
findPurchasedItems(this.user.purchased[category]);
|
||||
if (purchasedItemsCounter > 0) {
|
||||
purchasedItemsLengths.push(purchasedItemsCounter);
|
||||
}
|
||||
}
|
||||
|
||||
// We don't need to count the key sets (below)
|
||||
// if there are no purchased items at all.
|
||||
if (purchasedItemsLengths.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const allItemsLengths = [];
|
||||
// Key sets must be specify correctly.
|
||||
keySets.forEach(keySet => {
|
||||
allItemsLengths.push(Object.keys(this[keySet]).length);
|
||||
});
|
||||
|
||||
// Simply sum all the length values and
|
||||
// write them into variables for the convenience.
|
||||
const allItems = allItemsLengths.reduce((acc, val) => acc + val);
|
||||
const purchasedItems = purchasedItemsLengths.reduce((acc, val) => acc + val);
|
||||
|
||||
const unpurchasedItems = allItems - purchasedItems;
|
||||
return unpurchasedItems > 2;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
id="skin"
|
||||
class="section customize-section"
|
||||
class="customize-section d-flex flex-column justify-content-between"
|
||||
>
|
||||
<sub-menu
|
||||
class="text-center"
|
||||
|
|
@ -10,47 +10,27 @@
|
|||
@changeSubPage="changeSubPage($event)"
|
||||
/>
|
||||
<customize-options
|
||||
:items="freeSkins"
|
||||
:items="userSkins"
|
||||
:current-value="user.preferences.skin"
|
||||
/>
|
||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
||||
<div
|
||||
v-for="set in seasonalSkins"
|
||||
v-if="editing && set.key !== 'undefined'"
|
||||
:key="set.key"
|
||||
>
|
||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||
<customize-options
|
||||
:items="set.options"
|
||||
:current-value="user.preferences.skin"
|
||||
:full-set="!hideSet(set.key) && !userOwnsSet('skin', set.keys)"
|
||||
@unlock="unlock(`skin.${set.keys.join(',skin.')}`)"
|
||||
/>
|
||||
</div>
|
||||
<customize-banner />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import appearance from '@/../../common/script/content/appearance';
|
||||
import appearanceSets from '@/../../common/script/content/appearance/sets';
|
||||
import { subPageMixin } from '../../mixins/subPage';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||
import subMenu from './sub-menu';
|
||||
import customizeBanner from './customize-banner.vue';
|
||||
import customizeOptions from './customize-options';
|
||||
import gem from '@/assets/svg/gem.svg';
|
||||
|
||||
const skinsBySet = groupBy(appearance.skin, 'set.key');
|
||||
|
||||
const freeSkinKeys = skinsBySet[undefined].map(s => s.key);
|
||||
|
||||
// const specialSkinKeys = Object.keys(appearance.shirt)
|
||||
// .filter(k => appearance.shirt[k].price !== 0);
|
||||
import subMenu from './sub-menu';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
subMenu,
|
||||
customizeBanner,
|
||||
customizeOptions,
|
||||
},
|
||||
mixins: [
|
||||
|
|
@ -63,10 +43,6 @@ export default {
|
|||
],
|
||||
data () {
|
||||
return {
|
||||
freeSkinKeys,
|
||||
icons: Object.freeze({
|
||||
gem,
|
||||
}),
|
||||
skinSubMenuItems: [
|
||||
{
|
||||
id: 'color',
|
||||
|
|
@ -76,41 +52,13 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
freeSkins () {
|
||||
return freeSkinKeys.map(s => this.mapKeysToFreeOption(s, 'skin'));
|
||||
},
|
||||
seasonalSkins () {
|
||||
// @TODO: For some resonse when I use $set on the user purchases object,
|
||||
// this is not recomputed. Hack for now
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
|
||||
const seasonalSkins = [];
|
||||
for (const setKey of Object.keys(skinsBySet)) {
|
||||
const set = skinsBySet[setKey];
|
||||
|
||||
const keys = set.map(item => item.key);
|
||||
|
||||
const options = keys.map(optionKey => {
|
||||
const option = this.mapKeysToOption(optionKey, 'skin', '', setKey);
|
||||
|
||||
return option;
|
||||
});
|
||||
|
||||
let text = this.$t(setKey);
|
||||
if (appearanceSets[setKey] && appearanceSets[setKey].text) {
|
||||
text = appearanceSets[setKey].text();
|
||||
}
|
||||
|
||||
const compiledSet = {
|
||||
key: setKey,
|
||||
options,
|
||||
keys,
|
||||
text,
|
||||
};
|
||||
seasonalSkins.push(compiledSet);
|
||||
}
|
||||
|
||||
return seasonalSkins;
|
||||
userSkins () {
|
||||
const freeSkins = groupBy(appearance.skin, 'set.key')[undefined]
|
||||
.map(s => s.key).map(s => this.mapKeysToFreeOption(s, 'skin'));
|
||||
const ownedSkins = Object.keys(this.user.purchased.skin)
|
||||
.filter(k => this.user.purchased.skin[k])
|
||||
.map(s => this.mapKeysToFreeOption(s, 'skin'));
|
||||
return [...freeSkins, ...ownedSkins];
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
|
|
|
|||
|
|
@ -151,14 +151,14 @@
|
|||
<div
|
||||
v-if="activeTopPage === 'backgrounds'"
|
||||
id="backgrounds"
|
||||
class="section container customize-section"
|
||||
class="section customize-section"
|
||||
>
|
||||
<div class="row text-center title-row">
|
||||
<strong>{{ $t('incentiveBackgrounds') }}</strong>
|
||||
</div>
|
||||
<div
|
||||
class="row title-row"
|
||||
v-if="!ownsSet('background', standardBackgrounds)"
|
||||
v-if="standardBackgrounds.length < standardBackgroundMax"
|
||||
>
|
||||
<div
|
||||
class="col-12"
|
||||
|
|
@ -274,12 +274,12 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="row text-center title-row mt-2"
|
||||
>
|
||||
<strong>{{ $t('monthlyBackgrounds') }}</strong>
|
||||
</div>
|
||||
<div v-if="monthlyBackgrounds.length > 0">
|
||||
<div
|
||||
class="row text-center title-row mt-2"
|
||||
>
|
||||
<strong>{{ $t('monthlyBackgrounds') }}</strong>
|
||||
</div>
|
||||
<div class="row title-row">
|
||||
<div
|
||||
v-for="(bg) in monthlyBackgrounds"
|
||||
|
|
@ -301,6 +301,15 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<customize-banner />
|
||||
</div>
|
||||
<div v-else>
|
||||
<h3 v-once> {{ $t('noItemsOwned') }} </h3>
|
||||
<p
|
||||
v-once
|
||||
v-html="$t('visitCustomizationsShop')"
|
||||
class="w-50 mx-auto"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -605,7 +614,10 @@
|
|||
|
||||
.customize-section {
|
||||
text-align: center;
|
||||
padding-bottom: 2em;
|
||||
background-color: #f9f9f9;
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
min-height: 256px;
|
||||
}
|
||||
|
||||
#creator-background {
|
||||
|
|
@ -626,9 +638,9 @@
|
|||
}
|
||||
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
color: $gray-200;
|
||||
color: $gray-100;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.purchase-all {
|
||||
|
|
@ -819,11 +831,6 @@
|
|||
color: $yellow-10
|
||||
}
|
||||
|
||||
.customize-section {
|
||||
background-color: #f9f9f9;
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.interests-section {
|
||||
margin-top: 3em;
|
||||
margin-bottom: 60px;
|
||||
|
|
@ -1076,6 +1083,7 @@ import usernameForm from './settings/usernameForm';
|
|||
import guide from '@/mixins/guide';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import PinBadge from '@/components/ui/pinBadge';
|
||||
import customizeBanner from './avatarModal/customize-banner';
|
||||
import bodySettings from './avatarModal/body-settings';
|
||||
import skinSettings from './avatarModal/skin-settings';
|
||||
import hairSettings from './avatarModal/hair-settings';
|
||||
|
|
@ -1098,6 +1106,7 @@ import { avatarEditorUtilities } from '../mixins/avatarEditUtilities';
|
|||
export default {
|
||||
components: {
|
||||
avatar,
|
||||
customizeBanner,
|
||||
bodySettings,
|
||||
extraSettings,
|
||||
hairSettings,
|
||||
|
|
@ -1112,6 +1121,7 @@ export default {
|
|||
allBackgrounds: content.backgroundsFlat,
|
||||
monthlyBackgrounds: [],
|
||||
standardBackgrounds: [],
|
||||
standardBackgroundMax: 0,
|
||||
timeTravelBackgrounds: [],
|
||||
backgroundUpdate: new Date(),
|
||||
|
||||
|
|
@ -1173,6 +1183,9 @@ export default {
|
|||
},
|
||||
mounted () {
|
||||
forEach(this.allBackgrounds, bg => {
|
||||
if (bg.set === 'incentiveBackgrounds') {
|
||||
this.standardBackgroundMax += 1;
|
||||
}
|
||||
if (this.user.purchased.background[bg.key]) {
|
||||
if (bg.set === 'incentiveBackgrounds') {
|
||||
this.standardBackgrounds.push(bg);
|
||||
|
|
|
|||
|
|
@ -126,5 +126,7 @@
|
|||
"zombie2": "Undead",
|
||||
"allCustomizationsOwned": "You own all of these items. You can try them on by <a href=''>customizing your avatar</a>.",
|
||||
"checkNextMonth": "Be sure to check back later for next month's options!",
|
||||
"checkNextSeason": "Be sure to check back later for next season's options!"
|
||||
"checkNextSeason": "Be sure to check back later for next season's options!",
|
||||
"noItemsOwned": "You don't own any of these items",
|
||||
"visitCustomizationsShop": "Head over to the <a href='/shops/customizations'>Customizations Shop</a> to browse the many ways you can customize your avatar!"
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
"checkinEarned": "Your Check-In Counter went up!",
|
||||
"unlockedCheckInReward": "You unlocked a Check-In Prize!",
|
||||
"checkinProgressTitle": "Progress until next",
|
||||
"incentiveBackgroundsUnlockedWithCheckins": "Locked Plain Backgrounds will unlock with Daily Check-Ins.",
|
||||
"incentiveBackgroundsUnlockedWithCheckins": "More Standard Backgrounds will unlock with Daily Check-Ins.",
|
||||
"oneOfAllPetEggs": "one of each standard Pet Egg",
|
||||
"twoOfAllPetEggs": "two of each standard Pet Egg",
|
||||
"threeOfAllPetEggs": "three of each standard Pet Egg",
|
||||
|
|
|
|||
Loading…
Reference in a new issue