Attributes UI Refactoring (#9887)

* include class bonus in sorting

* wip - show more information in the attributes grid

* attributes tooltip + dialog redesign

* fix stat calculation

* fix spacings

* show class in equip-gear-modal

* fix buy-modal attributes-grid, clean up css

* show attributes popover in profile-stats overview

* add class / purchase type colors to colors.scss - replace colors by variable - clean up

* translate strings
This commit is contained in:
negue
2018-02-09 23:47:38 +01:00
committed by Sabe Jones
parent 6c677be9d2
commit 723b4f3451
19 changed files with 405 additions and 189 deletions

View File

@@ -61,3 +61,12 @@ $green-100: #5AEAB2;
$green-500: #A6FFDF;
$suggested-item-color: #D5C8FF;
$healer-color: #cf8229;
$rogue-color: #4F2A93;
$warrior-color: #B01515;
$wizard-color: #1f6ea2;
$gems-color: #24CC8F;
$gold-color: #FFA623;
$hourglass-color: #2995CD;

View File

@@ -28,6 +28,12 @@
height: 12px;
}
.icon-24 {
width: 24px;
height: 24px;
}
.icon-10 {
width: 10px;
height: 10px;

View File

@@ -36,17 +36,3 @@
margin-bottom: 0px;
}
.popover-content-attr {
width: 50%;
display: inline-block;
font-weight: bold;
margin-bottom: 4px;
&-key {
color: $white;
}
&-val {
color: $green-10;
}
}

View File

@@ -68,3 +68,8 @@ h4 {
font-size: 14px;
line-height: 1.43;
}
.textCondensed {
font-family: 'Roboto Condensed', sans-serif;
font-weight: bold;
}

View File

@@ -0,0 +1,169 @@
<template lang="pug">
div.attributes-group
.popover-content-attr(v-for="attr in ATTRIBUTES", :key="attr")
.group-content
span.popover-content-attr-cell.key(:class="{'hasValue': hasSumValue(attr) }") {{ `${$t(attr)}: ` }}
span.popover-content-attr-cell.label.value(:class="{'green': hasSumValue(attr) }") {{ `${stats.sum[attr]}` }}
span.popover-content-attr-cell.label.bold(:class="{'hasValue': hasGearValue(attr) }") {{ $t('gear') }}:
span.popover-content-attr-cell.label(:class="{'hasValue': hasGearValue(attr) }") {{ stats.gear[attr] }}
span.popover-content-attr-cell.label.bold(:class="{'hasValue': hasClassBonus(attr) }") {{ $t('classEquipBonus') }}:
span.popover-content-attr-cell.label(:class="{'hasValue': hasClassBonus(attr) }") {{ `${stats.classBonus[attr]}` }}
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
.attributes-group {
border-radius: 4px;
// unless we have a way to give a popover an id or class, it needs expand the attributes area
margin: -12px -16px;
display:flex;
flex-wrap: wrap;
}
.popover-content-attr {
font-weight: bold;
width: calc(50% - 1px);
background-color: $gray-50;
&:nth-of-type(even) {
margin-left: 1px;
}
&:nth-child(1), &:nth-child(2) {
margin-bottom: 1px;
}
}
.group-content {
display: inline-flex;
flex-wrap: wrap;
padding: 4px 12px;
width: 100%;
}
.popover-content-attr-cell {
width: 70%;
text-align: left;
&:nth-of-type(even) {
text-align: right;
width: 30%;
}
&.key {
color: $white;
font-size: 12px;
font-weight: bold;
line-height: 1.33;
}
&.label {
font-size: 10px;
line-height: 1.2;
color: $gray-300;
}
&.label.bold {
font-weight: bold;
}
&.label.value {
font-size: 12px;
font-weight: bold;
line-height: 1.33;
text-align: right;
&.green {
color: $green-10;
&:before {
content: '+';
}
}
}
}
.modal-body {
.group-content {
padding: 8px 17px;
}
.popover-content-attr {
background-color: #f4f4f4;
&:nth-of-type(even) {
margin-left: 1px;
width: 50%;
}
}
.popover-content-attr-cell {
&.key {
color: $gray-400;
font-size: 16px;
font-weight: bold;
line-height: 1.25;
&.hasValue {
color: $gray-50;
}
}
&.label {
color: $gray-400;
font-size: 12px;
font-weight: bold;
line-height: 1.33;
&.hasValue {
color: $gray-200;
}
}
&.label.value {
&.green {
color: $green-10;
}
}
}
}
</style>
<script>
import { mapState } from 'client/libs/store';
import statsMixin from 'client/mixins/stats';
export default {
mixins: [statsMixin],
props: {
item: {
type: Object,
},
},
computed: {
...mapState({
ATTRIBUTES: 'constants.ATTRIBUTES',
user: 'user.data',
flatGear: 'content.gear.flat',
}),
},
methods: {
hasSumValue (attr) {
return this.stats.sum[attr] > 0;
},
hasGearValue (attr) {
return this.stats.gear[attr] > 0;
},
hasClassBonus (attr) {
return this.stats.classBonus[attr] > 0;
},
},
};
</script>

View File

@@ -8,15 +8,25 @@ div
div(v-else)
h4.popover-content-title {{ itemText }}
.popover-content-text {{ itemNotes }}
.popover-content-attr(v-for="attr in ATTRIBUTES", :key="attr")
span.popover-content-attr-key {{ `${$t(attr)}: ` }}
span.popover-content-attr-val {{ `+${item[attr]}` }}
attributesGrid(:item="item")
</template>
<style scoped>
.popover-content-text {
margin-bottom: 25px;
}
</style>
<script>
import { mapState } from 'client/libs/store';
import attributesGrid from './attributesGrid';
import statsMixin from 'client/mixins/stats';
export default {
mixins: [statsMixin],
components: {
attributesGrid,
},
props: {
item: {
type: Object,
@@ -25,6 +35,7 @@ div
computed: {
...mapState({
ATTRIBUTES: 'constants.ATTRIBUTES',
user: 'user.data',
}),
itemText () {
if (this.item.text instanceof Function) {

View File

@@ -23,7 +23,11 @@
h4.title {{ itemText }}
div.text(v-html="itemNotes")
equipmentAttributesGrid.bordered(
span.classTag(v-if="showClassTag")
span.svg-icon.inline.icon-24(v-html="icons[itemClass]")
span.className.textCondensed(:class="itemClass") {{ getClassName(itemClass) }}
attributesGrid.attributesGrid(
:item="item",
v-if="attributesGridVisible"
)
@@ -58,11 +62,41 @@
width: 282px;
}
.bordered {
border-radius: 2px;
background-color: #f9f9f9;
margin-bottom: 24px;
padding: 24px 24px 10px;
.classTag {
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.className {
height: 24px;
font-size: 16px;
line-height: 1.5;
text-align: left;
margin-left: 8px;
}
.healer {
color: $healer-color;
}
.rogue {
color: $rogue-color;
}
.warrior {
color: $warrior-color;
}
.wizard {
color: $wizard-color;
}
.attributesGrid {
background-color: $gray-500;
margin: 10px 0 24px;
}
.avatar {
@@ -74,15 +108,6 @@
}
}
.content-text {
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: normal;
line-height: 1.43;
width: 400px;
}
button.btn.btn-primary {
margin-top: 24px;
margin-bottom: 24px;
@@ -94,19 +119,27 @@
import { mapState } from 'client/libs/store';
import svgClose from 'assets/svg/close.svg';
import svgWarrior from 'assets/svg/warrior.svg';
import svgWizard from 'assets/svg/wizard.svg';
import svgRogue from 'assets/svg/rogue.svg';
import svgHealer from 'assets/svg/healer.svg';
import Avatar from 'client/components/avatar';
import EquipmentAttributesGrid from 'client/components/shops/market/equipmentAttributesGrid.vue';
import attributesGrid from 'client/components/inventory/equipment/attributesGrid.vue';
export default {
components: {
Avatar,
EquipmentAttributesGrid,
attributesGrid,
},
data () {
return {
icons: Object.freeze({
close: svgClose,
warrior: svgWarrior,
wizard: svgWizard,
rogue: svgRogue,
healer: svgHealer,
}),
};
},
@@ -115,6 +148,9 @@
content: 'content',
user: 'user.data',
}),
showClassTag () {
return this.content.classes.includes(this.itemClass);
},
itemText () {
if (this.item.text instanceof Function) {
return this.item.text();
@@ -129,6 +165,9 @@
return this.item.notes;
}
},
itemClass () {
return this.item.klass || this.item.specialClass;
},
attributesGridVisible () {
if (this.costumeMode) {
return false;
@@ -153,6 +192,13 @@
[gear.type]: gear.key,
};
},
getClassName (classType) {
if (classType === 'wizard') {
return this.$t('mage');
} else {
return this.$t(classType);
}
},
},
props: {
item: {

View File

@@ -242,7 +242,19 @@ export default {
});
},
sortItems (items, sortBy) {
return sortBy === 'sortByName' ? _sortBy(items, sortGearTypeMap[sortBy]) : _reverse(_sortBy(items, sortGearTypeMap[sortBy]));
let userClass = this.user.stats.class;
return sortBy === 'sortByName' ?
_sortBy(items, sortGearTypeMap[sortBy]) :
_reverse(_sortBy(items, (item) => {
let attrToSort = sortGearTypeMap[sortBy];
let attrValue = item[attrToSort];
if (item.klass === userClass || item.specialClass === userClass) {
attrValue *= 1.5;
}
return attrValue;
}));
},
drawerToggled (newState) {
this.$store.state.equipmentDrawerOpen = newState;

View File

@@ -37,7 +37,7 @@
div.text(v-html="itemNotes")
slot(name="additionalInfo", :item="item")
equipmentAttributesGrid.bordered(
equipmentAttributesGrid.attributesGrid(
v-if="showAttributesGrid",
:item="item"
)
@@ -50,7 +50,7 @@
input(type='number', min='0', v-model='selectedAmountToBuy')
span(:class="{'notEnough': notEnoughCurrency}")
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons[getPriceClass()]")
span.value(:class="getPriceClass()") {{ item.value }}
span.cost(:class="getPriceClass()") {{ item.value }}
.gems-left(v-if='item.key === "gem"')
strong(v-if='gemsLeft > 0') {{ gemsLeft }} {{ $t('gemsRemaining') }}
@@ -125,7 +125,7 @@
width: 74px;
height: 40px;
border-radius: 2px;
background-color: #ffffff;
background-color: $white;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
margin-right: 24px;
@@ -144,15 +144,6 @@
}
}
.content-text {
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: normal;
line-height: 1.43;
width: 400px;
}
span.svg-icon.inline.icon-32 {
height: 32px;
width: 32px;
@@ -162,10 +153,9 @@
vertical-align: middle;
}
.value {
.cost {
width: 28px;
height: 32px;
font-family: Roboto;
font-size: 24px;
font-weight: bold;
line-height: 1.33;
@@ -173,15 +163,15 @@
vertical-align: middle;
&.gems {
color: $green-10;
color: $gems-color;
}
&.gold {
color: $yellow-10
color: $gold-color;
}
&.hourglasses {
color: $blue-10;
color: $hourglass-color;
}
}
@@ -229,7 +219,7 @@
.limitedTime {
height: 32px;
background-color: #6133b4;
background-color: $purple-300;
width: calc(100% + 30px);
margin: 0 -15px; // the modal content has its own padding
@@ -248,8 +238,12 @@
}
}
.bordered {
.attributesGrid {
margin-top: 8px;
border-radius: 2px;
background-color: $gray-500;
margin: 10px 0 24px;
}
.gems-left {
@@ -277,7 +271,7 @@
import { mapState } from 'client/libs/store';
import EquipmentAttributesGrid from './market/equipmentAttributesGrid.vue';
import EquipmentAttributesGrid from '../inventory/equipment/attributesGrid.vue';
import Item from 'client/components/inventory/item';
import Avatar from 'client/components/avatar';

View File

@@ -1,55 +0,0 @@
<template lang="pug">
div
.attribute-entry(v-for="attr in ATTRIBUTES", :key="attr")
span.key(:class="{'no-value': item[attr] == '0'}") {{ `${$t(attr)}: ` }}
span.val(:class="{'no-value': item[attr] == '0'}") {{ `+${item[attr]}` }}
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.attribute-entry {
width: 50%;
display: inline-block;
font-weight: bold;
margin-bottom: 16px;
}
span{
width: 38px;
height: 16px;
font-size: 16px;
font-weight: bold;
line-height: 1.0;
}
.no-value {
color: $gray-400 !important;
}
.key {
color: $gray-100;
}
.val {
color: $green-10;
}
</style>
<script>
import { mapState } from 'client/libs/store';
export default {
props: {
item: {
type: Object,
},
},
computed: {
...mapState({
ATTRIBUTES: 'constants.ATTRIBUTES',
}),
},
};
</script>

View File

@@ -255,13 +255,6 @@
margin: 24px auto;
}
.bordered {
border-radius: 2px;
background-color: #f9f9f9;
margin-bottom: 24px;
padding: 24px 24px 10px;
}
.item-wrapper.bordered-item .item {
width: 112px;
height: 112px;
@@ -359,7 +352,7 @@
import Avatar from 'client/components/avatar';
import SellModal from './sellModal.vue';
import EquipmentAttributesGrid from './equipmentAttributesGrid.vue';
import EquipmentAttributesGrid from '../../inventory/equipment/attributesGrid.vue';
import SelectMembersModal from 'client/components/selectMembersModal.vue';
import svgPin from 'assets/svg/pin.svg';

View File

@@ -63,15 +63,6 @@
width: 282px;
}
.content-text {
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: normal;
line-height: 1.43;
width: 400px;
}
span.svg-icon.inline.icon-32 {
height: 32px;
width: 32px;

View File

@@ -81,14 +81,6 @@
margin-bottom: 10px;
}
.content-text {
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: normal;
line-height: 1.43;
width: 400px;
}
.right-sidebar {
position: absolute;
right: -350px;

View File

@@ -241,13 +241,6 @@
margin: 24px auto;
}
.bordered {
border-radius: 2px;
background-color: #f9f9f9;
margin-bottom: 24px;
padding: 24px 24px 10px;
}
.group {
display: inline-block;
width: 33%;

View File

@@ -149,13 +149,6 @@
margin: 24px auto;
}
.bordered {
border-radius: 2px;
background-color: #f9f9f9;
margin-bottom: 24px;
padding: 24px 24px 10px;
}
.group {
display: inline-block;
width: 50%;

View File

@@ -129,13 +129,6 @@
margin: 24px auto;
}
.bordered {
border-radius: 2px;
background-color: #f9f9f9;
margin-bottom: 24px;
padding: 24px 24px 10px;
}
.group {
display: inline-block;
width: 33%;

View File

@@ -123,39 +123,26 @@ div
.col-12.col-md-6
h2.text-center {{$t('equipment')}}
.well
.col-12.col-md-4.item-wrapper
.box(:class='{white: equippedItems.eyewear && equippedItems.eyewear.indexOf("base_0") === -1}')
div(:class="`shop_${equippedItems.eyewear}`")
h3 {{$t('eyewear')}}
.col-12.col-md-4.item-wrapper
.box(:class='{white: equippedItems.head && equippedItems.head.indexOf("base_0") === -1}')
div(:class="`shop_${equippedItems.head}`")
h3 {{$t('headgearCapitalized')}}
.col-12.col-md-4.item-wrapper
.box(:class='{white: equippedItems.headAccessory && equippedItems.headAccessory.indexOf("base_0") === -1}')
div(:class="`shop_${equippedItems.headAccessory}`")
h3 {{$t('headAccess')}}
.col-12.col-md-4.item-wrapper
.box(:class='{white: equippedItems.back && equippedItems.back.indexOf("base_0") === -1}')
div(:class="`shop_${equippedItems.back}`")
h3 {{$t('backAccess')}}
.col-12.col-md-4.item-wrapper
.box(:class='{white: equippedItems.armor && equippedItems.armor.indexOf("base_0") === -1}')
div(:class="`shop_${equippedItems.armor}`")
h3 {{$t('armorCapitalized')}}
.col-12.col-md-4.item-wrapper
.box(:class='{white: equippedItems.body && equippedItems.body.indexOf("base_0") === -1}')
div(:class="`shop_${equippedItems.body}`")
h3 {{$t('bodyAccess')}}
.col-12.col-md-4.item-wrapper
.box(:class='{white: equippedItems.weapon && equippedItems.weapon.indexOf("base_0") === -1}')
div(:class="`shop_${equippedItems.weapon}`")
h3 {{$t('mainHand')}}
.col-12.col-md-4.item-wrapper
.col-12.col-md-4.item-wrapper
.box(:class='{white: equippedItems.shield && equippedItems.shield.indexOf("base_0") === -1}')
div(:class="`shop_${equippedItems.shield}`")
h3 {{$t('offHand')}}
.col-12.col-md-4.item-wrapper(v-for="(label, key) in equipTypes")
.box(
:id="key",
v-if="label !== 'skip'",
:class='{white: equippedItems[key] && equippedItems[key].indexOf("base_0") === -1}'
)
div(:class="`shop_${equippedItems[key]}`")
b-popover(
v-if="label !== 'skip' && equippedItems[key] && equippedItems[key].indexOf(\"base_0\") === -1",
:target="key",
triggers="hover",
:placement="'right'",
:preventOverflow="false",
)
h4.gearTitle {{ getGearTitle(equippedItems[key]) }}
attributesGrid.attributesGrid(
:item="content.gear.flat[equippedItems[key]]",
)
h3(v-if="label !== 'skip'") {{ label }}
.col-12.col-md-6
h2.text-center {{$t('costume')}}
.well
@@ -292,6 +279,11 @@ div
.modal-content {
background: #f9f9f9;
}
.gearTitle {
color: white;
margin-bottom: 20px;
}
}
.message-icon svg {
@@ -595,6 +587,7 @@ import achievementsLib from '../../../common/script/libs/achievements';
// @TODO: EMAILS.COMMUNITY_MANAGER_EMAIL
const COMMUNITY_MANAGER_EMAIL = 'admin@habitica.com';
import Content from '../../../common/script/content';
import attributesGrid from 'client/components/inventory/equipment/attributesGrid';
const DROP_ANIMALS = keys(Content.pets);
const TOTAL_NUMBER_OF_DROP_ANIMALS = DROP_ANIMALS.length;
@@ -612,6 +605,7 @@ export default {
sendGemsModal,
MemberDetails,
toggleSwitch,
attributesGrid,
},
data () {
return {
@@ -636,6 +630,17 @@ export default {
selectedPage: 'profile',
achievements: {},
content: Content,
equipTypes: {
eyewear: this.$t('eyewear'),
head: this.$t('headgearCapitalized'),
headAccessory: this.$t('headAccess'),
back: this.$t('backAccess'),
armor: this.$t('armorCapitalized'),
body: this.$t('bodyAccess'),
weapon: this.$t('mainHand'),
_skip: 'skip',
shield: this.$t('offHand'),
},
stats: {
str: {
title: 'strength',
@@ -755,6 +760,9 @@ export default {
userName: this.user.profile.name,
});
},
getGearTitle (key) {
return this.flatGear[key].text();
},
getProgressDisplay () {
// let currentLoginDay = Content.loginIncentives[this.user.loginIncentives];
// if (!currentLoginDay) return this.$t('checkinReceivedAllRewardsMessage');

View File

@@ -0,0 +1,69 @@
let emptyStats = () => {
return {
str: 0,
int: 0,
per: 0,
con: 0,
};
};
export default {
computed: {
stats () {
let gearStats = this.getItemStats(this.item);
let classBonus = this.getClassBonus(this.item);
let sumNewGearStats = this.calculateStats(gearStats, classBonus, (a1, a2) => {
return a1 + a2;
});
return {
gear: gearStats,
classBonus,
sum: sumNewGearStats,
};
},
},
methods: {
getItemStats (item) {
let result = emptyStats();
if (!item) {
return result;
}
for (let attr of this.ATTRIBUTES) {
result[attr] = Number(item[attr]);
}
return result;
},
getClassBonus (item) {
let result = emptyStats();
if (!item) {
return result;
}
let itemStats = this.getItemStats(item);
let userClass = this.user.stats.class;
if (userClass === item.klass || userClass === item.specialClass) {
for (let attr of this.ATTRIBUTES) {
result[attr] = itemStats[attr] * 0.5;
}
}
return result;
},
calculateStats (srcStats, otherStats, func) {
let result = emptyStats();
for (let attr of this.ATTRIBUTES) {
result[attr] = func(srcStats[attr], otherStats[attr]);
}
return result;
},
},
};

View File

@@ -64,6 +64,7 @@
"classBonusText": "Your class (Warrior, if you haven't unlocked or selected another class) uses its own equipment more effectively than gear from other classes. Equipped gear from your current class gets a 50% boost to the Stat bonus it grants.",
"classEquipBonus": "Class Bonus",
"battleGear": "Battle Gear",
"gear": "Gear",
"battleGearText": "This is the gear you wear into battle; it affects numbers when interacting with your tasks.",
"autoEquipBattleGear": "Auto-equip new gear",
"costume": "Costume",