Merge branch 'rubenvereecken-achievements'
|
@ -1,12 +0,0 @@
|
|||
// Fixtures
|
||||
|
||||
db.achievements.insert({
|
||||
query: '{"level.original": "52d97ecd32362bc86e004e87"}',
|
||||
index: true,
|
||||
slug: 'dungeon-arena-started',
|
||||
name: 'Dungeon Arena started',
|
||||
worth: 1,
|
||||
collection: 'level.session',
|
||||
description: 'Started playing Dungeon Arena.',
|
||||
userField: 'creator'
|
||||
});
|
|
@ -18,9 +18,10 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
|
||||
'about': go('AboutView')
|
||||
|
||||
'account/profile(/:userID)': go('account/JobProfileView')
|
||||
'account': go('account/MainAccountView')
|
||||
'account/settings': go('account/AccountSettingsView')
|
||||
'account/unsubscribe': go('account/UnsubscribeView')
|
||||
#'account/payment'
|
||||
|
||||
'admin': go('admin/MainAdminView')
|
||||
'admin/candidates': go('admin/CandidatesView')
|
||||
|
@ -50,7 +51,7 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
'editor': go('editor/MainEditorView')
|
||||
|
||||
'editor/achievement': go('editor/achievement/AchievementSearchView')
|
||||
'editor/achievement': go('editor/achievement/AchievementEditView')
|
||||
'editor/achievement/:articleID': go('editor/achievement/AchievementEditView')
|
||||
'editor/article': go('editor/article/ArticleSearchView')
|
||||
'editor/article/preview': go('editor/article/ArticlePreviewView')
|
||||
'editor/article/:articleID': go('editor/article/ArticleEditView')
|
||||
|
@ -79,6 +80,11 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
|
||||
'test(/*subpath)': go('TestView')
|
||||
|
||||
'user/:slugOrID': go('user/MainUserView')
|
||||
'user/:slugOrID/stats': go('user/AchievementsView')
|
||||
'user/:slugOrID/profile': go('user/JobProfileView')
|
||||
#'user/:slugOrID/code': go('user/CodeView')
|
||||
|
||||
'*name': 'showNotFoundView'
|
||||
|
||||
routeToServer: (e) ->
|
||||
|
|
|
@ -5,7 +5,6 @@ locale = require 'locale/locale'
|
|||
{me} = require 'lib/auth'
|
||||
Tracker = require 'lib/Tracker'
|
||||
CocoView = require 'views/kinds/CocoView'
|
||||
AchievementNotify = require '../../templates/achievement_notify'
|
||||
|
||||
marked.setOptions {gfm: true, sanitize: true, smartLists: true, breaks: false}
|
||||
|
||||
|
@ -40,7 +39,6 @@ Application = initialize: ->
|
|||
@facebookHandler = new FacebookHandler()
|
||||
@gplusHandler = new GPlusHandler()
|
||||
$(document).bind 'keydown', preventBackspace
|
||||
$.notify.addStyle 'achievement', html: $(AchievementNotify())
|
||||
@linkedinHandler = new LinkedInHandler()
|
||||
preload(COMMON_FILES)
|
||||
$.i18n.init {
|
||||
|
|
6
app/collections/AchievementCollection.coffee
Normal file
|
@ -0,0 +1,6 @@
|
|||
CocoCollection = require 'collections/CocoCollection'
|
||||
Achievement = require 'models/Achievement'
|
||||
|
||||
module.exports = class AchievementCollection extends CocoCollection
|
||||
url: '/db/achievement'
|
||||
model: Achievement
|
9
app/collections/EarnedAchievementCollection.coffee
Normal file
|
@ -0,0 +1,9 @@
|
|||
CocoCollection = require 'collections/CocoCollection'
|
||||
EarnedAchievement = require 'models/EarnedAchievement'
|
||||
|
||||
module.exports = class EarnedAchievementCollection extends CocoCollection
|
||||
model: EarnedAchievement
|
||||
|
||||
initialize: (userID) ->
|
||||
@url = "/db/user/#{userID}/achievements"
|
||||
super()
|
|
@ -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"
|
||||
|
||||
|
|
9
app/collections/RecentlyPlayedCollection.coffee
Normal file
|
@ -0,0 +1,9 @@
|
|||
CocoCollection = require './CocoCollection'
|
||||
LevelSession = require 'models/LevelSession'
|
||||
|
||||
module.exports = class RecentlyPlayedCollection extends CocoCollection
|
||||
model: LevelSession
|
||||
|
||||
constructor: (userID, options) ->
|
||||
@url = "/db/user/#{userID}/recently_played"
|
||||
super options
|
10
app/collections/RelatedAchievementsCollection.coffee
Normal file
|
@ -0,0 +1,10 @@
|
|||
CocoCollection = require 'collections/CocoCollection'
|
||||
Achievement = require 'models/Achievement'
|
||||
|
||||
class RelatedAchievementCollection extends CocoCollection
|
||||
model: Achievement
|
||||
|
||||
initialize: (relatedID) ->
|
||||
@url = "/db/achievement?related=#{relatedID}"
|
||||
|
||||
module.exports = RelatedAchievementCollection
|
|
@ -1,6 +1,7 @@
|
|||
Bus = require './Bus'
|
||||
{me} = require 'lib/auth'
|
||||
LevelSession = require 'models/LevelSession'
|
||||
utils = require 'lib/utils'
|
||||
|
||||
module.exports = class LevelBus extends Bus
|
||||
|
||||
|
@ -22,6 +23,7 @@ module.exports = class LevelBus extends Bus
|
|||
'tome:spell-changed': 'onSpellChanged'
|
||||
'tome:spell-created': 'onSpellCreated'
|
||||
'application:idle-changed': 'onIdleChanged'
|
||||
'goal-manager:new-goal-states': 'onNewGoalStates'
|
||||
|
||||
constructor: ->
|
||||
super(arguments...)
|
||||
|
@ -192,6 +194,14 @@ module.exports = class LevelBus extends Bus
|
|||
@changedSessionProperties.state = true
|
||||
@saveSession()
|
||||
|
||||
onNewGoalStates: ({goalStates})->
|
||||
state = @session.get 'state'
|
||||
unless utils.kindaEqual state.goalStates, goalStates # Only save when goals really change
|
||||
state.goalStates = goalStates
|
||||
@session.set 'state', state
|
||||
@changedSessionProperties.state = true
|
||||
@saveSession()
|
||||
|
||||
onPlayerJoined: (snapshot) =>
|
||||
super(arguments...)
|
||||
return unless @onPoint()
|
||||
|
|
|
@ -24,6 +24,7 @@ doQuerySelector = (value, operatorObj) ->
|
|||
|
||||
matchesQuery = (target, queryObj) ->
|
||||
return true unless queryObj
|
||||
throw new Error 'Expected an object to match a query against, instead got null' unless target
|
||||
for prop, query of queryObj
|
||||
if prop[0] == '$'
|
||||
switch prop
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
CocoClass = require 'lib/CocoClass'
|
||||
CocoClass = require './CocoClass'
|
||||
|
||||
namesCache = {}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
SystemNameLoader = require 'lib/SystemNameLoader'
|
||||
SystemNameLoader = require './SystemNameLoader'
|
||||
###
|
||||
Good-to-knows:
|
||||
dataPath: an array of keys that walks you up a JSON object that's being patched
|
||||
|
@ -12,7 +12,7 @@ module.exports.expandDelta = (delta, left, schema) ->
|
|||
(expandFlattenedDelta(fd, left, schema) for fd in flattenedDeltas)
|
||||
|
||||
|
||||
flattenDelta = (delta, dataPath=null, deltaPath=null) ->
|
||||
module.exports.flattenDelta = flattenDelta = (delta, dataPath=null, deltaPath=null) ->
|
||||
# takes a single jsondiffpatch delta and returns an array of objects with
|
||||
return [] unless delta
|
||||
dataPath ?= []
|
||||
|
@ -175,3 +175,5 @@ prunePath = (delta, path) ->
|
|||
prunePath delta[path[0]], path.slice(1) unless delta[path[0]] is undefined
|
||||
keys = (k for k in _.keys(delta[path[0]]) when k isnt '_t')
|
||||
delete delta[path[0]] if keys.length is 0
|
||||
|
||||
|
||||
|
|
|
@ -70,6 +70,7 @@ module.exports.i18n = (say, target, language=me.lang(), fallback='en') ->
|
|||
null
|
||||
|
||||
module.exports.getByPath = (target, path) ->
|
||||
throw new Error 'Expected an object to match a query against, instead got null' unless target
|
||||
pieces = path.split('.')
|
||||
obj = target
|
||||
for piece in pieces
|
||||
|
@ -82,7 +83,7 @@ module.exports.isID = (id) -> _.isString(id) and id.length is 24 and id.match(/[
|
|||
module.exports.round = _.curry (digits, n) ->
|
||||
n = +n.toFixed(digits)
|
||||
|
||||
positify = (func) -> (x) -> if x > 0 then func(x) else 0
|
||||
positify = (func) -> (params) -> (x) -> if x > 0 then func(params)(x) else 0
|
||||
|
||||
# f(x) = ax + b
|
||||
createLinearFunc = (params) ->
|
||||
|
@ -100,3 +101,32 @@ module.exports.functionCreators =
|
|||
linear: positify(createLinearFunc)
|
||||
quadratic: positify(createQuadraticFunc)
|
||||
logarithmic: positify(createLogFunc)
|
||||
|
||||
# Call done with true to satisfy the 'until' goal and stop repeating func
|
||||
module.exports.keepDoingUntil = (func, wait=100, totalWait=5000) ->
|
||||
waitSoFar = 0
|
||||
(done = (success) ->
|
||||
if (waitSoFar += wait) <= totalWait and not success
|
||||
_.delay (-> func done), wait) false
|
||||
|
||||
module.exports.grayscale = (imageData) ->
|
||||
d = imageData.data
|
||||
for i in [0..d.length] by 4
|
||||
r = d[i]
|
||||
g = d[i+1]
|
||||
b = d[i+2]
|
||||
v = 0.2126*r + 0.7152*g + 0.0722*b
|
||||
d[i] = d[i+1] = d[i+2] = v
|
||||
imageData
|
||||
|
||||
# Deep compares l with r, with the exception that undefined values are considered equal to missing values
|
||||
# Very practical for comparing Mongoose documents where undefined is not allowed, instead fields get deleted
|
||||
module.exports.kindaEqual = compare = (l, r) ->
|
||||
if _.isObject(l) and _.isObject(r)
|
||||
for key in _.union Object.keys(l), Object.keys(r)
|
||||
return false unless compare l[key], r[key]
|
||||
return true
|
||||
else if l is r
|
||||
return true
|
||||
else
|
||||
return false
|
||||
|
|
|
@ -49,6 +49,9 @@
|
|||
blog: "Blog"
|
||||
forum: "Forum"
|
||||
account: "Account"
|
||||
profile: "Profile"
|
||||
stats: "Stats"
|
||||
code: "Code"
|
||||
admin: "Admin"
|
||||
home: "Home"
|
||||
contribute: "Contribute"
|
||||
|
@ -176,12 +179,14 @@
|
|||
new_password: "New Password"
|
||||
new_password_verify: "Verify"
|
||||
email_subscriptions: "Email Subscriptions"
|
||||
email_subscriptions_none: "No Email Subscriptions."
|
||||
email_announcements: "Announcements"
|
||||
email_announcements_description: "Get emails on the latest news and developments at CodeCombat."
|
||||
email_notifications: "Notifications"
|
||||
email_notifications_summary: "Controls for personalized, automatic email notifications related to your CodeCombat activity."
|
||||
email_any_notes: "Any Notifications"
|
||||
email_any_notes_description: "Disable to stop all activity notification emails."
|
||||
email_news: "News"
|
||||
email_recruit_notes: "Job Opportunities"
|
||||
email_recruit_notes_description: "If you play really well, we may contact you about getting you a (better) job."
|
||||
contributor_emails: "Contributor Class Emails"
|
||||
|
@ -591,6 +596,10 @@
|
|||
level_search_title: "Search Levels Here"
|
||||
achievement_search_title: "Search Achievements"
|
||||
read_only_warning2: "Note: you can't save any edits here, because you're not logged in."
|
||||
no_achievements: "No achievements have been added for this level yet."
|
||||
achievement_query_misc: "Key achievement off of miscellanea"
|
||||
achievement_query_goals: "Key achievement off of level goals"
|
||||
level_completion: "Level Completion"
|
||||
|
||||
article:
|
||||
edit_btn_preview: "Preview"
|
||||
|
@ -599,6 +608,7 @@
|
|||
general:
|
||||
and: "and"
|
||||
name: "Name"
|
||||
date: "Date"
|
||||
body: "Body"
|
||||
version: "Version"
|
||||
commit_msg: "Commit Message"
|
||||
|
@ -938,3 +948,38 @@
|
|||
text_diff: "Text Diff"
|
||||
merge_conflict_with: "MERGE CONFLICT WITH"
|
||||
no_changes: "No Changes"
|
||||
|
||||
user:
|
||||
stats: "Stats"
|
||||
singleplayer_title: "Singleplayer Levels"
|
||||
multiplayer_title: "Multiplayer Levels"
|
||||
achievements_title: "Achievements"
|
||||
last_played: "Last Played"
|
||||
status: "Status"
|
||||
status_completed: "Completed"
|
||||
status_unfinished: "Unfinished"
|
||||
no_singleplayer: "No Singleplayer games played yet."
|
||||
no_multiplayer: "No Multiplayer games played yet."
|
||||
no_achievements: "No Achievements earned yet."
|
||||
favorite_prefix: "Favorite language is "
|
||||
favorite_postfix: "."
|
||||
|
||||
achievements:
|
||||
last_earned: "Last Earned"
|
||||
amount_achieved: "Amount"
|
||||
achievement: "Achievement"
|
||||
category_contributor: "Contributor"
|
||||
category_miscellaneous: "Miscellaneous"
|
||||
category_levels: "Levels"
|
||||
category_undefined: "Uncategorized"
|
||||
current_xp_prefix: ""
|
||||
current_xp_postfix: " in total"
|
||||
new_xp_prefix: ""
|
||||
new_xp_postfix: " earned"
|
||||
left_xp_prefix: ""
|
||||
left_xp_infix: " until level "
|
||||
left_xp_postfix: ""
|
||||
|
||||
account:
|
||||
recently_played: "Recently Played"
|
||||
no_recent_games: "No games played during the past two weeks."
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
CocoModel = require './CocoModel'
|
||||
util = require '../lib/utils'
|
||||
utils = require '../lib/utils'
|
||||
|
||||
module.exports = class Achievement extends CocoModel
|
||||
@className: 'Achievement'
|
||||
|
@ -11,6 +11,47 @@ module.exports = class Achievement extends CocoModel
|
|||
|
||||
# TODO logic is duplicated in Mongoose Achievement schema
|
||||
getExpFunction: ->
|
||||
kind = @get('function')?.kind or @schema.function.default.kind
|
||||
parameters = @get('function')?.parameters or @schema.function.default.parameters
|
||||
kind = @get('function')?.kind or jsonschema.properties.function.default.kind
|
||||
parameters = @get('function')?.parameters or jsonschema.properties.function.default.parameters
|
||||
return utils.functionCreators[kind](parameters) if kind of utils.functionCreators
|
||||
|
||||
@styleMapping:
|
||||
1: 'achievement-wood'
|
||||
2: 'achievement-stone'
|
||||
3: 'achievement-silver'
|
||||
4: 'achievement-gold'
|
||||
5: 'achievement-diamond'
|
||||
|
||||
getStyle: -> Achievement.styleMapping[@get 'difficulty']
|
||||
|
||||
@defaultImageURL: '/images/achievements/default.png'
|
||||
|
||||
getImageURL: ->
|
||||
if @get 'icon' then '/file/' + @get('icon') else Achievement.defaultImageURL
|
||||
|
||||
hasImage: -> @get('icon')?
|
||||
|
||||
# TODO Could cache the default icon separately
|
||||
cacheLockedImage: ->
|
||||
return @lockedImageURL if @lockedImageURL
|
||||
canvas = document.createElement 'canvas'
|
||||
image = new Image
|
||||
image.src = @getImageURL()
|
||||
defer = $.Deferred()
|
||||
image.onload = =>
|
||||
canvas.width = image.width
|
||||
canvas.height = image.height
|
||||
context = canvas.getContext '2d'
|
||||
context.drawImage image, 0, 0
|
||||
imgData = context.getImageData 0, 0, canvas.width, canvas.height
|
||||
imgData = utils.grayscale imgData
|
||||
context.putImageData imgData, 0, 0
|
||||
@lockedImageURL = canvas.toDataURL()
|
||||
defer.resolve @lockedImageURL
|
||||
defer
|
||||
|
||||
getLockedImageURL: -> @lockedImageURL
|
||||
|
||||
i18nName: -> utils.i18n @attributes, 'name'
|
||||
|
||||
i18nDescription: -> utils.i18n @attributes, 'description'
|
||||
|
|
|
@ -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
|
||||
|
@ -301,11 +299,13 @@ 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
|
||||
achievements = new NewAchievementCollection
|
||||
achievements.fetch(
|
||||
achievements.fetch
|
||||
success: (collection) ->
|
||||
me.fetch (success: -> Backbone.Mediator.publish('achievements:new', collection)) unless _.isEmpty(collection.models)
|
||||
)
|
||||
error: ->
|
||||
console.error 'Miserably failed to fetch unnotified achievements', arguments
|
||||
|
||||
CocoModel.pollAchievements = _.debounce CocoModel.pollAchievements, 500
|
||||
|
||||
|
|
7
app/models/EarnedAchievement.coffee
Normal file
|
@ -0,0 +1,7 @@
|
|||
CocoModel = require './CocoModel'
|
||||
utils = require '../lib/utils'
|
||||
|
||||
module.exports = class EarnedAchievement extends CocoModel
|
||||
@className: 'EarnedAchievement'
|
||||
@schema: require 'schemas/models/earned_achievement'
|
||||
urlRoot: '/db/earnedachievement'
|
|
@ -36,3 +36,9 @@ module.exports = class LevelSession extends CocoModel
|
|||
spell = item[1]
|
||||
return true if c1[thang][spell] isnt c2[thang]?[spell]
|
||||
false
|
||||
|
||||
isMultiplayer: ->
|
||||
@get('team')? # Only multiplayer level sessions have teams defined
|
||||
|
||||
completed: ->
|
||||
@get('state')?.complete || false
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -9,14 +9,24 @@ module.exports = class User extends CocoModel
|
|||
urlRoot: '/db/user'
|
||||
notyErrors: false
|
||||
|
||||
defaults:
|
||||
points: 0
|
||||
|
||||
initialize: ->
|
||||
super()
|
||||
@migrateEmails()
|
||||
|
||||
onLoaded: ->
|
||||
CocoModel.pollAchievements() # Check for achievements on login
|
||||
super arguments...
|
||||
|
||||
isAdmin: ->
|
||||
permissions = @attributes['permissions'] or []
|
||||
return 'admin' in permissions
|
||||
|
||||
isAnonymous: ->
|
||||
@get 'anonymous'
|
||||
|
||||
displayName: ->
|
||||
@get('name') or 'Anoner'
|
||||
|
||||
|
@ -32,47 +42,13 @@ module.exports = class User extends CocoModel
|
|||
return "/file/#{photoURL}#{prefix}s=#{size}"
|
||||
return "/db/user/#{@id}/avatar?s=#{size}&employerPageAvatar=#{useEmployerPageAvatar}"
|
||||
|
||||
@getByID = (id, properties, force) ->
|
||||
{me} = require 'lib/auth'
|
||||
return me if me.id is id
|
||||
user = cache[id] or new module.exports({_id: id})
|
||||
if force or not cache[id]
|
||||
user.loading = true
|
||||
user.fetch(
|
||||
success: ->
|
||||
user.loading = false
|
||||
Backbone.Mediator.publish('user:fetched')
|
||||
#user.trigger 'sync' # needed?
|
||||
)
|
||||
cache[id] = user
|
||||
user
|
||||
getSlugOrID: -> @get('slug') or @get('_id')
|
||||
|
||||
set: ->
|
||||
if arguments[0] is 'jobProfileApproved' and @get("jobProfileApproved") is false and not @get("jobProfileApprovedDate")
|
||||
@set "jobProfileApprovedDate", (new Date()).toISOString()
|
||||
super arguments...
|
||||
|
||||
# callbacks can be either success or error
|
||||
@getByIDOrSlug: (idOrSlug, force, callbacks={}) ->
|
||||
{me} = require 'lib/auth'
|
||||
isID = util.isID idOrSlug
|
||||
if me.id is idOrSlug or me.slug is idOrSlug
|
||||
callbacks.success me if callbacks.success?
|
||||
return me
|
||||
cached = cache[idOrSlug]
|
||||
user = cached or new @ _id: idOrSlug
|
||||
if force or not cached
|
||||
user.loading = true
|
||||
user.fetch
|
||||
success: ->
|
||||
user.loading = false
|
||||
Backbone.Mediator.publish 'user:fetched'
|
||||
callbacks.success user if callbacks.success?
|
||||
error: ->
|
||||
user.loading = false
|
||||
callbacks.error user if callbacks.error?
|
||||
cache[idOrSlug] = user
|
||||
user
|
||||
|
||||
@getUnconflictedName: (name, done) ->
|
||||
$.ajax "/auth/name/#{name}",
|
||||
success: (data) -> done data.name
|
||||
|
@ -111,19 +87,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'))
|
||||
|
||||
levelFromExp: (xp) -> User.levelFromExp(xp)
|
||||
|
||||
expForLevel: (level) -> User.expForLevel(level)
|
||||
|
|
|
@ -24,8 +24,9 @@ MongoFindQuerySchema =
|
|||
'^[-a-zA-Z0-9\.]*$':
|
||||
oneOf: [
|
||||
#{$ref: '#/definitions/' + MongoQueryOperatorSchema.id},
|
||||
{type: 'string'},
|
||||
{type: 'string'}
|
||||
{type: 'object'}
|
||||
{type: 'boolean'}
|
||||
]
|
||||
additionalProperties: true # TODO make Treema accept new pattern matched keys
|
||||
definitions: {}
|
||||
|
@ -34,36 +35,58 @@ MongoFindQuerySchema.definitions[MongoQueryOperatorSchema.id] = MongoQueryOperat
|
|||
|
||||
AchievementSchema = c.object()
|
||||
c.extendNamedProperties AchievementSchema
|
||||
c.extendBasicProperties AchievementSchema, 'article'
|
||||
c.extendBasicProperties AchievementSchema, 'achievement'
|
||||
c.extendSearchableProperties AchievementSchema
|
||||
|
||||
_.extend(AchievementSchema.properties,
|
||||
_.extend AchievementSchema.properties,
|
||||
query:
|
||||
#type:'object'
|
||||
$ref: '#/definitions/' + MongoFindQuerySchema.id
|
||||
worth: {type: 'number'}
|
||||
worth: c.float
|
||||
default: 10
|
||||
collection: {type: 'string'}
|
||||
description: {type: 'string'}
|
||||
userField: {type: 'string'}
|
||||
description: c.shortString
|
||||
default: 'Probably the coolest you\'ll ever get.'
|
||||
userField: c.shortString()
|
||||
related: c.objectId(description: 'Related entity')
|
||||
icon: {type: 'string', format: 'image-file', title: 'Icon'}
|
||||
category:
|
||||
enum: ['level', 'ladder', 'contributor']
|
||||
description: 'For categorizing and display purposes'
|
||||
difficulty: c.int
|
||||
description: 'The higher the more difficult'
|
||||
default: 1
|
||||
proportionalTo:
|
||||
type: 'string'
|
||||
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 elligible for recalculation.'
|
||||
default: true
|
||||
function:
|
||||
type: 'object'
|
||||
description: 'Function that gives total experience for X amount achieved'
|
||||
properties:
|
||||
kind: {enum: ['linear', 'logarithmic'], default: 'linear'}
|
||||
kind: {enum: ['linear', 'logarithmic', 'quadratic'], default: 'linear'}
|
||||
parameters:
|
||||
type: 'object'
|
||||
properties:
|
||||
a: {type: 'number', default: 1}
|
||||
b: {type: 'number', default: 1}
|
||||
c: {type: 'number', default: 1}
|
||||
additionalProperties: true
|
||||
default: {kind: 'linear', parameters: a: 1}
|
||||
required: ['kind', 'parameters']
|
||||
additionalProperties: false
|
||||
)
|
||||
i18n: c.object
|
||||
format: 'i18n'
|
||||
props: ['name', 'description']
|
||||
description: 'Help translate this achievement'
|
||||
|
||||
_.extend AchievementSchema, # Let's have these on the bottom
|
||||
# TODO We really need some required properties in my opinion but this makes creating new achievements impossible as it is now
|
||||
#required: ['name', 'description', 'query', 'worth', 'collection', 'userField', 'category', 'difficulty']
|
||||
additionalProperties: false
|
||||
|
||||
AchievementSchema.definitions = {}
|
||||
AchievementSchema.definitions[MongoFindQuerySchema.id] = MongoFindQuerySchema
|
||||
|
|
|
@ -101,6 +101,14 @@ _.extend LevelSessionSchema.properties,
|
|||
type: 'object'
|
||||
source:
|
||||
type: 'string'
|
||||
goalStates:
|
||||
type: 'object'
|
||||
description: 'Maps Goal ID on a goal state object'
|
||||
additionalProperties:
|
||||
title: 'Goal State'
|
||||
type: 'object'
|
||||
properties:
|
||||
status: enum: ['failure', 'incomplete', 'success']
|
||||
|
||||
code:
|
||||
type: 'object'
|
||||
|
|
|
@ -20,6 +20,9 @@ PatchSchema = c.object({title: 'Patch', required: ['target', 'delta', 'commitMes
|
|||
major: {type: 'number', minimum: 0}
|
||||
minor: {type: 'number', minimum: 0}
|
||||
})
|
||||
|
||||
wasPending: type: 'boolean'
|
||||
newlyAccepted: type: 'boolean'
|
||||
})
|
||||
|
||||
c.extendBasicProperties(PatchSchema, 'patch')
|
||||
|
|
|
@ -222,6 +222,33 @@ _.extend UserSchema.properties,
|
|||
|
||||
points: {type: 'number'}
|
||||
activity: {type: 'object', description: 'Summary statistics about user activity', additionalProperties: c.activity}
|
||||
stats: c.object {additionalProperties: false},
|
||||
gamesCompleted: c.int()
|
||||
articleEdits: c.int()
|
||||
levelEdits: c.int()
|
||||
levelSystemEdits: c.int()
|
||||
levelComponentEdits: c.int()
|
||||
thangTypeEdits: c.int()
|
||||
patchesSubmitted: c.int
|
||||
description: 'Amount of patches submitted, not necessarily accepted'
|
||||
patchesContributed: c.int
|
||||
description: 'Amount of patches submitted and accepted'
|
||||
patchesAccepted: c.int
|
||||
description: 'Amount of patches accepted by the user as owner'
|
||||
# The below patches only apply to those that actually got accepted
|
||||
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'
|
||||
|
||||
|
|
|
@ -19,6 +19,8 @@ me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ex
|
|||
# should just be string (Mongo ID), but sometimes mongoose turns them into objects representing those, so we are lenient
|
||||
me.objectId = (ext) -> schema = combine({type: ['object', 'string']}, ext)
|
||||
me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext)
|
||||
me.int = (ext) -> combine {type: 'integer'}, ext
|
||||
me.float = (ext) -> combine {type: 'number'}, ext
|
||||
|
||||
PointSchema = me.object {title: 'Point', description: 'An {x, y} coordinate point.', format: 'point2d', required: ['x', 'y']},
|
||||
x: {title: 'x', description: 'The x coordinate.', type: 'number', 'default': 15}
|
||||
|
|
32
app/styles/account/account_home.sass
Normal file
|
@ -0,0 +1,32 @@
|
|||
@import "../bootstrap/variables"
|
||||
@import "../bootstrap/mixins"
|
||||
|
||||
#account-home
|
||||
dl
|
||||
margin-bottom: 0px
|
||||
|
||||
img#picture
|
||||
max-width: 100%
|
||||
|
||||
.panel
|
||||
margin-bottom: 10px
|
||||
|
||||
h2
|
||||
margin-bottom: 0px
|
||||
|
||||
a
|
||||
font-size: 28px
|
||||
margin-left: 5px
|
||||
|
||||
.panel-title > a
|
||||
margin-left: 5px
|
||||
color: rgb(11, 99, 188)
|
||||
|
||||
.panel-me
|
||||
td
|
||||
padding-left: 15px
|
||||
|
||||
.panel-emails
|
||||
h4
|
||||
font-family: $font-family-base
|
||||
|
243
app/styles/achievements.sass
Normal file
|
@ -0,0 +1,243 @@
|
|||
@import 'bootstrap/variables'
|
||||
|
||||
.achievement-body
|
||||
position: relative
|
||||
|
||||
.achievement-icon
|
||||
position: absolute
|
||||
|
||||
.achievement-image
|
||||
width: 100%
|
||||
height: 100%
|
||||
img
|
||||
position: absolute
|
||||
margin: auto
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
|
||||
&.locked
|
||||
.achievement-content
|
||||
background-image: url("/images/achievements/achievement_background_locked.png")
|
||||
&:not(.locked)
|
||||
.achievement-content
|
||||
background-image: url("/images/achievements/achievement_background_light.png")
|
||||
|
||||
.achievement-content
|
||||
background-size: 100% 100%
|
||||
text-align: center
|
||||
overflow: hidden
|
||||
|
||||
> .achievement-title
|
||||
font-family: $font-family-base
|
||||
font-weight: bold
|
||||
white-space: nowrap
|
||||
max-height: 2em
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
|
||||
|
||||
> .achievement-description
|
||||
white-space: initial
|
||||
font-size: 12px
|
||||
line-height: 1.3em
|
||||
max-height: 2.6em
|
||||
margin-top: auto
|
||||
margin-bottom: 0px !important
|
||||
padding-left: 5px
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
|
||||
// Specific to the user stats page
|
||||
#user-achievements-view
|
||||
.achievement-body
|
||||
width: 335px
|
||||
height: 120px
|
||||
margin: 10px 0px
|
||||
|
||||
.achievement-icon
|
||||
width: 120px
|
||||
height: 120px
|
||||
top: -10px
|
||||
|
||||
.achievement-image
|
||||
img
|
||||
-moz-transform: scale(0.6)
|
||||
-webkit-transform: scale(0.6)
|
||||
transform: scale(0.6)
|
||||
|
||||
.achievement-content
|
||||
margin-left: 60px
|
||||
margin-right: 5px
|
||||
width: 260px
|
||||
height: 100px
|
||||
padding: 15px 10px 20px 60px
|
||||
|
||||
.achievement-title
|
||||
font-size: 20px
|
||||
|
||||
.achievement-description
|
||||
font-size: 12px
|
||||
line-height: 1.3em
|
||||
max-height: 2.6em
|
||||
|
||||
.achievement-popup
|
||||
padding: 20px 0px
|
||||
position: relative
|
||||
|
||||
.achievement-body
|
||||
.achievement-icon
|
||||
z-index: 1000
|
||||
width: 200px
|
||||
height: 200px
|
||||
left: -140px
|
||||
top: -20px
|
||||
|
||||
.achievement-image
|
||||
img
|
||||
position: absolute
|
||||
margin: auto
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
|
||||
.achievement-content
|
||||
background-image: url("/images/achievements/achievement_background.png")
|
||||
position: relative
|
||||
width: 450px
|
||||
height: 160px
|
||||
padding: 24px 30px 20px 60px
|
||||
|
||||
.achievement-title
|
||||
font-family: Bangers
|
||||
font-size: 28px
|
||||
padding-left: -50px
|
||||
|
||||
.achievement-description
|
||||
font-size: 15px
|
||||
line-height: 1.3em
|
||||
max-height: 2.6em
|
||||
margin-top: auto
|
||||
margin-bottom: 0px !important
|
||||
|
||||
.progress-wrapper
|
||||
margin-left: 20px
|
||||
position: absolute
|
||||
bottom: 48px
|
||||
|
||||
.user-level
|
||||
font-size: 20px
|
||||
color: white
|
||||
position: absolute
|
||||
left: -15px
|
||||
margin-top: -8px
|
||||
vertical-align: middle
|
||||
z-index: 1000
|
||||
|
||||
> .progress-bar-wrapper
|
||||
position: absolute
|
||||
margin-left: 17px
|
||||
width: 314px
|
||||
height: 20px
|
||||
z-index: 2
|
||||
|
||||
> .progress
|
||||
margin-top: 5px
|
||||
border-radius: 50px
|
||||
height: 14px
|
||||
|
||||
> .progress-bar-border
|
||||
position: absolute
|
||||
width: 340px
|
||||
height: 30px
|
||||
margin-top: -2px
|
||||
background: url("/images/achievements/bar_border.png") no-repeat
|
||||
background-size: 100% 100%
|
||||
z-index: 1
|
||||
|
||||
.achievement-icon
|
||||
background-size: 100% 100% !important
|
||||
|
||||
.achievement-wood
|
||||
&.locked
|
||||
.achievement-icon
|
||||
background: url("/images/achievements/border_wood_locked.png") no-repeat
|
||||
&:not(.locked)
|
||||
.achievement-icon
|
||||
background: url("/images/achievements/border_wood.png") no-repeat
|
||||
|
||||
.achievement-stone
|
||||
&.locked
|
||||
.achievement-icon
|
||||
background: url("/images/achievements/border_stone_locked.png") no-repeat
|
||||
&:not(.locked)
|
||||
.achievement-icon
|
||||
background: url("/images/achievements/border_stone.png") no-repeat
|
||||
|
||||
.achievement-silver
|
||||
&.locked
|
||||
.achievement-icon
|
||||
background: url("/images/achievements/border_silver_locked.png") no-repeat
|
||||
&:not(.locked)
|
||||
.achievement-icon
|
||||
background: url("/images/achievements/border_silver.png") no-repeat
|
||||
|
||||
.achievement-gold
|
||||
&.locked
|
||||
.achievement-icon
|
||||
background: url("/images/achievements/border_gold_locked.png") no-repeat
|
||||
&:not(.locked)
|
||||
.achievement-icon
|
||||
background: url("/images/achievements/border_gold.png") no-repeat
|
||||
|
||||
.achievement-diamond
|
||||
&.locked
|
||||
.achievement-icon
|
||||
background: url("/images/achievements/border_diamond_locked.png") no-repeat
|
||||
&:not(.locked)
|
||||
.achievement-icon
|
||||
background: url("/images/achievements/border_diamond.png") no-repeat
|
||||
|
||||
.xp-bar-old
|
||||
background-color: #680080
|
||||
|
||||
.xp-bar-new
|
||||
background-color: #0096ff
|
||||
|
||||
.xp-bar-left
|
||||
background-color: #fffbfd
|
||||
|
||||
// Achievements page
|
||||
.achievement-category-title
|
||||
margin-left: 20px
|
||||
font-family: $font-family-base
|
||||
font-weight: bold
|
||||
color: #5a5a5a
|
||||
text-transform: uppercase
|
||||
|
||||
.table-layout
|
||||
#no-achievements
|
||||
margin-top: 40px
|
||||
|
||||
.achievement-icon-small
|
||||
height: 18px
|
||||
|
||||
// Achievement Popup
|
||||
.achievement-popup-container
|
||||
position: fixed
|
||||
right: 0px
|
||||
bottom: 0px
|
||||
|
||||
.popup
|
||||
cursor: default
|
||||
left: 600px
|
||||
|
||||
.user-level
|
||||
background-image: url("/images/achievements/level-bg.png")
|
||||
width: 38px
|
||||
height: 38px
|
||||
line-height: 38px
|
||||
font-size: 20px
|
||||
font-family: $font-family-base
|
|
@ -21,7 +21,8 @@ h1 h2 h3 h4
|
|||
margin: 56px auto 0
|
||||
min-height: 600px
|
||||
padding: 14px 12px 5px 12px
|
||||
@include box-sizing(border-box)
|
||||
+box-sizing(border-box)
|
||||
+clearfix()
|
||||
|
||||
#outer-content-wrapper
|
||||
background: #B4B4B4
|
||||
|
@ -291,3 +292,9 @@ body[lang='ja']
|
|||
|
||||
a[data-toggle="coco-modal"]
|
||||
cursor: pointer
|
||||
|
||||
.achievement-corner
|
||||
position: fixed
|
||||
bottom: 0px
|
||||
right: 0px
|
||||
z-index: 1001
|
||||
|
|
|
@ -1,4 +1,44 @@
|
|||
@import "../bootstrap/variables"
|
||||
@import "../bootstrap/mixins"
|
||||
|
||||
// This is still very blocky. Browser reflows? Investigate why.
|
||||
.open > .dropdown-menu
|
||||
animation-name: fadeAnimation
|
||||
animation-duration: .7s
|
||||
animation-iteration-count: 1
|
||||
animation-timing-function: ease
|
||||
animation-fill-mode: forwards
|
||||
-webkit-animation-name: fadeAnimation
|
||||
-webkit-animation-duration: .7s
|
||||
-webkit-animation-iteration-count: 1
|
||||
-webkit-animation-timing-function: ease
|
||||
-webkit-animation-fill-mode: backwards
|
||||
-moz-animation-name: fadeAnimation
|
||||
-moz-animation-duration: .7s
|
||||
-moz-animation-iteration-count: 1
|
||||
-moz-animation-timing-function: ease
|
||||
-moz-animation-fill-mode: forwards
|
||||
|
||||
@keyframes fadeAnimation
|
||||
from
|
||||
opacity: 0
|
||||
top: 120%
|
||||
to
|
||||
opacity: 1
|
||||
top: 100%
|
||||
|
||||
@-webkit-keyframes fadeAnimation
|
||||
from
|
||||
opacity: 0
|
||||
top: 120%
|
||||
to
|
||||
opacity: 1
|
||||
top: 100%
|
||||
|
||||
a.disabled
|
||||
color: #5b5855
|
||||
text-decoration: none
|
||||
cursor: default
|
||||
|
||||
#top-nav
|
||||
a.navbar-brand
|
||||
|
@ -19,11 +59,65 @@
|
|||
.account-settings-image
|
||||
width: 18px
|
||||
height: 18px
|
||||
margin-right: 5px
|
||||
|
||||
.glyphicon-user
|
||||
font-size: 16px
|
||||
margin-right: 5px
|
||||
|
||||
.nav.navbar-link-text, .nav.navbar-link-text > li > a
|
||||
.dropdown
|
||||
.dropdown-menu
|
||||
left: auto
|
||||
width: 280px
|
||||
padding: 0px
|
||||
border-radius: 0px
|
||||
font-family: Bangers
|
||||
|
||||
> .user-dropdown-header
|
||||
position: relative
|
||||
background: #E4CF8C
|
||||
height: 160px
|
||||
padding: 10px
|
||||
text-align: center
|
||||
color: black
|
||||
border-bottom: #32281e 1px solid
|
||||
> a:hover
|
||||
background-color: transparent
|
||||
img
|
||||
border: #e3be7a 8px solid
|
||||
height: 98px // Includes the border
|
||||
&:hover
|
||||
box-shadow: 0 0 20px #e3be7a
|
||||
> h3
|
||||
margin-top: 10px
|
||||
text-shadow: 2px 2px 3px white
|
||||
color: #31281E
|
||||
.user-level
|
||||
position: absolute
|
||||
top: 73px
|
||||
right: 86px
|
||||
color: gold
|
||||
text-shadow: 1px 1px black, -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black
|
||||
|
||||
.user-dropdown-body
|
||||
color: black
|
||||
padding: 15px
|
||||
letter-spacing: 1px
|
||||
font: 15px 'Helvetica Neue', Helvetica, Arial, sans-serif
|
||||
+clearfix()
|
||||
|
||||
.user-dropdown-footer
|
||||
padding: 10px
|
||||
margin-left: 0px
|
||||
font-size: 14px
|
||||
+clearfix()
|
||||
|
||||
.btn-flat
|
||||
border: #ddd 1px solid
|
||||
border-radius: 0px
|
||||
margin: 0px
|
||||
|
||||
.nav.navbar-link-text > li > a
|
||||
font-weight: normal
|
||||
font-size: 25px
|
||||
letter-spacing: 2px
|
||||
|
@ -31,7 +125,7 @@
|
|||
&:hover
|
||||
color: #f8e413
|
||||
|
||||
.navbar-link-text a:hover
|
||||
.navbar-link-text > li > a:hover
|
||||
background: darken($body-bg, 3%)
|
||||
|
||||
.btn, .btn-group, .fancy-select
|
||||
|
@ -67,9 +161,6 @@
|
|||
top: 13px
|
||||
max-width: 140px
|
||||
|
||||
.nav
|
||||
margin-bottom: 0
|
||||
|
||||
div.fancy-select
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25)
|
||||
div.trigger
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
#editor-achievement-edit-view
|
||||
height: 100%
|
||||
|
||||
.treema-root
|
||||
margin: 28px 0px 20px
|
||||
|
||||
|
@ -10,3 +12,9 @@
|
|||
textarea
|
||||
width: 92%
|
||||
height: 300px
|
||||
|
||||
#achievement-view
|
||||
min-height: 200px
|
||||
position: relative
|
||||
padding-left: 200px
|
||||
|
||||
|
|
6
app/styles/editor/related-achievements.sass
Normal file
|
@ -0,0 +1,6 @@
|
|||
#related-achievements-view
|
||||
#new-achievement-button
|
||||
margin-bottom: 10px
|
||||
|
||||
.icon-column
|
||||
width: 25px
|
|
@ -1,50 +0,0 @@
|
|||
.notifyjs-achievement-base
|
||||
//background: url("/images/pages/base/notify_mockup.png")
|
||||
background-image: url("/images/pages/base/modal_background.png")
|
||||
background-size: 100% 100%
|
||||
width: 500px
|
||||
height: 200px
|
||||
padding: 35px 35px 15px 15px
|
||||
text-align: center
|
||||
cursor: auto
|
||||
|
||||
.achievement-body
|
||||
.achievement-image
|
||||
img
|
||||
float: left
|
||||
width: 100px
|
||||
height: 100px
|
||||
border-radius: 50%
|
||||
margin: 20px 30px 20px 30px
|
||||
-webkit-box-shadow: 0px 0px 36px 0px white
|
||||
-moz-box-shadow: 0px 0px 36px 0px white
|
||||
box-shadow: 0px 0px 36px 0px white
|
||||
|
||||
.achievement-title
|
||||
font-family: Bangers
|
||||
font-size: 28px
|
||||
|
||||
.achievement-description
|
||||
margin-top: 10px
|
||||
font-size: 16px
|
||||
|
||||
.achievement-progress
|
||||
padding: 15px 0px 0px 0px
|
||||
|
||||
.achievement-message
|
||||
font-family: Bangers
|
||||
font-size: 18px
|
||||
&:empty
|
||||
display: none
|
||||
|
||||
.progress-wrapper
|
||||
.progress-bar-wrapper
|
||||
width: 100%
|
||||
.earned-exp
|
||||
padding-left: 5px
|
||||
font-family: Bangers
|
||||
font-size: 16px
|
||||
float: right
|
||||
|
||||
.progress-bar-white
|
||||
background-color: white
|
73
app/styles/user/user_home.sass
Normal file
|
@ -0,0 +1,73 @@
|
|||
@import "../bootstrap/variables"
|
||||
@import "../bootstrap/mixins"
|
||||
|
||||
#user-home
|
||||
margin-top: 20px
|
||||
|
||||
.left-column
|
||||
+make-sm-column(4)
|
||||
|
||||
.right-column
|
||||
+make-sm-column(8)
|
||||
|
||||
.profile-wrapper
|
||||
text-align: center
|
||||
outline: 1px solid darkgrey
|
||||
max-width: 100%
|
||||
+center-block()
|
||||
|
||||
> .picture
|
||||
width: 100%
|
||||
background-color: #ffe4bc
|
||||
border: 4px solid white
|
||||
|
||||
> .profile-info
|
||||
background: white
|
||||
|
||||
.extra-info
|
||||
padding-bottom: 3px
|
||||
&:empty
|
||||
display: none
|
||||
|
||||
.name
|
||||
margin: 0px auto
|
||||
padding: 10px inherit
|
||||
color: white
|
||||
text-shadow: 2px 0 0 #000, -2px 0 0 #000, 0 2px 0 #000, 0 -2px 0 #000, 1px 1px #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000
|
||||
|
||||
.profile-menu
|
||||
padding-left: 0px
|
||||
width: 100%
|
||||
> a
|
||||
border-radius: 0
|
||||
border-width: 1px 0px 0px 0px
|
||||
border-color: darkgrey
|
||||
&:hover
|
||||
border-color: #888
|
||||
> span
|
||||
color: #555555
|
||||
font-size: 15px
|
||||
margin-left: 5px
|
||||
|
||||
.contributor-categories
|
||||
list-style: none
|
||||
padding: 0px
|
||||
margin-top: 15px
|
||||
|
||||
> .contributor-category
|
||||
outline: 1px solid black
|
||||
margin-bottom: 15px
|
||||
|
||||
> .contributor-image
|
||||
border: none
|
||||
width: 100%
|
||||
border-bottom: 1px solid black
|
||||
|
||||
> .contributor-title
|
||||
text-align: center
|
||||
padding: 5px 0px
|
||||
margin: 0px
|
||||
background: white
|
||||
|
||||
.vertical-buffer
|
||||
padding: 10px 0px
|
141
app/templates/account/account_home.jade
Normal file
|
@ -0,0 +1,141 @@
|
|||
extends /templates/base
|
||||
|
||||
block content
|
||||
if !me.isAnonymous()
|
||||
.clearfix
|
||||
.col-sm-6.clearfix
|
||||
h2
|
||||
span(data-i18n="account_settings.title") Account Settings
|
||||
a.spl(href="/account/settings")
|
||||
i.glyphicon.glyphicon-cog
|
||||
hr
|
||||
.row
|
||||
.col-xs-6
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
h3.panel-title
|
||||
i.glyphicon.glyphicon-picture
|
||||
a(href="account/settings#picture" data-i18n="account_settings.picture_tab") Picture
|
||||
.panel-body.text-center
|
||||
img#picture(src="#{me.getPhotoURL(150)}" alt="Picture")
|
||||
.col-xs-6
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
h3.panel-title
|
||||
i.glyphicon.glyphicon-user
|
||||
a(href="account/settings#wizard" data-i18n="account_settings.wizard_tab") Wizard
|
||||
if (wizardSource)
|
||||
.panel-body.text-center
|
||||
img(src="#{wizardSource}")
|
||||
.panel.panel-default.panel-me
|
||||
.panel-heading
|
||||
h3.panel-title
|
||||
i.glyphicon.glyphicon-user
|
||||
a(href="account/settings#me" data-i18n="account_settings.me_tab") Me
|
||||
.panel-body
|
||||
table
|
||||
tr
|
||||
th(data-i18n="general.name") Name
|
||||
td=me.displayName()
|
||||
tr
|
||||
th(data-i18n="general.email") Email
|
||||
td=me.get('email')
|
||||
.panel.panel-default.panel-emails
|
||||
.panel-heading
|
||||
h3.panel-title
|
||||
i.glyphicon.glyphicon-envelope
|
||||
a(href="account/settings#emails" data-i18n="account_settings.emails_tab") Emails
|
||||
.panel-body
|
||||
if !hasEmailNotes && !hasEmailNews && !hasGeneralNews
|
||||
p(data-i18n="account_settings.email_subscriptions_none") No email subscriptions.
|
||||
if hasGeneralNews
|
||||
h4(data-i18n="account_settings.email_news") News
|
||||
ul
|
||||
li(data-i18n="account_settings.email_announcements") Announcements
|
||||
if hasEmailNotes
|
||||
h4(data-i18n="account_settings.email_notifications") Notifications
|
||||
ul
|
||||
if subs.anyNotes
|
||||
li(data-i18n="account_settings.email_any_notes") Any Notifications
|
||||
if subs.recruitNotes
|
||||
li(data-i18n="account_settings.email_recruit_notes") Job Opportunities
|
||||
if hasEmailNews
|
||||
h4(data-i18n="account_settings.contributor_emails") Contributor Emails
|
||||
ul
|
||||
if (subs.archmageNews)
|
||||
li
|
||||
span(data-i18n="classes.archmage_title")
|
||||
| Archmage
|
||||
span(data-i18n="classes.archmage_title_description")
|
||||
| (Coder)
|
||||
if (subs.artisanNews)
|
||||
li
|
||||
span.spr(data-i18n="classes.artisan_title")
|
||||
| Artisan
|
||||
span(data-i18n="classes.artisan_title_description")
|
||||
| (Level Builder)
|
||||
if (subs.adventurerNews)
|
||||
li
|
||||
span.spr(data-i18n="classes.adventurer_title")
|
||||
| Adventurer
|
||||
span(data-i18n="classes.adventurer_title_description")
|
||||
| (Level Playtester)
|
||||
if (subs.scribeNews)
|
||||
li
|
||||
span.spr(data-i18n="classes.scribe_title")
|
||||
| Scribe
|
||||
span(data-i18n="classes.scribe_title_description")
|
||||
| (Article Editor)
|
||||
if (subs.diplomatNews)
|
||||
li
|
||||
span.spr(data-i18n="classes.diplomat_title")
|
||||
| Diplomat
|
||||
span(data-i18n="classes.diplomat_title_description")
|
||||
| (Translator)
|
||||
if (subs.ambassadorNews)
|
||||
li
|
||||
span.spr(data-i18n="classes.ambassador_title")
|
||||
| Ambassador
|
||||
span(data-i18n="classes.ambassador_title_description")
|
||||
| (Support)
|
||||
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
h3.panel-title
|
||||
i.glyphicon.glyphicon-wrench
|
||||
a(href="account/settings#password" data-i18n="general.password") Password
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
h3.panel-title
|
||||
i.glyphicon.glyphicon-briefcase
|
||||
a(href="account/settings#job-profile" data-i18n="account_settings.job_profile") Job Profile
|
||||
.col-sm-6
|
||||
h2(data-i18n="user.recently_played") Recently Played
|
||||
hr
|
||||
if !recentlyPlayed
|
||||
div(data-i18n="common.loading") Loading...
|
||||
else if recentlyPlayed.length
|
||||
table.table
|
||||
tr
|
||||
th(data-i18n="resources.level") Level
|
||||
th(data-i18n="user.last_played") Last Played
|
||||
th(data-i18n="user.status") Status
|
||||
each session in recentlyPlayed
|
||||
if session.get('levelName')
|
||||
tr
|
||||
td
|
||||
- var posturl = ''
|
||||
- if (session.get('team')) posturl = '?team=' + session.get('team')
|
||||
a(href="/play/level/#{session.get('levelID') + posturl}")= session.get('levelName') + (session.get('team') ? ' (' + session.get('team') + ')' : '')
|
||||
td= moment(session.get('changed')).fromNow()
|
||||
if session.get('state').complete === true
|
||||
td(data-i18n="user.status_completed") Completed
|
||||
else if ! session.isMultiplayer()
|
||||
td(data-i18n="user.status_unfinished") Unfinished
|
||||
else
|
||||
td
|
||||
|
||||
else
|
||||
.panel.panel-default
|
||||
.panel-body
|
||||
div(data-i18n="account.no_recent_games") No games played during the past two weeks.
|
|
@ -1,12 +0,0 @@
|
|||
div
|
||||
.clearfix.achievement-body
|
||||
.achievement-image(data-notify-html="image")
|
||||
.achievement-content
|
||||
.achievement-title(data-notify-html="title")
|
||||
.achievement-description(data-notify-html="description")
|
||||
|
||||
.achievement-progress
|
||||
.achievement-message(data-notify-html="message")
|
||||
.progress-wrapper
|
||||
.earned-exp(data-notify-html="earnedExp")
|
||||
.progress-bar-wrapper(data-notify-html="progressBar")
|
21
app/templates/achievements/achievement-popup.jade
Normal file
|
@ -0,0 +1,21 @@
|
|||
- var addedClass = style + (locked === true ? ' locked' : '')
|
||||
.clearfix.achievement-body(class=addedClass)
|
||||
.achievement-icon
|
||||
.achievement-image
|
||||
img(src=imgURL)
|
||||
.achievement-content
|
||||
.achievement-title= title
|
||||
p.achievement-description= description
|
||||
|
||||
if popup
|
||||
.progress-wrapper
|
||||
span.user-level= level
|
||||
.progress-bar-wrapper
|
||||
.progress
|
||||
- var currentTitle = $.i18n.t('achievements.current_xp_prefix') + currentXP + ' XP' + $.i18n.t('achievements.current_xp_postfix');
|
||||
- var newTitle = $.i18n.t('achievements.new_xp_prefix') + newXP + ' XP' + $.i18n.t('achievements.new_xp_postfix');
|
||||
- var leftTitle = $.i18n.t('achievements.left_xp_prefix') + newXP + ' XP' + $.i18n.t('achievements.left_xp_infix') + (level+1) + $.i18n.t('achievements.left_xp_postfix');
|
||||
.progress-bar.xp-bar-old(style="width:#{oldXPWidth}%" data-toggle="tooltip" data-placement="top" title="#{currentTitle}")
|
||||
.progress-bar.xp-bar-new(style="width:#{newXPWidth}%" data-toggle="tooltip" title="#{newTitle}")
|
||||
.progress-bar.xp-bar-left(style="width:#{leftXPWidth}%" data-toggle="tooltip" title="#{leftTitle}")
|
||||
.progress-bar-border
|
|
@ -27,26 +27,44 @@ body
|
|||
|
||||
select.language-dropdown
|
||||
|
||||
if me.get('anonymous') === false
|
||||
button.btn.btn-primary.navbuttontext.header-font#logout-button(data-i18n="login.log_out") Log Out
|
||||
a.btn.btn-primary.navbuttontext.header-font(href=me.get('jobProfile') ? "/account/profile/#{me.id}" : "/account/settings")
|
||||
div.navbuttontext-account(data-i18n="nav.account") Account
|
||||
if me.get('photoURL')
|
||||
img.account-settings-image(src=me.getPhotoURL(18), alt="")
|
||||
else
|
||||
span.glyphicon.glyphicon-user
|
||||
|
||||
else
|
||||
button.btn.btn-primary.navbuttontext.header-font.auth-button
|
||||
span(data-i18n="login.log_in") Log In
|
||||
span.spr.spl /
|
||||
span(data-i18n="login.sign_up") Create Account
|
||||
|
||||
ul(class='navbar-link-text').nav.navbar-nav.pull-right
|
||||
li.play
|
||||
a.header-font(href='/play', data-i18n="nav.play") Levels
|
||||
li
|
||||
a.header-font(href='/community', data-i18n="nav.community") Community
|
||||
if me.get('anonymous') === false
|
||||
li.dropdown
|
||||
button.btn.btn-primary.navbuttontext.header-font.dropdown-toggle(href="#", data-toggle="dropdown")
|
||||
if me.get('photoURL')
|
||||
img.account-settings-image(src=me.getPhotoURL(18), alt="")
|
||||
else
|
||||
i.glyphicon.glyphicon-user
|
||||
.navbuttontext-account(data-i18n="nav.account" href="/account") Account
|
||||
span.caret
|
||||
ul.dropdown-menu(role="menu")
|
||||
li.user-dropdown-header
|
||||
span.user-level= me.level()
|
||||
a(href="/user/#{me.getSlugOrID()}")
|
||||
img.img-circle(src="#{me.getPhotoURL()}" alt="")
|
||||
h3=me.displayName()
|
||||
li.user-dropdown-body
|
||||
.col-xs-4.text-center
|
||||
a(href="/user/#{me.getSlugOrID()}" data-i18n="nav.profile") Profile
|
||||
.col-xs-4.text-center
|
||||
a(href="/user/#{me.getSlugOrID()}/stats" data-i18n="nav.stats") Stats
|
||||
.col-xs-4.text-center
|
||||
a.disabled(data-i18n="nav.code") Code
|
||||
li.user-dropdown-footer
|
||||
.pull-left
|
||||
a.btn.btn-default.btn-flat(href="/account" data-i18n="nav.account") Account
|
||||
.pull-right
|
||||
button#logout-button.btn.btn-default.btn-flat(data-i18n="login.log_out") Log Out
|
||||
else
|
||||
li
|
||||
button.btn.btn-primary.navbuttontext.header-font.auth-button
|
||||
span(data-i18n="login.log_in") Log In
|
||||
span.spr.spl /
|
||||
span(data-i18n="login.sign_up") Create Account
|
||||
|
||||
block outer_content
|
||||
#outer-content-wrapper(class=showBackground ? 'show-background' : '')
|
||||
|
@ -55,6 +73,7 @@ body
|
|||
.main-content-area
|
||||
block content
|
||||
p If this is showing, you dun goofed
|
||||
.achievement-corner
|
||||
|
||||
block footer
|
||||
.footer.clearfix
|
||||
|
|
|
@ -2,16 +2,16 @@ extends /templates/base
|
|||
|
||||
block content
|
||||
if me.isAdmin()
|
||||
div
|
||||
ol.breadcrumb
|
||||
li
|
||||
a(href="/editor", data-i18n="editor.main_title") CodeCombat Editors
|
||||
li
|
||||
a(href="/editor/achievement", data-i18n="editor.achievement_title") Achievement Editor
|
||||
li.active
|
||||
| #{achievement.attributes.name}
|
||||
ol.breadcrumb
|
||||
li
|
||||
a(href="/editor", data-i18n="editor.main_title") CodeCombat Editors
|
||||
li
|
||||
a(href="/editor/achievement", data-i18n="editor.achievement_title") Achievement Editor
|
||||
li.active
|
||||
| #{achievement.attributes.name}
|
||||
|
||||
button(data-i18n="", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#recalculate-button Recalculate
|
||||
button(data-i18n="common.delete", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#delete-button Delete
|
||||
button(data-i18n="common.save", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#save-button Save
|
||||
|
||||
h3(data-i18n="achievement.edit_achievement_title") Edit Achievement
|
||||
|
@ -20,12 +20,10 @@ block content
|
|||
|
||||
#achievement-treema
|
||||
|
||||
#achievement-view
|
||||
#achievement-view.clearfix
|
||||
|
||||
hr
|
||||
|
||||
div#error-view
|
||||
|
||||
else
|
||||
.alert.alert-danger
|
||||
span Admin only. Turn around.
|
||||
|
|
|
@ -33,6 +33,8 @@ block header
|
|||
- var patches = level.get('patches')
|
||||
if patches && patches.length
|
||||
span.badge= patches.length
|
||||
li
|
||||
a(href="#related-achievements-view", data-toggle="tab") Achievements
|
||||
li
|
||||
a(href="#docs-components-view", data-toggle="tab", data-i18n="editor.level_tab_docs") Documentation
|
||||
.navbar-header
|
||||
|
@ -121,8 +123,10 @@ block outer_content
|
|||
div.tab-pane#editor-level-patches
|
||||
.patches-view
|
||||
|
||||
div.tab-pane#related-achievements-view
|
||||
|
||||
div.tab-pane#docs-components-view
|
||||
|
||||
div#error-view
|
||||
|
||||
block footer
|
||||
block footer
|
||||
|
|
23
app/templates/editor/level/modal/new-achievement.jade
Normal file
|
@ -0,0 +1,23 @@
|
|||
extends /templates/modal/new_model
|
||||
|
||||
block modal-body-content
|
||||
form.form
|
||||
.form-group
|
||||
label.control-label(for="name", data-i18n="general.name") Name
|
||||
input#name.form-control(name="name", type="text")
|
||||
.form-group
|
||||
label.control-label(for="description" data-i18n="general.description") Description
|
||||
input#description.form-control(name="description", type="text")
|
||||
h4(data-i18n="editor.achievement_query_misc") Key achievement off of miscellanea
|
||||
.radio
|
||||
label
|
||||
input(type="checkbox", name="queryOptions" id="misc-level-completion" value="misc-level-completion")
|
||||
span.spl(data-i18n="editor.level_completion") Level Completion
|
||||
- var goals = level.get('goals');
|
||||
if goals && goals.length
|
||||
h4(data-i18n="editor.achievement_query_goals") Key achievement off of level goals
|
||||
each goal in goals
|
||||
.radio
|
||||
label
|
||||
input(type="checkbox", name="queryOptions" id="#{goal.id}" value="#{goal.id}")
|
||||
span.spl= goal.name
|
26
app/templates/editor/level/related-achievements.jade
Normal file
|
@ -0,0 +1,26 @@
|
|||
|
||||
button.btn.btn-primary#new-achievement-button(disabled=me.isAdmin() === true ? undefined : "true" data-i18n="editor.new_achievement_title") Create a New Achievement
|
||||
|
||||
if achievements.loading
|
||||
h2(data-i18n="common.loading") Loading...
|
||||
else if ! achievements.models.length
|
||||
.panel
|
||||
.panel-body
|
||||
p(data-i18n="editor.no_achievements") No achievements added for this level yet.
|
||||
else
|
||||
table.table.table-hover
|
||||
thead
|
||||
tr
|
||||
th
|
||||
th(data-i18n="general.name") Name
|
||||
th(data-i18n="general.description") Description
|
||||
th XP
|
||||
tbody
|
||||
each achievement in achievements.models
|
||||
tr
|
||||
td(style="width: 20px")
|
||||
img.achievement-icon-small(src=achievement.getImageURL() alt="#{achievement.get('name') icon")
|
||||
td
|
||||
a(href="/editor/achievement/#{achievement.get('slug')}")= achievement.get('name')
|
||||
td= achievement.get('description')
|
||||
td= achievement.get('worth')
|
|
@ -12,32 +12,12 @@ block content
|
|||
if me.get('anonymous')
|
||||
a.btn.btn-primary.open-modal-button(data-toggle="coco-modal", data-target="modal/AuthModal", role="button", data-i18n="#{currentNewSignup}") Log in to Create a New Content
|
||||
else
|
||||
a.btn.btn-primary.open-modal-button(href='#new-model-modal', role="button", data-toggle="modal", data-i18n="#{currentNew}") Create a New Something
|
||||
a.btn.btn-primary.open-modal-button#new-model-button(data-i18n="#{currentNew}") Create a New Something
|
||||
input#search(data-i18n="[placeholder]#{currentSearch}")
|
||||
hr
|
||||
div.results
|
||||
table
|
||||
|
||||
// TODO: make this into a ModalView subview
|
||||
div.modal.fade#new-model-modal
|
||||
.modal-dialog
|
||||
.background-wrapper
|
||||
.modal-content
|
||||
.modal-header
|
||||
h3(data-i18n="#{currentNew}") Create New #{modelLabel}
|
||||
.modal-body
|
||||
form.form
|
||||
.form-group
|
||||
label.control-label(for="name", data-i18n="general.name") Name
|
||||
input#name.form-control(name="name", type="text")
|
||||
.modal-footer
|
||||
button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel
|
||||
button.btn.btn-primary.new-model-submit(data-i18n="common.create") Create
|
||||
.modal-body.wait.secret
|
||||
h3(data-i18n="play_level.tip_reticulating") Reticulating Splines...
|
||||
.progress.progress-striped.active
|
||||
.progress-bar
|
||||
|
||||
else
|
||||
.alert.alert-danger
|
||||
span Admin only. Turn around.
|
||||
|
|
13
app/templates/kinds/user.jade
Normal file
|
@ -0,0 +1,13 @@
|
|||
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
|
||||
a(href="/user/#{user.getSlugOrID()}") #{user.displayName()}
|
||||
li.active
|
||||
| #{viewName}
|
||||
if !user || user.loading
|
||||
| LOADING
|
19
app/templates/modal/new_model.jade
Normal file
|
@ -0,0 +1,19 @@
|
|||
extends /templates/modal/modal_base
|
||||
|
||||
block modal-header-content
|
||||
h3(data-i18n="#{currentNew}") Create New #{modelLabel}
|
||||
|
||||
block modal-body-content
|
||||
form.form
|
||||
.form-group
|
||||
label.control-label(for="name", data-i18n="general.name") Name
|
||||
input#name.form-control(name="name", type="text")
|
||||
|
||||
block modal-footer
|
||||
.modal-footer
|
||||
button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel
|
||||
button.btn.btn-primary.new-model-submit(data-i18n="common.create") Create
|
||||
.modal-body.wait.secret
|
||||
h3(data-i18n="play_level.tip_reticulating") Reticulating Splines...
|
||||
.progress.progress-striped.active
|
||||
.progress-bar
|
52
app/templates/user/achievements.jade
Normal file
|
@ -0,0 +1,52 @@
|
|||
extends /templates/kinds/user
|
||||
|
||||
block append content
|
||||
.btn-group.pull-right
|
||||
button#grid-layout-button.btn.btn-default(data-layout='grid', class=activeLayout==='grid' ? 'active' : '')
|
||||
i.glyphicon.glyphicon-th
|
||||
button#table-layout-button.btn.btn-default(data-layout='table', class=activeLayout==='table' ? 'active' : '')
|
||||
i.glyphicon.glyphicon-th-list
|
||||
if achievementsByCategory
|
||||
if activeLayout === 'grid'
|
||||
.grid-layout
|
||||
each achievements, category in achievementsByCategory
|
||||
.row
|
||||
h2.achievement-category-title(data-i18n="category_#{category}")=category
|
||||
each achievement, index in achievements
|
||||
- var title = achievement.i18nName();
|
||||
- var description = achievement.i18nDescription();
|
||||
- var locked = ! achievement.get('unlocked');
|
||||
- var style = achievement.getStyle()
|
||||
- var imgURL = achievement.getImageURL();
|
||||
if locked
|
||||
- var imgURL = achievement.getLockedImageURL();
|
||||
else
|
||||
- var imgURL = achievement.getImageURL();
|
||||
.col-lg-4.col-xs-12
|
||||
include ../achievements/achievement-popup
|
||||
else if activeLayout === 'table'
|
||||
.table-layout
|
||||
if earnedAchievements.length
|
||||
table.table
|
||||
tr
|
||||
th(data-i18n="general.name") Name
|
||||
th(data-i18n="general.description") Description
|
||||
th(data-i18n="general.date") Date
|
||||
th(data-i18n="achievements.amount_achieved") Amount
|
||||
th XP
|
||||
each earnedAchievement in earnedAchievements.models
|
||||
- var achievement = earnedAchievement.get('achievement');
|
||||
tr
|
||||
td= achievement.i18nName()
|
||||
td= achievement.i18nDescription()
|
||||
td= moment().format("MMMM Do YYYY", earnedAchievement.get('changed'))
|
||||
if achievement.isRepeatable()
|
||||
td= earnedAchievement.get('achievedAmount')
|
||||
else
|
||||
td
|
||||
td= earnedAchievement.get('earnedPoints')
|
||||
else
|
||||
.panel#no-achievements
|
||||
.panel-body(data-i18n="user.no_achievements") No achievements earned yet.
|
||||
else
|
||||
div How did you even do that?
|
133
app/templates/user/user_home.jade
Normal file
|
@ -0,0 +1,133 @@
|
|||
extends /templates/kinds/user
|
||||
|
||||
block append content
|
||||
if user
|
||||
.vertical-buffer
|
||||
.row
|
||||
.left-column
|
||||
.profile-wrapper
|
||||
img.picture(src="#{user.getPhotoURL(150)}" alt="")
|
||||
div.profile-info
|
||||
h3.name= user.get('name')
|
||||
if favoriteLanguage
|
||||
div.extra-info
|
||||
span(data-i18n="user.favorite_prefix") Favorite language is
|
||||
strong.favorite-language= favoriteLanguage
|
||||
span(data-i18n="user.favorite_postfix") .
|
||||
.btn-group-vertical.profile-menu
|
||||
a.btn.btn-default(href="/user/#{user.getSlugOrID()}/profile")
|
||||
i.glyphicon.glyphicon-briefcase
|
||||
span(data-i18n="account_settings.job_profile") Job Profile
|
||||
a.btn.btn-default(href="/user/#{user.getSlugOrID()}/stats")
|
||||
i.glyphicon.glyphicon-certificate
|
||||
span(data-i18n="user.stats") Stats
|
||||
a.btn.btn-default.disabled(href="#")
|
||||
i.glyphicon.glyphicon-pencil
|
||||
span(data-i18n="general.code") Code
|
||||
- var emails = user.get('emails')
|
||||
if emails
|
||||
ul.contributor-categories
|
||||
//li.contributor-category
|
||||
img.contributor-image(src="/images/pages/user/general.png")
|
||||
h4.contributor-title CodeCombateer
|
||||
if emails.adventurerNews
|
||||
li.contributor-category
|
||||
img.contributor-image(src="/images/pages/user/adventurer.png")
|
||||
h4.contributor-title
|
||||
a(href="/contribute#adventurer" data-i18n="classes.adventurer_title") Adventurer
|
||||
if emails.ambassadorNews
|
||||
li.contributor-category
|
||||
img.contributor-image(src="/images/pages/user/ambassador.png")
|
||||
h4.contributor-title
|
||||
a(href="/contribute#ambassador" data-i18n="classes.ambassador_title") Ambassador
|
||||
if emails.archmageNews
|
||||
li.contributor-category
|
||||
img.contributor-image(src="/images/pages/user/archmage.png")
|
||||
h4.contributor-title
|
||||
a(href="/contribute#archmage" data-i18n="classes.archmage_title") Archmage
|
||||
if emails.artisanNews
|
||||
li.contributor-category
|
||||
img.contributor-image(src="/images/pages/user/artisan.png")
|
||||
h4.contributor-title
|
||||
a(href="/contribute#artisan" data-i18n="classes.artisan_title") Artisan
|
||||
if emails.scribeNews
|
||||
li.contributor-category
|
||||
img.contributor-image(src="/images/pages/user/scribe.png")
|
||||
h4.contributor-title
|
||||
a(href="/contribute#scribe" data-i18n="classes.scribe_title") Scribe
|
||||
|
||||
.right-column
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
h3.panel-title(data-i18n="user.singleplayer_title") Singleplayer Levels
|
||||
if (!singlePlayerSessions)
|
||||
.panel-body
|
||||
p(data-i18n="common.loading") Loading...
|
||||
else if (singlePlayerSessions.length)
|
||||
table.table
|
||||
tr
|
||||
th.col-xs-4(data-i18n="resources.level") Level
|
||||
th.col-xs-4(data-i18n="user.last_played") Last Played
|
||||
th.col-xs-4(data-i18n="user.status") Status
|
||||
each session in singlePlayerSessions
|
||||
if session.get('levelName')
|
||||
tr
|
||||
td
|
||||
a(href="/play/level/#{session.get('levelID')}")= session.get('levelName')
|
||||
td= moment(session.get('changed')).fromNow()
|
||||
if session.get('state').complete === true
|
||||
td(data-i18n="user.status_completed") Completed
|
||||
else
|
||||
td(data-i18n="user.status_unfinished") Unfinished
|
||||
else
|
||||
.panel-body
|
||||
p(data-i18n="no_singleplayer") No Singleplayer games played yet.
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
h3.panel-title(data-i18n="no_multiplayer") Multiplayer Levels
|
||||
if (!multiPlayerSessions)
|
||||
.panel-body
|
||||
p(data-i18n="common.loading") Loading...
|
||||
else if (multiPlayerSessions.length)
|
||||
table.table
|
||||
tr
|
||||
th.col-xs-4(data-i18n="resources.level") Level
|
||||
th.col-xs-4(data-i18n="user.last_played") Last Played
|
||||
th.col-xs-4(data-i18n="general.score") Score
|
||||
each session in multiPlayerSessions
|
||||
tr
|
||||
td
|
||||
- var posturl = ''
|
||||
- if (session.get('team')) posturl = '?team=' + session.get('team')
|
||||
a(href="/play/level/#{session.get('levelID') + posturl}")= session.get('levelName') + (session.get('team') ? ' (' + session.get('team') + ')' : '')
|
||||
td= moment(session.get('changed')).fromNow()
|
||||
if session.get('totalScore')
|
||||
td= session.get('totalScore') * 100
|
||||
else
|
||||
td(data-i18n="user.status_unfinished") Unfinished
|
||||
else
|
||||
.panel-body
|
||||
p(data-i18n="user.no_multiplayer") No Multiplayer games played yet.
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
h3.panel-title(data-i18n="user.achievements") Achievements
|
||||
if ! earnedAchievements
|
||||
.panel-body
|
||||
p(data-i18n="common.loading") Loading...
|
||||
else if ! earnedAchievements.length
|
||||
.panel-body
|
||||
p(data-i18n="user.no_achievements") No achievements earned so far.
|
||||
else
|
||||
table.table
|
||||
tr
|
||||
th.col-xs-4(data-i18n="achievements.achievement") Achievement
|
||||
th.col-xs-4(data-i18n="achievements.last_earned") Last Earned
|
||||
th.col-xs-4(data-i18n="achievements.amount_achieved") Amount
|
||||
each achievement in earnedAchievements.models
|
||||
tr
|
||||
td= achievement.get('achievementName')
|
||||
td= moment().format("MMMM Do YYYY", achievement.get('changed'))
|
||||
if achievement.get('achievedAmount')
|
||||
td= achievement.get('achievedAmount')
|
||||
else
|
||||
td
|
|
@ -1,4 +1,4 @@
|
|||
CocoView = require 'views/kinds/CocoView'
|
||||
RootView = require 'views/kinds/RootView'
|
||||
ModalView = require 'views/kinds/ModalView'
|
||||
template = require 'templates/demo'
|
||||
requireUtils = require 'lib/requireUtils'
|
||||
|
@ -24,7 +24,7 @@ DEMO_URL_PREFIX = '/demo/'
|
|||
|
||||
###
|
||||
|
||||
module.exports = DemoView = class DemoView extends CocoView
|
||||
module.exports = DemoView = class DemoView extends RootView
|
||||
id: 'demo-view'
|
||||
template: template
|
||||
|
||||
|
|
38
app/views/account/MainAccountView.coffee
Normal file
|
@ -0,0 +1,38 @@
|
|||
View = require 'views/kinds/RootView'
|
||||
template = require 'templates/account/account_home'
|
||||
{me} = require 'lib/auth'
|
||||
User = require 'models/User'
|
||||
AuthModalView = require 'views/modal/AuthModal'
|
||||
RecentlyPlayedCollection = require 'collections/RecentlyPlayedCollection'
|
||||
ThangType = require 'models/ThangType'
|
||||
|
||||
module.exports = class MainAccountView extends View
|
||||
id: 'account-home'
|
||||
template: template
|
||||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
return unless me
|
||||
@wizardType = ThangType.loadUniversalWizard()
|
||||
@recentlyPlayed = new RecentlyPlayedCollection me.get('_id')
|
||||
@supermodel.loadModel @wizardType, 'thang'
|
||||
@supermodel.loadCollection @recentlyPlayed, 'recentlyPlayed'
|
||||
|
||||
onLoaded: ->
|
||||
super()
|
||||
|
||||
getRenderData: ->
|
||||
c = super()
|
||||
c.subs = {}
|
||||
enabledEmails = c.me.getEnabledEmails()
|
||||
c.subs[sub] = 1 for sub in enabledEmails
|
||||
c.hasEmailNotes = _.any enabledEmails, (sub) -> sub.contains 'Notes'
|
||||
c.hasEmailNews = _.any enabledEmails, (sub) -> sub.contains('News') and sub isnt 'generalNews'
|
||||
c.hasGeneralNews = 'generalNews' in enabledEmails
|
||||
c.wizardSource = @wizardType.getPortraitSource colorConfig: me.get('wizard')?.colorConfig if @wizardType.loaded
|
||||
c.recentlyPlayed = @recentlyPlayed.models
|
||||
c
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
@openModalView new AuthModalView if me.isAnonymous()
|
91
app/views/achievements/AchievementPopup.coffee
Normal file
|
@ -0,0 +1,91 @@
|
|||
CocoView = require 'views/kinds/CocoView'
|
||||
template = require 'templates/achievements/achievement-popup'
|
||||
User = require '../../models/User'
|
||||
Achievement = require '../../models/Achievement'
|
||||
|
||||
module.exports = class AchievementPopup extends CocoView
|
||||
className: 'achievement-popup'
|
||||
template: template
|
||||
|
||||
constructor: (options) ->
|
||||
@achievement = options.achievement
|
||||
@earnedAchievement = options.earnedAchievement
|
||||
@container = options.container or @getContainer()
|
||||
@popup = options.container
|
||||
@popup ?= true
|
||||
@className += ' popup' if @popup
|
||||
super options
|
||||
console.debug 'Created an AchievementPopup', @$el
|
||||
|
||||
@render()
|
||||
|
||||
calculateData: ->
|
||||
currentLevel = me.level()
|
||||
nextLevel = currentLevel + 1
|
||||
currentLevelExp = User.expForLevel(currentLevel)
|
||||
nextLevelXP = User.expForLevel(nextLevel)
|
||||
totalExpNeeded = nextLevelXP - currentLevelExp
|
||||
expFunction = @achievement.getExpFunction()
|
||||
currentXP = me.get 'points'
|
||||
if @achievement.isRepeatable()
|
||||
achievedXP = expFunction(@earnedAchievement.get('previouslyAchievedAmount')) * @achievement.get('worth') if @achievement.isRepeatable()
|
||||
else
|
||||
achievedXP = @achievement.get 'worth'
|
||||
previousXP = currentXP - achievedXP
|
||||
leveledUp = currentXP - achievedXP < currentLevelExp
|
||||
#console.debug 'Leveled up' if leveledUp
|
||||
alreadyAchievedPercentage = 100 * (previousXP - currentLevelExp) / totalExpNeeded
|
||||
alreadyAchievedPercentage = 0 if alreadyAchievedPercentage < 0 # In case of level up
|
||||
newlyAchievedPercentage = if leveledUp then 100 * (currentXP - currentLevelExp) / totalExpNeeded else 100 * achievedXP / totalExpNeeded
|
||||
|
||||
#console.debug "Current level is #{currentLevel} (#{currentLevelExp} xp), next level is #{nextLevel} (#{nextLevelXP} xp)."
|
||||
#console.debug "Need a total of #{nextLevelXP - currentLevelExp}, already had #{previousXP} and just now earned #{achievedXP} totalling on #{currentXP}"
|
||||
|
||||
data =
|
||||
title: @achievement.i18nName()
|
||||
imgURL: @achievement.getImageURL()
|
||||
description: @achievement.i18nDescription()
|
||||
level: currentLevel
|
||||
currentXP: currentXP
|
||||
newXP: achievedXP
|
||||
leftXP: nextLevelXP - currentXP
|
||||
oldXPWidth: alreadyAchievedPercentage
|
||||
newXPWidth: newlyAchievedPercentage
|
||||
leftXPWidth: 100 - newlyAchievedPercentage - alreadyAchievedPercentage
|
||||
|
||||
getRenderData: ->
|
||||
c = super()
|
||||
_.extend c, @calculateData()
|
||||
c.style = @achievement.getStyle()
|
||||
c.popup = true
|
||||
c.$ = $ # Allows the jade template to do i18n
|
||||
c
|
||||
|
||||
render: ->
|
||||
console.debug 'render achievement popup'
|
||||
super()
|
||||
@container.prepend @$el
|
||||
if @popup
|
||||
@$el.animate
|
||||
left: 0
|
||||
@$el.on 'click', (e) =>
|
||||
@$el.animate
|
||||
left: 600
|
||||
, =>
|
||||
@$el.remove()
|
||||
@destroy()
|
||||
|
||||
getContainer: ->
|
||||
unless @container
|
||||
@container = $('.achievement-popup-container')
|
||||
unless @container.length
|
||||
$('body').append('<div class="achievement-popup-container"></div>')
|
||||
@container = $('.achievement-popup-container')
|
||||
@container
|
||||
|
||||
afterRender: ->
|
||||
super()
|
||||
_.delay @initializeTooltips, 1000 # TODO this could be smoother
|
||||
|
||||
initializeTooltips: ->
|
||||
$('.progress-bar').tooltip()
|
|
@ -1,7 +1,10 @@
|
|||
RootView = require 'views/kinds/RootView'
|
||||
template = require 'templates/editor/achievement/edit'
|
||||
Achievement = require 'models/Achievement'
|
||||
AchievementPopup = require 'views/achievements/AchievementPopup'
|
||||
ConfirmModal = require 'views/modal/ConfirmModal'
|
||||
errors = require 'lib/errors'
|
||||
app = require 'application'
|
||||
|
||||
module.exports = class AchievementEditView extends RootView
|
||||
id: 'editor-achievement-edit-view'
|
||||
|
@ -11,6 +14,7 @@ module.exports = class AchievementEditView extends RootView
|
|||
events:
|
||||
'click #save-button': 'saveAchievement'
|
||||
'click #recalculate-button': 'confirmRecalculation'
|
||||
'click #delete-button': 'confirmDeletion'
|
||||
|
||||
subscriptions:
|
||||
'save-new': 'saveAchievement'
|
||||
|
@ -20,13 +24,10 @@ module.exports = class AchievementEditView extends RootView
|
|||
@achievement = new Achievement(_id: @achievementID)
|
||||
@achievement.saveBackups = true
|
||||
|
||||
@listenToOnce(@achievement, 'error',
|
||||
() =>
|
||||
@hideLoading()
|
||||
$(@$el).find('.main-content-area').children('*').not('#error-view').remove()
|
||||
|
||||
@insertSubView(new ErrorView())
|
||||
)
|
||||
@achievement.once 'error', (achievement, jqxhr) =>
|
||||
@hideLoading()
|
||||
$(@$el).find('.main-content-area').children('*').not('.breadcrumb').remove()
|
||||
errors.backboneFailure arguments...
|
||||
|
||||
@achievement.fetch()
|
||||
@listenToOnce(@achievement, 'sync', @buildTreema)
|
||||
|
@ -49,17 +50,31 @@ module.exports = class AchievementEditView extends RootView
|
|||
|
||||
@treema.build()
|
||||
|
||||
pushChangesToPreview: =>
|
||||
'TODO' # TODO might want some intrinsic preview thing
|
||||
|
||||
getRenderData: (context={}) ->
|
||||
context = super(context)
|
||||
context.achievement = @achievement
|
||||
context.authorized = me.isAdmin()
|
||||
context
|
||||
|
||||
afterRender: ->
|
||||
super(arguments...)
|
||||
@pushChangesToPreview()
|
||||
|
||||
pushChangesToPreview: =>
|
||||
$('#achievement-view').empty()
|
||||
|
||||
if @treema?
|
||||
for key, value of @treema.data
|
||||
@achievement.set key, value
|
||||
|
||||
earned =
|
||||
earnedPoints: @achievement.get 'worth'
|
||||
|
||||
popup = new AchievementPopup achievement: @achievement, earnedAchievement:earned, popup: false, container: $('#achievement-view')
|
||||
|
||||
|
||||
openSaveModal: ->
|
||||
'Maybe later' # TODO
|
||||
'Maybe later' # TODO patch patch patch
|
||||
|
||||
saveAchievement: (e) ->
|
||||
@treema.endExistingEdits()
|
||||
|
@ -75,20 +90,31 @@ module.exports = class AchievementEditView extends RootView
|
|||
url = "/editor/achievement/#{@achievement.get('slug') or @achievement.id}"
|
||||
document.location.href = url
|
||||
|
||||
confirmRecalculation: (e) ->
|
||||
confirmRecalculation: ->
|
||||
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?'
|
||||
'confirmDecline': 'Not really'
|
||||
'confirmConfirm': 'Definitely'
|
||||
|
||||
confirmModal = new ConfirmModal(renderData)
|
||||
confirmModal.onConfirm @recalculateAchievement
|
||||
confirmModal = new ConfirmModal renderData
|
||||
confirmModal.on 'confirm', @recalculateAchievement
|
||||
@openModalView confirmModal
|
||||
|
||||
confirmDeletion: ->
|
||||
renderData =
|
||||
'confirmTitle': 'Are you really sure?'
|
||||
'confirmBody': 'This will completely delete the achievement, potentially breaking a lot of stuff you don\'t want breaking. Are you entirely sure?'
|
||||
'confirmDecline': 'Not really'
|
||||
'confirmConfirm': 'Definitely'
|
||||
|
||||
confirmModal = new ConfirmModal renderData
|
||||
confirmModal.on 'confirm', @deleteAchievement
|
||||
@openModalView confirmModal
|
||||
|
||||
recalculateAchievement: =>
|
||||
$.ajax
|
||||
data: JSON.stringify(achievements: [@achievement.get('slug') or @achievement.get('_id')])
|
||||
data: JSON.stringify(earnedAchievements: [@achievement.get('slug') or @achievement.get('_id')])
|
||||
success: (data, status, jqXHR) ->
|
||||
noty
|
||||
timeout: 5000
|
||||
|
@ -105,3 +131,24 @@ module.exports = class AchievementEditView extends RootView
|
|||
url: '/admin/earned.achievement/recalculate'
|
||||
type: 'POST'
|
||||
contentType: 'application/json'
|
||||
|
||||
deleteAchievement: =>
|
||||
console.debug 'deleting'
|
||||
$.ajax
|
||||
type: 'DELETE'
|
||||
success: ->
|
||||
noty
|
||||
timeout: 5000
|
||||
text: 'Aaaand it\'s gone.'
|
||||
type: 'success'
|
||||
layout: 'topCenter'
|
||||
_.delay ->
|
||||
app.router.navigate '/editor/achievement', trigger: true
|
||||
, 500
|
||||
error: (jqXHR, status, error) ->
|
||||
console.error jqXHR
|
||||
timeout: 5000
|
||||
text: "Deleting achievement failed with error code #{jqXHR.status}"
|
||||
type: 'error'
|
||||
layout: 'topCenter'
|
||||
url: "/db/achievement/#{@achievement.id}"
|
||||
|
|
|
@ -15,6 +15,7 @@ SaveLevelModal = require './modals/SaveLevelModal'
|
|||
LevelForkView = require './modals/ForkLevelModal'
|
||||
SaveVersionModal = require 'views/modal/SaveVersionModal'
|
||||
PatchesView = require 'views/editor/PatchesView'
|
||||
RelatedAchievementsView = require 'views/editor/level/RelatedAchievementsView'
|
||||
VersionHistoryView = require './modals/LevelVersionsModal'
|
||||
ComponentDocsView = require 'views/docs/ComponentDocumentationView'
|
||||
|
||||
|
@ -75,7 +76,8 @@ module.exports = class LevelEditView extends RootView
|
|||
@insertSubView new ScriptsTabView world: @world, supermodel: @supermodel, files: @files
|
||||
@insertSubView new ComponentsTabView supermodel: @supermodel
|
||||
@insertSubView new SystemsTabView supermodel: @supermodel
|
||||
@insertSubView new ComponentDocsView()
|
||||
@insertSubView new RelatedAchievementsView supermodel: @supermodel, level: @level
|
||||
@insertSubView new ComponentDocsView supermodel: @supermodel
|
||||
|
||||
Backbone.Mediator.publish 'level-loaded', level: @level
|
||||
@showReadOnly() if me.get('anonymous')
|
||||
|
|
40
app/views/editor/level/RelatedAchievementsView.coffee
Normal file
|
@ -0,0 +1,40 @@
|
|||
CocoView = require 'views/kinds/CocoView'
|
||||
template = require 'templates/editor/level/related-achievements'
|
||||
RelatedAchievementsCollection = require 'collections/RelatedAchievementsCollection'
|
||||
Achievement = require 'models/Achievement'
|
||||
NewAchievementModal = require './modals/NewAchievementModal'
|
||||
app = require 'application'
|
||||
|
||||
module.exports = class RelatedAchievementsView extends CocoView
|
||||
id: 'related-achievements-view'
|
||||
template: template
|
||||
className: 'tab-pane'
|
||||
|
||||
events:
|
||||
'click #new-achievement-button': 'makeNewAchievement'
|
||||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
@level = options.level
|
||||
@relatedID = @level.id
|
||||
@achievements = new RelatedAchievementsCollection @relatedID
|
||||
@supermodel.loadCollection @achievements, 'achievements'
|
||||
|
||||
onLoaded: ->
|
||||
console.debug 'related achievements loaded'
|
||||
@achievements.loading = false
|
||||
super()
|
||||
|
||||
getRenderData: ->
|
||||
c = super()
|
||||
c.achievements = @achievements
|
||||
c.relatedID = @relatedID
|
||||
c
|
||||
|
||||
onNewAchievementSaved: (achievement) ->
|
||||
app.router.navigate('/editor/achievement/' + (achievement.get('slug') or achievement.id), {trigger: true})
|
||||
|
||||
makeNewAchievement: ->
|
||||
modal = new NewAchievementModal model: Achievement, modelLabel: 'Achievement', level: @level
|
||||
modal.once 'model-created', @onNewAchievementSaved
|
||||
@openModalView modal
|
54
app/views/editor/level/modals/NewAchievementModal.coffee
Normal file
|
@ -0,0 +1,54 @@
|
|||
NewModelModal = require 'views/modal/NewModelModal'
|
||||
template = require 'templates/editor/level/modal/new-achievement'
|
||||
forms = require 'lib/forms'
|
||||
Achievement = require 'models/Achievement'
|
||||
|
||||
module.exports = class NewAchievementModal extends NewModelModal
|
||||
id: 'new-achievement-modal'
|
||||
template: template
|
||||
plain: false
|
||||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
@level = options.level
|
||||
|
||||
getRenderData: ->
|
||||
c = super()
|
||||
c.level = @level
|
||||
console.debug 'level', c.level
|
||||
c
|
||||
|
||||
createQuery: ->
|
||||
checked = @$el.find('[name=queryOptions]:checked')
|
||||
checkedValues = ($(check).val() for check in checked)
|
||||
subQueries = []
|
||||
for id in checkedValues
|
||||
switch id
|
||||
when 'misc-level-completion'
|
||||
subQueries.push state: complete: true
|
||||
else # It's a goal
|
||||
q = state: goalStates: {}
|
||||
q.state.goalStates[id] = {}
|
||||
q.state.goalStates[id].status = 'success'
|
||||
subQueries.push q
|
||||
unless subQueries.length
|
||||
query = {}
|
||||
else if subQueries.length is 1
|
||||
query = subQueries[0]
|
||||
else
|
||||
query = $or: subQueries
|
||||
query
|
||||
|
||||
makeNewModel: ->
|
||||
achievement = new Achievement
|
||||
name = @$el.find('#name').val()
|
||||
description = @$el.find('#description').val()
|
||||
query = @createQuery()
|
||||
|
||||
achievement.set 'name', name
|
||||
achievement.set 'description', description
|
||||
achievement.set 'query', query
|
||||
achievement.set 'collection', 'level.sessions'
|
||||
achievement.set 'userField', 'creator'
|
||||
|
||||
achievement
|
|
@ -6,8 +6,9 @@ CocoView = require './CocoView'
|
|||
{logoutUser, me} = require('lib/auth')
|
||||
locale = require 'locale/locale'
|
||||
|
||||
Achievement = require '../../models/Achievement'
|
||||
User = require '../../models/User'
|
||||
AchievementPopup = require 'views/achievements/AchievementPopup'
|
||||
utils = require 'lib/utils'
|
||||
|
||||
# TODO remove
|
||||
|
||||
filterKeyboardEvents = (allowedEvents, func) ->
|
||||
|
@ -32,61 +33,13 @@ module.exports = class RootView extends CocoView
|
|||
'achievements:new': 'handleNewAchievements'
|
||||
|
||||
showNewAchievement: (achievement, earnedAchievement) ->
|
||||
currentLevel = me.level()
|
||||
nextLevel = currentLevel + 1
|
||||
currentLevelExp = User.expForLevel(currentLevel)
|
||||
nextLevelExp = User.expForLevel(nextLevel)
|
||||
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
|
||||
leveledUp = currentExp - achievedExp < currentLevelExp
|
||||
alreadyAchievedPercentage = 100 * (previousExp - currentLevelExp) / 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 "Need a total of #{nextLevelExp - currentLevelExp}, already had #{previousExp} and just now earned #{achievedExp} totalling on #{currentExp}"
|
||||
|
||||
alreadyAchievedBar = $("<div class='progress-bar progress-bar-warning' style='width:#{alreadyAchievedPercentage}%'></div>")
|
||||
newlyAchievedBar = $("<div data-toggle='tooltip' class='progress-bar progress-bar-success' style='width:#{newlyAchievedPercentage}%'></div>")
|
||||
emptyBar = $("<div data-toggle='tooltip' class='progress-bar progress-bar-white' style='width:#{100 - newlyAchievedPercentage - alreadyAchievedPercentage}%'></div>")
|
||||
progressBar = $('<div class="progress" data-toggle="tooltip"></div>').append(alreadyAchievedBar).append(newlyAchievedBar).append(emptyBar)
|
||||
message = if (currentLevel isnt 1) and leveledUp then "Reached level #{currentLevel}!" else null
|
||||
|
||||
alreadyAchievedBar.tooltip(title: "#{currentExp} XP in total")
|
||||
newlyAchievedBar.tooltip(title: "#{achievedExp} XP earned")
|
||||
emptyBar.tooltip(title: "#{nextLevelExp - currentExp} XP until level #{nextLevel}")
|
||||
|
||||
# TODO a default should be linked here
|
||||
imageURL = '/file/' + achievement.get('icon')
|
||||
data =
|
||||
title: achievement.get('name')
|
||||
image: $("<img src='#{imageURL}' />")
|
||||
description: achievement.get('description')
|
||||
progressBar: progressBar
|
||||
earnedExp: "+ #{achievedExp} XP"
|
||||
message: message
|
||||
|
||||
options =
|
||||
autoHideDelay: 10000
|
||||
globalPosition: 'bottom right'
|
||||
showDuration: 400
|
||||
style: 'achievement'
|
||||
autoHide: true
|
||||
clickToHide: true
|
||||
|
||||
$.notify( data, options )
|
||||
popup = new AchievementPopup achievement: achievement, earnedAchievement: earnedAchievement
|
||||
|
||||
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())
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
RootView = require 'views/kinds/RootView'
|
||||
NewModelModal = require 'views/modal/NewModelModal'
|
||||
template = require 'templates/kinds/search'
|
||||
forms = require 'lib/forms'
|
||||
app = require 'application'
|
||||
|
||||
class SearchCollection extends Backbone.Collection
|
||||
|
@ -26,9 +26,7 @@ module.exports = class SearchView extends RootView
|
|||
events:
|
||||
'change input#search': 'runSearch'
|
||||
'keydown input#search': 'runSearch'
|
||||
'click button.new-model-submit': 'makeNewModel'
|
||||
'submit form': 'makeNewModel'
|
||||
'shown.bs.modal #new-model-modal': 'focusOnName'
|
||||
'click #new-model-button': 'newModel'
|
||||
'hidden.bs.modal #new-model-modal': 'onModalHidden'
|
||||
|
||||
constructor: (options) ->
|
||||
|
@ -79,31 +77,11 @@ module.exports = class SearchView extends RootView
|
|||
@collection.off()
|
||||
@collection = null
|
||||
|
||||
makeNewModel: (e) ->
|
||||
e.preventDefault()
|
||||
name = @$el.find('#name').val()
|
||||
model = new @model()
|
||||
model.set('name', name)
|
||||
if @model.schema.properties.permissions
|
||||
model.set 'permissions', [{access: 'owner', target: me.id}]
|
||||
res = model.save()
|
||||
return unless res
|
||||
|
||||
modal = @$el.find('#new-model-modal')
|
||||
forms.clearFormAlerts(modal)
|
||||
@showLoading(modal.find('.modal-body'))
|
||||
res.error =>
|
||||
@hideLoading()
|
||||
forms.applyErrorsToForm(modal, JSON.parse(res.responseText))
|
||||
that = @
|
||||
res.success ->
|
||||
that.model = model
|
||||
modal.modal('hide')
|
||||
|
||||
onModalHidden: ->
|
||||
# Can only redirect after the modal hidden event has triggered
|
||||
onNewModelSaved: (@model) ->
|
||||
base = document.location.pathname[1..] + '/'
|
||||
app.router.navigate(base + (@model.get('slug') or @model.id), {trigger: true})
|
||||
|
||||
focusOnName: ->
|
||||
@$el.find('#name').focus()
|
||||
newModel: (e) ->
|
||||
modal = new NewModelModal model: @model, modelLabel: @modelLabel
|
||||
modal.once 'success', @onNewModelSaved
|
||||
@openModalView modal
|
||||
|
|
35
app/views/kinds/UserView.coffee
Normal file
|
@ -0,0 +1,35 @@
|
|||
RootView = require 'views/kinds/RootView'
|
||||
template = require 'templates/kinds/user'
|
||||
User = require 'models/User'
|
||||
|
||||
module.exports = class UserView extends RootView
|
||||
template: template
|
||||
className: 'user-view'
|
||||
viewName: null # Used for the breadcrumbs
|
||||
|
||||
constructor: (@userID, options) ->
|
||||
super options
|
||||
@listenTo @, 'userNotFound', @ifUserNotFound
|
||||
@fetchUser @userID
|
||||
|
||||
fetchUser: (id) ->
|
||||
if @isMe()
|
||||
@user = me
|
||||
@onLoaded()
|
||||
@user = new User _id: id
|
||||
@supermodel.loadModel @user, 'user'
|
||||
|
||||
getRenderData: ->
|
||||
context = super()
|
||||
context.viewName = @viewName
|
||||
context.user = @user unless @user?.isAnonymous()
|
||||
context
|
||||
|
||||
isMe: -> @userID is me.id
|
||||
|
||||
onLoaded: ->
|
||||
super()
|
||||
|
||||
ifUserNotFound: ->
|
||||
console.warn 'user not found'
|
||||
@render()
|
|
@ -8,8 +8,8 @@ module.exports = class ConfirmModal extends ModalView
|
|||
closeOnConfirm: true
|
||||
|
||||
events:
|
||||
'click #decline-button': 'doDecline'
|
||||
'click #confirm-button': 'doConfirm'
|
||||
'click #decline-button': 'onDecline'
|
||||
'click #confirm-button': 'onConfirm'
|
||||
|
||||
constructor: (@renderData={}, options={}) ->
|
||||
super(options)
|
||||
|
@ -21,10 +21,6 @@ module.exports = class ConfirmModal extends ModalView
|
|||
|
||||
setRenderData: (@renderData) ->
|
||||
|
||||
onDecline: (@decline) ->
|
||||
onDecline: -> @trigger 'decline'
|
||||
|
||||
onConfirm: (@confirm) ->
|
||||
|
||||
doConfirm: -> @confirm() if @confirm
|
||||
|
||||
doDecline: -> @decline() if @decline
|
||||
onConfirm: -> @trigger 'confirm'
|
||||
|
|
54
app/views/modal/NewModelModal.coffee
Normal file
|
@ -0,0 +1,54 @@
|
|||
ModalView = require 'views/kinds/ModalView'
|
||||
template = require 'templates/modal/new_model'
|
||||
forms = require 'lib/forms'
|
||||
|
||||
module.exports = class NewModelModal extends ModalView
|
||||
id: 'new-model-modal'
|
||||
template: template
|
||||
plain: false
|
||||
|
||||
events:
|
||||
'click button.new-model-submit': 'onModelSubmitted'
|
||||
'submit form': 'onModelSubmitted'
|
||||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
@model = options.model
|
||||
@modelLabel = options.modelLabel
|
||||
@properties = options.properties
|
||||
$('#name').ready @focusOnName
|
||||
|
||||
getRenderData: ->
|
||||
c = super()
|
||||
c.modelLabel = @modelLabel
|
||||
#c.newModelTitle = @newModelTitle
|
||||
c
|
||||
|
||||
makeNewModel: ->
|
||||
model = new @model
|
||||
name = @$el.find('#name').val()
|
||||
model.set('name', name)
|
||||
if @model.schema.properties.permissions
|
||||
model.set 'permissions', [{access: 'owner', target: me.id}]
|
||||
model.set(key, prop) for key, prop of @properties if @properties?
|
||||
model
|
||||
|
||||
onModelSubmitted: (e) ->
|
||||
e.preventDefault()
|
||||
model = @makeNewModel()
|
||||
res = model.save()
|
||||
return unless res
|
||||
|
||||
forms.clearFormAlerts @$el
|
||||
@showLoading(@$el.find('.modal-body'))
|
||||
res.error =>
|
||||
@hideLoading()
|
||||
forms.applyErrorsToForm(@$el, JSON.parse(res.responseText))
|
||||
#Backbone.Mediator.publish 'model-save-fail', model
|
||||
res.success =>
|
||||
@$el.modal('hide')
|
||||
@trigger 'model-created', model
|
||||
#Backbone.Mediator.publish 'model-save-success', model
|
||||
|
||||
focusOnName: (e) ->
|
||||
$('#name').focus() # TODO Why isn't this working anymore.. It does get called
|
56
app/views/user/AchievementsView.coffee
Normal file
|
@ -0,0 +1,56 @@
|
|||
UserView = require 'views/kinds/UserView'
|
||||
template = require 'templates/user/achievements'
|
||||
{me} = require 'lib/auth'
|
||||
Achievement = require 'models/Achievement'
|
||||
EarnedAchievement = require 'models/EarnedAchievement'
|
||||
AchievementCollection = require 'collections/AchievementCollection'
|
||||
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
|
||||
|
||||
onLoaded: ->
|
||||
unless @achievements or @earnedAchievements
|
||||
@supermodel.resetProgress()
|
||||
@achievements = new AchievementCollection
|
||||
@earnedAchievements = new EarnedAchievementCollection @user.getSlugOrID()
|
||||
@supermodel.loadCollection @achievements, 'achievements'
|
||||
@supermodel.loadCollection @earnedAchievements, 'earnedAchievements'
|
||||
else
|
||||
for earned in @earnedAchievements.models
|
||||
return unless relatedAchievement = _.find @achievements.models, (achievement) ->
|
||||
achievement.get('_id') is earned.get 'achievement'
|
||||
relatedAchievement.set 'unlocked', true
|
||||
earned.set 'achievement', relatedAchievement
|
||||
deferredImages = (achievement.cacheLockedImage() for achievement in @achievements.models when not achievement.get 'unlocked')
|
||||
whenever = $.when deferredImages...
|
||||
whenever.done => @render()
|
||||
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.earnedAchievements = @earnedAchievements
|
||||
context.achievements = @achievements
|
||||
context.achievementsByCategory = {}
|
||||
for achievement in @achievements.models
|
||||
context.achievementsByCategory[achievement.get('category')] ?= []
|
||||
context.achievementsByCategory[achievement.get('category')].push achievement
|
||||
context
|
|
@ -1,4 +1,4 @@
|
|||
RootView = require 'views/kinds/RootView'
|
||||
UserView = require 'views/kinds/UserView'
|
||||
template = require 'templates/account/profile'
|
||||
User = require 'models/User'
|
||||
LevelSession = require 'models/LevelSession'
|
||||
|
@ -26,7 +26,7 @@ adminContacts = [
|
|||
{id: '52a57252a89409700d0000d9', name: 'Ignore'}
|
||||
]
|
||||
|
||||
module.exports = class JobProfileView extends RootView
|
||||
module.exports = class JobProfileView extends UserView
|
||||
id: 'profile-view'
|
||||
template: template
|
||||
showBackground: false
|
||||
|
@ -54,8 +54,7 @@ module.exports = class JobProfileView extends RootView
|
|||
'change #admin-contact': 'onAdminContactChanged'
|
||||
'click .session-link': 'onSessionLinkPressed'
|
||||
|
||||
constructor: (options, @userID) ->
|
||||
@userID ?= me.id
|
||||
constructor: (userID, options) ->
|
||||
@onJobProfileNotesChanged = _.debounce @onJobProfileNotesChanged, 1000
|
||||
@onRemarkChanged = _.debounce @onRemarkChanged, 1000
|
||||
@authorizedWithLinkedIn = IN?.User?.isAuthorized()
|
||||
|
@ -64,32 +63,19 @@ module.exports = class JobProfileView extends RootView
|
|||
window.contractCallback = =>
|
||||
@authorizedWithLinkedIn = IN?.User?.isAuthorized()
|
||||
@render()
|
||||
super options
|
||||
if me.get('anonymous') is true
|
||||
@render()
|
||||
return
|
||||
if User.isObjectID @userID
|
||||
@finishInit()
|
||||
else
|
||||
$.ajax "/db/user/#{@userID}/nameToID", success: (@userID) =>
|
||||
@finishInit() unless @destroyed
|
||||
@render()
|
||||
super options, userID
|
||||
|
||||
onUserLoaded: ->
|
||||
@finishInit() unless @destroyed
|
||||
super()
|
||||
|
||||
finishInit: ->
|
||||
return unless @userID
|
||||
@uploadFilePath = "db/user/#{@userID}"
|
||||
@highlightedContainers = []
|
||||
if @userID is me.id
|
||||
@user = me
|
||||
else if me.isAdmin() or 'employer' in me.get('permissions')
|
||||
@user = User.getByID(@userID)
|
||||
@user.fetch()
|
||||
@listenTo @user, 'sync', =>
|
||||
@render()
|
||||
if me.isAdmin() or 'employer' in me.get('permissions')
|
||||
$.post "/db/user/#{me.id}/track/view_candidate"
|
||||
$.post "/db/user/#{@userID}/track/viewed_by_employer" unless me.isAdmin()
|
||||
else
|
||||
@user = User.getByID(@userID)
|
||||
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(@userID), 'candidate_sessions').model
|
||||
if me.isAdmin()
|
||||
# Mimicking how the VictoryModal fetches LevelFeedback
|
||||
|
@ -248,7 +234,7 @@ module.exports = class JobProfileView extends RootView
|
|||
jobProfile.name ?= (@user.get('firstName') + ' ' + @user.get('lastName')).trim() if @user?.get('firstName')
|
||||
context.profile = jobProfile
|
||||
context.user = @user
|
||||
context.myProfile = @user?.id is context.me.id
|
||||
context.myProfile = @isMe()
|
||||
context.allowedToViewJobProfile = @user and (me.isAdmin() or 'employer' in me.get('permissions') or (context.myProfile && !me.get('anonymous')))
|
||||
context.allowedToEditJobProfile = @user and (me.isAdmin() or (context.myProfile && !me.get('anonymous')))
|
||||
context.profileApproved = @user?.get 'jobProfileApproved'
|
||||
|
@ -289,7 +275,7 @@ module.exports = class JobProfileView extends RootView
|
|||
_.delay ->
|
||||
justSavedSection.removeClass 'just-saved', duration: 1500, easing: 'easeOutQuad'
|
||||
, 500
|
||||
if me.isAdmin()
|
||||
if me.isAdmin() and @user
|
||||
visibleSettings = ['history', 'tasks']
|
||||
data = _.pick (@remark.attributes), (value, key) -> key in visibleSettings
|
||||
data.history ?= []
|
||||
|
@ -596,4 +582,4 @@ module.exports = class JobProfileView extends RootView
|
|||
sessionID = $(e.target).closest('.session-link').data('session-id')
|
||||
session = _.find @sessions.models, (session) -> session.id is sessionID
|
||||
modal = new JobProfileCodeModal({session:session})
|
||||
@openModalView modal
|
||||
@openModalView modal
|
54
app/views/user/MainUserView.coffee
Normal file
|
@ -0,0 +1,54 @@
|
|||
UserView = require 'views/kinds/UserView'
|
||||
CocoCollection = require 'collections/CocoCollection'
|
||||
LevelSession = require 'models/LevelSession'
|
||||
template = require 'templates/user/user_home'
|
||||
{me} = require 'lib/auth'
|
||||
EarnedAchievementCollection = require 'collections/EarnedAchievementCollection'
|
||||
|
||||
class LevelSessionsCollection extends CocoCollection
|
||||
model: LevelSession
|
||||
|
||||
constructor: (userID) ->
|
||||
@url = "/db/user/#{userID}/level.sessions?project=state.complete,levelID,levelName,changed,team,submittedCodeLanguage,totalScore&order=-1"
|
||||
super()
|
||||
|
||||
module.exports = class MainUserView extends UserView
|
||||
id: 'user-home'
|
||||
template: template
|
||||
|
||||
constructor: (userID, options) ->
|
||||
super options
|
||||
|
||||
getRenderData: ->
|
||||
context = super()
|
||||
if @levelSessions and @levelSessions.loaded
|
||||
singlePlayerSessions = []
|
||||
multiPlayerSessions = []
|
||||
languageCounts = {}
|
||||
for levelSession in @levelSessions.models
|
||||
if levelSession.isMultiplayer()
|
||||
multiPlayerSessions.push levelSession
|
||||
else
|
||||
singlePlayerSessions.push levelSession
|
||||
languageCounts[levelSession.get 'submittedCodeLanguage'] = (languageCounts[levelSession.get 'submittedCodeLanguage'] or 0) + 1
|
||||
mostUsedCount = 0
|
||||
favoriteLanguage = null
|
||||
for language, count of languageCounts
|
||||
if count > mostUsedCount
|
||||
mostUsedCount = count
|
||||
favoriteLanguage = language
|
||||
context.singlePlayerSessions = singlePlayerSessions
|
||||
context.multiPlayerSessions = multiPlayerSessions
|
||||
context.favoriteLanguage = favoriteLanguage
|
||||
if @earnedAchievements and @earnedAchievements.loaded
|
||||
context.earnedAchievements = @earnedAchievements
|
||||
context
|
||||
|
||||
onLoaded: ->
|
||||
if @user.loaded and not (@earnedAchievements or @levelSessions)
|
||||
@supermodel.resetProgress()
|
||||
@levelSessions = new LevelSessionsCollection @user.getSlugOrID()
|
||||
@earnedAchievements = new EarnedAchievementCollection @user.getSlugOrID()
|
||||
@supermodel.loadCollection @levelSessions, 'levelSessions'
|
||||
@supermodel.loadCollection @earnedAchievements, 'earnedAchievements'
|
||||
super()
|
|
@ -42,6 +42,7 @@
|
|||
"winston": "0.6.x",
|
||||
"passport": "0.1.x",
|
||||
"passport-local": "0.1.x",
|
||||
"moment": "~2.5.0",
|
||||
"mongodb": "1.2.x",
|
||||
"mongoose": "3.8.x",
|
||||
"mongoose-text-search": "~0.0.2",
|
||||
|
|
BIN
public/images/achievements/achievement_background.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
public/images/achievements/achievement_background_light.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
public/images/achievements/achievement_background_locked.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
public/images/achievements/bar_border.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
public/images/achievements/border_diamond.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
public/images/achievements/border_diamond_locked.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
public/images/achievements/border_gold.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
public/images/achievements/border_gold_locked.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/images/achievements/border_silver.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/images/achievements/border_silver_locked.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/images/achievements/border_stone.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
public/images/achievements/border_stone_locked.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
public/images/achievements/border_wood.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
public/images/achievements/border_wood_locked.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
public/images/achievements/cross-01.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
public/images/achievements/cup-01.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
public/images/achievements/cup-02.png
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
public/images/achievements/default.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
public/images/achievements/level-bg.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
public/images/achievements/message-01.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
public/images/achievements/patch-01.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
public/images/achievements/pendant-01.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
public/images/achievements/scroll-01.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
public/images/achievements/star.png
Normal file
After Width: | Height: | Size: 8.8 KiB |
BIN
public/images/achievements/swords-01.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
public/images/pages/user/adventurer.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
public/images/pages/user/ambassador.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
public/images/pages/user/archmage.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
public/images/pages/user/artisan.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
public/images/pages/user/diplomat.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
public/images/pages/user/general.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
public/images/pages/user/scribe.png
Normal file
After Width: | Height: | Size: 84 KiB |
19
scripts/recalculateAchievements.coffee
Normal file
|
@ -0,0 +1,19 @@
|
|||
database = require '../server/commons/database'
|
||||
mongoose = require 'mongoose'
|
||||
log = require 'winston'
|
||||
async = require 'async'
|
||||
|
||||
### SET UP ###
|
||||
do (setupLodash = this) ->
|
||||
GLOBAL._ = require 'lodash'
|
||||
_.str = require 'underscore.string'
|
||||
_.mixin _.str.exports()
|
||||
|
||||
database.connect()
|
||||
|
||||
EarnedAchievementHandler = require '../server/achievements/earned_achievement_handler'
|
||||
log.info 'Starting earned achievement recalculation...'
|
||||
EarnedAchievementHandler.constructor.recalculate (err) ->
|
||||
log.error err if err?
|
||||
log.info 'Finished recalculating all earned achievements.'
|
||||
process.exit()
|
54
scripts/recalculateStatistics.coffee
Normal file
|
@ -0,0 +1,54 @@
|
|||
database = require '../server/commons/database'
|
||||
mongoose = require 'mongoose'
|
||||
log = require 'winston'
|
||||
async = require 'async'
|
||||
|
||||
### SET UP ###
|
||||
do (setupLodash = this) ->
|
||||
GLOBAL._ = require 'lodash'
|
||||
_.str = require 'underscore.string'
|
||||
_.mixin _.str.exports()
|
||||
|
||||
database.connect()
|
||||
|
||||
### USER STATS ###
|
||||
UserHandler = require '../server/users/user_handler'
|
||||
|
||||
report = (func, name, done) ->
|
||||
log.info 'Started ' + name + '...'
|
||||
func name, (err) ->
|
||||
log.warn err if err?
|
||||
log.info 'Finished ' + name
|
||||
done err if done?
|
||||
|
||||
whenAllFinished = ->
|
||||
log.info 'All recalculations finished.'
|
||||
process.exit()
|
||||
|
||||
async.parallel [
|
||||
# Misc
|
||||
(c) -> report UserHandler.recalculateStats, 'gamesCompleted', c
|
||||
# Edits
|
||||
(c) -> report UserHandler.recalculateStats, 'articleEdits', c
|
||||
(c) -> report UserHandler.recalculateStats, 'levelEdits', c
|
||||
(c) -> report UserHandler.recalculateStats, 'levelComponentEdits', c
|
||||
(c) -> report UserHandler.recalculateStats, 'levelSystemEdits', c
|
||||
(c) -> report UserHandler.recalculateStats, 'thangTypeEdits', c
|
||||
# Patches
|
||||
(c) -> report UserHandler.recalculateStats, 'patchesContributed', c
|
||||
(c) -> report UserHandler.recalculateStats, 'patchesSubmitted', c
|
||||
(c) -> report UserHandler.recalculateStats, 'totalTranslationPatches', c
|
||||
(c) -> report UserHandler.recalculateStats, 'totalMiscPatches', c
|
||||
|
||||
(c) -> report UserHandler.recalculateStats, 'articleMiscPatches', c
|
||||
(c) -> report UserHandler.recalculateStats, 'levelMiscPatches', c
|
||||
(c) -> report UserHandler.recalculateStats, 'levelComponentMiscPatches', c
|
||||
(c) -> report UserHandler.recalculateStats, 'levelSystemMiscPatches', c
|
||||
(c) -> report UserHandler.recalculateStats, 'thangTypeMiscPatches', c
|
||||
|
||||
(c) -> report UserHandler.recalculateStats, 'articleTranslationPatches', c
|
||||
(c) -> report UserHandler.recalculateStats, 'levelTranslationPatches', c
|
||||
(c) -> report UserHandler.recalculateStats, 'levelComponentTranslationPatches', c
|
||||
(c) -> report UserHandler.recalculateStats, 'levelSystemTranslationPatches', c
|
||||
(c) -> report UserHandler.recalculateStats, 'thangTypeTranslationPatches', c
|
||||
], whenAllFinished
|
232
scripts/setupAchievements.coffee
Normal file
|
@ -0,0 +1,232 @@
|
|||
database = require '../server/commons/database'
|
||||
mongoose = require 'mongoose'
|
||||
log = require 'winston'
|
||||
async = require 'async'
|
||||
|
||||
### SET UP ###
|
||||
do (setupLodash = this) ->
|
||||
GLOBAL._ = require 'lodash'
|
||||
_.str = require 'underscore.string'
|
||||
_.mixin _.str.exports()
|
||||
|
||||
database.connect()
|
||||
|
||||
|
||||
## Util
|
||||
|
||||
## Types
|
||||
contributor = (obj) ->
|
||||
_.extend obj, # This way we get the name etc on top
|
||||
collection: 'users'
|
||||
userField: '_id'
|
||||
category: 'contributor'
|
||||
|
||||
### UNLOCKABLES ###
|
||||
# Generally ordered according to user.stats schema
|
||||
unlockableAchievements =
|
||||
signup:
|
||||
name: 'Signed Up'
|
||||
description: 'Signed up to the most awesome coding game around.'
|
||||
query: 'anonymous': false
|
||||
worth: 10
|
||||
collection: 'users'
|
||||
userField: '_id'
|
||||
category: 'miscellaneous'
|
||||
difficulty: 1
|
||||
recalculable: true
|
||||
|
||||
completedFirstLevel:
|
||||
name: 'Completed 1 Level'
|
||||
description: 'Completed your very first level.'
|
||||
query: 'stats.gamesCompleted': $gte: 1
|
||||
worth: 20
|
||||
collection: 'users'
|
||||
userField: '_id'
|
||||
category: 'levels'
|
||||
difficulty: 1
|
||||
recalculable: true
|
||||
|
||||
completedFiveLevels:
|
||||
name: 'Completed 5 Levels'
|
||||
description: 'Completed 5 Levels.'
|
||||
query: 'stats.gamesCompleted': $gte: 5
|
||||
worth: 50
|
||||
collection: 'users'
|
||||
userField: '_id'
|
||||
category: 'levels'
|
||||
difficulty: 2
|
||||
recalculable: true
|
||||
|
||||
completedTwentyLevels:
|
||||
name: 'Completed 20 Levels'
|
||||
description: 'Completed 20 Levels.'
|
||||
query: 'stats.gamesCompleted': $gte: 20
|
||||
worth: 500
|
||||
collection: 'users'
|
||||
userField: '_id'
|
||||
category: 'levels'
|
||||
difficulty: 3
|
||||
recalculable: true
|
||||
|
||||
editedOneArticle: contributor
|
||||
name: 'Edited an Article'
|
||||
description: 'Edited your first Article.'
|
||||
query: 'stats.articleEdits': $gte: 1
|
||||
worth: 50
|
||||
difficulty: 1
|
||||
|
||||
editedOneLevel: contributor
|
||||
name: 'Edited a Level'
|
||||
description: 'Edited your first Level.'
|
||||
query: 'stats.levelEdits': $gte: 1
|
||||
worth: 50
|
||||
difficulty: 1
|
||||
recalculable: true
|
||||
|
||||
editedOneLevelSystem: contributor
|
||||
name: 'Edited a Level System'
|
||||
description: 'Edited your first Level System.'
|
||||
query: 'stats.levelSystemEdits': $gte: 1
|
||||
worth: 50
|
||||
difficulty: 1
|
||||
recalculable: true
|
||||
|
||||
editedOneLevelComponent: contributor
|
||||
name: 'Edited a Level Component'
|
||||
description: 'Edited your first Level Component.'
|
||||
query: 'stats.levelComponentEdits': $gte: 1
|
||||
worth: 50
|
||||
difficulty: 1
|
||||
recalculable: true
|
||||
|
||||
editedOneThangType: contributor
|
||||
name: 'Edited a Thang Type'
|
||||
description: 'Edited your first Thang Type.'
|
||||
query: 'stats.thangTypeEdits': $gte: 1
|
||||
worth: 50
|
||||
difficulty: 1
|
||||
recalculable: true
|
||||
|
||||
submittedOnePatch: contributor
|
||||
name: 'Submitted a Patch'
|
||||
description: 'Submitted your very first patch.'
|
||||
query: 'stats.patchesSubmitted': $gte: 1
|
||||
worth: 50
|
||||
difficulty: 1
|
||||
recalculable: true
|
||||
|
||||
contributedOnePatch: contributor
|
||||
name: 'Contributed a Patch'
|
||||
description: 'Got your very first accepted Patch.'
|
||||
query: 'stats.patchesContributed': $gte: 1
|
||||
worth: 50
|
||||
difficulty: 1
|
||||
recalculable: true
|
||||
|
||||
acceptedOnePatch: contributor
|
||||
name: 'Accepted a Patch'
|
||||
description: 'Accepted your very first patch.'
|
||||
query: 'stats.patchesAccepted': $gte: 1
|
||||
worth: 50
|
||||
difficulty: 1
|
||||
recalculable: false
|
||||
|
||||
oneTranslationPatch: contributor
|
||||
name: 'First Translation'
|
||||
description: 'Did your very first translation.'
|
||||
query: 'stats.totalTranslationPatches': $gte: 1
|
||||
worth: 50
|
||||
difficulty: 1
|
||||
recalculable: true
|
||||
|
||||
oneMiscPatch: contributor
|
||||
name: 'First Miscellaneous Patch'
|
||||
description: 'Did your first miscellaneous patch.'
|
||||
query: 'stats.totalMiscPatches': $gte: 1
|
||||
worth: 50
|
||||
difficulty: 1
|
||||
recalculable: true
|
||||
|
||||
oneArticleTranslationPatch: contributor
|
||||
name: 'First Article Translation'
|
||||
description: 'Did your very first Article translation.'
|
||||
query: 'stats.articleTranslationPatches': $gte: 1
|
||||
worth: 50
|
||||
difficulty: 1
|
||||
recalculable: true
|
||||
|
||||
oneArticleMiscPatch: contributor
|
||||
name: 'First Misc Article Patch'
|
||||
description: 'Did your first miscellaneous Article patch.'
|
||||
query: 'stats.totalMiscPatches': $gte: 1
|
||||
worth: 50
|
||||
difficulty: 1
|
||||
recalculable: true
|
||||
|
||||
oneLevelTranslationPatch: contributor
|
||||
name: 'First Level Translation'
|
||||
description: 'Did your very first Level translation.'
|
||||
query: 'stats.levelTranslationPatches': $gte: 1
|
||||
worth: 50
|
||||
difficulty: 1
|
||||
recalculable: true
|
||||
|
||||
oneLevelMiscPatch: contributor
|
||||
name: 'First Misc Level Patch'
|
||||
description: 'Did your first misc Level patch.'
|
||||
query: 'stats.levelMiscPatches': $gte: 1
|
||||
worth: 50
|
||||
difficulty: 1
|
||||
recalculable: true
|
||||
|
||||
|
||||
### REPEATABLES ###
|
||||
repeatableAchievements =
|
||||
simulatedBy:
|
||||
name: 'Simulated ladder game'
|
||||
description: 'Simulated a ladder game.'
|
||||
query: 'simulatedBy': $gte: 1
|
||||
worth: 1
|
||||
collection: 'users'
|
||||
userField: '_id'
|
||||
category: 'miscellaneous'
|
||||
difficulty: 1
|
||||
proportionalTo: 'simulatedBy'
|
||||
function:
|
||||
kind: 'logarithmic'
|
||||
parameters: # TODO tweak
|
||||
a: 5
|
||||
b: 1
|
||||
c: 0
|
||||
|
||||
Achievement = require '../server/achievements/Achievement'
|
||||
EarnedAchievement = require '../server/achievements/EarnedAchievement'
|
||||
|
||||
Achievement.find {}, (err, achievements) ->
|
||||
achievementIDs = (achievement.get('_id') + '' for achievement in achievements)
|
||||
EarnedAchievement.remove {achievement: $in: achievementIDs}, (err, count) ->
|
||||
return log.error err if err?
|
||||
log.info "Removed #{count} earned achievements that were related"
|
||||
|
||||
Achievement.remove {}, (err) ->
|
||||
log.error err if err?
|
||||
log.info 'Removed all achievements.'
|
||||
|
||||
log.info "Got #{Object.keys(unlockableAchievements).length} unlockable achievements"
|
||||
log.info "and #{Object.keys(repeatableAchievements).length} repeatable achievements"
|
||||
achievements = _.extend unlockableAchievements, repeatableAchievements
|
||||
|
||||
async.each Object.keys(achievements), (key, callback) ->
|
||||
achievement = achievements[key]
|
||||
log.info "Setting up '#{achievement.name}'..."
|
||||
achievementM = new Achievement achievement
|
||||
# What the actual * Mongoose? It automatically converts 'stats.edits' to a nested object
|
||||
achievementM.set 'query', achievement.query
|
||||
log.debug JSON.stringify achievementM.get 'query'
|
||||
achievementM.save (err) ->
|
||||
log.error err if err?
|
||||
callback()
|
||||
, (err) ->
|
||||
log.error err if err?
|
||||
log.info 'Finished setting up achievements.'
|
||||
process.exit()
|
|
@ -1,8 +1,9 @@
|
|||
mongoose = require 'mongoose'
|
||||
jsonschema = require '../../app/schemas/models/achievement'
|
||||
log = require 'winston'
|
||||
util = require '../../app/lib/utils'
|
||||
plugins = require '../plugins/plugins'
|
||||
utils = require '../../app/lib/utils'
|
||||
plugins = require('../plugins/plugins')
|
||||
AchievablePlugin = require '../plugins/achievements'
|
||||
|
||||
# `pre` and `post` are not called for update operations executed directly on the database,
|
||||
# including `Model.update`,`.findByIdAndUpdate`,`.findOneAndUpdate`, `.findOneAndRemove`,and `.findByIdAndRemove`.order
|
||||
|
@ -23,23 +24,48 @@ AchievementSchema.methods.objectifyQuery = ->
|
|||
AchievementSchema.methods.stringifyQuery = ->
|
||||
@set('query', JSON.stringify(@get('query'))) if typeof @get('query') != 'string'
|
||||
|
||||
getExpFunction: ->
|
||||
kind = @get('function')?.kind or jsonschema.function.default.kind
|
||||
parameters = @get('function')?.parameters or jsonschema.function.default.parameters
|
||||
return utils.functionCreators[kind](parameters) if kind of utils.functionCreators
|
||||
AchievementSchema.methods.getExpFunction = ->
|
||||
kind = @get('function')?.kind or jsonschema.properties.function.default.kind
|
||||
parameters = @get('function')?.parameters or jsonschema.properties.function.default.parameters
|
||||
return utils.functionCreators[kind](parameters) if kind of utils.functionCreators
|
||||
|
||||
AchievementSchema.post('init', (doc) -> doc.objectifyQuery())
|
||||
AchievementSchema.methods.isRecalculable = -> @get('recalculable') is true
|
||||
|
||||
AchievementSchema.pre('save', (next) ->
|
||||
AchievementSchema.statics.jsonschema = jsonschema
|
||||
AchievementSchema.statics.earnedAchievements = {}
|
||||
|
||||
# Reloads all achievements into memory.
|
||||
# TODO might want to tweak this to only load new achievements
|
||||
AchievementSchema.statics.loadAchievements = (done) ->
|
||||
AchievementSchema.statics.resetAchievements()
|
||||
Achievement = require('../achievements/Achievement')
|
||||
query = Achievement.find({})
|
||||
query.exec (err, docs) ->
|
||||
_.each docs, (achievement) ->
|
||||
category = achievement.get 'collection'
|
||||
AchievementSchema.statics.earnedAchievements[category] = [] unless category of AchievementSchema.statics.earnedAchievements
|
||||
AchievementSchema.statics.earnedAchievements[category].push achievement
|
||||
done?(AchievementSchema.statics.earnedAchievements)
|
||||
|
||||
AchievementSchema.statics.getLoadedAchievements = ->
|
||||
AchievementSchema.statics.earnedAchievements
|
||||
|
||||
AchievementSchema.statics.resetAchievements = ->
|
||||
delete AchievementSchema.statics.earnedAchievements[category] for category of AchievementSchema.statics.earnedAchievements
|
||||
|
||||
# Queries are stored as JSON strings, objectify them upon loading
|
||||
AchievementSchema.post 'init', (doc) -> doc.objectifyQuery()
|
||||
|
||||
AchievementSchema.pre 'save', (next) ->
|
||||
@stringifyQuery()
|
||||
next()
|
||||
)
|
||||
|
||||
# Reload achievements upon save
|
||||
AchievementSchema.post 'save', -> @constructor.loadAchievements()
|
||||
|
||||
AchievementSchema.plugin(plugins.NamedPlugin)
|
||||
AchievementSchema.plugin(plugins.SearchablePlugin, {searchable: ['name']})
|
||||
|
||||
module.exports = Achievement = mongoose.model('Achievement', AchievementSchema)
|
||||
module.exports = Achievement = mongoose.model('Achievement', AchievementSchema, 'achievements')
|
||||
|
||||
# Reload achievements upon save
|
||||
AchievablePlugin = require '../plugins/achievements'
|
||||
AchievementSchema.post 'save', (doc) -> AchievablePlugin.loadAchievements()
|
||||
AchievementSchema.statics.loadAchievements()
|
||||
|
|
|
@ -5,10 +5,32 @@ class AchievementHandler extends Handler
|
|||
modelClass: Achievement
|
||||
|
||||
# Used to determine which properties requests may edit
|
||||
editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function']
|
||||
editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function', 'related', 'difficulty', 'category', 'recalculable']
|
||||
allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
jsonSchema = require '../../app/schemas/models/achievement.coffee'
|
||||
|
||||
|
||||
hasAccess: (req) ->
|
||||
req.method is 'GET' or req.user?.isAdmin()
|
||||
|
||||
get: (req, res) ->
|
||||
# /db/achievement?related=<ID>
|
||||
if req.query.related
|
||||
return @sendUnauthorizedError(res) if not @hasAccess(req)
|
||||
Achievement.find {related: req.query.related}, (err, docs) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
docs = (@formatEntity(req, doc) for doc in docs)
|
||||
@sendSuccess res, docs
|
||||
else
|
||||
super req, res
|
||||
|
||||
delete: (req, res, slugOrID) ->
|
||||
return @sendUnauthorizedError res unless req.user?.isAdmin()
|
||||
@getDocumentForIdOrSlug slugOrID, (err, document) => # Check first
|
||||
return @sendDatabaseError(res, err) if err
|
||||
return @sendNotFoundError(res) unless document?
|
||||
document.remove (err, document) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
@sendNoContent res
|
||||
|
||||
module.exports = new AchievementHandler()
|
||||
|
|