mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 06:07:21 +01:00
feat(notifications): alert user to achievements
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
<template lang="pug">
|
||||
b-modal#generic-achievement(:title='data.message', size='md', :hide-footer='true')
|
||||
.modal-body
|
||||
.col-12
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
p(v-html='data.modalText')
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</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 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
props: ['data'],
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'generic-achievement');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
46
website/client/components/achievements/justAddWater.vue
Normal file
46
website/client/components/achievements/justAddWater.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template lang="pug">
|
||||
b-modal#just-add-water(:title='title', size='md', :hide-footer='true')
|
||||
.modal-body
|
||||
.col-12
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
p {{ $t('achievementJustAddWaterModalText') }}
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</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 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
title: `${this.$t('modalAchievement')} ${this.$t('achievementJustAddWater')}`,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'just-add-water');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
46
website/client/components/achievements/lostMasterclasser.vue
Normal file
46
website/client/components/achievements/lostMasterclasser.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template lang="pug">
|
||||
b-modal#lost-masterclasser(:title='title', size='md', :hide-footer='true')
|
||||
.modal-body
|
||||
.col-12
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
p {{ $t('achievementLostMasterclasserModalText') }}
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</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 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
title: `${this.$t('modalAchievement')} ${this.$t('achievementLostMasterclasser')}`,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'lost-masterclasser');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
46
website/client/components/achievements/mindOverMatter.vue
Normal file
46
website/client/components/achievements/mindOverMatter.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template lang="pug">
|
||||
b-modal#mind-over-matter(:title='title', size='md', :hide-footer='true')
|
||||
.modal-body
|
||||
.col-12
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
p {{ $t('achievementMindOverMatterModalText') }}
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</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 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
title: `${this.$t('modalAchievement')} ${this.$t('achievementMindOverMatter')}`,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'mind-over-matter');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action"
|
||||
)
|
||||
div(slot="content", v-html="achievementString")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
computed: {
|
||||
achievementString () {
|
||||
return `<strong>${this.$t('achievement')}</strong>: ${this.$t('achievementJustAddWater')}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$root.$emit('bv::show::modal', 'just-add-water');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action"
|
||||
)
|
||||
div(slot="content", v-html="achievementString")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
computed: {
|
||||
achievementString () {
|
||||
return `<strong>${this.$t('achievement')}</strong>: ${this.$t('achievementLostMasterclasser')}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$root.$emit('bv::show::modal', 'lost-masterclasser');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action"
|
||||
)
|
||||
div(slot="content", v-html="achievementString")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
computed: {
|
||||
achievementString () {
|
||||
return `<strong>${this.$t('achievement')}</strong>: ${this.$t('achievementMindOverMatter')}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$root.$emit('bv::show::modal', 'mind-over-matter');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -95,6 +95,9 @@ import NEW_INBOX_MESSAGE from './notifications/newInboxMessage';
|
||||
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';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -106,6 +109,7 @@ export default {
|
||||
QUEST_INVITATION, GROUP_TASK_APPROVAL, GROUP_TASK_APPROVED, GROUP_TASK_ASSIGNED,
|
||||
UNALLOCATED_STATS_POINTS, NEW_MYSTERY_ITEMS, CARD_RECEIVED,
|
||||
NEW_INBOX_MESSAGE, NEW_CHAT_MESSAGE,
|
||||
ACHIEVEMENT_JUST_ADD_WATER, ACHIEVEMENT_LOST_MASTERCLASSER, ACHIEVEMENT_MIND_OVER_MATTER,
|
||||
WorldBoss: WORLD_BOSS,
|
||||
VERIFY_USERNAME,
|
||||
},
|
||||
@@ -130,6 +134,7 @@ export default {
|
||||
'QUEST_INVITATION', 'GROUP_TASK_ASSIGNED', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED',
|
||||
'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',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -26,6 +26,10 @@ div
|
||||
quest-completed
|
||||
quest-invitation
|
||||
verify-username
|
||||
generic-achievement(:data='notificationData')
|
||||
just-add-water
|
||||
lost-masterclasser
|
||||
mind-over-matter
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
@@ -118,6 +122,10 @@ import rebirth from './achievements/rebirth';
|
||||
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 verifyUsername from './settings/verifyUsername';
|
||||
|
||||
@@ -147,6 +155,16 @@ const NOTIFICATIONS = {
|
||||
label: ($t) => $t('modalContribAchievement'),
|
||||
modalId: 'contributor',
|
||||
},
|
||||
ACHIEVEMENT_ALL_YOUR_BASE: {
|
||||
achievement: true,
|
||||
label: ($t) => `${$t('achievement')}: ${$t('achievementAllYourBase')}`,
|
||||
modalId: 'generic-achievement',
|
||||
},
|
||||
ACHIEVEMENT_BACK_TO_BASICS: {
|
||||
achievement: true,
|
||||
label: ($t) => `${$t('achievement')}: ${$t('achievementBackToBasics')}`,
|
||||
modalId: 'generic-achievement',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
@@ -176,6 +194,10 @@ export default {
|
||||
contributor,
|
||||
loginIncentives,
|
||||
verifyUsername,
|
||||
genericAchievement,
|
||||
lostMasterclasser,
|
||||
mindOverMatter,
|
||||
justAddWater,
|
||||
},
|
||||
data () {
|
||||
// Levels that already display modals and should not trigger generic Level Up
|
||||
@@ -197,7 +219,8 @@ export default {
|
||||
'GUILD_PROMPT', 'DROPS_ENABLED', 'REBIRTH_ENABLED', 'WON_CHALLENGE', 'STREAK_ACHIEVEMENT',
|
||||
'ULTIMATE_GEAR_ACHIEVEMENT', 'REBIRTH_ACHIEVEMENT', 'GUILD_JOINED_ACHIEVEMENT',
|
||||
'CHALLENGE_JOINED_ACHIEVEMENT', 'INVITED_FRIEND_ACHIEVEMENT', 'NEW_CONTRIBUTOR_LEVEL',
|
||||
'CRON', 'SCORED_TASK', 'LOGIN_INCENTIVE',
|
||||
'CRON', 'SCORED_TASK', 'LOGIN_INCENTIVE', 'ACHIEVEMENT_ALL_YOUR_BASE', 'ACHIEVEMENT_BACK_TO_BASICS',
|
||||
'GENERIC_ACHIEVEMENT',
|
||||
].forEach(type => {
|
||||
handledNotifications[type] = true;
|
||||
});
|
||||
@@ -342,8 +365,8 @@ export default {
|
||||
this.playSound('Death');
|
||||
this.$root.$emit('bv::show::modal', 'death');
|
||||
},
|
||||
showNotificationWithModal (type, forceToModal) {
|
||||
const config = NOTIFICATIONS[type];
|
||||
showNotificationWithModal (notification, forceToModal) {
|
||||
const config = NOTIFICATIONS[notification.type];
|
||||
|
||||
if (!config) {
|
||||
return;
|
||||
@@ -355,6 +378,10 @@ export default {
|
||||
this.playSound(config.sound);
|
||||
}
|
||||
|
||||
if (notification.data) {
|
||||
this.notificationData = notification.data;
|
||||
}
|
||||
|
||||
if (forceToModal) {
|
||||
this.$root.$emit('bv::show::modal', config.modalId);
|
||||
} else {
|
||||
@@ -566,7 +593,10 @@ export default {
|
||||
case 'CHALLENGE_JOINED_ACHIEVEMENT':
|
||||
case 'INVITED_FRIEND_ACHIEVEMENT':
|
||||
case 'NEW_CONTRIBUTOR_LEVEL':
|
||||
this.showNotificationWithModal(notification.type);
|
||||
case 'ACHIEVEMENT_ALL_YOUR_BASE':
|
||||
case 'ACHIEVEMENT_BACK_TO_BASICS':
|
||||
case 'GENERIC_ACHIEVEMENT':
|
||||
this.showNotificationWithModal(notification);
|
||||
break;
|
||||
case 'CRON':
|
||||
if (notification.data) {
|
||||
|
||||
@@ -6,12 +6,17 @@
|
||||
"reachedLevel": "You Reached Level <%= level %>",
|
||||
"achievementLostMasterclasser": "Quest Completionist: Masterclasser Series",
|
||||
"achievementLostMasterclasserText": "Completed all sixteen quests in the Masterclasser Quest Series and solved the mystery of the Lost Masterclasser!",
|
||||
"achievementLostMasterclasserModalText": "You completed all sixteen quests in the Masterclasser Quest Series and solved the mystery of the Lost Masterclasser!",
|
||||
"achievementMindOverMatter": "Mind Over Matter",
|
||||
"achievementMindOverMatterText": "Has completed Rock, Slime, and Yarn pet quests.",
|
||||
"achievementMindOverMatterModalText": "You completed the Rock, Slime, and Yarn pet quests!",
|
||||
"achievementJustAddWater": "Just Add Water",
|
||||
"achievementJustAddWaterText": "Has completed Octopus, Seahorse, Cuttlefish, Whale, Turtle, Nudibranch, Sea Serpent, and Dolphin pet quests.",
|
||||
"achievementJustAddWaterModalText": "You completed the Octopus, Seahorse, Cuttlefish, Whale, Turtle, Nudibranch, Sea Serpent, and Dolphin pet quests!",
|
||||
"achievementBackToBasics": "Back to Basics",
|
||||
"achievementBackToBasicsText": "Has collected all Base Pets.",
|
||||
"achievementBackToBasicsModalText": "You collected all the Base Pets!",
|
||||
"achievementAllYourBase": "All Your Base",
|
||||
"achievementAllYourBaseText": "Has tamed all Base Mounts."
|
||||
"achievementAllYourBaseText": "Has tamed all Base Mounts.",
|
||||
"achievementAllYourBaseModalText": "You tamed all the Base Mounts!"
|
||||
}
|
||||
|
||||
@@ -96,6 +96,13 @@ module.exports = function feed (user, req = {}) {
|
||||
});
|
||||
if (mountIndex === -1) {
|
||||
user.achievements.allYourBase = true;
|
||||
if (user.addNotification) {
|
||||
user.addNotification('ACHIEVEMENT_ALL_YOUR_BASE', {
|
||||
achievement: 'allYourBase',
|
||||
message: `${i18n.t('modalAchievement')} ${i18n.t('achievementAllYourBase')}`,
|
||||
modalText: i18n.t('achievementAllYourBaseModalText'),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,13 @@ module.exports = function hatch (user, req = {}) {
|
||||
});
|
||||
if (petIndex === -1) {
|
||||
user.achievements.backToBasics = true;
|
||||
if (user.addNotification) {
|
||||
user.addNotification('ACHIEVEMENT_BACK_TO_BASICS', {
|
||||
achievement: 'backToBasics',
|
||||
message: `${i18n.t('modalAchievement')} ${i18n.t('achievementBackToBasics')}`,
|
||||
modalText: i18n.t('achievementBackToBasicsModalText'),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -886,8 +886,20 @@ schema.methods.finishQuest = async function finishQuest (quest) {
|
||||
}
|
||||
});
|
||||
|
||||
let questAchievementUpdate = {$set: {}};
|
||||
let 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,
|
||||
data: {
|
||||
achievement,
|
||||
message: `${shared.i18n.t('modalAchievement')} ${shared.i18n.t(`achievement${achievementTitleCase}`)}`,
|
||||
modalText: shared.i18n.t(`achievement${achievementTitleCase}ModalText`),
|
||||
},
|
||||
}).toObject(),
|
||||
};
|
||||
|
||||
promises.push(participants.map(userId => {
|
||||
return _updateUserWithRetries(userId, questAchievementUpdate, null, questAchievementQuery);
|
||||
|
||||
@@ -32,6 +32,10 @@ const NOTIFICATION_TYPES = [
|
||||
'NEW_STUFF',
|
||||
'NEW_CHAT_MESSAGE',
|
||||
'LEVELED_UP',
|
||||
'ACHIEVEMENT_ALL_YOUR_BASE',
|
||||
'ACHIEVEMENT_BACK_TO_BASICS',
|
||||
'ACHIEVEMENT_JUST_ADD_WATER',
|
||||
'ACHIEVEMENT_MIND_OVER_MATTER',
|
||||
];
|
||||
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
Reference in New Issue
Block a user