diff --git a/src/app/index.coffee b/src/app/index.coffee index b25e52cb9d..73a33d3c9b 100644 --- a/src/app/index.coffee +++ b/src/app/index.coffee @@ -45,13 +45,12 @@ get '/', (page, model, params, next) -> ready (model) -> user = model.at('_user') - score = new scoring.Scoring(model) #set cron immediately lastCron = user.get('lastCron') user.set('lastCron', +new Date) if (!lastCron? or lastCron == 'new') - score.cron() + scoring.cron(model) character.app(exports, model) tasks.app(exports, model) diff --git a/src/app/scoring.coffee b/src/app/scoring.coffee index a25bc8d34e..6818db1bac 100644 --- a/src/app/scoring.coffee +++ b/src/app/scoring.coffee @@ -7,240 +7,242 @@ character = require './character' items = require './items' algos = require './algos' -module.exports.Scoring = (model) -> - MODIFIER = algos.MODIFIER # each new level, armor, weapon add 2% modifier (this mechanism will change) +MODIFIER = algos.MODIFIER # each new level, armor, weapon add 2% modifier (this mechanism will change) + +# {taskId} task you want to score +# {direction} 'up' or 'down' +# {times} # times to call score on this task (1 unless cron, usually) +# {update} if we're running updates en-mass (eg, cron on server) pass in userObj +score = (model, taskId, direction, times, batch, cron) -> user = model.at '_user' - # {taskId} task you want to score - # {direction} 'up' or 'down' - # {times} # times to call score on this task (1 unless cron, usually) - # {update} if we're running updates en-mass (eg, cron on server) pass in userObj - score = (taskId, direction, times, batch, cron) -> - commit = false - unless batch? - commit = true - batch = new character.BatchUpdate(model) - batch.startTransaction() - obj = batch.obj() + commit = false + unless batch? + commit = true + batch = new character.BatchUpdate(model) + batch.startTransaction() + obj = batch.obj() - {gp, hp, exp, lvl} = obj.stats + {gp, hp, exp, lvl} = obj.stats - taskPath = "tasks.#{taskId}" - taskObj = obj.tasks[taskId] - {type, value} = taskObj + taskPath = "tasks.#{taskId}" + taskObj = obj.tasks[taskId] + {type, value} = taskObj - # If they're trying to purhcase a too-expensive reward, confirm they want to take a hit for it - if taskObj.value > obj.stats.gp and taskObj.type is 'reward' - r = confirm "Not enough GP to purchase this reward, buy anyway and lose HP? (Punishment for taking a reward you didn't earn)." - unless r - batch.commit() - return - - delta = 0 - times ?= 1 - calculateDelta = (adjustvalue=true) -> - # If multiple days have passed, multiply times days missed - _.times times, (n) -> - # Each iteration calculate the delta (nextDelta), which is then accumulated in delta - # (aka, the total delta). This weirdness won't be necessary when calculating mathematically - # rather than iteratively - nextDelta = algos.taskDeltaFormula(value, direction) - value += nextDelta if adjustvalue - delta += nextDelta - - addPoints = -> - level = user.get('stats.lvl') - weaponStrength = items.items.weapon[user.get('items.weapon')].strength - modified = algos.expModifier(delta,weaponStrength,level) - exp += modified*10 - gp += delta - - subtractPoints = -> - level = user.get('stats.lvl') - armorDefense = items.items.armor[user.get('items.armor')].defense - helmDefense = items.items.head[user.get('items.head')].defense - shieldDefense = items.items.shield[user.get('items.shield')].defense - modified = algos.hpModifier(delta,armorDefense,helmDefense,shieldDefense,level) - hp += modified - - switch type - when 'habit' - # Don't adjust values for habits that don't have both + and - - #adjustvalue = if (taskObj.up==false or taskObj.down==false) then false else true - adjustvalue = true; - calculateDelta(adjustvalue) - # Add habit value to habit-history (if different) - if (delta > 0) then addPoints() else subtractPoints() - taskObj.history ?= [] - if taskObj.value != value - historyEntry = { date: +new Date, value: value } - taskObj.history.push historyEntry - batch.set "#{taskPath}.history", taskObj.history - - when 'daily' - #calculateDelta() - if cron? # cron - calculateDelta() - subtractPoints() - else - calculateDelta(false) - addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes - - when 'todo' - if cron? #cron - calculateDelta() - #don't touch stats on cron - else - calculateDelta(false) - addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes - - when 'reward' - # Don't adjust values for rewards - calculateDelta(false) - # purchase item - gp -= Math.abs(taskObj.value) - num = parseFloat(taskObj.value).toFixed(2) - # if too expensive, reduce health & zero gp - if gp < 0 - hp += gp # hp - gp difference - gp = 0 - - taskObj.value = value - batch.set "#{taskPath}.value", taskObj.value - origStats = _.clone obj.stats - updateStats {hp: hp, exp: exp, gp: gp}, batch - if commit - # newStats / origStats is a glorious hack to trick Derby into seeing the change in model.on(*) - newStats = _.clone batch.obj().stats - _.each Object.keys(origStats), (key) -> obj.stats[key] = origStats[key] - batch.setStats(newStats) - # batch.setStats() + # If they're trying to purhcase a too-expensive reward, confirm they want to take a hit for it + if taskObj.value > obj.stats.gp and taskObj.type is 'reward' + r = confirm "Not enough GP to purchase this reward, buy anyway and lose HP? (Punishment for taking a reward you didn't earn)." + unless r batch.commit() - return delta + return - ### - Updates user stats with new stats. Handles death, leveling up, etc - {stats} new stats - {update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately - ### - updateStats = (newStats, batch) -> - obj = batch.obj() + delta = 0 + times ?= 1 + calculateDelta = (adjustvalue=true) -> + # If multiple days have passed, multiply times days missed + _.times times, (n) -> + # Each iteration calculate the delta (nextDelta), which is then accumulated in delta + # (aka, the total delta). This weirdness won't be necessary when calculating mathematically + # rather than iteratively + nextDelta = algos.taskDeltaFormula(value, direction) + value += nextDelta if adjustvalue + delta += nextDelta - # if user is dead, dont do anything - return if obj.stats.lvl == 0 + addPoints = -> + level = user.get('stats.lvl') + weaponStrength = items.items.weapon[user.get('items.weapon')].strength + modified = algos.expModifier(delta,weaponStrength,level) + exp += modified*10 + gp += delta - if newStats.hp? - # Game Over - if newStats.hp <= 0 - obj.stats.lvl = 0 # signifies dead - obj.stats.hp = 0 - return + subtractPoints = -> + level = user.get('stats.lvl') + armorDefense = items.items.armor[user.get('items.armor')].defense + helmDefense = items.items.head[user.get('items.head')].defense + shieldDefense = items.items.shield[user.get('items.shield')].defense + modified = algos.hpModifier(delta,armorDefense,helmDefense,shieldDefense,level) + hp += modified + + switch type + when 'habit' + # Don't adjust values for habits that don't have both + and - + #adjustvalue = if (taskObj.up==false or taskObj.down==false) then false else true + adjustvalue = true; + calculateDelta(adjustvalue) + # Add habit value to habit-history (if different) + if (delta > 0) then addPoints() else subtractPoints() + taskObj.history ?= [] + if taskObj.value != value + historyEntry = { date: +new Date, value: value } + taskObj.history.push historyEntry + batch.set "#{taskPath}.history", taskObj.history + + when 'daily' + #calculateDelta() + if cron? # cron + calculateDelta() + subtractPoints() else - obj.stats.hp = newStats.hp + calculateDelta(false) + addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes - if newStats.exp? - # level up & carry-over exp - tnl = model.get '_tnl' - silent = false - if newStats.exp >= tnl - silent = true - user.set('stats.exp', newStats.exp) - newStats.exp -= tnl - obj.stats.lvl++ - obj.stats.hp = 50 + when 'todo' + if cron? #cron + calculateDelta() + #don't touch stats on cron + else + calculateDelta(false) + addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes - obj.stats.exp = newStats.exp - user.pass(silent:true).set('stats.exp', obj.stats.exp) if silent + when 'reward' + # Don't adjust values for rewards + calculateDelta(false) + # purchase item + gp -= Math.abs(taskObj.value) + num = parseFloat(taskObj.value).toFixed(2) + # if too expensive, reduce health & zero gp + if gp < 0 + hp += gp # hp - gp difference + gp = 0 - # Set flags when they unlock features - if !obj.flags.customizationsNotification and (obj.stats.exp > 10 or obj.stats.lvl > 1) - batch.set 'flags.customizationsNotification', true - obj.flags.customizationsNotification = true - if !obj.flags.itemsEnabled and obj.stats.lvl >= 2 - # Set to object, then also send to browser right away to get model.on() subscription notification - batch.set 'flags.itemsEnabled', true - obj.flags.itemsEnabled = true - if !obj.flags.partyEnabled and obj.stats.lvl >= 3 - batch.set 'flags.partyEnabled', true - obj.flags.partyEnabled = true - if !obj.flags.petsEnabled and obj.stats.lvl >= 4 - batch.set 'flags.petsEnabled', true - obj.flags.petsEnabled = true + taskObj.value = value + batch.set "#{taskPath}.value", taskObj.value + origStats = _.clone obj.stats + updateStats model, {hp: hp, exp: exp, gp: gp}, batch + if commit + # newStats / origStats is a glorious hack to trick Derby into seeing the change in model.on(*) + newStats = _.clone batch.obj().stats + _.each Object.keys(origStats), (key) -> obj.stats[key] = origStats[key] + batch.setStats(newStats) + # batch.setStats() + batch.commit() + return delta - if newStats.gp? - #FIXME what was I doing here? I can't remember, gp isn't defined - gp = 0.0 if (!gp? or gp<0) - obj.stats.gp = newStats.gp +### + Updates user stats with new stats. Handles death, leveling up, etc + {stats} new stats + {update} if aggregated changes, pass in userObj as update. otherwise commits will be made immediately +### +updateStats = (model, newStats, batch) -> + user = model.at '_user' + obj = batch.obj() - ### - At end of day, add value to all incomplete Daily & Todo tasks (further incentive) - For incomplete Dailys, deduct experience - ### - cron = () -> - today = +new Date - daysPassed = helpers.daysBetween(today, user.get('lastCron')) - if daysPassed > 0 - batch = new character.BatchUpdate(model) - batch.startTransaction() - batch.set 'lastCron', today - obj = batch.obj() - hpBefore = obj.stats.hp #we'll use this later so we can animate hp loss - # Tally each task - todoTally = 0 - _.each obj.tasks, (taskObj) -> - {id, type, completed, repeat} = taskObj - if type in ['todo', 'daily'] - # Deduct experience for missed Daily tasks, - # but not for Todos (just increase todo's value) - unless completed - # for todos & typical dailies, these are equivalent - daysFailed = daysPassed - # however, for dailys which have repeat dates, need - # to calculate how many they've missed according to their own schedule - if type=='daily' && repeat - daysFailed = 0 - _.times daysPassed, (n) -> - thatDay = moment().subtract('days', n+1) - if repeat[helpers.dayMapping[thatDay.day()]]==true - daysFailed++ - score id, 'down', daysFailed, batch, true + # if user is dead, dont do anything + return if obj.stats.lvl == 0 - if type == 'daily' - taskObj.history ?= [] - taskObj.history.push { date: +new Date, value: value } - batch.set "tasks.#{taskObj.id}.history", taskObj.history - batch.set "tasks.#{taskObj.id}.completed", false - else - value = obj.tasks[taskObj.id].value #get updated value - absVal = if (completed) then Math.abs(value) else value - todoTally += absVal + if newStats.hp? + # Game Over + if newStats.hp <= 0 + obj.stats.lvl = 0 # signifies dead + obj.stats.hp = 0 + return + else + obj.stats.hp = newStats.hp - # Finished tallying - obj.history ?= {}; obj.history.todos ?= []; obj.history.exp ?= [] - obj.history.todos.push { date: today, value: todoTally } - # tally experience - expTally = obj.stats.exp - lvl = 0 #iterator - while lvl < (obj.stats.lvl-1) - lvl++ - expTally += algos.tnl(lvl) - obj.history.exp.push { date: today, value: expTally } + if newStats.exp? + # level up & carry-over exp + tnl = model.get '_tnl' + silent = false + if newStats.exp >= tnl + silent = true + user.set('stats.exp', newStats.exp) + newStats.exp -= tnl + obj.stats.lvl++ + obj.stats.hp = 50 - # Set the new user specs, and animate HP loss - [hpAfter, obj.stats.hp] = [obj.stats.hp, hpBefore] - batch.setStats() - batch.set('history', obj.history) - batch.commit() - browser.resetDom(model) - setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss + obj.stats.exp = newStats.exp + user.pass(silent:true).set('stats.exp', obj.stats.exp) if silent + + # Set flags when they unlock features + if !obj.flags.customizationsNotification and (obj.stats.exp > 10 or obj.stats.lvl > 1) + batch.set 'flags.customizationsNotification', true + obj.flags.customizationsNotification = true + if !obj.flags.itemsEnabled and obj.stats.lvl >= 2 + # Set to object, then also send to browser right away to get model.on() subscription notification + batch.set 'flags.itemsEnabled', true + obj.flags.itemsEnabled = true + if !obj.flags.partyEnabled and obj.stats.lvl >= 3 + batch.set 'flags.partyEnabled', true + obj.flags.partyEnabled = true + if !obj.flags.petsEnabled and obj.stats.lvl >= 4 + batch.set 'flags.petsEnabled', true + obj.flags.petsEnabled = true + + if newStats.gp? + #FIXME what was I doing here? I can't remember, gp isn't defined + gp = 0.0 if (!gp? or gp<0) + obj.stats.gp = newStats.gp + +### + At end of day, add value to all incomplete Daily & Todo tasks (further incentive) + For incomplete Dailys, deduct experience +### +cron = (model) -> + user = model.at '_user' + today = +new Date + daysPassed = helpers.daysBetween(today, user.get('lastCron')) + if daysPassed > 0 + batch = new character.BatchUpdate(model) + batch.startTransaction() + batch.set 'lastCron', today + obj = batch.obj() + hpBefore = obj.stats.hp #we'll use this later so we can animate hp loss + # Tally each task + todoTally = 0 + _.each obj.tasks, (taskObj) -> + {id, type, completed, repeat} = taskObj + if type in ['todo', 'daily'] + # Deduct experience for missed Daily tasks, + # but not for Todos (just increase todo's value) + unless completed + # for todos & typical dailies, these are equivalent + daysFailed = daysPassed + # however, for dailys which have repeat dates, need + # to calculate how many they've missed according to their own schedule + if type=='daily' && repeat + daysFailed = 0 + _.times daysPassed, (n) -> + thatDay = moment().subtract('days', n+1) + if repeat[helpers.dayMapping[thatDay.day()]]==true + daysFailed++ + score model, id, 'down', daysFailed, batch, true + + if type == 'daily' + taskObj.history ?= [] + taskObj.history.push { date: +new Date, value: value } + batch.set "tasks.#{taskObj.id}.history", taskObj.history + batch.set "tasks.#{taskObj.id}.completed", false + else + value = obj.tasks[taskObj.id].value #get updated value + absVal = if (completed) then Math.abs(value) else value + todoTally += absVal + + # Finished tallying + obj.history ?= {}; obj.history.todos ?= []; obj.history.exp ?= [] + obj.history.todos.push { date: today, value: todoTally } + # tally experience + expTally = obj.stats.exp + lvl = 0 #iterator + while lvl < (obj.stats.lvl-1) + lvl++ + expTally += algos.tnl(lvl) + obj.history.exp.push { date: today, value: expTally } + + # Set the new user specs, and animate HP loss + [hpAfter, obj.stats.hp] = [obj.stats.hp, hpBefore] + batch.setStats() + batch.set('history', obj.history) + batch.commit() + browser.resetDom(model) + setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss - return { - score: score - cron: cron +module.exports = { + score: score + cron: cron - # testing stuff - expModifier: algos.expModifier - hpModifier: algos.hpModifier - taskDeltaFormula: algos.taskDeltaFormula - } + # testing stuff + expModifier: algos.expModifier + hpModifier: algos.hpModifier + taskDeltaFormula: algos.taskDeltaFormula +} diff --git a/src/app/tasks.coffee b/src/app/tasks.coffee index 22995d868c..f84e8d2b2f 100644 --- a/src/app/tasks.coffee +++ b/src/app/tasks.coffee @@ -26,7 +26,6 @@ module.exports.view = (view) -> module.exports.app = (appExports, model) -> user = model.at('_user') - score = new scoring.Scoring(model) user.on 'set', 'tasks.*.completed', (i, completed, previous, isLocal, passed) -> return if passed? && passed.cron # Don't do this stuff on cron @@ -37,7 +36,7 @@ module.exports.app = (appExports, model) -> # Score the user based on todo task task = user.at("tasks.#{i}") - score.score(i, direction()) + scoring.score(model, i, direction()) appExports.addTask = (e, el, next) -> type = $(el).attr('data-task-type') @@ -82,7 +81,7 @@ module.exports.app = (appExports, 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" - score.score(id, direction:'down') + scoring.score(model, id, direction:'down') # prevent accidently deleting long-standing tasks else @@ -172,4 +171,4 @@ module.exports.app = (appExports, model) -> direction = 'up' if direction == 'true/' direction = 'down' if direction == 'false/' task = model.at $(el).parents('li')[0] - score.score(task.get('id'), direction) + scoring.score(model, task.get('id'), direction) diff --git a/src/server/deprecated.coffee b/src/server/deprecated.coffee index d4e5d3b694..c781433aca 100644 --- a/src/server/deprecated.coffee +++ b/src/server/deprecated.coffee @@ -48,8 +48,7 @@ router.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) -> down: true notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." - score = new scoring.Scoring(model) - delta = score.score(taskId, direction) + delta = scoring.score(model, taskId, direction) result = model.get ('_user.stats') result.delta = delta res.send(result)