Merge branch 'master' into production

This commit is contained in:
Nick Winter 2014-09-26 02:32:10 -07:00
commit 6d0c30d69d
23 changed files with 171 additions and 33 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View file

@ -95,7 +95,7 @@
home: home:
slogan: "Learn to Code by Playing a Game" slogan: "Learn to Code by Playing a Game"
no_ie: "CodeCombat does not run in Internet Explorer 9 or older. Sorry!" no_ie: "CodeCombat does not run in Internet Explorer 8 or older. Sorry!"
no_mobile: "CodeCombat wasn't designed for mobile devices and may not work!" no_mobile: "CodeCombat wasn't designed for mobile devices and may not work!"
play: "Play" # The big play button that just starts playing a level play: "Play" # The big play button that just starts playing a level
old_browser: "Uh oh, your browser is too old to run CodeCombat. Sorry!" old_browser: "Uh oh, your browser is too old to run CodeCombat. Sorry!"

View file

@ -64,3 +64,15 @@ module.exports = class User extends CocoModel
level: -> level: ->
User.levelFromExp(@get('points')) User.levelFromExp(@get('points'))
gems: ->
gemsEarned = @get('earned')?.gems ? 0
purchased = @get('purchased') ? {}
gemsPurchased = purchased.gems ? 0
sum = (arr) -> arr?.reduce((a, b) -> a + b) ? 0
gemsSpent = sum(purchased.heroes) + sum(purchased.items) + sum(purchased.levels)
gemsEarned + gemsPurchased - gemsSpent
earnedHero: (heroOriginal) -> heroOriginal in me.get('earned')?.heroes ? []
earnedItem: (itemOriginal) -> itemOriginal in me.get('earned')?.items ? []
earnedLevel: (levelOriginal) -> levelOriginal in me.get('earned')?.levels ? []

View file

@ -45,7 +45,7 @@ _.extend AchievementSchema.properties,
query: query:
#type:'object' #type:'object'
$ref: '#/definitions/mongoFindQuery' $ref: '#/definitions/mongoFindQuery'
worth: c.float worth: c.float()
collection: {type: 'string'} collection: {type: 'string'}
description: c.shortString() description: c.shortString()
userField: c.shortString() userField: c.shortString()
@ -61,7 +61,7 @@ _.extend AchievementSchema.properties,
description: 'For repeatables only. Denotes the field a repeatable achievement needs for its calculations' description: 'For repeatables only. Denotes the field a repeatable achievement needs for its calculations'
recalculable: recalculable:
type: 'boolean' type: 'boolean'
description: 'Needs to be set to true before it is elligible for recalculation.' description: 'Needs to be set to true before it is eligible for recalculation.'
function: function:
type: 'object' type: 'object'
description: 'Function that gives total experience for X amount achieved' description: 'Function that gives total experience for X amount achieved'
@ -82,6 +82,8 @@ _.extend AchievementSchema.properties,
format: 'i18n' format: 'i18n'
props: ['name', 'description'] props: ['name', 'description']
description: 'Help translate this achievement' description: 'Help translate this achievement'
rewards: c.RewardSchema 'awarded by this achievement'
_.extend AchievementSchema, # Let's have these on the bottom _.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 # TODO We really need some required properties in my opinion but this makes creating new achievements impossible as it is now

View file

@ -5,7 +5,7 @@ module.exports =
type: 'object' type: 'object'
default: default:
previouslyAchievedAmount: 0 previouslyAchievedAmount: 0
properties: properties:
user: c.objectId user: c.objectId
links: links:
@ -30,4 +30,5 @@ module.exports =
achievedAmount: type: 'number' achievedAmount: type: 'number'
earnedPoints: type: 'number' earnedPoints: type: 'number'
previouslyAchievedAmount: {type: 'number'} previouslyAchievedAmount: {type: 'number'}
earnedRewards: c.RewardSchema 'awarded by this achievement to this user'
notified: type: 'boolean' notified: type: 'boolean'

View file

@ -17,6 +17,8 @@ UserSchema = c.object
simulatedBy: 0 simulatedBy: 0
simulatedFor: 0 simulatedFor: 0
jobProfile: {} jobProfile: {}
earned: {heroes: [], items: [], levels: [], gems: 0}
purchased: {heroes: [], items: [], levels: [], gems: 0}
c.extendNamedProperties UserSchema # let's have the name be the first property c.extendNamedProperties UserSchema # let's have the name be the first property
@ -265,6 +267,8 @@ _.extend UserSchema.properties,
thangTypeTranslationPatches: c.int() thangTypeTranslationPatches: c.int()
thangTypeMiscPatches: c.int() thangTypeMiscPatches: c.int()
earned: c.RewardSchema 'earned by achievements'
purchased: c.RewardSchema 'purchased with gems'
c.extendBasicProperties UserSchema, 'user' c.extendBasicProperties UserSchema, 'user'

View file

@ -18,6 +18,7 @@ me.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext)
me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ext) me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ext)
# should just be string (Mongo ID), but sometimes mongoose turns them into objects representing those, so we are lenient # 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.objectId = (ext) -> schema = combine({type: ['object', 'string']}, ext)
me.stringID = (ext) -> schema = combine({type: 'string', minLength: 24, maxLength: 24}, ext)
me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext) me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext)
me.int = (ext) -> combine {type: 'integer'}, ext me.int = (ext) -> combine {type: 'integer'}, ext
me.float = (ext) -> combine {type: 'number'}, ext me.float = (ext) -> combine {type: 'number'}, ext
@ -209,3 +210,16 @@ me.HeroConfigSchema = me.object {description: 'Which hero the player is using, e
description: 'The inventory of the hero: slots to item ThangTypes.' description: 'The inventory of the hero: slots to item ThangTypes.'
additionalProperties: me.objectId(description: 'An item ThangType.') additionalProperties: me.objectId(description: 'An item ThangType.')
thangType: me.objectId(links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], title: 'Thang Type', description: 'The ThangType of the hero.', format: 'thang-type') thangType: me.objectId(links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], title: 'Thang Type', description: 'The ThangType of the hero.', format: 'thang-type')
me.RewardSchema = (descriptionFragment='earned by achievements') ->
type: 'object'
additionalProperties: false
description: "Rewards #{descriptionFragment}."
properties:
heroes: me.array {uniqueItems: true, description: "Heroes #{descriptionFragment}."},
me.stringID(links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], title: 'Hero ThangType', description: 'A reference to the earned hero ThangType.', format: 'thang-type')
items: me.array {uniqueItems: true, description: "Items #{descriptionFragment}."},
me.stringID(links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], title: 'Item ThangType', description: 'A reference to the earned item ThangType.', format: 'thang-type')
levels: me.array {uniqueItems: true, description: "Levels #{descriptionFragment}."},
me.stringID(links: [{rel: 'db', href: '/db/level/{($)}/version'}], title: 'Level', description: 'A reference to the earned Level.', format: 'latest-version-original-reference')
gems: me.int {description: "Gems #{descriptionFragment}."}

View file

@ -314,3 +314,27 @@ kbd
background-color: #333 background-color: #333
border-radius: 3px border-radius: 3px
@include box-shadow(inset 0 -1px 0 rgba(0, 0, 0, .25)) @include box-shadow(inset 0 -1px 0 rgba(0, 0, 0, .25))
.gem
display: inline-block
background: transparent url(/images/common/gem.png) no-repeat center
background-size: contain
width: 80px
height: 80px
margin: 0px 2px
&.gem-20
width: 20px
height: 20px
&.gem-25
width: 25px
height: 25px
&.gem-40
width: 40px
height: 40px
&.gem-60
width: 60px
height: 60px

View file

@ -52,9 +52,16 @@ $gameControlMargin: 30px
border: 2px groove white border: 2px groove white
@include transition(margin-bottom 0.5s ease) @include transition(margin-bottom 0.5s ease)
&.disabled &.disabled, &.locked
background-image: url(/images/pages/game-menu/lock.png)
background-size: 75%
background-repeat: no-repeat
background-position: 50% 50%
opacity: 0.7 opacity: 0.7
a
cursor: default
&.next &.next
width: 2 * $levelDotWidth width: 2 * $levelDotWidth
height: 2 * $levelDotHeight height: 2 * $levelDotHeight

View file

@ -1,6 +1,5 @@
@import "bootstrap/variables" @import "bootstrap/variables"
@import "bootstrap/mixins" @import "bootstrap/mixins"
@import "base"
#employers-wrapper #employers-wrapper
background-color: #B4B4B4 background-color: #B4B4B4
@ -51,5 +50,3 @@
#login-button #login-button
margin-left: 40% margin-left: 40%
width: 20% width: 20%

View file

@ -2,23 +2,25 @@
.carousel-indicator-container .carousel-indicator-container
ol.carousel-indicators ol.carousel-indicators
for hero, index in heroes for hero, index in heroes
- var info = heroInfo[hero.get('slug')] li(data-hero-id=hero.get('original'), title=hero.get('name'), data-slide-to=index, data-target="#hero-carousel", class="hero-indicator" + (hero.locked ? " locked" : ""))
li(data-hero-id=hero.get('original'), title=hero.get('name'), data-slide-to=index, data-target="#hero-carousel", class="hero-indicator" + (info.status == "Locked" ? " locked" : ""))
.hero-avatar .hero-avatar
if info.status == "Locked" if hero.locked
img.lock-indicator(src="/images/pages/game-menu/lock.png") img.lock-indicator(src="/images/pages/game-menu/lock.png")
.carousel-inner .carousel-inner
for hero in heroes for hero in heroes
- var info = heroInfo[hero.get('slug')] - var info = heroInfo[hero.get('slug')]
div(class="item hero-item" + (info.status == "Locked" ? " locked" : ""), data-hero-id=hero.get('original')) div(class="item hero-item" + (hero.locked ? " locked" : ""), data-hero-id=hero.get('original'))
canvas.hero-canvas canvas.hero-canvas
.hero-stats .hero-stats
h2= info.fullName h2= info.fullName
p p
span(data-i18n="choose_hero.status") Status span(data-i18n="choose_hero.status") Status
span.spr : span.spr :
| #{info.status} if hero.locked
| #{info.status}
else
| Available
p p
span(data-i18n="choose_hero.weapons") Weapons span(data-i18n="choose_hero.weapons") Weapons
span.spr : span.spr :

View file

@ -4,8 +4,8 @@ block content
h1#site-slogan(data-i18n="home.slogan") Learn to Code by Playing a Game h1#site-slogan(data-i18n="home.slogan") Learn to Code by Playing a Game
.alert.alert-danger.lt-ie10 .alert.alert-danger.lt-ie9
strong(data-i18n="home.no_ie") CodeCombat does not run in Internet Explorer 9 or older. Sorry! strong(data-i18n="home.no_ie") CodeCombat does not run in Internet Explorer 8 or older. Sorry!
if isMobile if isMobile
.alert.alert-danger.mobile .alert.alert-danger.mobile

View file

@ -6,12 +6,12 @@
each level in campaign.levels each level in campaign.levels
- var next = !seenNext && levelStatusMap[level.id] != "complete"; - var next = !seenNext && levelStatusMap[level.id] != "complete";
- seenNext = seenNext || next; - seenNext = seenNext || next;
div(style="left: #{level.x}%; bottom: #{level.y}%; background-color: #{campaign.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + " " + levelStatusMap[level.id] || "", data-level-id=level.id, title=level.name) div(style="left: #{level.x}%; bottom: #{level.y}%; background-color: #{campaign.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.id] || "", data-level-id=level.id, title=level.name)
a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.id}", disabled=level.disabled, data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name) a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.id}", disabled=level.disabled, data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name)
div(style="left: #{level.x}%; bottom: #{level.y}%", class="level-shadow" + (next ? " next" : "") + " " + levelStatusMap[level.id] || "") div(style="left: #{level.x}%; bottom: #{level.y}%", class="level-shadow" + (next ? " next" : "") + " " + levelStatusMap[level.id] || "")
.level-info-container(data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name) .level-info-container(data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name)
div(class="level-info " + (levelStatusMap[level.id] || "")) div(class="level-info " + (levelStatusMap[level.id] || ""))
h3= level.name + (level.disabled ? " (Coming soon!)" : "") h3= level.name + (level.disabled ? " (Coming soon!)" : (level.locked ? " (Locked)" : ""))
.level-description= level.description .level-description= level.description
span(data-i18n="play.level_difficulty") Difficulty: span(data-i18n="play.level_difficulty") Difficulty:
each i in Array(level.difficulty) each i in Array(level.difficulty)
@ -46,6 +46,8 @@
a(href="/play-old", data-i18n="play.older_campaigns").header-font Older Campaigns a(href="/play-old", data-i18n="play.older_campaigns").header-font Older Campaigns
.user-status.header-font .user-status.header-font
span.gem.gem-20
span.spr= me.gems()
if me.get('anonymous') if me.get('anonymous')
span.spr(data-i18n="play.anonymous_player") Anonymous Player span.spr(data-i18n="play.anonymous_player") Anonymous Player
button.btn.btn-default.btn-flat.btn-sm(data-toggle='coco-modal', data-target='modal/AuthModal', data-i18n="login.log_in") button.btn.btn-default.btn-flat.btn-sm(data-toggle='coco-modal', data-target='modal/AuthModal', data-i18n="login.log_in")

View file

@ -360,11 +360,14 @@ class LatestVersionReferenceNode extends TreemaNode
return 'Unknown' unless @settings.supermodel? return 'Unknown' unless @settings.supermodel?
m = CocoModel.getReferencedModel(@getData(), @workingSchema) m = CocoModel.getReferencedModel(@getData(), @workingSchema)
data = @getData() data = @getData()
m = @settings.supermodel.getModelByOriginalAndMajorVersion(m.constructor, data.original, data.majorVersion) if _.isString data # LatestVersionOriginalReferenceNode just uses original
m = @settings.supermodel.getModelByOriginal(m.constructor, data)
else
m = @settings.supermodel.getModelByOriginalAndMajorVersion(m.constructor, data.original, data.majorVersion)
if @instance and not m if @instance and not m
m = @instance m = @instance
@settings.supermodel.registerModel(m) @settings.supermodel.registerModel(m)
return 'Unknown' unless m return 'Unknown - ' + (data.original ? data) unless m
return @modelToString(m) return @modelToString(m)
saveChanges: -> saveChanges: ->
@ -409,6 +412,15 @@ class LatestVersionReferenceNode extends TreemaNode
selected = @getSelectedResultEl() selected = @getSelectedResultEl()
return not selected.length return not selected.length
class LatestVersionOriginalReferenceNode extends LatestVersionReferenceNode
# Just for saving the original, not the major version.
saveChanges: ->
selected = @getSelectedResultEl()
return unless selected.length
fullValue = selected.data('value')
@data = fullValue.attributes.original
@instance = fullValue
class LevelComponentReferenceNode extends LatestVersionReferenceNode class LevelComponentReferenceNode extends LatestVersionReferenceNode
# HACK: this list of properties is needed by the thang components edit view and config views. # HACK: this list of properties is needed by the thang components edit view and config views.
# need a better way to specify this, or keep the search models from bleeding into those # need a better way to specify this, or keep the search models from bleeding into those
@ -436,6 +448,7 @@ module.exports.setup = ->
TreemaNode.setNodeSubclass('javascript', JavaScriptTreema) TreemaNode.setNodeSubclass('javascript', JavaScriptTreema)
TreemaNode.setNodeSubclass('image-file', ImageFileTreema) TreemaNode.setNodeSubclass('image-file', ImageFileTreema)
TreemaNode.setNodeSubclass('latest-version-reference', LatestVersionReferenceNode) TreemaNode.setNodeSubclass('latest-version-reference', LatestVersionReferenceNode)
TreemaNode.setNodeSubclass('latest-version-original-reference', LatestVersionOriginalReferenceNode)
TreemaNode.setNodeSubclass('component-reference', LevelComponentReferenceNode) TreemaNode.setNodeSubclass('component-reference', LevelComponentReferenceNode)
TreemaNode.setNodeSubclass('i18n', InternationalizationNode) TreemaNode.setNodeSubclass('i18n', InternationalizationNode)
TreemaNode.setNodeSubclass('sound-file', SoundFileTreema) TreemaNode.setNodeSubclass('sound-file', SoundFileTreema)

View file

@ -15,8 +15,6 @@ module.exports = class AchievementPopup extends CocoView
@popup ?= true @popup ?= true
@className += ' popup' if @popup @className += ' popup' if @popup
super options super options
console.debug 'Created an AchievementPopup', @$el
@render() @render()
calculateData: -> calculateData: ->
@ -62,7 +60,6 @@ module.exports = class AchievementPopup extends CocoView
c c
render: -> render: ->
console.debug 'render achievement popup'
super() super()
@container.prepend @$el @container.prepend @$el
if @popup if @popup

View file

@ -5,6 +5,7 @@ AchievementPopup = require 'views/achievements/AchievementPopup'
ConfirmModal = require 'views/modal/ConfirmModal' ConfirmModal = require 'views/modal/ConfirmModal'
errors = require 'lib/errors' errors = require 'lib/errors'
app = require 'application' app = require 'application'
nodes = require 'views/editor/level/treema_nodes'
module.exports = class AchievementEditView extends RootView module.exports = class AchievementEditView extends RootView
id: 'editor-achievement-edit-view' id: 'editor-achievement-edit-view'
@ -36,8 +37,13 @@ module.exports = class AchievementEditView extends RootView
readOnly: me.get('anonymous') readOnly: me.get('anonymous')
callbacks: callbacks:
change: @pushChangesToPreview change: @pushChangesToPreview
nodeClasses:
'thang-type': nodes.ThangTypeNode
'item-thang-type': nodes.ItemThangTypeNode
supermodel: @supermodel
@treema = @$el.find('#achievement-treema').treema(options) @treema = @$el.find('#achievement-treema').treema(options)
@treema.build() @treema.build()
@treema.childrenTreemas.rewards?.open(3)
@pushChangesToPreview() @pushChangesToPreview()
getRenderData: (context={}) -> getRenderData: (context={}) ->

View file

@ -36,6 +36,7 @@ module.exports = class ChooseHeroView extends CocoView
getRenderData: (context={}) -> getRenderData: (context={}) ->
context = super(context) context = super(context)
context.heroes = @heroes.models context.heroes = @heroes.models
hero.locked = temporaryHeroInfo[hero.get('slug')].status is 'Locked' and not me.earnedHero hero.get('original') for hero in context.heroes
context.level = @options.level context.level = @options.level
context.codeLanguages = [ context.codeLanguages = [
{id: 'python', name: 'Python'} {id: 'python', name: 'Python'}
@ -76,7 +77,7 @@ module.exports = class ChooseHeroView extends CocoView
size = 100 - (50 / 3) * distance size = 100 - (50 / 3) * distance
$(@).css width: size, height: size, top: -(100 - size) / 2 $(@).css width: size, height: size, top: -(100 - size) / 2
heroInfo = temporaryHeroInfo[hero.get('slug')] heroInfo = temporaryHeroInfo[hero.get('slug')]
locked = heroInfo.status is 'Locked' locked = heroInfo.status is 'Locked' and not me.earnedHero ThangType.heroes[hero.get('slug')]
hero = @loadHero hero, heroIndex hero = @loadHero hero, heroIndex
@preloadHero heroIndex + 1 @preloadHero heroIndex + 1
@preloadHero heroIndex - 1 @preloadHero heroIndex - 1

View file

@ -321,6 +321,8 @@ module.exports = class InventoryView extends CocoView
for slot, item of items for slot, item of items
@allowedItems.push gear[item] unless gear[item] in @allowedItems @allowedItems.push gear[item] unless gear[item] in @allowedItems
break if level is @options.levelID break if level is @options.levelID
for item in me.get('earned')?.items ? [] when not (item in @allowedItems)
@allowedItems.push item
onHeroSelectionUpdated: (e) -> onHeroSelectionUpdated: (e) ->
@selectedHero = e.hero @selectedHero = e.hero

View file

@ -65,9 +65,10 @@ module.exports = class WorldMapView extends RootView
context = super(context) context = super(context)
context.campaigns = campaigns context.campaigns = campaigns
for campaign in context.campaigns for campaign in context.campaigns
for level in campaign.levels for level, index in campaign.levels
level.x ?= 10 + 80 * Math.random() level.x ?= 10 + 80 * Math.random()
level.y ?= 10 + 80 * Math.random() level.y ?= 10 + 80 * Math.random()
level.locked = index > 0 and not me.earnedLevel level.original
context.levelStatusMap = @levelStatusMap context.levelStatusMap = @levelStatusMap
context.levelPlayCountMap = @levelPlayCountMap context.levelPlayCountMap = @levelPlayCountMap
context.isIPadApp = application.isIPadApp context.isIPadApp = application.isIPadApp
@ -96,7 +97,7 @@ module.exports = class WorldMapView extends RootView
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
@$levelInfo?.hide() @$levelInfo?.hide()
return if $(e.target).attr('disabled') return if $(e.target).attr('disabled') or $(e.target).parent().hasClass 'locked'
if application.isIPadApp if application.isIPadApp
levelID = $(e.target).parents('.level').data('level-id') levelID = $(e.target).parents('.level').data('level-id')
@$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show() @$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show()
@ -515,6 +516,7 @@ hero = [
type: 'hero' type: 'hero'
difficulty: 1 difficulty: 1
id: 'dungeons-of-kithgard' id: 'dungeons-of-kithgard'
original: '528110f30268d018e3000001'
description: 'Grab the gem, but touch nothing else. Start here.' description: 'Grab the gem, but touch nothing else. Start here.'
x: 17.23 x: 17.23
y: 36.94 y: 36.94
@ -524,6 +526,7 @@ hero = [
type: 'hero' type: 'hero'
difficulty: 1 difficulty: 1
id: 'gems-in-the-deep' id: 'gems-in-the-deep'
original: '54173c90844506ae0195a0b4'
description: 'Quickly collect the gems; you will need them.' description: 'Quickly collect the gems; you will need them.'
x: 22.6 x: 22.6
y: 35.1 y: 35.1
@ -533,6 +536,7 @@ hero = [
type: 'hero' type: 'hero'
difficulty: 1 difficulty: 1
id: 'shadow-guard' id: 'shadow-guard'
original: '54174347844506ae0195a0b8'
description: 'Evade the Kithgard minion.' description: 'Evade the Kithgard minion.'
x: 27.74 x: 27.74
y: 35.17 y: 35.17
@ -542,6 +546,7 @@ hero = [
type: 'hero' type: 'hero'
difficulty: 1 difficulty: 1
id: 'true-names' id: 'true-names'
original: '541875da4c16460000ab990f'
description: 'Learn an enemy\'s true name to defeat it.' description: 'Learn an enemy\'s true name to defeat it.'
x: 32.7 x: 32.7
y: 36.7 y: 36.7
@ -551,6 +556,7 @@ hero = [
type: 'hero' type: 'hero'
difficulty: 1 difficulty: 1
id: 'the-raised-sword' id: 'the-raised-sword'
original: '5418aec24c16460000ab9aa6'
description: 'Learn to equip yourself for combat.' description: 'Learn to equip yourself for combat.'
x: 36.6 x: 36.6
y: 39.5 y: 39.5
@ -560,6 +566,7 @@ hero = [
type: 'hero' type: 'hero'
difficulty: 1 difficulty: 1
id: 'the-first-kithmaze' id: 'the-first-kithmaze'
original: '5418b9d64c16460000ab9ab4'
description: 'The builders of Kith constructed many mazes to confuse travelers.' description: 'The builders of Kith constructed many mazes to confuse travelers.'
x: 38.4 x: 38.4
y: 43.5 y: 43.5
@ -569,6 +576,7 @@ hero = [
type: 'hero' type: 'hero'
difficulty: 1 difficulty: 1
id: 'the-second-kithmaze' id: 'the-second-kithmaze'
original: '5418cf256bae62f707c7e1c3'
description: 'Many have tried, few have found their way through this maze.' description: 'Many have tried, few have found their way through this maze.'
x: 38.9 x: 38.9
y: 48.1 y: 48.1
@ -578,6 +586,7 @@ hero = [
type: 'hero' type: 'hero'
difficulty: 1 difficulty: 1
id: 'new-sight' id: 'new-sight'
original: '5418d40f4c16460000ab9ac2'
description: 'A true name can only be seen with the correct lenses.' description: 'A true name can only be seen with the correct lenses.'
x: 39.3 x: 39.3
y: 53.1 y: 53.1
@ -587,6 +596,7 @@ hero = [
type: 'hero' type: 'hero'
difficulty: 1 difficulty: 1
id: 'lowly-kithmen' id: 'lowly-kithmen'
original: '541b24511ccc8eaae19f3c1f'
description: 'Use your glasses to seek out and attack the Kithmen.' description: 'Use your glasses to seek out and attack the Kithmen.'
x: 39.4 x: 39.4
y: 57.7 y: 57.7
@ -596,6 +606,7 @@ hero = [
type: 'hero' type: 'hero'
difficulty: 1 difficulty: 1
id: 'a-bolt-in-the-dark' id: 'a-bolt-in-the-dark'
original: '541b288e1ccc8eaae19f3c25'
description: 'Kithmen are not the only ones to stand in your way.' description: 'Kithmen are not the only ones to stand in your way.'
x: 40.0 x: 40.0
y: 63.2 y: 63.2
@ -605,6 +616,7 @@ hero = [
type: 'hero' type: 'hero'
difficulty: 1 difficulty: 1
id: 'the-final-kithmaze' id: 'the-final-kithmaze'
original: '541b434e1ccc8eaae19f3c33'
description: 'To escape you must find your way through an Elder Kithman\'s maze.' description: 'To escape you must find your way through an Elder Kithman\'s maze.'
x: 42.67 x: 42.67
y: 67.98 y: 67.98
@ -614,6 +626,7 @@ hero = [
type: 'hero' type: 'hero'
difficulty: 1 difficulty: 1
id: 'kithgard-gates' id: 'kithgard-gates'
original: '541c9a30c6362edfb0f34479'
description: 'Escape the Kithgard dungeons and don\'t let the guardians get you.' description: 'Escape the Kithgard dungeons and don\'t let the guardians get you.'
x: 47.38 x: 47.38
y: 70.55 y: 70.55
@ -624,6 +637,7 @@ hero = [
type: 'hero' type: 'hero'
difficulty: 1 difficulty: 1
id: 'defence-of-plainswood' id: 'defence-of-plainswood'
original: '541b67f71ccc8eaae19f3c62'
description: 'Protect the peasants from the pursuing ogres.' description: 'Protect the peasants from the pursuing ogres.'
x: 52.66 x: 52.66
y: 69.66 y: 69.66

View file

@ -75,7 +75,7 @@
"javascript-brunch": "> 1.0 < 1.8", "javascript-brunch": "> 1.0 < 1.8",
"coffee-script-brunch": "https://github.com/brunch/coffee-script-brunch/tarball/master", "coffee-script-brunch": "https://github.com/brunch/coffee-script-brunch/tarball/master",
"coffeelint-brunch": "> 1.0 < 1.8", "coffeelint-brunch": "> 1.0 < 1.8",
"sass-brunch": "1.7.0", "sass-brunch": "1.8.3",
"css-brunch": "> 1.0 < 1.8", "css-brunch": "> 1.0 < 1.8",
"jade-brunch": "> 1.0 < 1.8", "jade-brunch": "> 1.0 < 1.8",
"uglify-js-brunch": "~1.7.4", "uglify-js-brunch": "~1.7.4",
@ -86,7 +86,7 @@
"marked": "0.2.x", "marked": "0.2.x",
"telepath-brunch": "https://github.com/nwinter/telepath-brunch/tarball/master", "telepath-brunch": "https://github.com/nwinter/telepath-brunch/tarball/master",
"bower": "~1.3.8", "bower": "~1.3.8",
"bless-brunch": "~1.6.1", "bless-brunch": "https://github.com/ThomasConner/bless-brunch/tarball/master",
"karma-script-launcher": "~0.1.0", "karma-script-launcher": "~0.1.0",
"karma-chrome-launcher": "~0.1.2", "karma-chrome-launcher": "~0.1.2",
"karma-firefox-launcher": "~0.1.3", "karma-firefox-launcher": "~0.1.3",

View file

@ -5,7 +5,7 @@ class AchievementHandler extends Handler
modelClass: Achievement modelClass: Achievement
# Used to determine which properties requests may edit # Used to determine which properties requests may edit
editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function', 'related', 'difficulty', 'category', 'recalculable'] editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function', 'related', 'difficulty', 'category', 'recalculable', 'rewards']
allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
jsonSchema = require '../../app/schemas/models/achievement.coffee' jsonSchema = require '../../app/schemas/models/achievement.coffee'

View file

@ -77,15 +77,21 @@ class EarnedAchievementHandler extends Handler
EarnedAchievement.find {user: userID}, (err, alreadyEarned) -> EarnedAchievement.find {user: userID}, (err, alreadyEarned) ->
alreadyEarnedIDs = [] alreadyEarnedIDs = []
previousPoints = 0 previousPoints = 0
previousRewards = heroes: [], items: [], levels: [], gems: 0
async.each alreadyEarned, ((earned, doneWithEarned) -> async.each alreadyEarned, ((earned, doneWithEarned) ->
if (_.find achievements, (single) -> earned.get('achievement') is single.get('_id').toHexString()) # if already earned if (_.find achievements, (single) -> earned.get('achievement') is single.get('_id').toHexString()) # if already earned
alreadyEarnedIDs.push earned.get('achievement') alreadyEarnedIDs.push earned.get('achievement')
previousPoints += earned.get 'earnedPoints' previousPoints += earned.get 'earnedPoints'
for rewardType in ['heroes', 'items', 'levels']
previousRewards[rewardType] = previousRewards[rewardType].concat(earned.get('earnedRewards')?[rewardType] ? [])
previousRewards.gems += earned.get('earnedRewards')?.gems ? 0
doneWithEarned() doneWithEarned()
), -> # After checking already achieved ), (err) -> # After checking already achieved
log.error err if err
# TODO maybe also delete earned? Make sure you don't delete too many # TODO maybe also delete earned? Make sure you don't delete too many
newTotalPoints = 0 newTotalPoints = 0
newTotalRewards = heroes: [], items: [], levels: [], gems: 0
async.each achievements, ((achievement, doneWithAchievement) -> async.each achievements, ((achievement, doneWithAchievement) ->
return doneWithAchievement() unless achievement.isRecalculable() return doneWithAchievement() unless achievement.isRecalculable()
@ -122,17 +128,43 @@ class EarnedAchievementHandler extends Handler
earned.earnedPoints = newPoints earned.earnedPoints = newPoints
newTotalPoints += newPoints newTotalPoints += newPoints
earned.earnedRewards = achievement.get('rewards')
for rewardType in ['heroes', 'items', 'levels']
newTotalRewards[rewardType] = newTotalRewards[rewardType].concat(achievement.get('rewards')?[rewardType] ? [])
newTotalRewards.gems += achievement.get('rewards')?.gems ? 0
EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) -> EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) ->
doneWithAchievement err doneWithAchievement err
), -> # Wrap up a user, save points ), (err) -> # Wrap up a user, save points
log.error err if err
# Since some achievements cannot be recalculated it's important to deduct the old amount of exp # Since some achievements cannot be recalculated it's important to deduct the old amount of exp
# and add the new amount, instead of just setting to the new amount # and add the new amount, instead of just setting to the new amount
return doneWithUser(user) unless newTotalPoints #console.log 'User', user.get('name'), 'had newTotalPoints', newTotalPoints, 'and newTotalRewards', newTotalRewards
return doneWithUser(user) unless newTotalPoints or newTotalRewards.gems or _.some(newTotalRewards, (r) -> r.length)
# log.debug "Matched a total of #{newTotalPoints} new points" # log.debug "Matched a total of #{newTotalPoints} new points"
# log.debug "Incrementing score for these achievements with #{newTotalPoints - previousPoints}" # log.debug "Incrementing score for these achievements with #{newTotalPoints - previousPoints}"
pctDone = (100 * usersFinished / total).toFixed(2) pctDone = (100 * usersFinished / total).toFixed(2)
console.log "Updated points to #{newTotalPoints}(+#{newTotalPoints - previousPoints}) for #{user.get('name') or '???'} (#{user.get('_id')}) (#{pctDone}%)" console.log "Updated points to #{newTotalPoints}(+#{newTotalPoints - previousPoints}) for #{user.get('name') or '???'} (#{user.get('_id')}) (#{pctDone}%)"
User.update {_id: userID}, {$inc: points: newTotalPoints - previousPoints}, {}, (err) -> update = {$inc: {points: newTotalPoints - previousPoints}}
for rewardType, rewards of newTotalRewards
if rewardType is 'gems'
update.$inc['earned.gems'] = rewards - previousRewards.gems
else
previousCounts = _.countBy previousRewards[rewardType]
newCounts = _.countBy rewards
relevantRewards = _.union _.keys(previousCounts), _.keys(newCounts)
for reward in relevantRewards
[previousCount, newCount] = [previousCounts[reward], newCounts[reward]]
if newCount and not previousCount
update.$addToSet ?= {}
update.$addToSet["earned.#{rewardType}"] ?= {$each: []}
update.$addToSet["earned.#{rewardType}"].$each.push reward
else if previousCount and not newCount
# Might $pull $each also work here?
update.$pullAll ?= {}
update.$pullAll["earned.#{rewardType}"] ?= []
update.$pullAll["earned.#{rewardType}"].push reward
User.update {_id: userID}, update, {}, (err) ->
log.error err if err? log.error err if err?
doneWithUser(user) doneWithUser(user)

View file

@ -50,12 +50,20 @@ AchievablePlugin = (schema, options) ->
user: userID user: userID
achievement: achievement._id.toHexString() achievement: achievement._id.toHexString()
achievementName: achievement.get 'name' achievementName: achievement.get 'name'
earnedRewarsd: achievement.get 'rewards'
worth = achievement.get('worth') ? 10 worth = achievement.get('worth') ? 10
earnedPoints = 0 earnedPoints = 0
wrapUp = -> wrapUp = ->
# Update user's experience points # Update user's experience points
User.update {_id: userID}, {$inc: {points: earnedPoints}}, {}, (err, count) -> update = {$inc: {points: earnedPoints}}
for rewardType, rewards of achievement.get('rewards') ? {}
if rewardType is 'gems'
update.$inc['earned.gems'] = rewards if rewards
else if rewards.length
update.$addToSet ?= {}
update.$addToSet["earned.#{rewardType}"] = $each: rewards
User.update {_id: userID}, update, {}, (err, count) ->
log.error err if err? log.error err if err?
if isRepeatable if isRepeatable