mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-23 23:58:02 -05:00
Several fixes for achievement recalculation.
This commit is contained in:
parent
5c77e103f3
commit
8d37309de1
6 changed files with 60 additions and 32 deletions
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue