mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-01 11:27:14 -05:00
140 lines
6.1 KiB
CoffeeScript
140 lines
6.1 KiB
CoffeeScript
log = require 'winston'
|
|
mongoose = require 'mongoose'
|
|
async = require 'async'
|
|
Achievement = require './Achievement'
|
|
EarnedAchievement = require './EarnedAchievement'
|
|
User = require '../users/User'
|
|
Handler = require '../commons/Handler'
|
|
LocalMongo = require '../../app/lib/LocalMongo'
|
|
|
|
class EarnedAchievementHandler extends Handler
|
|
modelClass: EarnedAchievement
|
|
|
|
# Don't allow POSTs or anything yet
|
|
hasAccess: (req) ->
|
|
req.method is 'GET' # or req.user.isAdmin()
|
|
|
|
recalculate: (req, res) ->
|
|
onSuccess = (data) => log.debug 'Finished recalculating achievements'
|
|
if 'achievements' of req.body # Support both slugs and IDs separated by commas
|
|
achievementSlugsOrIDs = req.body.earnedAchievements
|
|
EarnedAchievementHandler.recalculate achievementSlugsOrIDs, onSuccess
|
|
else
|
|
EarnedAchievementHandler.recalculate onSuccess
|
|
@sendAccepted res, {}
|
|
|
|
@recalculate: (callbackOrSlugsOrIDs, callback) ->
|
|
if _.isArray callbackOrSlugsOrIDs # slugs or ids
|
|
achievementSlugs = (thing for thing in callbackOrSlugsOrIDs when not Handler.isID(thing))
|
|
achievementIDs = (thing for thing in callbackOrSlugsOrIDs when Handler.isID(thing))
|
|
else # just a callback
|
|
callback = callbackOrSlugsOrIDs
|
|
t0 = new Date().getTime()
|
|
total = 100000
|
|
User.count {anonymous:false}, (err, count) -> total = count
|
|
|
|
onFinished = ->
|
|
t1 = new Date().getTime()
|
|
runningTime = ((t1-t0)/1000/60/60).toFixed(2)
|
|
console.log "we finished in #{runningTime} hours"
|
|
callback arguments...
|
|
|
|
filter = {}
|
|
filter.$or = [
|
|
{_id: $in: achievementIDs},
|
|
{slug: $in: achievementSlugs}
|
|
] if achievementSlugs? or achievementIDs?
|
|
|
|
# Fetch all relevant achievements
|
|
Achievement.find filter, (err, achievements) ->
|
|
callback?(err) if err?
|
|
callback?(new Error 'No achievements to recalculate') unless achievements.length
|
|
log.info "Recalculating a total of #{achievements.length} achievements..."
|
|
|
|
# Fetch every single user. This tends to get big so do it in a streaming fashion.
|
|
userStream = User.find().sort('_id').stream()
|
|
streamFinished = false
|
|
usersTotal = 0
|
|
usersFinished = 0
|
|
numberRunning = 0
|
|
doneWithUser = ->
|
|
++usersFinished
|
|
numberRunning -= 1
|
|
userStream.resume()
|
|
|
|
onFinished?() if streamFinished and usersFinished is usersTotal
|
|
userStream.on 'error', (err) -> log.error err
|
|
userStream.on 'close', -> streamFinished = true
|
|
userStream.on 'data', (user) ->
|
|
++usersTotal
|
|
numberRunning += 1
|
|
userStream.pause() if numberRunning > 20
|
|
|
|
# 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
|
|
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'
|
|
doneWithEarned()
|
|
), -> # After checking already achieved
|
|
# TODO maybe also delete earned? Make sure you don't delete too many
|
|
|
|
newTotalPoints = 0
|
|
|
|
async.each achievements, ((achievement, doneWithAchievement) ->
|
|
return doneWithAchievement() unless achievement.isRecalculable()
|
|
|
|
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'
|
|
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
|
|
earned.achievedAmount = something.get(achievement.get 'proportionalTo')
|
|
earned.previouslyAchievedAmount = 0
|
|
|
|
expFunction = achievement.getExpFunction()
|
|
newPoints = expFunction(earned.achievedAmount) * achievement.get('worth')
|
|
else
|
|
newPoints = achievement.get 'worth'
|
|
|
|
earned.earnedPoints = newPoints
|
|
newTotalPoints += newPoints
|
|
|
|
EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) ->
|
|
doneWithAchievement err
|
|
), -> # Wrap up a user, save points
|
|
# Since some achievements cannot be recalculated it's important to deduct the old amount of exp
|
|
# and add the new amount, instead of just setting to the new amount
|
|
return doneWithUser(user) unless newTotalPoints
|
|
# log.debug "Matched a total of #{newTotalPoints} new points"
|
|
# log.debug "Incrementing score for these achievements with #{newTotalPoints - previousPoints}"
|
|
pctDone = (100 * usersFinished / total).toFixed(2)
|
|
console.log "Updated points to #{newTotalPoints}(+#{newTotalPoints - previousPoints}) for #{user.get('name') or '???'} (#{user.get('_id')}) (#{pctDone}%)"
|
|
User.update {_id: userID}, {$inc: points: newTotalPoints - previousPoints}, {}, (err) ->
|
|
log.error err if err?
|
|
doneWithUser(user)
|
|
|
|
|
|
module.exports = new EarnedAchievementHandler()
|