Rechecked and added cool stuff for achievements

This commit is contained in:
Ruben Vereecken 2014-07-30 22:23:43 +02:00
parent d4043ac3db
commit 871149b2bc
17 changed files with 201 additions and 112 deletions

View file

@ -1,6 +1,9 @@
CocoCollection = require 'collections/CocoCollection' CocoCollection = require 'collections/CocoCollection'
Achievement = require 'models/Achievement'
class NewAchievementCollection extends CocoCollection class NewAchievementCollection extends CocoCollection
model: Achievement
initialize: (me = require('lib/auth').me) -> initialize: (me = require('lib/auth').me) ->
@url = "/db/user/#{me.id}/achievements?notified=false" @url = "/db/user/#{me.id}/achievements?notified=false"

View file

@ -7,7 +7,7 @@ module.exports = class Achievement extends CocoModel
urlRoot: '/db/achievement' urlRoot: '/db/achievement'
isRepeatable: -> isRepeatable: ->
@get('proportionalTo')?a @get('proportionalTo')?
# TODO logic is duplicated in Mongoose Achievement schema # TODO logic is duplicated in Mongoose Achievement schema
getExpFunction: -> getExpFunction: ->

View file

@ -1,8 +1,6 @@
storage = require 'lib/storage' storage = require 'lib/storage'
deltasLib = require 'lib/deltas' deltasLib = require 'lib/deltas'
NewAchievementCollection = require '../collections/NewAchievementCollection'
class CocoModel extends Backbone.Model class CocoModel extends Backbone.Model
idAttribute: '_id' idAttribute: '_id'
loaded: false loaded: false
@ -298,13 +296,15 @@ class CocoModel extends Backbone.Model
return if _.isString @url then @url else @url() return if _.isString @url then @url else @url()
@pollAchievements: -> @pollAchievements: ->
NewAchievementCollection = require '../collections/NewAchievementCollection' # Nasty mutual inclusion if put on top
console.debug 'Polling for new achievements'
achievements = new NewAchievementCollection achievements = new NewAchievementCollection
achievements.fetch( achievements.fetch
success: (collection) -> success: (collection) ->
console.debug 'Polling for achievements success', collection
me.fetch (success: -> Backbone.Mediator.publish('achievements:new', collection)) unless _.isEmpty(collection.models) me.fetch (success: -> Backbone.Mediator.publish('achievements:new', collection)) unless _.isEmpty(collection.models)
error: (collection, res, options) -> error: (collection, res, options) ->
console.error 'Miserably failed to fetch unnotified achievements' console.error 'Miserably failed to fetch unnotified achievements'
)
CocoModel.pollAchievements = _.debounce CocoModel.pollAchievements, 500 CocoModel.pollAchievements = _.debounce CocoModel.pollAchievements, 500

View file

@ -58,9 +58,15 @@ module.exports = class SuperModel extends Backbone.Model
return res return res
else else
@addCollection collection @addCollection collection
@listenToOnce collection, 'sync', (c) -> onCollectionSynced = (c) ->
console.debug 'Registering collection', url if collection.url is c.url
console.debug 'Registering collection', url, c
@registerCollection c @registerCollection c
else
console.warn 'Sync triggered for collection', c
console.warn 'Yet got other object', c
@listenToOnce collection, 'sync', onCollectionSynced
@listenToOnce collection, 'sync', onCollectionSynced
res = @addModelResource(collection, name, fetchOptions, value) res = @addModelResource(collection, name, fetchOptions, value)
res.load() if not (res.isLoading or res.isLoaded) res.load() if not (res.isLoading or res.isLoaded)
return res return res

View file

@ -16,6 +16,10 @@ module.exports = class User extends CocoModel
super() super()
@migrateEmails() @migrateEmails()
onLoaded: ->
CocoModel.pollAchievements() # Check for achievements on login
super arguments...
isAdmin: -> isAdmin: ->
permissions = @attributes['permissions'] or [] permissions = @attributes['permissions'] or []
return 'admin' in permissions return 'admin' in permissions
@ -121,15 +125,16 @@ module.exports = class User extends CocoModel
isEmailSubscriptionEnabled: (name) -> (@get('emails') or {})[name]?.enabled isEmailSubscriptionEnabled: (name) -> (@get('emails') or {})[name]?.enabled
a = 5 a = 5
b = 40 b = 100
c = b
# y = a * ln(1/b * (x + b)) + 1 # y = a * ln(1/b * (x + c)) + 1
@levelFromExp: (xp) -> @levelFromExp: (xp) ->
if xp > 0 then Math.floor(a * Math.log((1/b) * (xp + b))) + 1 else 1 if xp > 0 then Math.floor(a * Math.log((1/b) * (xp + c))) + 1 else 1
# x = (e^((y-1)/a) - 1) * b # x = b * e^((y-1)/a) - c
@expForLevel: (level) -> @expForLevel: (level) ->
Math.ceil((Math.exp((level - 1)/ a) - 1) * b) if level > 1 then Math.ceil Math.exp((level - 1)/ a) * b - c else 0
level: -> level: ->
User.levelFromExp(@get('points')) User.levelFromExp(@get('points'))

View file

@ -229,25 +229,25 @@ _.extend UserSchema.properties,
levelSystemEdits: c.int() levelSystemEdits: c.int()
levelComponentEdits: c.int() levelComponentEdits: c.int()
thangTypeEdits: c.int() thangTypeEdits: c.int()
'stats.patchesSubmitted': c.int patchesSubmitted: c.int
description: 'Amount of patches submitted, not necessarily accepted' description: 'Amount of patches submitted, not necessarily accepted'
'stats.patchesContributed': c.int patchesContributed: c.int
description: 'Amount of patches submitted and accepted' description: 'Amount of patches submitted and accepted'
'stats.patchesAccepted': c.int patchesAccepted: c.int
description: 'Amount of patches accepted by the user as owner' description: 'Amount of patches accepted by the user as owner'
# The below patches only apply to those that actually got accepted # The below patches only apply to those that actually got accepted
'stats.totalTranslationPatches': c.int() totalTranslationPatches: c.int()
'stats.totalMiscPatches': c.int() totalMiscPatches: c.int()
'stats.articleTranslationPatches': c.int() articleTranslationPatches: c.int()
'stats.articleMiscPatches': c.int() articleMiscPatches: c.int()
'stats.levelTranslationPatches': c.int() levelTranslationPatches: c.int()
'stats.levelMiscPatches': c.int() levelMiscPatches: c.int()
'stats.levelComponentTranslationPatches': c.int() levelComponentTranslationPatches: c.int()
'stats.levelComponentMiscPatches': c.int() levelComponentMiscPatches: c.int()
'stats.levelSystemTranslationPatches': c.int() levelSystemTranslationPatches: c.int()
'stats.levelSystemMiscPatches': c.int() levelSystemMiscPatches: c.int()
'stats.thangTypeTranslationPatches': c.int() thangTypeTranslationPatches: c.int()
'stats.thangTypeMiscPatches': c.int() thangTypeMiscPatches: c.int()
c.extendBasicProperties UserSchema, 'user' c.extendBasicProperties UserSchema, 'user'

View file

@ -129,7 +129,8 @@
> .progress-bar-wrapper > .progress-bar-wrapper
position: absolute position: absolute
width: 331px margin-left: 12px
width: 319px
height: 20px height: 20px
z-index: 2 z-index: 2
@ -147,27 +148,27 @@
background-size: 100% 100% background-size: 100% 100%
z-index: 1 z-index: 1
.notifyjs-achievement-wood-base .notifyjs-achievement-wood-base, .achievement-wood
.achievement-icon .achievement-icon
background: url("/images/achievements/border_wood.png") no-repeat background: url("/images/achievements/border_wood.png") no-repeat
background-size: 100% 100% background-size: 100% 100%
.notifyjs-achievement-stone-base .notifyjs-achievement-stone-base, .achievement-stone
.achievement-icon .achievement-icon
background: url("/images/achievements/border_stone.png") no-repeat background: url("/images/achievements/border_stone.png") no-repeat
background-size: 100% 100% background-size: 100% 100%
.notifyjs-achievement-silver-base .notifyjs-achievement-silver-base, .achievement-silver
.achievement-icon .achievement-icon
background: url("/images/achievements/border_silver.png") no-repeat background: url("/images/achievements/border_silver.png") no-repeat
background-size: 100% 100% background-size: 100% 100%
.notifyjs-achievement-gold-base .notifyjs-achievement-gold-base, .achievement-gold
.achievement-icon .achievement-icon
background: url("/images/achievements/border_gold.png") no-repeat background: url("/images/achievements/border_gold.png") no-repeat
background-size: 100% 100% background-size: 100% 100%
.notifyjs-achievement-diamond-base .notifyjs-achievement-diamond-base, .achievement-diamond
.achievement-icon .achievement-icon
background: url("/images/achievements/border_diamond.png") no-repeat background: url("/images/achievements/border_diamond.png") no-repeat
background-size: 100% 100% background-size: 100% 100%
@ -185,3 +186,11 @@
z-index: 1000 z-index: 1000
box-shadow: 0 0 0 1px black, 0 0 0 3px lightgrey, 0 0 0 4px black box-shadow: 0 0 0 1px black, 0 0 0 3px lightgrey, 0 0 0 4px black
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif
// Achievements page
h2.achievements-category
margin-left: 20px
.table-layout
#no-achievements
margin-top: 40px

View file

@ -1,6 +1,6 @@
div div(class=notifyClass)
.clearfix.achievement-body .clearfix.achievement-body(class=locked === true ? "locked" : "")
.achievement-icon(class=locked === true ? "locked" : "", class=border) .achievement-icon
.achievement-image(data-notify-html="image") .achievement-image(data-notify-html="image")
if imgURL if imgURL
img(src=imgURL) img(src=imgURL)

View file

@ -51,7 +51,7 @@ body
.col-xs-4.text-center .col-xs-4.text-center
a(href="/user/#{me.get('slug') || me.get('_id')}") Profile a(href="/user/#{me.get('slug') || me.get('_id')}") Profile
.col-xs-4.text-center .col-xs-4.text-center
a(href="#") Stats a(href="/user/#{me.get('slug') || me.get('_id')}/stats") Stats
.col-xs-4.text-center .col-xs-4.text-center
a.disabled() Code a.disabled() Code
li.user-dropdown-footer li.user-dropdown-footer

View file

@ -3,14 +3,12 @@ extends /templates/base
// User pages might have some user page specific header, if not remove this // User pages might have some user page specific header, if not remove this
block content block content
.clearfix .clearfix
//-
if user && viewName if user && viewName
ol.breadcrumb ol.breadcrumb
li li
- var userName = user.get('name'); a(href="/user/#{user.id}") #{user.displayName()}
//_a(href="/user/#{user.id}") #{userName}
li.active li.active
//-| #{viewName} | #{viewName}
if !userLoaded if !userLoaded
| LOADING | LOADING
else if !user else if !user

View file

@ -1,12 +1,46 @@
extends /templates/kinds/user extends /templates/kinds/user
block append content block append content
if achievements .btn-group.pull-right
button#grid-layout-button.btn.btn-default(data-layout='grid', class=activeLayout==='grid' ? 'active' : '') Grid
button#table-layout-button.btn.btn-default(data-layout='table', class=activeLayout==='table' ? 'active' : '') Table
if achievementsByCategory
if activeLayout === 'grid'
.grid-layout
each achievements, category in achievementsByCategory
.row .row
h2.achievements-category=category
each achievement, index in achievements each achievement, index in achievements
- var title = achievement.get('name'); - var title = achievement.get('name');
- var description = achievement.get('description'); - var description = achievement.get('description');
- var imgURL = achievement.get('icon'); - var imgURL = achievement.getImageURL();
- var locked = ! achievement.get('unlocked'); - var locked = ! achievement.get('unlocked');
- var notifyClass = achievement.getNotifyStyle()
.col-lg-4.col-xs-12 .col-lg-4.col-xs-12
include ../achievement_notify include ../achievement_notify
else if activeLayout === 'table'
.table-layout
if earnedAchievements.length
table.table
tr
th Name
th Description
th Date
th Amount
th XP
each earnedAchievement in earnedAchievements
- var achievement = earnedAchievement.get('achievement');
tr
td= achievement.get('name')
td= achievement.get('description')
td= moment().format("MMMM Do YY", earnedAchievement.get('changed'))
if achievement.isRepeatable()
td= earnedAchievement.get('achievedAmount')
else
td
td= earnedAchievement.get('earnedPoints')
else
.panel#no-achievements
.panel-body No achievements earned yet.
else
div How did you even do that?

View file

@ -8,6 +8,8 @@ locale = require 'locale/locale'
Achievement = require '../../models/Achievement' Achievement = require '../../models/Achievement'
User = require '../../models/User' User = require '../../models/User'
utils = require 'lib/utils'
# TODO remove # TODO remove
filterKeyboardEvents = (allowedEvents, func) -> filterKeyboardEvents = (allowedEvents, func) ->
@ -39,11 +41,15 @@ module.exports = class RootView extends CocoView
totalExpNeeded = nextLevelExp - currentLevelExp totalExpNeeded = nextLevelExp - currentLevelExp
expFunction = achievement.getExpFunction() expFunction = achievement.getExpFunction()
currentExp = me.get('points') currentExp = me.get('points')
previousExp = currentExp - achievement.get('worth') if achievement.isRepeatable()
previousExp = expFunction(earnedAchievement.get('previouslyAchievedAmount')) * achievement.get('worth') if achievement.isRepeatable() achievedExp = expFunction(earnedAchievement.get('previouslyAchievedAmount')) * achievement.get('worth') if achievement.isRepeatable()
achievedExp = currentExp - previousExp else
achievedExp = achievement.get 'worth'
previousExp = currentExp - achievedExp
leveledUp = currentExp - achievedExp < currentLevelExp leveledUp = currentExp - achievedExp < currentLevelExp
console.debug 'Leveled up' if leveledUp
alreadyAchievedPercentage = 100 * (previousExp - currentLevelExp) / totalExpNeeded alreadyAchievedPercentage = 100 * (previousExp - currentLevelExp) / totalExpNeeded
alreadyAchievedPercentage = 0 if alreadyAchievedPercentage < 0 # In case of level up
newlyAchievedPercentage = if leveledUp then 100 * (currentExp - currentLevelExp) / totalExpNeeded else 100 * achievedExp / totalExpNeeded newlyAchievedPercentage = if leveledUp then 100 * (currentExp - currentLevelExp) / totalExpNeeded else 100 * achievedExp / totalExpNeeded
console.debug "Current level is #{currentLevel} (#{currentLevelExp} xp), next level is #{nextLevel} (#{nextLevelExp} xp)." console.debug "Current level is #{currentLevel} (#{currentLevelExp} xp), next level is #{nextLevel} (#{nextLevelExp} xp)."
@ -92,25 +98,23 @@ module.exports = class RootView extends CocoView
data data
showNewAchievement: (achievement, earnedAchievement) -> showNewAchievement: (achievement, earnedAchievement) ->
data = createNotifyData achievement, earnedAchievement data = @createNotifyData achievement, earnedAchievement
options = options =
autoHideDelay: 10000 autoHideDelay: 1000000
globalPosition: 'bottom right' globalPosition: 'bottom right'
showDuration: 400 showDuration: 400
style: achievement.getNotifyStyle() style: achievement.getNotifyStyle()
autoHide: true autoHide: false
clickToHide: true clickToHide: true
console.debug 'showing achievement', achievement.get 'name'
$.notify( data, options ) $.notify( data, options )
handleNewAchievements: (earnedAchievements) -> handleNewAchievements: (earnedAchievements) ->
_.each(earnedAchievements.models, (earnedAchievement) => _.each earnedAchievements.models, (earnedAchievement) =>
achievement = new Achievement(_id: earnedAchievement.get('achievement')) achievement = new Achievement(_id: earnedAchievement.get('achievement'))
console.log achievement achievement.fetch
achievement.fetch(
success: (achievement) => @showNewAchievement(achievement, earnedAchievement) success: (achievement) => @showNewAchievement(achievement, earnedAchievement)
)
)
logoutAccount: -> logoutAccount: ->
logoutUser($('#login-email').val()) logoutUser($('#login-email').val())

View file

@ -9,6 +9,12 @@ EarnedAchievementCollection = require 'collections/EarnedAchievementCollection'
module.exports = class AchievementsView extends UserView module.exports = class AchievementsView extends UserView
id: 'user-achievements-view' id: 'user-achievements-view'
template: template template: template
viewName: 'Stats'
activeLayout: 'grid'
events:
'click #grid-layout-button': 'layoutChanged'
'click #table-layout-button': 'layoutChanged'
constructor: (userID, options) -> constructor: (userID, options) ->
super options, userID super options, userID
@ -29,9 +35,20 @@ module.exports = class AchievementsView extends UserView
earned.set 'achievement', relatedAchievement earned.set 'achievement', relatedAchievement
super() super()
layoutChanged: (e) ->
@activeLayout = $(e.currentTarget).data 'layout'
@render()
getRenderData: -> getRenderData: ->
context = super() context = super()
context.activeLayout = @activeLayout
# After user is loaded
if @user and not @user.isAnonymous() if @user and not @user.isAnonymous()
context.achievements = @achievements.models
context.earnedAchievements = @earnedAchievements.models context.earnedAchievements = @earnedAchievements.models
context.achievements = @achievements.models
context.achievementsByCategory = {}
for achievement in @achievements.models
context.achievementsByCategory[achievement.get('category')] ?= []
context.achievementsByCategory[achievement.get('category')].push achievement
context context

View file

@ -19,7 +19,7 @@ achievements =
worth: 10 worth: 10
collection: 'users' collection: 'users'
userField: '_id' userField: '_id'
category: 'Miscellaneous' category: 'miscellaneous'
difficulty: 1 difficulty: 1
completedFirstLevel: completedFirstLevel:
@ -29,7 +29,17 @@ achievements =
worth: 50 worth: 50
collection: 'users' collection: 'users'
userField: '_id' userField: '_id'
category: 'Levels' category: 'levels'
difficulty: 1
completedFiveLevels:
name: 'Completed one Level'
description: 'Completed your very first level.'
query: 'stats.gamesCompleted': $gte: 1
worth: 50
collection: 'users'
userField: '_id'
category: 'levels'
difficulty: 1 difficulty: 1
simulatedBy: simulatedBy:
@ -39,7 +49,7 @@ achievements =
worth: 1 worth: 1
collection: 'users' collection: 'users'
userField: '_id' userField: '_id'
category: 'Miscellaneous' category: 'miscellaneous'
difficulty: 1 difficulty: 1
proportionalTo: 'simulatedBy' proportionalTo: 'simulatedBy'
function: function:

View file

@ -75,9 +75,9 @@ class EarnedAchievementHandler extends Handler
return doneWithAchievement() return doneWithAchievement()
finalQuery = _.clone achievement.get 'query' finalQuery = _.clone achievement.get 'query'
finalQuery.$or = [{}, {}] # Allow both ObjectIDs or hexa string IDs finalQuery.$or = [{}, {}] # Allow both ObjectIDs or hex string IDs
finalQuery.$or[0][achievement.userField] = userID finalQuery.$or[0][achievement.userField] = userID
finalQuery.$or[1][achievement.userField] = ObjectId userID finalQuery.$or[1][achievement.userField] = mongoose.Types.ObjectId userID
model.findOne finalQuery, (err, something) -> model.findOne finalQuery, (err, something) ->
return doneWithAchievement() if _.isEmpty something return doneWithAchievement() if _.isEmpty something

View file

@ -1,6 +1,7 @@
describe 'utils library', -> describe 'Utility library', ->
util = require 'lib/utils' util = require 'lib/utils'
describe 'i18n', ->
beforeEach -> beforeEach ->
this.fixture1 = this.fixture1 =
'text': 'G\'day, Wizard! Come to practice? Well, let\'s get started...' 'text': 'G\'day, Wizard! Come to practice? Well, let\'s get started...'
@ -43,3 +44,5 @@ describe 'utils library', ->
it 'i18n can fall forward if a general language is not found', -> it 'i18n can fall forward if a general language is not found', ->
expect(util.i18n(this.fixture1, 'text', 'pt')).toEqual(this.fixture1.i18n['pt-BR'].text) expect(util.i18n(this.fixture1, 'text', 'pt')).toEqual(this.fixture1.i18n['pt-BR'].text)
describe 'Miscellaneous utility', ->

View file

@ -3,7 +3,8 @@ User = require 'models/User'
describe 'UserModel', -> describe 'UserModel', ->
it 'experience functions are correct', -> it 'experience functions are correct', ->
expect(User.expForLevel(User.levelFromExp 0)).toBe 0 expect(User.expForLevel(User.levelFromExp 0)).toBe 0
expect(User.expForLevel(User.levelFromExp 50)).toBe 50 expect(User.levelFromExp User.expForLevel 1).toBe 1
expect(User.levelFromExp User.expForLevel 10).toBe 10
expect(User.expForLevel 1).toBe 0 expect(User.expForLevel 1).toBe 0
expect(User.expForLevel 2).toBeGreaterThan User.expForLevel 1 expect(User.expForLevel 2).toBeGreaterThan User.expForLevel 1
@ -13,4 +14,3 @@ describe 'UserModel', ->
me.set 'points', 50 me.set 'points', 50
expect(me.level()).toBe User.levelFromExp 50 expect(me.level()).toBe User.levelFromExp 50