Several fixes for achievement recalculation.

This commit is contained in:
Nick Winter 2014-09-30 19:32:11 -07:00
parent 5c77e103f3
commit 8d37309de1
6 changed files with 60 additions and 32 deletions

View file

@ -61,7 +61,7 @@ _.extend AchievementSchema.properties,
description: 'For repeatables only. Denotes the field a repeatable achievement needs for its calculations'
recalculable:
type: 'boolean'
description: 'Needs to be set to true before it is eligible for recalculation.'
description: 'Deprecated: all achievements must be recalculable now. Used to need to be set to true before it is eligible for recalculation.'
function:
type: 'object'
description: 'Function that gives total experience for X amount achieved'

View file

@ -10,6 +10,7 @@ block content
li.active
| #{achievement.attributes.name}
button.achievement-tool-button(data-i18n="", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#recalculate-all-button Recalculate All
button.achievement-tool-button(data-i18n="", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#recalculate-button Recalculate
button.achievement-tool-button(data-i18n="common.delete", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#delete-button Delete
button.achievement-tool-button(data-i18n="common.save", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#save-button Save

View file

@ -14,6 +14,7 @@ module.exports = class AchievementEditView extends RootView
events:
'click #save-button': 'saveAchievement'
'click #recalculate-button': 'confirmRecalculation'
'click #recalculate-all-button': 'confirmAllRecalculation'
'click #delete-button': 'confirmDeletion'
constructor: (options, @achievementID) ->
@ -82,17 +83,21 @@ module.exports = class AchievementEditView extends RootView
url = "/editor/achievement/#{@achievement.get('slug') or @achievement.id}"
document.location.href = url
confirmRecalculation: ->
confirmRecalculation: (e, all=false) ->
renderData =
'confirmTitle': 'Are you really sure?'
'confirmBody': 'This will trigger recalculation of the achievement for all users. Are you really sure you want to go down this path?'
'confirmBody': "This will trigger recalculation of #{if all then 'all achievements' else 'the achievement'} for all users. Are you really sure you want to go down this path?"
'confirmDecline': 'Not really'
'confirmConfirm': 'Definitely'
confirmModal = new ConfirmModal renderData
confirmModal.on 'confirm', @recalculateAchievement
@recalculatingAll = all
@openModalView confirmModal
confirmAllRecalculation: (e) ->
@confirmRecalculation e, true
confirmDeletion: ->
renderData =
'confirmTitle': 'Are you really sure?'
@ -105,8 +110,9 @@ module.exports = class AchievementEditView extends RootView
@openModalView confirmModal
recalculateAchievement: =>
data = if @recalculatingAll then {} else {achievements: [@achievement.get('slug') or @achievement.get('_id')]}
$.ajax
data: JSON.stringify(earnedAchievements: [@achievement.get('slug') or @achievement.get('_id')])
data: JSON.stringify data
success: (data, status, jqXHR) ->
noty
timeout: 5000

View file

@ -30,8 +30,6 @@ AchievementSchema.methods.getExpFunction = ->
TreemaUtils.populateDefaults(func, jsonschema.properties.function)
return utils.functionCreators[func.kind](func.parameters) if func.kind of utils.functionCreators
AchievementSchema.methods.isRecalculable = -> @get('recalculable') isnt false
AchievementSchema.statics.jsonschema = jsonschema
AchievementSchema.statics.earnedAchievements = {}

View file

@ -5,7 +5,7 @@ class AchievementHandler extends Handler
modelClass: Achievement
# Used to determine which properties requests may edit
editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function', 'related', 'difficulty', 'category', 'recalculable', 'rewards']
editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function', 'related', 'difficulty', 'category', 'rewards']
allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
jsonSchema = require '../../app/schemas/models/achievement.coffee'

View file

@ -16,8 +16,9 @@ class EarnedAchievementHandler extends Handler
recalculate: (req, res) ->
onSuccess = (data) => log.debug 'Finished recalculating achievements'
console.log 'req.body.achievements is', req.body.achievements
if 'achievements' of req.body # Support both slugs and IDs separated by commas
achievementSlugsOrIDs = req.body.earnedAchievements
achievementSlugsOrIDs = req.body.achievements
EarnedAchievementHandler.recalculate achievementSlugsOrIDs, onSuccess
else
EarnedAchievementHandler.recalculate onSuccess
@ -27,8 +28,10 @@ class EarnedAchievementHandler extends Handler
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))
recalculatingAll = false
else # just a callback
callback = callbackOrSlugsOrIDs
recalculatingAll = true
t0 = new Date().getTime()
total = 100000
User.count {anonymous:false}, (err, count) -> total = count
@ -52,7 +55,7 @@ class EarnedAchievementHandler extends Handler
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()
userStream = User.find(slug: 'nick').sort('_id').stream()
streamFinished = false
usersTotal = 0
usersFinished = 0
@ -94,8 +97,6 @@ class EarnedAchievementHandler extends Handler
newTotalRewards = heroes: [], items: [], levels: [], gems: 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?
@ -137,36 +138,58 @@ class EarnedAchievementHandler extends Handler
doneWithAchievement err
), (err) -> # Wrap up a user, save points
log.error err if err
# 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
#console.log 'User', user.get('name'), 'had newTotalPoints', newTotalPoints, 'and newTotalRewards', newTotalRewards
#console.log 'User', user.get('name'), 'had newTotalPoints', newTotalPoints, 'and newTotalRewards', newTotalRewards, 'previousRewards', previousRewards
return doneWithUser(user) unless newTotalPoints or newTotalRewards.gems or _.some(newTotalRewards, (r) -> r.length)
# log.debug "Matched a total of #{newTotalPoints} new points"
# log.debug "Incrementing score for these achievements with #{newTotalPoints - previousPoints}"
pointDelta = 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}%)"
update = {$inc: {points: newTotalPoints - previousPoints}}
console.log "Updated points to #{newTotalPoints} (#{if pointDelta < 0 then '' else '+'}#{pointDelta}) for #{user.get('name') or '???'} (#{user.get('_id')}) (#{pctDone}%)"
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.
for rewardType, rewards of newTotalRewards
updateKey = "earned.#{rewardType}"
if rewardType is 'gems'
update.$inc['earned.gems'] = rewards - previousRewards.gems
if recalculatingAll
update.$set[updateKey] = rewards
else
update.$inc[updateKey] = rewards - previousRewards.gems
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["earned.#{rewardType}"] ?= {$each: []}
update.$addToSet["earned.#{rewardType}"].$each.push reward
else if previousCount and not newCount
# Might $pull $each also work here?
update.$pullAll ?= {}
update.$pullAll["earned.#{rewardType}"] ?= []
update.$pullAll["earned.#{rewardType}"].push reward
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
User.update {_id: userID}, update, {}, (err) ->
log.error err if err?
doneWithUser(user)
if _.size secondUpdate
User.update {_id: userID}, secondUpdate, {}, (err) ->
log.error err if err?
doneWithUser user
else
doneWithUser user
module.exports = new EarnedAchievementHandler()