Files
habitica/website/client/src/components/avatar.vue
2025-06-11 19:20:11 -05:00

396 lines
10 KiB
Vue

<template>
<div
v-if="member.preferences"
class="avatar"
:style="{width, height, paddingTop}"
:class="topLevelClassList"
@click.prevent="castEnd()"
>
<div
class="character-sprites"
:style="{margin: spritesMargin}"
>
<template v-if="!avatarOnly">
<!-- Mount Body-->
<span
v-if="member.items.currentMount"
:class="'Mount_Body_' + member.items.currentMount"
></span>
</template>
<!-- Buffs that cause visual changes to avatar: Snowman, Ghost, Flower, etc-->
<template v-for="(klass, item) in visualBuffs">
<span
v-if="member.stats.buffs[item] && showVisualBuffs"
:key="item"
:class="klass"
></span>
</template>
<!-- Show flower ALL THE TIME!!!-->
<!-- See https://github.com/HabitRPG/habitica/issues/7133-->
<span :class="'hair_flower_' + member.preferences.hair.flower"></span>
<!-- Show avatar only if not currently affected by visual buff-->
<template v-if="showAvatar()">
<span :class="['chair_' + member.preferences.chair, specialMountClass]"></span>
<span :class="[getGearClass('back'), specialMountClass]"></span>
<span :class="[skinClass, specialMountClass]"></span>
<!-- eslint-disable max-len-->
<span
:class="[shirtClass, specialMountClass]"
></span>
<!-- eslint-enable max-len-->
<span :class="['head_0', specialMountClass]"></span>
<!-- eslint-disable max-len-->
<span :class="[member.preferences.size + '_' + getGearClass('armor'), specialMountClass]"></span>
<!-- eslint-enable max-len-->
<span :class="[getGearClass('back_collar'), specialMountClass]"></span>
<template
v-for="type in ['bangs', 'base', 'mustache', 'beard']"
>
<span
:key="type"
:class="[hairClass(type), specialMountClass]"
></span>
</template>
<span :class="[getGearClass('body'), specialMountClass]"></span>
<span :class="[getGearClass('eyewear'), specialMountClass]"></span>
<span :class="[getGearClass('head'), specialMountClass]"></span>
<span :class="[getGearClass('headAccessory'), specialMountClass]"></span>
<span
:class="[
'hair_flower_' + member.preferences.hair.flower, specialMountClass
]"
></span>
<span
v-if="!hideGear('shield')"
:class="[getGearClass('shield'), specialMountClass]"
></span>
<span
v-if="!hideGear('weapon')"
:class="[getGearClass('weapon'), specialMountClass]"
class="weapon"
></span>
</template>
<!-- Resting-->
<span
v-if="member.preferences.sleep"
class="zzz"
></span>
<template v-if="!avatarOnly">
<!-- Mount Head-->
<span
v-if="member.items.currentMount"
:class="'Mount_Head_' + member.items.currentMount"
></span>
<!-- Pet-->
<span
class="current-pet"
:class="petClass"
></span>
</template>
</div>
<class-badge
v-if="hasClass && !hideClassBadge"
class="under-avatar"
:member-class="member.stats.class"
/>
</div>
</template>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
.avatar {
width: 141px;
image-rendering: pixelated;
position: relative;
cursor: pointer;
&.centered-avatar {
margin: 0 auto;
}
// resetting the additional padding
margin-bottom: -0.5rem !important;
}
.character-sprites {
width: 90px;
height: 90px;
display: inline-flex;
}
.character-sprites span {
position: absolute;
}
.current-pet {
bottom: 0px;
left: 0px;
}
.offset-kangaroo {
margin-top: 24px;
}
.invert {
filter: invert(100%);
}
.debug {
border: 1px solid red;
.character-sprites {
border: 1px solid blue;
}
.weapon {
border: 1px solid green;
}
span {
border: 1px solid yellow;
}
}
</style>
<script>
import moment from 'moment';
import { mapState } from '@/libs/store';
import foolPet from '../mixins/foolPet';
import ClassBadge from '@/components/members/classBadge';
/**
* TODO replace avatarOnly with multiple options like
* - showMount
* - showPet
* - showBackground
* - showWeapons
*/
export default {
components: {
ClassBadge,
},
mixins: [foolPet],
props: {
debugMode: {
type: Boolean,
default: false,
},
member: {
type: Object,
required: true,
},
avatarOnly: {
type: Boolean,
default: false,
},
hideClassBadge: {
type: Boolean,
default: false,
},
withBackground: {
type: Boolean,
},
overrideAvatarGear: {
type: Object,
default (data) {
return data;
},
},
width: {
type: String,
default: '141px',
},
height: {
type: String,
default: '147px',
},
centerAvatar: {
type: Boolean,
default: false,
},
spritesMargin: {
type: String,
default: '0 auto 0 24px',
},
overrideTopPadding: {
type: String,
default: null,
},
showVisualBuffs: {
type: Boolean,
default: true,
},
showWeapon: {
type: Boolean,
default: true,
},
},
computed: {
...mapState({
flatGear: 'content.gear.flat',
currentEventList: 'worldState.data.currentEventList',
}),
hasClass () {
if (!this.member) return false;
return this.$store.getters['members:hasClass'](this.member);
},
isBuffed () {
if (!this.member) return false;
return this.$store.getters['members:isBuffed'](this.member);
},
paddingTop () {
if (this.overrideTopPadding) {
return this.overrideTopPadding;
}
let val = '24px';
if (!this.avatarOnly) {
if (this.member?.items.currentPet) val = '24px';
if (this.member?.items.currentMount) val = '0px';
}
return val;
},
topLevelClassList () {
const classes = [this.backgroundClass];
if (this.debugMode) {
classes.push('debug');
}
if (this.centerAvatar) {
classes.push('centered-avatar');
}
return classes.join(' ');
},
backgroundClass () {
if (this.member) {
const { background } = this.member.preferences;
const allowToShowBackground = !this.avatarOnly || this.withBackground;
if (this.overrideAvatarGear && this.overrideAvatarGear.background) {
return `background_${this.overrideAvatarGear.background}`;
}
if (background && allowToShowBackground) {
return `background_${this.member.preferences.background}`;
}
}
return '';
},
visualBuffs () {
if (!this.member) return {};
return {
snowball: `avatar_snowball_${this.member.stats.class}`,
spookySparkles: 'ghost',
shinySeed: `avatar_floral_${this.member.stats.class}`,
seafoam: 'seafoam_star',
};
},
skinClass () {
if (!this.member) return '';
if (this.overrideAvatarGear?.skin) {
return `skin_${this.overrideAvatarGear.skin}`;
}
const baseClass = `skin_${this.member.preferences.skin}`;
return `${baseClass}${this.member.preferences.sleep ? '_sleep' : ''}`;
},
shirtClass () {
if (!this.member) return '';
if (this.overrideAvatarGear?.shirt) {
return `${this.member.preferences.size}_shirt_${this.overrideAvatarGear.shirt}`;
}
return `${this.member.preferences.size}_shirt_${this.member.preferences.shirt}`;
},
costumeClass () {
return this.member?.preferences.costume ? 'costume' : 'equipped';
},
specialMountClass () {
if (!this.avatarOnly && this.member?.items.currentMount && this.member?.items.currentMount.includes('Kangaroo')) {
return 'offset-kangaroo';
}
return null;
},
petClass () {
const foolEvent = this.currentEventList?.find(event => moment()
.isBetween(event.start, event.end) && event.aprilFools);
if (foolEvent) {
return this.foolPet(this.member.items.currentPet, foolEvent.aprilFools);
}
if (this.member?.items.currentPet) return `Pet-${this.member.items.currentPet}`;
return '';
},
},
methods: {
getGearClass (gearType) {
if (!this.member) return '';
let result = this.member.items.gear[this.costumeClass][gearType];
if (this.overrideAvatarGear && this.overrideAvatarGear[gearType]) {
result = this.overrideAvatarGear[gearType];
}
return result;
},
hairClass (slot) {
if (this.overrideAvatarGear?.hair) {
if (this.overrideAvatarGear.hair[slot]) {
return `hair_${slot}_${this.overrideAvatarGear.hair[slot]}_${this.member.preferences.hair.color}`;
}
if (this.overrideAvatarGear.hair.color) {
return `hair_${slot}_${this.member.preferences.hair[slot]}_${this.overrideAvatarGear.hair.color}`;
}
}
return `hair_${slot}_${this.member.preferences.hair[slot]}_${this.member.preferences.hair.color}`;
},
hideGear (gearType) {
if (!this.member) return true;
if (!this.showWeapon) {
return true;
}
if (gearType === 'weapon') {
const equippedWeapon = this.member.items.gear[this.costumeClass][gearType];
if (!equippedWeapon) {
return false;
}
const equippedIsTwoHanded = this.flatGear[equippedWeapon].twoHanded;
const hasOverrideShield = this.overrideAvatarGear && this.overrideAvatarGear.shield;
return equippedIsTwoHanded && hasOverrideShield;
} if (gearType === 'shield') {
const overrideWeapon = this.overrideAvatarGear && this.overrideAvatarGear.weapon;
const overrideIsTwoHanded = overrideWeapon && this.flatGear[overrideWeapon].twoHanded;
return overrideIsTwoHanded;
}
return false;
},
castEnd (e) {
if (!this.$store.state.spellOptions.castingSpell) return;
this.$root.$emit('castEnd', this.member, 'user', e);
},
showAvatar () {
if (!this.member) return false;
if (!this.showVisualBuffs) return true;
const { buffs } = this.member.stats;
return !buffs.snowball && !buffs.spookySparkles && !buffs.shinySeed && !buffs.seafoam;
},
},
};
</script>