Habits v2: adding counter to habits (cleaned up branch) - fixes #8113 (#8198)

* Clean version of PR 8175

The original PR for this was here:
https://github.com/HabitRPG/habitica/pull/8175

Unfortunately while fixing a conflict in tasks.json, I messed up the rebase and wound up pulling in too many commits and making a giant mess. Sorry. :P

* Fixing test failure

This test seems to occasionally start failing (another coder reported the same thing happening to them in the blacksmiths’ guild) because the order in which the tasks are created can sometimes not match the order in the array. So I have sorted the tasks array after creation by the task name to ensure a consistent ordering, and slightly reordered the expect statements to match.
This commit is contained in:
astolat
2017-02-27 13:15:45 -05:00
committed by Keith Holliday
parent 6f0d0b1fb3
commit 6d0df78441
13 changed files with 150 additions and 2 deletions

View File

@@ -481,6 +481,67 @@ describe('cron', () => {
expect(tasksByType.habits[0].value).to.equal(1); expect(tasksByType.habits[0].value).to.equal(1);
}); });
describe('counters', () => {
let notStartOfWeekOrMonth = new Date(2016, 9, 28).getTime(); // a Friday
let clock;
beforeEach(() => {
// Replace system clocks so we can get predictable results
clock = sinon.useFakeTimers(notStartOfWeekOrMonth);
});
afterEach(() => {
return clock.restore();
});
it('should reset a daily habit counter each day', () => {
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(0);
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should reset a weekly habit counter each Monday', () => {
tasksByType.habits[0].frequency = 'weekly';
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
// should not reset
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(1);
expect(tasksByType.habits[0].counterDown).to.equal(1);
// should reset
daysMissed = 8;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(0);
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should reset a monthly habit counter the first day of each month', () => {
tasksByType.habits[0].frequency = 'monthly';
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
// should not reset
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(1);
expect(tasksByType.habits[0].counterDown).to.equal(1);
// should reset
daysMissed = 32;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].counterUp).to.equal(0);
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
});
}); });
describe('perfect day', () => { describe('perfect day', () => {

View File

@@ -12,6 +12,9 @@ describe('taskDefaults', () => {
expect(task.up).to.eql(true); expect(task.up).to.eql(true);
expect(task.down).to.eql(true); expect(task.down).to.eql(true);
expect(task.history).to.eql([]); expect(task.history).to.eql([]);
expect(task.frequency).to.equal('daily');
expect(task.counterUp).to.equal(0);
expect(task.counterDown).to.equal(0);
}); });
it('applies defaults to a daily', () => { it('applies defaults to a daily', () => {

View File

@@ -33,6 +33,9 @@ describe('shared.ops.addTask', () => {
expect(habit.down).to.equal(false); expect(habit.down).to.equal(false);
expect(habit.history).to.eql([]); expect(habit.history).to.eql([]);
expect(habit.checklist).to.not.exist; expect(habit.checklist).to.not.exist;
expect(habit.frequency).to.equal('daily');
expect(habit.counterUp).to.equal(0);
expect(habit.counterDown).to.equal(0);
}); });
it('adds an habtit when type is invalid', () => { it('adds an habtit when type is invalid', () => {

View File

@@ -138,6 +138,9 @@ describe('shared.ops.scoreTask', () => {
todo = generateTodo({ userId: ref.afterUser._id, text: 'some todo' }); todo = generateTodo({ userId: ref.afterUser._id, text: 'some todo' });
expect(habit.history.length).to.eql(0); expect(habit.history.length).to.eql(0);
expect(habit.frequency).to.equal('daily');
expect(habit.counterUp).to.equal(0);
expect(habit.counterDown).to.equal(0);
// before and after are the same user // before and after are the same user
expect(ref.beforeUser._id).to.exist; expect(ref.beforeUser._id).to.exist;
@@ -202,6 +205,7 @@ describe('shared.ops.scoreTask', () => {
expect(habit.history.length).to.eql(1); expect(habit.history.length).to.eql(1);
expect(habit.value).to.be.greaterThan(0); expect(habit.value).to.be.greaterThan(0);
expect(habit.counterUp).to.equal(5);
expect(ref.afterUser.stats.hp).to.eql(50); expect(ref.afterUser.stats.hp).to.eql(50);
expect(ref.afterUser.stats.exp).to.be.greaterThan(ref.beforeUser.stats.exp); expect(ref.afterUser.stats.exp).to.be.greaterThan(ref.beforeUser.stats.exp);
@@ -213,6 +217,7 @@ describe('shared.ops.scoreTask', () => {
expect(habit.history.length).to.eql(1); expect(habit.history.length).to.eql(1);
expect(habit.value).to.be.lessThan(0); expect(habit.value).to.be.lessThan(0);
expect(habit.counterDown).to.equal(5);
expect(ref.afterUser.stats.hp).to.be.lessThan(ref.beforeUser.stats.hp); expect(ref.afterUser.stats.hp).to.be.lessThan(ref.beforeUser.stats.hp);
expect(ref.afterUser.stats.exp).to.eql(0); expect(ref.afterUser.stats.exp).to.eql(0);

View File

@@ -136,6 +136,13 @@
"intelligenceExample": "Relating to academic or mentally challenging pursuits", "intelligenceExample": "Relating to academic or mentally challenging pursuits",
"perceptionExample": "Relating to work or financial tasks", "perceptionExample": "Relating to work or financial tasks",
"constitutionExample": "Relating to health, wellness, and social interaction", "constitutionExample": "Relating to health, wellness, and social interaction",
"counterPeriod": "Counter Resets Every",
"counterPeriodDay": "Day",
"counterPeriodWeek": "Week",
"counterPeriodMonth": "Month",
"habitCounter": "Counter",
"habitCounterUp": "Positive Counter",
"habitCounterDown": "Negative Counter",
"taskRequiresApproval": "This task must be approved before you can complete it. Approval has already been requested", "taskRequiresApproval": "This task must be approved before you can complete it. Approval has already been requested",
"taskApprovalHasBeenRequested": "Approval has been requested", "taskApprovalHasBeenRequested": "Approval has been requested",
"approvals": "Approvals", "approvals": "Approvals",

View File

@@ -49,6 +49,9 @@ module.exports = function taskDefaults (task = {}) {
_.defaults(task, { _.defaults(task, {
up: true, up: true,
down: true, down: true,
frequency: 'daily',
counterUp: 0,
counterDown: 0,
}); });
} }

View File

@@ -172,6 +172,14 @@ function _changeTaskValue (user, task, direction, times, cron) {
return addToDelta; return addToDelta;
} }
function _updateCounter (task, direction, times) {
if (direction === 'up') {
task.counterUp += times;
} else {
task.counterDown += times;
}
}
module.exports = function scoreTask (options = {}, req = {}) { module.exports = function scoreTask (options = {}, req = {}) {
let {user, task, direction, times = 1, cron = false} = options; let {user, task, direction, times = 1, cron = false} = options;
let delta = 0; let delta = 0;
@@ -207,6 +215,8 @@ module.exports = function scoreTask (options = {}, req = {}) {
date: Number(new Date()), date: Number(new Date()),
value: task.value, value: task.value,
}); });
_updateCounter(task, direction, times);
} else if (task.type === 'daily') { } else if (task.type === 'daily') {
if (cron) { if (cron) {
delta += _changeTaskValue(user, task, direction, times, cron); delta += _changeTaskValue(user, task, direction, times, cron);

View File

@@ -316,8 +316,41 @@ export function cron (options = {}) {
} }
}); });
// move singleton Habits towards yellow. // check if we've passed a day on which we should reset the habit counters, including today
tasksByType.habits.forEach((task) => { // slowly reset 'onlies' value to 0 let resetWeekly = false;
let resetMonthly = false;
for (let i = 0; i <= daysMissed; i++) {
if (resetWeekly === true && resetMonthly === true) {
break;
}
let thatDay = moment(now).subtract({days: i}).toDate();
if (thatDay.getDay() === 1) {
resetWeekly = true;
}
if (thatDay.getDate() === 1) {
resetMonthly = true;
}
}
tasksByType.habits.forEach((task) => {
// reset counters if appropriate
// this enormously clunky thing brought to you by lint
let reset = false;
if (task.frequency === 'daily') {
reset = true;
} else if (task.frequency === 'weekly' && resetWeekly === true) {
reset = true;
} else if (task.frequency === 'monthly' && resetMonthly === true) {
reset = true;
}
if (reset === true) {
task.counterUp = 0;
task.counterDown = 0;
}
// slowly reset value to 0 for "onlies" (Habits with + or - but not both)
// move singleton Habits towards yellow.
if (task.up === false || task.down === false) { if (task.up === false || task.down === false) {
task.value = Math.abs(task.value) < 0.1 ? 0 : task.value = task.value / 2; task.value = Math.abs(task.value) < 0.1 ? 0 : task.value = task.value / 2;
} }

View File

@@ -214,6 +214,9 @@ let dailyTodoSchema = () => {
export let HabitSchema = new Schema(_.defaults({ export let HabitSchema = new Schema(_.defaults({
up: {type: Boolean, default: true}, up: {type: Boolean, default: true},
down: {type: Boolean, default: true}, down: {type: Boolean, default: true},
counterUp: {type: Number, default: 0},
counterDown: {type: Number, default: 0},
frequency: {type: String, default: 'daily', enum: ['daily', 'weekly', 'monthly']},
}, habitDailySchema()), subDiscriminatorOptions); }, habitDailySchema()), subDiscriminatorOptions);
export let habit = Task.discriminator('habit', HabitSchema); export let habit = Task.discriminator('habit', HabitSchema);

View File

@@ -93,6 +93,8 @@ footer.footer(ng-controller='FooterCtrl')
a.btn.btn-default(ng-click='setHealthLow()') Health = 1 a.btn.btn-default(ng-click='setHealthLow()') Health = 1
a.btn.btn-default(ng-click='addMissedDay(1)') +1 Missed Day a.btn.btn-default(ng-click='addMissedDay(1)') +1 Missed Day
a.btn.btn-default(ng-click='addMissedDay(2)') +2 Missed Days a.btn.btn-default(ng-click='addMissedDay(2)') +2 Missed Days
a.btn.btn-default(ng-click='addMissedDay(8)') +8 Missed Days
a.btn.btn-default(ng-click='addMissedDay(32)') +32 Missed Days
a.btn.btn-default(ng-click='addTenGems()') +10 Gems a.btn.btn-default(ng-click='addTenGems()') +10 Gems
a.btn.btn-default(ng-click='addHourglass()') +1 Mystic Hourglass a.btn.btn-default(ng-click='addHourglass()') +1 Mystic Hourglass
a.btn.btn-default(ng-click='addGold()') +500GP a.btn.btn-default(ng-click='addGold()') +500GP

View File

@@ -9,6 +9,8 @@ div(ng-if='(task.type !== "reward") || (!obj.auth && obj.purchased && obj.purcha
a.hint(href='http://habitica.wikia.com/wiki/Task_Alias', target='_blank', popover-trigger='mouseenter', popover="{{::env.t('taskAliasPopover')}} {{::task._edit.alias ? '\n\n\' + env.t('taskAliasPopoverWarning') : ''}}")=env.t('taskAlias') a.hint(href='http://habitica.wikia.com/wiki/Task_Alias', target='_blank', popover-trigger='mouseenter', popover="{{::env.t('taskAliasPopover')}} {{::task._edit.alias ? '\n\n\' + env.t('taskAliasPopoverWarning') : ''}}")=env.t('taskAlias')
input.form-control(ng-model='task._edit.alias' type='text' placeholder=env.t('taskAliasPlaceholder')) input.form-control(ng-model='task._edit.alias' type='text' placeholder=env.t('taskAliasPlaceholder'))
include ./habits/frequency
fieldset.option-group.advanced-option(ng-show="task._edit._advanced", ng-if="!obj.auth && obj.purchased && obj.purchased.active") fieldset.option-group.advanced-option(ng-show="task._edit._advanced", ng-if="!obj.auth && obj.purchased && obj.purchased.active")
group-tasks-actions(task='task', group='obj') group-tasks-actions(task='task', group='obj')

View File

@@ -0,0 +1,8 @@
fieldset.option-group.counter_period(ng-if='task.type === "habit" && canEdit(task)')
.form-group
legend.option-title=env.t('counterPeriod')
select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)')
option(value='daily')=env.t('counterPeriodDay')
option(value='weekly')=env.t('counterPeriodWeek')
option(value='monthly')=env.t('counterPeriodMonth')

View File

@@ -1,5 +1,13 @@
.task-meta-controls .task-meta-controls
// Counter
span(ng-if='task.up && task.down')
span(tooltip=env.t('habitCounterUp')) +{{task.counterUp}}|
span(tooltip=env.t('habitCounterDown')) -{{task.counterDown}}&nbsp;
span(ng-if='task.type=="habit" && (!task.up || !task.down)')
span(tooltip=env.t('habitCounter')) {{task.up ? task.counterUp : task.counterDown}}&nbsp;
// Due Date // Due Date
span(ng-if='task.type=="todo" && task.date') span(ng-if='task.type=="todo" && task.date')
span(ng-class='{"label label-danger":(moment(task.date).isBefore(_today, "days") && !task.completed)}') {{task.date | date:(user.preferences.dateFormat.indexOf('yyyy') == 0 ? user.preferences.dateFormat.substr(5) : user.preferences.dateFormat.substr(0,5))}} span(ng-class='{"label label-danger":(moment(task.date).isBefore(_today, "days") && !task.completed)}') {{task.date | date:(user.preferences.dateFormat.indexOf('yyyy') == 0 ? user.preferences.dateFormat.substr(5) : user.preferences.dateFormat.substr(0,5))}}