WIP(shops): first full test version

This commit is contained in:
Sabe Jones
2024-04-24 00:02:41 -05:00
parent 7a50b2d2ff
commit 3bf323032c
9 changed files with 258 additions and 474 deletions

View File

@@ -1,7 +1,7 @@
<template> <template>
<div <div
id="body" id="body"
class="section customize-section" class="customize-section d-flex flex-column justify-content-between"
> >
<sub-menu <sub-menu
class="text-center" class="text-center"
@@ -17,17 +17,11 @@
</div> </div>
<div v-if="activeSubPage === 'shirt'"> <div v-if="activeSubPage === 'shirt'">
<customize-options <customize-options
:items="freeShirts" :items="userShirts"
:current-value="user.preferences.shirt" :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> </div>
<customize-banner />
</div> </div>
</template> </template>
@@ -36,16 +30,14 @@ import appearance from '@/../../common/script/content/appearance';
import { subPageMixin } from '../../mixins/subPage'; import { subPageMixin } from '../../mixins/subPage';
import { userStateMixin } from '../../mixins/userState'; import { userStateMixin } from '../../mixins/userState';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities'; import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
import subMenu from './sub-menu'; import customizeBanner from './customize-banner.vue';
import customizeOptions from './customize-options'; 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 { export default {
components: { components: {
subMenu, subMenu,
customizeBanner,
customizeOptions, customizeOptions,
}, },
mixins: [ mixins: [
@@ -58,10 +50,6 @@ export default {
], ],
data () { data () {
return { return {
specialShirtKeys,
icons: Object.freeze({
gem,
}),
items: [ items: [
{ {
id: 'size', id: 'size',
@@ -78,25 +66,19 @@ export default {
sizes () { sizes () {
return ['slim', 'broad'].map(s => this.mapKeysToFreeOption(s, 'size')); return ['slim', 'broad'].map(s => this.mapKeysToFreeOption(s, 'size'));
}, },
freeShirts () { userShirts () {
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt')); const freeShirts = Object.keys(appearance.shirt)
}, .filter(k => appearance.shirt[k].price === 0)
specialShirts () { .map(s => this.mapKeysToFreeOption(s, 'shirt'));
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line const ownedShirts = Object.keys(this.user.purchased.shirt)
const keys = this.specialShirtKeys; .filter(k => this.user.purchased.shirt[k])
const options = keys.map(key => this.mapKeysToOption(key, 'shirt')); .map(s => this.mapKeysToFreeOption(s, 'shirt'));
return options;
return [...freeShirts, ...ownedShirts];
}, },
}, },
mounted () { mounted () {
this.changeSubPage('size'); this.changeSubPage('size');
}, },
methods: {
},
}; };
</script> </script>
<style scoped>
</style>

View File

@@ -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>

View File

@@ -1,7 +1,6 @@
<template> <template>
<div <div
class="customize-options" class="customize-options"
:class="{'background-set': fullSet}"
> >
<div <div
v-for="option in items" v-for="option in items"
@@ -28,38 +27,6 @@
</div> </div>
</div> </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>
</div> </div>
</template> </template>
@@ -73,7 +40,7 @@ export default {
mixins: [ mixins: [
avatarEditorUtilities, avatarEditorUtilities,
], ],
props: ['items', 'currentValue', 'fullSet'], props: ['items', 'currentValue'],
data () { data () {
return { return {
icons: Object.freeze({ icons: Object.freeze({

View File

@@ -1,7 +1,8 @@
<template> <template>
<div <div
id="extra" id="extra"
class="section container customize-section" class="customize-section d-flex flex-column"
:class="{ 'justify-content-between': !showEmptySection}"
> >
<sub-menu <sub-menu
class="text-center" class="text-center"
@@ -20,9 +21,8 @@
id="animal-ears" id="animal-ears"
> >
<customize-options <customize-options
v-if="animalItems('back').length > 1"
:items="animalItems('headAccessory')" :items="animalItems('headAccessory')"
:full-set="!animalItemsOwned('headAccessory')"
@unlock="unlock(animalItemsUnlockString('headAccessory'))"
/> />
</div> </div>
<div <div
@@ -30,9 +30,8 @@
id="animal-tails" id="animal-tails"
> >
<customize-options <customize-options
v-if="animalItems('back').length > 1"
:items="animalItems('back')" :items="animalItems('back')"
:full-set="!animalItemsOwned('back')"
@unlock="unlock(animalItemsUnlockString('back'))"
/> />
</div> </div>
<div <div
@@ -53,6 +52,22 @@
> >
<customize-options :items="flowers" /> <customize-options :items="flowers" />
</div> </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> </div>
</template> </template>
@@ -61,17 +76,18 @@ import appearance from '@/../../common/script/content/appearance';
import { subPageMixin } from '../../mixins/subPage'; import { subPageMixin } from '../../mixins/subPage';
import { userStateMixin } from '../../mixins/userState'; import { userStateMixin } from '../../mixins/userState';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities'; import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
import subMenu from './sub-menu'; import customizeBanner from './customize-banner';
import customizeOptions from './customize-options'; 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 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); const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
export default { export default {
components: { components: {
subMenu, customizeBanner,
customizeOptions, customizeOptions,
subMenu,
}, },
mixins: [ mixins: [
subPageMixin, 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'], chairKeys: ['none', 'black', 'blue', 'green', 'pink', 'red', 'yellow', 'handleless_black', 'handleless_blue', 'handleless_green', 'handleless_pink', 'handleless_red', 'handleless_yellow'],
specialShirtKeys, specialShirtKeys,
icons: Object.freeze({
gem,
}),
items: [ items: [
{ {
id: 'size', id: 'size',
@@ -228,6 +241,16 @@ export default {
}); });
return options; 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 () { mounted () {
this.changeSubPage(this.extraSubMenuItems[0].id); this.changeSubPage(this.extraSubMenuItems[0].id);
@@ -248,7 +271,7 @@ export default {
for (const key of keys) { for (const key of keys) {
const newKey = `${category}_special_${key}`; const newKey = `${category}_special_${key}`;
const userPurchased = this.user.items.gear.owned[newKey]; const userPurchased = this.user.items.gear.owned[newKey];
if (userPurchased) {
const option = {}; const option = {};
option.key = key; option.key = key;
option.active = this.user.preferences.costume option.active = this.user.preferences.costume
@@ -258,27 +281,13 @@ export default {
if (category === 'back') { if (category === 'back') {
option.class = `icon_back_special_${option.key} 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 = () => { option.click = () => {
if (option.gemLocked) {
return this.unlock(`items.gear.owned.${newKey}`);
} if (option.goldLocked) {
return this.buy(newKey);
}
const type = this.user.preferences.costume ? 'costume' : 'equipped'; const type = this.user.preferences.costume ? 'costume' : 'equipped';
return this.equip(newKey, type); return this.equip(newKey, type);
}; };
options.push(option); options.push(option);
} }
}
return options; return options;
}, },
@@ -287,17 +296,6 @@ export default {
return keys.join(','); 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) { createGearItem (key, gearType, subGearType, additionalClass) {
const newKey = `${gearType}_${subGearType ? `${subGearType}_` : ''}${key}`; const newKey = `${gearType}_${subGearType ? `${subGearType}_` : ''}${key}`;
const option = {}; const option = {};
@@ -339,7 +337,3 @@ export default {
}, },
}; };
</script> </script>
<style scoped>
</style>

View File

@@ -1,7 +1,8 @@
<template> <template>
<div <div
id="hair" id="hair"
class="section customize-section" class="customize-section d-flex flex-column"
:class="{ 'justify-content-between': !showEmptySection}"
> >
<sub-menu <sub-menu
class="text-center" class="text-center"
@@ -14,37 +15,9 @@
id="hair-color" id="hair-color"
> >
<customize-options <customize-options
:items="freeHairColors" :items="userHairColors"
:current-value="user.preferences.hair.color" :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>
<div <div
v-if="activeSubPage === 'bangs'" v-if="activeSubPage === 'bangs'"
@@ -55,44 +28,62 @@
:current-value="user.preferences.hair.bangs" :current-value="user.preferences.hair.bangs"
/> />
</div> </div>
<div
v-if="activeSubPage === 'style'"
id="style"
>
<customize-options
:items="userHairStyles"
:current-value="user.preferences.hair.base"
/>
</div>
<div <div
v-if="activeSubPage === 'facialhair'" v-if="activeSubPage === 'facialhair'"
id="facialhair" id="facialhair"
> >
<customize-options <customize-options
v-if="editing" v-if="editing && userMustaches.length > 1"
:items="mustacheList" :items="userMustaches"
/> />
<!-- eslint-disable max-len -->
<customize-options <customize-options
v-if="editing" v-if="editing && userBeards.length > 1"
:items="beardList" :items="userBeards"
:full-set="isPurchaseAllNeeded('hair', ['baseHair5', 'baseHair6'], ['mustache', 'beard'])"
@unlock="unlock(`hair.mustache.${baseHair5Keys.join(',hair.mustache.')},hair.beard.${baseHair6Keys.join(',hair.beard.')}`)"
/> />
<!-- 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>
</div> </div>
<customize-banner
v-if="!showEmptySection"
/>
</div>
</template> </template>
<script> <script>
import groupBy from 'lodash/groupBy'; import groupBy from 'lodash/groupBy';
import appearance from '@/../../common/script/content/appearance'; import appearance from '@/../../common/script/content/appearance';
import appearanceSets from '@/../../common/script/content/appearance/sets';
import { subPageMixin } from '../../mixins/subPage'; import { subPageMixin } from '../../mixins/subPage';
import { userStateMixin } from '../../mixins/userState'; import { userStateMixin } from '../../mixins/userState';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities'; import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
import subMenu from './sub-menu'; import customizeBanner from './customize-banner';
import customizeOptions from './customize-options'; import customizeOptions from './customize-options';
import gem from '@/assets/svg/gem.svg'; import subMenu from './sub-menu';
const hairColorBySet = groupBy(appearance.hair.color, 'set.key');
const freeHairColorKeys = hairColorBySet[undefined].map(s => s.key);
export default { export default {
components: { components: {
subMenu, customizeBanner,
customizeOptions, customizeOptions,
subMenu,
}, },
mixins: [ mixins: [
subPageMixin, subPageMixin,
@@ -102,20 +93,6 @@ export default {
props: [ props: [
'editing', '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: { computed: {
hairSubMenuItems () { hairSubMenuItems () {
const items = [ const items = [
@@ -142,91 +119,46 @@ export default {
return items; return items;
}, },
freeHairColors () { userHairColors () {
return freeHairColorKeys.map(s => this.mapKeysToFreeOption(s, 'hair', 'color')); 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 () { userHairStyles () {
// @TODO: For some resonse when I use $set on the user purchases object, const emptyHairStyle = {
// this is not recomputed. Hack for now ...this.mapKeysToFreeOption(0, 'hair', 'base'),
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line none: true,
const seasonalHairColors = [];
for (const key of Object.keys(hairColorBySet)) {
const set = hairColorBySet[key];
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); 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'));
return seasonalHairColors; return [emptyMustache, ...ownedMustaches];
}, },
premiumHairColors () { userBeards () {
// @TODO: For some resonse when I use $set on the user purchases object, const emptyBeard = {
// this is not recomputed. Hack for now ...this.mapKeysToFreeOption(0, 'hair', 'beard'),
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line none: true,
const keys = this.premiumHairColorKeys; };
const options = keys.map(key => this.mapKeysToOption(key, 'hair', 'color')); const ownedBeards = Object.keys(this.user.purchased.hair.beard || {})
return options; .filter(k => this.user.purchased.hair.beard[k])
}, .map(h => this.mapKeysToFreeOption(h, 'hair', 'beard'));
baseHair2 () {
// @TODO: For some resonse when I use $set on the user purchases object, return [emptyBeard, ...ownedBeards];
// 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;
}, },
hairBangs () { hairBangs () {
const none = this.mapKeysToFreeOption(0, 'hair', 'bangs'); const none = this.mapKeysToFreeOption(0, 'hair', 'bangs');
@@ -236,136 +168,13 @@ export default {
return [none, ...options]; return [none, ...options];
}, },
mustacheList () { showEmptySection () {
const noneOption = this.mapKeysToFreeOption(0, 'hair', 'mustache'); return this.editing && this.activeSubPage === 'facialhair'
noneOption.none = true; && this.userMustaches.length === 1 && this.userBeards.length === 1;
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;
}, },
}, },
mounted () { mounted () {
this.changeSubPage('color'); 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> </script>
<style scoped>
</style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div <div
id="skin" id="skin"
class="section customize-section" class="customize-section d-flex flex-column justify-content-between"
> >
<sub-menu <sub-menu
class="text-center" class="text-center"
@@ -10,47 +10,27 @@
@changeSubPage="changeSubPage($event)" @changeSubPage="changeSubPage($event)"
/> />
<customize-options <customize-options
:items="freeSkins" :items="userSkins"
:current-value="user.preferences.skin" :current-value="user.preferences.skin"
/> />
<!-- eslint-disable vue/no-use-v-if-with-v-for --> <customize-banner />
<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>
</div> </div>
</template> </template>
<script> <script>
import groupBy from 'lodash/groupBy'; import groupBy from 'lodash/groupBy';
import appearance from '@/../../common/script/content/appearance'; import appearance from '@/../../common/script/content/appearance';
import appearanceSets from '@/../../common/script/content/appearance/sets';
import { subPageMixin } from '../../mixins/subPage'; import { subPageMixin } from '../../mixins/subPage';
import { userStateMixin } from '../../mixins/userState'; import { userStateMixin } from '../../mixins/userState';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities'; import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
import subMenu from './sub-menu'; import customizeBanner from './customize-banner.vue';
import customizeOptions from './customize-options'; import customizeOptions from './customize-options';
import gem from '@/assets/svg/gem.svg'; import subMenu from './sub-menu';
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);
export default { export default {
components: { components: {
subMenu, subMenu,
customizeBanner,
customizeOptions, customizeOptions,
}, },
mixins: [ mixins: [
@@ -63,10 +43,6 @@ export default {
], ],
data () { data () {
return { return {
freeSkinKeys,
icons: Object.freeze({
gem,
}),
skinSubMenuItems: [ skinSubMenuItems: [
{ {
id: 'color', id: 'color',
@@ -76,41 +52,13 @@ export default {
}; };
}, },
computed: { computed: {
freeSkins () { userSkins () {
return freeSkinKeys.map(s => this.mapKeysToFreeOption(s, 'skin')); const freeSkins = groupBy(appearance.skin, 'set.key')[undefined]
}, .map(s => s.key).map(s => this.mapKeysToFreeOption(s, 'skin'));
seasonalSkins () { const ownedSkins = Object.keys(this.user.purchased.skin)
// @TODO: For some resonse when I use $set on the user purchases object, .filter(k => this.user.purchased.skin[k])
// this is not recomputed. Hack for now .map(s => this.mapKeysToFreeOption(s, 'skin'));
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line return [...freeSkins, ...ownedSkins];
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;
}, },
}, },
mounted () { mounted () {

View File

@@ -151,14 +151,14 @@
<div <div
v-if="activeTopPage === 'backgrounds'" v-if="activeTopPage === 'backgrounds'"
id="backgrounds" id="backgrounds"
class="section container customize-section" class="section customize-section"
> >
<div class="row text-center title-row"> <div class="row text-center title-row">
<strong>{{ $t('incentiveBackgrounds') }}</strong> <strong>{{ $t('incentiveBackgrounds') }}</strong>
</div> </div>
<div <div
class="row title-row" class="row title-row"
v-if="!ownsSet('background', standardBackgrounds)" v-if="standardBackgrounds.length < standardBackgroundMax"
> >
<div <div
class="col-12" class="col-12"
@@ -274,12 +274,12 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="monthlyBackgrounds.length > 0">
<div <div
class="row text-center title-row mt-2" class="row text-center title-row mt-2"
> >
<strong>{{ $t('monthlyBackgrounds') }}</strong> <strong>{{ $t('monthlyBackgrounds') }}</strong>
</div> </div>
<div v-if="monthlyBackgrounds.length > 0">
<div class="row title-row"> <div class="row title-row">
<div <div
v-for="(bg) in monthlyBackgrounds" v-for="(bg) in monthlyBackgrounds"
@@ -301,6 +301,15 @@
/> />
</div> </div>
</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> </div>
</div> </div>
@@ -605,7 +614,10 @@
.customize-section { .customize-section {
text-align: center; text-align: center;
padding-bottom: 2em; background-color: #f9f9f9;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
min-height: 256px;
} }
#creator-background { #creator-background {
@@ -626,9 +638,9 @@
} }
h3 { h3 {
font-size: 20px; color: $gray-100;
font-weight: normal; font-weight: 700;
color: $gray-200; line-height: 24px;
} }
.purchase-all { .purchase-all {
@@ -819,11 +831,6 @@
color: $yellow-10 color: $yellow-10
} }
.customize-section {
background-color: #f9f9f9;
min-height: 280px;
}
.interests-section { .interests-section {
margin-top: 3em; margin-top: 3em;
margin-bottom: 60px; margin-bottom: 60px;
@@ -1076,6 +1083,7 @@ import usernameForm from './settings/usernameForm';
import guide from '@/mixins/guide'; import guide from '@/mixins/guide';
import notifications from '@/mixins/notifications'; import notifications from '@/mixins/notifications';
import PinBadge from '@/components/ui/pinBadge'; import PinBadge from '@/components/ui/pinBadge';
import customizeBanner from './avatarModal/customize-banner';
import bodySettings from './avatarModal/body-settings'; import bodySettings from './avatarModal/body-settings';
import skinSettings from './avatarModal/skin-settings'; import skinSettings from './avatarModal/skin-settings';
import hairSettings from './avatarModal/hair-settings'; import hairSettings from './avatarModal/hair-settings';
@@ -1098,6 +1106,7 @@ import { avatarEditorUtilities } from '../mixins/avatarEditUtilities';
export default { export default {
components: { components: {
avatar, avatar,
customizeBanner,
bodySettings, bodySettings,
extraSettings, extraSettings,
hairSettings, hairSettings,
@@ -1112,6 +1121,7 @@ export default {
allBackgrounds: content.backgroundsFlat, allBackgrounds: content.backgroundsFlat,
monthlyBackgrounds: [], monthlyBackgrounds: [],
standardBackgrounds: [], standardBackgrounds: [],
standardBackgroundMax: 0,
timeTravelBackgrounds: [], timeTravelBackgrounds: [],
backgroundUpdate: new Date(), backgroundUpdate: new Date(),
@@ -1173,6 +1183,9 @@ export default {
}, },
mounted () { mounted () {
forEach(this.allBackgrounds, bg => { forEach(this.allBackgrounds, bg => {
if (bg.set === 'incentiveBackgrounds') {
this.standardBackgroundMax += 1;
}
if (this.user.purchased.background[bg.key]) { if (this.user.purchased.background[bg.key]) {
if (bg.set === 'incentiveBackgrounds') { if (bg.set === 'incentiveBackgrounds') {
this.standardBackgrounds.push(bg); this.standardBackgrounds.push(bg);

View File

@@ -126,5 +126,7 @@
"zombie2": "Undead", "zombie2": "Undead",
"allCustomizationsOwned": "You own all of these items. You can try them on by <a href=''>customizing your avatar</a>.", "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!", "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!"
} }

View File

@@ -7,7 +7,7 @@
"checkinEarned": "Your Check-In Counter went up!", "checkinEarned": "Your Check-In Counter went up!",
"unlockedCheckInReward": "You unlocked a Check-In Prize!", "unlockedCheckInReward": "You unlocked a Check-In Prize!",
"checkinProgressTitle": "Progress until next", "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", "oneOfAllPetEggs": "one of each standard Pet Egg",
"twoOfAllPetEggs": "two of each standard Pet Egg", "twoOfAllPetEggs": "two of each standard Pet Egg",
"threeOfAllPetEggs": "three of each standard Pet Egg", "threeOfAllPetEggs": "three of each standard Pet Egg",