2013-05-19 17:52:13 +00:00
moment = require ' moment '
_ = require ' lodash '
2013-07-12 20:32:28 +00:00
items = require ( ' ./items.coffee ' )
2013-05-19 17:52:13 +00:00
2013-08-21 01:33:37 +00:00
# ##
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 ( 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 }
startOfWeek = (options={}) ->
o = sanitizeOptions ( options )
moment ( o . now ) . startOf ( ' week ' )
startOfDay = (options={}) ->
o = sanitizeOptions ( options )
moment ( o . now ) . startOf ( ' day ' ) . add ( ' h ' , options . dayStart )
2013-05-19 17:52:13 +00:00
dayMapping = { 0 : ' su ' , 1 : ' m ' , 2 : ' t ' , 3 : ' w ' , 4 : ' th ' , 5 : ' f ' , 6 : ' s ' }
# ##
2013-08-21 01:33:37 +00:00
Absolute diff from " yesterday " till now
2013-05-19 17:52:13 +00:00
# ##
2013-08-21 01:33:37 +00:00
daysSince = (yesterday, options = {}) ->
o = sanitizeOptions options
Math . abs startOfDay ( _ . defaults { now : yesterday } , o ) . diff ( o . now , ' days ' )
2013-05-19 17:52:13 +00:00
# ##
Should the user do this taks on this date , given the task ' s repeat options and user.preferences.dayStart?
# ##
2013-05-25 17:26:07 +00:00
shouldDo = (day, repeat, options={}) ->
2013-05-19 17:52:13 +00:00
return false unless repeat
2013-08-21 01:33:37 +00:00
o = sanitizeOptions options
selected = repeat [ dayMapping [ startOfDay ( _ . defaults { now : day } , o ) . day ( ) ] ]
return selected unless moment ( day ) . zone ( o . timezoneOffset ) . isSame ( o . now , ' d ' )
if options . dayStart <= o . now . hour ( ) # we're past the dayStart mark, is it due today?
2013-05-19 17:52:13 +00:00
return selected
else # we're not past dayStart mark, check if it was due "yesterday"
2013-08-21 01:33:37 +00:00
yesterday = moment ( o . now ) . subtract ( 1 , ' d ' ) . day ( ) # have to wrap o.now so as not to modify original
return repeat [ dayMapping [ yesterday ] ] # FIXME is this correct?? Do I need to do any timezone calcaulation here?
2013-05-19 17:52:13 +00:00
2013-05-24 10:31:32 +00:00
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
2013-05-19 17:52:13 +00:00
module.exports =
2013-05-24 10:31:32 +00:00
uuid: uuid
2013-08-25 18:53:45 +00:00
taskDefaults: (task, filters={}) ->
self = @
defaults =
id: self . uuid ( )
text: ' '
up: true
down: true
type: ' habit '
completed: false
repeat: { su : true , m : true , t : true , w : true , th : true , f : true , s : true }
notes: ' '
tags: _ . transform ( filters , (m,v,k) -> m [ k ] = v if v )
_ . defaults task , defaults
task . value ? = if task . type is ' reward ' then 10 else 0
task
2013-10-26 17:23:39 +00:00
# FIXME - should we remove this completely, since all defaults are accounted for in mongoose?
# Or should we keep it so mobile can create an offline new user in the future?
newUser: () ->
2013-05-24 10:31:32 +00:00
userSchema =
# _id / id handled by Racer
stats: { gp: 0 , exp: 0 , lvl: 1 , hp: 50 }
2013-05-28 16:32:31 +00:00
invitations: { party : null , guilds: [ ] }
2013-05-24 10:31:32 +00:00
items: { weapon: 0 , armor: 0 , head: 0 , shield: 0 }
preferences: { gender: ' m ' , skin: ' white ' , hair: ' blond ' , armorSet: ' v1 ' , dayStart : 0 , showHelm: true }
apiToken: uuid ( ) # set in newUserObject below
2013-08-24 23:07:42 +00:00
lastCron: + new Date #this will be replaced with `+new Date` on first run
2013-05-24 10:31:32 +00:00
balance: 0
flags:
partyEnabled: false
itemsEnabled: false
ads: ' show '
tags: [ ]
2013-10-26 17:23:39 +00:00
userSchema.habits = [ ]
userSchema.dailys = [ ]
userSchema.todos = [ ]
userSchema.rewards = [ ]
2013-05-24 10:31:32 +00:00
# deep clone, else further new users get duplicate objects
newUser = _ . cloneDeep userSchema
repeat = { m : true , t : true , w : true , th : true , f : true , s : true , su : true }
defaultTasks = [
{ type: ' habit ' , text: ' 1h Productive Work ' , notes: ' -- Habits: Constantly Track -- \n For some habits, it only makes sense to *gain* points (like this one). ' , value: 0 , up: true , down: false }
{ type: ' habit ' , text: ' Eat Junk Food ' , notes: ' For others, it only makes sense to *lose* points ' , value: 0 , up: false , down: true }
{ type: ' habit ' , text: ' Take The Stairs ' , notes: ' For the rest, both + and - make sense (stairs = gain, elevator = lose) ' , value: 0 , up: true , down: true }
{ type: ' daily ' , text: ' 1h Personal Project ' , notes: ' -- Dailies: Complete Once a Day -- \n At the end of each day, non-completed Dailies dock you points. ' , value: 0 , completed: false , repeat: repeat }
{ type: ' daily ' , text: ' Exercise ' , notes: " If you are doing well, they turn green and are less valuable (experience, gold) and less damaging (HP). This means you can ease up on them for a bit. " , value: 3 , completed: false , repeat: repeat }
{ type: ' daily ' , text: ' 45m Reading ' , notes: ' But if you are doing poorly, they turn red. The worse you do, the more valuable (exp, gold) and more damaging (HP) these goals become. This encourages you to focus on your shortcomings, the reds. ' , value: - 10 , completed: false , repeat: repeat }
{ type: ' todo ' , text: ' Call Mom ' , notes: " -- Todos: Complete Eventually -- \n Non-completed Todos won ' t hurt you, but they will become more valuable over time. This will encourage you to wrap up stale Todos. " , value: - 3 , completed: false }
{ type: ' reward ' , text: ' 1 Episode of Game of Thrones ' , notes: ' -- Rewards: Treat Yourself! -- \n As you complete goals, you earn gold to buy rewards. Buy them liberally - rewards are integral in forming good habits. ' , value: 20 }
{ type: ' reward ' , text: ' Cake ' , notes: ' But only buy if you have enough gold - you lose HP otherwise. ' , value: 10 }
]
defaultTags = [
{ name: ' morning ' }
{ name: ' afternoon ' }
{ name: ' evening ' }
]
for task in defaultTasks
guid = task.id = uuid ( )
2013-10-26 17:23:39 +00:00
newUser [ " #{ task . type } s " ] . push task
2013-05-24 10:31:32 +00:00
for tag in defaultTags
tag.id = uuid ( )
newUser . tags . push tag
newUser
2013-08-25 13:07:58 +00:00
percent: (x,y) ->
x = 1 if x == 0
Math . round ( x / y * 100 )
2013-05-19 17:52:13 +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 .
# ##
2013-05-19 20:43:43 +00:00
dotSet: (path, val, obj) ->
2013-08-11 16:18:58 +00:00
return if ~ path . indexOf ( ' undefined ' )
2013-09-04 02:03:36 +00:00
try
arr = path . split ( ' . ' )
_ . reduce arr , (curr, next, index) ->
if ( arr . length - 1 ) == index
curr [ next ] = val
2013-09-17 17:45:30 +00:00
( curr [ next ] ? = { } )
2013-09-04 02:03:36 +00:00
, obj
catch err
console . error { err , path , val , _id : obj . _id }
2013-05-19 17:52:13 +00:00
2013-08-11 16:18:58 +00:00
dotGet: (path, obj) ->
return undefined if ~ path . indexOf ( ' undefined ' )
2013-09-04 02:03:36 +00:00
try
2013-09-17 17:45:30 +00:00
_ . reduce path . split ( ' . ' ) , ( (curr, next) -> curr ? [ next ] ) , obj
2013-09-04 02:03:36 +00:00
catch err
console . error { err , path , val , _id : obj . _id }
2013-05-19 20:43:43 +00:00
2013-08-21 01:33:37 +00:00
daysSince: daysSince
startOfWeek: startOfWeek
startOfDay: startOfDay
2013-05-19 17:52:13 +00:00
shouldDo: shouldDo
# ##
Get a random property from an object
http : / / stackoverflow . com / questions / 2532218 / pick - random - property - from - a - javascript - object
returns random property ( the value )
# ##
randomVal: (obj) ->
result = undefined
count = 0
for key , val of obj
result = val if Math . random ( ) < ( 1 / ++ count )
result
# ##
Remove whitespace #FIXME are we using this anywwhere? Should we be?
# ##
removeWhitespace: (str) ->
return ' ' unless str
str . replace / \ s / g , ' '
# ##
Generate the username , since it can be one of many things: their username , their facebook fullname , their manually - set profile name
# ##
username: (auth, override) ->
#some people define custom profile name in Avatar -> Profile
return override if override
if auth ? . facebook ? . displayName ?
auth . facebook . displayName
else if auth ? . facebook ?
fb = auth . facebook
if fb . _raw then " #{ fb . name . givenName } #{ fb . name . familyName } " else fb . name
else if auth ? . local ?
auth . local . username
else
' Anonymous '
# ##
Encode the download link for . ics iCal file
# ##
encodeiCalLink: (uid, apiToken) ->
loc = window ? . location . host or process ? . env ? . BASE_URL or ' '
encodeURIComponent " http:// #{ loc } /v1/users/ #{ uid } /calendar.ics?apiToken= #{ apiToken } "
# ##
User ' s currently equiped item
# ##
equipped: (type, item=0, preferences={gender:'m', armorSet:'v1'}, backerTier=0) ->
{ gender , armorSet } = preferences
2013-07-24 20:45:48 +00:00
item = ~ ~ item
backerTier = ~ ~ backerTier
2013-05-19 17:52:13 +00:00
switch type
when ' armor '
if item > 5
return ' armor_6 ' if backerTier >= 45
item = 5 # set them back if they're trying to cheat
if gender is ' f '
return if ( item is 0 ) then " f_armor_ #{ item } _ #{ armorSet } " else " f_armor_ #{ item } "
else
return " m_armor_ #{ item } "
when ' head '
if item > 5
return ' head_6 ' if backerTier >= 45
item = 5
if gender is ' f '
return if ( item > 1 ) then " f_head_ #{ item } _ #{ armorSet } " else " f_head_ #{ item } "
else
return " m_head_ #{ item } "
when ' shield '
if item > 5
return ' shield_6 ' if backerTier >= 45
item = 5
return " #{ preferences . gender } _shield_ #{ item } "
when ' weapon '
if item > 6
return ' weapon_7 ' if backerTier >= 70
item = 6
return " #{ preferences . gender } _weapon_ #{ item } "
# ##
Gold amount from their money
# ##
gold: (num) ->
if num
2013-09-24 01:59:46 +00:00
return Math . floor num
2013-05-19 17:52:13 +00:00
else
return " 0 "
# ##
Silver amount from their money
# ##
silver: (num) ->
if num
2013-09-24 02:30:13 +00:00
( " 0 " + Math . floor ( num - Math . floor ( num ) ) * 100 ) . slice - 2
2013-05-19 17:52:13 +00:00
else
return " 00 "
# ##
Task classes given everything about the class
# ##
2013-10-26 17:23:39 +00:00
taskClasses: (task, filters=[], dayStart=0, lastCron=+new Date, showCompleted=false, main=false) ->
2013-05-19 17:52:13 +00:00
return unless task
{ type , completed , value , repeat } = task
# completed / remaining toggle
return ' hidden ' if ( type is ' todo ' ) and ( completed != showCompleted )
2013-05-26 14:22:49 +00:00
# Filters
if main # only show when on your own list
for filter , enabled of filters
if enabled and not task . tags ? [ filter ]
# All the other classes don't matter
return ' hidden '
2013-05-19 17:52:13 +00:00
classes = type
# show as completed if completed (naturally) or not required for today
if type in [ ' todo ' , ' daily ' ]
2013-05-25 17:26:07 +00:00
if completed or ( type is ' daily ' and ! shouldDo ( + new Date , task . repeat , { dayStart } ) )
2013-05-19 17:52:13 +00:00
classes += " completed "
else
classes += " uncompleted "
else if type is ' habit '
classes += ' habit-wide ' if task . down and task . up
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
# ##
Does the user own this pet ?
# ##
ownsPet: (pet, userPets) -> _ . isArray ( userPets ) and userPets . indexOf ( pet ) != - 1
# ##
Friendly timestamp
# ##
friendlyTimestamp: (timestamp) -> moment ( timestamp ) . format ( ' MM/DD h:mm:ss a ' )
# ##
Does user have new chat messages ?
# ##
newChatMessages: (messages, lastMessageSeen) ->
return false unless messages ? . length > 0
messages ? [ 0 ] and ( messages [ 0 ] . id != lastMessageSeen )
# ##
Relative Date
# ##
relativeDate: require ( ' relative-date ' )
# ##
are any tags active ?
# ##
noTags: (tags) -> _ . isEmpty ( tags ) or _ . isEmpty ( _ . filter ( tags , (t) -> t ) )
# ##
Are there tags applied ?
# ##
appliedTags: (userTags, taskTags) ->
arr = [ ]
_ . each userTags , (t) ->
return unless t ?
arr . push ( t . name ) if taskTags ? [ t . id ]
arr . join ( ' , ' )
# ##
User stats
# ##
2013-07-24 20:45:48 +00:00
userStr: (level) ->
( level - 1 ) / 2
2013-05-19 17:52:13 +00:00
totalStr: (level, weapon=0) ->
str = ( level - 1 ) / 2
2013-07-24 20:45:48 +00:00
( str + items . getItem ( ' weapon ' , weapon ) . strength )
userDef: (level) ->
( level - 1 ) / 2
totalDef: (level, armor=0, head=0, shield=0) ->
totalDef =
( level - 1 ) / 2 + # defense
items . getItem ( ' armor ' , armor ) . defense +
items . getItem ( ' head ' , head ) . defense +
items . getItem ( ' shield ' , shield ) . defense
return totalDef
itemText: (type, item=0) ->
items . getItem ( type , item ) . text
itemStat: (type, item=0) ->
i = items . getItem ( type , item )
2013-08-11 03:30:17 +00:00
if type is ' weapon ' then i . strength else i . defense
# ##
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Derby - specific helpers . Will remove after the rewrite , need them here for now
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# ##
# ##
Make sure model . get ( ) returns all properties , see https : / / github . com / codeparty / racer / issues / 116
# ##
hydrate: (spec) ->
if _ . isObject ( spec ) and ! _ . isArray ( spec )
hydrated = { }
keys = _ . keys ( spec ) . concat ( _ . keys ( spec . __proto__ ) )
2013-08-12 17:18:05 +00:00
keys . forEach (k) => hydrated [ k ] = @ hydrate ( spec [ k ] )
2013-08-11 03:30:17 +00:00
hydrated
2013-08-24 23:07:42 +00:00
else spec