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'
Achievement = require 'models/Achievement'
class NewAchievementCollection extends CocoCollection
model: Achievement
initialize: (me = require('lib/auth').me) ->
@url = "/db/user/#{me.id}/achievements?notified=false"

View file

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

View file

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

View file

@ -58,9 +58,15 @@ module.exports = class SuperModel extends Backbone.Model
return res
else
@addCollection collection
@listenToOnce collection, 'sync', (c) ->
console.debug 'Registering collection', url
@registerCollection c
onCollectionSynced = (c) ->
if collection.url is c.url
console.debug 'Registering collection', url, 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.load() if not (res.isLoading or res.isLoaded)
return res

View file

@ -16,6 +16,10 @@ module.exports = class User extends CocoModel
super()
@migrateEmails()
onLoaded: ->
CocoModel.pollAchievements() # Check for achievements on login
super arguments...
isAdmin: ->
permissions = @attributes['permissions'] or []
return 'admin' in permissions
@ -121,15 +125,16 @@ module.exports = class User extends CocoModel
isEmailSubscriptionEnabled: (name) -> (@get('emails') or {})[name]?.enabled
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) ->
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) ->
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: ->
User.levelFromExp(@get('points'))

View file

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

View file

@ -129,7 +129,8 @@
> .progress-bar-wrapper
position: absolute
width: 331px
margin-left: 12px
width: 319px
height: 20px
z-index: 2
@ -147,27 +148,27 @@
background-size: 100% 100%
z-index: 1
.notifyjs-achievement-wood-base
.notifyjs-achievement-wood-base, .achievement-wood
.achievement-icon
background: url("/images/achievements/border_wood.png") no-repeat
background-size: 100% 100%
.notifyjs-achievement-stone-base
.notifyjs-achievement-stone-base, .achievement-stone
.achievement-icon
background: url("/images/achievements/border_stone.png") no-repeat
background-size: 100% 100%
.notifyjs-achievement-silver-base
.notifyjs-achievement-silver-base, .achievement-silver
.achievement-icon
background: url("/images/achievements/border_silver.png") no-repeat
background-size: 100% 100%
.notifyjs-achievement-gold-base
.notifyjs-achievement-gold-base, .achievement-gold
.achievement-icon
background: url("/images/achievements/border_gold.png") no-repeat
background-size: 100% 100%
.notifyjs-achievement-diamond-base
.notifyjs-achievement-diamond-base, .achievement-diamond
.achievement-icon
background: url("/images/achievements/border_diamond.png") no-repeat
background-size: 100% 100%
@ -185,3 +186,11 @@
z-index: 1000
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
// Achievements page
h2.achievements-category
margin-left: 20px
.table-layout
#no-achievements
margin-top: 40px

View file

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

View file

@ -51,7 +51,7 @@ body
.col-xs-4.text-center
a(href="/user/#{me.get('slug') || me.get('_id')}") Profile
.col-xs-4.text-center
a(href="#") Stats
a(href="/user/#{me.get('slug') || me.get('_id')}/stats") Stats
.col-xs-4.text-center
a.disabled() Code
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
block content
.clearfix
//-
if user && viewName
ol.breadcrumb
li
- var userName = user.get('name');
//_a(href="/user/#{user.id}") #{userName}
li.active
//-| #{viewName}
if user && viewName
ol.breadcrumb
li
a(href="/user/#{user.id}") #{user.displayName()}
li.active
| #{viewName}
if !userLoaded
| LOADING
else if !user

View file

@ -1,12 +1,46 @@
extends /templates/kinds/user
block append content
if achievements
.row
each achievement, index in achievements
- var title = achievement.get('name');
- var description = achievement.get('description');
- var imgURL = achievement.get('icon');
- var locked = ! achievement.get('unlocked');
.col-lg-4.col-xs-12
include ../achievement_notify
.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
h2.achievements-category=category
each achievement, index in achievements
- var title = achievement.get('name');
- var description = achievement.get('description');
- var imgURL = achievement.getImageURL();
- var locked = ! achievement.get('unlocked');
- var notifyClass = achievement.getNotifyStyle()
.col-lg-4.col-xs-12
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'
User = require '../../models/User'
utils = require 'lib/utils'
# TODO remove
filterKeyboardEvents = (allowedEvents, func) ->
@ -39,11 +41,15 @@ module.exports = class RootView extends CocoView
totalExpNeeded = nextLevelExp - currentLevelExp
expFunction = achievement.getExpFunction()
currentExp = me.get('points')
previousExp = currentExp - achievement.get('worth')
previousExp = expFunction(earnedAchievement.get('previouslyAchievedAmount')) * achievement.get('worth') if achievement.isRepeatable()
achievedExp = currentExp - previousExp
if achievement.isRepeatable()
achievedExp = expFunction(earnedAchievement.get('previouslyAchievedAmount')) * achievement.get('worth') if achievement.isRepeatable()
else
achievedExp = achievement.get 'worth'
previousExp = currentExp - achievedExp
leveledUp = currentExp - achievedExp < currentLevelExp
console.debug 'Leveled up' if leveledUp
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
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
showNewAchievement: (achievement, earnedAchievement) ->
data = createNotifyData achievement, earnedAchievement
data = @createNotifyData achievement, earnedAchievement
options =
autoHideDelay: 10000
autoHideDelay: 1000000
globalPosition: 'bottom right'
showDuration: 400
style: achievement.getNotifyStyle()
autoHide: true
autoHide: false
clickToHide: true
console.debug 'showing achievement', achievement.get 'name'
$.notify( data, options )
handleNewAchievements: (earnedAchievements) ->
_.each(earnedAchievements.models, (earnedAchievement) =>
_.each earnedAchievements.models, (earnedAchievement) =>
achievement = new Achievement(_id: earnedAchievement.get('achievement'))
console.log achievement
achievement.fetch(
achievement.fetch
success: (achievement) => @showNewAchievement(achievement, earnedAchievement)
)
)
logoutAccount: ->
logoutUser($('#login-email').val())

View file

@ -9,6 +9,12 @@ EarnedAchievementCollection = require 'collections/EarnedAchievementCollection'
module.exports = class AchievementsView extends UserView
id: 'user-achievements-view'
template: template
viewName: 'Stats'
activeLayout: 'grid'
events:
'click #grid-layout-button': 'layoutChanged'
'click #table-layout-button': 'layoutChanged'
constructor: (userID, options) ->
super options, userID
@ -29,9 +35,20 @@ module.exports = class AchievementsView extends UserView
earned.set 'achievement', relatedAchievement
super()
layoutChanged: (e) ->
@activeLayout = $(e.currentTarget).data 'layout'
@render()
getRenderData: ->
context = super()
context.activeLayout = @activeLayout
# After user is loaded
if @user and not @user.isAnonymous()
context.achievements = @achievements.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

View file

@ -19,7 +19,7 @@ achievements =
worth: 10
collection: 'users'
userField: '_id'
category: 'Miscellaneous'
category: 'miscellaneous'
difficulty: 1
completedFirstLevel:
@ -29,7 +29,17 @@ achievements =
worth: 50
collection: 'users'
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
simulatedBy:
@ -39,7 +49,7 @@ achievements =
worth: 1
collection: 'users'
userField: '_id'
category: 'Miscellaneous'
category: 'miscellaneous'
difficulty: 1
proportionalTo: 'simulatedBy'
function:

View file

@ -75,9 +75,9 @@ class EarnedAchievementHandler extends Handler
return doneWithAchievement()
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[1][achievement.userField] = ObjectId userID
finalQuery.$or[1][achievement.userField] = mongoose.Types.ObjectId userID
model.findOne finalQuery, (err, something) ->
return doneWithAchievement() if _.isEmpty something

View file

@ -1,45 +1,48 @@
describe 'utils library', ->
describe 'Utility library', ->
util = require 'lib/utils'
beforeEach ->
this.fixture1 =
'text': 'G\'day, Wizard! Come to practice? Well, let\'s get started...'
'blurb': 'G\'day'
'i18n':
'es-419':
'text': '¡Buenas, Hechicero! ¿Vienes a practicar? Bueno, empecemos...'
'es-ES':
'text': '¡Buenas Mago! ¿Vienes a practicar? Bien, empecemos...'
'es':
'text': '¡Buenas Mago! ¿Vienes a practicar? Muy bien, empecemos...'
'fr':
'text': 'S\'lut, Magicien! Venu pratiquer? Ok, bien débutons...'
'pt-BR':
'text': 'Bom dia, feiticeiro! Veio praticar? Então vamos começar...'
'en':
'text': 'Ohai Magician!'
'de':
'text': '\'N Tach auch, Zauberer! Kommst Du zum Üben? Dann lass uns anfangen...'
'sv':
'text': 'Godagens, trollkarl! Kommit för att öva? Nå, låt oss börja...'
describe 'i18n', ->
beforeEach ->
this.fixture1 =
'text': 'G\'day, Wizard! Come to practice? Well, let\'s get started...'
'blurb': 'G\'day'
'i18n':
'es-419':
'text': '¡Buenas, Hechicero! ¿Vienes a practicar? Bueno, empecemos...'
'es-ES':
'text': '¡Buenas Mago! ¿Vienes a practicar? Bien, empecemos...'
'es':
'text': '¡Buenas Mago! ¿Vienes a practicar? Muy bien, empecemos...'
'fr':
'text': 'S\'lut, Magicien! Venu pratiquer? Ok, bien débutons...'
'pt-BR':
'text': 'Bom dia, feiticeiro! Veio praticar? Então vamos começar...'
'en':
'text': 'Ohai Magician!'
'de':
'text': '\'N Tach auch, Zauberer! Kommst Du zum Üben? Dann lass uns anfangen...'
'sv':
'text': 'Godagens, trollkarl! Kommit för att öva? Nå, låt oss börja...'
it 'i18n should find a valid target string', ->
expect(util.i18n(this.fixture1, 'text', 'sv')).toEqual(this.fixture1.i18n['sv'].text)
expect(util.i18n(this.fixture1, 'text', 'es-ES')).toEqual(this.fixture1.i18n['es-ES'].text)
it 'i18n should find a valid target string', ->
expect(util.i18n(this.fixture1, 'text', 'sv')).toEqual(this.fixture1.i18n['sv'].text)
expect(util.i18n(this.fixture1, 'text', 'es-ES')).toEqual(this.fixture1.i18n['es-ES'].text)
it 'i18n picks the correct fallback for a specific language', ->
expect(util.i18n(this.fixture1, 'text', 'fr-be')).toEqual(this.fixture1.i18n['fr'].text)
it 'i18n picks the correct fallback for a specific language', ->
expect(util.i18n(this.fixture1, 'text', 'fr-be')).toEqual(this.fixture1.i18n['fr'].text)
it 'i18n picks the correct fallback', ->
expect(util.i18n(this.fixture1, 'text', 'nl')).toEqual(this.fixture1.i18n['en'].text)
expect(util.i18n(this.fixture1, 'text', 'nl', 'de')).toEqual(this.fixture1.i18n['de'].text)
it 'i18n picks the correct fallback', ->
expect(util.i18n(this.fixture1, 'text', 'nl')).toEqual(this.fixture1.i18n['en'].text)
expect(util.i18n(this.fixture1, 'text', 'nl', 'de')).toEqual(this.fixture1.i18n['de'].text)
it 'i18n falls back to the default text, even for other targets (like blurb)', ->
delete this.fixture1.i18n['en']
expect(util.i18n(this.fixture1, 'text', 'en')).toEqual(this.fixture1.text)
expect(util.i18n(this.fixture1, 'blurb', 'en')).toEqual(this.fixture1.blurb)
delete this.fixture1.blurb
expect(util.i18n(this.fixture1, 'blurb', 'en')).toEqual(null)
it 'i18n falls back to the default text, even for other targets (like blurb)', ->
delete this.fixture1.i18n['en']
expect(util.i18n(this.fixture1, 'text', 'en')).toEqual(this.fixture1.text)
expect(util.i18n(this.fixture1, 'blurb', 'en')).toEqual(this.fixture1.blurb)
delete this.fixture1.blurb
expect(util.i18n(this.fixture1, 'blurb', 'en')).toEqual(null)
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)
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)
describe 'Miscellaneous utility', ->

View file

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