2013-12-11 16:30:07 +00:00
moment = require ( ' moment ' )
_ = require ( ' lodash ' )
content = require ( ' ./content.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
2013-12-28 18:31:21 +00:00
# little helper for large arrays of strings. %w"this that another" equivalent from Rails, 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={}) ->
o = sanitizeOptions ( options )
2014-10-08 23:38:37 +00:00
moment ( o . now ) . startOf ( ' day ' ) . add ( { hours : o . 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
Math . abs api . startOfDay ( _ . defaults { now : yesterday } , o ) . diff ( o . now , ' days ' )
# ##
Should the user do this taks on this date , given the task ' s repeat options and user.preferences.dayStart?
# ##
api.shouldDo = (day, repeat, options={}) ->
return false unless repeat
o = sanitizeOptions options
2014-01-18 04:41:20 +00:00
selected = repeat [ api . dayMapping [ api . startOfDay ( _ . defaults { now : day } , o ) . day ( ) ] ]
2013-12-11 16:30:07 +00:00
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?
return selected
else # we're not past dayStart mark, check if it was due "yesterday"
2014-10-08 23:38:37 +00:00
yesterday = moment ( o . now ) . subtract ( { days : 1 } ) . day ( ) # have to wrap o.now so as not to modify original
2014-01-18 04:41:20 +00:00
return repeat [ api . dayMapping [ yesterday ] ] # FIXME is this correct?? Do I need to do any timezone calcaulation here?
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 ) , { }
#sortOrder.potion = _.size(sortOrder) #potion goes last #actually, _.sortBy puts anything else last, so this is unecessary
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) ->
2014-02-23 04:41:48 +00:00
v . klass in [ ' special ' , ' mystery ' ] and ! user . items . gear . owned [ v . key ] and v . canOwn ? ( user )
2013-12-16 05:37:40 +00:00
changes . push content . potion
# 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 ' ]
_ . defaults ( task , { streak : 0 , repeat: { su : 1 , m : 1 , t : 1 , w : 1 , th : 1 , f : 1 , s : 1 } } ) if task . type is ' daily '
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
# ##
api.taskClasses = (task, filters=[], dayStart=0, lastCron=+new Date, showCompleted=false, main=false) ->
return unless task
{ type , completed , value , repeat } = task
# completed / remaining toggle
2014-01-03 21:42:18 +00:00
return ' hidden ' if ( type is ' todo ' and completed != showCompleted ) and main
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 ' ]
if completed or ( type is ' daily ' and ! api . shouldDo ( + new Date , task . repeat , { dayStart } ) )
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
# ##
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 ( ' , ' )
api.countPets = (originalCount, pets) ->
count = if originalCount ? then originalCount else _ . size ( pets )
2014-02-09 00:56:07 +00:00
for pet of content . questPets
count - - if pets [ pet ]
2013-12-11 16:30:07 +00:00
for pet of content . specialPets
count - - if pets [ pet ]
count
2014-02-09 00:56:07 +00:00
api.countMounts= (originalCount, mounts) ->
count = if originalCount ? then originalCount else _ . size ( mounts )
for mount of content . specialMounts
count - - if mounts [ mount ]
count
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 , { }
revive: (req, cb) ->
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-01-07 17:36:24 +00:00
# Reset stats
_ . 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
# Note, they can actually lose item weapon_*_0 - it's 0 to buy back, no big deal
# Note ""+k string-casting. Without this, when run on the server Mongoose returns funny objects
lostItem = user . fns . randomVal _ . reduce ( user . items . gear . owned , ( (m,v,k)-> m [ ' ' + k ] = ' ' + k if v ; m ) , { } )
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 '
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 '
2014-08-01 03:56:00 +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
2014-02-12 00:45:56 +00:00
reroll: (req, cb, ga) ->
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
cb ? null , user
2014-02-12 00:45:56 +00:00
ga ? . event ( ' purchase ' , ' reroll ' ) . send ( )
2014-01-07 17:36:24 +00:00
2014-02-12 00:45:56 +00:00
rebirth: (req, cb, ga) ->
2014-01-07 17:36:24 +00:00
# Cost is 8 Gems ($2)
2014-03-28 05:27:37 +00:00
if ( user . balance < 2 && user . stats . lvl < 100 )
2014-03-16 17:37:53 +00:00
return cb ? { code : 401 , message: i18n . t ( ' notEnoughGems ' , req . language ) }
2014-03-28 05:27:37 +00:00
# only charge people if they are under level 100 - ryan
if user . stats . lvl < 100
user . balance -= 2
2014-01-07 17:36:24 +00:00
# Save off user's level, for calculating achievement eligibility later
2014-05-11 14:53:31 +00:00
if user . stats . lvl < 100
lvl = user . stats . lvl
else
lvl = 100
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 } } )
2014-01-09 01:35:54 +00:00
# Strip owned gear down to the training sword, but preserve purchase history so user can re-purchase limited edition equipment
2014-01-09 01:55:46 +00:00
_ . each gear . owned , (v, k) -> if gear . owned [ k ] 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
if not ( user . achievements . ultimateGear or user . achievements . beastMaster )
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-02-12 00:45:56 +00:00
ga ? . event ( ' purchase ' , ' Rebirth ' ) . send ( )
2014-01-07 17:36:24 +00:00
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 ]
2014-03-20 20:56:34 +00:00
_ . merge task , _ . omit ( req . body , [ ' checklist ' , ' id ' ] )
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 )
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
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
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 ]
return cb ? ( { code : 401 , message : i18n . t ( ' messageCannotFeedPet ' , req . language ) } ) if content . specialPets [ pet ] or ( egg is " Egg " )
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
2014-05-17 09:44:59 +00:00
message = i18n . t ( ' messageEvolve ' , { egg: egg } , 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
2014-04-15 16:15:11 +00:00
message = i18n . t ( ' messageLikesFood ' , { egg: egg , 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
2014-04-15 16:15:11 +00:00
message = i18n . t ( ' messageDontEnjoyFood ' , { egg: egg , 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 ] - -
cb ? { code : 200 , message } , userPets [ pet ]
2014-10-03 06:31:49 +00:00
#FIXME stupid method of special-handling spookDust, since it can be purchased with gold and the system only accomodates gem-purchasable holiday spells
buySpookDust: (req,cb) ->
item = content . special . spookDust
return cb ? ( { code : 401 , message: i18n . t ( ' messageNotEnoughGold ' , req . language ) } ) if user . stats . gp < item . value
user . stats . gp -= item . value
user . items . special . spookDust ? = 0
user . items . special . spookDust ++
user . markModified ? ' items.special '
cb ? null , _ . pick ( user , $w ' items stats ' )
2014-01-07 17:36:24 +00:00
# buy is for gear, purchase is for gem-purchaseables (i know, I know...)
2014-02-12 00:45:56 +00:00
purchase: (req, cb, ga) ->
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-03-09 04:12:55 +00:00
return cb ? ( { code : 401 , message : " Must subscribe to purchase gems with GP " } , req ) unless user . purchased ? . plan ? . planId
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
return cb ? { code : 200 , message : " +1 Gems " } , _ . pick ( user , $w ' stats balance ' )
2014-01-07 17:36:24 +00:00
return cb ? ( { code : 404 , message : " :type must be in [hatchingPotions,eggs,food,quests,special] " } , req ) unless type in [ ' eggs ' , ' hatchingPotions ' , ' food ' , ' quests ' , ' special ' ]
item = content [ type ] [ key ]
return cb ? ( { code : 404 , message : " :key not found for Content. #{ type } " } , req ) unless item
2014-04-07 17:04:59 +00:00
return cb ? ( { code : 401 , message: i18n . t ( ' notEnoughGems ' , req . language ) } ) if user . balance < ( item . value / 4 )
2014-05-11 16:30:43 +00:00
user . items [ type ] [ key ] = 0 unless user . items [ type ] [ key ] > 0
2014-01-07 17:36:24 +00:00
user . items [ type ] [ key ] ++
user . balance -= ( item . value / 4 )
cb ? null , _ . pick ( user , $w ' items balance ' )
2014-02-12 00:45:56 +00:00
ga ? . event ( ' purchase ' , key ) . send ( )
2014-01-07 17:36:24 +00:00
2014-09-29 22:11:24 +00:00
release: (req, cb) ->
if user . balance < 1
return cb ? { code : 401 , message: i18n . t ( ' notEnoughGems ' , req . language ) }
else
user . balance - -
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 = " "
cb ? null , user
release2: (req, cb) ->
if user . balance < 2
return cb ? { code : 401 , message: i18n . t ( ' notEnoughGems ' , req . language ) }
else
user . balance -= 2
user.items.currentMount = " "
user.items.currentPet = " "
for pet of content . pets
user . items . mounts [ pet ] = false
user . items . pets [ pet ] = 0
if not user . achievements . beastMasterCount
user.achievements.beastMasterCount = 0
user . achievements . beastMasterCount ++
cb ? null , user
2014-01-07 17:36:24 +00:00
# buy is for gear, purchase is for gem-purchaseables (i know, I know...)
buy: (req, cb) ->
{ key } = req . params
2014-11-29 18:16:54 +00:00
2014-01-07 17:36:24 +00:00
item = if key is ' potion ' then content . potion else content . gear . flat [ key ]
2014-11-29 18:16:54 +00:00
return cb ? ( { code : 404 , message : " Item ' #{ key } not found (see https://github.com/HabitRPG/habitrpg-shared/blob/develop/script/content.coffee) " } ) unless item
return cb ? ( { code : 401 , message: i18n . t ( ' messageNotEnoughGold ' , req . language ) } ) if user . stats . gp < item . value
return cb ? ( { code : 401 , message: " You can ' t own this item " } ) if item . canOwn ? and ! item . canOwn ( user )
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
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 )
2014-01-10 04:33:01 +00:00
if not user . achievements . ultimateGear and item . last
2014-01-24 06:02:51 +00:00
user . fns . ultimateGear ( )
2014-11-29 18:16:54 +00:00
user . stats . gp -= item . value
2014-01-07 17:36:24 +00:00
cb ? { code : 200 , message } , _ . pick ( user , $w ' items achievements stats ' )
2014-11-29 18:16:54 +00:00
buyMysterySet: (req, cb)->
return cb ? ( { code : 401 , message : " You don ' t have enough Mystic Hourglasses " } ) unless user . purchased . plan . consecutive . trinkets > 0
mysterySet = content . timeTravelerStore ( user . items . gear . owned ) ? [ req . params . key ]
if window ? . confirm ?
return unless window . confirm ( " Buy this full set of items for 1 Mystic Hourglass? " )
return cb ? ( { code : 404 , message : " Mystery set not found, or set already owned " } ) unless mysterySet
_ . each mysterySet . items , (i)-> user . items . gear . owned [ i . key ] = true
user . purchased . plan . consecutive . trinkets - -
cb ? null , _ . pick ( user , $w ' items purchased.plan.consecutive ' )
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
2014-02-12 00:45:56 +00:00
unlock: (req, cb, ga) ->
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
if ~ path . indexOf ( ' background. ' ) # FIXME, store prices of things in content.coffee instead of hard-coded here?
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
2014-04-07 17:04:59 +00:00
return cb ? ( { code : 401 , message: i18n . t ( ' notEnoughGems ' , req . language ) } ) if user . balance < cost and ! alreadyOwns
2014-01-07 17:36:24 +00:00
if fullSet
_ . each path . split ( " , " ) , (p) ->
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
user . markModified ? ' purchased '
cb ? null , _ . pick ( user , $w ' purchased preferences ' )
2014-02-12 00:45:56 +00:00
ga ? . event ( ' purchase ' , path ) . send ( )
2014-01-07 17:36:24 +00:00
# ------
# Classes
# ------
2014-02-12 00:45:56 +00:00
changeClass: (req, cb, ga) ->
2014-01-07 17:36:24 +00:00
klass = req . query ? . class
if klass in [ ' warrior ' , ' rogue ' , ' wizard ' , ' healer ' ]
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
_ . merge user . stats , { str: 0 , con: 0 , per: 0 , int: 0 , points: user . stats . lvl }
user.flags.classSelected = false
2014-02-12 00:45:56 +00:00
ga ? . event ( ' purchase ' , ' changeClass ' ) . send ( )
2014-01-07 17:36:24 +00:00
#'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
user.stats.str = user . stats . lvl
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 ' )
2014-02-14 00:57:10 +00:00
readValentine: (req,cb) ->
user . items . special . valentineReceived . shift ( )
user . markModified ? ' items.special.valentineReceived '
cb ? null , ' items.special '
2014-03-09 04:12:55 +00:00
openMysteryItem: (req,cb,ga) ->
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 '
# Could show {code:200} message, but it's yellow with no icon. This is round-about, but prettier. FIXME
( user . _tmp ? = { } ) . drop = { type: ' gear ' , dialog: " #{ item . text ( req . language ) } inside! " } if typeof window != ' undefined '
#cb? {code:200, message:"#{item.text} inside!"}, user.items.gear.owned
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-01-07 17:36:24 +00:00
# ===== STRENGTH =====
# (Only for up-scoring, ignore up-onlies and rewards)
# Note, we create a new val (adjustAmt) to add to task.value, since delta will be used in Exp & GP calculations - we don't want STR to bonus that
2014-02-28 03:21:04 +00:00
if direction is ' up ' and ! ( task . type is ' habit ' and ! task . down )
2014-01-07 17:36:24 +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 ' ]
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
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 ( )
# 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
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-01-09 19:16:39 +00:00
mpDelta = _ . max ( [ ( multiplier ) , ( . 01 * user . _statsComputed . maxMP * multiplier ) ] )
2014-06-22 19:44:39 +00:00
mpDelta *= user . _tmp . crit or 1
2014-01-07 17:36:24 +00:00
mpDelta *= - 1 if direction is ' down ' # unticking a todo
user . stats . mp += mpDelta
user.stats.mp = user . _statsComputed . maxMP if user . stats . mp >= user . _statsComputed . maxMP
user.stats.mp = 0 if user . stats . mp < 0 # BUT DO WE WANT THIS? SEE COMMIT DESCRIPTION
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))
if user . fns . predictableRandom ( ) <= chance * ( 1 + user . _statsComputed [ stat ] / 100 ) then 1.5 + ( . 02 * user . _statsComputed [ stat ] )
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) ->
2014-01-11 08:43:07 +00:00
array = if options ? . key then _ . keys ( obj ) else _ . values ( obj )
2014-09-02 03:47:57 +00:00
rand = user . fns . predictableRandom ( options ? . seed )
2014-09-02 02:35:40 +00:00
array . sort ( )
2014-01-11 08:43:07 +00:00
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.
ideal = [ ( user . stats . lvl / 7 * 3 ) , ( user . stats . lvl / 7 * 2 ) , ( user . stats . lvl / 7 ) , ( user . stats . lvl / 7 ) ]
# 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
2014-04-07 17:04:59 +00:00
updateStats: (stats, req) ->
2013-12-13 22:55:47 +00:00
# Game Over
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 )
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 )
# 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
user.stats.points = user . stats . lvl - ( user . stats . con + user . stats . str + user . stats . per + user . stats . int ) ;
2013-12-14 00:50:09 +00:00
2013-12-13 22:55:47 +00:00
user.stats.hp = 50
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
if ! user . flags . partyEnabled and user . stats . lvl >= 3
user.flags.partyEnabled = true
if ! user . flags . dropsEnabled and user . stats . lvl >= 4
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 '
user._tmp.drop = _ . defaults content . quests [ k ] ,
type: ' Quest '
dialog: i18n . t ( ' messageFoundQuest ' , { questText: content . quests [ k ] . text ( req . language ) } , req . language )
2013-12-24 15:04:18 +00:00
if ! user . flags . rebirthEnabled and ( user . stats . lvl >= 50 or user . achievements . ultimateGear or user . achievements . beastMaster )
2013-12-24 15:09:48 +00:00
user.flags.rebirthEnabled = true
2014-08-01 20:20:13 +00:00
if user . stats . lvl >= 100 and ! user . flags . freeRebirth
2014-05-11 14:53:31 +00:00
user.flags.freeRebirth = 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 ++ / --?
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 '
2013-12-13 22:11:04 +00:00
# User is resting at the inn. Used to be we un-checked each daily without performing calculation (see commits before fb29e35)
# but to prevent abusing the inn (http://goo.gl/GDb9x) we now do *not* calculate dailies, and simply set lastCron to today
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
2013-12-16 19:54:46 +00:00
return
2013-12-13 22:11:04 +00:00
2013-12-23 05:56:37 +00:00
# Tally each task
2013-12-13 22:11:04 +00:00
todoTally = 0
2013-12-25 04:57:46 +00:00
user . party . quest . progress . down ? = 0
2013-12-13 22:11:04 +00:00
user . todos . concat ( user . dailys ) . forEach (task) ->
return unless task
{ id , type , completed , repeat } = task
2014-01-19 01:53:38 +00:00
return if ( type is ' daily ' ) && ! completed && user . stats . buffs . stealth && user . stats . buffs . stealth - - # User "evades" a certain number of uncompleted dailies
2014-02-09 00:56:07 +00:00
2013-12-13 22:11:04 +00:00
# Deduct experience for missed Daily tasks, but not for Todos (just increase todo's value)
unless completed
scheduleMisses = daysMissed
# for dailys which have repeat dates, need to calculate how many they've missed according to their own schedule
if ( type is ' daily ' ) and repeat
scheduleMisses = 0
_ . times daysMissed , (n) ->
2014-10-08 23:38:37 +00:00
thatDay = moment ( now ) . subtract ( { days: n + 1 } )
2013-12-13 22:11:04 +00:00
scheduleMisses ++ if api . shouldDo ( thatDay , repeat , user . preferences )
if scheduleMisses > 0
2014-01-18 04:41:20 +00:00
perfect = false if type is ' daily '
2013-12-23 05:56:37 +00:00
delta = user . ops . score ( { params : { id : task . id , direction : ' down ' } , query : { times : scheduleMisses , cron : true } } ) ;
2013-12-25 04:57:46 +00:00
user . party . quest . progress . down += delta if type is ' daily '
2013-12-13 22:11:04 +00:00
switch type
when ' daily '
( task . history ? = [ ] ) . push ( { date: + new Date , value: task . value } )
task.completed = false
2013-12-30 20:23:48 +00:00
_ . each task . checklist , ( (i)-> i . completed = false ; true )
2013-12-13 22:11:04 +00:00
when ' todo '
#get updated value
absVal = if ( completed ) then Math . abs ( task . value ) else task . value
todoTally += absVal
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 ++
2014-05-11 14:53:31 +00:00
if user . stats . lvl < 100
lvlDiv2 = Math . ceil ( user . stats . lvl / 2 )
else
lvlDiv2 = 50
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
user . stats . mp += _ . max ( [ 10 , . 1 * user . _statsComputed . maxMP ] )
user.stats.mp = user . _statsComputed . maxMP if user . stats . mp > user . _statsComputed . maxMP
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
# ----------------------------------------------------------------------
ultimateGear: () ->
# on the server this is a LoDash transform, on the client its an object
gear = if window ? then user . items . gear . owned else user . items . gear . owned . toObject ( )
ownedLastGear = _ . chain ( content . gear . flat )
. pick ( _ . keys gear )
. values ( )
. filter (gear) -> gear . last
lastGearClassTypeMatrix = { }
_ . each content . classes , (klass) ->
lastGearClassTypeMatrix [ klass ] = { }
2014-03-21 20:02:29 +00:00
#_.each content.gearTypes, (type) ->
_ . each [ ' armor ' , ' weapon ' , ' shield ' , ' head ' ] , (type) ->
2014-01-24 06:02:51 +00:00
lastGearClassTypeMatrix [ klass ] [ type ] = false
return true # false exits the each loop early
ownedLastGear . each (gear) ->
lastGearClassTypeMatrix [ gear . klass ] [ " shield " ] = true if gear . twoHanded
lastGearClassTypeMatrix [ gear . klass ] [ gear . type ] = true
shouldGrant = _ ( lastGearClassTypeMatrix )
. values ( )
. reduce ( ( (ans, klass) -> ans or _ ( klass ) . values ( ) . reduce ( ( (ans, gearType) -> ans and gearType ) , true ) ) , false )
. valueOf ( )
user.achievements.ultimateGear = shouldGrant
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
2014-05-11 14:53:31 +00:00
if user . stats . lvl < 100
m [ stat ] += ( user . stats . lvl - 1 ) / 2
else
m [ stat ] += 50
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 )