diff --git a/lib/app/index.js b/lib/app/index.js index 0eadde1de3..5c0b767b94 100644 --- a/lib/app/index.js +++ b/lib/app/index.js @@ -39,14 +39,14 @@ get('/:uidParam?', function(page, model, _arg, next) { potion: content.items.potion, reroll: content.items.reroll }); - model.fn('_user._tnl', '_user.stats.lvl', function(lvl) { - return (lvl * 100) / 5; - }); model.refList("_habitList", "_user.tasks", "_user.habitIds"); model.refList("_dailyList", "_user.tasks", "_user.dailyIds"); model.refList("_todoList", "_user.tasks", "_user.todoIds"); model.refList("_completedList", "_user.tasks", "_user.completedIds"); model.refList("_rewardList", "_user.tasks", "_user.rewardIds"); + model.fn('_user._tnl', '_user.stats.lvl', function(lvl) { + return (lvl * 100) / 5; + }); return page.render(); }); }); @@ -102,7 +102,7 @@ ready(function(model) { } tour.start(); model.on('set', '_user.tasks.*.completed', function(i, completed, previous, isLocal, passed) { - var direction, from, fromIds, task, to, toIds, _ref3, _ref4; + var direction, from, fromIds, to, toIds, _ref3, _ref4; if ((passed != null) && passed.cron) { return; } @@ -115,11 +115,7 @@ ready(function(model) { } throw new Error("Direction neither 'up' nor 'down' on checkbox set."); }; - task = model.at("_user.tasks." + i); - scoring.score({ - task: task, - direction: direction() - }); + scoring.score(i, direction()); if (task.get('type') === 'todo') { _ref3 = direction() === 'up' ? ['todo', 'completed'] : ['completed', 'todo'], from = _ref3[0], to = _ref3[1]; _ref4 = ["_user." + from + "Ids", "_user." + to + "Ids"], from = _ref4[0], to = _ref4[1]; @@ -195,8 +191,7 @@ ready(function(model) { return; } else { task.set('type', 'habit'); - scoring.score({ - task: task, + scoring.score(task.get('id'), { direction: 'down' }); } @@ -290,10 +285,7 @@ ready(function(model) { direction = 'down'; } task = model.at($(el).parents('li')[0]); - return scoring.score({ - task: task, - direction: direction - }); + return scoring.score(task.get('id'), direction); }; exports.revive = function(e, el) { var stats; diff --git a/lib/app/scoring.js b/lib/app/scoring.js index 7cc646b3c8..d285617daa 100644 --- a/lib/app/scoring.js +++ b/lib/app/scoring.js @@ -1,5 +1,7 @@ // Generated by CoffeeScript 1.3.3 -var MODIFIER, content, cron, expModifier, helpers, hpModifier, model, score, setModel, setupNotifications, tally, updateStats, user; +var MODIFIER, content, cron, expModifier, helpers, hpModifier, model, moment, score, setModel, setupNotifications, tally, updateStats, user; + +moment = require('moment'); content = require('./content'); @@ -120,18 +122,17 @@ updateStats = function(stats) { } }; -score = function(spec) { - var adjustvalue, cron, delta, direction, exp, hp, lvl, modified, money, num, sign, task, type, value, _ref, _ref1, _ref2; - if (spec == null) { - spec = { - task: null, - direction: null, - cron: null - }; +score = function(taskId, direction, cron) { + var adjustvalue, delta, exp, hp, lvl, modified, money, num, sign, task, taskObj, taskPath, type, userObj, value, _ref, _ref1, _ref2, _ref3; + if (cron == null) { + cron = false; } - _ref = [spec.task, spec.direction, spec.cron], task = _ref[0], direction = _ref[1], cron = _ref[2]; + taskPath = "_user.tasks." + taskId; + _ref = [model.at(taskPath), model.get(taskPath)], task = _ref[0], taskObj = _ref[1]; + _ref1 = [taskObj.type, taskObj.value], type = _ref1[0], value = _ref1[1]; + userObj = user.get(); if (!task) { - _ref1 = [user.get('stats.money'), user.get('stats.hp'), user.get('stats.exp')], money = _ref1[0], hp = _ref1[1], exp = _ref1[2]; + _ref2 = [userObj.stats.money, userObj.stats.hp, userObj.stats.exp], money = _ref2[0], hp = _ref2[1], exp = _ref2[2]; if (direction === "up") { modified = expModifier(1); money += modified; @@ -148,26 +149,24 @@ score = function(spec) { return; } sign = direction === "up" ? 1 : -1; - value = task.get('value'); delta = value < 0 ? (-0.1 * value + 1) * sign : (Math.pow(0.9, value)) * sign; - type = task.get('type'); adjustvalue = type !== 'reward'; - if ((type === 'habit') && (task.get("up") === false || task.get("down") === false)) { + if ((type === 'habit') && (taskObj.up === false || taskObj.down === false)) { adjustvalue = false; } if (adjustvalue) { value += delta; } if (type === 'habit') { - if (task.get('value') !== value) { + if (taskObj.value !== value) { task.push('history', { - date: new Date(), + date: moment().sod().toDate(), value: value }); } } task.set('value', value); - _ref2 = [user.get('stats.money'), user.get('stats.hp'), user.get('stats.exp'), user.get('stats.lvl')], money = _ref2[0], hp = _ref2[1], exp = _ref2[2], lvl = _ref2[3]; + _ref3 = [userObj.stats.money, userObj.stats.hp, userObj.stats.exp, userObj.stats.lvl], money = _ref3[0], hp = _ref3[1], exp = _ref3[2], lvl = _ref3[3]; if (type === 'reward') { money -= task.get('value'); num = parseFloat(task.get('value')).toFixed(2); @@ -232,11 +231,7 @@ tally = function(momentDate) { }; dueToday = repeat && repeat[dayMapping[momentDate.day()]] === true; if (dueToday || type === 'todo') { - score({ - task: task, - direction: 'down', - cron: true - }); + score(taskId, 'down', true); } } if (type === 'daily') { diff --git a/src/app/index.coffee b/src/app/index.coffee index 88e582fc46..a08c7caacc 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -12,6 +12,7 @@ helpers.viewHelpers(view) # $ = require('jQuery') # und = require('underscore') # node.js uses _ + # ========== ROUTES ========== get '/:uidParam?', (page, model, {uidParam}, next) -> @@ -23,36 +24,41 @@ get '/:uidParam?', (page, model, {uidParam}, next) -> model.set('_facebookAuthenticated', true) model.set '_userId', sess.userId model.subscribe "users.#{sess.userId}", (err, user) -> + # Set variables which are passed from the controller to the view model.ref '_user', user - + + #FIXME remove this eventually, part of user schema user.setNull 'balance', 2 - # Store + # Setup Item Store model.set '_items' armor: content.items.armor[parseInt(user.get('items.armor')) + 1] weapon: content.items.weapon[parseInt(user.get('items.weapon')) + 1] potion: content.items.potion reroll: content.items.reroll - - model.fn '_user._tnl', '_user.stats.lvl', (lvl) -> - # see https://github.com/lefnire/habitrpg/issues/4 - # also update in scoring.coffee. TODO create a function accessible in both locations - (lvl*100)/5 - - # Default Tasks + + # Setup Task Lists model.refList "_habitList", "_user.tasks", "_user.habitIds" model.refList "_dailyList", "_user.tasks", "_user.dailyIds" model.refList "_todoList", "_user.tasks", "_user.todoIds" model.refList "_completedList", "_user.tasks", "_user.completedIds" model.refList "_rewardList", "_user.tasks", "_user.rewardIds" + # Setup Model Functions + model.fn '_user._tnl', '_user.stats.lvl', (lvl) -> + # see https://github.com/lefnire/habitrpg/issues/4 + # also update in scoring.coffee. TODO create a function accessible in both locations + (lvl*100)/5 + + # Render Page page.render() # ========== CONTROLLER FUNCTIONS ========== ready (model) -> + # Setup model in scoring functions scoring.setModel(model) - + $('[rel=tooltip]').tooltip() $('[rel=popover]').popover() # FIXME: this isn't very efficient, do model.on set for specific attrs for popover @@ -101,8 +107,8 @@ ready (model) -> throw new Error("Direction neither 'up' nor 'down' on checkbox set.") # Score the user based on todo task - task = model.at("_user.tasks.#{i}") - scoring.score({task:task, direction:direction()}) + # task = model.at("_user.tasks.#{i}") + scoring.score(i, direction()) # Then move the todos to/from _todoList/_completedList if task.get('type') == 'todo' @@ -155,7 +161,7 @@ ready (model) -> return # Cancel. Don't delete, don't hurt user else task.set('type','habit') # hack to make sure it hits HP, instead of performing "undo checkbox" - scoring.score({task:task, direction:'down'}) + scoring.score(task.get('id'), direction:'down') # prevent accidently deleting long-standing tasks else @@ -236,7 +242,7 @@ ready (model) -> direction = 'up' if direction == 'true/' direction = 'down' if direction == 'false/' task = model.at $(el).parents('li')[0] - scoring.score({task:task, direction:direction}) + scoring.score(task.get('id'), direction) exports.revive = (e, el) -> stats = model.at '_user.stats' diff --git a/src/app/scoring.coffee b/src/app/scoring.coffee index d91c08130c..139e445c07 100644 --- a/src/app/scoring.coffee +++ b/src/app/scoring.coffee @@ -1,5 +1,6 @@ -content = require('./content') -helpers = require('./helpers') +moment = require 'moment' +content = require './content' +helpers = require './helpers' MODIFIER = .03 # each new level, armor, weapon add 3% modifier (this number may change) user = undefined model = undefined @@ -103,12 +104,20 @@ updateStats = (stats) -> money = 0.0 if (!money? or money<0) user.set 'stats.money', stats.money -score = (spec = {task:null, direction:null, cron:null}) -> - [task, direction, cron] = [spec.task, spec.direction, spec.cron] +# {taskId} task you want to score +# {direction} 'up' or 'down' +# {cron} is this function being called by cron? (this will usually be false) +score = (taskId, direction, cron=false) -> + taskPath = "_user.tasks.#{taskId}" + [task, taskObj] = [model.at(taskPath), model.get(taskPath)] + [type, value] = [taskObj.type, taskObj.value] + userObj = user.get() + # up / down was called by itself, probably as REST from 3rd party service + #FIXME handle this if !task - [money, hp, exp] = [user.get('stats.money'), user.get('stats.hp'), user.get('stats.exp')] + [money, hp, exp] = [userObj.stats.money, userObj.stats.hp, userObj.stats.exp] if (direction == "up") modified = expModifier(1) money += modified @@ -124,24 +133,21 @@ score = (spec = {task:null, direction:null, cron:null}) -> # For positibe values, taper off with inverse log: y=.9^x # Would love to use inverse log for the whole thing, but after 13 fails it hits infinity sign = if (direction == "up") then 1 else -1 - value = task.get('value') delta = if (value < 0) then (( -0.1 * value + 1 ) * sign) else (( Math.pow(0.9,value) ) * sign) - - type = task.get('type') # Don't adjust values for rewards, or for habits that don't have both + and - adjustvalue = (type != 'reward') - if (type == 'habit') and (task.get("up")==false or task.get("down")==false) + if (type == 'habit') and (taskObj.up==false or taskObj.down==false) adjustvalue = false value += delta if adjustvalue if type == 'habit' # Add habit value to habit-history (if different) - task.push 'history', { date: new Date(), value: value } if task.get('value') != value + task.push 'history', { date: moment().sod().toDate(), value: value } if taskObj.value != value task.set('value', value) # Update the user's status - [money, hp, exp, lvl] = [user.get('stats.money'), user.get('stats.hp'), user.get('stats.exp'), user.get('stats.lvl')] + [money, hp, exp, lvl] = [userObj.stats.money, userObj.stats.hp, userObj.stats.exp, userObj.stats.lvl] if type == 'reward' # purchase item @@ -154,7 +160,7 @@ score = (spec = {task:null, direction:null, cron:null}) -> # Add points to exp & money if positive delta # Only take away mony if it was a mistake (aka, a checkbox) - if (delta > 0 or ( type in ['daily', 'todo'])) and !cron + if (delta > 0 or (type in ['daily', 'todo'])) and !cron modified = expModifier(delta) exp += modified money += modified @@ -194,7 +200,7 @@ tally = (momentDate) -> dayMapping = {0:'su',1:'m',2:'t',3:'w',4:'th',5:'f',6:'s',7:'su'} dueToday = (repeat && repeat[dayMapping[momentDate.day()]]==true) if dueToday or type=='todo' - score({task:task, direction:'down', cron:true}) + score(taskId, 'down', true) if type == 'daily' task.push "history", { date: new Date(momentDate), value: value } else diff --git a/test/user.mocha.coffee b/test/user.mocha.coffee index 8bd30175d8..91c4eb816b 100644 --- a/test/user.mocha.coffee +++ b/test/user.mocha.coffee @@ -7,6 +7,9 @@ scoring = require '../src/app/scoring' schema = require '../src/app/schema' _ = require '../public/js/underscore-min' +modifictionLookup = (value, direction) -> + #TODO implement a lookup table to test if user stats & task value has been modified properly + describe 'User', -> model = null @@ -48,33 +51,54 @@ describe 'User', -> expect(task.value).to.eql 0 it 'made proper modifications when down-scored', -> - # Down-score the habit - [userBefore, taskBefore] = [model.get('_user'), model.get(taskPath)] - scoring.score({taskId:uuid, direction:'down'}) - [userAfter, taskAfter] = [model.get('_user'), model.get(taskPath)] + # Setup 'before' objects for before/after comparisons + statsBefore = _.clone(model.get('_user.stats')) + taskBefore = _.clone(model.get(taskPath)) + + ## Trial 1 + scoring.score(uuid, 'down') + statsAfter = _.clone(model.get('_user.stats')) + taskAfter = _.clone(model.get(taskPath)) # User should have lost HP - expect(userAfter.stats.hp).to.be.lessThan userBefore.stats.hp + expect(statsAfter.hp).to.be.lessThan statsBefore.hp # Exp, GP should stay the same - expect(userAfter.stats.money).to.eql userBefore.stats.money - expect(userAfter.stats.exp).to.eql userBefore.stats.exp - # Task should have gained in value - expect(taskAfter.value).to.be.greaterThan taskBefore.value + expect(statsAfter.money).to.eql statsBefore.money + expect(statsAfter.exp).to.eql statsBefore.exp + + # Task should have gained in value (we're going down, so think Math.abs(task.value)) + expect(taskBefore.value).to.eql 0 + expect(taskAfter.value).to.eql -1 + + ## Trial 2 + taskBefore = _.clone(taskAfter) + scoring.score(uuid, 'down') + taskAfter = _.clone(model.get(taskPath)) + # Should have gained in value + expect(taskAfter.value).to.be < taskBefore.value + # And gained more than trial 1 + expect(Math.abs(taskAfter.value) - Math.abs(taskBefore.value)).to.be.greaterThan 1 it 'made proper modifications when up-scored', -> # Up-score the habit - [userBefore, taskBefore] = [model.get('_user'), model.get(taskPath)] + statsBefore = _.clone(model.get('_user.stats')) + taskBefore = _.clone(model.get(taskPath)) scoring.score(uuid, 'up') - [userAfter, taskAfter] = [model.get('_user'), model.get(taskPath)] + statsAfter = _.clone(model.get('_user.stats')) + taskAfter = _.clone(model.get(taskPath)) # User should have gained Exp, GP - expect(userAfter.stats.exp).to.be.greaterThan userBefore.stats.exp - expect(userAfter.stats.money).to.be.greaterThan userBefore.stats.money + expect(statsAfter.exp).to.be.greaterThan statsBefore.exp + expect(statsAfter.money).to.be.greaterThan statsBefore.money # HP should not change - expect(userAfter.stats.hp).to.eql userBefore.stats.hp + expect(statsAfter.hp).to.eql statsBefore.hp # Task should have lost value - expect(taskAfter.value).to.be.lessThan taskBefore.value + expect(taskBefore.value).to.eql 0 + expect(taskAfter.value).to.be.lessThan 1 + it 'makes history entry for habit' + it 'makes proper modifications each time when clicking + / - in rapid succession' + # saw an issue here once, so test that it wasn't a fluke it 'should not modify certain attributes given certain conditions' # non up+down habits @@ -82,26 +106,24 @@ describe 'User', -> it 'should show "undo" notification if user unchecks completed daily' - describe 'Dailies', -> + describe 'Dailies', -> #TODO clicking repeat dates on newly-created item doesn't refresh until you refresh the page #TODO dates on dailies is having issues, possibility: date cusps? my saturday exempts were set to exempt at 8pm friday #TODO refactor as user->habits, user->dailys, user->todos, user->rewards describe 'Lvl & Items', -> - it 'modified damage based on lvl & armor' + it 'modified damage based on lvl & armor' it 'always decreases hp with damage, regardless of stats/items' it 'always increases exp/gp with gain, regardless of stats/items' describe 'Cron', -> + it 'should calculate user.stats & task.value properly on cron' it 'should calculate cron based on difference between start-of-days, and not run in the middle of the day' - it 'should only run set operations once per task, even when daysPassed > 1' # pass in daysPassed to score, multiply modification values by daysPassed before running set - it 'should only push a history point for lastCron, not each day in between' # stop passing in tallyFor, let moment().sod().toDate() be handled in scoring.score() - it 'should defer saving user modifications until, save as aggregate values' # pass in commit parameter to scoring func, if true save right away, otherwise return aggregated array so can save in the end (so total hp loss, etc)