mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 15:17:25 +01:00
Show accurate experience notifications (#10676)
* Show accurate experience notifications Add unit tests for exp notifications * use array to compute exp and lvl values for notification changes * Add tests for user loosing xp cases
This commit is contained in:
committed by
Matteo Pagliazzi
parent
eee5f2f1df
commit
fe39ef72ff
@@ -2,12 +2,14 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
|
|||||||
import NotificationsComponent from 'client/components/notifications.vue';
|
import NotificationsComponent from 'client/components/notifications.vue';
|
||||||
import Store from 'client/libs/store';
|
import Store from 'client/libs/store';
|
||||||
import { hasClass } from 'client/store/getters/members';
|
import { hasClass } from 'client/store/getters/members';
|
||||||
|
import { toNextLevel } from 'common/script/statHelpers';
|
||||||
|
|
||||||
const localVue = createLocalVue();
|
const localVue = createLocalVue();
|
||||||
localVue.use(Store);
|
localVue.use(Store);
|
||||||
|
|
||||||
describe('Notifications', () => {
|
describe('Notifications', () => {
|
||||||
let store;
|
let store;
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
store = new Store({
|
store = new Store({
|
||||||
@@ -29,16 +31,22 @@ describe('Notifications', () => {
|
|||||||
actions: {
|
actions: {
|
||||||
'user:fetch': () => {},
|
'user:fetch': () => {},
|
||||||
'tasks:fetchUserTasks': () => {},
|
'tasks:fetchUserTasks': () => {},
|
||||||
|
'snackbars:add': () => {},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
'members:hasClass': hasClass,
|
'members:hasClass': hasClass,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
wrapper = shallowMount(NotificationsComponent, {
|
||||||
|
store,
|
||||||
|
localVue,
|
||||||
|
mocks: {
|
||||||
|
$t: (string) => string,
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('set user has class computed prop', () => {
|
it('set user has class computed prop', () => {
|
||||||
const wrapper = shallowMount(NotificationsComponent, { store, localVue });
|
|
||||||
|
|
||||||
expect(wrapper.vm.userHasClass).to.be.false;
|
expect(wrapper.vm.userHasClass).to.be.false;
|
||||||
|
|
||||||
store.state.user.data.stats.lvl = 10;
|
store.state.user.data.stats.lvl = 10;
|
||||||
@@ -47,4 +55,130 @@ describe('Notifications', () => {
|
|||||||
|
|
||||||
expect(wrapper.vm.userHasClass).to.be.true;
|
expect(wrapper.vm.userHasClass).to.be.true;
|
||||||
});
|
});
|
||||||
|
describe('user exp notifcation', () => {
|
||||||
|
it('notifies when user gets more exp', () => {
|
||||||
|
const expSpy = sinon.spy(wrapper.vm, 'exp');
|
||||||
|
|
||||||
|
const userLevel = 10;
|
||||||
|
store.state.user.data.stats.lvl = userLevel;
|
||||||
|
|
||||||
|
const userExpBefore = 10;
|
||||||
|
const userExpAfter = 12;
|
||||||
|
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevel, userLevel);
|
||||||
|
|
||||||
|
expect(expSpy).to.be.calledWith(userExpAfter - userExpBefore);
|
||||||
|
expSpy.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when user levels with exact xp', () => {
|
||||||
|
const expSpy = sinon.spy(wrapper.vm, 'exp');
|
||||||
|
|
||||||
|
const userLevelBefore = 9;
|
||||||
|
const userLevelAfter = 10;
|
||||||
|
store.state.user.data.stats.lvl = userLevelAfter;
|
||||||
|
|
||||||
|
const expEarned = 5;
|
||||||
|
const userExpBefore = toNextLevel(userLevelBefore) - expEarned;
|
||||||
|
const userExpAfter = 0;
|
||||||
|
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevelAfter, userLevelBefore);
|
||||||
|
|
||||||
|
expect(expSpy).to.be.calledWith(expEarned);
|
||||||
|
expSpy.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when user levels with exact more exp than needed', () => {
|
||||||
|
const expSpy = sinon.spy(wrapper.vm, 'exp');
|
||||||
|
|
||||||
|
const userLevelBefore = 9;
|
||||||
|
const userLevelAfter = 10;
|
||||||
|
store.state.user.data.stats.lvl = userLevelAfter;
|
||||||
|
|
||||||
|
const expEarned = 10;
|
||||||
|
const expNeeded = 5;
|
||||||
|
const userExpBefore = toNextLevel(userLevelBefore) - expNeeded;
|
||||||
|
const userExpAfter = 5;
|
||||||
|
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevelAfter, userLevelBefore);
|
||||||
|
|
||||||
|
expect(expSpy).to.be.calledWith(expEarned);
|
||||||
|
expSpy.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when user has more exp than needed then levels', () => {
|
||||||
|
const expSpy = sinon.spy(wrapper.vm, 'exp');
|
||||||
|
|
||||||
|
const userLevelBefore = 9;
|
||||||
|
const userLevelAfter = 10;
|
||||||
|
store.state.user.data.stats.lvl = userLevelAfter;
|
||||||
|
|
||||||
|
const expEarned = 10;
|
||||||
|
const expNeeded = -5;
|
||||||
|
const userExpBefore = toNextLevel(userLevelBefore) - expNeeded;
|
||||||
|
const userExpAfter = 15;
|
||||||
|
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevelAfter, userLevelBefore);
|
||||||
|
|
||||||
|
expect(expSpy).to.be.calledWith(expEarned);
|
||||||
|
expSpy.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when user multilevels', () => {
|
||||||
|
const expSpy = sinon.spy(wrapper.vm, 'exp');
|
||||||
|
|
||||||
|
const userLevelBefore = 8;
|
||||||
|
const userLevelAfter = 10;
|
||||||
|
store.state.user.data.stats.lvl = userLevelAfter;
|
||||||
|
|
||||||
|
const expEarned = 10 + toNextLevel(userLevelBefore + 1);
|
||||||
|
const expNeeded = 5;
|
||||||
|
const userExpBefore = toNextLevel(userLevelBefore) - expNeeded;
|
||||||
|
const userExpAfter = 5;
|
||||||
|
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevelAfter, userLevelBefore);
|
||||||
|
|
||||||
|
expect(expSpy).to.be.calledWith(expEarned);
|
||||||
|
expSpy.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when user looses xp', () => {
|
||||||
|
const expSpy = sinon.spy(wrapper.vm, 'exp');
|
||||||
|
|
||||||
|
const userLevel = 10;
|
||||||
|
store.state.user.data.stats.lvl = userLevel;
|
||||||
|
|
||||||
|
const userExpBefore = 10;
|
||||||
|
const userExpAfter = 5;
|
||||||
|
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevel, userLevel);
|
||||||
|
|
||||||
|
expect(expSpy).to.be.calledWith(userExpAfter - userExpBefore);
|
||||||
|
expSpy.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when user looses xp under 0', () => {
|
||||||
|
const expSpy = sinon.spy(wrapper.vm, 'exp');
|
||||||
|
|
||||||
|
const userLevel = 10;
|
||||||
|
store.state.user.data.stats.lvl = userLevel;
|
||||||
|
|
||||||
|
const userExpBefore = 5;
|
||||||
|
const userExpAfter = -3;
|
||||||
|
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevel, userLevel);
|
||||||
|
|
||||||
|
expect(expSpy).to.be.calledWith(userExpAfter - userExpBefore);
|
||||||
|
expSpy.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when user dies', () => {
|
||||||
|
const expSpy = sinon.spy(wrapper.vm, 'exp');
|
||||||
|
|
||||||
|
const userLevelBefore = 10;
|
||||||
|
const userLevelAfter = 9;
|
||||||
|
store.state.user.data.stats.lvl = userLevelAfter;
|
||||||
|
|
||||||
|
const expEarned = -20;
|
||||||
|
const userExpBefore = 20;
|
||||||
|
const userExpAfter = 0;
|
||||||
|
wrapper.vm.displayUserExpAndLvlNotifications(userExpAfter, userExpBefore, userLevelAfter, userLevelBefore);
|
||||||
|
|
||||||
|
expect(expSpy).to.be.calledWith(expEarned);
|
||||||
|
expSpy.restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ import axios from 'axios';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
|
|
||||||
|
import { toNextLevel } from '../../common/script/statHelpers';
|
||||||
import { shouldDo } from '../../common/script/cron';
|
import { shouldDo } from '../../common/script/cron';
|
||||||
import { mapState } from 'client/libs/store';
|
import { mapState } from 'client/libs/store';
|
||||||
import notifications from 'client/mixins/notifications';
|
import notifications from 'client/mixins/notifications';
|
||||||
@@ -186,10 +187,8 @@ export default {
|
|||||||
...mapState({
|
...mapState({
|
||||||
user: 'user.data',
|
user: 'user.data',
|
||||||
userHp: 'user.data.stats.hp',
|
userHp: 'user.data.stats.hp',
|
||||||
userExp: 'user.data.stats.exp',
|
|
||||||
userGp: 'user.data.stats.gp',
|
userGp: 'user.data.stats.gp',
|
||||||
userMp: 'user.data.stats.mp',
|
userMp: 'user.data.stats.mp',
|
||||||
userLvl: 'user.data.stats.lvl',
|
|
||||||
userNotifications: 'user.data.notifications',
|
userNotifications: 'user.data.notifications',
|
||||||
userAchievements: 'user.data.achievements', // @TODO: does this watch deeply?
|
userAchievements: 'user.data.achievements', // @TODO: does this watch deeply?
|
||||||
armoireEmpty: 'user.data.flags.armoireEmpty',
|
armoireEmpty: 'user.data.flags.armoireEmpty',
|
||||||
@@ -204,6 +203,9 @@ export default {
|
|||||||
invitedToQuest () {
|
invitedToQuest () {
|
||||||
return this.user.party.quest.RSVPNeeded && !this.user.party.quest.completed;
|
return this.user.party.quest.RSVPNeeded && !this.user.party.quest.completed;
|
||||||
},
|
},
|
||||||
|
userExpAndLvl () {
|
||||||
|
return [this.user.stats.exp, this.user.stats.lvl];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
userHp (after, before) {
|
userHp (after, before) {
|
||||||
@@ -222,11 +224,6 @@ export default {
|
|||||||
|
|
||||||
if (after < 0) this.playSound('Minus_Habit');
|
if (after < 0) this.playSound('Minus_Habit');
|
||||||
},
|
},
|
||||||
userExp (after, before) {
|
|
||||||
if (after === before) return;
|
|
||||||
if (this.user.stats.lvl === 0) return;
|
|
||||||
this.exp(after - before);
|
|
||||||
},
|
|
||||||
userGp (after, before) {
|
userGp (after, before) {
|
||||||
if (after === before) return;
|
if (after === before) return;
|
||||||
if (this.user.stats.lvl === 0) return;
|
if (this.user.stats.lvl === 0) return;
|
||||||
@@ -252,10 +249,6 @@ export default {
|
|||||||
const mana = after - before;
|
const mana = after - before;
|
||||||
this.mp(mana);
|
this.mp(mana);
|
||||||
},
|
},
|
||||||
userLvl (after, before) {
|
|
||||||
if (after <= before || this.$store.state.isRunningYesterdailies) return;
|
|
||||||
this.showLevelUpNotifications(after);
|
|
||||||
},
|
|
||||||
userClassSelect (after) {
|
userClassSelect (after) {
|
||||||
if (this.user.needsCron) return;
|
if (this.user.needsCron) return;
|
||||||
if (!after) return;
|
if (!after) return;
|
||||||
@@ -279,6 +272,9 @@ export default {
|
|||||||
if (after !== true) return;
|
if (after !== true) return;
|
||||||
this.$root.$emit('bv::show::modal', 'quest-invitation');
|
this.$root.$emit('bv::show::modal', 'quest-invitation');
|
||||||
},
|
},
|
||||||
|
userExpAndLvl (after, before) {
|
||||||
|
this.displayUserExpAndLvlNotifications(after[0], before[0], after[1], before[1]);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
@@ -310,6 +306,35 @@ export default {
|
|||||||
document.removeEventListener('keydown', this.checkNextCron);
|
document.removeEventListener('keydown', this.checkNextCron);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
displayUserExpAndLvlNotifications (afterExp, beforeExp, afterLvl, beforeLvl) {
|
||||||
|
if (afterExp === beforeExp && afterLvl === beforeLvl) return;
|
||||||
|
|
||||||
|
// XP evaluation
|
||||||
|
if (afterExp !== beforeExp) {
|
||||||
|
if (this.user.stats.lvl === 0) return;
|
||||||
|
|
||||||
|
const lvlUps = afterLvl - beforeLvl;
|
||||||
|
let exp = afterExp - beforeExp;
|
||||||
|
|
||||||
|
if (lvlUps > 0) {
|
||||||
|
let level = Math.trunc(beforeLvl);
|
||||||
|
exp += toNextLevel(level);
|
||||||
|
|
||||||
|
// loop if more than 1 lvl up
|
||||||
|
for (let i = 1; i < lvlUps; i += 1) {
|
||||||
|
level += 1;
|
||||||
|
exp += toNextLevel(level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.exp(exp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lvl evaluation
|
||||||
|
if (afterLvl !== beforeLvl) {
|
||||||
|
if (afterLvl <= beforeLvl || this.$store.state.isRunningYesterdailies) return;
|
||||||
|
this.showLevelUpNotifications(afterLvl);
|
||||||
|
}
|
||||||
|
},
|
||||||
checkUserAchievements () {
|
checkUserAchievements () {
|
||||||
if (this.user.needsCron) return;
|
if (this.user.needsCron) return;
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,5 @@ export function round (number, nDigits) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getXPMessage (val) {
|
export function getXPMessage (val) {
|
||||||
if (val < -50) return; // don't show when they level up (resetting their exp)
|
|
||||||
return `${getSign(val)} ${round(val)}`;
|
return `${getSign(val)} ${round(val)}`;
|
||||||
}
|
}
|
||||||
@@ -29,9 +29,7 @@ export default {
|
|||||||
},
|
},
|
||||||
exp (val) {
|
exp (val) {
|
||||||
const message = getXPMessage(val);
|
const message = getXPMessage(val);
|
||||||
if (message) {
|
this.notify(message, 'xp', 'glyphicon glyphicon-star', this.sign(val));
|
||||||
this.notify(message, 'xp', 'glyphicon glyphicon-star', this.sign(val));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
error (error) {
|
error (error) {
|
||||||
this.notify(error, 'error', 'glyphicon glyphicon-exclamation-sign');
|
this.notify(error, 'error', 'glyphicon glyphicon-exclamation-sign');
|
||||||
|
|||||||
Reference in New Issue
Block a user