2014-06-03 10:14:10 -04:00
log = require ' winston '
2014-06-03 16:54:56 -04:00
mongoose = require ' mongoose '
async = require ' async '
Achievement = require ' ./Achievement '
2014-05-18 10:51:23 -04:00
EarnedAchievement = require ' ./EarnedAchievement '
2014-06-03 16:54:56 -04:00
User = require ' ../users/User '
2014-05-18 10:51:23 -04:00
Handler = require ' ../commons/Handler '
2014-06-03 16:54:56 -04:00
LocalMongo = require ' ../../app/lib/LocalMongo '
2015-01-07 18:03:32 -05:00
util = require ' ../../app/core/utils '
2015-02-18 12:38:20 -05:00
LevelSession = require ' ../levels/sessions/LevelSession '
2014-05-18 10:51:23 -04:00
class EarnedAchievementHandler extends Handler
modelClass: EarnedAchievement
2015-02-13 19:33:03 -05:00
2015-01-08 14:57:24 -05:00
editableProperties: [ ' notified ' ]
2014-05-18 10:51:23 -04:00
# Don't allow POSTs or anything yet
hasAccess: (req) ->
2014-11-22 21:40:28 -05:00
return false unless req . user
2015-01-08 14:57:24 -05:00
req . method in [ ' GET ' , ' POST ' , ' PUT ' ] # or req.user.isAdmin()
2014-05-18 10:51:23 -04:00
2014-10-10 16:11:35 -04:00
get: (req, res) ->
return @ getByAchievementIDs ( req , res ) if req . query . view is ' get-by-achievement-ids '
2014-11-25 10:52:20 -05:00
unless req . user
2014-11-29 11:43:40 -05:00
return @ sendForbiddenError ( res , " You need to have a user to view earned achievements " )
2014-11-19 17:55:01 -05:00
query = { user: req . user . _id + ' ' }
projection = { }
if req . query . project
projection [ field ] = 1 for field in req . query . project . split ( ' , ' )
q = EarnedAchievement . find ( query , projection )
skip = parseInt ( req . query . skip )
if skip ? and skip < 1000000
q . skip ( skip )
limit = parseInt ( req . query . limit )
if limit ? and limit < 1000
q . limit ( limit )
q . exec (err, documents) =>
return @ sendDatabaseError ( res , err ) if err
documents = ( @ formatEntity ( req , doc ) for doc in documents )
@ sendSuccess ( res , documents )
2014-11-29 11:43:40 -05:00
2014-11-21 01:08:49 -05:00
post: (req, res) ->
achievementID = req . body . achievement
triggeredBy = req . body . triggeredBy
collection = req . body . collection
if collection isnt ' level.sessions '
return @ sendBadInputError ( res , ' Only doing level session achievements for now. ' )
2014-11-29 11:43:40 -05:00
2014-11-21 01:08:49 -05:00
model = mongoose . modelNameByCollection ( collection )
2014-11-29 11:43:40 -05:00
2014-11-21 01:08:49 -05:00
async . parallel ( {
achievement: (callback) ->
Achievement . findById achievementID , (err, achievement) -> callback ( err , achievement )
2014-11-29 11:43:40 -05:00
2014-11-21 01:08:49 -05:00
trigger: (callback) ->
model . findById triggeredBy , (err, trigger) -> callback ( err , trigger )
2014-11-29 11:43:40 -05:00
2014-11-21 01:08:49 -05:00
earned: (callback) ->
q = { achievement: achievementID , user: req . user . _id + ' ' }
EarnedAchievement . findOne q , (err, earned) -> callback ( err , earned )
} , (err, { achievement, trigger, earned } ) =>
return @ sendDatabaseError ( res , err ) if err
if not achievement
return @ sendNotFoundError ( res , ' Could not find achievement. ' )
2014-11-26 15:02:42 -05:00
else if not trigger
2014-11-21 01:08:49 -05:00
return @ sendNotFoundError ( res , ' Could not find trigger. ' )
2015-01-07 18:03:32 -05:00
else if achievement . get ( ' proportionalTo ' ) and earned
EarnedAchievement . createForAchievement ( achievement , trigger , null , earned , (earnedAchievementDoc) =>
2015-01-06 21:14:19 -05:00
@ sendCreated ( res , ( earnedAchievementDoc or earned ) ? . toObject ( ) )
)
2014-11-26 15:02:42 -05:00
else if earned
achievementEarned = achievement . get ( ' rewards ' )
actuallyEarned = earned . get ( ' earnedRewards ' )
if not _ . isEqual ( achievementEarned , actuallyEarned )
earned . set ( ' earnedRewards ' , achievementEarned )
earned . save ( (err) =>
return @ sendDatabaseError ( res , err ) if err
@ upsertNonNumericRewards ( req . user , achievement , (err) =>
return @ sendDatabaseError ( res , err ) if err
return @ sendSuccess ( res , earned . toObject ( ) )
)
)
else
@ upsertNonNumericRewards ( req . user , achievement , (err) =>
return @ sendDatabaseError ( res , err ) if err
return @ sendSuccess ( res , earned . toObject ( ) )
)
else
2015-01-07 15:25:31 -05:00
EarnedAchievement . createForAchievement ( achievement , trigger , null , null , (earnedAchievementDoc) =>
2014-11-26 15:02:42 -05:00
@ sendCreated ( res , earnedAchievementDoc . toObject ( ) )
)
2014-11-21 01:08:49 -05:00
)
2014-11-29 11:43:40 -05:00
2014-11-26 15:02:42 -05:00
upsertNonNumericRewards: (user, achievement, done) ->
update = { }
for rewardType , rewards of achievement . get ( ' rewards ' ) ? { }
continue if rewardType is ' gems '
if rewards . length
update . $addToSet ? = { }
update . $addToSet [ " earned. #{ rewardType } " ] = $each: rewards
User . update { _id: user . _id } , update , { } , (err, count) ->
log . error err if err ?
done ? ( err )
2014-10-10 16:11:35 -04:00
getByAchievementIDs: (req, res) ->
query = { user: req . user . _id + ' ' }
ids = req . query . achievementIDs
if ( not ids ) or ( ids . length is 0 )
return @ sendBadInputError ( res , ' For a get-by-achievement-ids request, need to provide ids. ' )
2014-11-17 11:44:53 -05:00
2014-10-10 16:11:35 -04:00
ids = ids . split ( ' , ' )
for id in ids
if not Handler . isID ( id )
return @ sendBadInputError ( res , " Not a MongoDB ObjectId: #{ id } " )
2014-11-17 11:44:53 -05:00
2014-10-10 16:11:35 -04:00
query.achievement = { $in: ids }
EarnedAchievement . find query , (err, earnedAchievements) ->
return @ sendDatabaseError ( res , err ) if err
res . send ( earnedAchievements )
2014-06-03 10:14:10 -04:00
recalculate: (req, res) ->
2015-02-20 14:11:12 -05:00
return @ sendForbiddenError ( res ) unless req . user ? . isAdmin ( )
2014-06-30 22:16:26 -04:00
onSuccess = (data) => log . debug ' Finished recalculating achievements '
2014-06-08 18:33:06 -04:00
if ' achievements ' of req . body # Support both slugs and IDs separated by commas
2014-09-30 22:32:11 -04:00
achievementSlugsOrIDs = req . body . achievements
2014-06-03 16:54:56 -04:00
EarnedAchievementHandler . recalculate achievementSlugsOrIDs , onSuccess
else
EarnedAchievementHandler . recalculate onSuccess
2014-06-24 12:14:26 -04:00
@ sendAccepted res , { }
2014-06-03 16:54:56 -04:00
2014-07-03 15:20:06 -04:00
@recalculate: (callbackOrSlugsOrIDs, callback) ->
if _ . isArray callbackOrSlugsOrIDs # slugs or ids
2014-06-04 14:47:32 -04:00
achievementSlugs = ( thing for thing in callbackOrSlugsOrIDs when not Handler . isID ( thing ) )
achievementIDs = ( thing for thing in callbackOrSlugsOrIDs when Handler . isID ( thing ) )
2014-09-30 22:32:11 -04:00
recalculatingAll = false
2014-07-03 15:20:06 -04:00
else # just a callback
callback = callbackOrSlugsOrIDs
2014-09-30 22:32:11 -04:00
recalculatingAll = true
2014-08-15 12:32:58 -04:00
t0 = new Date ( ) . getTime ( )
total = 100000
2015-02-13 19:33:03 -05:00
#testUsers = ['livelily+test37@gmail.com']
if testUsers ?
userQuery = emailLower: { $in: testUsers }
else
userQuery = anonymous: false
User . count userQuery , (err, count) -> total = count
2014-08-15 12:32:58 -04:00
onFinished = ->
t1 = new Date ( ) . getTime ( )
runningTime = ( ( t1 - t0 ) / 1000 / 60 / 60 ) . toFixed ( 2 )
console . log " we finished in #{ runningTime } hours "
callback arguments . . .
2014-06-03 16:54:56 -04:00
filter = { }
filter.$or = [
2014-06-04 14:47:32 -04:00
{ _id: $in: achievementIDs } ,
{ slug: $in: achievementSlugs }
2014-06-03 16:54:56 -04:00
] if achievementSlugs ? or achievementIDs ?
2014-06-08 18:33:06 -04:00
# Fetch all relevant achievements
2014-06-03 16:54:56 -04:00
Achievement . find filter , (err, achievements) ->
2014-08-07 16:03:00 -04:00
callback ? ( err ) if err ?
callback ? ( new Error ' No achievements to recalculate ' ) unless achievements . length
2015-02-13 19:33:03 -05:00
log . info " Recalculating a total of #{ achievements . length } achievements... "
2014-06-08 18:33:06 -04:00
2014-08-14 13:17:44 -04:00
# Fetch every single user. This tends to get big so do it in a streaming fashion.
2015-02-13 19:33:03 -05:00
userStream = User . find ( userQuery ) . sort ( ' _id ' ) . stream ( )
2014-08-14 13:17:44 -04:00
streamFinished = false
usersTotal = 0
usersFinished = 0
2014-08-15 12:32:58 -04:00
numberRunning = 0
2014-08-14 13:17:44 -04:00
doneWithUser = ->
++ usersFinished
2014-08-15 12:32:58 -04:00
numberRunning -= 1
userStream . resume ( )
2014-08-14 13:17:44 -04:00
onFinished ? ( ) if streamFinished and usersFinished is usersTotal
userStream . on ' error ' , (err) -> log . error err
userStream . on ' close ' , -> streamFinished = true
userStream . on ' data ' , (user) ->
++ usersTotal
2014-08-15 12:32:58 -04:00
numberRunning += 1
2015-02-16 12:21:59 -05:00
#return doneWithUser() if usersTotal / total < 0.96 # If it died, we can skip ahead on restart like this.
2014-08-15 12:32:58 -04:00
userStream . pause ( ) if numberRunning > 20
2014-08-14 13:17:44 -04:00
# Keep track of a user's already achieved in order to set the notified values correctly
userID = user . get ( ' _id ' ) . toHexString ( )
# Fetch all of a user's earned achievements
EarnedAchievement . find { user: userID } , (err, alreadyEarned) ->
alreadyEarnedIDs = [ ]
previousPoints = 0
2014-09-26 05:28:54 -04:00
previousRewards = heroes: [ ] , items: [ ] , levels: [ ] , gems: 0
2014-08-14 13:17:44 -04:00
async . each alreadyEarned , ( (earned, doneWithEarned) ->
if ( _ . find achievements , (single) -> earned . get ( ' achievement ' ) is single . get ( ' _id ' ) . toHexString ( ) ) # if already earned
alreadyEarnedIDs . push earned . get ( ' achievement ' )
previousPoints += earned . get ' earnedPoints '
2014-09-26 05:28:54 -04:00
for rewardType in [ ' heroes ' , ' items ' , ' levels ' ]
previousRewards [ rewardType ] = previousRewards [ rewardType ] . concat ( earned . get ( ' earnedRewards ' ) ? [ rewardType ] ? [ ] )
previousRewards . gems += earned . get ( ' earnedRewards ' ) ? . gems ? 0
2014-08-14 13:17:44 -04:00
doneWithEarned ( )
2014-09-26 05:28:54 -04:00
) , (err) -> # After checking already achieved
log . error err if err
2014-08-14 13:17:44 -04:00
# TODO maybe also delete earned? Make sure you don't delete too many
newTotalPoints = 0
2014-09-26 05:28:54 -04:00
newTotalRewards = heroes: [ ] , items: [ ] , levels: [ ] , gems: 0
2014-08-14 13:17:44 -04:00
async . each achievements , ( (achievement, doneWithAchievement) ->
isRepeatable = achievement . get ( ' proportionalTo ' ) ?
model = mongoose . modelNameByCollection ( achievement . get ( ' collection ' ) )
return doneWithAchievement new Error " Model with collection ' #{ achievement . get ' collection ' } ' doesn ' t exist. " unless model ?
finalQuery = _ . clone achievement . get ' query '
2014-11-17 11:44:53 -05:00
return doneWithAchievement ( ) if _ . isEmpty finalQuery
2014-08-14 13:17:44 -04:00
finalQuery.$or = [ { } , { } ] # Allow both ObjectIDs or hex string IDs
finalQuery . $or [ 0 ] [ achievement . userField ] = userID
finalQuery . $or [ 1 ] [ achievement . userField ] = mongoose . Types . ObjectId userID
model . findOne finalQuery , (err, something) ->
return doneWithAchievement ( ) if _ . isEmpty something
#log.debug "Matched an achievement: #{achievement.get 'name'} for #{user.get 'name'}"
earned =
user: userID
achievement: achievement . _id . toHexString ( )
achievementName: achievement . get ' name '
notified: achievement . _id in alreadyEarnedIDs
if isRepeatable
2015-01-07 18:03:32 -05:00
earned.achievedAmount = util . getByPath ( something . toObject ( ) , achievement . get ' proportionalTo ' ) or 0
2014-08-14 13:17:44 -04:00
earned.previouslyAchievedAmount = 0
expFunction = achievement . getExpFunction ( )
2014-09-20 18:18:21 -04:00
newPoints = expFunction ( earned . achievedAmount ) * achievement . get ( ' worth ' ) ? 10
2015-01-07 18:03:32 -05:00
newGems = expFunction ( earned . achievedAmount ) * ( achievement . get ( ' rewards ' ) ? . gems ? 0 )
2014-08-14 13:17:44 -04:00
else
2014-09-20 18:18:21 -04:00
newPoints = achievement . get ( ' worth ' ) ? 10
2015-01-07 18:03:32 -05:00
newGems = achievement . get ( ' rewards ' ) ? . gems ? 0
2014-08-14 13:17:44 -04:00
earned.earnedPoints = newPoints
newTotalPoints += newPoints
2014-09-26 05:28:54 -04:00
earned.earnedRewards = achievement . get ( ' rewards ' )
for rewardType in [ ' heroes ' , ' items ' , ' levels ' ]
newTotalRewards [ rewardType ] = newTotalRewards [ rewardType ] . concat ( achievement . get ( ' rewards ' ) ? [ rewardType ] ? [ ] )
2015-01-07 18:03:32 -05:00
if isRepeatable and earned . earnedRewards
earned.earnedRewards = _ . clone earned . earnedRewards
earned.earnedRewards.gems = newGems
newTotalRewards . gems += newGems
2014-09-26 05:28:54 -04:00
2014-08-14 13:17:44 -04:00
EarnedAchievement . update { achievement : earned . achievement , user : earned . user } , earned , { upsert: true } , (err) ->
doneWithAchievement err
2014-09-26 05:28:54 -04:00
) , (err) -> # Wrap up a user, save points
log . error err if err
2014-09-30 22:32:11 -04:00
#console.log 'User', user.get('name'), 'had newTotalPoints', newTotalPoints, 'and newTotalRewards', newTotalRewards, 'previousRewards', previousRewards
2014-09-26 05:28:54 -04:00
return doneWithUser ( user ) unless newTotalPoints or newTotalRewards . gems or _ . some ( newTotalRewards , (r) -> r . length )
2015-02-13 19:33:03 -05:00
#log.debug "Matched a total of #{newTotalPoints} new points"
#log.debug "Incrementing score for these achievements with #{newTotalPoints - previousPoints}"
2014-09-30 22:32:11 -04:00
pointDelta = newTotalPoints - previousPoints
2014-08-15 12:32:58 -04:00
pctDone = ( 100 * usersFinished / total ) . toFixed ( 2 )
2015-02-16 12:21:59 -05:00
console . log " Updated points to #{ newTotalPoints } ( #{ if pointDelta < 0 then ' ' else ' + ' } #{ pointDelta } ) for #{ user . get ( ' name ' ) or ' ??? ' } ( #{ user . get ( ' _id ' ) } ) ( #{ pctDone } %) "
2014-09-30 22:32:11 -04:00
if recalculatingAll
update = { $set: { points: newTotalPoints , ' earned.gems ' : 0 , ' earned.heroes ' : [ ] , ' earned.items ' : [ ] , ' earned.levels ' : [ ] } }
else
update = { $inc: { points: pointDelta } }
secondUpdate = { } # In case we need to pull, then push.
2014-09-26 05:28:54 -04:00
for rewardType , rewards of newTotalRewards
2014-09-30 22:32:11 -04:00
updateKey = " earned. #{ rewardType } "
2014-09-26 05:28:54 -04:00
if rewardType is ' gems '
2014-09-30 22:32:11 -04:00
if recalculatingAll
update . $set [ updateKey ] = rewards
else
update . $inc [ updateKey ] = rewards - previousRewards . gems
2014-09-26 05:28:54 -04:00
else
2014-09-30 22:32:11 -04:00
if recalculatingAll
update . $set [ updateKey ] = _ . uniq rewards
else
previousCounts = _ . countBy previousRewards [ rewardType ]
newCounts = _ . countBy rewards
relevantRewards = _ . union _ . keys ( previousCounts ) , _ . keys ( newCounts )
for reward in relevantRewards
[ previousCount , newCount ] = [ previousCounts [ reward ] , newCounts [ reward ] ]
if newCount and not previousCount
update . $addToSet ? = { }
update . $addToSet [ updateKey ] ? = { $each: [ ] }
update . $addToSet [ updateKey ] . $each . push reward
else if previousCount and not newCount
# Might $pull $each also work here?
update . $pullAll ? = { }
update . $pullAll [ updateKey ] ? = [ ]
update . $pullAll [ updateKey ] . push reward
if update . $addToSet ? [ updateKey ] and update . $pullAll ? [ updateKey ]
# Perform the update in two calls to avoid "MongoError: Cannot update 'earned.levels' and 'earned.levels' at the same time"
secondUpdate . $addToSet ? = { }
secondUpdate . $addToSet [ updateKey ] = update . $addToSet [ updateKey ]
delete update . $addToSet [ updateKey ]
delete update . $addToSet unless _ . size update . $addToSet
#console.log 'recalculatingAll?', recalculatingAll, 'so update is', update, 'secondUpdate', secondUpdate
2014-09-26 05:28:54 -04:00
User . update { _id: userID } , update , { } , (err) ->
2014-08-14 13:17:44 -04:00
log . error err if err ?
2014-09-30 22:32:11 -04:00
if _ . size secondUpdate
User . update { _id: userID } , secondUpdate , { } , (err) ->
log . error err if err ?
doneWithUser user
else
doneWithUser user
2014-08-14 13:17:44 -04:00
2014-06-03 16:54:56 -04:00
2014-05-19 18:24:16 -04:00
module.exports = new EarnedAchievementHandler ( )