habitica/src/app/scoring.coffee

249 lines
8.3 KiB
CoffeeScript
Raw Normal View History

2012-09-24 20:19:27 +00:00
async = require 'async'
2012-09-24 16:52:44 +00:00
moment = require 'moment'
_ = require 'underscore'
2012-09-24 16:52:44 +00:00
content = require './content'
helpers = require './helpers'
2013-01-20 21:52:56 +00:00
browser = require './browser'
2013-02-01 05:02:19 +00:00
schema = require './schema'
MODIFIER = .03 # each new level, armor, weapon add 3% modifier (this number may change)
user = undefined
model = undefined
# This is required by all the functions, make sure it's set before anythign else is called
setModel = (m) ->
model = m
user = model.at('_user')
2013-01-21 15:00:08 +00:00
2013-01-20 21:52:56 +00:00
###
Calculates Exp & GP modification based on weapon & lvl
{value} task.value for gain
{modifiers} may manually pass in stats as {weapon, exp}. This is used for testing
###
expModifier = (value, modifiers = {}) ->
weapon = modifiers.weapon || user.get('items.weapon')
lvl = modifiers.lvl || user.get('stats.lvl')
dmg = weapon * MODIFIER # each new weapon increases exp gain
dmg += (lvl-1) * MODIFIER # same for lvls
modified = value + (value * dmg)
return modified
###
Calculates HP-loss modification based on armor & lvl
{value} task.value which is hurting us
{modifiers} may manually pass in modifier as {armor, lvl}. This is used for testing
###
hpModifier = (value, modifiers = {}) ->
armor = modifiers.armor || user.get('items.armor')
lvl = modifiers.lvl || user.get('stats.lvl')
ac = armor * MODIFIER # each new armor decreases HP loss
ac += (lvl-1) * MODIFIER # same for lvls
modified = value - (value * ac)
return modified
###
Calculates the next task.value based on direction
For negative values, use a line: something like y=-.1x+1
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. Revisit this formula later
{currentValue} the current value of the task, determines it's next value
{direction} 'up' or 'down'
###
taskDeltaFormula = (currentValue, direction) ->
sign = if (direction == "up") then 1 else -1
delta = if (currentValue < 0) then (( -0.1 * currentValue + 1 ) * sign) else (( Math.pow(0.9,currentValue) ) * sign)
return delta
###
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) ->
userObj = batch.userObj
# if user is dead, dont do anything
return if userObj.stats.lvl == 0
if newStats.hp?
# Game Over
if newStats.hp <= 0
batch.set 'stats.lvl', 0 # signifies dead
batch.set 'stats.hp', 0
return
else
batch.set 'stats.hp', newStats.hp
2013-01-21 04:21:50 +00:00
if newStats.exp?
# level up & carry-over exp
tnl = user.get '_tnl'
if newStats.exp >= tnl
newStats.exp -= tnl
batch.set 'stats.lvl', userObj.stats.lvl + 1
batch.set 'stats.hp', 50
2013-01-31 01:53:09 +00:00
newStats.lvl = userObj.stats.lvl
if !userObj.items?.itemsEnabled and newStats.lvl >= 2
batch.set 'items.itemsEnabled', true #bit of trouble using userSet here
if !userObj.flags?.partyEnabled and newStats.lvl >= 3
batch.set 'flags.partyEnabled', true
batch.set 'stats.exp', newStats.exp
if newStats.money?
money = 0.0 if (!money? or money<0)
batch.set 'stats.money', newStats.money
2012-09-24 16:52:44 +00:00
# {taskId} task you want to score
# {direction} 'up' or 'down'
2013-01-20 20:39:47 +00:00
# {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
2013-02-01 18:49:18 +00:00
score = (taskId, direction, times, batch, cron) ->
times ?= 1
2013-01-20 22:30:51 +00:00
commit = false
unless batch?
commit = true
batch = new schema.BatchUpdate(model)
batch.startTransaction()
userObj = batch.userObj
2013-01-20 22:30:51 +00:00
{money, hp, exp, lvl} = userObj.stats
2013-01-20 20:39:47 +00:00
taskPath = "tasks.#{taskId}"
taskObj = userObj.tasks[taskId]
2012-09-24 20:19:27 +00:00
{type, value} = taskObj
2012-10-25 00:56:00 +00:00
2013-01-20 22:30:51 +00:00
delta = 0
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 = taskDeltaFormula(value, direction)
value += nextDelta if adjustvalue
delta += nextDelta
addPoints = ->
modified = expModifier(delta)
exp += modified
money += modified
2013-01-20 22:30:51 +00:00
subtractPoints = ->
modified = hpModifier(delta)
hp += modified
2013-01-20 22:30:51 +00:00
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
calculateDelta(adjustvalue)
# Add habit value to habit-history (if different)
historyEntry = { date: +new Date, value: value }
2013-01-20 22:30:51 +00:00
if (delta > 0) then addPoints() else subtractPoints()
taskObj.history ?= []
taskObj.history.push historyEntry
batch.set "#{taskPath}.history", taskObj.history if taskObj.value != value
2013-01-20 22:30:51 +00:00
when 'daily'
2013-01-20 22:30:51 +00:00
calculateDelta()
2013-02-01 18:49:18 +00:00
if cron? # cron
2013-01-20 22:30:51 +00:00
subtractPoints()
else
addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes
when 'todo'
calculateDelta()
2013-02-01 18:49:18 +00:00
unless cron? # don't touch stats on cron
addPoints() # obviously for delta>0, but also a trick to undo accidental checkboxes
2013-01-20 22:30:51 +00:00
when 'reward'
# Don't adjust values for rewards
calculateDelta(false)
# purchase item
money -= taskObj.value
num = parseFloat(taskObj.value).toFixed(2)
# if too expensive, reduce health & zero money
if money < 0
hp += money # hp - money difference
money = 0
batch.set "#{taskPath}.value", value
updateStats {hp: hp, exp: exp, money: money}, batch
batch.commit() if commit
return delta
2013-01-20 22:30:51 +00:00
2013-01-21 21:42:23 +00:00
###
At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
For incomplete Dailys, deduct experience
###
2013-01-30 18:33:36 +00:00
cron = (resetDom_cb) ->
today = +new Date
daysPassed = helpers.daysBetween(today, user.get('lastCron'))
if daysPassed > 0
2013-01-29 02:44:26 +00:00
user.set 'lastCron', today
2013-02-01 05:02:19 +00:00
batch = new schema.BatchUpdate(model)
userObj = batch.userObj
hpBefore = userObj.stats.hp #we'll use this later so we can animate hp loss
2013-01-20 22:30:51 +00:00
# Tally each task
todoTally = 0
_.each userObj.tasks, (taskObj) ->
#FIXME remove broken tasks
if taskObj.id? # a task had a null id during cron, this should not be happening
{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++
2013-02-01 18:49:18 +00:00
score id, 'down', daysFailed, batch, true
2013-01-29 02:44:26 +00:00
value = taskObj.value #get updated value
if type == 'daily'
2013-01-29 02:44:26 +00:00
taskObj.history ?= []
taskObj.history.push { date: today, value: value }
taskObj.completed = false
else
absVal = if (completed) then Math.abs(value) else value
todoTally += absVal
batch.set('tasks.' + taskObj.id, taskObj)
# Finished tallying
userObj.history ?= {}; userObj.history.todos ?= []; userObj.history.exp ?= []
2013-01-20 22:30:51 +00:00
userObj.history.todos.push { date: today, value: todoTally }
# tally experience
expTally = userObj.stats.exp
lvl = 0 #iterator
while lvl < (userObj.stats.lvl-1)
lvl++
expTally += (lvl*100)/5
userObj.history.exp.push { date: today, value: expTally }
# Set the new user specs, and animate HP loss
[hpAfter, userObj.stats.hp] = [userObj.stats.hp, hpBefore]
batch.set('stats', userObj.stats)
batch.set('history', userObj.history)
2013-02-01 05:02:19 +00:00
batch.commit()
2013-01-30 18:33:36 +00:00
resetDom_cb(model)
setTimeout (-> user.set 'stats.hp', hpAfter), 1000 # animate hp loss
module.exports = {
setModel: setModel
MODIFIER: MODIFIER
score: score
cron: cron
# testing stuff
expModifier: expModifier
hpModifier: hpModifier
taskDeltaFormula: taskDeltaFormula
}