2013-12-11 16:30:07 +00:00
moment = require ( ' moment ' )
_ = require ( ' lodash ' )
2015-09-06 20:31:08 +00:00
content = require ( ' ./content/index.coffee ' )
2014-03-16 17:37:53 +00:00
i18n = require ( ' ./i18n.coffee ' )
2013-12-11 16:30:07 +00:00
api = module.exports = { }
2014-03-16 17:37:53 +00:00
api.i18n = i18n
2014-03-05 14:48:31 +00:00
2015-03-15 13:48:36 +00:00
# little helper for large arrays of strings. %w"this that another" equivalent from Ruby, I really miss that function
2014-07-17 18:55:45 +00:00
$w = api.$w = (s)-> s . split ( ' ' )
2014-03-09 04:12:55 +00:00
2014-07-17 18:55:45 +00:00
api.dotSet = (obj,path,val)->
arr = path . split ( ' . ' )
_ . reduce arr , (curr, next, index) =>
if ( arr . length - 1 ) == index
curr [ next ] = val
( curr [ next ] ? = { } )
, obj
api.dotGet = (obj,path)->
_ . reduce path . split ( ' . ' ) , ( (curr, next) => curr ? [ next ] ) , obj
2013-12-28 18:31:21 +00:00
2014-11-21 02:06:00 +00:00
# ##
Reflists are arrays , but stored as objects . Mongoose has a helluvatime working with arrays ( the main problem for our
syncing issues ) - so the goal is to move away from arrays to objects , since mongoose can reference elements by ID
no problem . To maintain sorting , we use these helper functions:
# ##
2014-11-20 23:22:42 +00:00
api.refPush = (reflist, item, prune=0) ->
item.sort = if _ . isEmpty ( reflist ) then 0 else _ . max ( reflist , ' sort ' ) . sort + 1
item.id = api . uuid ( ) unless item . id and ! reflist [ item . id ]
reflist [ item . id ] = item
2014-03-09 04:12:55 +00:00
api.planGemLimits =
convRate: 20 #how much does a gem cost?
convCap: 25 #how many gems can be converted / month?
2013-12-11 16:30:07 +00:00
# ##
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Time / Day
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# ##
# ##
Each time we ' re performing date math (cron, task-due-days, etc), we need to take user preferences into consideration.
Specifically { dayStart } ( custom day start ) and { timezoneOffset } . This function sanitizes / defaults those values .
{ now } is also passed in for various purposes , one example being the test scripts scripts testing different " now " times
# ##
sanitizeOptions = (o) ->
dayStart = if ( ! _ . isNaN ( + o . dayStart ) and 0 <= + o . dayStart <= 24 ) then + o . dayStart else 0
timezoneOffset = if o . timezoneOffset then + ( o . timezoneOffset ) else + moment ( ) . zone ( )
now = if o . now then moment ( o . now ) . zone ( timezoneOffset ) else moment ( + new Date ) . zone ( timezoneOffset )
# return a new object, we don't want to add "now" to user object
{ dayStart , timezoneOffset , now }
api.startOfWeek = api.startOfWeek = (options={}) ->
o = sanitizeOptions ( options )
moment ( o . now ) . startOf ( ' week ' )
api.startOfDay = (options={}) ->
2015-06-24 06:55:07 +00:00
# This is designed for use with any date that has an important time portion (e.g., when comparing the current date-time with the previous cron's date-time for determing if cron should run now).
# It changes the time portion of the date-time to be the Custom Day Start hour, so that the date-time is now the user's correct start of day.
# It SUBTRACTS a day if the date-time's original hour is before CDS (e.g., if your CDS is 5am and it's currently 4am, it's still the previous day).
# This is NOT suitable for manipulating any dates that are displayed to the user as a date with no time portion, such as a Daily's Start Dates (e.g., a Start Date of today shows only the date, so it should be considered to be today even if the hidden time portion is before CDS).
2013-12-11 16:30:07 +00:00
o = sanitizeOptions ( options )
2014-10-10 15:42:53 +00:00
dayStart = moment ( o . now ) . startOf ( ' day ' ) . add ( { hours : o . dayStart } )
if moment ( o . now ) . hour ( ) < o . dayStart
dayStart . subtract ( { days : 1 } )
dayStart
2013-12-11 16:30:07 +00:00
2014-01-18 04:41:20 +00:00
api.dayMapping = { 0 : ' su ' , 1 : ' m ' , 2 : ' t ' , 3 : ' w ' , 4 : ' th ' , 5 : ' f ' , 6 : ' s ' }
2013-12-11 16:30:07 +00:00
# ##
Absolute diff from " yesterday " till now
# ##
api.daysSince = (yesterday, options = {}) ->
o = sanitizeOptions options
2014-10-10 15:42:53 +00:00
Math . abs api . startOfDay ( _ . defaults { now : yesterday } , o ) . diff ( api . startOfDay ( _ . defaults { now : o . now } , o ) , ' days ' )
2013-12-11 16:30:07 +00:00
# ##
2015-05-07 22:23:54 +00:00
Should the user do this task on this date , given the task ' s repeat options and user.preferences.dayStart?
2013-12-11 16:30:07 +00:00
# ##
2015-05-07 22:23:54 +00:00
api.shouldDo = (day, dailyTask, options = {}) ->
2015-06-24 03:58:45 +00:00
return false unless dailyTask . type == ' daily '
2013-12-11 16:30:07 +00:00
o = sanitizeOptions options
2015-06-24 06:55:07 +00:00
startOfDayWithCDSTime = api . startOfDay ( _ . defaults { now : day } , o ) # a moment()
2015-05-07 22:23:54 +00:00
2015-07-05 07:39:11 +00:00
taskStartDate = moment ( dailyTask . startDate ) . zone ( o . timezoneOffset )
2015-05-07 22:23:54 +00:00
2015-06-25 01:47:00 +00:00
# The time portion of the Start Date is never visible to or modifiable by the user so we must ignore it.
# Therefore, we must also ignore the time portion of the user's day start (startOfDayWithCDSTime), otherwise the date comparison will be wrong for some times.
# NB: The user's day start date has already been converted to the PREVIOUS day's date if the time portion was before CDS.
2015-07-18 15:52:50 +00:00
taskStartDate = moment ( taskStartDate ) . startOf ( ' day ' )
2015-06-30 04:28:00 +00:00
2015-06-25 01:47:00 +00:00
if taskStartDate > startOfDayWithCDSTime . startOf ( ' day ' )
2015-06-23 23:06:53 +00:00
return false # Daily starts in the future
2015-05-07 22:23:54 +00:00
2015-06-24 06:55:07 +00:00
if dailyTask . frequency == ' daily ' # "Every X Days"
2015-06-24 03:58:45 +00:00
if ! dailyTask . everyX
return false # error condition
2015-06-25 01:47:00 +00:00
daysSinceTaskStart = startOfDayWithCDSTime . startOf ( ' day ' ) . diff ( taskStartDate , ' days ' )
2015-05-07 22:23:54 +00:00
everyXCheck = ( daysSinceTaskStart % dailyTask . everyX == 0 )
2015-06-23 23:06:53 +00:00
return everyXCheck
2015-06-24 06:55:07 +00:00
else if dailyTask . frequency == ' weekly ' # "On Certain Days of the Week"
2015-06-24 03:58:45 +00:00
if ! dailyTask . repeat
return false # error condition
2015-06-24 04:28:55 +00:00
dayOfWeekNum = startOfDayWithCDSTime . day ( ) # e.g. 0 for Sunday
2015-05-23 07:19:52 +00:00
dayOfWeekCheck = dailyTask . repeat [ api . dayMapping [ dayOfWeekNum ] ]
2015-06-23 23:06:53 +00:00
return dayOfWeekCheck
2015-05-07 22:23:54 +00:00
else
# unexpected frequency string
2015-05-18 02:10:40 +00:00
return false
2015-05-07 22:23:54 +00:00
2015-03-13 12:48:14 +00:00
# ##
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Level cap
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# ##
api.maxLevel = 100
api.capByLevel = (lvl) ->
if lvl > api . maxLevel then api . maxLevel else lvl
2015-07-03 19:55:41 +00:00
# ##
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Health cap
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# ##
2015-07-12 01:05:01 +00:00
api.maxHealth = 50
2015-07-03 19:55:41 +00:00
2013-12-11 16:30:07 +00:00
# ##
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Scoring
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# ##
2013-12-13 22:11:04 +00:00
api.tnl = (lvl) ->
2014-05-11 14:53:31 +00:00
Math . round ( ( ( Math . pow ( lvl , 2 ) * 0.25 ) + ( 10 * lvl ) + 139.75 ) / 10 ) * 10
2013-12-13 22:11:04 +00:00
# round to nearest 10?
2013-12-11 16:30:07 +00:00
2013-12-21 17:56:05 +00:00
# ##
A hyperbola function that creates diminishing returns , so you can ' t go to infinite (eg, with Exp gain).
{ max } The asymptote
{ bonus } All the numbers combined for your point bonus ( eg , task . value * user . stats . int * critChance , etc )
{ halfway } ( optional ) the point at which the graph starts bending
# ##
2013-12-29 23:58:45 +00:00
api.diminishingReturns = (bonus, max, halfway=max/2) ->
2013-12-21 17:56:05 +00:00
max * ( bonus / ( bonus + halfway ) )
2013-12-24 16:32:15 +00:00
api.monod = (bonus, rateOfIncrease, max) ->
rateOfIncrease * max * bonus / ( rateOfIncrease * bonus + max )
2013-12-21 17:56:05 +00:00
2013-12-11 16:30:07 +00:00
# ##
Preen history for users with > 7 history entries
This takes an infinite array of single day entries [ day day day day day . . . ] , and turns it into a condensed array
of averages , condensing more the further back in time we go . Eg , 7 entries each for last 7 days ; 1 entry each week
of this month ; 1 entry for each month of this year ; 1 entry per previous year: [ day * 7 week * 4 month * 12 year * infinite ]
# ##
preenHistory = (history) ->
history = _ . filter ( history , (h) -> ! ! h ) # discard nulls (corrupted somehow)
newHistory = [ ]
preen = (amount, groupBy) ->
groups = _ . chain ( history )
. groupBy ( (h) -> moment ( h . date ) . format groupBy ) # get date groupings to average against
. sortBy ( (h, k) -> k ) # sort by date
. value ( ) # turn into an array
groups = groups . slice ( - amount )
groups . pop ( ) # get rid of "this week", "this month", etc (except for case of days)
_ . each groups , (group) ->
newHistory . push
date: moment ( group [ 0 ] . date ) . toDate ( )
#date: moment(group[0].date).format('MM/DD/YYYY') # Use this one when testing
2013-12-22 02:08:58 +00:00
value: _ . reduce ( group , ( (m, obj) -> m + obj . value ) , 0 ) / group . length # average
2013-12-11 16:30:07 +00:00
true
# Keep the last:
preen 50 , " YYYY " # 50 years (habit will toootally be around that long!)
preen moment ( ) . format ( ' MM ' ) , " YYYYMM " # last MM months (eg, if today is 05, keep the last 5 months)
# Then keep all days of this month. Note, the extra logic is to account for Habits, which can be counted multiple times per day
# FIXME I'd rather keep 1-entry/week of this month, then last 'd' days in this week. However, I'm having issues where the 1st starts mid week
thisMonth = moment ( ) . format ( ' YYYYMM ' )
newHistory = newHistory . concat _ . filter ( history , (h)-> moment ( h . date ) . format ( ' YYYYMM ' ) is thisMonth )
#preen Math.ceil(moment().format('D')/7), "YYYYww" # last __ weeks (# weeks so far this month)
#newHistory = newHistory.concat(history.slice -moment().format('D')) # each day of this week
newHistory
2013-12-16 05:37:40 +00:00
# ##
Update the in - browser store with new gear . FIXME this was in user . fns , but it was causing strange issues there
# ##
2014-04-16 23:42:21 +00:00
sortOrder = _ . reduce content . gearTypes , ( (m,v,k)-> m [ v ] = k ; m ) , { }
2013-12-16 05:37:40 +00:00
api.updateStore = (user) ->
return unless user
2014-04-16 23:42:21 +00:00
changes= [ ]
_ . each content . gearTypes , (type) ->
2013-12-16 05:37:40 +00:00
found = _ . find content . gear . tree [ type ] [ user . stats . class ] , (item) ->
! user . items . gear . owned [ item . key ]
changes . push ( found ) if found
true
# Add special items (contrib gear, backer gear, etc)
2013-12-17 05:37:53 +00:00
changes = changes . concat _ . filter content . gear . flat , (v) ->
2015-06-03 17:50:57 +00:00
v . klass in [ ' special ' , ' mystery ' , ' armoire ' ] and ! user . items . gear . owned [ v . key ] and v . canOwn ? ( user )
2013-12-16 05:37:40 +00:00
# Return sorted store (array)
2014-04-16 23:42:21 +00:00
_ . sortBy changes , (c)-> sortOrder [ c . type ]
2013-12-16 05:37:40 +00:00
2013-12-11 16:30:07 +00:00
# ##
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Content
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# ##
api.content = content
# ##
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Misc Helpers
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# ##
api.uuid = ->
" xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx " . replace / [ xy ] / g , (c) ->
r = Math . random ( ) * 16 | 0
v = ( if c is " x " then r else ( r & 0x3 | 0x8 ) )
v . toString 16
2014-01-06 21:11:57 +00:00
api.countExists = (items)-> _ . reduce ( items , ( (m,v)-> m + ( if v then 1 else 0 ) ) , 0 )
2013-12-11 16:30:07 +00:00
# ##
Even though Mongoose handles task defaults , we want to make sure defaults are set on the client - side before
sending up to the server for performance
# ##
2013-12-28 18:31:21 +00:00
api.taskDefaults = (task={}) ->
2013-12-25 17:56:21 +00:00
task.type = ' habit ' unless task . type and task . type in [ ' habit ' , ' daily ' , ' todo ' , ' reward ' ]
2013-12-11 16:30:07 +00:00
defaults =
id: api . uuid ( )
2013-12-25 17:56:21 +00:00
text: if task . id ? then task . id else ' '
2013-12-11 16:30:07 +00:00
notes: ' '
2013-12-13 03:40:05 +00:00
priority: 1
2013-12-11 16:30:07 +00:00
challenge: { }
2013-12-14 00:50:09 +00:00
attribute: ' str '
2014-01-20 01:09:29 +00:00
dateCreated: new Date ( )
2013-12-11 16:30:07 +00:00
_ . defaults task , defaults
_ . defaults ( task , { up : true , down : true } ) if task . type is ' habit '
_ . defaults ( task , { history: [ ] } ) if task . type in [ ' habit ' , ' daily ' ]
_ . defaults ( task , { completed : false } ) if task . type in [ ' daily ' , ' todo ' ]
2015-06-27 09:14:52 +00:00
_ . defaults ( task , { streak : 0 , repeat: { su : true , m : true , t : true , w : true , th : true , f : true , s : true } } , startDate: new Date ( ) , everyX: 1 , frequency: ' weekly ' ) if task . type is ' daily '
2013-12-11 16:30:07 +00:00
task._id = task . id # may need this for TaskSchema if we go back to using it, see http://goo.gl/a5irq4
task . value ? = if task . type is ' reward ' then 10 else 0
2013-12-17 08:06:20 +00:00
task.priority = 1 unless _ . isNumber ( task . priority ) # hotfix for apiv1. once we're off apiv1, we can remove this
2013-12-11 16:30:07 +00:00
task
2014-01-24 21:31:08 +00:00
api.percent = (x,y, dir) ->
switch dir
when " up " then roundFn = Math . ceil
when " down " then roundFn = Math . floor
else roundFn = Math . round
2013-12-11 16:30:07 +00:00
x = 1 if x == 0
2014-09-17 23:26:52 +00:00
Math . max ( 0 , roundFn ( x / y * 100 ) )
2013-12-11 16:30:07 +00:00
# ##
Remove whitespace #FIXME are we using this anywwhere? Should we be?
# ##
api.removeWhitespace = (str) ->
return ' ' unless str
str . replace / \ s / g , ' '
# ##
Encode the download link for . ics iCal file
# ##
api.encodeiCalLink = (uid, apiToken) ->
loc = window ? . location . host or process ? . env ? . BASE_URL or ' '
encodeURIComponent " http:// #{ loc } /v1/users/ #{ uid } /calendar.ics?apiToken= #{ apiToken } "
# ##
Gold amount from their money
# ##
api.gold = (num) ->
if num
return Math . floor num
else
return " 0 "
# ##
Silver amount from their money
# ##
api.silver = (num) ->
if num
( " 0 " + Math . floor ( num - Math . floor ( num ) ) * 100 ) . slice - 2
else
return " 00 "
# ##
Task classes given everything about the class
# ##
2015-01-20 00:50:54 +00:00
api.taskClasses = (task, filters=[], dayStart=0, lastCron=+new Date, showCompleted=false, main=false) ->
2013-12-11 16:30:07 +00:00
return unless task
2015-02-16 21:02:04 +00:00
{ type , completed , value , repeat , priority } = task
2013-12-11 16:30:07 +00:00
# Filters
if main # only show when on your own list
2014-08-22 16:17:01 +00:00
if ! task . _editing # always show task being edited (even when tag removed)
for filter , enabled of filters
if enabled and not task . tags ? [ filter ]
# All the other classes don't matter
return ' hidden '
2013-12-11 16:30:07 +00:00
classes = type
2014-08-22 16:17:01 +00:00
if task . _editing
classes += " beingEdited " # Assists filtering by third-party themes.
# Example: theme normally hides daily when not scheduled for today,
# BUT keeps showing daily when being edited to unschedule it for today
2013-12-11 16:30:07 +00:00
# show as completed if completed (naturally) or not required for today
if type in [ ' todo ' , ' daily ' ]
2015-05-07 22:23:54 +00:00
if completed or ( type is ' daily ' and ! api . shouldDo ( + new Date , task , { dayStart } ) )
2013-12-11 16:30:07 +00:00
classes += " completed "
else
classes += " uncompleted "
else if type is ' habit '
classes += ' habit-wide ' if task . down and task . up
2014-07-22 20:57:38 +00:00
classes += ' habit-narrow ' if ! task . down and ! task . up
2013-12-11 16:30:07 +00:00
2015-06-28 02:33:31 +00:00
if priority == 0.1
classes += ' difficulty-trivial '
else if priority == 1
2015-02-16 21:02:04 +00:00
classes += ' difficulty-easy '
else if priority == 1.5
classes += ' difficulty-medium '
2015-02-16 21:49:31 +00:00
else if priority == 2
2015-02-16 21:02:04 +00:00
classes += ' difficulty-hard '
2013-12-11 16:30:07 +00:00
if value < - 20
classes += ' color-worst '
else if value < - 10
classes += ' color-worse '
else if value < - 1
classes += ' color-bad '
else if value < 1
classes += ' color-neutral '
else if value < 5
classes += ' color-good '
else if value < 10
classes += ' color-better '
else
classes += ' color-best '
return classes
# ##
Friendly timestamp
# ##
api.friendlyTimestamp = (timestamp) -> moment ( timestamp ) . format ( ' MM/DD h:mm:ss a ' )
# ##
Does user have new chat messages ?
# ##
api.newChatMessages = (messages, lastMessageSeen) ->
return false unless messages ? . length > 0
messages ? [ 0 ] and ( messages [ 0 ] . id != lastMessageSeen )
# ##
are any tags active ?
# ##
2013-12-15 20:23:27 +00:00
api.noTags = (tags) -> _ . isEmpty ( tags ) or _ . isEmpty ( _ . filter ( tags , (t)-> t ) )
2013-12-11 16:30:07 +00:00
# ##
Are there tags applied ?
# ##
api.appliedTags = (userTags, taskTags) ->
arr = [ ]
_ . each userTags , (t) ->
return unless t ?
arr . push ( t . name ) if taskTags ? [ t . id ]
arr . join ( ' , ' )
2015-07-22 22:32:12 +00:00
# ##
Various counting functions
# ##
api.count = require ( ' ./count ' )
2015-06-03 20:16:09 +00:00
2013-12-11 16:30:07 +00:00
# ##
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
User ( prototype wrapper to give it ops , helper funcs , and virtuals
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# ##
# ##
2013-12-12 05:20:11 +00:00
User is now wrapped ( both on client and server ) , adding a few new properties:
* getters ( _statsComputed , tasks , etc )
* user . fns , which is a bunch of helper functions
These were originally up above , but they make more sense belonging to the user object so we don ' t have to pass
the user object all over the place . In fact , we should pull in more functions such as cron ( ) , updateStats ( ) , etc .
* user . ops , which is super important:
If a function is inside user . ops , it has magical properties . If you call it on the client it updates the user object in
2013-12-15 21:49:41 +00:00
the browser and when it ' s done it automatically POSTs to the server, calling src/controllers/user.js # OP_NAME (the exact same name
of the op function ) . The first argument req is { query , body , params } , it ' s what the express controller function
expects . This means we call our functions as if we were calling an Express route . Eg , instead of score ( task , direction ) ,
we call score ( { params : { id : task . id , direction : direction } } ) . This also forces us to think about our routes ( whether to use
params , query , or body for variables ) . see http : / / stackoverflow . com / questions / 4024271 / rest - api - best - practices - where - to - put - parameters
If ` src / controllers / user . js #OP_NAME` doesn't exist on the server, it's automatically added. It runs the code in user.ops.OP_NAME
2013-12-12 05:20:11 +00:00
to update the user model server - side , then performs ` user . save ( ) ` . You can see this in action for ` user . ops . buy ` . That
function doesn ' t exist on the server - so the client calls it, it updates user in the browser, auto-POSTs to server, server
handles it by calling ` user . ops . buy ` again ( to update user on the server ) , and then saves . We can do this for
2013-12-15 21:49:41 +00:00
everything that doesn ' t need any code difference from what ' s in user . ops . OP_NAME for special - handling server - side . If we
* do * need special handling , just add ` src / controllers / user . js #OP_NAME` to override the user.ops.OP_NAME, and be
2013-12-12 05:20:11 +00:00
sure to call user . ops . OP_NAME at some point within the overridden function .
TODO
* Is this the best way to wrap the user object ? I thought of using user . prototype , but user is an object not a Function .
user on the server is a Mongoose model , so we can use prototype - but to do it on the client , we ' d probably have to
move to $resource for user
* Move to $resource !
2013-12-11 16:30:07 +00:00
# ##
2014-01-07 17:36:24 +00:00
api.wrap = (user, main=true) ->
2013-12-11 16:30:07 +00:00
return if user . _wrapped
user._wrapped = true
2013-12-13 22:11:04 +00:00
# ----------------------------------------------------------------------
# user.ops shared client/server operations
# ----------------------------------------------------------------------
2014-01-07 17:36:24 +00:00
if main
user.ops =
# ------
# User
# ------
update: (req, cb) ->
_ . each req . body , (v,k) ->
user . fns . dotSet ( k , v ) ; true
cb ? null , user
sleep: (req, cb) ->
user.preferences.sleep = ! user . preferences . sleep
cb ? null , { }
2015-07-10 03:18:37 +00:00
revive: (req, cb, analytics) ->
2014-11-03 18:55:10 +00:00
return cb ? ( { code : 400 , message: " Cannot revive if not dead " } ) unless user . stats . hp <= 0
2014-11-03 18:53:27 +00:00
2014-09-10 04:06:01 +00:00
# Reset stats after death
2014-01-07 17:36:24 +00:00
_ . merge user . stats , { hp : 50 , exp : 0 , gp : 0 }
user . stats . lvl - - if user . stats . lvl > 1
# Lose a stat point
lostStat = user . fns . randomVal _ . reduce ( [ ' str ' , ' con ' , ' per ' , ' int ' ] , ( (m,k)-> m [ k ] = k if user . stats [ k ] ; m ) , { } )
user . stats [ lostStat ] - - if lostStat
# Lose a gear piece
2014-09-12 01:48:37 +00:00
# Free items (value:0) cannot be lost to avoid "pay to win". Subscribers have more free (Mystery) items and so would have a higher chance of losing a free one. The only exception is that the weapon_warrior_0 free item can be lost so that a new player who dies before buying any gear does experience equipment loss.
2014-01-07 17:36:24 +00:00
# Note ""+k string-casting. Without this, when run on the server Mongoose returns funny objects
2014-09-10 04:06:01 +00:00
cl = user . stats . class
2014-11-01 04:05:35 +00:00
gearOwned = user . items . gear . owned . toObject ? ( ) or user . items . gear . owned
2014-09-13 17:19:25 +00:00
losableItems = { }
_ . each gearOwned , (v,k) ->
if v
2014-09-10 04:06:01 +00:00
itm = content . gear . flat [ ' ' + k ]
if itm
2015-06-03 17:50:57 +00:00
if ( itm . value > 0 || k == ' weapon_warrior_0 ' ) && ( itm . klass == cl || ( itm . klass == ' special ' && ( ! itm . specialClass || itm . specialClass == cl ) ) || itm . klass == ' armoire ' )
2014-09-13 17:19:25 +00:00
losableItems [ ' ' + k ] = ' ' + k
lostItem = user . fns . randomVal losableItems
2014-01-07 17:36:24 +00:00
if item = content . gear . flat [ lostItem ]
user . items . gear . owned [ lostItem ] = false
user . items . gear . equipped [ item . type ] = " #{ item . type } _base_0 " if user . items . gear . equipped [ item . type ] is lostItem
user . items . gear . costume [ item . type ] = " #{ item . type } _base_0 " if user . items . gear . costume [ item . type ] is lostItem
user . markModified ? ' items.gear '
2015-07-10 03:18:37 +00:00
analyticsData = {
uuid: user . _id ,
lostItem: lostItem ,
gaLabel: lostItem ,
category: ' behavior '
}
analytics ? . track ( ' Death ' , analyticsData )
2014-04-15 16:15:11 +00:00
cb ? ( if item then { code : 200 , message: i18n . t ( ' messageLostItem ' , { itemText: item . text ( req . language ) } , req . language ) } else null ) , user
2014-01-07 17:36:24 +00:00
reset: (req, cb) ->
user.habits = [ ]
user.dailys = [ ]
user.todos = [ ]
user.rewards = [ ]
user.stats.hp = 50
user.stats.lvl = 1
user.stats.gp = 0
user.stats.exp = 0
# TODO handle MP
gear = user . items . gear
_ . each [ ' equipped ' , ' costume ' ] , (type) ->
gear [ type ] . armor = ' armor_base_0 '
gear [ type ] . weapon = ' weapon_base_0 '
gear [ type ] . head = ' head_base_0 '
gear [ type ] . shield = ' shield_base_0 '
2015-07-18 15:52:50 +00:00
gear.owned = { } if typeof gear . owned == ' undefined '
2014-07-08 00:28:04 +00:00
_ . each gear . owned , (v, k)-> gear . owned [ k ] = false if gear . owned [ k ] ; true
gear.owned.weapon_warrior_0 = true
2014-01-07 17:36:24 +00:00
user . markModified ? ' items.gear.owned '
user.preferences.costume = false
cb ? null , user
2015-07-10 03:18:37 +00:00
reroll: (req, cb, analytics) ->
2014-01-07 17:36:24 +00:00
if user . balance < 1
2014-03-16 17:37:53 +00:00
return cb ? { code : 401 , message: i18n . t ( ' notEnoughGems ' , req . language ) }
2014-01-07 17:36:24 +00:00
user . balance - -
_ . each user . tasks , (task) ->
unless task . type is ' reward '
task.value = 0
user.stats.hp = 50
2015-07-10 03:18:37 +00:00
analyticsData = {
uuid: user . _id ,
acquireMethod: ' Gems ' ,
gemCost: 4 ,
category: ' behavior '
}
analytics ? . track ( ' Fortify Potion ' , analyticsData )
2014-01-07 17:36:24 +00:00
cb ? null , user
2015-07-10 03:18:37 +00:00
rebirth: (req, cb, analytics) ->
2014-01-07 17:36:24 +00:00
# Cost is 8 Gems ($2)
2015-03-13 12:48:14 +00:00
if ( user . balance < 2 && user . stats . lvl < api . maxLevel )
2014-03-16 17:37:53 +00:00
return cb ? { code : 401 , message: i18n . t ( ' notEnoughGems ' , req . language ) }
2015-07-10 03:18:37 +00:00
analyticsData = {
uuid: user . _id ,
category: ' behavior '
}
2015-03-13 12:48:14 +00:00
# only charge people if they are under the max level - ryan
if user . stats . lvl < api . maxLevel
2014-03-28 05:27:37 +00:00
user . balance -= 2
2015-07-10 03:18:37 +00:00
analyticsData.acquireMethod = ' Gems '
analyticsData.gemCost = 8
else
analyticsData.gemCost = 0
analyticsData.acquireMethod = ' > 100 '
analytics ? . track ( ' Rebirth ' , analyticsData )
2014-01-07 17:36:24 +00:00
# Save off user's level, for calculating achievement eligibility later
2015-03-13 12:48:14 +00:00
lvl = api . capByLevel ( user . stats . lvl )
2014-01-07 17:36:24 +00:00
# Turn tasks yellow, zero out streaks
_ . each user . tasks , (task) ->
unless task . type is ' reward '
task.value = 0
if task . type is ' daily '
task.streak = 0
# Reset all dynamic stats
stats = user . stats
stats.buffs = { }
stats.hp = 50
stats.lvl = 1
stats.class = ' warrior '
_ . each [ ' per ' , ' int ' , ' con ' , ' str ' , ' points ' , ' gp ' , ' exp ' , ' mp ' ] , (value) ->
stats [ value ] = 0
# Deequip character, set back to base armor and training sword
gear = user . items . gear
_ . each [ ' equipped ' , ' costume ' ] , (type) ->
2014-09-10 08:40:08 +00:00
gear [ type ] = { } ; # deequip weapon, eyewear, headAccessory, etc, plus future new types
2014-01-07 17:36:24 +00:00
gear [ type ] . armor = ' armor_base_0 '
gear [ type ] . weapon = ' weapon_warrior_0 '
gear [ type ] . head = ' head_base_0 '
gear [ type ] . shield = ' shield_base_0 '
if user . items . currentPet then user . ops . equip ( { params : { type: ' pet ' , key: user . items . currentPet } } )
if user . items . currentMount then user . ops . equip ( { params : { type: ' mount ' , key: user . items . currentMount } } )
2015-08-12 06:01:53 +00:00
# Strip owned gear down to the training sword and free items (zero gold value), but preserve purchase history so user can re-purchase limited edition equipment
_ . each gear . owned , (v, k) -> if gear . owned [ k ] and content . gear . flat [ k ] . value then gear . owned [ k ] = false ; true
2014-01-09 01:35:54 +00:00
gear.owned.weapon_warrior_0 = true
2014-01-07 17:36:24 +00:00
user . markModified ? ' items.gear.owned '
user.preferences.costume = false
# Remove unlocked features
flags = user . flags
2015-06-12 21:17:50 +00:00
if not user . achievements . beastMaster
2014-01-07 17:36:24 +00:00
flags.rebirthEnabled = false
flags.itemsEnabled = false
flags.dropsEnabled = false
flags.classSelected = false
2014-09-10 08:40:08 +00:00
flags.levelDrops = { }
2014-01-07 17:36:24 +00:00
# Award an achievement if this is their first Rebirth, or if they made it further than last time
if not ( user . achievements . rebirths )
user.achievements.rebirths = 1
user.achievements.rebirthLevel = lvl
else if ( lvl > user . achievements . rebirthLevel or lvl is 100 )
user . achievements . rebirths ++
user.achievements.rebirthLevel = lvl
2014-09-10 08:40:08 +00:00
user.stats.buffs = { }
2014-09-16 20:24:39 +00:00
# user.markModified? 'stats'
2014-01-07 17:36:24 +00:00
cb ? null , user
2014-01-28 14:55:42 +00:00
allocateNow: (req, cb) ->
_ . times user . stats . points , user . fns . autoAllocate
user.stats.points = 0
user . markModified ? ' stats '
cb ? null , user . stats
2014-01-07 17:36:24 +00:00
# ------
# Tasks
# ------
clearCompleted: (req, cb) ->
2014-05-16 05:48:02 +00:00
_ . remove user . todos , (t)-> t . completed and ! t . challenge ? . id
2014-01-07 17:36:24 +00:00
user . markModified ? ' todos '
cb ? null , user . todos
sortTask: (req, cb) ->
{ id } = req . params
{ to , from } = req . query
task = user . tasks [ id ]
2014-03-16 17:37:53 +00:00
return cb ? ( { code : 404 , message: i18n . t ( ' messageTaskNotFound ' , req . language ) } ) unless task
2014-01-07 17:36:24 +00:00
return cb ? ( ' ?to=__&from=__ are required ' ) unless to ? and from ?
tasks = user [ " #{ task . type } s " ]
2014-09-09 07:26:00 +00:00
movedTask = tasks . splice ( from , 1 ) [ 0 ]
if to == - 1 # we've used the Push To Bottom feature
tasks . push ( movedTask )
else # any other sort method uses only positive 'to' values
tasks . splice ( to , 0 , movedTask )
2014-01-07 17:36:24 +00:00
cb ? null , tasks
updateTask: (req, cb) ->
2014-03-16 17:37:53 +00:00
return cb ? ( { code : 404 , message : i18n . t ( ' messageTaskNotFound ' , req . language ) } ) unless task = user . tasks [ req . params ? . id ]
2015-01-14 02:17:36 +00:00
_ . merge task , _ . omit ( req . body , [ ' checklist ' , ' id ' , ' type ' ] )
2014-01-07 17:36:24 +00:00
task.checklist = req . body . checklist if req . body . checklist
task . markModified ? ' tags '
cb ? null , task
deleteTask: (req, cb) ->
task = user . tasks [ req . params ? . id ]
2014-03-16 17:37:53 +00:00
return cb ? ( { code : 404 , message : i18n . t ( ' messageTaskNotFound ' , req . language ) } ) unless task
2014-01-07 17:36:24 +00:00
i = user [ task . type + " s " ] . indexOf ( task )
user [ task . type + " s " ] . splice ( i , 1 ) if ~ i
cb ? null , { }
addTask: (req, cb) ->
task = api . taskDefaults ( req . body )
2015-03-13 01:24:01 +00:00
return cb ? ( { code : 409 , message : i18n . t ( ' messageDuplicateTaskID ' , req . language ) } ) if user . tasks [ task . id ] ?
2014-01-07 17:36:24 +00:00
user [ " #{ task . type } s " ] . unshift ( task )
2014-01-08 02:16:35 +00:00
if user . preferences . newTaskEdit then task._editing = true
2014-01-09 04:21:32 +00:00
if user . preferences . tagsCollapsed then task._tags = true
if user . preferences . advancedCollapsed then task._advanced = true
2014-01-07 17:36:24 +00:00
cb ? null , task
task
# ------
# Tags
# ------
addTag: (req, cb) ->
user . tags ? = [ ]
2014-01-08 00:23:22 +00:00
user . tags . push
name: req . body . name
id: req . body . id or api . uuid ( )
2014-01-07 17:36:24 +00:00
cb ? null , user . tags
2014-08-10 14:01:30 +00:00
sortTag: (req, cb) ->
{ to , from } = req . query
return cb ? ( ' ?to=__&from=__ are required ' ) unless to ? and from ?
user . tags . splice to , 0 , user . tags . splice ( from , 1 ) [ 0 ]
cb ? null , user . tags
2014-01-07 17:36:24 +00:00
updateTag: (req, cb) ->
tid = req . params . id
i = _ . findIndex user . tags , { id: tid }
2014-03-16 17:37:53 +00:00
return cb ? ( { code : 404 , message : i18n . t ( ' messageTagNotFound ' , req . language ) } ) if ! ~ i
2014-01-07 17:36:24 +00:00
user . tags [ i ] . name = req . body . name
cb ? null , user . tags [ i ]
deleteTag: (req, cb) ->
tid = req . params . id
i = _ . findIndex user . tags , { id: tid }
2014-03-16 17:37:53 +00:00
return cb ? ( { code : 404 , message : i18n . t ( ' messageTagNotFound ' , req . language ) } ) if ! ~ i
2014-01-07 17:36:24 +00:00
tag = user . tags [ i ]
delete user . filters [ tag . id ]
user . tags . splice i , 1
# remove tag from all tasks
_ . each user . tasks , (task) ->
delete task . tags [ tag . id ]
_ . each [ ' habits ' , ' dailys ' , ' todos ' , ' rewards ' ] , (type) ->
user . markModified ? type
cb ? null , user . tags
2014-11-17 04:03:20 +00:00
# ------
# Webhooks
# ------
addWebhook: (req, cb) ->
wh = user . preferences . webhooks
2014-11-20 23:22:42 +00:00
api . refPush ( wh , { url : req . body . url , enabled: req . body . enabled or true , id : req . body . id } )
2014-11-17 04:03:20 +00:00
user . markModified ? ' preferences.webhooks '
cb ? null , user . preferences . webhooks
updateWebhook: (req, cb) ->
_ . merge ( user . preferences . webhooks [ req . params . id ] , req . body )
user . markModified ? ' preferences.webhooks '
cb ? null , user . preferences . webhooks
deleteWebhook: (req, cb) ->
delete user . preferences . webhooks [ req . params . id ]
user . markModified ? ' preferences.webhooks '
cb ? null , user . preferences . webhooks
2015-03-27 17:35:32 +00:00
# ------
# Push Notifications
# ------
addPushDevice: (req, cb) ->
user.pushDevices = [ ] unless user . pushDevices
pd = user . pushDevices
2015-07-18 15:52:50 +00:00
item = { regId : req . body . regId , type : req . body . type }
2015-03-27 17:35:32 +00:00
i = _ . findIndex pd , { regId: item . regId }
pd . push ( item ) unless i != - 1
cb ? null , user . pushDevices
2015-06-14 16:08:58 +00:00
2014-11-21 02:06:00 +00:00
# ------
# Inbox
# ------
2014-11-21 15:42:08 +00:00
clearPMs: (req, cb) ->
user.inbox.messages = { }
user . markModified ? ' inbox.messages '
cb ? null , user . inbox . messages
2014-11-21 02:06:00 +00:00
deletePM: (req, cb) ->
delete user . inbox . messages [ req . params . id ]
user . markModified ? ' inbox.messages. ' + req . params . id
cb ? null , user . inbox . messages
blockUser: (req, cb) ->
i = user . inbox . blocks . indexOf ( req . params . uuid )
if ~ i then user . inbox . blocks . splice ( i , 1 ) else user . inbox . blocks . push ( req . params . uuid )
user . markModified ? ' inbox.blocks '
cb ? null , user . inbox . blocks
2014-01-07 17:36:24 +00:00
# ------
# Inventory
# ------
feed: (req, cb) ->
{ pet , food } = req . params
food = content . food [ food ]
[ egg , potion ] = pet . split ( ' - ' )
userPets = user . items . pets
2015-02-22 17:12:02 +00:00
# Generate pet display name variable
potionText = if content . hatchingPotions [ potion ] then content . hatchingPotions [ potion ] . text ( ) else potion
eggText = if content . eggs [ egg ] then content . eggs [ egg ] . text ( ) else egg
2015-03-25 15:48:38 +00:00
petDisplayName = i18n . t ( ' petName ' , {
2015-02-22 17:12:02 +00:00
potion: potionText
egg: eggText
} )
2014-03-16 17:37:53 +00:00
return cb ? ( { code : 404 , message : i18n . t ( ' messagePetNotFound ' , req . language ) } ) unless userPets [ pet ]
return cb ? ( { code : 404 , message : i18n . t ( ' messageFoodNotFound ' , req . language ) } ) unless user . items . food ? [ food . key ]
2015-03-26 03:01:42 +00:00
return cb ? ( { code : 401 , message : i18n . t ( ' messageCannotFeedPet ' , req . language ) } ) if content . specialPets [ pet ]
2014-03-16 17:37:53 +00:00
return cb ? ( { code : 401 , message : i18n . t ( ' messageAlreadyMount ' , req . language ) } ) if user . items . mounts [ pet ]
2014-01-07 17:36:24 +00:00
message = ' '
evolve = ->
2014-01-26 01:58:18 +00:00
userPets [ pet ] = - 1
# changed to -1 to mark "owned" pets
2014-01-07 17:36:24 +00:00
user . items . mounts [ pet ] = true
user.items.currentPet = " " if pet is user . items . currentPet
2015-02-24 22:58:48 +00:00
message = i18n . t ( ' messageEvolve ' , { egg: petDisplayName } , req . language )
2014-01-07 17:36:24 +00:00
if food . key is ' Saddle '
2013-12-13 00:38:19 +00:00
evolve ( )
2013-12-17 23:43:10 +00:00
else
2014-01-07 17:36:24 +00:00
if food . target is potion
userPets [ pet ] += 5
2015-02-24 22:58:48 +00:00
message = i18n . t ( ' messageLikesFood ' , { egg: petDisplayName , foodText: food . text ( req . language ) } , req . language )
2013-12-13 22:55:47 +00:00
else
2014-01-07 17:36:24 +00:00
userPets [ pet ] += 2
2015-02-24 22:58:48 +00:00
message = i18n . t ( ' messageDontEnjoyFood ' , { egg: petDisplayName , foodText: food . text ( req . language ) } , req . language )
2014-01-07 17:36:24 +00:00
if userPets [ pet ] >= 50 and ! user . items . mounts [ pet ]
evolve ( )
user . items . food [ food . key ] - -
2015-07-12 21:45:26 +00:00
cb ? { code : 200 , message } , { value: userPets [ pet ] }
2014-01-07 17:36:24 +00:00
2014-12-27 17:48:22 +00:00
buySpecialSpell: (req,cb) ->
{ key } = req . params
item = content . special [ key ]
2014-10-03 06:31:49 +00:00
return cb ? ( { code : 401 , message: i18n . t ( ' messageNotEnoughGold ' , req . language ) } ) if user . stats . gp < item . value
user . stats . gp -= item . value
2014-12-30 02:57:12 +00:00
user . items . special [ key ] ? = 0
user . items . special [ key ] ++
2014-10-03 06:31:49 +00:00
user . markModified ? ' items.special '
2014-12-27 17:48:22 +00:00
message = i18n . t ( ' messageBought ' , { itemText: item . text ( req . language ) } , req . language )
cb ? { code : 200 , message } , _ . pick ( user , $w ' items stats ' )
2014-10-03 06:31:49 +00:00
2014-12-25 20:58:25 +00:00
# buy is for using Gold, purchase is for Gems (I know, I know...)
2015-07-10 03:18:37 +00:00
purchase: (req, cb, analytics) ->
2014-01-07 17:36:24 +00:00
{ type , key } = req . params
2014-03-09 04:12:55 +00:00
if type is ' gems ' and key is ' gem '
2014-12-04 03:24:35 +00:00
{ convRate , convCap } = api . planGemLimits
convCap += user . purchased . plan . consecutive . gemCapExtra
2014-12-05 22:11:27 +00:00
return cb ? ( { code : 401 , message : " Must subscribe to purchase gems with GP " } , req ) unless user . purchased ? . plan ? . customerId
2014-03-09 04:12:55 +00:00
return cb ? ( { code : 401 , message : " Not enough Gold " } ) unless user . stats . gp >= convRate
2014-11-24 19:16:36 +00:00
return cb ? ( { code : 401 , message : " You ' ve reached the Gold=>Gem conversion cap ( #{ convCap } ) for this month. We have this to prevent abuse / farming. The cap will reset within the first three days of next month. " } ) if user . purchased . plan . gemsBought >= convCap
2014-03-09 04:12:55 +00:00
user . balance += . 25
user . purchased . plan . gemsBought ++
user . stats . gp -= convRate
2015-07-10 03:18:37 +00:00
analyticsData = {
uuid: user . _id ,
2015-07-12 15:03:41 +00:00
itemKey: key ,
2015-07-10 03:18:37 +00:00
acquireMethod: ' Gold ' ,
goldCost: convRate ,
category: ' behavior '
}
analytics ? . track ( ' purchase gems ' , analyticsData )
2015-05-27 17:44:38 +00:00
return cb ? { code : 200 , message : " +1 Gem " } , _ . pick ( user , $w ' stats balance ' )
2014-03-09 04:12:55 +00:00
2014-12-25 20:58:25 +00:00
return cb ? ( { code : 404 , message : " :type must be in [eggs,hatchingPotions,food,quests,gear] " } , req ) unless type in [ ' eggs ' , ' hatchingPotions ' , ' food ' , ' quests ' , ' gear ' ]
if type is ' gear '
item = content . gear . flat [ key ]
return cb ? ( { code : 401 , message: i18n . t ( ' alreadyHave ' , req . language ) } ) if user . items . gear . owned [ key ]
2015-05-13 16:19:08 +00:00
price = ( if item . twoHanded or item . gearSet is ' animal ' then 2 else 1 ) / 4
2014-12-25 20:58:25 +00:00
else
item = content [ type ] [ key ]
price = item . value / 4
2014-01-07 17:36:24 +00:00
return cb ? ( { code : 404 , message : " :key not found for Content. #{ type } " } , req ) unless item
2015-05-13 20:15:40 +00:00
return cb ? ( { code : 401 , message: i18n . t ( ' notEnoughGems ' , req . language ) } ) if ( user . balance < price ) or ! user . balance
2014-12-25 20:58:25 +00:00
user . balance -= price
if type is ' gear ' then user . items . gear . owned [ key ] = true
else
user . items [ type ] [ key ] = 0 unless user . items [ type ] [ key ] > 0
user . items [ type ] [ key ] ++
2015-07-10 03:18:37 +00:00
analyticsData = {
uuid: user . _id ,
2015-07-12 15:03:41 +00:00
itemKey: key ,
2015-07-10 12:47:52 +00:00
itemType: ' Market ' ,
2015-07-10 03:18:37 +00:00
acquireMethod: ' Gems ' ,
gemCost: item . value ,
category: ' behavior '
}
analytics ? . track ( ' acquire item ' , analyticsData )
2014-01-07 17:36:24 +00:00
cb ? null , _ . pick ( user , $w ' items balance ' )
2015-07-10 03:18:37 +00:00
releasePets: (req, cb, analytics) ->
2015-03-26 22:30:50 +00:00
if user . balance < 1
2014-09-29 22:11:24 +00:00
return cb ? { code : 401 , message: i18n . t ( ' notEnoughGems ' , req . language ) }
else
2015-03-26 22:30:50 +00:00
user . balance -= 1
2014-09-29 22:11:24 +00:00
for pet of content . pets
user . items . pets [ pet ] = 0
if not user . achievements . beastMasterCount
user.achievements.beastMasterCount = 0
user . achievements . beastMasterCount ++
user.items.currentPet = " "
2015-07-10 03:18:37 +00:00
analyticsData = {
uuid: user . _id ,
acquireMethod: ' Gems ' ,
gemCost: 4 ,
category: ' behavior '
}
analytics ? . track ( ' release pets ' , analyticsData )
2014-09-29 22:11:24 +00:00
cb ? null , user
2015-07-10 03:18:37 +00:00
releaseMounts: (req, cb, analytics) ->
2015-03-26 22:30:50 +00:00
if user . balance < 1
2014-09-29 22:11:24 +00:00
return cb ? { code : 401 , message: i18n . t ( ' notEnoughGems ' , req . language ) }
else
2015-03-26 22:30:50 +00:00
user . balance -= 1
2014-09-29 22:11:24 +00:00
user.items.currentMount = " "
2015-01-02 18:41:36 +00:00
for mount of content . pets
2015-01-10 15:13:25 +00:00
user . items . mounts [ mount ] = null
2015-01-02 18:41:36 +00:00
if not user . achievements . mountMasterCount
user.achievements.mountMasterCount = 0
user . achievements . mountMasterCount ++
2015-07-10 03:18:37 +00:00
analyticsData = {
uuid: user . _id ,
acquireMethod: ' Gems ' ,
gemCost: 4 ,
category: ' behavior '
}
analytics ? . track ( ' release mounts ' , analyticsData )
2014-09-29 22:11:24 +00:00
cb ? null , user
2015-01-02 19:20:25 +00:00
releaseBoth: (req, cb) ->
2015-03-25 15:48:38 +00:00
if user . balance < 1.5 and not user . achievements . triadBingo
2015-01-02 19:20:25 +00:00
return cb ? { code : 401 , message: i18n . t ( ' notEnoughGems ' , req . language ) }
else
2015-01-16 23:01:01 +00:00
giveTriadBingo = true
2015-03-25 15:48:38 +00:00
if not user . achievements . triadBingo
2015-07-10 03:18:37 +00:00
analyticsData = {
uuid: user . _id ,
acquireMethod: ' Gems ' ,
gemCost: 6 ,
category: ' behavior '
}
analytics ? . track ( ' release pets & mounts ' , analyticsData )
2015-03-25 15:48:38 +00:00
user . balance -= 1.5
2014-09-29 22:11:24 +00:00
user.items.currentMount = " "
user.items.currentPet = " "
2015-01-02 19:20:25 +00:00
for animal of content . pets
2015-02-16 21:02:04 +00:00
giveTriadBingo = false if user . items . pets [ animal ] == - 1
2015-01-02 19:20:25 +00:00
user . items . pets [ animal ] = 0
2015-01-10 15:13:25 +00:00
user . items . mounts [ animal ] = null
2014-09-29 22:11:24 +00:00
if not user . achievements . beastMasterCount
user.achievements.beastMasterCount = 0
user . achievements . beastMasterCount ++
2015-01-02 19:20:25 +00:00
if not user . achievements . mountMasterCount
user.achievements.mountMasterCount = 0
user . achievements . mountMasterCount ++
2015-01-16 23:01:01 +00:00
if giveTriadBingo
if not user . achievements . triadBingoCount
user.achievements.triadBingoCount = 0
user . achievements . triadBingoCount ++
2014-09-29 22:11:24 +00:00
cb ? null , user
2014-01-07 17:36:24 +00:00
# buy is for gear, purchase is for gem-purchaseables (i know, I know...)
2015-07-10 03:18:37 +00:00
buy: (req, cb, analytics) ->
2014-01-07 17:36:24 +00:00
{ key } = req . params
2014-02-14 00:57:10 +00:00
2015-05-26 21:06:04 +00:00
item = if key is ' potion ' then content . potion
else if key is ' armoire ' then content . armoire
else content . gear . flat [ key ]
2015-09-06 20:31:08 +00:00
return cb ? ( { code : 404 , message : " Item ' #{ key } not found (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.coffee) " } ) unless item
2014-04-15 16:15:11 +00:00
return cb ? ( { code : 401 , message: i18n . t ( ' messageNotEnoughGold ' , req . language ) } ) if user . stats . gp < item . value
2015-05-26 21:06:04 +00:00
return cb ? ( { code : 401 , message: " You can ' t buy this item " } ) if item . canOwn ? and ! item . canOwn ( user )
2015-06-14 16:08:58 +00:00
armoireResp = undefined
2014-01-07 17:36:24 +00:00
if item . key is ' potion '
user . stats . hp += 15
user.stats.hp = 50 if user . stats . hp > 50
2015-05-26 21:06:04 +00:00
else if item . key is ' armoire '
2015-06-08 11:15:51 +00:00
armoireResult = user . fns . predictableRandom ( user . stats . gp )
# We use a different seed to choose the Armoire action than we use
# to choose the sub-action, otherwise only some of the foods can
# be given. E.g., if a seed gives armoireResult < .5 (food) then
# the same seed would give one of the first five foods only.
2015-05-26 21:06:04 +00:00
eligibleEquipment = _ . filter ( content . gear . flat , ( (i)-> i . klass is ' armoire ' and ! user . items . gear . owned [ i . key ] ) )
2015-06-05 15:22:22 +00:00
if ! _ . isEmpty ( eligibleEquipment ) and ( armoireResult < . 6 or ! user . flags . armoireOpened )
2015-06-20 10:22:27 +00:00
eligibleEquipment . sort ( ) # https://github.com/HabitRPG/habitrpg/issues/5376#issuecomment-111799217
2015-05-26 21:06:04 +00:00
drop = user . fns . randomVal ( eligibleEquipment )
user . items . gear . owned [ drop . key ] = true
user.flags.armoireOpened = true
2015-06-04 20:45:55 +00:00
message = i18n . t ( ' armoireEquipment ' , { image: ' <span class= " shop_ ' + drop . key + ' pull-left " ></span> ' , dropText: drop . text ( req . language ) } , req . language )
2015-07-22 22:37:35 +00:00
if api . count . remainingGearInSet ( user . items . gear . owned , ' armoire ' ) is 0 then user.flags.armoireEmpty = true
2015-06-15 10:57:08 +00:00
armoireResp = { type: " gear " , dropKey: drop . key , dropText: drop . text ( req . language ) }
2015-06-05 15:22:22 +00:00
else if ( ! _ . isEmpty ( eligibleEquipment ) and armoireResult < . 8 ) or armoireResult < . 5
2015-05-26 21:06:04 +00:00
drop = user . fns . randomVal _ . where ( content . food , { canDrop : true } )
user . items . food [ drop . key ] ? = 0
user . items . food [ drop . key ] += 1
2015-06-03 17:50:57 +00:00
message = i18n . t ( ' armoireFood ' , { image: ' <span class= " Pet_Food_ ' + drop . key + ' pull-left " ></span> ' , dropArticle: drop . article , dropText: drop . text ( req . language ) } , req . language )
2015-06-15 10:57:08 +00:00
armoireResp = { type: " food " , dropKey: drop . key , dropArticle: drop . article , dropText: drop . text ( req . language ) }
2015-05-26 21:06:04 +00:00
else
2015-06-14 16:08:58 +00:00
armoireExp = Math . floor ( user . fns . predictableRandom ( user . stats . exp ) * 40 + 10 )
user . stats . exp += armoireExp
2015-05-26 21:06:04 +00:00
message = i18n . t ( ' armoireExp ' , req . language )
2015-06-14 16:08:58 +00:00
armoireResp = { " type " : " experience " , " value " : armoireExp }
2014-01-07 17:36:24 +00:00
else
user . items . gear . equipped [ item . type ] = item . key
user . items . gear . owned [ item . key ] = true
2014-04-07 17:04:59 +00:00
message = user . fns . handleTwoHanded ( item , null , req )
2014-04-15 16:15:11 +00:00
message ? = i18n . t ( ' messageBought ' , { itemText: item . text ( req . language ) } , req . language )
2015-05-26 21:06:04 +00:00
if item . last then user . fns . ultimateGear ( )
2014-01-07 17:36:24 +00:00
user . stats . gp -= item . value
2015-07-10 03:18:37 +00:00
analyticsData = {
uuid: user . _id ,
2015-07-12 15:03:41 +00:00
itemKey: key ,
2015-07-10 03:18:37 +00:00
acquireMethod: ' Gold ' ,
goldCost: item . value ,
category: ' behavior '
}
analytics ? . track ( ' acquire item ' , analyticsData )
2015-06-14 16:08:58 +00:00
buyResp = _ . pick ( user , $w ' items achievements stats flags ' )
buyResp [ " armoire " ] = armoireResp if armoireResp
cb ? { code : 200 , message } , buyResp
2014-01-07 17:36:24 +00:00
2015-07-19 15:13:34 +00:00
buyQuest: (req, cb, analytics) ->
2015-07-08 22:43:08 +00:00
{ key } = req . params
item = content . quests [ key ]
2015-09-06 20:31:08 +00:00
return cb ? ( { code : 404 , message : " Quest ' #{ key } not found (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.coffee) " } ) unless item
return cb ? ( { code : 404 , message : " Quest ' #{ key } is not a Gold-purchasable quest (see https://github.com/HabitRPG/habitrpg/blob/develop/common/script/content/index.coffee) " } ) unless item . category is ' gold ' and item . goldValue
2015-07-13 23:21:32 +00:00
return cb ? ( { code : 401 , message: i18n . t ( ' messageNotEnoughGold ' , req . language ) } ) if user . stats . gp < item . goldValue
2015-07-08 22:43:08 +00:00
message = i18n . t ( ' messageBought ' , { itemText: item . text ( req . language ) } , req . language )
user . items . quests [ item . key ] ? = 0
user . items . quests [ item . key ] += 1
2015-07-13 23:21:32 +00:00
user . stats . gp -= item . goldValue
2015-07-19 15:13:34 +00:00
analyticsData = {
uuid: user . _id ,
itemKey: item . key ,
itemType: ' Market ' ,
goldCost: item . goldValue ,
acquireMethod: ' Gold ' ,
category: ' behavior '
}
analytics ? . track ( ' acquire item ' , analyticsData )
2015-07-08 22:43:08 +00:00
cb ? { code : 200 , message } , user . items . quests
2015-07-10 03:18:37 +00:00
buyMysterySet: (req, cb, analytics)->
2015-09-01 21:17:50 +00:00
return cb ? ( { code : 401 , message : i18n . t ( ' notEnoughHourglasses ' , req . language ) } ) unless user . purchased . plan . consecutive . trinkets > 0
2014-11-29 18:16:54 +00:00
mysterySet = content . timeTravelerStore ( user . items . gear . owned ) ? [ req . params . key ]
if window ? . confirm ?
2015-09-01 21:17:50 +00:00
return unless window . confirm ( i18n . t ( ' hourglassBuyEquipSetConfirm ' ) )
2014-11-29 18:16:54 +00:00
return cb ? ( { code : 404 , message : " Mystery set not found, or set already owned " } ) unless mysterySet
2015-05-27 17:44:38 +00:00
_ . each mysterySet . items , (i)->
user . items . gear . owned [ i . key ] = true
2015-07-10 03:18:37 +00:00
analyticsData = {
uuid: user . _id ,
2015-07-12 15:03:41 +00:00
itemKey: i . key ,
2015-07-10 12:47:52 +00:00
itemType: ' Subscriber Gear ' ,
2015-07-10 03:18:37 +00:00
acquireMethod: ' Hourglass ' ,
category: ' behavior '
}
analytics ? . track ( ' acquire item ' , analyticsData )
2014-11-29 18:16:54 +00:00
user . purchased . plan . consecutive . trinkets - -
2015-09-15 19:41:57 +00:00
cb ? { code : 200 , message : i18n . t ( ' hourglassPurchaseSet ' , req . language ) } , _ . pick ( user , $w ' items purchased.plan.consecutive ' )
2014-11-29 18:16:54 +00:00
2015-09-01 21:17:50 +00:00
hourglassPurchase: (req, cb, analytics)->
2015-09-15 17:40:01 +00:00
{ type , key } = req . params
2015-09-15 18:55:38 +00:00
return cb ? ( { code : 403 , message : i18n . t ( ' typeNotAllowedHourglass ' , req . language ) + JSON . stringify ( _ . keys ( content . timeTravelStable ) ) } ) unless content . timeTravelStable [ type ]
return cb ? ( { code : 403 , message : i18n . t ( type + ' NotAllowedHourglass ' , req . language ) } ) unless _ . contains ( content . timeTravelStable [ type ] , key )
return cb ? ( { code : 403 , message : i18n . t ( type + ' AlreadyOwned ' , req . language ) } ) if user . items [ type ] [ key ]
return cb ? ( { code : 403 , message : i18n . t ( ' notEnoughHourglasses ' , req . language ) } ) unless user . purchased . plan . consecutive . trinkets > 0
user . purchased . plan . consecutive . trinkets - -
if type is ' pets '
user . items . pets [ key ] = 5
if type is ' mounts '
user . items . mounts [ key ] = true
2015-09-15 19:41:57 +00:00
analyticsData = {
uuid: user . _id ,
itemKey: key ,
itemType: type ,
acquireMethod: ' Hourglass ' ,
category: ' behavior '
}
analytics ? . track ( ' acquire item ' , analyticsData )
2015-09-15 18:55:38 +00:00
cb ? { code : 200 , message : i18n . t ( ' hourglassPurchase ' , req . language ) } , _ . pick ( user , $w ' items purchased.plan.consecutive ' )
2015-09-01 21:17:50 +00:00
2014-01-07 17:36:24 +00:00
sell: (req, cb) ->
{ key , type } = req . params
return cb ? ( { code : 404 , message : " :type not found. Must bes in [eggs, hatchingPotions, food] " } ) unless type in [ ' eggs ' , ' hatchingPotions ' , ' food ' ]
return cb ? ( { code : 404 , message : " :key not found for user.items. #{ type } " } ) unless user . items [ type ] [ key ]
user . items [ type ] [ key ] - -
user . stats . gp += content [ type ] [ key ] . value
cb ? null , _ . pick ( user , $w ' stats items ' )
equip: (req, cb) ->
[ type , key ] = [ req . params . type || ' equipped ' , req . params . key ]
switch type
when ' mount '
2014-06-27 12:36:25 +00:00
return cb ? ( { code : 404 , message : " :You do not own this mount. " } ) unless user . items . mounts [ key ]
2014-01-07 17:36:24 +00:00
user.items.currentMount = if user . items . currentMount is key then ' ' else key
when ' pet '
2014-06-27 12:36:25 +00:00
return cb ? ( { code : 404 , message : " :You do not own this pet. " } ) unless user . items . pets [ key ]
2014-01-07 17:36:24 +00:00
user.items.currentPet = if user . items . currentPet is key then ' ' else key
when ' costume ' , ' equipped '
item = content . gear . flat [ key ]
2014-06-27 12:36:25 +00:00
return cb ? ( { code : 404 , message : " :You do not own this gear. " } ) unless user . items . gear . owned [ key ]
2014-03-21 21:06:26 +00:00
if user . items . gear [ type ] [ item . type ] is key
user . items . gear [ type ] [ item . type ] = " #{ item . type } _base_0 "
2014-05-27 02:08:15 +00:00
message = i18n . t ( ' messageUnEquipped ' , { itemText: item . text ( req . language ) } , req . language )
2014-03-21 21:06:26 +00:00
else
user . items . gear [ type ] [ item . type ] = item . key
2014-04-07 17:04:59 +00:00
message = user . fns . handleTwoHanded ( item , type , req )
2014-06-09 19:31:32 +00:00
user . markModified ? " items.gear. #{ type } "
2014-01-07 17:36:24 +00:00
cb ? ( if message then { code : 200 , message } else null ) , user . items
hatch: (req, cb) ->
{ egg , hatchingPotion } = req . params
return cb ? ( { code : 404 , message : " Please specify query.egg & query.hatchingPotion " } ) unless egg and hatchingPotion
2014-04-07 17:04:59 +00:00
return cb ? ( { code : 401 , message : i18n . t ( ' messageMissingEggPotion ' , req . language ) } ) unless user . items . eggs [ egg ] > 0 and user . items . hatchingPotions [ hatchingPotion ] > 0
2014-01-07 17:36:24 +00:00
pet = " #{ egg } - #{ hatchingPotion } "
2014-04-07 17:04:59 +00:00
return cb ? ( { code : 401 , message : i18n . t ( ' messageAlreadyPet ' , req . language ) } ) if user . items . pets [ pet ] and user . items . pets [ pet ] > 0
2014-01-07 17:36:24 +00:00
user . items . pets [ pet ] = 5
user . items . eggs [ egg ] - -
user . items . hatchingPotions [ hatchingPotion ] - -
2014-04-07 17:04:59 +00:00
cb ? { code : 200 , message : i18n . t ( ' messageHatched ' , req . language ) } , user . items
2014-01-07 17:36:24 +00:00
2015-07-10 03:18:37 +00:00
unlock: (req, cb, analytics) ->
2014-01-07 17:36:24 +00:00
{ path } = req . query
fullSet = ~ path . indexOf ( " , " )
2014-06-09 19:32:37 +00:00
cost =
# (Backgrounds) 15G per set, 7G per individual
2015-09-06 20:31:08 +00:00
if ~ path . indexOf ( ' background. ' ) # FIXME, store prices of things in content/index.coffee instead of hard-coded here?
2014-06-09 19:32:37 +00:00
if fullSet then 3.75 else 1.75
# (Skin, hair, etc) 5G per set, 2G per individual
else
if fullSet then 1.25 else 0.5
2014-01-07 17:36:24 +00:00
alreadyOwns = ! fullSet and user . fns . dotGet ( " purchased. " + path ) is true
2015-05-13 20:34:23 +00:00
return cb ? ( { code : 401 , message: i18n . t ( ' notEnoughGems ' , req . language ) } ) if ( user . balance < cost or ! user . balance ) and ! alreadyOwns
2014-01-07 17:36:24 +00:00
if fullSet
_ . each path . split ( " , " ) , (p) ->
2015-05-13 16:19:08 +00:00
if ~ path . indexOf ( ' gear. ' )
user . fns . dotSet ( " #{ p } " , true ) ; true
else
2014-01-07 17:36:24 +00:00
user . fns . dotSet ( " purchased. #{ p } " , true ) ; true
else
if alreadyOwns
split = path . split ( ' . ' ) ; v = split . pop ( ) ; k = split . join ( ' . ' )
2014-06-09 19:32:37 +00:00
v = ' ' if k is ' background ' and v == user . preferences . background
2014-01-07 17:36:24 +00:00
user . fns . dotSet ( " preferences. #{ k } " , v )
return cb ? null , req
user . fns . dotSet " purchased. " + path , true
user . balance -= cost
2015-05-13 16:19:08 +00:00
if ~ path . indexOf ( ' gear. ' ) then user . markModified ? ' gear.owned ' else user . markModified ? ' purchased '
2015-07-10 03:18:37 +00:00
analyticsData = {
uuid: user . _id ,
2015-07-12 15:03:41 +00:00
itemKey: path ,
2015-07-10 03:18:37 +00:00
itemType: ' customization ' ,
acquireMethod: ' Gems ' ,
gemCost: ( cost / . 25 ) ,
category: ' behavior '
}
analytics ? . track ( ' acquire item ' , analyticsData )
2015-05-13 16:19:08 +00:00
cb ? null , _ . pick ( user , $w ' purchased preferences items ' )
2014-01-07 17:36:24 +00:00
# ------
# Classes
# ------
2015-07-10 03:18:37 +00:00
changeClass: (req, cb, analytics) ->
2014-01-07 17:36:24 +00:00
klass = req . query ? . class
if klass in [ ' warrior ' , ' rogue ' , ' wizard ' , ' healer ' ]
2015-07-10 03:18:37 +00:00
analyticsData = {
uuid: user . _id ,
class : klass ,
acquireMethod: ' Gems ' ,
gemCost: 3 ,
category: ' behavior '
}
analytics ? . track ( ' change class ' , analyticsData )
2014-01-07 17:36:24 +00:00
user.stats.class = klass
user.flags.classSelected = true
# Clear their gear and equip their new class's gear (can still equip old gear from inventory)
# If they've rolled this class before, restore their progress
_ . each [ " weapon " , " armor " , " shield " , " head " ] , (type) ->
foundKey = false
_ . findLast user . items . gear . owned , (v, k) ->
return foundKey = k if ~ k . indexOf ( type + " _ " + klass ) and v is true
# restore progress from when they last rolled this class
# weapon_0 is significant, don't reset to base_0
# rogues start with an off-hand weapon
user . items . gear . equipped [ type ] =
if foundKey then foundKey
else if type is " weapon " then " weapon_ #{ klass } _0 "
else if type is " shield " and klass is " rogue " then " shield_rogue_0 "
else " #{ type } _base_0 " # naked for the rest!
# Grant them their new class's gear
user . items . gear . owned [ " #{ type } _ #{ klass } _0 " ] = true if type is " weapon " or ( type is " shield " and klass is " rogue " )
true
else
# Null ?class value means "reset class"
if user . preferences . disableClasses
user.preferences.disableClasses = false
user.preferences.autoAllocate = false
2013-12-11 16:30:07 +00:00
else
2014-04-07 17:04:59 +00:00
return cb ? ( { code : 401 , message : i18n . t ( ' notEnoughGems ' , req . language ) } ) unless user . balance >= . 75
2014-01-07 17:36:24 +00:00
user . balance -= . 75
2015-03-13 12:48:14 +00:00
_ . merge user . stats , { str: 0 , con: 0 , per: 0 , int: 0 , points: api . capByLevel ( user . stats . lvl ) }
2014-01-07 17:36:24 +00:00
user.flags.classSelected = false
#'stats.points': this is handled on the server
cb ? null , _ . pick ( user , $w ' stats flags items preferences ' )
disableClasses: (req, cb) ->
user.stats.class = ' warrior '
user.flags.classSelected = true
user.preferences.disableClasses = true
user.preferences.autoAllocate = true
2015-03-13 12:48:14 +00:00
user.stats.str = api . capByLevel ( user . stats . lvl )
2014-01-07 17:36:24 +00:00
user.stats.points = 0
cb ? null , _ . pick ( user , $w ' stats flags preferences ' )
allocate: (req, cb) ->
stat = req . query . stat or ' str '
if user . stats . points > 0
user . stats [ stat ] ++
user . stats . points - -
user . stats . mp ++ if stat is ' int ' #increase their MP along with their max MP
cb ? null , _ . pick ( user , $w ' stats ' )
2015-08-13 21:58:31 +00:00
readCard: (req, cb) ->
{ cardType } = req . params
2015-08-12 13:36:57 +00:00
user . items . special [ " #{ cardType } Received " ] . shift ( )
user . markModified ? " items.special. #{ cardType } Received "
2015-08-13 19:31:34 +00:00
user.flags.cardReceived = false
cb ? null , ' items.special flags.cardReceived '
2014-02-14 00:57:10 +00:00
2015-07-10 03:18:37 +00:00
openMysteryItem: (req,cb,analytics) ->
2014-03-09 04:12:55 +00:00
item = user . purchased . plan ? . mysteryItems ? . shift ( )
return cb ? ( code : 400 , message : " Empty " ) unless item
item = content . gear . flat [ item ]
user . items . gear . owned [ item . key ] = true
user . markModified ? ' purchased.plan.mysteryItems '
2015-08-25 09:23:25 +00:00
item.notificationType = ' Mystery ' # needed for website/public/js/controllers/notificationCtrl.js line 59 approx.
2015-07-10 03:18:37 +00:00
analyticsData = {
uuid: user . _id ,
2015-07-12 15:03:41 +00:00
itemKey: item ,
2015-07-10 12:47:52 +00:00
itemType: ' Subscriber Gear ' ,
2015-07-10 03:18:37 +00:00
acquireMethod: ' Subscriber ' ,
category: ' behavior '
}
analytics ? . track ( ' open mystery item ' , analyticsData )
2015-07-23 17:05:13 +00:00
( user . _tmp ? = { } ) . drop = item if typeof window != ' undefined '
2014-03-09 04:12:55 +00:00
cb ? null , user . items . gear . owned
2014-01-07 17:36:24 +00:00
# ------
# Score
# ------
score: (req, cb) ->
{ id , direction } = req . params # up or down
task = user . tasks [ id ]
options = req . query or { } ; _ . defaults ( options , { times : 1 , cron : false } )
# This is for setting one-time temporary flags, such as streakBonus or itemDropped. Useful for notifying
# the API consumer, then cleared afterwards
user._tmp = { }
# TODO do we need this fail-safe casting anymore? Are we safe now we're off Derby?
stats = { gp: + user . stats . gp , hp: + user . stats . hp , exp: + user . stats . exp }
task.value = + task . value ; task.streak = ~ ~ task . streak ; task . priority ? = 1
# If they're trying to purhcase a too-expensive reward, don't allow them to do that.
if task . value > stats . gp and task . type is ' reward '
2014-04-07 17:04:59 +00:00
return cb ? { code : 401 , message : i18n . t ( ' messageNotEnoughGold ' , req . language ) }
2014-01-07 17:36:24 +00:00
delta = 0
calculateDelta = ->
2014-10-01 23:13:51 +00:00
# Calculates the next task.value based on direction
# Uses a capped inverse log y=.95^x, y>= -5
# Min/max on task redness
currVal =
if task . value < - 47.27 then - 47.27
else if task . value > 21.27 then 21.27
else task . value
nextDelta = Math . pow ( 0.9747 , currVal ) * ( if direction is ' down ' then - 1 else 1 )
# Checklists
if task . checklist ? . length > 0
# If the Daily, only dock them them a portion based on their checklist completion
if direction is ' down ' and task . type is ' daily ' and options . cron
nextDelta *= ( 1 - _ . reduce ( task . checklist , ( (m,i)-> m + ( if i . completed then 1 else 0 ) ) , 0 ) / task . checklist . length )
# If To-Do, point-match the TD per checklist item completed
if task . type is ' todo '
nextDelta *= ( 1 + _ . reduce ( task . checklist , ( (m,i)-> m + ( if i . completed then 1 else 0 ) ) , 0 ) )
nextDelta
calculateReverseDelta = ->
# Approximates the reverse delta for the task value
# This is meant to return the task value to its original value when unchecking a task.
# First, calculate the the value using the normal way for our first guess although
# it will be a bit off
currVal =
if task . value < - 47.27 then - 47.27
else if task . value > 21.27 then 21.27
else task . value
testVal = currVal + Math . pow ( 0.9747 , currVal ) * ( if direction is ' down ' then - 1 else 1 )
# Now keep moving closer to the original value until we get "close enough"
2014-10-06 19:33:43 +00:00
closeEnough = 0.00001
2014-10-01 23:13:51 +00:00
while true
# Check how close we are to the original value by computing the delta off our guess
# and looking at the difference between that and our current value.
calc = ( testVal ) + Math . pow ( 0.9747 , testVal )
diff = currVal - calc
if Math . abs ( diff ) < closeEnough
break
if diff > 0
testVal -= diff
else
testVal += diff
2014-10-06 19:33:43 +00:00
2014-10-01 23:13:51 +00:00
# When we get close enough, return the difference between our approximated value
# and the current value. This will be the delta calculated from the original value
# before the task was checked.
2014-10-06 19:33:43 +00:00
nextDelta = testVal - currVal
2014-10-01 23:13:51 +00:00
2014-10-06 19:33:43 +00:00
# Checklists
if task . checklist ? . length > 0
# If To-Do, point-match the TD per checklist item completed
if task . type is ' todo '
nextDelta *= ( 1 + _ . reduce ( task . checklist , ( (m,i)-> m + ( if i . completed then 1 else 0 ) ) , 0 ) )
nextDelta
changeTaskValue = ->
2014-01-07 17:36:24 +00:00
# If multiple days have passed, multiply times days missed
_ . times options . times , ->
# Each iteration calculate the nextDelta, which is then accumulated in the total delta.
2014-10-06 19:33:43 +00:00
nextDelta = if not options . cron and direction is ' down ' then calculateReverseDelta ( ) else calculateDelta ( )
2014-01-07 17:36:24 +00:00
unless task . type is ' reward '
2014-01-19 15:36:23 +00:00
if ( user . preferences . automaticAllocation is true and user . preferences . allocationMode is ' taskbased ' and ! ( task . type is ' todo ' and direction is ' down ' ) ) then user . stats . training [ task . attribute ] += nextDelta
2014-12-12 01:50:46 +00:00
if direction is ' up ' # Make progress on quest based on STR
2015-07-18 15:52:50 +00:00
user.party.quest.progress.up = user . party . quest . progress . up || 0
2014-02-28 03:21:04 +00:00
user . party . quest . progress . up += ( nextDelta * ( 1 + ( user . _statsComputed . str / 200 ) ) ) if task . type in [ ' daily ' , ' todo ' ]
2014-12-12 01:50:46 +00:00
user . party . quest . progress . up += ( nextDelta * ( 0.5 + ( user . _statsComputed . str / 400 ) ) ) if task . type is ' habit '
2014-02-28 03:21:04 +00:00
task . value += nextDelta
2014-01-07 17:36:24 +00:00
delta += nextDelta
addPoints = ->
# ===== CRITICAL HITS =====
2014-02-15 22:21:32 +00:00
# allow critical hit only when checking off a task, not when unchecking it:
_crit = ( if delta > 0 then user . fns . crit ( ) else 1 )
2014-01-07 17:36:24 +00:00
# if there was a crit, alert the user via notification
user._tmp.crit = _crit if _crit > 1
# Exp Modifier
# ===== Intelligence =====
# TODO Increases Experience gain by .2% per point.
intBonus = 1 + ( user . _statsComputed . int * . 025 )
stats . exp += Math . round ( delta * intBonus * task . priority * _crit * 6 )
# GP modifier
# ===== PERCEPTION =====
# TODO Increases Gold gained from tasks by .3% per point.
perBonus = ( 1 + user . _statsComputed . per * . 02 )
gpMod = ( delta * task . priority * _crit * perBonus )
stats . gp +=
if task . streak
2014-10-06 19:33:43 +00:00
currStreak = if direction is ' down ' then task . streak - 1 else task . streak
streakBonus = currStreak / 100 + 1 # eg, 1-day streak is 1.01, 2-day is 1.02, etc
2014-01-07 17:36:24 +00:00
afterStreak = gpMod * streakBonus
2014-10-06 19:33:43 +00:00
if currStreak > 0
user._tmp.streakBonus = afterStreak - gpMod if ( gpMod > 0 ) # keep this on-hand for later, so we can notify streak-bonus
2014-01-07 17:36:24 +00:00
afterStreak
else gpMod
# HP modifier
subtractPoints = ->
# ===== CONSTITUTION =====
# TODO Decreases HP loss from bad habits / missed dailies by 0.5% per point.
conBonus = 1 - ( user . _statsComputed . con / 250 )
conBonus = 0.1 if conBonus < . 1
hpMod = delta * conBonus * task . priority * 2 # constant 2 multiplier for better results
stats . hp += Math . round ( hpMod * 10 ) / 10 # round to 1dp
2014-12-11 03:45:51 +00:00
gainMP = (delta) ->
delta *= user . _tmp . crit or 1
user . stats . mp += delta
user.stats.mp = user . _statsComputed . maxMP if user . stats . mp >= user . _statsComputed . maxMP
user.stats.mp = 0 if user . stats . mp < 0
2015-01-26 04:53:13 +00:00
# ===== starting to actually do stuff, most of above was definitions =====
2014-01-07 17:36:24 +00:00
switch task . type
when ' habit '
2014-10-01 23:13:51 +00:00
changeTaskValue ( )
2014-01-07 17:36:24 +00:00
# Add habit value to habit-history (if different)
if ( delta > 0 ) then addPoints ( ) else subtractPoints ( )
2015-05-10 08:17:17 +00:00
gainMP ( _ . max ( [ 0.25 , ( . 0025 * user . _statsComputed . maxMP ) ] ) * if direction is ' down ' then - 1 else 1 )
2014-01-07 17:36:24 +00:00
# History
th = ( task . history ? = [ ] )
if th [ th . length - 1 ] and moment ( th [ th . length - 1 ] . date ) . isSame ( new Date , ' day ' )
th [ th . length - 1 ] . value = task . value
2013-12-11 16:30:07 +00:00
else
2014-01-07 17:36:24 +00:00
th . push { date: + new Date , value: task . value }
user . markModified ? " habits. #{ _ . findIndex ( user . habits , { id : task . id } )}.history "
2013-12-11 16:30:07 +00:00
2014-01-07 17:36:24 +00:00
when ' daily '
if options . cron
2014-10-01 23:13:51 +00:00
changeTaskValue ( )
2014-01-07 17:36:24 +00:00
subtractPoints ( )
task.streak = 0 unless user . stats . buffs . streaks
else
2014-10-06 19:33:43 +00:00
changeTaskValue ( )
if direction is ' down '
delta = calculateDelta ( ) # recalculate delta for unchecking so the gp and exp come out correctly
2014-01-07 17:36:24 +00:00
addPoints ( ) # obviously for delta>0, but also a trick to undo accidental checkboxes
2015-05-10 08:17:17 +00:00
gainMP ( _ . max ( [ 1 , ( . 01 * user . _statsComputed . maxMP ) ] ) * if direction is ' down ' then - 1 else 1 )
2014-01-07 17:36:24 +00:00
if direction is ' up '
task.streak = if task . streak then task . streak + 1 else 1
# Give a streak achievement when the streak is a multiple of 21
if ( task . streak % 21 ) is 0
user.achievements.streak = if user . achievements . streak then user . achievements . streak + 1 else 1
else
# Remove a streak achievement if streak was a multiple of 21 and the daily was undone
if ( task . streak % 21 ) is 0
user.achievements.streak = if user . achievements . streak then user . achievements . streak - 1 else 0
task.streak = if task . streak then task . streak - 1 else 0
when ' todo '
if options . cron
2014-10-01 23:13:51 +00:00
changeTaskValue ( )
2014-01-07 17:36:24 +00:00
#don't touch stats on cron
else
2014-01-20 01:09:29 +00:00
task.dateCompleted = if direction is ' up ' then new Date else undefined
2014-10-06 19:33:43 +00:00
changeTaskValue ( )
if direction is ' down '
delta = calculateDelta ( ) # recalculate delta for unchecking so the gp and exp come out correctly
2014-01-07 17:36:24 +00:00
addPoints ( ) # obviously for delta>0, but also a trick to undo accidental checkboxes
2014-01-09 19:16:39 +00:00
# MP++ per checklist item in ToDo, bonus per CLI
2014-02-01 16:26:26 +00:00
multiplier = _ . max ( [ ( _ . reduce ( task . checklist , ( (m,i)-> m + ( if i . completed then 1 else 0 ) ) , 1 ) ) , 1 ] )
2014-12-11 03:21:09 +00:00
gainMP ( _ . max ( [ ( multiplier ) , ( . 01 * user . _statsComputed . maxMP * multiplier ) ] ) * if direction is ' down ' then - 1 else 1 )
2014-01-07 17:36:24 +00:00
when ' reward '
# Don't adjust values for rewards
2014-10-01 23:13:51 +00:00
changeTaskValue ( )
2014-01-07 17:36:24 +00:00
# purchase item
stats . gp -= Math . abs ( task . value )
num = parseFloat ( task . value ) . toFixed ( 2 )
# if too expensive, reduce health & zero gp
if stats . gp < 0
# hp - gp difference
stats . hp += stats . gp
stats.gp = 0
2014-04-07 17:04:59 +00:00
user . fns . updateStats stats , req
2014-01-07 17:36:24 +00:00
# Drop system (don't run on the client, as it would only be discarded since ops are sent to the API, not the results)
if typeof window is ' undefined '
2014-04-07 17:04:59 +00:00
user . fns . randomDrop ( { task , delta } , req ) if direction is ' up '
2014-01-07 17:36:24 +00:00
cb ? null , user
return delta
2013-12-11 16:30:07 +00:00
2013-12-13 22:11:04 +00:00
# ----------------------------------------------------------------------
# user.fns helpers
# ----------------------------------------------------------------------
2013-12-11 16:30:07 +00:00
user.fns =
2013-12-13 22:55:47 +00:00
2013-12-11 16:30:07 +00:00
getItem: (type) ->
item = content . gear . flat [ user . items . gear . equipped [ type ] ]
return content . gear . flat [ " #{ type } _base_0 " ] unless item
item
2014-04-07 17:04:59 +00:00
handleTwoHanded: (item, type='equipped', req) ->
2013-12-15 18:26:13 +00:00
# If they're buying a shield and wearing a staff, dequip the staff
if item . type is " shield " and ( weapon = content . gear . flat [ user . items . gear [ type ] . weapon ] ) ? . twoHanded
user . items . gear [ type ] . weapon = ' weapon_base_0 '
2014-04-15 16:15:11 +00:00
message = i18n . t ( ' messageTwoHandled ' , { gearText: weapon . text ( req . language ) } , req . language )
2013-12-15 18:26:13 +00:00
# If they're buying a staff and wearing a shield, dequip the shield
if item . twoHanded
user . items . gear [ type ] . shield = " shield_base_0 "
2014-04-15 16:15:11 +00:00
message = i18n . t ( ' messageTwoHandled ' , { gearText: item . text ( req . language ) } , req . language )
2013-12-15 18:26:13 +00:00
message
2013-12-14 19:48:43 +00:00
# ##
Because the same op needs to be performed on the client and the server ( critical hits , item drops , etc ) ,
we need things to be " random " , but technically predictable so that they don ' t go out-of-sync
# ##
predictableRandom: (seed) ->
# Default seed is all user stats combined. Fairly safe, meh - pass in a good seed for situations where that doesn't work
seed = _ . reduce ( user . stats , ( (m,v)-> if _ . isNumber ( v ) then m + v else m ) , 0 ) if ! seed or seed is Math . PI
x = Math . sin ( seed ++ ) * 10000
x - Math . floor ( x )
2013-12-21 17:56:05 +00:00
crit: (stat='str', chance=.03) ->
2014-03-02 18:40:33 +00:00
#console.log("Crit Chance:"+chance*(1+user._statsComputed[stat]/100))
2014-11-09 02:35:03 +00:00
s = user . _statsComputed [ stat ]
if user . fns . predictableRandom ( ) <= chance * ( 1 + s / 100 )
1.5 + 4 * s / ( s + 200 )
2013-12-21 17:56:05 +00:00
else 1
2013-12-14 19:48:43 +00:00
# ##
Get a random property from an object
returns random property ( the value )
# ##
randomVal: (obj, options) ->
2015-08-05 13:18:03 +00:00
array = if options ? . key then _ . keys ( obj ) else _ . values ( obj )
rand = user . fns . predictableRandom ( options ? . seed )
array . sort ( )
array [ Math . floor ( rand * array . length ) ]
2013-12-14 19:48:43 +00:00
2013-12-11 16:30:07 +00:00
# ##
This allows you to set object properties by dot - path . Eg , you can run pathSet ( ' stats.hp ' , 50 , user ) which is the same as
user.stats.hp = 50 . This is useful because in our habitrpg - shared functions we ' re returning changesets as {path:value},
so that different consumers can implement setters their own way . Derby needs model . set ( path , value ) for example , where
Angular sets object properties directly - in which case , this function will be used .
# ##
2014-07-17 18:55:45 +00:00
dotSet: (path, val)-> api . dotSet user , path , val
dotGet: (path)-> api . dotGet user , path
2013-12-13 22:11:04 +00:00
# ----------------------------------------------------------------------
# Scoring
# ----------------------------------------------------------------------
2014-04-07 17:04:59 +00:00
randomDrop: (modifiers, req) ->
2013-12-24 16:32:15 +00:00
{ task } = modifiers
2013-12-13 22:11:04 +00:00
2013-12-24 17:24:45 +00:00
# % chance of getting a drop
2014-02-16 17:12:04 +00:00
2014-03-02 18:40:33 +00:00
chance = _ . min ( [ Math . abs ( task . value - 21.27 ) , 37.5 ] ) / 150 + . 02 # Base drop chance is a percentage based on task value. Typical fresh task: 15%. Very ripe task: 25%. Very blue task: 2%.
2014-02-16 17:12:04 +00:00
chance *=
2014-02-17 05:52:26 +00:00
task . priority * # Task priority: +50% for Medium, +100% for Hard
( 1 + ( task . streak / 100 or 0 ) ) * # Streak bonus: +1% per streak
2014-03-02 00:56:09 +00:00
( 1 + ( user . _statsComputed . per / 100 ) ) * # PERception: +1% per point
2014-05-19 03:42:31 +00:00
( 1 + ( user . contributor . level / 40 or 0 ) ) * # Contrib levels: +2.5% per level
2014-03-02 18:40:33 +00:00
( 1 + ( user . achievements . rebirths / 20 or 0 ) ) * # Rebirths: +5% per achievement
2014-03-02 00:56:09 +00:00
( 1 + ( user . achievements . streak / 200 or 0 ) ) * # Streak achievements: +0.5% per achievement
2014-02-16 17:12:04 +00:00
( user . _tmp . crit or 1 ) * # Use the crit multiplier if we got one
2014-03-02 18:40:33 +00:00
( 1 + . 5 * ( _ . reduce ( task . checklist , ( (m,i)-> m + ( if i . completed then 1 else 0 ) ) , 0 ) or 0 ) ) # +50% per checklist item complete. TODO: make this into X individual drop chances instead
chance = api . diminishingReturns ( chance , 0.75 )
2014-02-16 17:12:04 +00:00
#console.log("Drop chance: " + chance)
2013-12-24 16:32:15 +00:00
2013-12-24 17:24:45 +00:00
quest = content . quests [ user . party . quest ? . key ]
2014-03-22 03:56:28 +00:00
if quest ? . collect and user . fns . predictableRandom ( user . stats . gp ) < chance
2013-12-24 17:24:45 +00:00
dropK = user . fns . randomVal quest . collect , { key : true }
2013-12-25 04:57:46 +00:00
user . party . quest . progress . collect [ dropK ] ++
user . markModified ? ' party.quest.progress '
2014-01-30 22:20:44 +00:00
#console.log {progress:user.party.quest.progress}
2013-12-24 17:24:45 +00:00
2014-03-09 04:12:55 +00:00
dropMultiplier = if user . purchased ? . plan ? . customerId then 2 else 1
return if ( api . daysSince ( user . items . lastDrop . date , user . preferences ) is 0 ) and ( user . items . lastDrop . count >= dropMultiplier * ( 5 + Math . floor ( user . _statsComputed . per / 25 ) + ( user . contributor . level or 0 ) ) )
2013-12-24 16:32:15 +00:00
if user . flags ? . dropsEnabled and user . fns . predictableRandom ( user . stats . exp ) < chance
2013-12-13 22:11:04 +00:00
# current breakdown - 1% (adjustable) chance on drop
# If they got a drop: 50% chance of egg, 50% Hatching Potion. If hatchingPotion, broken down further even further
2013-12-18 06:24:38 +00:00
rarity = user . fns . predictableRandom ( user . stats . gp )
2013-12-13 22:11:04 +00:00
# Food: 40% chance
if rarity > . 6
2014-09-16 22:44:30 +00:00
drop = user . fns . randomVal _ . where ( content . food , { canDrop : true } )
2013-12-23 05:56:37 +00:00
user . items . food [ drop . key ] ? = 0
user . items . food [ drop . key ] += 1
2013-12-13 22:11:04 +00:00
drop.type = ' Food '
2014-04-15 16:15:11 +00:00
drop.dialog = i18n . t ( ' messageDropFood ' , { dropArticle: drop . article , dropText: drop . text ( req . language ) , dropNotes: drop . notes ( req . language ) } , req . language )
2013-12-13 22:11:04 +00:00
# Eggs: 30% chance
else if rarity > . 3
2014-01-21 22:49:56 +00:00
drop = user . fns . randomVal _ . where ( content . eggs , { canBuy : true } )
2013-12-23 05:56:37 +00:00
user . items . eggs [ drop . key ] ? = 0
user . items . eggs [ drop . key ] ++
2013-12-13 22:11:04 +00:00
drop.type = ' Egg '
2014-04-15 16:15:11 +00:00
drop.dialog = i18n . t ( ' messageDropEgg ' , { dropText: drop . text ( req . language ) , dropNotes: drop . notes ( req . language ) } , req . language )
2013-12-13 22:11:04 +00:00
# Hatching Potion, 30% chance - break down by rarity.
else
acceptableDrops =
# Very Rare: 10% (of 30%)
2014-03-07 04:34:43 +00:00
if rarity < . 02 then [ ' Golden ' ]
2013-12-13 22:11:04 +00:00
# Rare: 20% (of 30%)
else if rarity < . 09 then [ ' Zombie ' , ' CottonCandyPink ' , ' CottonCandyBlue ' ]
# Uncommon: 30% (of 30%)
else if rarity < . 18 then [ ' Red ' , ' Shade ' , ' Skeleton ' ]
# Common: 40% (of 30%)
else [ ' Base ' , ' White ' , ' Desert ' ]
# No Rarity (@see https://github.com/HabitRPG/habitrpg/issues/1048, we may want to remove rareness when we add mounts)
#drop = helpers.randomVal hatchingPotions
2013-12-14 19:48:43 +00:00
drop = user . fns . randomVal _ . pick ( content . hatchingPotions , ( (v,k) -> k in acceptableDrops ) )
2013-12-13 22:11:04 +00:00
2013-12-23 05:56:37 +00:00
user . items . hatchingPotions [ drop . key ] ? = 0
user . items . hatchingPotions [ drop . key ] ++
2013-12-13 22:11:04 +00:00
drop.type = ' HatchingPotion '
2014-04-15 16:15:11 +00:00
drop.dialog = i18n . t ( ' messageDropPotion ' , { dropText: drop . text ( req . language ) , dropNotes: drop . notes ( req . language ) } , req . language )
2013-12-13 22:11:04 +00:00
# if they've dropped something, we want the consuming client to know so they can notify the user. See how the Derby
# app handles it for example. Would this be better handled as an emit() ?
user._tmp.drop = drop
user.items.lastDrop.date = + new Date
user . items . lastDrop . count ++
# ##
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
# ##
2013-12-30 17:39:53 +00:00
autoAllocate: ->
2014-01-04 23:35:10 +00:00
user . stats [ ( ->
switch user . preferences . allocationMode
when " flat "
2014-01-05 01:10:59 +00:00
# Favor in order (right-to-left): INT, PER, STR, CON
stats = _ . pick user . stats , $w ' con str per int '
_ . invert ( stats ) [ _ . min stats ]
2014-01-04 23:35:10 +00:00
when " classbased "
# Attributes get 3:2:1:1 per 7 levels.
2015-03-13 12:48:14 +00:00
lvlDiv7 = user . stats . lvl / 7
ideal = [ ( lvlDiv7 * 3 ) , ( lvlDiv7 * 2 ) , lvlDiv7 , lvlDiv7 ]
2014-01-04 23:35:10 +00:00
# Primary, secondary etc. attributes aren't explicitly defined, so hardcode them. In order as above
preference = switch user . stats . class
when " wizard " then [ " int " , " per " , " con " , " str " ]
when " rogue " then [ " per " , " str " , " int " , " con " ]
when " healer " then [ " con " , " int " , " str " , " per " ]
else [ " str " , " con " , " per " , " int " ]
# Get the difference between the ideal attribute spread according to level, and the user's current spread.
diff = [ ( user . stats [ preference [ 0 ] ] - ideal [ 0 ] ) , ( user . stats [ preference [ 1 ] ] - ideal [ 1 ] ) , ( user . stats [ preference [ 2 ] ] - ideal [ 2 ] ) , ( user . stats [ preference [ 3 ] ] - ideal [ 3 ] ) ]
suggested = _ . findIndex ( diff , ( (val) -> if val is _ . min ( diff ) then true ) ) # Returns the index of the first attribute that's furthest behind the ideal
return if ~ suggested then preference [ suggested ] else " str " # If _.findIndex failed, we'd get a -1...
when " taskbased "
2014-01-05 01:10:59 +00:00
suggested = _ . invert ( user . stats . training ) [ _ . max user . stats . training ] # Returns the stat that's been trained up the most this level
_ . merge user . stats . training , { str : 0 , int : 0 , con : 0 , per : 0 } # Reset training for this level.
2014-01-04 23:35:10 +00:00
return suggested or " str " # Failed _.findkey gives undefined
else " str " # if all else fails, dump into STR
) ( ) ] ++
2013-12-30 17:39:53 +00:00
2015-07-10 03:18:37 +00:00
updateStats: (stats, req, analytics) ->
2014-09-10 04:06:01 +00:00
# Game Over (death)
2013-12-13 22:55:47 +00:00
return user . stats . hp = 0 if stats . hp <= 0
user.stats.hp = stats . hp
user.stats.gp = if stats . gp >= 0 then stats . gp else 0
tnl = api . tnl ( user . stats . lvl )
2015-02-16 21:02:04 +00:00
2014-05-11 14:53:31 +00:00
# level up & carry-over exp
if stats . exp >= tnl
#silent = true # push through the negative xp silently
user.stats.exp = stats . exp # push normal + notification
while stats . exp >= tnl # keep levelling up
stats . exp -= tnl
user . stats . lvl ++
tnl = api . tnl ( user . stats . lvl )
2015-03-13 12:48:14 +00:00
user.stats.hp = 50
2015-03-15 02:32:27 +00:00
continue if user . stats . lvl > api . maxLevel
2015-03-13 12:48:14 +00:00
2014-05-11 14:53:31 +00:00
# Auto-allocate a point, or give them a new manual point
if user . preferences . automaticAllocation
user . fns . autoAllocate ( )
else
# add new allocatable points. We could do user.stats.points++, but this does a fail-safe just in case
2015-07-18 15:52:50 +00:00
user.stats.points = user . stats . lvl - ( user . stats . con + user . stats . str + user . stats . per + user . stats . int )
2015-04-04 23:03:39 +00:00
if user . stats . points < 0
user.stats.points = 0
# This happens after dropping level with Fix Character Values and perhaps from other causes.
# TODO: Subtract points from attributes in the same manner as on death.
2013-12-13 22:55:47 +00:00
user.stats.exp = stats . exp
# Set flags when they unlock features
user . flags ? = { }
2014-10-09 23:23:54 +00:00
if ! user . flags . customizationsNotification and ( user . stats . exp > 5 or user . stats . lvl > 1 )
2013-12-13 22:55:47 +00:00
user.flags.customizationsNotification = true
2014-10-09 23:23:54 +00:00
if ! user . flags . itemsEnabled and ( user . stats . exp > 10 or user . stats . lvl > 1 )
2013-12-13 22:55:47 +00:00
user.flags.itemsEnabled = true
2015-07-28 18:28:45 +00:00
if ! user . flags . dropsEnabled and user . stats . lvl >= 3
2013-12-13 22:55:47 +00:00
user.flags.dropsEnabled = true
2014-01-07 14:04:50 +00:00
if user . items . eggs [ " Wolf " ] > 0 then user . items . eggs [ " Wolf " ] ++ else user . items . eggs [ " Wolf " ] = 1
2013-12-21 22:05:04 +00:00
if ! user . flags . classSelected and user . stats . lvl >= 10
2013-12-13 22:55:47 +00:00
user . flags . classSelected
2014-08-12 23:18:08 +00:00
# Level Drops
2014-11-12 04:04:21 +00:00
_ . each { vice1 : 30 , atom1 : 15 , moonstone1 : 60 , goldenknight1: 40 } , (lvl,k)->
2014-08-12 23:18:08 +00:00
if ! user . flags . levelDrops ? [ k ] and user . stats . lvl >= lvl
user . items . quests [ k ] ? = 0
user . items . quests [ k ] ++
( user . flags . levelDrops ? = { } ) [ k ] = true
user . markModified ? ' flags.levelDrops '
2015-07-10 03:18:37 +00:00
analyticsData = {
uuid: user . _id ,
2015-07-12 15:05:00 +00:00
itemKey: k ,
acquireMethod: ' Level Drop ' ,
2015-07-10 03:18:37 +00:00
category: ' behavior '
}
analytics ? . track ( ' acquire item ' , analyticsData )
2015-07-16 21:22:49 +00:00
user._tmp.drop = { type: ' Quest ' , key: k }
2015-06-12 21:17:50 +00:00
if ! user . flags . rebirthEnabled and ( user . stats . lvl >= 50 or user . achievements . beastMaster )
2013-12-24 15:09:48 +00:00
user.flags.rebirthEnabled = true
2013-12-13 22:11:04 +00:00
# ##
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cron
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# ##
# ##
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 .
{ user }
# ##
cron: (options={}) ->
2013-12-20 19:42:31 +00:00
now = + options . now || + new Date
2013-12-13 22:11:04 +00:00
# They went to a different timezone
# FIXME:
# (1) This exit-early code isn't taking timezone into consideration!!
# (2) Won't switching timezones be handled automatically client-side anyway? (aka, can we just remove this code?)
# (3) And if not, is this the correct way to handle switching timezones
# if moment(user.lastCron).isAfter(now)
# user.lastCron = now
# return
daysMissed = api . daysSince user . lastCron , _ . defaults ( { now } , user . preferences )
return unless daysMissed > 0
user.auth.timestamps.loggedin = new Date ( )
user.lastCron = now
# Reset the lastDrop count to zero
if user . items . lastDrop . count > 0
user.items.lastDrop.count = 0
2014-01-15 23:26:37 +00:00
# "Perfect Day" achievement for perfect-days
2014-01-18 04:41:20 +00:00
perfect = true
clearBuffs = { str : 0 , int : 0 , per : 0 , con : 0 , stealth : 0 , streaks : false }
2014-01-15 23:26:37 +00:00
2014-12-04 03:16:33 +00:00
# end-of-month perks for subscribers
plan = user . purchased ? . plan
if plan ? . customerId
if moment ( plan . dateUpdated ) . format ( ' MMYYYY ' ) != moment ( ) . format ( ' MMYYYY ' )
plan.gemsBought = 0 # reset gem-cap
plan.dateUpdated = new Date ( )
# 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 ++ / --?
2014-12-06 19:47:51 +00:00
_ . defaults plan . consecutive , { count : 0 , offset : 0 , trinkets : 0 , gemCapExtra : 0 } #fixme see https://github.com/HabitRPG/habitrpg/issues/4317
2014-12-04 03:16:33 +00:00
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
plan . consecutive . gemCapExtra = 25 if 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 '
2015-06-14 16:08:58 +00:00
# User is resting at the inn.
2015-05-13 13:44:08 +00:00
# On cron, buffs are cleared and all dailies are reset without performing damage
2013-12-16 19:54:46 +00:00
if user . preferences . sleep is true
2014-01-15 23:26:37 +00:00
user.stats.buffs = clearBuffs
2015-05-13 13:44:08 +00:00
user . dailys . forEach (daily) ->
{ completed , repeat } = daily
thatDay = moment ( now ) . subtract ( { days: 1 } )
2015-05-16 03:21:43 +00:00
if api . shouldDo ( thatDay . toDate ( ) , daily , user . preferences ) || completed
2015-05-13 13:44:08 +00:00
_ . each daily . checklist , ( (box)-> box . completed = false ; true )
daily.completed = false
2013-12-16 19:54:46 +00:00
return
2013-12-13 22:11:04 +00:00
2015-07-28 04:39:47 +00:00
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.
2013-12-23 05:56:37 +00:00
# Tally each task
2013-12-13 22:11:04 +00:00
todoTally = 0
2015-07-26 01:21:23 +00:00
user . todos . forEach (task) -> # make uncompleted todos redder
return unless task
{ id , completed } = task
2015-07-28 04:39:47 +00:00
delta = user . ops . score ( { params : { id : task . id , direction : ' down ' } , query : { times : ( multiDaysCountAsOneDay ? 1 : daysMissed ) , cron : true } } )
2015-07-26 01:21:23 +00:00
absVal = if ( completed ) then Math . abs ( task . value ) else task . value
todoTally += absVal
2015-01-19 19:43:16 +00:00
dailyChecked = 0 # how many dailies were checked?
dailyDueUnchecked = 0 # how many dailies were due but not checked?
2013-12-25 04:57:46 +00:00
user . party . quest . progress . down ? = 0
2015-07-26 01:21:23 +00:00
user . dailys . forEach (task) ->
2013-12-13 22:11:04 +00:00
return unless task
2015-07-26 01:21:23 +00:00
{ id , completed } = task
2014-01-19 01:53:38 +00:00
2015-08-09 04:09:10 +00:00
# Deduct points for missed Daily tasks
2015-02-23 02:22:49 +00:00
EvadeTask = 0
scheduleMisses = daysMissed
2015-01-24 17:07:39 +00:00
if completed
2015-07-26 01:21:23 +00:00
dailyChecked += 1
2015-01-24 17:07:39 +00:00
else
2015-07-26 01:21:23 +00:00
# dailys repeat, so need to calculate how many they've missed according to their own schedule
scheduleMisses = 0
2015-08-03 18:30:42 +00:00
for n in [ 0 . . . daysMissed ]
thatDay = moment ( now ) . subtract ( { days: n + 1 } )
if api . shouldDo ( thatDay . toDate ( ) , task , user . preferences )
scheduleMisses ++
if user . stats . buffs . stealth
user . stats . buffs . stealth - -
EvadeTask ++
if multiDaysCountAsOneDay
break
2013-12-13 22:11:04 +00:00
2015-07-26 01:21:23 +00:00
if scheduleMisses > EvadeTask
perfect = false
if task . checklist ? . length > 0 # Partially completed checklists dock fewer mana points
fractionChecked = _ . reduce ( task . checklist , ( (m,i)-> m + ( if i . completed then 1 else 0 ) ) , 0 ) / task . checklist . length
dailyDueUnchecked += ( 1 - fractionChecked )
dailyChecked += fractionChecked
else
dailyDueUnchecked += 1
2015-07-28 04:39:47 +00:00
delta = user . ops . score ( { params : { id : task . id , direction : ' down ' } , query : { times : ( multiDaysCountAsOneDay ? 1 : ( scheduleMisses - EvadeTask ) ) , cron : true } } )
2015-07-26 01:21:23 +00:00
# Apply damage from a boss, less damage for Trivial priority (difficulty)
user . party . quest . progress . down += delta * ( if task . priority < 1 then task . priority else 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: + new Date , value: task . value } )
task.completed = false
if completed || ( scheduleMisses > 0 )
_ . each task . checklist , ( (i)-> i . completed = false ; true ) # this should not happen for grey tasks unless they are completed
2013-12-13 22:11:04 +00:00
user . habits . forEach (task) -> # slowly reset 'onlies' value to 0
if task . up is false or task . down is false
if Math . abs ( task . value ) < 0.1
task.value = 0
else
task.value = task . value / 2
# Finished tallying
( ( user . history ? = { } ) . todos ? = [ ] ) . push { date: now , value: todoTally }
# tally experience
expTally = user . stats . exp
lvl = 0 #iterator
while lvl < ( user . stats . lvl - 1 )
lvl ++
expTally += api . tnl ( lvl )
( user . history . exp ? = [ ] ) . push { date: now , value: expTally }
2014-03-09 04:12:55 +00:00
# premium subscribers can keep their full history.
# TODO figure out something performance-wise
unless user . purchased ? . plan ? . customerId
user . fns . preenUserHistory ( )
user . markModified ? ' history '
user . markModified ? ' dailys ' # covers dailys.*.history
2014-01-18 04:41:20 +00:00
user.stats.buffs =
if perfect
user . achievements . perfect ? = 0
user . achievements . perfect ++
2015-03-13 12:48:14 +00:00
lvlDiv2 = Math . ceil ( api . capByLevel ( user . stats . lvl ) / 2 )
2014-01-18 04:41:20 +00:00
{ str : lvlDiv2 , int : lvlDiv2 , per : lvlDiv2 , con : lvlDiv2 , stealth : 0 , streaks : false }
else clearBuffs
2013-12-20 19:42:31 +00:00
2014-04-23 02:05:58 +00:00
# Add 10 MP, or 10% of max MP if that'd be more. Perform this after Perfect Day for maximum benefit
2015-01-19 19:43:16 +00:00
# Adjust for fraction of dailies completed
2015-01-22 17:16:06 +00:00
dailyChecked = 1 if dailyDueUnchecked is 0 and dailyChecked is 0
2015-01-19 19:43:16 +00:00
user . stats . mp += _ . max ( [ 10 , . 1 * user . _statsComputed . maxMP ] ) * dailyChecked / ( dailyDueUnchecked + dailyChecked )
2014-04-23 02:05:58 +00:00
user.stats.mp = user . _statsComputed . maxMP if user . stats . mp > user . _statsComputed . maxMP
2015-03-26 17:15:23 +00:00
# Analytics
user . flags . cronCount ? = 0
user . flags . cronCount ++
2015-07-08 13:08:45 +00:00
analyticsData = {
2015-07-10 03:18:37 +00:00
category: ' behavior ' ,
2015-07-19 13:41:45 +00:00
gaLabel: ' Cron Count ' ,
gaValue: user . flags . cronCount ,
2015-07-08 13:08:45 +00:00
uuid: user . _id ,
user: user ,
resting: user . preferences . sleep ,
cronCount: user . flags . cronCount
}
options . analytics ? . track ( ' Cron ' , analyticsData )
2015-03-26 17:15:23 +00:00
2013-12-25 04:57:46 +00:00
# After all is said and done, progress up user's effect on quest, return those values & reset the user's
progress = user . party . quest . progress ; _progress = _ . cloneDeep progress
_ . merge progress , { down : 0 , up : 0 }
progress.collect = _ . transform progress . collect , ( (m,v,k)-> m [ k ] = 0 )
_progress
2013-12-13 22:11:04 +00:00
# Registered users with some history
preenUserHistory: (minHistLen = 7) ->
_ . each user . habits . concat ( user . dailys ) , (task) ->
task.history = preenHistory ( task . history ) if task . history ? . length > minHistLen
true
_ . defaults user . history , { todos : [ ] , exp: [ ] }
user.history.exp = preenHistory ( user . history . exp ) if user . history . exp . length > minHistLen
user.history.todos = preenHistory ( user . history . todos ) if user . history . todos . length > minHistLen
2013-12-13 22:55:47 +00:00
#user.markModified? 'history'
#user.markModified? 'habits'
#user.markModified? 'dailys'
2013-12-13 22:11:04 +00:00
2014-01-24 06:02:51 +00:00
# ----------------------------------------------------------------------
# Achievements
# ----------------------------------------------------------------------
2015-05-26 21:06:04 +00:00
ultimateGear: ->
# on the server this is a Lodash transform, on the client its an object
owned = if window ? then user . items . gear . owned else user . items . gear . owned . toObject ( )
user . achievements . ultimateGearSets ? = { healer: false , wizard: false , rogue: false , warrior: false }
content . classes . forEach (klass) ->
2015-06-20 05:58:54 +00:00
if user . achievements . ultimateGearSets [ klass ] isnt true
2015-06-09 15:47:59 +00:00
user . achievements . ultimateGearSets [ klass ] = _ . reduce [ ' armor ' , ' shield ' , ' head ' , ' weapon ' ] , (soFarGood, type) ->
found = _ . find content . gear . tree [ type ] [ klass ] , { last : true }
soFarGood and ( ! found or owned [ found . key ] == true ) #!found only true when weapon is two-handed (mages)
, true # start with true, else `and` will fail right away
2015-05-26 21:06:04 +00:00
user . markModified ? ' achievements.ultimateGearSets '
if _ . contains ( user . achievements . ultimateGearSets , true ) and user . flags . armoireEnabled != true
user.flags.armoireEnabled = true
user . markModified ? ' flags '
2014-01-24 06:02:51 +00:00
2014-10-01 21:43:32 +00:00
nullify: ->
user.ops = null
user.fns = null
user = null
2014-01-24 06:02:51 +00:00
2013-12-13 22:11:04 +00:00
# ----------------------------------------------------------------------
# Virtual Attributes
# ----------------------------------------------------------------------
2013-12-11 16:30:07 +00:00
# Aggregate all intrinsic stats, buffs, weapon, & armor into computed stats
Object . defineProperty user , ' _statsComputed ' ,
get: ->
2014-01-07 17:36:24 +00:00
computed = _ . reduce [ ' per ' , ' con ' , ' str ' , ' int ' ] , (m,stat) =>
m [ stat ] = _ . reduce $w ( ' stats stats.buffs items.gear.equipped.weapon items.gear.equipped.armor items.gear.equipped.head items.gear.equipped.shield ' ) , (m2,path) =>
2013-12-13 02:28:29 +00:00
val = user . fns . dotGet ( path )
2013-12-11 16:30:07 +00:00
m2 +
2013-12-13 02:28:29 +00:00
if ~ path . indexOf ( ' items.gear ' )
# get the gear stat, and multiply it by 1.5 if it's class-gear
2014-01-07 17:36:24 +00:00
item = content . gear . flat [ val ]
2014-03-23 22:29:47 +00:00
( + item ? [ stat ] or 0 ) * ( if item ? . klass is user . stats . class || item ? . specialClass is user . stats . class then 1.5 else 1 )
2013-12-13 02:28:29 +00:00
else
+ val [ stat ] or 0
2014-01-07 17:36:24 +00:00
, 0
2015-03-13 12:48:14 +00:00
m [ stat ] += Math . floor ( api . capByLevel ( user . stats . lvl ) / 2 )
2013-12-13 22:55:47 +00:00
m
2014-01-07 17:36:24 +00:00
, { }
2013-12-16 18:09:03 +00:00
computed.maxMP = computed . int * 2 + 30
computed
2013-12-11 16:30:07 +00:00
Object . defineProperty user , ' tasks ' ,
get: ->
tasks = user . habits . concat ( user . dailys ) . concat ( user . todos ) . concat ( user . rewards )
_ . object ( _ . pluck ( tasks , " id " ) , tasks )