mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 22:57:21 +01:00
Added tests for cron (#7081)
* Added inital cron tests * Added more subscribe tests and updated various tests for syntax and expectations
This commit is contained in:
committed by
Matteo Pagliazzi
parent
c608d03e35
commit
050539d8f3
573
test/api/v3/unit/libs/cron.test.js
Normal file
573
test/api/v3/unit/libs/cron.test.js
Normal file
@@ -0,0 +1,573 @@
|
|||||||
|
/* eslint-disable global-require */
|
||||||
|
import moment from 'moment';
|
||||||
|
import { cron } from '../../../../../website/src/libs/api-v3/cron';
|
||||||
|
import { model as User } from '../../../../../website/src/models/user';
|
||||||
|
import * as Tasks from '../../../../../website/src/models/task';
|
||||||
|
import { clone } from 'lodash';
|
||||||
|
import common from '../../../../../common';
|
||||||
|
|
||||||
|
// const scoreTask = common.ops.scoreTask;
|
||||||
|
|
||||||
|
describe('cron', () => {
|
||||||
|
let user;
|
||||||
|
let tasksByType = {habits: [], dailys: [], todos: [], rewards: []};
|
||||||
|
let daysMissed = 0;
|
||||||
|
let analytics = {
|
||||||
|
track: sinon.spy(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
user = new User({
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: 'username',
|
||||||
|
lowerCaseUsername: 'username',
|
||||||
|
email: 'email@email.email',
|
||||||
|
salt: 'salt',
|
||||||
|
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
user._statsComputed = {
|
||||||
|
mp: 10,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates user.auth.timestamps.loggedin and lastCron', () => {
|
||||||
|
let now = new Date();
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics, now});
|
||||||
|
|
||||||
|
expect(user.auth.timestamps.loggedin).to.equal(now);
|
||||||
|
expect(user.lastCron).to.equal(now);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates user.preferences.timezoneOffsetAtLastCron', () => {
|
||||||
|
let timezoneOffsetFromUserPrefs = 1;
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics, timezoneOffsetFromUserPrefs});
|
||||||
|
|
||||||
|
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(timezoneOffsetFromUserPrefs);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets user.items.lastDrop.count', () => {
|
||||||
|
user.items.lastDrop.count = 4;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.items.lastDrop.count).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('increments user cron count', () => {
|
||||||
|
let cronCountBefore = user.flags.cronCount;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.flags.cronCount).to.be.greaterThan(cronCountBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('end of the month perks', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
user.purchased.plan.customerId = 'subscribedId';
|
||||||
|
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets plan.gemsBought on a new month', () => {
|
||||||
|
user.purchased.plan.gemsBought = 10;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets plan.dateUpdated on a new month', () => {
|
||||||
|
let currentMonth = moment().format('MMYYYY');
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(moment(user.purchased.plan.dateUpdated).format('MMYYYY')).to.equal(currentMonth);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('increments plan.consecutive.count', () => {
|
||||||
|
user.purchased.plan.consecutive.count = 0;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.purchased.plan.consecutive.count).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('decrements plan.consecutive.offset when offset is greater than 0', () => {
|
||||||
|
user.purchased.plan.consecutive.offset = 1;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.purchased.plan.consecutive.offset).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('increments plan.consecutive.trinkets when user has reached a month that is a multiple of 3', () => {
|
||||||
|
user.purchased.plan.consecutive.count = 5;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('increments plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', () => {
|
||||||
|
user.purchased.plan.consecutive.count = 5;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', () => {
|
||||||
|
user.purchased.plan.consecutive.gemCapExtra = 25;
|
||||||
|
user.purchased.plan.consecutive.count = 5;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(25);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not reset plan stats if we are before the last day of the cancelled month', () => {
|
||||||
|
user.purchased.plan.dateTerminated = moment(new Date()).add({days: 1});
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.purchased.plan.customerId).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does reset plan stats until we are after the last day of the cancelled month', () => {
|
||||||
|
user.purchased.plan.dateTerminated = moment(new Date()).subtract({days: 1});
|
||||||
|
user.purchased.plan.consecutive.gemCapExtra = 20;
|
||||||
|
user.purchased.plan.consecutive.count = 5;
|
||||||
|
user.purchased.plan.consecutive.offset = 1;
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(user.purchased.plan.customerId).to.not.exist;
|
||||||
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.be.empty;
|
||||||
|
expect(user.purchased.plan.consecutive.count).to.be.empty;
|
||||||
|
expect(user.purchased.plan.consecutive.offset).to.be.empty;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('end of the month perks when user is not subscribed', () => {
|
||||||
|
it('does not reset plan.gemsBought on a new month', () => {
|
||||||
|
user.purchased.plan.gemsBought = 10;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.purchased.plan.gemsBought).to.equal(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not reset plan.dateUpdated on a new month', () => {
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.purchased.plan.dateUpdated).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not increment plan.consecutive.count', () => {
|
||||||
|
user.purchased.plan.consecutive.count = 0;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.purchased.plan.consecutive.count).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not decrement plan.consecutive.offset when offset is greater than 0', () => {
|
||||||
|
user.purchased.plan.consecutive.offset = 1;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.purchased.plan.consecutive.offset).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not increment plan.consecutive.trinkets when user has reached a month that is a multiple of 3', () => {
|
||||||
|
user.purchased.plan.consecutive.count = 5;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.purchased.plan.consecutive.trinkets).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('doest not increment plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', () => {
|
||||||
|
user.purchased.plan.consecutive.count = 5;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', () => {
|
||||||
|
user.purchased.plan.consecutive.gemCapExtra = 25;
|
||||||
|
user.purchased.plan.consecutive.count = 5;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(25);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing to plan stats if we are before the last day of the cancelled month', () => {
|
||||||
|
user.purchased.plan.dateTerminated = moment(new Date()).add({days: 1});
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.purchased.plan.customerId).to.not.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
xit('does nothing to plan stats when we are after the last day of the cancelled month', () => {
|
||||||
|
user.purchased.plan.dateTerminated = moment(new Date()).subtract({days: 1});
|
||||||
|
user.purchased.plan.consecutive.gemCapExtra = 20;
|
||||||
|
user.purchased.plan.consecutive.count = 5;
|
||||||
|
user.purchased.plan.consecutive.offset = 1;
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(user.purchased.plan.customerId).to.exist;
|
||||||
|
expect(user.purchased.plan.consecutive.gemCapExtra).to.exist;
|
||||||
|
expect(user.purchased.plan.consecutive.count).to.exist;
|
||||||
|
expect(user.purchased.plan.consecutive.offset).to.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('user is sleeping', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
user.preferences.sleep = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears user buffs', () => {
|
||||||
|
user.stats.buffs = {
|
||||||
|
str: 1,
|
||||||
|
int: 1,
|
||||||
|
per: 1,
|
||||||
|
con: 1,
|
||||||
|
stealth: 1,
|
||||||
|
streaks: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(user.stats.buffs.str).to.equal(0);
|
||||||
|
expect(user.stats.buffs.int).to.equal(0);
|
||||||
|
expect(user.stats.buffs.per).to.equal(0);
|
||||||
|
expect(user.stats.buffs.con).to.equal(0);
|
||||||
|
expect(user.stats.buffs.stealth).to.equal(0);
|
||||||
|
expect(user.stats.buffs.streaks).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets all dailies without damaging user', () => {
|
||||||
|
let daily = {
|
||||||
|
text: 'test daily',
|
||||||
|
type: 'daily',
|
||||||
|
frequency: 'daily',
|
||||||
|
everyX: 5,
|
||||||
|
startDate: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
|
||||||
|
tasksByType.dailys.push(task);
|
||||||
|
tasksByType.dailys[0].completed = true;
|
||||||
|
|
||||||
|
let healthBefore = user.stats.hp;
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||||
|
expect(user.stats.hp).to.equal(healthBefore);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('todos', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
let todo = {
|
||||||
|
text: 'test todo',
|
||||||
|
type: 'todo',
|
||||||
|
value: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line babel/new-cap
|
||||||
|
tasksByType.todos.push(task);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should make uncompleted todos redder', () => {
|
||||||
|
let valueBefore = tasksByType.todos[0].value;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(tasksByType.todos[0].value).to.be.lessThan(valueBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add history of completed todos to user history', () => {
|
||||||
|
tasksByType.todos[0].completed = true;
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(user.history.todos).to.be.lengthOf(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dailys', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
let daily = {
|
||||||
|
text: 'test daily',
|
||||||
|
type: 'daily',
|
||||||
|
};
|
||||||
|
|
||||||
|
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
|
||||||
|
tasksByType.dailys = [];
|
||||||
|
tasksByType.dailys.push(task);
|
||||||
|
|
||||||
|
user._statsComputed = {
|
||||||
|
con: 1,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add history', () => {
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set tasks completed to false', () => {
|
||||||
|
tasksByType.dailys[0].completed = true;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set task checklist to completed for completed dailys', () => {
|
||||||
|
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
|
||||||
|
tasksByType.dailys[0].completed = true;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(tasksByType.dailys[0].checklist[0].completed).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set task checklist to completed for dailys with scheduled misses', () => {
|
||||||
|
daysMissed = 10;
|
||||||
|
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
|
||||||
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(tasksByType.dailys[0].checklist[0].completed).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do damage for missing a daily', () => {
|
||||||
|
daysMissed = 1;
|
||||||
|
let hpBefore = user.stats.hp;
|
||||||
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(user.stats.hp).to.be.lessThan(hpBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not do damage for missing a daily if user stealth buff is greater than or equal to days missed', () => {
|
||||||
|
daysMissed = 1;
|
||||||
|
let hpBefore = user.stats.hp;
|
||||||
|
user.stats.buffs.stealth = 2;
|
||||||
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(user.stats.hp).to.equal(hpBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do less damage for missing a daily with partial completion', () => {
|
||||||
|
daysMissed = 1;
|
||||||
|
let hpBefore = user.stats.hp;
|
||||||
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
let hpDifferenceOfFullyIncompleteDaily = hpBefore - user.stats.hp;
|
||||||
|
|
||||||
|
hpBefore = user.stats.hp;
|
||||||
|
tasksByType.dailys[0].checklist.push({title: 'test', completed: true});
|
||||||
|
tasksByType.dailys[0].checklist.push({title: 'test2', completed: false});
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
let hpDifferenceOfPartiallyIncompleteDaily = hpBefore - user.stats.hp;
|
||||||
|
|
||||||
|
expect(hpDifferenceOfPartiallyIncompleteDaily).to.be.lessThan(hpDifferenceOfFullyIncompleteDaily);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should decrement quest progress down for missing a daily', () => {
|
||||||
|
daysMissed = 1;
|
||||||
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||||
|
|
||||||
|
let progress = cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(progress.down).to.equal(-1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('habits', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
let habit = {
|
||||||
|
text: 'test habit',
|
||||||
|
type: 'habit',
|
||||||
|
};
|
||||||
|
|
||||||
|
let task = new Tasks.habit(Tasks.Task.sanitize(habit)); // eslint-disable-line babel/new-cap
|
||||||
|
tasksByType.habits = [];
|
||||||
|
tasksByType.habits.push(task);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should decrement only up value', () => {
|
||||||
|
tasksByType.habits[0].value = 1;
|
||||||
|
tasksByType.habits[0].down = false;
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(tasksByType.habits[0].value).to.be.lessThan(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should decrement only down value', () => {
|
||||||
|
tasksByType.habits[0].value = 1;
|
||||||
|
tasksByType.habits[0].up = false;
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(tasksByType.habits[0].value).to.be.lessThan(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing to habits with both up and down', () => {
|
||||||
|
tasksByType.habits[0].value = 1;
|
||||||
|
tasksByType.habits[0].up = true;
|
||||||
|
tasksByType.habits[0].down = true;
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(tasksByType.habits[0].value).to.equal(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('perfect day', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
let daily = {
|
||||||
|
text: 'test daily',
|
||||||
|
type: 'daily',
|
||||||
|
};
|
||||||
|
|
||||||
|
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
|
||||||
|
tasksByType.dailys = [];
|
||||||
|
tasksByType.dailys.push(task);
|
||||||
|
|
||||||
|
user._statsComputed = {
|
||||||
|
con: 1,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores a new entry in user.history.exp', () => {
|
||||||
|
user.stats.lvl = 2;
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(user.history.exp).to.have.lengthOf(1);
|
||||||
|
expect(user.history.exp[0].value).to.equal(150);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('increments perfect day achievement', () => {
|
||||||
|
tasksByType.dailys[0].completed = true;
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(user.achievements.perfect).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('increments user buffs if they have a perfect day', () => {
|
||||||
|
tasksByType.dailys[0].completed = true;
|
||||||
|
|
||||||
|
let previousBuffs = clone(user.stats.buffs);
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||||
|
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
|
||||||
|
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
|
||||||
|
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears buffs if user does not have a perfect day', () => {
|
||||||
|
daysMissed = 1;
|
||||||
|
tasksByType.dailys[0].completed = false;
|
||||||
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||||
|
|
||||||
|
user.stats.buffs = {
|
||||||
|
str: 1,
|
||||||
|
int: 1,
|
||||||
|
per: 1,
|
||||||
|
con: 1,
|
||||||
|
stealth: 0,
|
||||||
|
streaks: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(user.stats.buffs.str).to.equal(0);
|
||||||
|
expect(user.stats.buffs.int).to.equal(0);
|
||||||
|
expect(user.stats.buffs.per).to.equal(0);
|
||||||
|
expect(user.stats.buffs.con).to.equal(0);
|
||||||
|
expect(user.stats.buffs.stealth).to.equal(0);
|
||||||
|
expect(user.stats.buffs.streaks).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('adding mp', () => {
|
||||||
|
it('should add mp to user', () => {
|
||||||
|
let mpBefore = user.stats.mp;
|
||||||
|
tasksByType.dailys[0].completed = true;
|
||||||
|
user._statsComputed.maxMP = 100;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.stats.mp).to.be.greaterThan(mpBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('set user\'s mp to user._statsComputed.maxMP when user.stats.mp is greater', () => {
|
||||||
|
user.stats.mp = 120;
|
||||||
|
user._statsComputed.maxMP = 100;
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.stats.mp).to.equal(user._statsComputed.maxMP);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('quest progress', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
let daily = {
|
||||||
|
text: 'test daily',
|
||||||
|
type: 'daily',
|
||||||
|
};
|
||||||
|
|
||||||
|
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
|
||||||
|
tasksByType.dailys = [];
|
||||||
|
tasksByType.dailys.push(task);
|
||||||
|
|
||||||
|
user._statsComputed = {
|
||||||
|
con: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
daysMissed = 1;
|
||||||
|
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets user progress', () => {
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.party.quest.progress.up).to.equal(0);
|
||||||
|
expect(user.party.quest.progress.down).to.equal(0);
|
||||||
|
expect(user.party.quest.progress.collect).to.be.empty;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies the user progress', () => {
|
||||||
|
let progress = cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(progress.down).to.equal(-1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('private messages', () => {
|
||||||
|
let lastMessageId;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
let maxPMs = 200;
|
||||||
|
for (let index = 0; index < maxPMs - 1; index += 1) {
|
||||||
|
let messageId = common.uuid();
|
||||||
|
user.inbox.messages[messageId] = {
|
||||||
|
id: messageId,
|
||||||
|
text: `test ${index}`,
|
||||||
|
timestamp: Number(new Date()),
|
||||||
|
likes: {},
|
||||||
|
flags: {},
|
||||||
|
flagCount: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMessageId = common.uuid();
|
||||||
|
user.inbox.messages[lastMessageId] = {
|
||||||
|
id: lastMessageId,
|
||||||
|
text: `test ${lastMessageId}`,
|
||||||
|
timestamp: Number(new Date()),
|
||||||
|
likes: {},
|
||||||
|
flags: {},
|
||||||
|
flagCount: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
xit('does not clear pms under 200', () => {
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
expect(user.inbox.messages[lastMessageId]).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
xit('clears pms over 200', () => {
|
||||||
|
let messageId = common.uuid();
|
||||||
|
user.inbox.messages[messageId] = {
|
||||||
|
id: messageId,
|
||||||
|
text: `test ${messageId}`,
|
||||||
|
timestamp: Number(new Date()),
|
||||||
|
likes: {},
|
||||||
|
flags: {},
|
||||||
|
flagCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
cron({user, tasksByType, daysMissed, analytics});
|
||||||
|
|
||||||
|
expect(user.inbox.messages[messageId]).to.not.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
176
test/api/v3/unit/middlewares/cronMiddleware.js
Normal file
176
test/api/v3/unit/middlewares/cronMiddleware.js
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import {
|
||||||
|
generateRes,
|
||||||
|
generateReq,
|
||||||
|
generateNext,
|
||||||
|
generateTodo,
|
||||||
|
generateDaily,
|
||||||
|
} from '../../../../helpers/api-unit.helper';
|
||||||
|
import cronMiddleware from '../../../../../website/src/middlewares/api-v3/cron';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { model as User } from '../../../../../website/src/models/user';
|
||||||
|
import { model as Group } from '../../../../../website/src/models/group';
|
||||||
|
import * as Tasks from '../../../../../website/src/models/task';
|
||||||
|
import analyticsService from '../../../../../website/src/libs/api-v3/analyticsService';
|
||||||
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
|
||||||
|
describe('cron middleware', () => {
|
||||||
|
let res, req, next;
|
||||||
|
let user;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
res = generateRes();
|
||||||
|
req = generateReq();
|
||||||
|
next = generateNext();
|
||||||
|
user = new User({
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: 'username',
|
||||||
|
lowerCaseUsername: 'username',
|
||||||
|
email: 'email@email.email',
|
||||||
|
salt: 'salt',
|
||||||
|
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
user._statsComputed = {
|
||||||
|
mp: 10,
|
||||||
|
maxMP: 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.locals.user = user;
|
||||||
|
res.analytics = analyticsService;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls next when user is not attached', () => {
|
||||||
|
res.locals.user = null;
|
||||||
|
cronMiddleware(req, res, next);
|
||||||
|
expect(next).to.be.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls next when days have not been missed', () => {
|
||||||
|
cronMiddleware(req, res, next);
|
||||||
|
expect(next).to.be.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear todos older than 30 days for free users', async (done) => {
|
||||||
|
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||||
|
let task = generateTodo(user);
|
||||||
|
task.dateCompleted = moment(new Date()).subtract({days: 31});
|
||||||
|
task.completed = true;
|
||||||
|
await task.save();
|
||||||
|
|
||||||
|
cronMiddleware(req, res, () => {
|
||||||
|
Tasks.Task.findOne({_id: task}, function (err, taskFound) {
|
||||||
|
expect(err).to.not.exist;
|
||||||
|
expect(taskFound).to.not.exist;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not clear todos older than 30 days for subscribed users', (done) => {
|
||||||
|
user.purchased.plan.customerId = 'subscribedId';
|
||||||
|
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||||
|
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||||
|
let task = generateTodo(user);
|
||||||
|
task.dateCompleted = moment(new Date()).subtract({days: 31});
|
||||||
|
task.completed = true;
|
||||||
|
task.save();
|
||||||
|
|
||||||
|
cronMiddleware(req, res, () => {
|
||||||
|
Tasks.Task.findOne({_id: task}, function (err, taskFound) {
|
||||||
|
expect(err).to.not.exist;
|
||||||
|
expect(taskFound).to.exist;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear todos older than 90 days for subscribed users', (done) => {
|
||||||
|
user.purchased.plan.customerId = 'subscribedId';
|
||||||
|
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||||
|
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||||
|
|
||||||
|
let task = generateTodo(user);
|
||||||
|
task.dateCompleted = moment(new Date()).subtract({days: 91});
|
||||||
|
task.completed = true;
|
||||||
|
task.save();
|
||||||
|
|
||||||
|
cronMiddleware(req, res, () => {
|
||||||
|
Tasks.Task.findOne({_id: task}, function (err, taskFound) {
|
||||||
|
expect(err).to.not.exist;
|
||||||
|
expect(taskFound).to.not.exist;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call next is user was not modified after cron', (done) => {
|
||||||
|
let hpBefore = user.stats.hp;
|
||||||
|
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||||
|
generateDaily(user);
|
||||||
|
|
||||||
|
cronMiddleware(req, res, () => {
|
||||||
|
expect(user.stats.hp).to.be.equal(hpBefore);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does damage for missing dailies', (done) => {
|
||||||
|
let hpBefore = user.stats.hp;
|
||||||
|
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||||
|
let daily = generateDaily(user);
|
||||||
|
daily.startDate = moment(new Date()).subtract({days: 2});
|
||||||
|
daily.save();
|
||||||
|
|
||||||
|
cronMiddleware(req, res, () => {
|
||||||
|
expect(user.stats.hp).to.be.lessThan(hpBefore);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates tasks', (done) => {
|
||||||
|
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||||
|
let todo = generateTodo(user);
|
||||||
|
let todoValueBefore = todo.value;
|
||||||
|
|
||||||
|
cronMiddleware(req, res, () => {
|
||||||
|
Tasks.Task.findOne({_id: todo._id}, function (err, todoFound) {
|
||||||
|
expect(err).to.not.exist;
|
||||||
|
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies quest progress', async (done) => {
|
||||||
|
let hpBefore = user.stats.hp;
|
||||||
|
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||||
|
let daily = generateDaily(user);
|
||||||
|
daily.startDate = moment(new Date()).subtract({days: 2});
|
||||||
|
daily.save();
|
||||||
|
|
||||||
|
let questKey = 'dilatory';
|
||||||
|
user.party.quest.key = questKey;
|
||||||
|
|
||||||
|
let party = new Group({
|
||||||
|
type: 'party',
|
||||||
|
name: generateUUID(),
|
||||||
|
leader: user._id,
|
||||||
|
});
|
||||||
|
party.quest.members[user._id] = true;
|
||||||
|
party.quest.key = questKey;
|
||||||
|
await party.save();
|
||||||
|
|
||||||
|
user.party._id = party._id;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
party.startQuest(user);
|
||||||
|
|
||||||
|
cronMiddleware(req, res, () => {
|
||||||
|
expect(user.stats.hp).to.be.lessThan(hpBefore);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,6 +6,7 @@ import { model as Group } from '../../website/src/models/group';
|
|||||||
import mongo from './mongo'; // eslint-disable-line
|
import mongo from './mongo'; // eslint-disable-line
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import i18n from '../../common/script/i18n';
|
import i18n from '../../common/script/i18n';
|
||||||
|
import * as Tasks from '../../website/src/models/task';
|
||||||
|
|
||||||
afterEach((done) => {
|
afterEach((done) => {
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
@@ -70,3 +71,33 @@ export function generateHistory (days) {
|
|||||||
|
|
||||||
return history;
|
return history;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateTodo (user) {
|
||||||
|
let todo = {
|
||||||
|
text: 'test todo',
|
||||||
|
type: 'todo',
|
||||||
|
value: 0,
|
||||||
|
completed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line babel/new-cap
|
||||||
|
task.userId = user._id;
|
||||||
|
task.save();
|
||||||
|
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateDaily (user) {
|
||||||
|
let daily = {
|
||||||
|
text: 'test daily',
|
||||||
|
type: 'daily',
|
||||||
|
value: 0,
|
||||||
|
completed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
|
||||||
|
task.userId = user._id;
|
||||||
|
task.save();
|
||||||
|
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|||||||
273
website/src/libs/api-v3/cron.js
Normal file
273
website/src/libs/api-v3/cron.js
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import common from '../../../../common/';
|
||||||
|
import { preenUserHistory } from '../../libs/api-v3/preening';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
const shouldDo = common.shouldDo;
|
||||||
|
const scoreTask = common.ops.scoreTask;
|
||||||
|
// const maxPMs = 200;
|
||||||
|
|
||||||
|
let CLEAR_BUFFS = {
|
||||||
|
str: 0,
|
||||||
|
int: 0,
|
||||||
|
per: 0,
|
||||||
|
con: 0,
|
||||||
|
stealth: 0,
|
||||||
|
streaks: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function grantEndOfTheMonthPerks (user, now) {
|
||||||
|
let plan = user.purchased.plan;
|
||||||
|
|
||||||
|
if (moment(plan.dateUpdated).format('MMYYYY') !== moment().format('MMYYYY')) {
|
||||||
|
plan.gemsBought = 0; // reset gem-cap
|
||||||
|
plan.dateUpdated = now;
|
||||||
|
// For every month, inc their "consecutive months" counter. Give perks based on consecutive blocks
|
||||||
|
// If they already got perks for those blocks (eg, 6mo subscription, subscription gifts, etc) - then dec the offset until it hits 0
|
||||||
|
// TODO use month diff instead of ++ / --?
|
||||||
|
_.defaults(plan.consecutive, {count: 0, offset: 0, trinkets: 0, gemCapExtra: 0}); // FIXME see https://github.com/HabitRPG/habitrpg/issues/4317
|
||||||
|
|
||||||
|
plan.consecutive.count++;
|
||||||
|
|
||||||
|
if (plan.consecutive.offset > 0) {
|
||||||
|
plan.consecutive.offset--;
|
||||||
|
} else if (plan.consecutive.count % 3 === 0) { // every 3 months
|
||||||
|
plan.consecutive.trinkets++;
|
||||||
|
plan.consecutive.gemCapExtra += 5;
|
||||||
|
if (plan.consecutive.gemCapExtra > 25) plan.consecutive.gemCapExtra = 25; // cap it at 50 (hard 25 limit + extra 25)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user cancelled subscription, we give them until 30day's end until it terminates
|
||||||
|
if (plan.dateTerminated && moment(plan.dateTerminated).isBefore(new Date())) {
|
||||||
|
_.merge(plan, {
|
||||||
|
planId: null,
|
||||||
|
customerId: null,
|
||||||
|
paymentMethod: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
_.merge(plan.consecutive, {
|
||||||
|
count: 0,
|
||||||
|
offset: 0,
|
||||||
|
gemCapExtra: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
user.markModified('purchased.plan');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function performSleepTasks (user, tasksByType, now) {
|
||||||
|
user.stats.buffs = _.cloneDeep(CLEAR_BUFFS);
|
||||||
|
|
||||||
|
tasksByType.dailys.forEach((daily) => {
|
||||||
|
let completed = daily.completed;
|
||||||
|
let thatDay = moment(now).subtract({days: 1});
|
||||||
|
|
||||||
|
if (shouldDo(thatDay.toDate(), daily, user.preferences) || completed) {
|
||||||
|
daily.checklist.forEach(box => box.completed = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
daily.completed = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
|
||||||
|
// For incomplete Dailys, deduct experience
|
||||||
|
// Make sure to run this function once in a while as server will not take care of overnight calculations.
|
||||||
|
// And you have to run it every time client connects.
|
||||||
|
export function cron (options = {}) {
|
||||||
|
let {user, tasksByType, analytics, now = new Date(), daysMissed, timezoneOffsetFromUserPrefs} = options;
|
||||||
|
|
||||||
|
user.auth.timestamps.loggedin = now;
|
||||||
|
user.lastCron = now;
|
||||||
|
user.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs;
|
||||||
|
// Reset the lastDrop count to zero
|
||||||
|
if (user.items.lastDrop.count > 0) user.items.lastDrop.count = 0;
|
||||||
|
|
||||||
|
// "Perfect Day" achievement for perfect-days
|
||||||
|
let perfect = true;
|
||||||
|
|
||||||
|
if (user.isSubscribed()) {
|
||||||
|
grantEndOfTheMonthPerks(user, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
// User is resting at the inn.
|
||||||
|
// On cron, buffs are cleared and all dailies are reset without performing damage
|
||||||
|
if (user.preferences.sleep === true) {
|
||||||
|
performSleepTasks(user, tasksByType, now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let multiDaysCountAsOneDay = true;
|
||||||
|
// If the user does not log in for two or more days, cron (mostly) acts as if it were only one day.
|
||||||
|
// When site-wide difficulty settings are introduced, this can be a user preference option.
|
||||||
|
|
||||||
|
// Tally each task
|
||||||
|
let todoTally = 0;
|
||||||
|
|
||||||
|
tasksByType.todos.forEach(task => { // make uncompleted todos redder
|
||||||
|
scoreTask({
|
||||||
|
task,
|
||||||
|
user,
|
||||||
|
direction: 'down',
|
||||||
|
cron: true,
|
||||||
|
times: multiDaysCountAsOneDay ? 1 : daysMissed,
|
||||||
|
});
|
||||||
|
|
||||||
|
todoTally += task.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
let dailyChecked = 0; // how many dailies were checked?
|
||||||
|
let dailyDueUnchecked = 0; // how many dailies were cun-hecked?
|
||||||
|
if (!user.party.quest.progress.down) user.party.quest.progress.down = 0;
|
||||||
|
|
||||||
|
tasksByType.dailys.forEach((task) => {
|
||||||
|
let completed = task.completed;
|
||||||
|
// Deduct points for missed Daily tasks
|
||||||
|
let EvadeTask = 0;
|
||||||
|
let scheduleMisses = daysMissed;
|
||||||
|
|
||||||
|
if (completed) {
|
||||||
|
dailyChecked += 1;
|
||||||
|
} else {
|
||||||
|
// dailys repeat, so need to calculate how many they've missed according to their own schedule
|
||||||
|
scheduleMisses = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < daysMissed; i++) {
|
||||||
|
let thatDay = moment(now).subtract({days: i + 1});
|
||||||
|
|
||||||
|
if (shouldDo(thatDay.toDate(), task, user.preferences)) {
|
||||||
|
scheduleMisses++;
|
||||||
|
if (user.stats.buffs.stealth) {
|
||||||
|
user.stats.buffs.stealth--;
|
||||||
|
EvadeTask++;
|
||||||
|
}
|
||||||
|
if (multiDaysCountAsOneDay) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduleMisses > EvadeTask) {
|
||||||
|
perfect = false;
|
||||||
|
|
||||||
|
if (task.checklist && task.checklist.length > 0) { // Partially completed checklists dock fewer mana points
|
||||||
|
let fractionChecked = _.reduce(task.checklist, (m, i) => m + (i.completed ? 1 : 0), 0) / task.checklist.length;
|
||||||
|
dailyDueUnchecked += 1 - fractionChecked;
|
||||||
|
dailyChecked += fractionChecked;
|
||||||
|
} else {
|
||||||
|
dailyDueUnchecked += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let delta = scoreTask({
|
||||||
|
user,
|
||||||
|
task,
|
||||||
|
direction: 'down',
|
||||||
|
times: multiDaysCountAsOneDay ? 1 : scheduleMisses - EvadeTask,
|
||||||
|
cron: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply damage from a boss, less damage for Trivial priority (difficulty)
|
||||||
|
user.party.quest.progress.down += delta * (task.priority < 1 ? task.priority : 1);
|
||||||
|
// NB: Medium and Hard priorities do not increase damage from boss. This was by accident
|
||||||
|
// initially, and when we realised, we could not fix it because users are used to
|
||||||
|
// their Medium and Hard Dailies doing an Easy amount of damage from boss.
|
||||||
|
// Easy is task.priority = 1. Anything < 1 will be Trivial (0.1) or any future
|
||||||
|
// setting between Trivial and Easy.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task.history.push({
|
||||||
|
date: Number(new Date()),
|
||||||
|
value: task.value,
|
||||||
|
});
|
||||||
|
task.completed = false;
|
||||||
|
|
||||||
|
if (completed || scheduleMisses > 0) {
|
||||||
|
task.checklist.forEach(i => i.completed = true); // FIXME this should not happen for grey tasks unless they are completed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tasksByType.habits.forEach((task) => { // slowly reset 'onlies' value to 0
|
||||||
|
if (task.up === false || task.down === false) {
|
||||||
|
task.value = Math.abs(task.value) < 0.1 ? 0 : task.value = task.value / 2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Finished tallying
|
||||||
|
user.history.todos.push({date: now, value: todoTally});
|
||||||
|
|
||||||
|
// tally experience
|
||||||
|
let expTally = user.stats.exp;
|
||||||
|
let lvl = 0; // iterator
|
||||||
|
while (lvl < user.stats.lvl - 1) {
|
||||||
|
lvl++;
|
||||||
|
expTally += common.tnl(lvl);
|
||||||
|
}
|
||||||
|
|
||||||
|
user.history.exp.push({date: now, value: expTally});
|
||||||
|
|
||||||
|
// preen user history so that it doesn't become a performance problem
|
||||||
|
// also for subscribed users but differentyly
|
||||||
|
// premium subscribers can keep their full history.
|
||||||
|
preenUserHistory(user, tasksByType, user.preferences.timezoneOffset);
|
||||||
|
|
||||||
|
if (perfect) {
|
||||||
|
user.achievements.perfect++;
|
||||||
|
let lvlDiv2 = Math.ceil(common.capByLevel(user.stats.lvl) / 2);
|
||||||
|
user.stats.buffs = {
|
||||||
|
str: lvlDiv2,
|
||||||
|
int: lvlDiv2,
|
||||||
|
per: lvlDiv2,
|
||||||
|
con: lvlDiv2,
|
||||||
|
stealth: 0,
|
||||||
|
streaks: false,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
user.stats.buffs = _.cloneDeep(CLEAR_BUFFS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add 10 MP, or 10% of max MP if that'd be more. Perform this after Perfect Day for maximum benefit
|
||||||
|
// Adjust for fraction of dailies completed
|
||||||
|
user.stats.mp += _.max([10, 0.1 * user._statsComputed.maxMP]) * dailyChecked / (dailyDueUnchecked + dailyChecked);
|
||||||
|
if (user.stats.mp > user._statsComputed.maxMP) user.stats.mp = user._statsComputed.maxMP;
|
||||||
|
|
||||||
|
if (dailyDueUnchecked === 0 && dailyChecked === 0) dailyChecked = 1;
|
||||||
|
user.stats.mp += _.max([10, 0.1 * user._statsComputed.maxMP]) * dailyChecked / (dailyDueUnchecked + dailyChecked);
|
||||||
|
if (user.stats.mp > user._statsComputed.maxMP) {
|
||||||
|
user.stats.mp = user._statsComputed.maxMP;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After all is said and done, progress up user's effect on quest, return those values & reset the user's
|
||||||
|
let progress = user.party.quest.progress;
|
||||||
|
let _progress = _.cloneDeep(progress);
|
||||||
|
_.merge(progress, {down: 0, up: 0});
|
||||||
|
progress.collect = _.transform(progress.collect, (m, v, k) => m[k] = 0);
|
||||||
|
|
||||||
|
// @TODO: Clean PMs - keep 200 for subscribers and 50 for free users
|
||||||
|
// let numberOfPMs = Object.keys(user.inbox.messages).length;
|
||||||
|
// if (numberOfPMs > maxPMs) {
|
||||||
|
// _(user.inbox.messages)
|
||||||
|
// .sortBy('timestamp')
|
||||||
|
// .takeRight(numberOfPMs - maxPMs)
|
||||||
|
// .each(pm => {
|
||||||
|
// delete user.inbox.messages[pm.id];
|
||||||
|
// }).value();
|
||||||
|
//
|
||||||
|
// user.markModified('inbox.messages');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Analytics
|
||||||
|
user.flags.cronCount++;
|
||||||
|
analytics.track('Cron', {
|
||||||
|
category: 'behavior',
|
||||||
|
gaLabel: 'Cron Count',
|
||||||
|
gaValue: user.flags.cronCount,
|
||||||
|
uuid: user._id,
|
||||||
|
user, // TODO is it really necessary passing the whole user object?
|
||||||
|
resting: user.preferences.sleep,
|
||||||
|
cronCount: user.flags.cronCount,
|
||||||
|
progressUp: _.min([_progress.up, 900]),
|
||||||
|
progressDown: _progress.down,
|
||||||
|
});
|
||||||
|
|
||||||
|
return _progress;
|
||||||
|
}
|
||||||
@@ -5,274 +5,13 @@ import * as Tasks from '../../models/task';
|
|||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
import { model as Group } from '../../models/group';
|
import { model as Group } from '../../models/group';
|
||||||
import { model as User } from '../../models/user';
|
import { model as User } from '../../models/user';
|
||||||
import { preenUserHistory } from '../../libs/api-v3/preening';
|
import { cron } from '../../libs/api-v3/cron';
|
||||||
|
|
||||||
const daysSince = common.daysSince;
|
const daysSince = common.daysSince;
|
||||||
const shouldDo = common.shouldDo;
|
|
||||||
|
|
||||||
const scoreTask = common.ops.scoreTask;
|
|
||||||
|
|
||||||
let clearBuffs = {
|
|
||||||
str: 0,
|
|
||||||
int: 0,
|
|
||||||
per: 0,
|
|
||||||
con: 0,
|
|
||||||
stealth: 0,
|
|
||||||
streaks: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
// At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
|
|
||||||
// For incomplete Dailys, deduct experience
|
|
||||||
// Make sure to run this function once in a while as server will not take care of overnight calculations.
|
|
||||||
// And you have to run it every time client connects.
|
|
||||||
function cron (options = {}) {
|
|
||||||
let {user, tasksByType, analytics, now = new Date(), daysMissed, timezoneOffsetFromUserPrefs} = options;
|
|
||||||
|
|
||||||
user.auth.timestamps.loggedin = now;
|
|
||||||
user.lastCron = now;
|
|
||||||
user.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs;
|
|
||||||
// Reset the lastDrop count to zero
|
|
||||||
if (user.items.lastDrop.count > 0) user.items.lastDrop.count = 0;
|
|
||||||
|
|
||||||
// "Perfect Day" achievement for perfect-days
|
|
||||||
let perfect = true;
|
|
||||||
|
|
||||||
// end-of-month perks for subscribers
|
|
||||||
let plan = user.purchased.plan;
|
|
||||||
if (user.isSubscribed()) {
|
|
||||||
if (moment(plan.dateUpdated).format('MMYYYY') !== moment().format('MMYYYY')) {
|
|
||||||
plan.gemsBought = 0; // reset gem-cap
|
|
||||||
plan.dateUpdated = now;
|
|
||||||
// For every month, inc their "consecutive months" counter. Give perks based on consecutive blocks
|
|
||||||
// If they already got perks for those blocks (eg, 6mo subscription, subscription gifts, etc) - then dec the offset until it hits 0
|
|
||||||
// TODO use month diff instead of ++ / --?
|
|
||||||
_.defaults(plan.consecutive, {count: 0, offset: 0, trinkets: 0, gemCapExtra: 0}); // TODO see https://github.com/HabitRPG/habitrpg/issues/4317
|
|
||||||
plan.consecutive.count++;
|
|
||||||
if (plan.consecutive.offset > 0) {
|
|
||||||
plan.consecutive.offset--;
|
|
||||||
} else if (plan.consecutive.count % 3 === 0) { // every 3 months
|
|
||||||
plan.consecutive.trinkets++;
|
|
||||||
plan.consecutive.gemCapExtra += 5;
|
|
||||||
if (plan.consecutive.gemCapExtra > 25) plan.consecutive.gemCapExtra = 25; // cap it at 50 (hard 25 limit + extra 25)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If user cancelled subscription, we give them until 30day's end until it terminates
|
|
||||||
if (plan.dateTerminated && moment(plan.dateTerminated).isBefore(new Date())) {
|
|
||||||
_.merge(plan, {
|
|
||||||
planId: null,
|
|
||||||
customerId: null,
|
|
||||||
paymentMethod: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
_.merge(plan.consecutive, {
|
|
||||||
count: 0,
|
|
||||||
offset: 0,
|
|
||||||
gemCapExtra: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
user.markModified('purchased.plan');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// User is resting at the inn.
|
|
||||||
// On cron, buffs are cleared and all dailies are reset without performing damage
|
|
||||||
if (user.preferences.sleep === true) {
|
|
||||||
user.stats.buffs = _.cloneDeep(clearBuffs);
|
|
||||||
|
|
||||||
tasksByType.dailys.forEach((daily) => {
|
|
||||||
let completed = daily.completed;
|
|
||||||
let thatDay = moment(now).subtract({days: 1});
|
|
||||||
|
|
||||||
if (shouldDo(thatDay.toDate(), daily, user.preferences) || completed) {
|
|
||||||
daily.checklist.forEach(box => box.completed = false);
|
|
||||||
}
|
|
||||||
daily.completed = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let multiDaysCountAsOneDay = true;
|
|
||||||
// If the user does not log in for two or more days, cron (mostly) acts as if it were only one day.
|
|
||||||
// When site-wide difficulty settings are introduced, this can be a user preference option.
|
|
||||||
|
|
||||||
// Tally each task
|
|
||||||
let todoTally = 0;
|
|
||||||
|
|
||||||
tasksByType.todos.forEach(task => { // make uncompleted todos redder
|
|
||||||
scoreTask({
|
|
||||||
task,
|
|
||||||
user,
|
|
||||||
direction: 'down',
|
|
||||||
cron: true,
|
|
||||||
times: multiDaysCountAsOneDay ? 1 : daysMissed,
|
|
||||||
});
|
|
||||||
|
|
||||||
todoTally += task.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
let dailyChecked = 0; // how many dailies were checked?
|
|
||||||
let dailyDueUnchecked = 0; // how many dailies were cun-hecked?
|
|
||||||
if (!user.party.quest.progress.down) user.party.quest.progress.down = 0;
|
|
||||||
|
|
||||||
tasksByType.dailys.forEach((task) => {
|
|
||||||
let completed = task.completed;
|
|
||||||
// Deduct points for missed Daily tasks
|
|
||||||
let EvadeTask = 0;
|
|
||||||
let scheduleMisses = daysMissed;
|
|
||||||
|
|
||||||
if (completed) {
|
|
||||||
dailyChecked += 1;
|
|
||||||
} else {
|
|
||||||
// dailys repeat, so need to calculate how many they've missed according to their own schedule
|
|
||||||
scheduleMisses = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < daysMissed; i++) {
|
|
||||||
let thatDay = moment(now).subtract({days: i + 1});
|
|
||||||
|
|
||||||
if (shouldDo(thatDay.toDate(), task, user.preferences)) {
|
|
||||||
scheduleMisses++;
|
|
||||||
if (user.stats.buffs.stealth) {
|
|
||||||
user.stats.buffs.stealth--;
|
|
||||||
EvadeTask++;
|
|
||||||
}
|
|
||||||
if (multiDaysCountAsOneDay) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scheduleMisses > EvadeTask) {
|
|
||||||
perfect = false;
|
|
||||||
|
|
||||||
if (task.checklist && task.checklist.length > 0) { // Partially completed checklists dock fewer mana points
|
|
||||||
let fractionChecked = _.reduce(task.checklist, (m, i) => m + (i.completed ? 1 : 0), 0) / task.checklist.length;
|
|
||||||
dailyDueUnchecked += 1 - fractionChecked;
|
|
||||||
dailyChecked += fractionChecked;
|
|
||||||
} else {
|
|
||||||
dailyDueUnchecked += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let delta = scoreTask({
|
|
||||||
user,
|
|
||||||
task,
|
|
||||||
direction: 'down',
|
|
||||||
times: multiDaysCountAsOneDay ? 1 : scheduleMisses - EvadeTask,
|
|
||||||
cron: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply damage from a boss, less damage for Trivial priority (difficulty)
|
|
||||||
user.party.quest.progress.down += delta * (task.priority < 1 ? task.priority : 1);
|
|
||||||
// NB: Medium and Hard priorities do not increase damage from boss. This was by accident
|
|
||||||
// initially, and when we realised, we could not fix it because users are used to
|
|
||||||
// their Medium and Hard Dailies doing an Easy amount of damage from boss.
|
|
||||||
// Easy is task.priority = 1. Anything < 1 will be Trivial (0.1) or any future
|
|
||||||
// setting between Trivial and Easy.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
task.history.push({
|
|
||||||
date: Number(new Date()),
|
|
||||||
value: task.value,
|
|
||||||
});
|
|
||||||
task.completed = false;
|
|
||||||
|
|
||||||
if (completed || scheduleMisses > 0) {
|
|
||||||
task.checklist.forEach(i => i.completed = true); // TODO this should not happen for grey tasks unless they are completed
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
tasksByType.habits.forEach((task) => { // slowly reset 'onlies' value to 0
|
|
||||||
if (task.up === false || task.down === false) {
|
|
||||||
task.value = Math.abs(task.value) < 0.1 ? 0 : task.value = task.value / 2;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Finished tallying
|
|
||||||
user.history.todos.push({date: now, value: todoTally});
|
|
||||||
|
|
||||||
// tally experience
|
|
||||||
let expTally = user.stats.exp;
|
|
||||||
let lvl = 0; // iterator
|
|
||||||
while (lvl < user.stats.lvl - 1) {
|
|
||||||
lvl++;
|
|
||||||
expTally += common.tnl(lvl);
|
|
||||||
}
|
|
||||||
user.history.exp.push({date: now, value: expTally});
|
|
||||||
|
|
||||||
// preen user history so that it doesn't become a performance problem
|
|
||||||
// also for subscribed users but differentyly
|
|
||||||
// premium subscribers can keep their full history.
|
|
||||||
preenUserHistory(user, tasksByType, user.preferences.timezoneOffset);
|
|
||||||
|
|
||||||
if (perfect) {
|
|
||||||
user.achievements.perfect++;
|
|
||||||
let lvlDiv2 = Math.ceil(common.capByLevel(user.stats.lvl) / 2);
|
|
||||||
user.stats.buffs = {
|
|
||||||
str: lvlDiv2,
|
|
||||||
int: lvlDiv2,
|
|
||||||
per: lvlDiv2,
|
|
||||||
con: lvlDiv2,
|
|
||||||
stealth: 0,
|
|
||||||
streaks: false,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
user.stats.buffs = _.cloneDeep(clearBuffs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add 10 MP, or 10% of max MP if that'd be more. Perform this after Perfect Day for maximum benefit
|
|
||||||
// Adjust for fraction of dailies completed
|
|
||||||
user.stats.mp += _.max([10, 0.1 * user._statsComputed.maxMP]) * dailyChecked / (dailyDueUnchecked + dailyChecked);
|
|
||||||
if (user.stats.mp > user._statsComputed.maxMP) user.stats.mp = user._statsComputed.maxMP;
|
|
||||||
|
|
||||||
if (dailyDueUnchecked === 0 && dailyChecked === 0) dailyChecked = 1;
|
|
||||||
user.stats.mp += _.max([10, 0.1 * user._statsComputed.maxMP]) * dailyChecked / (dailyDueUnchecked + dailyChecked);
|
|
||||||
if (user.stats.mp > user._statsComputed.maxMP) {
|
|
||||||
user.stats.mp = user._statsComputed.maxMP;
|
|
||||||
}
|
|
||||||
|
|
||||||
// After all is said and done, progress up user's effect on quest, return those values & reset the user's
|
|
||||||
let progress = user.party.quest.progress;
|
|
||||||
let _progress = _.cloneDeep(progress);
|
|
||||||
|
|
||||||
progress.down = 0;
|
|
||||||
progress.up = 0;
|
|
||||||
|
|
||||||
progress.collect = _.transform(progress.collect, (m, v, k) => m[k] = 0);
|
|
||||||
|
|
||||||
// Clean PMs - keep 200 for subscribers and 50 for free users
|
|
||||||
// TODO tests
|
|
||||||
let maxPMs = user.isSubscribed() ? 200 : 50; // TODO 200 limit for contributors too
|
|
||||||
let numberOfPMs = Object.keys(user.inbox.messages).length;
|
|
||||||
if (Object.keys(user.inbox.messages).length > maxPMs) {
|
|
||||||
_(user.inbox.messages)
|
|
||||||
.sortBy('timestamp')
|
|
||||||
.takeRight(numberOfPMs - maxPMs)
|
|
||||||
.each(pm => {
|
|
||||||
user.inbox.messages[pm.id] = undefined;
|
|
||||||
}).value();
|
|
||||||
|
|
||||||
user.markModified('inbox.messages');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Analytics
|
|
||||||
user.flags.cronCount++;
|
|
||||||
analytics.track('Cron', {
|
|
||||||
category: 'behavior',
|
|
||||||
gaLabel: 'Cron Count',
|
|
||||||
gaValue: user.flags.cronCount,
|
|
||||||
uuid: user._id,
|
|
||||||
user,
|
|
||||||
resting: user.preferences.sleep,
|
|
||||||
cronCount: user.flags.cronCount,
|
|
||||||
progressUp: _.min([_progress.up, 900]),
|
|
||||||
progressDown: _progress.down,
|
|
||||||
});
|
|
||||||
|
|
||||||
return _progress;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = function cronMiddleware (req, res, next) {
|
module.exports = function cronMiddleware (req, res, next) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|
||||||
if (!user) return next(); // User might not be available when authentication is not mandatory
|
if (!user) return next(); // User might not be available when authentication is not mandatory
|
||||||
|
|
||||||
let analytics = res.analytics;
|
let analytics = res.analytics;
|
||||||
@@ -381,7 +120,7 @@ module.exports = function cronMiddleware (req, res, next) {
|
|||||||
type: 'todo',
|
type: 'todo',
|
||||||
completed: true,
|
completed: true,
|
||||||
dateCompleted: {
|
dateCompleted: {
|
||||||
$lt: moment(now).subtract(user.isSubscribed() ? 90 : 30, 'days'),
|
$lt: moment(now).subtract(user.isSubscribed() ? 90 : 30, 'days').toDate(),
|
||||||
},
|
},
|
||||||
'challenge.id': {$exists: false},
|
'challenge.id': {$exists: false},
|
||||||
}).exec(); // TODO wait before returning?
|
}).exec(); // TODO wait before returning?
|
||||||
@@ -397,13 +136,13 @@ module.exports = function cronMiddleware (req, res, next) {
|
|||||||
// Save user and tasks
|
// Save user and tasks
|
||||||
let toSave = [user.save()];
|
let toSave = [user.save()];
|
||||||
tasks.forEach(task => {
|
tasks.forEach(task => {
|
||||||
if (task.isModified) toSave.push(task.save());
|
toSave.push(task.save());
|
||||||
});
|
});
|
||||||
|
|
||||||
Q.all(toSave)
|
Q.all(toSave)
|
||||||
.then(saved => {
|
.then(saved => {
|
||||||
user = res.locals.user = saved[0];
|
user = res.locals.user = saved[0];
|
||||||
if (!quest) return;
|
if (!quest) return;
|
||||||
|
|
||||||
// If user is on a quest, roll for boss & player, or handle collections
|
// If user is on a quest, roll for boss & player, or handle collections
|
||||||
let questType = quest.boss ? 'boss' : 'collect';
|
let questType = quest.boss ? 'boss' : 'collect';
|
||||||
// TODO this saves user, runs db updates, loads user. Is there a better way to handle this?
|
// TODO this saves user, runs db updates, loads user. Is there a better way to handle this?
|
||||||
|
|||||||
@@ -475,7 +475,6 @@ function _isOnQuest (user, progress, group) {
|
|||||||
// Returns a promise
|
// Returns a promise
|
||||||
schema.statics.collectQuest = async function collectQuest (user, progress) {
|
schema.statics.collectQuest = async function collectQuest (user, progress) {
|
||||||
let group = await this.getGroup({user, groupId: 'party'});
|
let group = await this.getGroup({user, groupId: 'party'});
|
||||||
|
|
||||||
if (!_isOnQuest(user, progress, group)) return;
|
if (!_isOnQuest(user, progress, group)) return;
|
||||||
let quest = shared.content.quests[group.quest.key];
|
let quest = shared.content.quests[group.quest.key];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user