mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-23 15:48:11 -05:00
Rechecked and added cool stuff for achievements
This commit is contained in:
parent
d4043ac3db
commit
871149b2bc
17 changed files with 201 additions and 112 deletions
|
@ -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"
|
||||
|
||||
|
|
|
@ -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: ->
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
a(href="/user/#{user.id}") #{user.displayName()}
|
||||
li.active
|
||||
//-| #{viewName}
|
||||
| #{viewName}
|
||||
if !userLoaded
|
||||
| LOADING
|
||||
else if !user
|
||||
|
|
|
@ -1,12 +1,46 @@
|
|||
extends /templates/kinds/user
|
||||
|
||||
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
|
||||
h2.achievements-category=category
|
||||
each achievement, index in achievements
|
||||
- var title = achievement.get('name');
|
||||
- var description = achievement.get('description');
|
||||
- var imgURL = achievement.get('icon');
|
||||
- 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?
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
describe 'utils library', ->
|
||||
describe 'Utility library', ->
|
||||
util = require 'lib/utils'
|
||||
|
||||
describe 'i18n', ->
|
||||
beforeEach ->
|
||||
this.fixture1 =
|
||||
'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', ->
|
||||
expect(util.i18n(this.fixture1, 'text', 'pt')).toEqual(this.fixture1.i18n['pt-BR'].text)
|
||||
|
||||
describe 'Miscellaneous utility', ->
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue