Files
habitica/website/client/components/shops/buyModal.vue
aszlig 4f2d066d66 client: Fix display of class bonus for other users (#10376)
Whenever one is hovering an item from another user, the bonuses of these
items are shown for the own user. So for example if you're a mage and
view a Royal Magus Robe of another mage, the class bonus is 6.

However if you're a warrior, the class bonus displays as 0 because the
attributes grid is always using the stats for the own user even if
viewing equipment of a different user.

I've fixed this by moving the user object to the properties in
attributesGrid and passing the current user from every other Vue file
that's using attributesGrid.

Not sure whether this is the right approach, as I'm no expert in Vue.js
but some testing with the client now shows the correct values.

Signed-off-by: aszlig <aszlig@nix.build>
2018-05-25 12:38:02 +02:00

478 lines
12 KiB
Vue

<template lang="pug">
b-modal#buy-modal(
:hide-header="true",
@change="onChange($event)"
)
span.badge.badge-pill.badge-dialog(
:class="{'item-selected-badge': isPinned}",
v-if="withPin",
@click.prevent.stop="togglePinned()"
)
span.svg-icon.inline.color.icon-10(v-html="icons.pin")
div.close
span.svg-icon.inline.icon-10(aria-hidden="true", v-html="icons.close", @click="hideDialog()")
div.content(v-if="item != null")
div.inner-content
slot(name="item", :item="item")
div(v-if="showAvatar")
avatar(
:showVisualBuffs="false",
:member="user",
:avatarOnly="true",
:hideClassBadge="true",
:withBackground="true",
:overrideAvatarGear="getAvatarOverrides(item)",
:spritesMargin="'0px auto 0px -24px'",
)
item.flat.bordered-item(
:item="item",
:itemContentClass="item.class",
:showPopover="false",
v-else-if="item.key != 'gem'"
)
h4.title {{ itemText }}
div.text(v-html="itemNotes")
slot(name="additionalInfo", :item="item")
equipmentAttributesGrid.attributesGrid(
v-if="showAttributesGrid",
:item="item",
:user="user"
)
.purchase-amount(v-if='item.value > 0')
.how-many-to-buy(v-if='showAmountToBuy(item)')
strong {{ $t('howManyToBuy') }}
div(v-if='showAmountToBuy(item)')
.box
input(type='number', min='0', v-model.number='selectedAmountToBuy')
span(:class="{'notEnough': notEnoughCurrency}")
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons[getPriceClass()]")
span.cost(:class="getPriceClass()") {{ item.value }}
div(v-else)
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons[getPriceClass()]")
span.cost(:class="getPriceClass()") {{ item.value }}
.gems-left(v-if='item.key === "gem"')
strong(v-if='gemsLeft > 0') {{ gemsLeft }} {{ $t('gemsRemaining') }}
strong(v-if='gemsLeft === 0') {{ $t('maxBuyGems') }}
div(v-if='attemptingToPurchaseMoreGemsThanAreLeft')
| {{$t('notEnoughGemsToBuy')}}
button.btn.btn-primary(
@click="purchaseGems()",
v-if="getPriceClass() === 'gems' && !this.enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)"
) {{ $t('purchaseGems') }}
button.btn.btn-primary(
@click="buyItem()",
v-else,
:disabled='item.key === "gem" && gemsLeft === 0 || attemptingToPurchaseMoreGemsThanAreLeft',
:class="{'notEnough': !preventHealthPotion || !this.enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}"
) {{ $t('buyNow') }}
div.limitedTime(v-if="item.event")
span.svg-icon.inline.icon-16(v-html="icons.clock")
span.limitedString {{ limitedString }}
div.clearfix(slot="modal-footer")
span.balance.float-left {{ $t('yourBalance') }}
balanceInfo(
:withHourglass="getPriceClass() === 'hourglasses'",
:currencyNeeded="getPriceClass()",
:amountNeeded="item.value"
).float-right
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/modal.scss';
#buy-modal {
@include centeredModal();
.modal-dialog {
width: 330px;
}
.avatar {
cursor: default;
margin: 0 auto;
}
.content {
text-align: center;
}
.item-wrapper {
margin-bottom: 0 !important;
}
.inner-content {
margin: 33px auto auto;
width: 282px;
}
.purchase-amount {
margin-top: 24px;
.how-many-to-buy {
margin-bottom: 16px;
}
.box {
display: inline-block;
width: 74px;
height: 40px;
border-radius: 2px;
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;
input {
width: 100%;
border: none;
}
input::-webkit-contacts-auto-fill-button {
visibility: hidden;
display: none !important;
pointer-events: none;
position: absolute;
right: 0;
}
}
}
span.svg-icon.inline.icon-32 {
height: 32px;
width: 32px;
margin-right: 8px;
vertical-align: middle;
}
.cost {
width: 28px;
height: 32px;
font-size: 24px;
font-weight: bold;
line-height: 1.33;
vertical-align: middle;
&.gems {
color: $gems-color;
}
&.gold {
color: $gold-color;
}
&.hourglasses {
color: $hourglass-color;
}
}
button.btn.btn-primary {
margin-top: 24px;
margin-bottom: 24px;
}
.balance {
width: 74px;
height: 16px;
font-size: 12px;
font-weight: bold;
line-height: 1.33;
color: $gray-200;
}
.modal-footer {
height: 48px;
background-color: $gray-700;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
display: block;
}
.badge-dialog {
color: $gray-300;
position: absolute;
left: -14px;
padding: 8px 10px;
top: -12px;
background: white;
cursor: pointer;
&.item-selected-badge {
background: $purple-300;
color: $white;
}
}
.notEnough {
pointer-events: none;
opacity: 0.55;
}
.limitedTime {
height: 32px;
background-color: $purple-300;
width: calc(100% + 30px);
margin: 0 -15px; // the modal content has its own padding
font-size: 12px;
line-height: 1.33;
text-align: center;
color: $white;
display: flex;
align-items: center;
justify-content: center;
.limitedString {
height: 16px;
margin-left: 8px;
}
}
.attributesGrid {
margin-top: 8px;
border-radius: 2px;
background-color: $gray-500;
margin: 10px 0 24px;
}
.gems-left {
margin-top: .5em;
}
}
</style>
<script>
import * as Analytics from 'client/libs/analytics';
import spellsMixin from 'client/mixins/spells';
import planGemLimits from 'common/script/libs/planGemLimits';
import svgClose from 'assets/svg/close.svg';
import svgGold from 'assets/svg/gold.svg';
import svgGem from 'assets/svg/gem.svg';
import svgHourglasses from 'assets/svg/hourglass.svg';
import svgPin from 'assets/svg/pin.svg';
import svgClock from 'assets/svg/clock.svg';
import BalanceInfo from './balanceInfo.vue';
import currencyMixin from './_currencyMixin';
import notifications from 'client/mixins/notifications';
import buyMixin from 'client/mixins/buy';
import { mapState } from 'client/libs/store';
import EquipmentAttributesGrid from '../inventory/equipment/attributesGrid.vue';
import Item from 'client/components/inventory/item';
import Avatar from 'client/components/avatar';
import seasonalShopConfig from 'common/script/libs/shops-seasonal.config';
import moment from 'moment';
const hideAmountSelectionForPurchaseTypes = [
'gear', 'backgrounds', 'mystery_set', 'card',
'rebirth_orb', 'fortify', 'armoire', 'keys',
'debuffPotion',
];
export default {
mixins: [currencyMixin, notifications, spellsMixin, buyMixin],
components: {
BalanceInfo,
EquipmentAttributesGrid,
Item,
Avatar,
},
data () {
return {
icons: Object.freeze({
close: svgClose,
gold: svgGold,
gems: svgGem,
hourglasses: svgHourglasses,
pin: svgPin,
clock: svgClock,
}),
selectedAmountToBuy: 1,
isPinned: false,
};
},
computed: {
...mapState({user: 'user.data'}),
showAvatar () {
return ['backgrounds', 'gear', 'mystery_set'].includes(this.item.purchaseType);
},
preventHealthPotion () {
if (this.item.key === 'potion' && this.user.stats.hp >= 50) {
return false;
}
return true;
},
showAttributesGrid () {
return this.item.purchaseType === 'gear';
},
itemText () {
if (this.item.text instanceof Function) {
return this.item.text();
} else {
return this.item.text;
}
},
itemNotes () {
if (this.item.notes instanceof Function) {
return this.item.notes();
} else {
return this.item.notes;
}
},
limitedString () {
return this.$t('limitedOffer', {date: moment(seasonalShopConfig.dateRange.end).format('LL')});
},
gemsLeft () {
if (!this.user.purchased.plan) return 0;
return planGemLimits.convCap + this.user.purchased.plan.consecutive.gemCapExtra - this.user.purchased.plan.gemsBought;
},
attemptingToPurchaseMoreGemsThanAreLeft () {
if (this.item && this.item.key && this.item.key === 'gem' && this.selectedAmountToBuy > this.gemsLeft) return true;
return false;
},
notEnoughCurrency () {
return !this.enoughCurrency(this.getPriceClass(), this.item.value * this.selectedAmountToBuy);
},
},
watch: {
item: function itemChanged () {
this.isPinned = this.item && this.item.pinned;
this.selectedAmountToBuy = 1;
},
},
methods: {
onChange ($event) {
this.$emit('change', $event);
},
buyItem () {
// @TODO: I think we should buying to the items. Turn the items into classes, and use polymorphism
if (this.item.buy) {
this.item.buy();
this.$emit('buyPressed', this.item);
this.hideDialog();
return;
}
if (this.item.currency === 'gems' &&
!confirm(this.$t('purchaseFor', { cost: this.item.value * this.selectedAmountToBuy }))) {
return;
}
if (this.item.currency === 'hourglasses' &&
!confirm(this.$t('purchaseForHourglasses', { cost: this.item.value }))) {
return;
}
if (this.genericPurchase) {
this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
this.purchased(this.item.text);
}
this.$emit('buyPressed', this.item);
this.hideDialog();
},
purchaseGems () {
if (this.item.key === 'rebirth_orb') {
Analytics.track({
hitType: 'event',
eventCategory: 'button',
eventAction: 'click',
eventLabel: 'Gems > Rebirth',
});
}
this.$root.$emit('bv::show::modal', 'buy-gems');
},
togglePinned () {
this.isPinned = this.$store.dispatch('user:togglePinnedItem', {type: this.item.pinType, path: this.item.path});
if (!this.isPinned) {
this.text(this.$t('unpinnedItem', {item: this.item.text}));
}
},
hideDialog () {
this.$root.$emit('bv::hide::modal', 'buy-modal');
},
getPriceClass () {
if (this.priceType && this.icons[this.priceType]) {
return this.priceType;
} else if (this.item.currency && this.icons[this.item.currency]) {
return this.item.currency;
} else {
return 'gold';
}
},
showAmountToBuy (item) {
if (hideAmountSelectionForPurchaseTypes.includes(item.purchaseType)) {
return false;
} else {
return true;
}
},
getAvatarOverrides (item) {
switch (item.purchaseType) {
case 'gear':
return {
[item.type]: item.key,
};
case 'backgrounds':
return {
background: item.key,
};
case 'mystery_set': {
let gear = {};
item.items.map((setItem) => {
gear[setItem.type] = setItem.key;
});
return gear;
}
}
return {};
},
},
props: {
item: {
type: Object,
},
priceType: {
type: String,
},
withPin: {
type: Boolean,
},
genericPurchase: {
type: Boolean,
default: true,
},
},
};
</script>