Merge branch 'develop' into sabrecat/teams-rebase

This commit is contained in:
SabreCat
2022-05-17 09:34:51 -05:00
44 changed files with 1410 additions and 899 deletions

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "habitica",
"version": "4.230.0",
"version": "4.230.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.230.0",
"version": "4.230.2",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.17.10",

View File

@@ -42,3 +42,109 @@ describe('xml marshaller marshalls user data', () => {
</user>`);
});
});
describe('xml marshaller marshalls user data (with purchases)', () => {
const minimumUser = {
pinnedItems: [],
unpinnedItems: [],
inbox: {},
};
function userDataWith (fields) {
return { ...minimumUser, ...fields };
}
it('maps the purchases field with data that begins with a number', () => {
const userData = userDataWith({
purchased: {
ads: false,
txnCount: 0,
skin: {
eb052b: true,
'0ff591': true,
'2b43f6': true,
d7a9f7: true,
'800ed0': true,
rainbow: true,
},
},
});
const xml = xmlMarshaller.marshallUserData(userData);
expect(xml).to.equal(`<user>
<inbox/>
<purchased>
<ads>false</ads>
<txnCount>0</txnCount>
<skin>eb052b</skin>
<skin>0ff591</skin>
<skin>2b43f6</skin>
<skin>d7a9f7</skin>
<skin>800ed0</skin>
<skin>rainbow</skin>
</purchased>
</user>`);
});
});
describe('xml marshaller marshalls user data (with purchases nested)', () => {
const minimumUser = {
pinnedItems: [],
unpinnedItems: [],
inbox: {},
};
function userDataWith (fields) {
return { ...minimumUser, ...fields };
}
it('maps the purchases field with data that begins with a number and nested objects', () => {
const userData = userDataWith({
purchased: {
ads: false,
txnCount: 0,
skin: {
eb052b: true,
'0ff591': true,
'2b43f6': true,
d7a9f7: true,
'800ed0': true,
rainbow: true,
},
plan: {
consecutive: {
count: 0,
offset: 0,
gemCapExtra: 0,
trinkets: 0,
},
},
},
});
const xml = xmlMarshaller.marshallUserData(userData);
expect(xml).to.equal(`<user>
<inbox/>
<purchased>
<ads>false</ads>
<txnCount>0</txnCount>
<skin>eb052b</skin>
<skin>0ff591</skin>
<skin>2b43f6</skin>
<skin>d7a9f7</skin>
<skin>800ed0</skin>
<skin>rainbow</skin>
<plan>
<item>
<count>0</count>
<offset>0</offset>
<gemCapExtra>0</gemCapExtra>
<trinkets>0</trinkets>
</item>
</plan>
</purchased>
</user>`);
});
});

View File

@@ -634,7 +634,7 @@ describe('User Model', () => {
user = await user.save();
// verify that it's been awarded
expect(user.achievements.beastMaster).to.equal(true);
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_BEAST_MASTER')).to.exist;
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_STABLE')).to.exist;
// reset the user
user.achievements.beastMasterCount = 0;
@@ -683,9 +683,9 @@ describe('User Model', () => {
user = await user.save();
// verify that it's been awarded
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_BEAST_MASTER')).to.exist;
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_MOUNT_MASTER')).to.exist;
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_TRIAD_BINGO')).to.exist;
expect(user.notifications.find(
notification => notification.type === 'ACHIEVEMENT_STABLE',
)).to.exist;
});
context('manage unallocated stats points notifications', () => {

View File

@@ -0,0 +1,27 @@
<svg width="176" height="67" viewBox="0 0 176 67" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path fill="#77F4C7" d="M35.667 11.667 40 9.5l-4.333-2.167L33.5 3l-2.167 4.333L27 9.5l4.333 2.167L33.5 16z"/>
<path fill="#BDA8FF" d="M24.667 38.667 30 36l-5.333-2.667L22 28l-2.667 5.333L14 36l5.333 2.667L22 44z"/>
<path fill="#8EEDF6" d="M35.667 63.667 39 62l-3.333-1.667L34 57l-1.667 3.333L29 62l3.333 1.667L34 67z"/>
<path fill="#FFBE5D" d="M6.667 49.667 10 48l-3.333-1.667L5 43l-1.667 3.333L0 48l3.333 1.667L5 53z"/>
<path fill="#FFB6B8" d="M5.667 20.667 8 19.5l-2.333-1.167L4.5 16l-1.167 2.333L1 19.5l2.333 1.167L4.5 23z"/>
<g>
<path fill="#77F4C7" d="M140.333 11.667 136 9.5l4.333-2.167L142.5 3l2.167 4.333L149 9.5l-4.333 2.167L142.5 16z"/>
<path fill="#BDA8FF" d="M151.333 38.667 146 36l5.333-2.667L154 28l2.667 5.333L162 36l-5.333 2.667L154 44z"/>
<path fill="#8EEDF6" d="M140.333 63.667 137 62l3.333-1.667L142 57l1.667 3.333L147 62l-3.333 1.667L142 67z"/>
<path fill="#FFBE5D" d="M169.333 49.667 166 48l3.333-1.667L171 43l1.667 3.333L176 48l-3.333 1.667L171 53z"/>
<path fill="#FFB6B8" d="M170.333 20.667 168 19.5l2.333-1.167L171.5 16l1.167 2.333L175 19.5l-2.333 1.167L171.5 23z"/>
</g>
<g>
<path d="M81.117 13.904c-2.139-4.838-6.274-9.113-11.25-9.324-4.976-.211-7.828 3.779-6.367 7.309 1.461 3.53 4.94 4.177 16.227 7.202 3.204.858 3.528-.35 1.39-5.187z" stroke="#22AEB7" stroke-width="4"/>
<path d="M93.833 13.904c2.138-4.838 6.273-9.113 11.25-9.324 4.975-.211 7.828 3.779 6.367 7.309-1.462 3.53-4.94 4.177-16.227 7.202-3.205.858-3.528-.35-1.39-5.187z" stroke="#38C9C6" stroke-width="4"/>
<path d="M87.128 11c-9.738 0-3.907 11.145 0 11.145 3.908 0 9.74-11.145 0-11.145z" fill="#46DDDA"/>
<path fill="#6133B4" d="M62 33h52v34H62zM56 21h64v12H56z"/>
<path fill-opacity=".5" fill="#FFF" style="mix-blend-mode:soft-light" d="M32 30h26v34H32z" transform="translate(56 3)"/>
<path fill="#8EEDF6" d="M88 33h6v34h-6z"/>
<path fill="#3BCAD7" d="M82 33h6v34h-6zM76 21h12v12H76z"/>
<path fill="#8EEDF6" d="M88 21h12v12H88z"/>
<path fill-opacity=".2" fill="#000" style="mix-blend-mode:multiply" d="M6 30h26v6H6zM20 18h12v6H20zM0 24h20v6H0zM44 24h20v6H44zM32 18h12v6H32zM6 58h26v6H6zM32 30h26v6H32zM32 58h26v6H32z" transform="translate(56 3)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,5 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
<g fill="none" fill-rule="evenodd" stroke="#A5A1AC" stroke-width="2">
<path d="M1 11L11 1M11 11L1 1"/>
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon/Close</title>
<g id="Modals" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Shop-Modals" transform="translate(-183.000000, -655.000000)" fill="#878190" fill-rule="nonzero">
<g id="Icon/Close" transform="translate(183.000000, 655.000000)">
<polygon id="Mask" points="12.1973467 2 14 3.80265326 9.80187117 8 14 12.1973467 12.1973467 14 8 9.80187117 3.80265326 14 2 12.1973467 6.19812883 8 2 3.80265326 3.80265326 2 8 6.19812883"></polygon>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 215 B

After

Width:  |  Height:  |  Size: 747 B

View File

@@ -1,60 +0,0 @@
<template>
<b-modal
id="just-add-water"
:title="title"
size="md"
:hide-footer="true"
>
<div class="modal-body">
<div class="col-12">
<achievement-avatar class="avatar" />
</div>
<div class="col-6 offset-3 text-center">
<p>{{ $t('achievementJustAddWaterModalText') }}</p>
<button
class="btn btn-primary"
@click="close()"
>
{{ $t('huzzah') }}
</button>
</div>
</div>
<achievement-footer />
</b-modal>
</template>
<style scoped>
.avatar {
width: 140px;
margin: 0 auto;
margin-bottom: 1.5em;
margin-top: 1.5em;
}
</style>
<script>
import achievementFooter from './achievementFooter';
import achievementAvatar from './achievementAvatar';
import { mapState } from '@/libs/store';
export default {
components: {
achievementFooter,
achievementAvatar,
},
data () {
return {
title: `${this.$t('modalAchievement')} ${this.$t('achievementJustAddWater')}`,
};
},
computed: {
...mapState({ user: 'user.data' }),
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'just-add-water');
},
},
};
</script>

View File

@@ -1,60 +0,0 @@
<template>
<b-modal
id="lost-masterclasser"
:title="title"
size="md"
:hide-footer="true"
>
<div class="modal-body">
<div class="col-12">
<achievement-avatar class="avatar" />
</div>
<div class="col-6 offset-3 text-center">
<p>{{ $t('achievementLostMasterclasserModalText') }}</p>
<button
class="btn btn-primary"
@click="close()"
>
{{ $t('huzzah') }}
</button>
</div>
</div>
<achievement-footer />
</b-modal>
</template>
<style scoped>
.avatar {
width: 140px;
margin: 0 auto;
margin-bottom: 1.5em;
margin-top: 1.5em;
}
</style>
<script>
import achievementFooter from './achievementFooter';
import achievementAvatar from './achievementAvatar';
import { mapState } from '@/libs/store';
export default {
components: {
achievementFooter,
achievementAvatar,
},
data () {
return {
title: `${this.$t('modalAchievement')} ${this.$t('achievementLostMasterclasser')}`,
};
},
computed: {
...mapState({ user: 'user.data' }),
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'lost-masterclasser');
},
},
};
</script>

View File

@@ -1,60 +0,0 @@
<template>
<b-modal
id="mind-over-matter"
:title="title"
size="md"
:hide-footer="true"
>
<div class="modal-body">
<div class="col-12">
<achievement-avatar class="avatar" />
</div>
<div class="col-6 offset-3 text-center">
<p>{{ $t('achievementMindOverMatterModalText') }}</p>
<button
class="btn btn-primary"
@click="close()"
>
{{ $t('huzzah') }}
</button>
</div>
</div>
<achievement-footer />
</b-modal>
</template>
<style scoped>
.avatar {
width: 140px;
margin: 0 auto;
margin-bottom: 1.5em;
margin-top: 1.5em;
}
</style>
<script>
import achievementFooter from './achievementFooter';
import achievementAvatar from './achievementAvatar';
import { mapState } from '@/libs/store';
export default {
components: {
achievementFooter,
achievementAvatar,
},
data () {
return {
title: `${this.$t('modalAchievement')} ${this.$t('achievementMindOverMatter')}`,
};
},
computed: {
...mapState({ user: 'user.data' }),
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'mind-over-matter');
},
},
};
</script>

View File

@@ -3,7 +3,7 @@
<creator-intro />
<profileModal />
<report-flag-modal />
<send-gems-modal />
<send-gift-modal />
<select-user-modal />
<b-navbar
id="habitica-menu"
@@ -747,7 +747,7 @@ import creatorIntro from '../creatorIntro';
import notificationMenu from './notificationsDropdown';
import profileModal from '../userMenu/profileModal';
import reportFlagModal from '../chat/reportFlagModal';
import sendGemsModal from '@/components/payments/sendGemsModal';
import sendGiftModal from '@/components/payments/sendGiftModal';
import selectUserModal from '@/components/payments/selectUserModal';
import sync from '@/mixins/sync';
import userDropdown from './userDropdown';
@@ -759,7 +759,7 @@ export default {
notificationMenu,
profileModal,
reportFlagModal,
sendGemsModal,
sendGiftModal,
selectUserModal,
userDropdown,
},

View File

@@ -1,34 +0,0 @@
<template>
<base-notification
:can-remove="canRemove"
:notification="notification"
:read-after-click="true"
@click="action"
>
<div
slot="content"
v-html="achievementString"
></div>
</base-notification>
</template>
<script>
import BaseNotification from './base';
export default {
components: {
BaseNotification,
},
props: ['notification', 'canRemove'],
computed: {
achievementString () {
return `<strong>${this.$t('achievement')}</strong>: ${this.$t('achievementJustAddWater')}`;
},
},
methods: {
action () {
this.$root.$emit('bv::show::modal', 'just-add-water');
},
},
};
</script>

View File

@@ -1,34 +0,0 @@
<template>
<base-notification
:can-remove="canRemove"
:notification="notification"
:read-after-click="true"
@click="action"
>
<div
slot="content"
v-html="achievementString"
></div>
</base-notification>
</template>
<script>
import BaseNotification from './base';
export default {
components: {
BaseNotification,
},
props: ['notification', 'canRemove'],
computed: {
achievementString () {
return `<strong>${this.$t('achievement')}</strong>: ${this.$t('achievementLostMasterclasser')}`;
},
},
methods: {
action () {
this.$root.$emit('bv::show::modal', 'lost-masterclasser');
},
},
};
</script>

View File

@@ -1,34 +0,0 @@
<template>
<base-notification
:can-remove="canRemove"
:notification="notification"
:read-after-click="true"
@click="action"
>
<div
slot="content"
v-html="achievementString"
></div>
</base-notification>
</template>
<script>
import BaseNotification from './base';
export default {
components: {
BaseNotification,
},
props: ['notification', 'canRemove'],
computed: {
achievementString () {
return `<strong>${this.$t('achievement')}</strong>: ${this.$t('achievementMindOverMatter')}`;
},
},
methods: {
action () {
this.$root.$emit('bv::show::modal', 'mind-over-matter');
},
},
};
</script>

View File

@@ -140,9 +140,6 @@ import NEW_INBOX_MESSAGE from './notifications/newPrivateMessage';
import NEW_CHAT_MESSAGE from './notifications/newChatMessage';
import WORLD_BOSS from './notifications/worldBoss';
import VERIFY_USERNAME from './notifications/verifyUsername';
import ACHIEVEMENT_JUST_ADD_WATER from './notifications/justAddWater';
import ACHIEVEMENT_LOST_MASTERCLASSER from './notifications/lostMasterclasser';
import ACHIEVEMENT_MIND_OVER_MATTER from './notifications/mindOverMatter';
import ONBOARDING_COMPLETE from './notifications/onboardingComplete';
import GIFT_ONE_GET_ONE from './notifications/g1g1';
import OnboardingGuide from './onboardingGuide';
@@ -167,9 +164,6 @@ export default {
CARD_RECEIVED,
NEW_INBOX_MESSAGE,
NEW_CHAT_MESSAGE,
ACHIEVEMENT_JUST_ADD_WATER,
ACHIEVEMENT_LOST_MASTERCLASSER,
ACHIEVEMENT_MIND_OVER_MATTER,
WorldBoss: WORLD_BOSS,
VERIFY_USERNAME,
OnboardingGuide,
@@ -194,13 +188,24 @@ export default {
// listed in the order they should appear in the notifications panel.
// NOTE: Those not listed here won't be shown in the notification panel!
handledNotifications: [
'NEW_STUFF', 'GIFT_ONE_GET_ONE', 'GROUP_TASK_NEEDS_WORK',
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
'QUEST_INVITATION', 'GROUP_TASK_ASSIGNED', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED',
'GROUP_TASK_CLAIMED', 'NEW_MYSTERY_ITEMS', 'CARD_RECEIVED',
'NEW_INBOX_MESSAGE', 'NEW_CHAT_MESSAGE', 'UNALLOCATED_STATS_POINTS',
'ACHIEVEMENT_JUST_ADD_WATER', 'ACHIEVEMENT_LOST_MASTERCLASSER', 'ACHIEVEMENT_MIND_OVER_MATTER',
'VERIFY_USERNAME', 'ONBOARDING_COMPLETE',
'NEW_STUFF',
'GIFT_ONE_GET_ONE',
'GROUP_TASK_NEEDS_WORK',
'GUILD_INVITATION',
'PARTY_INVITATION',
'CHALLENGE_INVITATION',
'QUEST_INVITATION',
'GROUP_TASK_ASSIGNED',
'GROUP_TASK_APPROVAL',
'GROUP_TASK_APPROVED',
'GROUP_TASK_CLAIMED',
'NEW_MYSTERY_ITEMS',
'CARD_RECEIVED',
'NEW_INBOX_MESSAGE',
'NEW_CHAT_MESSAGE',
'UNALLOCATED_STATS_POINTS',
'VERIFY_USERNAME',
'ONBOARDING_COMPLETE',
],
};
},

View File

@@ -30,9 +30,6 @@
v-if="notificationData && notificationData.achievement"
:data="notificationData"
/>
<just-add-water />
<lost-masterclasser />
<mind-over-matter />
<onboarding-complete />
<first-drops />
</div>
@@ -141,25 +138,34 @@ import streak from './achievements/streak';
import ultimateGear from './achievements/ultimateGear';
import wonChallenge from './achievements/wonChallenge';
import genericAchievement from './achievements/genericAchievement';
import justAddWater from './achievements/justAddWater';
import lostMasterclasser from './achievements/lostMasterclasser';
import mindOverMatter from './achievements/mindOverMatter';
import loginIncentives from './achievements/login-incentives';
import onboardingComplete from './achievements/onboardingComplete';
import verifyUsername from './settings/verifyUsername';
import firstDrops from './achievements/firstDrops';
const NOTIFICATIONS = {
// general notifications
NEW_CONTRIBUTOR_LEVEL: {
achievement: true,
label: $t => $t('modalContribAchievement'),
modalId: 'contributor',
sticky: true,
},
// achievement notifications
ACHIEVEMENT: { // null data filled in handleUserNotifications
achievement: true,
modalId: 'generic-achievement',
label: null,
data: {
message: $t => $t('achievement'),
modalText: null,
},
},
CHALLENGE_JOINED_ACHIEVEMENT: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('joinedChallenge')}`,
modalId: 'joined-challenge',
},
ULTIMATE_GEAR_ACHIEVEMENT: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('gearAchievementNotification')}`,
modalId: 'ultimate-gear',
},
GUILD_JOINED_ACHIEVEMENT: {
label: $t => `${$t('achievement')}: ${$t('joinedGuild')}`,
achievement: true,
@@ -170,42 +176,14 @@ const NOTIFICATIONS = {
label: $t => `${$t('achievement')}: ${$t('invitedFriend')}`,
modalId: 'invited-friend',
},
NEW_CONTRIBUTOR_LEVEL: {
ACHIEVEMENT_PARTY_ON: {
achievement: true,
label: $t => $t('modalContribAchievement'),
modalId: 'contributor',
sticky: true,
},
ACHIEVEMENT_ALL_YOUR_BASE: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementAllYourBase')}`,
label: $t => `${$t('achievement')}: ${$t('achievementPartyOn')}`,
modalId: 'generic-achievement',
data: {
achievement: 'allYourBase', // defined manually until the server sends all the necessary data
},
},
ACHIEVEMENT_BACK_TO_BASICS: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementBackToBasics')}`,
modalId: 'generic-achievement',
data: {
achievement: 'backToBasics',
},
},
ACHIEVEMENT_DUST_DEVIL: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementDustDevil')}`,
modalId: 'generic-achievement',
data: {
achievement: 'dustDevil',
},
},
ACHIEVEMENT_ARID_AUTHORITY: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementAridAuthority')}`,
modalId: 'generic-achievement',
data: {
achievement: 'aridAuthority',
message: $t => $t('achievement'),
modalText: $t => $t('achievementPartyOn'),
achievement: 'partyOn',
},
},
ACHIEVEMENT_PARTY_UP: {
@@ -218,245 +196,47 @@ const NOTIFICATIONS = {
achievement: 'partyUp',
},
},
ACHIEVEMENT_PARTY_ON: {
ULTIMATE_GEAR_ACHIEVEMENT: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementPartyOn')}`,
modalId: 'generic-achievement',
data: {
message: $t => $t('achievement'),
modalText: $t => $t('achievementPartyOn'),
achievement: 'partyOn',
label: $t => `${$t('achievement')}: ${$t('gearAchievementNotification')}`,
modalId: 'ultimate-gear',
},
},
ACHIEVEMENT_BEAST_MASTER: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('beastAchievement')}`,
modalId: 'generic-achievement',
data: {
message: $t => $t('achievement'),
modalText: $t => $t('beastAchievement'),
achievement: 'beastMaster',
},
},
ACHIEVEMENT_MOUNT_MASTER: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('mountAchievement')}`,
modalId: 'generic-achievement',
data: {
message: $t => $t('achievement'),
modalText: $t => $t('mountAchievement'),
achievement: 'mountMaster',
},
},
ACHIEVEMENT_TRIAD_BINGO: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('triadBingoAchievement')}`,
modalId: 'generic-achievement',
data: {
message: $t => $t('achievement'),
modalText: $t => $t('triadBingoAchievement'),
achievement: 'triadBingo',
},
},
ACHIEVEMENT_MONSTER_MAGUS: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementMonsterMagus')}`,
modalId: 'generic-achievement',
data: {
achievement: 'monsterMagus',
},
},
ACHIEVEMENT_UNDEAD_UNDERTAKER: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementUndeadUndertaker')}`,
modalId: 'generic-achievement',
data: {
achievement: 'undeadUndertaker',
},
},
ACHIEVEMENT: { // data filled in handleUserNotifications
ACHIEVEMENT_STABLE: {
achievement: true,
modalId: 'generic-achievement',
label: null, // data filled in handleUserNotifications
data: {
message: $t => $t('achievement'),
modalText: null, // data filled in handleUserNotifications
achievement: 'stableAchievs',
},
},
ACHIEVEMENT_PRIMED_FOR_PAINTING: {
ACHIEVEMENT_QUESTS: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementPrimedForPainting')}`,
modalId: 'generic-achievement',
data: {
achievement: 'primedForPainting',
achievement: 'questSeriesAchievs',
},
},
ACHIEVEMENT_PEARLY_PRO: {
ACHIEVEMENT_ANIMAL_SET: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementPearlyPro')}`,
label: $t => `${$t('achievement')}: ${$t('achievementAnimalSet')}`,
modalId: 'generic-achievement',
data: {
achievement: 'pearlyPro',
achievement: 'animalSetAchievs',
},
},
ACHIEVEMENT_TICKLED_PINK: {
ACHIEVEMENT_PET_COLOR: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementTickledPink')}`,
label: $t => `${$t('achievement')}: ${$t('achievementPetColor')}`,
modalId: 'generic-achievement',
data: {
achievement: 'tickledPink',
achievement: 'petColorAchievs',
},
},
ACHIEVEMENT_ROSY_OUTLOOK: {
ACHIEVEMENT_MOUNT_COLOR: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementRosyOutlook')}`,
label: $t => `${$t('achievement')}: ${$t('achievementMountColor')}`,
modalId: 'generic-achievement',
data: {
achievement: 'rosyOutlook',
},
},
ACHIEVEMENT_BUG_BONANZA: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementBugBonanza')}`,
modalId: 'generic-achievement',
data: {
achievement: 'bugBonanza',
},
},
ACHIEVEMENT_BARE_NECESSITIES: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementBareNecessities')}`,
modalId: 'generic-achievement',
data: {
achievement: 'bareNecessities',
},
},
ACHIEVEMENT_FRESHWATER_FRIENDS: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementFreshwaterFriends')}`,
modalId: 'generic-achievement',
data: {
achievement: 'freshwaterFriends',
},
},
ACHIEVEMENT_GOOD_AS_GOLD: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementGoodAsGold')}`,
modalId: 'generic-achievement',
data: {
achievement: 'goodAsGold',
},
},
ACHIEVEMENT_ALL_THAT_GLITTERS: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementAllThatGlitters')}`,
modalId: 'generic-achievement',
data: {
achievement: 'allThatGlitters',
},
},
ACHIEVEMENT_BONE_COLLECTOR: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementBoneCollector')}`,
modalId: 'generic-achievement',
data: {
achievement: 'boneCollector',
},
},
ACHIEVEMENT_SKELETON_CREW: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementSkeletonCrew')}`,
modalId: 'generic-achievement',
data: {
achievement: 'skeletonCrew',
},
},
ACHIEVEMENT_SEEING_RED: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementSeeingRed')}`,
modalId: 'generic-achievement',
data: {
achievement: 'seeingRed',
},
},
ACHIEVEMENT_RED_LETTER_DAY: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementRedLetterDay')}`,
modalId: 'generic-achievement',
data: {
achievement: 'redLetterDay',
},
},
ACHIEVEMENT_LEGENDARY_BESTIARY: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementLegendaryBestiary')}`,
modalId: 'generic-achievement',
data: {
achievement: 'legendaryBestiary',
},
},
ACHIEVEMENT_SEASONAL_SPECIALIST: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementSeasonalSpecialist')}`,
modalId: 'generic-achievement',
data: {
achievement: 'seasonalSpecialist',
},
},
ACHIEVEMENT_VIOLETS_ARE_BLUE: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementVioletsAreBlue')}`,
modalId: 'generic-achievement',
data: {
achievement: 'violetsAreBlue',
},
},
ACHIEVEMENT_WILD_BLUE_YONDER: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementWildBlueYonder')}`,
modalId: 'generic-achievement',
data: {
achievement: 'wildBlueYonder',
},
},
ACHIEVEMENT_DOMESTICATED: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementDomesticated')}`,
modalId: 'generic-achievement',
data: {
achievement: 'domesticated',
},
},
ACHIEVEMENT_SHADY_CUSTOMER: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementShadyCustomer')}`,
modalId: 'generic-achievement',
data: {
achievement: 'shadyCustomer',
},
},
ACHIEVEMENT_SHADE_OF_IT_ALL: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementShadeOfItAll')}`,
modalId: 'generic-achievement',
data: {
achievement: 'shadeOfItAll',
},
},
ACHIEVEMENT_ZODIAC_ZOOKEEPER: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementZodiacZookeeper')}`,
modalId: 'generic-achievement',
data: {
achievement: 'zodiacZookeeper',
},
},
ACHIEVEMENT_BIRDS_OF_A_FEATHER: {
achievement: true,
label: $t => `${$t('achievement')}: ${$t('achievementBirdsOfAFeather')}`,
modalId: 'generic-achievement',
data: {
achievement: 'birdsOfAFeather',
achievement: 'mountColorAchievs',
},
},
};
@@ -486,9 +266,6 @@ export default {
loginIncentives,
verifyUsername,
genericAchievement,
lostMasterclasser,
mindOverMatter,
justAddWater,
onboardingComplete,
firstDrops,
},
@@ -509,21 +286,30 @@ export default {
const handledNotifications = {};
[
'GUILD_PROMPT', 'REBIRTH_ENABLED', 'WON_CHALLENGE', 'STREAK_ACHIEVEMENT',
'ULTIMATE_GEAR_ACHIEVEMENT', 'REBIRTH_ACHIEVEMENT', 'GUILD_JOINED_ACHIEVEMENT',
'CHALLENGE_JOINED_ACHIEVEMENT', 'INVITED_FRIEND_ACHIEVEMENT', 'NEW_CONTRIBUTOR_LEVEL',
'CRON', 'LOGIN_INCENTIVE', 'ACHIEVEMENT_ALL_YOUR_BASE', 'ACHIEVEMENT_BACK_TO_BASICS',
'GENERIC_ACHIEVEMENT', 'ACHIEVEMENT_PARTY_UP', 'ACHIEVEMENT_PARTY_ON', 'ACHIEVEMENT_BEAST_MASTER',
'ACHIEVEMENT_MOUNT_MASTER', 'ACHIEVEMENT_TRIAD_BINGO', 'ACHIEVEMENT_DUST_DEVIL', 'ACHIEVEMENT_ARID_AUTHORITY',
'ACHIEVEMENT_MONSTER_MAGUS', 'ACHIEVEMENT_UNDEAD_UNDERTAKER', 'ACHIEVEMENT_PRIMED_FOR_PAINTING',
'ACHIEVEMENT_PEARLY_PRO', 'ACHIEVEMENT_TICKLED_PINK', 'ACHIEVEMENT_ROSY_OUTLOOK', 'ACHIEVEMENT',
'ONBOARDING_COMPLETE', 'FIRST_DROPS', 'ACHIEVEMENT_BUG_BONANZA', 'ACHIEVEMENT_BARE_NECESSITIES',
'ACHIEVEMENT_FRESHWATER_FRIENDS', 'ACHIEVEMENT_GOOD_AS_GOLD', 'ACHIEVEMENT_ALL_THAT_GLITTERS',
'ACHIEVEMENT_BONE_COLLECTOR', 'ACHIEVEMENT_SKELETON_CREW', 'ACHIEVEMENT_SEEING_RED',
'ACHIEVEMENT_RED_LETTER_DAY', 'ACHIEVEMENT_LEGENDARY_BESTIARY', 'ACHIEVEMENT_SEASONAL_SPECIALIST',
'ACHIEVEMENT_VIOLETS_ARE_BLUE', 'ACHIEVEMENT_WILD_BLUE_YONDER', 'ACHIEVEMENT_DOMESTICATED',
'ACHIEVEMENT_SHADY_CUSTOMER', 'ACHIEVEMENT_SHADE_OF_IT_ALL', 'ACHIEVEMENT_ZODIAC_ZOOKEEPER',
'ACHIEVEMENT_BIRDS_OF_A_FEATHER',
// general notifications
'CRON',
'FIRST_DROPS',
'GUILD_PROMPT',
'LOGIN_INCENTIVE',
'NEW_CONTRIBUTOR_LEVEL',
'ONBOARDING_COMPLETE',
'REBIRTH_ENABLED',
'WON_CHALLENGE',
// achievement notifications
'ACHIEVEMENT',
'CHALLENGE_JOINED_ACHIEVEMENT',
'GUILD_JOINED_ACHIEVEMENT',
'INVITED_FRIEND_ACHIEVEMENT',
'ACHIEVEMENT_PARTY_ON',
'ACHIEVEMENT_PARTY_UP',
'REBIRTH_ACHIEVEMENT',
'STREAK_ACHIEVEMENT',
'ULTIMATE_GEAR_ACHIEVEMENT',
'ACHIEVEMENT_STABLE',
'ACHIEVEMENT_QUESTS',
'ACHIEVEMENT_ANIMAL_SET',
'ACHIEVEMENT_PET_COLOR',
'ACHIEVEMENT_MOUNT_COLOR',
].forEach(type => {
handledNotifications[type] = true;
});
@@ -921,57 +707,68 @@ export default {
case 'WON_CHALLENGE':
this.$root.$emit('habitica:won-challenge', notification);
break;
case 'REBIRTH_ACHIEVEMENT':
this.playSound('Achievement_Unlocked');
this.$root.$emit('bv::show::modal', 'rebirth');
break;
case 'STREAK_ACHIEVEMENT':
this.text(`${this.$t('streaks')}: ${this.user.achievements.streak}`, () => {
this.$root.$emit('bv::show::modal', 'streak');
}, this.user.preferences.suppressModals.streak);
this.playSound('Achievement_Unlocked');
break;
case 'REBIRTH_ACHIEVEMENT':
this.playSound('Achievement_Unlocked');
this.$root.$emit('bv::show::modal', 'rebirth');
break;
case 'ULTIMATE_GEAR_ACHIEVEMENT':
case 'GUILD_JOINED_ACHIEVEMENT':
case 'CHALLENGE_JOINED_ACHIEVEMENT':
case 'INVITED_FRIEND_ACHIEVEMENT':
case 'NEW_CONTRIBUTOR_LEVEL':
case 'ACHIEVEMENT_ALL_YOUR_BASE':
case 'ACHIEVEMENT_BACK_TO_BASICS':
case 'ACHIEVEMENT_DUST_DEVIL':
case 'ACHIEVEMENT_ARID_AUTHORITY':
case 'ACHIEVEMENT_PARTY_UP':
case 'CHALLENGE_JOINED_ACHIEVEMENT':
case 'GUILD_JOINED_ACHIEVEMENT':
case 'INVITED_FRIEND_ACHIEVEMENT':
case 'ACHIEVEMENT_PARTY_ON':
case 'ACHIEVEMENT_BEAST_MASTER':
case 'ACHIEVEMENT_MOUNT_MASTER':
case 'ACHIEVEMENT_TRIAD_BINGO':
case 'ACHIEVEMENT_MONSTER_MAGUS':
case 'ACHIEVEMENT_UNDEAD_UNDERTAKER':
case 'ACHIEVEMENT_PRIMED_FOR_PAINTING':
case 'ACHIEVEMENT_PEARLY_PRO':
case 'ACHIEVEMENT_TICKLED_PINK':
case 'ACHIEVEMENT_ROSY_OUTLOOK':
case 'ACHIEVEMENT_BUG_BONANZA':
case 'ACHIEVEMENT_BARE_NECESSITIES':
case 'ACHIEVEMENT_FRESHWATER_FRIENDS':
case 'ACHIEVEMENT_GOOD_AS_GOLD':
case 'ACHIEVEMENT_ALL_THAT_GLITTERS':
case 'ACHIEVEMENT_BONE_COLLECTOR':
case 'ACHIEVEMENT_SKELETON_CREW':
case 'ACHIEVEMENT_SEEING_RED':
case 'ACHIEVEMENT_RED_LETTER_DAY':
case 'ACHIEVEMENT_LEGENDARY_BESTIARY':
case 'ACHIEVEMENT_SEASONAL_SPECIALIST':
case 'ACHIEVEMENT_VIOLETS_ARE_BLUE':
case 'ACHIEVEMENT_WILD_BLUE_YONDER':
case 'ACHIEVEMENT_DOMESTICATED':
case 'ACHIEVEMENT_SHADY_CUSTOMER':
case 'ACHIEVEMENT_SHADE_OF_IT_ALL':
case 'ACHIEVEMENT_ZODIAC_ZOOKEEPER':
case 'ACHIEVEMENT_BIRDS_OF_A_FEATHER':
case 'GENERIC_ACHIEVEMENT':
case 'ACHIEVEMENT_PARTY_UP':
case 'ULTIMATE_GEAR_ACHIEVEMENT':
this.showNotificationWithModal(notification);
break;
case 'ACHIEVEMENT_QUESTS': {
const { achievement } = notification.data;
const upperCaseAchievement = achievement.charAt(0).toUpperCase() + achievement.slice(1);
const achievementTitleKey = `achievement${upperCaseAchievement}`;
NOTIFICATIONS.ACHIEVEMENT_QUESTS.label = $t => `${$t('achievement')}: ${$t(achievementTitleKey)}`;
this.showNotificationWithModal(notification);
Vue.set(this.user.achievements, achievement, true);
break;
}
case 'ACHIEVEMENT_STABLE': {
const { achievement, achievementNotification } = notification.data;
NOTIFICATIONS.ACHIEVEMENT_STABLE.label = $t => `${$t('achievement')}: ${$t(achievementNotification)}`;
this.showNotificationWithModal(notification);
Vue.set(this.user.achievements, achievement, true);
break;
}
case 'ACHIEVEMENT_ANIMAL_SET': {
const { achievement } = notification.data;
const upperCaseAchievement = achievement.charAt(0).toUpperCase() + achievement.slice(1);
const achievementTitleKey = `achievement${upperCaseAchievement}`;
NOTIFICATIONS.ACHIEVEMENT_ANIMAL_SET.label = $t => `${$t('achievement')}: ${$t(achievementTitleKey)}`;
this.showNotificationWithModal(notification);
Vue.set(this.user.achievements, achievement, true);
break;
}
case 'ACHIEVEMENT_PET_COLOR': {
const { achievement } = notification.data;
const upperCaseAchievement = achievement.charAt(0).toUpperCase() + achievement.slice(1);
const achievementTitleKey = `achievement${upperCaseAchievement}`;
NOTIFICATIONS.ACHIEVEMENT_PET_COLOR.label = $t => `${$t('achievement')}: ${$t(achievementTitleKey)}`;
this.showNotificationWithModal(notification);
Vue.set(this.user.achievements, achievement, true);
break;
}
case 'ACHIEVEMENT_MOUNT_COLOR': {
const { achievement } = notification.data;
const upperCaseAchievement = achievement.charAt(0).toUpperCase() + achievement.slice(1);
const achievementTitleKey = `achievement${upperCaseAchievement}`;
NOTIFICATIONS.ACHIEVEMENT_MOUNT_COLOR.label = $t => `${$t('achievement')}: ${$t(achievementTitleKey)}`;
this.showNotificationWithModal(notification);
Vue.set(this.user.achievements, achievement, true);
break;
}
case 'ACHIEVEMENT': { // generic achievement
const { achievement } = notification.data;
const upperCaseAchievement = achievement.charAt(0).toUpperCase() + achievement.slice(1);
@@ -984,10 +781,6 @@ export default {
Vue.set(this.user.achievements, achievement, true);
break;
}
case 'CRON':
// Not needed because it's shown already by the userHp and userMp watchers
// Keeping an empty block so that it gets read
break;
case 'LOGIN_INCENTIVE':
if (this.user.flags.tour.intro === this.TOUR_END && this.user.flags.welcomed) {
this.notificationData = notification.data;

View File

@@ -1,5 +1,6 @@
<template>
<div class="payments-column mx-auto mt-auto">
<h4>{{ $t('choosePaymentMethod') }}</h4>
<button
v-if="stripeAvailable"
class="btn btn-primary payment-button payment-item with-icon"
@@ -80,6 +81,13 @@
cursor: default !important;
}
}
h4 {
font-size: 0.875rem;
font-weight: bold;
text-align: center;
margin-bottom: 1rem;
}
</style>
<script>

View File

@@ -14,15 +14,44 @@
</div>
<h2
v-else
class="ml-2"
class="d-flex flex-column mx-auto align-items-center"
>
{{ $t('sendGift') }}
{{ $t('sendAGift') }}
</h2>
<div
v-if="currentEvent && currentEvent.promo === 'g1g1'"
class="g1g1-margin d-flex flex-column align-items-center"
>
<div
class="svg-big-gift"
v-once
v-html="icons.bigGift"
></div>
</div>
<div
v-else
class="d-flex flex-column align-items-center">
<div
class="svg-big-gift"
v-once
v-html="icons.bigGift"
></div>
</div>
<div class="d-flex flex-column align-items-center">
<div
class="modal-close"
v-if="currentEvent && currentEvent.promo === 'g1g1'"
class="g1g1-modal-close"
@click="close()"
>
<div
class="g1g1-svg-icon"
v-html="icons.close"
></div>
</div>
<div
v-else
class="modal-close"
@click="close()">
<div
class="svg-icon"
v-html="icons.close"
@@ -42,6 +71,7 @@
v-model="userSearchTerm"
class="form-control"
type="text"
ref="textBox"
:placeholder="$t('usernameOrUserId')"
:class="{
'input-valid': foundUser._id,
@@ -70,15 +100,20 @@
<div
v-else
>
{{ $t('selectGift') }}
{{ $t('next') }}
</div>
</button>
<a
class="cancel-link mx-auto mt-3"
<div
v-if="currentEvent && currentEvent.promo ==='g1g1'"
class="g1g1-cancel d-flex justify-content-center"
v-html="$t('cancel')"
@click="close()"
>
{{ $t('cancel') }}
</a>
</div>
<div
v-else>
</div>
</div>
</div>
</div>
@@ -110,13 +145,16 @@
@import '~@/assets/scss/mixins.scss';
#select-user-modal {
.modal-content {
width:448px;
}
.input-group {
margin-top: 0rem;
}
.modal-dialog {
width: 29.5rem;
margin-top: 25vh;
width: 448px;
}
.modal-footer {
@@ -126,7 +164,16 @@
margin: 0rem 0.25rem 0.25rem 0.25rem;
}
}
body.modal-open .modal {
display: flex !important;
height: 100%;
}
body.modal-open .modal .modal-dialog {
margin: auto;
}
}
</style>
<style lang="scss" scoped>
@@ -146,12 +193,12 @@
.g1g1 {
background-image: url('~@/assets/images/g1g1-send.png');
background-size: 472px 152px;
width: 470px;
background-size: 446px 152px;
width: 446px;
height: 152px;
margin: -1rem 0rem 0rem -1rem;
border-radius: 0.3rem 0.3rem 0rem 0rem;
padding: 1.5rem;
margin: -16px 0px 0px -16px;
border-radius: 4.8px 4.8px 0px 0px;
padding: 24px;
color: $white;
h1 {
@@ -169,6 +216,16 @@
}
}
.g1g1-margin {
margin-top: 24px;
}
.g1g1-cancel {
margin-top: 16px;
color: $blue-10;
cursor: pointer;
}
.g1g1-fine-print {
color: $gray-100;
background-color: $gray-700;
@@ -176,6 +233,29 @@
line-height: 1.33;
}
.g1g1-modal-close {
position: absolute;
width: 18px;
height: 18px;
padding: 4px;
right: 16px;
top: 16px;
cursor: pointer;
.g1g1-svg-icon {
width: 12px;
height: 12px;
& ::v-deep svg path {
fill: #FFFFFF;
}
}
}
.g1g1-modal-dialog {
margin-top: 10vh;
}
.input-error {
color: $red-50;
font-size: 90%;
@@ -192,6 +272,18 @@
border-color: $purple-500;
}
h2 {
font-size: 1.25rem;
line-height: 1.75rem;
color: $purple-300;
padding-top: 1rem;
}
.svg-big-gift {
width: 176px;
height: 64px;
}
.modal-close {
position: absolute;
width: 18px;
@@ -206,14 +298,17 @@
height: 12px;
}
}
</style>
<script>
// import { nextTick } from 'vue'; // may not need this? I don't know!
import debounce from 'lodash/debounce';
import find from 'lodash/find';
import isUUID from 'validator/lib/isUUID';
import { mapState } from '@/libs/store';
import closeIcon from '@/assets/svg/close.svg';
import bigGiftIcon from '@/assets/svg/big-gift.svg';
export default {
data () {
@@ -223,6 +318,7 @@ export default {
foundUser: {},
icons: Object.freeze({
close: closeIcon,
bigGift: bigGiftIcon,
}),
};
},
@@ -281,7 +377,7 @@ export default {
this.foundUser = result;
}, 500),
selectUser () {
this.$root.$emit('habitica::send-gems', this.foundUser);
this.$root.$emit('habitica::send-gift', this.foundUser);
this.close();
},
onHide () {

View File

@@ -0,0 +1,634 @@
<template>
<b-modal
id="send-gift"
:hide-footer="true"
:hide-header="true"
size="md"
@hide="onHide()"
>
<div>
<!-- header -->
<div
class="modal-close"
@click="close()"
>
<div
class="icon-close"
v-html="icons.closeIcon"
>
</div>
</div>
<div>
<h2 class="d-flex flex-column mx-auto align-items-center">
{{ $t('sendAGift') }}
</h2>
</div>
<!-- user avatar -->
<div
v-if="userReceivingGift"
class="modal-body"
>
<avatar
:member="userReceivingGift"
:hideClassBadge="true"
class="d-flex flex-column mx-auto align-items-center"
/>
<div class="avatar-spacer"></div>
<div class="d-flex flex-column mx-auto align-items-center display-name">
<!-- user display name and username -->
<user-link
:user-id="displayName"
:name="displayName"
:backer="userBacker"
:contributor="userContributor"
:class="display-name"
/>
</div>
<div class="d-flex flex-column mx-auto align-items-center user-name">
@{{ userName }}
</div>
</div>
<!-- menu area -->
<div class="row">
<div class="col-12 col-md-8 offset-md-2 text-center nav">
<div
class="nav-item"
:class="{active: selectedPage === 'subscription'}"
@click="selectPage('subscription')"
>
{{ $t('subscription') }}
</div>
<div
class="nav-item"
:class="{active: selectedPage !== 'subscription'}"
@click="selectPage('buyGems')"
>
{{ $t('gems') }}
</div>
</div>
</div>
<!-- subscriber block -->
<subscription-options
v-show="selectedPage === 'subscription'"
class="subscribe-option"
/>
<!-- gem block -->
<div
v-show="selectedPage === 'buyGems'"
>
<div class="gem-group">
<!-- buy gems with money -->
<label v-once>
{{ $t('howManyGemsPurchase') }}
</label>
<div class="d-flex flex-row align-items-center justify-content-center">
<div
class="gray-circle"
@click="gift.gems.amount <= 0 ? gift.gems.amount = 0 : gift.gems.amount--"
>
<div
class="icon-negative"
v-html="icons.negativeIcon"
></div>
</div>
<div class="input-group">
<div class="input-group-prepend input-group-icon align-items-center">
<div
class="icon-gem"
v-html="icons.gemIcon"
></div>
</div>
<input
id="gemsForm"
v-model.number="gift.gems.amount"
class="form-control"
min="0"
max="9999"
>
</div>
<div
class="gray-circle"
@click="gift.gems.amount++"
>
<div
class="icon-positive"
v-html="icons.positiveIcon"
></div>
</div>
</div>
<!-- the word "total" -->
<div class="buy-gem-total">
{{ $t('sendGiftTotal') }}
</div>
<!-- the actual dollar amount -->
<div class="buy-gem-amount">
<span>
{{formatter.format(totalGems)}}
</span>
</div>
<!-- change to sending own gems page -->
<div
:class="{active: selectedPage === 'ownGems'}"
@click="selectPage('ownGems')"
class="gem-state-change"
>
{{ $t('wantToSendOwnGems') }}
</div>
</div>
<!-- paying for gems -->
<payments-buttons
class="payment-buttons"
:stripe-fn="() => redirectToStripe({gift, uuid: userReceivingGift._id, receiverName})"
:paypal-fn="() => openPaypalGift({
gift: gift, giftedTo: userReceivingGift._id, receiverName,
})"
:amazon-data="{type: 'single', gift, giftedTo: userReceivingGift._id, receiverName}"
/>
</div>
<!-- send gems from balance -->
<div
v-show="selectedPage === 'ownGems'"
>
<div class="gem-group">
<label v-once>
{{ $t('howManyGemsSend') }}
</label>
<div class="d-flex align-items-center justify-content-center">
<div
class="gray-circle"
@click="gift.gems.amount <= 0 ? gift.gems.amount = 0 : gift.gems.amount--"
>
<div
class="icon-negative"
v-html="icons.negativeIcon"
></div>
</div>
<div class="input-group">
<div class="input-group-prepend input-group-icon align-items-center">
<div
class="icon-gem"
v-html="icons.gemIcon"
></div>
</div>
<input
id="gemsForm"
v-model="gift.gems.amount"
class="form-control"
min="0"
:max="maxGems"
>
</div>
<div
class="gray-circle"
@click="gift.gems.amount < maxGems ? gift.gems.amount++ : gift.gems.amount = maxGems"
>
<div
class="icon-positive"
v-html="icons.positiveIcon"
></div>
</div>
</div>
<div class="align-items-middle">
<div class="d-flex justify-content-center align-items-middle">
<span class="balance-text">
{{ $t('yourBalance') }}
</span>
<span
class="icon-gem balance-gem-margin"
v-html="icons.gemIcon"
style="display: inline-block;"
></span>
<span
class="balance-gems">
{{ maxGems }}
</span>
</div>
</div>
<div class="d-flex flex-column justify-content-center align-items-middle mt-3">
<button
v-if="fromBal"
class="btn btn-primary mx-auto mt-2"
type="submit"
:disabled="sendingInProgress"
@click="sendGift()"
>
{{ $t("send") }}
</button>
</div>
<!-- change to buying gems page -->
<div
:class="{active: selectedPage === 'buyGems'}"
@click="selectPage('buyGems')"
class="gem-state-change"
>
{{ $t('needToPurchaseGems') }}
</div>
</div>
</div>
</div>
</b-modal>
</template>
<style lang="scss">
@import '~@/assets/scss/mixins.scss';
#send-gift {
.modal-dialog {
max-width: 448px;
}
.modal-content {
width: 448px;
border-radius: 8px;
box-shadow: 0 14px 28px 0 rgba(26, 24, 29, 0.24), 0 10px 10px 0 rgba(26, 24, 29, 0.28);
}
.modal-body{
padding: 0px;
}
.modal-close {
position: absolute;
width: 18px;
height: 18px;
padding: 4px;
right: 16px;
top: 16px;
cursor: pointer;
.icon-close {
width: 15px;
height: 15px;
& ::v-deep svg path {
fill: #878190;
}
}
}
#subscription-form .subscribe-option {
background: #F9F9F9;
}
}
</style>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
h2 {
color: $purple-300;
padding-top: 2rem;
}
.avatar-spacer {
height: 9px;
}
.display-name {
font-size: 0.875rem;
font-weight: bold;
line-height: 1.71;
margin: 0px 6px 0 20px;
}
.display-name a:hover{
text-decoration: none;
}
.user-name {
font-size: 0.75rem;
line-height: 1.33;
text-align: center;
color: $gray-100;
padding-bottom: 16px;
}
.row {
background-color: $gray-700;
margin: 0 0 0 0;
min-height: 32px;
}
.nav {
font-weight: bold;
font-size: 0.75rem;
min-height: 32px;
padding: 16px 0 0 0;
color: $purple-300;
justify-content: center;
}
.nav-item {
display: inline-block;
padding: 0px 8px 6px 8px;
}
.nav-item:hover, .nav-item.active {
color: $purple-300;
border-bottom: 2px solid $purple-400;
cursor: pointer;
}
.nav-item.inactive {
color: $purple-300;
border-bottom: 0px;
cursor: pointer;
}
.gem-group {
padding: 0 0 24px 0;
background-color: $gray-700;
margin: 0 0 0 0;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px
}
label {
color: $gray-50;
font-size: 0.875rem;
font-weight: bold;
line-height: 1.71;
margin: 12px 0 16px 0;
width: 100%;
text-align: center;
}
.input-group {
width: 94px;
height: 32px;
margin: 0px 16px 0px 16px;
padding: 0;
border-radius: 2px;
border: solid 1px $gray-400;
background-color: $white;
}
.gray-circle {
border-radius: 100%;
border: solid 2px $gray-300;
width: 32px;
height: 32px;
cursor: pointer;
&:hover {
border-color: $purple-400;
}
}
.gray-circle:hover{
.icon-positive, .icon-negative {
& ::v-deep svg path {
fill: $purple-400;
}
}
}
.icon-gem {
width: 16px;
height: 16px;
}
.icon-positive, .icon-negative {
width: 10px;
height: 10px;
margin: 4px auto;
& ::v-deep svg path {
fill: $gray-300;
}
}
.buy-gem-total {
font-size: 0.875rem;
font-weight: bold;
line-height: 1.71;
padding-top: 24px;
text-align: center;
height: 28px;
}
.buy-gem-amount {
font-size: 1.25rem;
font-weight: bold;
line-height: 1.4;
margin: 16px 0 24px 0;
text-align: center;
height: 28px;
color: $green-10;
}
.balance-text {
font-size: 0.75rem;
font-weight: bold;
color: $gray-100;
line-height: 1.33;
margin: 12px 0px 0px 70px;
}
.balance-gem-margin {
margin: 8px 4px 0px 8px;
}
.balance-gems {
font-size: 0.75rem;
color: $gray-100;
line-height: 1.33;
margin: 12px 71px 0px 4px;
}
.gem-state-change {
color: $blue-10;
font-size: 0.875rem;
min-height: 24px;
margin: 16px 0 0;
text-align: center;
cursor: pointer;
}
.subscribe-option {
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
padding-bottom: 24px;
}
.payment-buttons {
padding: 24px 0;
}
</style>
<script>
// libs imports
import { mapState } from '@/libs/store';
// mixins imports
import paymentsMixin from '../../mixins/payments';
// component imports
import avatar from '../avatar';
import userLink from '../userLink';
import subscriptionOptions from '../settings/subscriptionOptions.vue';
import paymentsButtons from '@/components/payments/buttons/list';
// svg imports
import closeIcon from '@/assets/svg/close.svg';
import gemIcon from '@/assets/svg/gem.svg';
import positiveIcon from '@/assets/svg/positive.svg';
import negativeIcon from '@/assets/svg/negative.svg';
export default {
components: {
avatar,
subscriptionOptions,
paymentsButtons,
userLink,
},
mixins: [
paymentsMixin,
],
data () {
return {
subscription: {
key: '',
},
icons: Object.freeze({
closeIcon,
gemIcon,
positiveIcon,
negativeIcon,
}),
userReceivingGift: {
profile: '',
},
name: '',
display: '',
selectedPage: 'subscription',
gift: {
type: 'gems',
gems: {
amount: 0,
fromBalance: true,
},
},
sendingInProgress: false,
amazonPayments: {},
gemCost: 1,
};
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'send-gift');
},
selectPage (page) {
this.selectedPage = page || 'subscription';
},
async sendGift () {
this.sendingInProgress = true;
await this.$store.dispatch('members:transferGems', {
toUserId: this.userReceivingGift._id,
gemAmount: this.gift.gems.amount,
});
this.close();
setTimeout(() => { // wait for the send gem modal to be closed
this.$root.$emit('habitica:payment-success', {
paymentMethod: 'balance',
paymentCompleted: true,
paymentType: 'gift-gems-balance',
gift: {
gems: {
amount: this.gift.gems.amount,
},
},
giftReceiver: this.receiverName,
});
}, 500);
},
onHide () {
this.sendingInProgress = false;
},
},
computed: {
...mapState({
userLoggedIn: 'user.data',
}),
userName () {
const userName = this.userReceivingGift.auth
&& this.userReceivingGift.auth.local
&& this.userReceivingGift.auth.local.username;
return userName;
},
displayName () {
const displayName = this.userReceivingGift.profile.name;
return displayName;
},
userBacker () {
const userBacker = this.userReceivingGift.backer;
return userBacker;
},
userContributor () {
const userContributor = this.userReceivingGift.contributor;
return userContributor;
},
tierIcon () {
if (this.isNPC) {
return this.icons.tierNPC;
}
return this.icons[`tier${this.level}`];
},
fromBal () {
return this.gift.type === 'gems' && this.gift.gems.fromBalance;
},
maxGems () {
const maxGems = this.fromBal ? this.userLoggedIn.balance * 4 : 9999;
return maxGems;
},
formatter () {
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
});
return formatter;
},
totalGems () {
const totalGems = this.gift.gems.amount * 0.25;
return totalGems;
},
receiverName () {
if (
this.userReceivingGift.auth
&& this.userReceivingGift.auth.local
&& this.userReceivingGift.auth.local.username
) {
return this.userReceivingGift.auth.local.username;
}
return this.userReceivingGift.profile.name;
},
},
watch: {
startingPage () {
this.selectedPage = this.startingPage;
},
},
mounted () {
this.$root.$on('habitica::send-gift', data => {
this.userReceivingGift = data;
if (this.$store.state.giftModalOptions.startingPage) {
this.selectedPage = this.$store.state.giftModalOptions.startingPage;
this.$store.state.giftModalOptions.startingPage = '';
this.selectPage(this.selectedPage);
} else {
this.selectPage(this.startingPage);
}
this.$root.$emit('bv::show::modal', 'send-gift');
});
},
};
</script>

View File

@@ -450,10 +450,6 @@
background-color: $white;
}
.subscribe-option {
border-bottom: 1px solid $gray-600;
}
.svg-amazon-pay {
width: 208px;
}

View File

@@ -969,7 +969,8 @@ export default {
axios.post(`/api/v4/user/block/${this.user._id}`);
},
openSendGemsModal () {
this.$root.$emit('habitica::send-gems', this.user);
this.$store.state.giftModalOptions.startingPage = 'buyGems';
this.$root.$emit('habitica::send-gift', this.user);
},
adminTurnOnShadowMuting () {
if (!this.hero.flags) {

View File

@@ -124,6 +124,9 @@ export default function () {
profileOptions: {
startingPage: '',
},
giftModalOptions: {
startingPage: '',
},
rageModalOptions: {
npc: '',
},

View File

@@ -129,6 +129,7 @@
"sendGiftHeading": "Send Gift to <%= name %>",
"sendGiftGemsBalance": "From <%= number %> Gems",
"sendGiftCost": "Total: $<%= cost %> USD",
"sendGiftTotal": "Total:",
"sendGiftFromBalance": "From Balance",
"sendGiftPurchase": "Purchase",
"sendGiftMessagePlaceholder": "Personal message (optional)",

View File

@@ -780,7 +780,7 @@
"rockingReptilesNotes": "Contains 'The Insta-Gator,' 'The Serpent of Distraction,' and 'The Veloci-Rapper.' Available until September 30.",
"delightfulDinosText": "Delightful Dinos Quest Bundle",
"delightfulDinosNotes": "Contains 'The Pterror-dactyl,' 'The Trampling Triceratops,' and 'The Dinosaur Unearthed.' Available until November 30.",
"delightfulDinosNotes": "Contains 'The Pterror-dactyl,' 'The Trampling Triceratops,' and 'The Dinosaur Unearthed.' Available until May 31.",
"questAmberText": "The Amber Alliance",
"questAmberNotes": "Youre sitting in the Tavern with @beffymaroo and @-Tyr- when @Vikte bursts through the door and excitedly tells you about the rumors of another type of Magic Hatching Potion hidden in the Taskwoods. Having completed your Dailies, the three of you immediately agree to help @Vikte on their search. After all, whats the harm in a little adventure?<br><br>After walking through the Taskwoods for hours, youre beginning to regret joining such a wild chase. Youre about to head home, when you hear a surprised yelp and turn to see a huge lizard with shiny amber scales coiled around a tree, clutching @Vikte in her claws. @beffymaroo reaches for her sword.<br><br>“Wait!” cries @-Tyr-. “Its the Trerezin! Shes not dangerous, just dangerously clingy!”",

View File

@@ -3,6 +3,10 @@
"subscriptions": "Subscriptions",
"viewSubscriptions": "View Subscriptions",
"sendGems": "Send Gems",
"howManyGemsPurchase": "How many Gems would you like to purchase?",
"howManyGemsSend":"How many Gems would you like to send?",
"needToPurchaseGems": "Need to purchase Gems as a gift?",
"wantToSendOwnGems": "Want to send your own Gems?",
"buyGemsGold": "Buy Gems with Gold",
"mustSubscribeToPurchaseGems": "Must subscribe to purchase gems with GP",
"reachedGoldToGemCapQuantity": "Your requested amount <%= quantity %> exceeds the amount you can buy for this month (<%= convCap %>). The full amount becomes available within the first three days of each month. Thanks for subscribing!",
@@ -196,5 +200,6 @@
"needToUpdateCard": "Need to update your card?",
"readyToResubscribe": "Are you ready to resubscribe?",
"cancelYourSubscription": "Cancel your subscription?",
"cancelSubAlternatives": "If you're having technical problems or Habitica doesn't seem to be working out for you, please consider <a href='mailto:admin@habitica.com'>contacting us</a>. We want to help you get the most from Habitica."
"cancelSubAlternatives": "If you're having technical problems or Habitica doesn't seem to be working out for you, please consider <a href='mailto:admin@habitica.com'>contacting us</a>. We want to help you get the most from Habitica.",
"sendAGift": "Send Gift"
}

View File

@@ -55,27 +55,30 @@ const seasonalSpellAchievs = {
};
Object.assign(achievementsData, seasonalSpellAchievs);
const masterAchievs = {
const stableAchievs = {
beastMaster: {
icon: 'achievement-rat',
titleKey: 'beastMasterName',
textKey: 'beastMasterText',
text2Key: 'beastMasterText2',
notificationText: 'beastAchievement',
},
mountMaster: {
icon: 'achievement-wolf',
titleKey: 'mountMasterName',
textKey: 'mountMasterText',
text2Key: 'mountMasterText2',
notificationText: 'mountAchievement',
},
triadBingo: {
icon: 'achievement-triadbingo',
titleKey: 'triadBingoName',
textKey: 'triadBingoText',
text2Key: 'triadBingoText2',
notificationText: 'triadBingoAchievement',
},
};
Object.assign(achievementsData, masterAchievs);
Object.assign(achievementsData, stableAchievs);
const basicAchievs = {
partyUp: {
@@ -122,146 +125,173 @@ const basicAchievs = {
titleKey: 'invitedFriend',
textKey: 'invitedFriendText',
},
};
Object.assign(achievementsData, basicAchievs);
const questSeriesAchievs = {
lostMasterclasser: {
icon: 'achievement-lostMasterclasser',
titleKey: 'achievementLostMasterclasser',
textKey: 'achievementLostMasterclasserText',
},
mindOverMatter: {
icon: 'achievement-mindOverMatter',
titleKey: 'achievementMindOverMatter',
textKey: 'achievementMindOverMatterText',
},
justAddWater: {
icon: 'achievement-justAddWater',
titleKey: 'achievementJustAddWater',
textKey: 'achievementJustAddWaterText',
},
backToBasics: {
icon: 'achievement-backToBasics',
titleKey: 'achievementBackToBasics',
textKey: 'achievementBackToBasicsText',
},
allYourBase: {
icon: 'achievement-allYourBase',
titleKey: 'achievementAllYourBase',
textKey: 'achievementAllYourBaseText',
},
dustDevil: {
icon: 'achievement-dustDevil',
titleKey: 'achievementDustDevil',
textKey: 'achievementDustDevilText',
},
aridAuthority: {
icon: 'achievement-aridAuthority',
titleKey: 'achievementAridAuthority',
textKey: 'achievementAridAuthorityText',
},
monsterMagus: {
icon: 'achievement-monsterMagus',
titleKey: 'achievementMonsterMagus',
textKey: 'achievementMonsterMagusText',
},
undeadUndertaker: {
icon: 'achievement-undeadUndertaker',
titleKey: 'achievementUndeadUndertaker',
textKey: 'achievementUndeadUndertakerText',
},
primedForPainting: {
icon: 'achievement-primedForPainting',
titleKey: 'achievementPrimedForPainting',
textKey: 'achievementPrimedForPaintingText',
},
pearlyPro: {
icon: 'achievement-pearlyPro',
titleKey: 'achievementPearlyPro',
textKey: 'achievementPearlyProText',
},
tickledPink: {
icon: 'achievement-tickledPink',
titleKey: 'achievementTickledPink',
textKey: 'achievementTickledPinkText',
},
rosyOutlook: {
icon: 'achievement-rosyOutlook',
titleKey: 'achievementRosyOutlook',
textKey: 'achievementRosyOutlookText',
},
bugBonanza: {
icon: 'achievement-bugBonanza',
titleKey: 'achievementBugBonanza',
textKey: 'achievementBugBonanzaText',
},
bareNecessities: {
icon: 'achievement-bareNecessities',
titleKey: 'achievementBareNecessities',
textKey: 'achievementBareNecessitiesText',
},
bugBonanza: {
icon: 'achievement-bugBonanza',
titleKey: 'achievementBugBonanza',
textKey: 'achievementBugBonanzaText',
},
freshwaterFriends: {
icon: 'achievement-freshwaterFriends',
titleKey: 'achievementFreshwaterFriends',
textKey: 'achievementFreshwaterFriendsText',
},
goodAsGold: {
icon: 'achievement-goodAsGold',
titleKey: 'achievementGoodAsGold',
textKey: 'achievementGoodAsGoldText',
justAddWater: {
icon: 'achievement-justAddWater',
titleKey: 'achievementJustAddWater',
textKey: 'achievementJustAddWaterText',
},
allThatGlitters: {
icon: 'achievement-allThatGlitters',
titleKey: 'achievementAllThatGlitters',
textKey: 'achievementAllThatGlittersText',
},
boneCollector: {
icon: 'achievement-boneCollector',
titleKey: 'achievementBoneCollector',
textKey: 'achievementBoneCollectorText',
},
skeletonCrew: {
icon: 'achievement-skeletonCrew',
titleKey: 'achievementSkeletonCrew',
textKey: 'achievementSkeletonCrewText',
},
seeingRed: {
icon: 'achievement-seeingRed',
titleKey: 'achievementSeeingRed',
textKey: 'achievementSeeingRedText',
},
redLetterDay: {
icon: 'achievement-redLetterDay',
titleKey: 'achievementRedLetterDay',
textKey: 'achievementRedLetterDayText',
},
legendaryBestiary: {
icon: 'achievement-legendaryBestiary',
titleKey: 'achievementLegendaryBestiary',
textKey: 'achievementLegendaryBestiaryText',
mindOverMatter: {
icon: 'achievement-mindOverMatter',
titleKey: 'achievementMindOverMatter',
textKey: 'achievementMindOverMatterText',
},
seasonalSpecialist: {
icon: 'achievement-seasonalSpecialist',
titleKey: 'achievementSeasonalSpecialist',
textKey: 'achievementSeasonalSpecialistText',
},
violetsAreBlue: {
icon: 'achievement-violetsAreBlue',
titleKey: 'achievementVioletsAreBlue',
textKey: 'achievementVioletsAreBlueText',
};
Object.assign(achievementsData, questSeriesAchievs);
const animalSetAchievs = {
legendaryBestiary: {
icon: 'achievement-legendaryBestiary',
titleKey: 'achievementLegendaryBestiary',
textKey: 'achievementLegendaryBestiaryText',
},
wildBlueYonder: {
icon: 'achievement-wildBlueYonder',
titleKey: 'achievementWildBlueYonder',
textKey: 'achievementWildBlueYonderText',
birdsOfAFeather: {
icon: 'achievement-birdsOfAFeather',
titleKey: 'achievementBirdsOfAFeather',
textKey: 'achievementBirdsOfAFeatherText',
},
domesticated: {
icon: 'achievement-domesticated',
titleKey: 'achievementDomesticated',
textKey: 'achievementDomesticatedText',
},
zodiacZookeeper: {
icon: 'achievement-zodiac',
titleKey: 'achievementZodiacZookeeper',
textKey: 'achievementZodiacZookeeperText',
},
};
Object.assign(achievementsData, animalSetAchievs);
const petColorAchievs = {
backToBasics: {
icon: 'achievement-backToBasics',
titleKey: 'achievementBackToBasics',
textKey: 'achievementBackToBasicsText',
},
dustDevil: {
icon: 'achievement-dustDevil',
titleKey: 'achievementDustDevil',
textKey: 'achievementDustDevilText',
},
monsterMagus: {
icon: 'achievement-monsterMagus',
titleKey: 'achievementMonsterMagus',
textKey: 'achievementMonsterMagusText',
},
primedForPainting: {
icon: 'achievement-primedForPainting',
titleKey: 'achievementPrimedForPainting',
textKey: 'achievementPrimedForPaintingText',
},
tickledPink: {
icon: 'achievement-tickledPink',
titleKey: 'achievementTickledPink',
textKey: 'achievementTickledPinkText',
},
goodAsGold: {
icon: 'achievement-goodAsGold',
titleKey: 'achievementGoodAsGold',
textKey: 'achievementGoodAsGoldText',
},
boneCollector: {
icon: 'achievement-boneCollector',
titleKey: 'achievementBoneCollector',
textKey: 'achievementBoneCollectorText',
},
seeingRed: {
icon: 'achievement-seeingRed',
titleKey: 'achievementSeeingRed',
textKey: 'achievementSeeingRedText',
modalTextKey: 'achievementSeeingRedModalText',
},
violetsAreBlue: {
icon: 'achievement-violetsAreBlue',
titleKey: 'achievementVioletsAreBlue',
textKey: 'achievementVioletsAreBlueText',
},
shadyCustomer: {
icon: 'achievement-shadyCustomer',
titleKey: 'achievementShadyCustomer',
textKey: 'achievementShadyCustomerText',
},
};
Object.assign(achievementsData, petColorAchievs);
const mountColorAchievs = {
allYourBase: {
icon: 'achievement-allYourBase',
titleKey: 'achievementAllYourBase',
textKey: 'achievementAllYourBaseText',
},
aridAuthority: {
icon: 'achievement-aridAuthority',
titleKey: 'achievementAridAuthority',
textKey: 'achievementAridAuthorityText',
},
undeadUndertaker: {
icon: 'achievement-undeadUndertaker',
titleKey: 'achievementUndeadUndertaker',
textKey: 'achievementUndeadUndertakerText',
},
pearlyPro: {
icon: 'achievement-pearlyPro',
titleKey: 'achievementPearlyPro',
textKey: 'achievementPearlyProText',
},
rosyOutlook: {
icon: 'achievement-rosyOutlook',
titleKey: 'achievementRosyOutlook',
textKey: 'achievementRosyOutlookText',
},
allThatGlitters: {
icon: 'achievement-allThatGlitters',
titleKey: 'achievementAllThatGlitters',
textKey: 'achievementAllThatGlittersText',
},
skeletonCrew: {
icon: 'achievement-skeletonCrew',
titleKey: 'achievementSkeletonCrew',
textKey: 'achievementSkeletonCrewText',
},
redLetterDay: {
icon: 'achievement-redLetterDay',
titleKey: 'achievementRedLetterDay',
textKey: 'achievementRedLetterDayText',
},
wildBlueYonder: {
icon: 'achievement-wildBlueYonder',
titleKey: 'achievementWildBlueYonder',
textKey: 'achievementWildBlueYonderText',
},
shadeOfItAll: {
icon: 'achievement-shadeOfItAll',
titleKey: 'achievementShadeOfItAll',
@@ -278,7 +308,7 @@ const basicAchievs = {
textKey: 'achievementBirdsOfAFeatherText',
},
};
Object.assign(achievementsData, basicAchievs);
Object.assign(achievementsData, mountColorAchievs);
const onboardingAchievs = {
createdTask: {

View File

@@ -220,7 +220,7 @@ const bundles = {
'trex_undead',
],
canBuy () {
return moment().isBetween('2019-11-14', '2019-12-02');
return moment().isBetween('2022-05-16', '2022-05-31');
},
type: 'quests',
value: 7,

View File

@@ -2,72 +2,72 @@ const ANIMAL_COLOR_ACHIEVEMENTS = [
{
color: 'Base',
petAchievement: 'backToBasics',
petNotificationType: 'ACHIEVEMENT_BACK_TO_BASICS',
petNotificationType: 'ACHIEVEMENT_PET_COLOR',
mountAchievement: 'allYourBase',
mountNotificationType: 'ACHIEVEMENT_ALL_YOUR_BASE',
mountNotificationType: 'ACHIEVEMENT_MOUNT_COLOR',
},
{
color: 'Desert',
petAchievement: 'dustDevil',
petNotificationType: 'ACHIEVEMENT_DUST_DEVIL',
petNotificationType: 'ACHIEVEMENT_PET_COLOR',
mountAchievement: 'aridAuthority',
mountNotificationType: 'ACHIEVEMENT_ARID_AUTHORITY',
mountNotificationType: 'ACHIEVEMENT_MOUNT_COLOR',
},
{
color: 'Zombie',
petAchievement: 'monsterMagus',
petNotificationType: 'ACHIEVEMENT_MONSTER_MAGUS',
petNotificationType: 'ACHIEVEMENT_PET_COLOR',
mountAchievement: 'undeadUndertaker',
mountNotificationType: 'ACHIEVEMENT_UNDEAD_UNDERTAKER',
mountNotificationType: 'ACHIEVEMENT_MOUNT_COLOR',
},
{
color: 'White',
petAchievement: 'primedForPainting',
petNotificationType: 'ACHIEVEMENT_PRIMED_FOR_PAINTING',
petNotificationType: 'ACHIEVEMENT_PET_COLOR',
mountAchievement: 'pearlyPro',
mountNotificationType: 'ACHIEVEMENT_PEARLY_PRO',
mountNotificationType: 'ACHIEVEMENT_MOUNT_COLOR',
},
{
color: 'CottonCandyPink',
petAchievement: 'tickledPink',
petNotificationType: 'ACHIEVEMENT_TICKLED_PINK',
petNotificationType: 'ACHIEVEMENT_PET_COLOR',
mountAchievement: 'rosyOutlook',
mountNotificationType: 'ACHIEVEMENT_ROSY_OUTLOOK',
mountNotificationType: 'ACHIEVEMENT_MOUNT_COLOR',
},
{
color: 'Golden',
petAchievement: 'goodAsGold',
petNotificationType: 'ACHIEVEMENT_GOOD_AS_GOLD',
petNotificationType: 'ACHIEVEMENT_PET_COLOR',
mountAchievement: 'allThatGlitters',
mountNotificationType: 'ACHIEVEMENT_ALL_THAT_GLITTERS',
mountNotificationType: 'ACHIEVEMENT_MOUNT_COLOR',
},
{
color: 'Skeleton',
petAchievement: 'boneCollector',
petNotificationType: 'ACHIEVEMENT_BONE_COLLECTOR',
petNotificationType: 'ACHIEVEMENT_PET_COLOR',
mountAchievement: 'skeletonCrew',
mountNotificationType: 'ACHIEVEMENT_SKELETON_CREW',
mountNotificationType: 'ACHIEVEMENT_MOUNT_COLOR',
},
{
color: 'Red',
petAchievement: 'seeingRed',
petNotificationType: 'ACHIEVEMENT_SEEING_RED',
petNotificationType: 'ACHIEVEMENT_PET_COLOR',
mountAchievement: 'redLetterDay',
mountNotificationType: 'ACHIEVEMENT_RED_LETTER_DAY',
mountNotificationType: 'ACHIEVEMENT_MOUNT_COLOR',
},
{
color: 'CottonCandyBlue',
petAchievement: 'violetsAreBlue',
petNotificationType: 'ACHIEVEMENT_VIOLETS_ARE_BLUE',
petNotificationType: 'ACHIEVEMENT_PET_COLOR',
mountAchievement: 'wildBlueYonder',
mountNotificationType: 'ACHIEVEMENT_WILD_BLUE_YONDER',
mountNotificationType: 'ACHIEVEMENT_MOUNT_COLOR',
},
{
color: 'Shade',
petAchievement: 'shadyCustomer',
petNotificationType: 'ACHIEVEMENT_SHADY_CUSTOMER',
petNotificationType: 'ACHIEVEMENT_PET_COLOR',
mountAchievement: 'shadeOfItAll',
mountNotificationType: 'ACHIEVEMENT_SHADE_OF_IT_ALL',
mountNotificationType: 'ACHIEVEMENT_MOUNT_COLOR',
},
];

View File

@@ -9,7 +9,7 @@ const ANIMAL_SET_ACHIEVEMENTS = {
'Unicorn',
],
achievementKey: 'legendaryBestiary',
notificationType: 'ACHIEVEMENT_LEGENDARY_BESTIARY',
notificationType: 'ACHIEVEMENT_ANIMAL_SET',
},
birdsOfAFeather: {
type: 'pet',
@@ -24,7 +24,7 @@ const ANIMAL_SET_ACHIEVEMENTS = {
'Peacock',
],
achievementKey: 'birdsOfAFeather',
notificationType: 'ACHIEVEMENT_BIRDS_OF_A_FEATHER',
notificationType: 'ACHIEVEMENT_ANIMAL_SET',
},
domesticated: {
type: 'pet',
@@ -39,7 +39,7 @@ const ANIMAL_SET_ACHIEVEMENTS = {
'Cow',
],
achievementKey: 'domesticated',
notificationType: 'ACHIEVEMENT_DOMESTICATED',
notificationType: 'ACHIEVEMENT_ANIMAL_SET',
},
zodiacZookeeper: {
type: 'pet',
@@ -53,12 +53,12 @@ const ANIMAL_SET_ACHIEVEMENTS = {
'Monkey',
'Rooster',
'Wolf',
'TigerCub',
'Tiger',
'FlyingPig',
'Dragon',
],
achievementKey: 'zodiacZookeeper',
notificationType: 'ACHIEVEMENT_ZODIAC_ZOOKEEPER',
notificationType: 'ACHIEVEMENT_ANIMAL_SET',
},
};

View File

@@ -10,11 +10,17 @@ const gemsPromo = {
export const EVENTS = {
noCurrentEvent: {
start: '2022-04-30T20:00-05:00',
start: '2022-05-31T20:00-04:00',
end: '2022-06-30T20:00-05:00',
season: 'normal',
npcImageSuffix: '',
},
potions202205: {
start: '2022-05-17T08:00-04:00',
end: '2022-05-31T20:00-04:00',
season: 'normal',
npcImageSuffix: '',
},
spring2022: {
start: '2022-03-22T08:00-05:00',
end: '2022-04-30T20:00-05:00',

View File

@@ -32,6 +32,7 @@ export { default as SEASONAL_SETS } from './seasonalSets';
export { default as ANIMAL_COLOR_ACHIEVEMENTS } from './animalColorAchievements';
export { default as ANIMAL_SET_ACHIEVEMENTS } from './animalSetAchievements';
export { default as QUEST_SERIES_ACHIEVEMENTS } from './questSeriesAchievements';
export { default as STABLE_ACHIEVEMENTS } from './stableAchievements';
export { default as ITEM_LIST } from './itemList';
export { default as QUEST_SERIES } from '../quests/series';
export { default as QUEST_MASTERCLASSER } from '../quests/masterclasser';

View File

@@ -17,10 +17,21 @@ const QUEST_SERIES_ACHIEVEMENTS = {
'lostMasterclasser3',
'lostMasterclasser4',
],
mindOverMatter: [
'rock',
'slime',
'yarn',
bareNecessities: [
'monkey',
'sloth',
'treeling',
],
bugBonanza: [
'beetle',
'butterfly',
'snail',
'spider',
],
freshwaterFriends: [
'axolotl',
'frog',
'hippo',
],
justAddWater: [
'octopus',
@@ -32,21 +43,10 @@ const QUEST_SERIES_ACHIEVEMENTS = {
'seaserpent',
'dolphin',
],
bugBonanza: [
'beetle',
'butterfly',
'snail',
'spider',
],
bareNecessities: [
'monkey',
'sloth',
'treeling',
],
freshwaterFriends: [
'axolotl',
'frog',
'hippo',
mindOverMatter: [
'rock',
'slime',
'yarn',
],
seasonalSpecialist: [
'egg',

View File

@@ -0,0 +1,16 @@
const STABLE_ACHIEVEMENTS = {
ACHIEVEMENT_BEAST_MASTER: {
masterAchievement: 'beastMasterName',
masterNotificationType: 'ACHIEVEMENT_STABLE',
},
ACHIEVEMENT_MOUNT_MASTER: {
masterAchievement: 'mountMasterName',
masterNotificationType: 'ACHIEVEMENT_STABLE',
},
ACHIEVEMENT_TRIAD_BINGO: {
masterAchievement: 'triadBingoName',
masterNotificationType: 'ACHIEVEMENT_STABLE',
},
};
export default STABLE_ACHIEVEMENTS;

View File

@@ -96,13 +96,13 @@ const premium = {
value: 2,
text: t('hatchingPotionFloral'),
limited: true,
event: EVENTS.potions202105,
event: EVENTS.potions202205,
_addlNotes: t('eventAvailabilityReturning', {
availableDate: t('dateEndMay'),
previousDate: t('mayYYYY', { year: 2019 }),
previousDate: t('mayYYYY', { year: 2021 }),
}),
canBuy () {
return moment().isBefore(EVENTS.potions202105.end);
return moment().isBefore(EVENTS.potions202205.end);
},
},
Aquatic: {
@@ -297,12 +297,13 @@ const premium = {
value: 2,
text: t('hatchingPotionSunshine'),
limited: true,
event: EVENTS.potions202205,
_addlNotes: t('eventAvailabilityReturning', {
availableDate: t('dateEndMay'),
previousDate: t('mayYYYY', { year: 2019 }),
previousDate: t('mayYYYY', { year: 2020 }),
}),
canBuy () {
return moment().isBefore('2020-06-02');
return moment().isBefore(EVENTS.potions202205.end);
},
},
Bronze: {

View File

@@ -12,6 +12,7 @@ import {
QUEST_SERIES_ACHIEVEMENTS,
ANIMAL_COLOR_ACHIEVEMENTS,
ANIMAL_SET_ACHIEVEMENTS,
STABLE_ACHIEVEMENTS,
} from './constants';
import achievements from './achievements';
@@ -41,6 +42,7 @@ api.achievements = achievements;
api.questSeriesAchievements = QUEST_SERIES_ACHIEVEMENTS;
api.animalColorAchievements = ANIMAL_COLOR_ACHIEVEMENTS;
api.animalSetAchievements = ANIMAL_SET_ACHIEVEMENTS;
api.stableAchievements = STABLE_ACHIEVEMENTS;
api.quests = quests;
api.questsByLevel = questsByLevel;

View File

@@ -1,11 +1,11 @@
import moment from 'moment';
import { EVENTS } from './constants';
// import { EVENTS } from './constants';
// Magic Hatching Potions are configured like this:
// type: 'premiumHatchingPotion', // note no "s" at the end
// path: 'premiumHatchingPotions.Rainbow',
const featuredItems = {
market () {
if (moment().isBefore(EVENTS.spring2022.end)) {
if (moment().isBefore('2022-05-31T20:00-04:00')) {
return [
{
type: 'armoire',
@@ -13,15 +13,15 @@ const featuredItems = {
},
{
type: 'premiumHatchingPotion',
path: 'premiumHatchingPotions.Shimmer',
path: 'premiumHatchingPotions.Sunshine',
},
{
type: 'premiumHatchingPotion',
path: 'premiumHatchingPotions.Celestial',
path: 'premiumHatchingPotions.Floral',
},
{
type: 'premiumHatchingPotion',
path: 'premiumHatchingPotions.PolkaDot',
type: 'hatchingPotions',
path: 'hatchingPotions.Golden',
},
];
}
@@ -45,19 +45,19 @@ const featuredItems = {
];
},
quests () {
if (moment().isBefore('2022-03-31T20:00-04:00')) {
if (moment().isBefore('2022-05-31T20:00-04:00')) {
return [
{
type: 'bundles',
path: 'bundles.cuddleBuddies',
path: 'bundles.delightfulDinos',
},
{
type: 'quests',
path: 'quests.egg',
path: 'quests.alligator',
},
{
type: 'quests',
path: 'quests.ghost_stag',
path: 'quests.turtle',
},
];
}

View File

@@ -108,12 +108,6 @@ shops.checkMarketGearLocked = function checkMarketGearLocked (user, items) {
gear.locked = true;
}
if (Boolean(gear.specialClass) && Boolean(gear.set)) {
const currentSet = gear.set === seasonalShopConfig.pinnedSets[gear.specialClass];
gear.locked = currentSet && user.stats.class !== gear.specialClass;
}
if (gear.canOwn) {
gear.locked = !gear.canOwn(user);
}
@@ -124,6 +118,12 @@ shops.checkMarketGearLocked = function checkMarketGearLocked (user, items) {
gear.locked = true;
}
if (Boolean(gear.specialClass) && Boolean(gear.set)) {
const currentSet = gear.set === seasonalShopConfig.pinnedSets[gear.specialClass];
gear.locked = currentSet && user.stats.class !== gear.specialClass;
}
gear.owned = itemOwned;
// @TODO: I'm not sure what the logic for locking is supposed to be

View File

@@ -28,7 +28,7 @@ export class BuyMarketGearOperation extends AbstractGoldItemOperation { // eslin
const checkSpecialClass = item.klass === 'special' && item.specialClass && item.specialClass !== user.stats.class;
// check for different class gear
if (checkKlass || checkSpecialClass) {
if ((checkKlass || checkSpecialClass) && user.items.gear.owned[item.key] !== false) {
throw new NotAuthorized(this.i18n('cannotBuyItem'));
}
}

View File

@@ -130,6 +130,7 @@ export default function feed (user, req = {}, analytics) {
if (user.addNotification) {
const achievementString = `achievement${upperFirst(achievement.mountAchievement)}`;
user.addNotification(achievement.mountNotificationType, {
label: `${'achievement'}: ${achievementString}`,
achievement: achievement.mountAchievement,
message: `${i18n.t('modalAchievement')} ${i18n.t(achievementString)}`,
modalText: i18n.t(`${achievementString}ModalText`),

View File

@@ -72,6 +72,7 @@ export default function hatch (user, req = {}, analytics) {
if (user.addNotification) {
const achievementString = `achievement${upperFirst(achievement.petAchievement)}`;
user.addNotification(achievement.petNotificationType, {
label: `${'achievement'}: ${achievementString}`,
achievement: achievement.petAchievement,
message: `${i18n.t('modalAchievement')} ${i18n.t(achievementString)}`,
modalText: i18n.t(`${achievementString}ModalText`),
@@ -100,6 +101,7 @@ export default function hatch (user, req = {}, analytics) {
if (user.addNotification) {
const achievementString = `achievement${upperFirst(achievement.achievementKey)}`;
user.addNotification(achievement.notificationType, {
label: `${'achievement'}: ${achievementString}`,
achievement: achievement.achievementKey,
message: `${i18n.t('modalAchievement')} ${i18n.t(achievementString)}`,
modalText: i18n.t(`${achievementString}ModalText`),

View File

@@ -420,5 +420,5 @@ export default function scoreTask (options = {}, req = {}, analytics) {
checkOnboardingStatus(user, req, analytics);
}
return [delta];
return delta;
}

View File

@@ -23,7 +23,33 @@ export function marshallUserData (userData) {
type: i.type,
}));
return js2xml.parse('user', userData, {
const copyUserData = JSON.parse(JSON.stringify(userData));
const newPurchased = {};
if (userData.purchased != null) {
for (const itemType in userData.purchased) {
if (userData.purchased[itemType] != null) {
if (typeof userData.purchased[itemType] === 'object') {
const fixedData = [];
for (const item in userData.purchased[itemType]) {
if (item != null) {
if (typeof userData.purchased[itemType][item] === 'object') {
fixedData.push({ item: userData.purchased[itemType][item] });
} else {
fixedData.push(item);
}
}
}
newPurchased[itemType] = fixedData;
} else {
newPurchased[itemType] = userData.purchased[itemType];
}
}
}
copyUserData.purchased = newPurchased;
}
return js2xml.parse('user', copyUserData, {
cdataInvalidChars: true,
replaceInvalidChars: true,
declaration: {

View File

@@ -1002,10 +1002,9 @@ schema.methods.finishQuest = async function finishQuest (quest) {
const questAchievementUpdate = { $set: {}, $push: {} };
questAchievementUpdate.$set[`achievements.${achievement}`] = true;
const achievementTitleCase = `${achievement.slice(0, 1).toUpperCase()}${achievement.slice(1, achievement.length)}`;
const achievementSnakeCase = `ACHIEVEMENT_${_.snakeCase(achievement).toUpperCase()}`;
questAchievementUpdate.$push = {
notifications: new UserNotification({
type: achievementSnakeCase,
type: 'ACHIEVEMENT_QUESTS',
data: {
achievement,
message: `${shared.i18n.t('modalAchievement')} ${shared.i18n.t(`achievement${achievementTitleCase}`)}`,

View File

@@ -258,7 +258,13 @@ schema.pre('save', true, function preSaveUser (next, done) {
&& this.achievements.beastMaster !== true
) {
this.achievements.beastMaster = true;
this.addNotification('ACHIEVEMENT_BEAST_MASTER');
this.addNotification(
'ACHIEVEMENT_STABLE',
{
achievement: 'beastMaster',
achievementNotification: 'beastAchievement',
},
);
}
// Determines if Mount Master should be awarded
@@ -269,7 +275,13 @@ schema.pre('save', true, function preSaveUser (next, done) {
&& this.achievements.mountMaster !== true
) {
this.achievements.mountMaster = true;
this.addNotification('ACHIEVEMENT_MOUNT_MASTER');
this.addNotification(
'ACHIEVEMENT_STABLE',
{
achievement: 'mountMaster',
achievementNotification: 'mountAchievement',
},
);
}
// Determines if Triad Bingo should be awarded
@@ -281,7 +293,13 @@ schema.pre('save', true, function preSaveUser (next, done) {
&& this.achievements.triadBingo !== true
) {
this.achievements.triadBingo = true;
this.addNotification('ACHIEVEMENT_TRIAD_BINGO');
this.addNotification(
'ACHIEVEMENT_STABLE',
{
achievement: 'triadBingo',
achievementNotification: 'triadBingoAchievement',
},
);
}
// EXAMPLE CODE for allowing all existing and new players to be

View File

@@ -5,75 +5,85 @@ import validator from 'validator';
import baseModel from '../libs/baseModel';
const NOTIFICATION_TYPES = [
'DROPS_ENABLED', // unused
'REBIRTH_ENABLED',
'WON_CHALLENGE',
'STREAK_ACHIEVEMENT',
'ULTIMATE_GEAR_ACHIEVEMENT',
'REBIRTH_ACHIEVEMENT',
'NEW_CONTRIBUTOR_LEVEL',
// general notifications
'CARD_RECEIVED',
'CRON',
'DROP_CAP_REACHED',
'DROPS_ENABLED', // unused
'FIRST_DROPS',
'GIFT_ONE_GET_ONE',
'GROUP_INVITE_ACCEPTED',
'GROUP_TASK_APPROVAL',
'GROUP_TASK_APPROVED',
'GROUP_TASK_ASSIGNED',
'GROUP_TASK_CLAIMED',
'GROUP_TASK_NEEDS_WORK',
'LOGIN_INCENTIVE',
'GROUP_INVITE_ACCEPTED',
'SCORED_TASK',
'BOSS_DAMAGE', // Not used currently but kept to avoid validation errors
'GIFT_ONE_GET_ONE',
'GUILD_PROMPT',
'GUILD_JOINED_ACHIEVEMENT',
'CHALLENGE_JOINED_ACHIEVEMENT',
'INVITED_FRIEND_ACHIEVEMENT',
'CARD_RECEIVED',
'NEW_MYSTERY_ITEMS',
'UNALLOCATED_STATS_POINTS',
'NEW_INBOX_MESSAGE',
'NEW_STUFF',
'LEVELED_UP', // not in use
'LOGIN_INCENTIVE',
'NEW_CHAT_MESSAGE',
'LEVELED_UP', // Not in use
'FIRST_DROPS',
'NEW_CONTRIBUTOR_LEVEL',
'NEW_INBOX_MESSAGE',
'NEW_MYSTERY_ITEMS',
'NEW_STUFF',
'ONBOARDING_COMPLETE',
'ACHIEVEMENT_ALL_YOUR_BASE',
'ACHIEVEMENT_BACK_TO_BASICS',
'ACHIEVEMENT_JUST_ADD_WATER',
'ACHIEVEMENT_LOST_MASTERCLASSER',
'ACHIEVEMENT_MIND_OVER_MATTER',
'ACHIEVEMENT_DUST_DEVIL',
'ACHIEVEMENT_ARID_AUTHORITY',
'ACHIEVEMENT_PARTY_UP',
'ACHIEVEMENT_PARTY_ON',
'ACHIEVEMENT_BEAST_MASTER',
'ACHIEVEMENT_MOUNT_MASTER',
'ACHIEVEMENT_TRIAD_BINGO',
'ACHIEVEMENT_MONSTER_MAGUS',
'ACHIEVEMENT_UNDEAD_UNDERTAKER',
'ACHIEVEMENT_PRIMED_FOR_PAINTING',
'ACHIEVEMENT_PEARLY_PRO',
'ACHIEVEMENT_TICKLED_PINK',
'ACHIEVEMENT_ROSY_OUTLOOK',
'ACHIEVEMENT_BUG_BONANZA',
'ACHIEVEMENT_BARE_NECESSITIES',
'ACHIEVEMENT_FRESHWATER_FRIENDS',
'ACHIEVEMENT_GOOD_AS_GOLD',
'ACHIEVEMENT_ALL_THAT_GLITTERS',
'ACHIEVEMENT_BONE_COLLECTOR',
'ACHIEVEMENT_SKELETON_CREW',
'ACHIEVEMENT_SEEING_RED',
'ACHIEVEMENT_RED_LETTER_DAY',
'ACHIEVEMENT_LEGENDARY_BESTIARY',
'ACHIEVEMENT_SEASONAL_SPECIALIST',
'ACHIEVEMENT_VIOLETS_ARE_BLUE',
'ACHIEVEMENT_WILD_BLUE_YONDER',
'ACHIEVEMENT_DOMESTICATED',
'ACHIEVEMENT_SHADY_CUSTOMER',
'ACHIEVEMENT_SHADE_OF_IT_ALL',
'ACHIEVEMENT_ZODIAC_ZOOKEEPER',
'ACHIEVEMENT_BIRDS_OF_A_FEATHER',
'REBIRTH_ENABLED',
'SCORED_TASK',
'UNALLOCATED_STATS_POINTS',
'WON_CHALLENGE',
// achievement notifications
'ACHIEVEMENT', // generic achievement notification, details inside `notification.data`
'DROP_CAP_REACHED',
'CHALLENGE_JOINED_ACHIEVEMENT',
'GUILD_JOINED_ACHIEVEMENT',
'ACHIEVEMENT_PARTY_ON',
'ACHIEVEMENT_PARTY_UP',
'INVITED_FRIEND_ACHIEVEMENT',
'REBIRTH_ACHIEVEMENT',
'STREAK_ACHIEVEMENT',
'ULTIMATE_GEAR_ACHIEVEMENT',
'ACHIEVEMENT_STABLE',
'ACHIEVEMENT_QUESTS',
'ACHIEVEMENT_ANIMAL_SET',
'ACHIEVEMENT_PET_COLOR',
'ACHIEVEMENT_MOUNT_COLOR',
// Deprecated notification types. Can be removed once old data is cleaned out
'BOSS_DAMAGE', // deprecated
'ACHIEVEMENT_ALL_YOUR_BASE', // deprecated
'ACHIEVEMENT_BACK_TO_BASICS', // deprecated
'ACHIEVEMENT_JUST_ADD_WATER', // deprecated
'ACHIEVEMENT_LOST_MASTERCLASSER', // deprecated
'ACHIEVEMENT_MIND_OVER_MATTER', // deprecated
'ACHIEVEMENT_DUST_DEVIL', // deprecated
'ACHIEVEMENT_ARID_AUTHORITY', // deprecated
'ACHIEVEMENT_PARTY_UP', // deprecated
'ACHIEVEMENT_PARTY_ON', // deprecated
'ACHIEVEMENT_BEAST_MASTER', // deprecated
'ACHIEVEMENT_MOUNT_MASTER', // deprecated
'ACHIEVEMENT_TRIAD_BINGO', // deprecated
'ACHIEVEMENT_MONSTER_MAGUS', // deprecated
'ACHIEVEMENT_UNDEAD_UNDERTAKER', // deprecated
'ACHIEVEMENT_PRIMED_FOR_PAINTING', // deprecated
'ACHIEVEMENT_PEARLY_PRO', // deprecated
'ACHIEVEMENT_TICKLED_PINK', // deprecated
'ACHIEVEMENT_ROSY_OUTLOOK', // deprecated
'ACHIEVEMENT_BUG_BONANZA', // deprecated
'ACHIEVEMENT_BARE_NECESSITIES', // deprecated
'ACHIEVEMENT_FRESHWATER_FRIENDS', // deprecated
'ACHIEVEMENT_GOOD_AS_GOLD', // deprecated
'ACHIEVEMENT_ALL_THAT_GLITTERS', // deprecated
'ACHIEVEMENT_BONE_COLLECTOR', // deprecated
'ACHIEVEMENT_SKELETON_CREW', // deprecated
'ACHIEVEMENT_SEEING_RED', // deprecated
'ACHIEVEMENT_RED_LETTER_DAY', // deprecated
'ACHIEVEMENT_LEGENDARY_BESTIARY', // deprecated
'ACHIEVEMENT_SEASONAL_SPECIALIST', // deprecated
'ACHIEVEMENT_VIOLETS_ARE_BLUE', // deprecated
'ACHIEVEMENT_WILD_BLUE_YONDER', // deprecated
'ACHIEVEMENT_DOMESTICATED', // deprecated
'ACHIEVEMENT_SHADY_CUSTOMER', // deprecated
'ACHIEVEMENT_SHADE_OF_IT_ALL', // deprecated
'ACHIEVEMENT_ZODIAC_ZOOKEEPER', // deprecated
'ACHIEVEMENT_BIRDS_OF_A_FEATHER', // deprecated
];
const { Schema } = mongoose;