From 2af827182904df6b1f0de3e5a746c523e8df258f Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Wed, 19 Nov 2014 09:01:01 -0800
Subject: [PATCH 01/11] Some size/styling tweaks for goals and gold.

---
 app/styles/play/level.sass       |  7 +++++++
 app/styles/play/level/goals.sass |  2 ++
 app/styles/play/level/gold.sass  | 15 ++++++++-------
 3 files changed, 17 insertions(+), 7 deletions(-)

diff --git a/app/styles/play/level.sass b/app/styles/play/level.sass
index cad311dad..c5145b627 100644
--- a/app/styles/play/level.sass
+++ b/app/styles/play/level.sass
@@ -43,6 +43,13 @@ $level-resize-transition-time: 0.5s
       visibility: hidden
     #gold-view
       right: 1%
+      @include box-shadow(-1px 1px 10px cyan)
+      .team-gold
+        font-size: 2vw
+        line-height: 2vw
+      img
+        width: 1.8vw
+        heighT: 1.8vw
     #control-bar-view .title
       left: 20%
       width: 60%
diff --git a/app/styles/play/level/goals.sass b/app/styles/play/level/goals.sass
index 310a1ee12..1d8e4a06f 100644
--- a/app/styles/play/level/goals.sass
+++ b/app/styles/play/level/goals.sass
@@ -12,11 +12,13 @@
   padding: 19px 0px 2px 25px
   z-index: 3
   font-size: 14px
+  min-width: 230px
 
   &.brighter
     font-size: 18px
     font-size: 1.4vw
     border-width: 0.91vw 1.22vw 3.10vw 0.91vw
+    min-width: 23vw
 
   .goals-status
     margin: 5px 0 0 0
diff --git a/app/styles/play/level/gold.sass b/app/styles/play/level/gold.sass
index 370d9ec21..bc2331294 100644
--- a/app/styles/play/level/gold.sass
+++ b/app/styles/play/level/gold.sass
@@ -9,7 +9,7 @@
   z-index: 6
   @include transition(box-shadow .2s linear)
   @include user-select(none)
-  padding: 4px
+  padding: 0.4vw
   background: transparent url(/images/level/gold_background.png) no-repeat
   background-size: 100% 100%
   border-radius: 4px
@@ -18,9 +18,9 @@
     box-shadow: 2px 2px 2px black
 
   .team-gold
-    font-size: 18px
+    font-size: 1.4vw
+    line-height: 1.4vw
     margin: 0
-    line-height: 20px
     color: hsla(205,0%,51%,1)
     display: inline-block
     padding: 0px 4px
@@ -35,11 +35,12 @@
       color: hsla(116,80%,51%,1)
 
     img
-      width: 16px
-      height: 16px
+      width: 1.2vw
+      height: 1.2vw
       border-radius: 2px
-      padding: 1px
-      margin-top: -1px
+      padding: 0.1vw
+      margin-top: -0.2vw
+      margin-right: 0.1vw
   
     .gold-amount
       display: inline-block

From 63c516c5f510d594fb42cce1e09e7623ab7b77c8 Mon Sep 17 00:00:00 2001
From: Scott Erickson <sderickson@gmail.com>
Date: Wed, 19 Nov 2014 10:05:02 -0800
Subject: [PATCH 02/11] Quick fix to get people to the forest levels even if,
 for some reason, the first forest level isn't in the User list of earned
 levels.

---
 app/views/play/WorldMapView.coffee | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/app/views/play/WorldMapView.coffee b/app/views/play/WorldMapView.coffee
index fc556515d..7061f8e9f 100644
--- a/app/views/play/WorldMapView.coffee
+++ b/app/views/play/WorldMapView.coffee
@@ -102,7 +102,7 @@ module.exports = class WorldMapView extends RootView
     context.isIPadApp = application.isIPadApp
     context.mapType = _.string.slugify @terrain
     context.nextLevel = @nextLevel
-    context.forestIsAvailable = '541b67f71ccc8eaae19f3c62' in (me.get('earned')?.levels or [])
+    context.forestIsAvailable = @startedForestLevel or '541b67f71ccc8eaae19f3c62' in (me.get('earned')?.levels or [])
     context
 
   afterRender: ->
@@ -131,8 +131,10 @@ module.exports = class WorldMapView extends RootView
     @openModalView authModal
 
   onSessionsLoaded: (e) ->
+    forestLevels = (f.id for f in forest)
     for session in @sessions.models
       @levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started'
+      @startedForestLevel = true if session.get('levelID') in forestLevels
     if @nextLevel and @levelStatusMap[@nextLevel] is 'complete'
       @nextLevel = null
     @render()

From 0640f382ba52f412605685384fc5cf54ed915e6f Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Wed, 19 Nov 2014 13:23:55 -0800
Subject: [PATCH 03/11] Fixed coordinate hover cursors fighting flag cursors.
 Added suspect code check functionality for scripts to slap hands.

---
 app/lib/LevelOptions.coffee                | 12 ++++++++++
 app/lib/surface/CoordinateDisplay.coffee   |  6 ++++-
 app/schemas/subscriptions/tome.coffee      |  4 ++++
 app/views/game-menu/InventoryModal.coffee  |  2 +-
 app/views/play/level/tome/SpellView.coffee | 26 +++++++++++++---------
 5 files changed, 38 insertions(+), 12 deletions(-)

diff --git a/app/lib/LevelOptions.coffee b/app/lib/LevelOptions.coffee
index 4b82d8764..df0cfa2f6 100644
--- a/app/lib/LevelOptions.coffee
+++ b/app/lib/LevelOptions.coffee
@@ -10,6 +10,7 @@ module.exports = LevelOptions =
     hidesRealTimePlayback: true
     requiredGear: {feet: 'simple-boots'}
     restrictedGear: {feet: 'leather-boots'}
+    requiredCode: ['moveRight']
   'gems-in-the-deep':
     disableSpaces: true
     hidesSubmitUntilRun: true
@@ -71,6 +72,7 @@ module.exports = LevelOptions =
     hidesRealTimePlayback: true
     requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', waist: 'leather-belt'}
     restrictedGear: {feet: 'leather-boots'}
+    requiredCode: ['Brak']
   'favorable-odds':
     disableSpaces: true
     hidesRunShortcut: true
@@ -97,6 +99,7 @@ module.exports = LevelOptions =
     hidesRealTimePlayback: true
     requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
     restrictedGear: {feet: 'leather-boots'}
+    requiredCode: ['loop']
   'haunted-kithmaze':
     hidesRunShortcut: true
     hidesHUD: true
@@ -105,6 +108,7 @@ module.exports = LevelOptions =
     hidesRealTimePlayback: true
     requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
     restrictedGear: {feet: 'leather-boots'}
+    requiredCode: ['loop']
   'descending-further':
     hidesHUD: true
     hidesSay: true
@@ -140,6 +144,8 @@ module.exports = LevelOptions =
     hidesRealTimePlayback: true
     requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'leather-tunic'}
     restrictedGear: {feet: 'leather-boots'}
+    requiredCode: ['findNearestEnemy']
+    suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
   'lowly-kithmen':
     hidesHUD: true
     hidesSay: true
@@ -147,6 +153,8 @@ module.exports = LevelOptions =
     hidesRealTimePlayback: true
     requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'leather-tunic'}
     restrictedGear: {feet: 'leather-boots'}
+    requiredCode: ['findNearestEnemy']
+    suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
   'closing-the-distance':
     hidesHUD: true
     hidesSay: true
@@ -154,6 +162,7 @@ module.exports = LevelOptions =
     hidesRealTimePlayback: true
     requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', eyes: 'crude-glasses'}
     restrictedGear: {feet: 'leather-boots'}
+    suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
   'tactical-strike':
     hidesHUD: true
     hidesSay: true
@@ -161,12 +170,14 @@ module.exports = LevelOptions =
     hidesRealTimePlayback: true
     requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', eyes: 'crude-glasses'}
     restrictedGear: {feet: 'leather-boots'}
+    suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
   'the-final-kithmaze':
     hidesHUD: true
     hidesSay: true
     hidesCodeToolbar: true
     hidesRealTimePlayback: true
     requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
+    suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
   'the-gauntlet':
     hidesHUD: true
     hidesSay: true
@@ -174,6 +185,7 @@ module.exports = LevelOptions =
     hidesRealTimePlayback: true
     requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
     restrictedGear: {feet: 'leather-boots'}
+    suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
   'kithgard-gates':
     hidesSay: true
     hidesCodeToolbar: true
diff --git a/app/lib/surface/CoordinateDisplay.coffee b/app/lib/surface/CoordinateDisplay.coffee
index e9928ed60..5f4684884 100644
--- a/app/lib/surface/CoordinateDisplay.coffee
+++ b/app/lib/surface/CoordinateDisplay.coffee
@@ -6,6 +6,7 @@ module.exports = class CoordinateDisplay extends createjs.Container
     'surface:mouse-over': 'onMouseOver'
     'surface:stage-mouse-down': 'onMouseDown'
     'camera:zoom-updated': 'onZoomUpdated'
+    'level:flag-color-selected': 'onFlagColorSelected'
 
   constructor: (options) ->
     super()
@@ -60,6 +61,9 @@ module.exports = class CoordinateDisplay extends createjs.Container
     @hide()
     @show()
 
+  onFlagColorSelected: (e) ->
+    @placingFlag = Boolean e.color
+
   hide: ->
     return unless @label.parent
     @removeChild @label
@@ -154,6 +158,6 @@ module.exports = class CoordinateDisplay extends createjs.Container
     @y = sup.y
     @addChild @background
     @addChild @label
-    @addChild @pointMarker
+    @addChild @pointMarker unless @placingFlag
     @updateCache()
     Backbone.Mediator.publish 'surface:coordinates-shown', {}
diff --git a/app/schemas/subscriptions/tome.coffee b/app/schemas/subscriptions/tome.coffee
index b4530a9a8..26712d1e1 100644
--- a/app/schemas/subscriptions/tome.coffee
+++ b/app/schemas/subscriptions/tome.coffee
@@ -122,6 +122,10 @@ module.exports =
   'tome:required-code-fragment-deleted': c.object {title: 'Required Code Fragment Deleted', description: 'Published when a required code fragment is deleted from the sample code.', required: ['codeFragment']},
     codeFragment: {type: 'string'}
 
+  'tome:suspect-code-fragment-added': c.object {title: 'Suspect Code Fragment Added', description: 'Published when a suspect code fragment is added to the sample code.', required: ['codeFragment']},
+    codeFragment: {type: 'string'}
+    codeLanguage: {type: 'string'}
+
   'tome:winnability-updated': c.object {title: 'Winnability Updated', description: 'When we think we can now win (or can no longer win), we may want to emphasize the submit button versus the run button (or vice versa), so this fires when we get new goal states (even preloaded goal states) suggesting success or failure change.', required: ['winnable']},
     winnable: {type: 'boolean'}
 
diff --git a/app/views/game-menu/InventoryModal.coffee b/app/views/game-menu/InventoryModal.coffee
index 4de79d40d..d83089468 100644
--- a/app/views/game-menu/InventoryModal.coffee
+++ b/app/views/game-menu/InventoryModal.coffee
@@ -86,7 +86,7 @@ module.exports = class InventoryModal extends ModalView
     locked = not (item.get('original') in me.items())
     locked = false if me.get('slug') is 'nick'
 
-    if not item.getFrontFacingStats().props.length and not _.size item.getFrontFacingStats().stats  # Temp: while there are placeholder items
+    if not item.getFrontFacingStats().props.length and not _.size(item.getFrontFacingStats().stats) and not locked  # Temp: while there are placeholder items
       null  # Don't put into a collection
     else if locked and item.get('slug') isnt 'simple-boots'
       @itemGroups.lockedItems.add(item)
diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee
index 986eb749b..209ac6f36 100644
--- a/app/views/play/level/tome/SpellView.coffee
+++ b/app/views/play/level/tome/SpellView.coffee
@@ -450,7 +450,8 @@ module.exports = class SpellView extends CocoView
       _.throttle @updateLines, 500
       _.throttle @hideProblemAlert, 500
     ]
-    onSignificantChange.push _.debounce @checkRequiredCode, 1500 if requiredCodePerLevel[@options.level.get('slug')]
+    onSignificantChange.push _.debounce @checkRequiredCode, 750 if LevelOptions[@options.level.get('slug')]?.requiredCode
+    onSignificantChange.push _.debounce @checkSuspectCode, 750 if LevelOptions[@options.level.get('slug')]?.suspectCode
     @onCodeChangeMetaHandler = =>
       return if @eventsSuppressed
       Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'code-change', volume: 0.5
@@ -881,13 +882,26 @@ module.exports = class SpellView extends CocoView
   checkRequiredCode: =>
     return if @destroyed
     source = @getSource().replace @singleLineCommentRegex(), ''
-    for requiredCodeFragment in requiredCodePerLevel[@options.level.get('slug')]
+    requiredCodeFragments = LevelOptions[@options.level.get('slug')].requiredCode
+    for requiredCodeFragment in requiredCodeFragments
+      # Could make this obey regular expressions like suspectCode if needed
       if source.indexOf(requiredCodeFragment) is -1
         @warnedCodeFragments ?= {}
         unless @warnedCodeFragments[requiredCodeFragment]
           Backbone.Mediator.publish 'tome:required-code-fragment-deleted', codeFragment: requiredCodeFragment
         @warnedCodeFragments[requiredCodeFragment] = true
 
+  checkSuspectCode: =>
+    return if @destroyed
+    source = @getSource().replace @singleLineCommentRegex(), ''
+    suspectCodeFragments = LevelOptions[@options.level.get('slug')].suspectCode
+    for suspectCodeFragment in suspectCodeFragments
+      if suspectCodeFragment.pattern.test source
+        @warnedCodeFragments ?= {}
+        unless @warnedCodeFragments[suspectCodeFragment.name]
+          Backbone.Mediator.publish 'tome:suspect-code-fragment-added', codeFragment: suspectCodeFragment.name, codeLanguage: @spell.language
+        @warnedCodeFragments[suspectCodeFragment] = true
+
   destroy: ->
     $(@ace?.container).find('.ace_gutter').off 'click', '.ace_error, .ace_warning, .ace_info', @onAnnotationClick
     $(@ace?.container).find('.ace_gutter').off 'click', @onGutterClick
@@ -900,11 +914,3 @@ module.exports = class SpellView extends CocoView
     @debugView?.destroy()
     $(window).off 'resize', @onWindowResize
     super()
-
-
-requiredCodePerLevel =
-  'dungeons-of-kithgard': ['moveRight']
-  'true-names': ['Brak']
-  'the-first-kithmaze': ['loop']
-  'haunted-kithmaze': ['loop']
-  'lowly-kithmen': ['findNearestEnemy']

From 5a71d93d8a5e1953db27a54d9ad0db10a851b6f3 Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Wed, 19 Nov 2014 13:25:09 -0800
Subject: [PATCH 04/11] Hid play button for a couple more levels.

---
 app/lib/LevelOptions.coffee | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/app/lib/LevelOptions.coffee b/app/lib/LevelOptions.coffee
index df0cfa2f6..6e036418a 100644
--- a/app/lib/LevelOptions.coffee
+++ b/app/lib/LevelOptions.coffee
@@ -75,6 +75,7 @@ module.exports = LevelOptions =
     requiredCode: ['Brak']
   'favorable-odds':
     disableSpaces: true
+    hidesPlayButton: true
     hidesRunShortcut: true
     hidesHUD: true
     hidesSay: true
@@ -84,6 +85,7 @@ module.exports = LevelOptions =
     restrictedGear: {feet: 'leather-boots'}
   'the-raised-sword':
     disableSpaces: true
+    hidesPlayButton: true
     hidesRunShortcut: true
     hidesHUD: true
     hidesSay: true

From fe018309d0b81afcb4ef7db2c8d1fb96714b6aab Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Wed, 19 Nov 2014 13:47:44 -0800
Subject: [PATCH 05/11] Don't show blurbs for aggro messages. Big announces
 action blurbs for heroes.

---
 app/lib/surface/Lank.coffee | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/app/lib/surface/Lank.coffee b/app/lib/surface/Lank.coffee
index 731119d1c..4418ffbea 100644
--- a/app/lib/surface/Lank.coffee
+++ b/app/lib/surface/Lank.coffee
@@ -664,7 +664,9 @@ module.exports = Lank = class Lank extends CocoClass
   updateLabels: ->
     return unless @thang
     blurb = if @thang.health <= 0 then null else @thang.sayMessage  # Dead men tell no tales
-    @addLabel 'say', Label.STYLE_SAY if blurb
+    blurb = null if blurb in ['For Thoktar!', 'Bones!', 'Behead!', 'Destroy!', 'Die, humans!']  # Let's just hear, not see, these ones.
+    labelStyle = if /Hero Placeholder/.test(@thang.id) then Label.STYLE_DIALOGUE else Label.STYLE_SAY
+    @addLabel 'say', labelStyle if blurb
     if @labels.say?.setText blurb
       @notifySpeechUpdated blurb: blurb
     label.update() for name, label of @labels

From 13de055c0b306c04e7e8f63c277f8f92a60b7a93 Mon Sep 17 00:00:00 2001
From: Scott Erickson <sderickson@gmail.com>
Date: Wed, 19 Nov 2014 14:55:01 -0800
Subject: [PATCH 06/11] Set up a basic achievements list modal for the world
 map.

---
 app/models/CocoModel.coffee                   |  2 +
 .../play/modal/play-achievements-modal.sass   | 82 +++++++++++++++++-
 .../play/modal/play-achievements-modal.jade   | 27 +++++-
 app/templates/play/world-map-view.jade        |  2 +-
 .../play/modal/PlayAchievementsModal.coffee   | 83 +++++++++++++++++--
 server/achievements/EarnedAchievement.coffee  |  6 --
 .../earned_achievement_handler.coffee         | 21 ++++-
 server/commons/Handler.coffee                 | 13 ++-
 8 files changed, 215 insertions(+), 21 deletions(-)

diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee
index 4aeff828f..c6238d86b 100644
--- a/app/models/CocoModel.coffee
+++ b/app/models/CocoModel.coffee
@@ -50,6 +50,8 @@ class CocoModel extends Backbone.Model
     @loading = false
     @jqxhr = null
     @loadFromBackup()
+    
+  getCreationDate: -> new Date(parseInt(@id.slice(0,8), 16)*1000)
 
   getNormalizedURL: -> "#{@urlRoot}/#{@id}"
 
diff --git a/app/styles/play/modal/play-achievements-modal.sass b/app/styles/play/modal/play-achievements-modal.sass
index 5f3652962..db553646e 100644
--- a/app/styles/play/modal/play-achievements-modal.sass
+++ b/app/styles/play/modal/play-achievements-modal.sass
@@ -1,4 +1,82 @@
 #play-achievements-modal
-  .achievement-view
-    color: black
+
+  .modal-header
+    padding-bottom: 20px
   
+  img.icon
+    float: left
+    width: 40px
+    margin-right: 10px
+
+    
+  //- Unachieved Panels
+
+  .panel
+    margin: 5px 0
+    position: relative
+    border: 2px solid rgb(75,75,75)
+    padding: 2px
+    
+    h3
+      margin: 0 0 0 50px
+      color: rgb(75,75,75)
+
+    p
+      margin: 0 0 0 50px
+      color: rgb(75,75,75)
+
+    .panel-body
+      padding: 5px 150px 5px 5px
+      border: 2px solid rgb(150,150,150)
+      
+    .created
+      position: absolute
+      right: 10px
+      top: 5px
+      color: rgb(75,75,75)
+      font-size: 12px
+
+
+  //- Achieved Panels
+    
+  .panel.earned
+    background: rgb(50,40,33)
+    border: 5px solid rgb(26,21,17)
+    padding: 0
+    
+    h3
+      color: white
+      
+    p
+      color: rgb(203,170,148)
+
+    .panel-body
+      border: 2px solid rgb(75,62,51)
+      
+    .created
+      color: rgb(255,189,68)
+
+
+  //- Rewards
+    
+  .rewards
+    position: absolute
+    right: .2em
+    //bottom: 10px
+    top: 29px
+    
+    .label
+      font-size: 18px
+      margin-left: 5px
+      color: rgb(50,40,33)
+      background: rgb(203,170,148)
+      
+    .gems
+      background: #94ccc7
+      
+    .worth
+      background: #d8c488
+    
+    img
+      width: 12px
+      height: 12px
\ No newline at end of file
diff --git a/app/templates/play/modal/play-achievements-modal.jade b/app/templates/play/modal/play-achievements-modal.jade
index 13d244310..b71a88d9d 100644
--- a/app/templates/play/modal/play-achievements-modal.jade
+++ b/app/templates/play/modal/play-achievements-modal.jade
@@ -4,4 +4,29 @@ block modal-header-content
   h3(data-i18n="play.achievements") Achievements
 
 block modal-body-content
-  p TODO: show all dem achievements
+  for achievement in achievements
+    .panel(class=achievement.earned ? 'earned' : '')
+      .panel-body
+        img.icon(src=achievement.getImageURL())
+        h3= achievement.name
+        p= achievement.description
+        
+      if achievement.earnedDate
+        .created=moment(achievement.earnedDate).fromNow()
+      else
+        .created(data-i18n="user.status_unfinished")
+
+      .rewards
+        - rewards = achievement.get('rewards')
+        - rewards = { gems: 100 }
+        if rewards && rewards.gems
+          span.gems.label.label-default
+            span= rewards.gems
+            img.gem(src="/images/common/gem.png")
+        
+        - worth = achievement.get('worth')
+        if worth
+          span.worth.label.label-default
+            span #{worth}xp
+        // maybe add more icons/numbers for items, heroes, levels, xp?
+block modal-footer
\ No newline at end of file
diff --git a/app/templates/play/world-map-view.jade b/app/templates/play/world-map-view.jade
index b56457b49..fc43175f7 100644
--- a/app/templates/play/world-map-view.jade
+++ b/app/templates/play/world-map-view.jade
@@ -43,10 +43,10 @@
 .game-controls.header-font
   button.btn.items(data-toggle='coco-modal', data-target='play/modal/PlayItemsModal', data-i18n="[title]play.items")
   button.btn.heroes(data-toggle='coco-modal', data-target='play/modal/PlayHeroesModal', data-i18n="[title]play.heroes")
+  button.btn.achievements(data-toggle='coco-modal', data-target='play/modal/PlayAchievementsModal', data-i18n="[title]play.achievements")
   if me.get('anonymous') === false
     button.btn.gems(data-toggle='coco-modal', data-target='play/modal/BuyGemsModal', data-i18n="[title]play.buy_gems")
   if me.isAdmin()
-    button.btn.achievements(data-toggle='coco-modal', data-target='play/modal/PlayAchievementsModal', data-i18n="[title]play.achievements")
     button.btn.account(data-toggle='coco-modal', data-target='play/modal/PlayAccountModal', data-i18n="[title]play.account")
     button.btn.settings(data-toggle='coco-modal', data-target='play/modal/PlaySettingsModal', data-i18n="[title]play.settings")
   else if me.get('anonymous', true)
diff --git a/app/views/play/modal/PlayAchievementsModal.coffee b/app/views/play/modal/PlayAchievementsModal.coffee
index ca0940b88..360330685 100644
--- a/app/views/play/modal/PlayAchievementsModal.coffee
+++ b/app/views/play/modal/PlayAchievementsModal.coffee
@@ -2,27 +2,92 @@ ModalView = require 'views/kinds/ModalView'
 template = require 'templates/play/modal/play-achievements-modal'
 CocoCollection = require 'collections/CocoCollection'
 Achievement = require 'models/Achievement'
-#AchievementView = require 'views/game-menu/AchievementView'
+EarnedAchievement = require 'models/EarnedAchievement'
+
+utils = require 'lib/utils'
+
+PAGE_SIZE = 200
 
 module.exports = class PlayAchievementsModal extends ModalView
   className: 'modal fade play-modal'
   template: template
   modalWidthPercent: 90
   id: 'play-achievements-modal'
-  #instant: true
-
-  #events:
-  #  'change input.select': 'onSelectionChanged'
+  plain: true
+  
+  earnedMap: {}
 
   constructor: (options) ->
     super options
-    #@achievements = new CocoCollection([], {model: Achievement})
-    #@achievements.url = '/db/thang.type?view=achievements&project=name,description,components,original,rasterIcon'
-    #@supermodel.loadCollection(@achievements, 'achievements')
+    @achievements = new Backbone.Collection()
+    earnedMap = {}
+    
+    achievementsFetcher = new CocoCollection([], {url: '/db/achievement', model: Achievement})
+    achievementsFetcher.setProjection([
+      'name'
+      'description'
+      'icon'
+      'worth'
+      'i18n'
+      'rewards'
+      'collection'
+    ])
+    
+    earnedAchievementsFetcher = new CocoCollection([], {url: '/db/earned_achievement', model: EarnedAchievement})
+    earnedAchievementsFetcher.setProjection([ 'achievement' ])
+    
+    achievementsFetcher.skip = 0
+    achievementsFetcher.fetch({data: {skip: 0, limit: PAGE_SIZE}})
+    earnedAchievementsFetcher.skip = 0
+    earnedAchievementsFetcher.fetch({data: {skip: 0, limit: PAGE_SIZE}})
+
+    @listenTo achievementsFetcher, 'sync', @onAchievementsLoaded
+    @listenTo earnedAchievementsFetcher, 'sync', @onEarnedAchievementsLoaded
+
+    @supermodel.loadCollection(achievementsFetcher, 'achievement')
+    @supermodel.loadCollection(earnedAchievementsFetcher, 'achievement')
+    
+    @onEverythingLoaded = _.after(2, @onEverythingLoaded)
+
+  onAchievementsLoaded: (fetcher) ->
+    needMore = fetcher.models.length is PAGE_SIZE
+    @achievements.add(fetcher.models)
+    if needMore
+      fetcher.skip += PAGE_SIZE
+      fetcher.fetch({data: {skip: fetcher.skip, limit: PAGE_SIZE}})
+    else
+      @stopListening(fetcher)
+      @onEverythingLoaded()
+
+  onEarnedAchievementsLoaded: (fetcher) ->
+    needMore = fetcher.models.length is PAGE_SIZE
+    for earned in fetcher.models
+      @earnedMap[earned.get('achievement')] = earned
+    if needMore
+      fetcher.skip += PAGE_SIZE
+      fetcher.fetch({data: {skip: fetcher.skip, limit: PAGE_SIZE}})
+    else
+      @stopListening(fetcher)
+      @onEverythingLoaded()
+
+  onEverythingLoaded: =>
+    @achievements.set(@achievements.filter((m) -> m.get('collection') isnt 'level.sessions'))
+    for achievement in @achievements.models
+      if earned = @earnedMap[achievement.id]
+        achievement.earned = earned
+        achievement.earnedDate = earned.getCreationDate()
+      achievement.earnedDate ?= ''
+    @achievements.comparator = (m) -> m.earnedDate
+    @achievements.sort()
+    @achievements.set(@achievements.models.reverse())
+    for achievement in @achievements.models
+      achievement.name = utils.i18n achievement.attributes, 'name'
+      achievement.description = utils.i18n achievement.attributes, 'description'
+    @render()
 
   getRenderData: (context={}) ->
     context = super(context)
-    #context.achievements = @achievements.models
+    context.achievements = @achievements.models
     context
 
   afterRender: ->
diff --git a/server/achievements/EarnedAchievement.coffee b/server/achievements/EarnedAchievement.coffee
index 685a502c6..80d6f6249 100644
--- a/server/achievements/EarnedAchievement.coffee
+++ b/server/achievements/EarnedAchievement.coffee
@@ -2,12 +2,6 @@ mongoose = require 'mongoose'
 jsonschema = require '../../app/schemas/models/earned_achievement'
 
 EarnedAchievementSchema = new mongoose.Schema({
-  created:
-    type: Date
-    default: Date.now
-  changed:
-    type: Date
-    default: Date.now
   notified:
     type: Boolean
     default: false
diff --git a/server/achievements/earned_achievement_handler.coffee b/server/achievements/earned_achievement_handler.coffee
index e39e1dd4c..7eb2c7437 100644
--- a/server/achievements/earned_achievement_handler.coffee
+++ b/server/achievements/earned_achievement_handler.coffee
@@ -16,7 +16,26 @@ class EarnedAchievementHandler extends Handler
 
   get: (req, res) ->
     return @getByAchievementIDs(req, res) if req.query.view is 'get-by-achievement-ids'
-    super(arguments...)
+    query = { user: req.user._id+''}
+
+    projection = {}
+    if req.query.project
+      projection[field] = 1 for field in req.query.project.split(',')
+
+    q = EarnedAchievement.find(query, projection)
+
+    skip = parseInt(req.query.skip)
+    if skip? and skip < 1000000
+      q.skip(skip)
+
+    limit = parseInt(req.query.limit)
+    if limit? and limit < 1000
+      q.limit(limit)
+
+    q.exec (err, documents) =>
+      return @sendDatabaseError(res, err) if err
+      documents = (@formatEntity(req, doc) for doc in documents)
+      @sendSuccess(res, documents)
 
   getByAchievementIDs: (req, res) ->
     query = { user: req.user._id+''}
diff --git a/server/commons/Handler.coffee b/server/commons/Handler.coffee
index 15ecc3b11..b9a5d6f2e 100644
--- a/server/commons/Handler.coffee
+++ b/server/commons/Handler.coffee
@@ -129,6 +129,10 @@ module.exports = class Handler
       term = req.query.term
       matchedObjects = []
       filters = if @modelClass.schema.uses_coco_versions or @modelClass.schema.uses_coco_permissions then [filter: {index: true}] else [filter: {}]
+
+      skip = parseInt(req.query.skip)
+      limit = parseInt(req.query.limit)
+
       if @modelClass.schema.uses_coco_permissions and req.user
         filters.push {filter: {index: req.user.get('id')}}
       projection = null
@@ -158,7 +162,14 @@ module.exports = class Handler
         else
           args = [filter.filter]
           args.push projection if projection
-          @modelClass.find(args...).limit(FETCH_LIMIT).exec callback
+          q = @modelClass.find(args...)
+          if skip? and skip < 1000000
+            q.skip(skip)
+          if limit? and limit < FETCH_LIMIT
+            q.limit(limit)
+          else
+            q.limit(FETCH_LIMIT)
+          q.exec callback
     # if it's not a text search but the user is an admin, let him try stuff anyway
     else if req.user?.isAdmin()
       # admins can send any sort of query down the wire

From 07a09e34d87565cfaf8b92fa96c304680812b436 Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Wed, 19 Nov 2014 15:01:20 -0800
Subject: [PATCH 07/11] Fixed small-caps font-variant on docs popover titles.

---
 app/styles/play/level/tome/spell_palette_entry.sass            | 1 +
 app/templates/play/level/tome/spell_palette_entry_popover.jade | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/app/styles/play/level/tome/spell_palette_entry.sass b/app/styles/play/level/tome/spell_palette_entry.sass
index cb37378b5..b88b6bfb2 100644
--- a/app/styles/play/level/tome/spell_palette_entry.sass
+++ b/app/styles/play/level/tome/spell_palette_entry.sass
@@ -80,6 +80,7 @@
 
   h1:not(.not-code), h2:not(.not-code), h3:not(.not-code), h4:not(.not-code), h5:not(.not-code), h6:not(.not-code)
     font-family: Menlo, Monaco, Consolas, "Courier New", monospace
+    font-variant: normal
 
   .popover-title
     background-color: transparent
diff --git a/app/templates/play/level/tome/spell_palette_entry_popover.jade b/app/templates/play/level/tome/spell_palette_entry_popover.jade
index c13df8394..bd10fc3dd 100644
--- a/app/templates/play/level/tome/spell_palette_entry_popover.jade
+++ b/app/templates/play/level/tome/spell_palette_entry_popover.jade
@@ -1,5 +1,5 @@
 h4
-  span= doc.shortName
+  span.prop-name= doc.shortName
   |  - 
   code.prop-type= doc.type == 'function' && doc.owner == 'this' ? 'method' : doc.type
   if doc.type != 'function'

From c82d1d0f362c77260a5d34b27e1b032ed8d9e123 Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Wed, 19 Nov 2014 15:13:45 -0800
Subject: [PATCH 08/11] Additional scriptable property on thang-died events.

---
 app/schemas/subscriptions/world.coffee | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/schemas/subscriptions/world.coffee b/app/schemas/subscriptions/world.coffee
index bd0b8c2ce..a3da038c3 100644
--- a/app/schemas/subscriptions/world.coffee
+++ b/app/schemas/subscriptions/world.coffee
@@ -9,6 +9,7 @@ module.exports =
     thang: {type: 'object'}
     killer: {type: 'object'}
     killerHealth: {type: ['number', 'undefined']}
+    maxHealth: {type: 'number'}
 
   'world:thang-touched-goal': c.object {required: ['actor', 'touched']},
     replacedNoteChain: {type: 'array'}

From 6755c90f022f2dd971398351e5a6b87c9f567a6b Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Wed, 19 Nov 2014 15:18:59 -0800
Subject: [PATCH 09/11] Properly continue to Haunted Kithmaze from The Raised
 Sword if that's the A/B test group.

---
 app/views/play/WorldMapView.coffee | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/views/play/WorldMapView.coffee b/app/views/play/WorldMapView.coffee
index 7061f8e9f..a6e45686c 100644
--- a/app/views/play/WorldMapView.coffee
+++ b/app/views/play/WorldMapView.coffee
@@ -773,3 +773,4 @@ if me.getKithmazeGroup() is 'the-first-kithmaze'
   _.remove dungeon, id: 'haunted-kithmaze'
 else
   _.remove dungeon, id: 'the-first-kithmaze'
+  _.find(dungeon, id: 'the-raised-sword').nextLevels.continue = 'haunted-kithmaze'

From f310719ceee00c095e727d659a7093bda4c0bc75 Mon Sep 17 00:00:00 2001
From: Nick Winter <livelily@gmail.com>
Date: Wed, 19 Nov 2014 15:24:50 -0800
Subject: [PATCH 10/11] Restarting level now doesn't clear anything except code
 unless you hold down shift.

---
 app/schemas/subscriptions/tome.coffee              | 2 +-
 app/views/play/level/modal/ReloadLevelModal.coffee | 8 +++++++-
 app/views/play/level/tome/SpellView.coffee         | 2 +-
 3 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/app/schemas/subscriptions/tome.coffee b/app/schemas/subscriptions/tome.coffee
index 26712d1e1..2d5d4a966 100644
--- a/app/schemas/subscriptions/tome.coffee
+++ b/app/schemas/subscriptions/tome.coffee
@@ -35,7 +35,7 @@ module.exports =
 
   'tome:toggle-spell-list': c.object {title: 'Toggle Spell List', description: 'Published when you toggle the dropdown for a thang\'s spells'}
 
-  'tome:reload-code': c.object {title: 'Reload Code', description: 'Published when you reset a spell to its original source', required: ['spell']},
+  'tome:reload-code': c.object {title: 'Reload Code', description: 'Published when you reset a spell to its original source', required: []},
     spell: {type: 'object'}
 
   'tome:palette-cleared': c.object {title: 'Palette Cleared', description: 'Published when the spell palette is about to be cleared and recreated.'},
diff --git a/app/views/play/level/modal/ReloadLevelModal.coffee b/app/views/play/level/modal/ReloadLevelModal.coffee
index 320ecde21..d11bb4239 100644
--- a/app/views/play/level/modal/ReloadLevelModal.coffee
+++ b/app/views/play/level/modal/ReloadLevelModal.coffee
@@ -6,4 +6,10 @@ module.exports = class ReloadLevelModal extends ModalView
   template: template
 
   events:
-    'click #restart-level-confirm-button': -> Backbone.Mediator.publish 'level:restart', {}
+    'click #restart-level-confirm-button': 'onClickRestart'
+
+  onClickRestart: (e) ->
+    if key.shift
+      Backbone.Mediator.publish 'level:restart', {}
+    else
+      Backbone.Mediator.publish 'tome:reload-code', {}
diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee
index 209ac6f36..c1429a400 100644
--- a/app/views/play/level/tome/SpellView.coffee
+++ b/app/views/play/level/tome/SpellView.coffee
@@ -394,7 +394,7 @@ module.exports = class SpellView extends CocoView
     @focus() if cast
 
   onCodeReload: (e) ->
-    return unless e.spell is @spell
+    return unless e.spell is @spell or not e.spell
     @reloadCode true
     @ace.clearSelection()
     _.delay (=> @ace?.clearSelection()), 500  # Make double sure this gets done (saw some timing issues?)

From 41db27c709471785201d065bb9619fdbe884be4d Mon Sep 17 00:00:00 2001
From: Matt Lott <mattlott@live.com>
Date: Wed, 19 Nov 2014 15:27:06 -0800
Subject: [PATCH 11/11] Apply markdown to error messages and hints

---
 app/templates/play/level/tome/problem_alert.jade  | 1 -
 app/views/play/level/tome/ProblemAlertView.coffee | 2 +-
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/app/templates/play/level/tome/problem_alert.jade b/app/templates/play/level/tome/problem_alert.jade
index cc80b0a53..6951145b8 100644
--- a/app/templates/play/level/tome/problem_alert.jade
+++ b/app/templates/play/level/tome/problem_alert.jade
@@ -3,7 +3,6 @@ h3.problem-alert-title(data-i18n="play_level.problem_alert_title") Fix Your Code
 if hint
   span.problem-title!= hint
   br
-  br
   span.problem-subtitle!= message
 else
   span.problem-title!= message
diff --git a/app/views/play/level/tome/ProblemAlertView.coffee b/app/views/play/level/tome/ProblemAlertView.coffee
index 907621365..24eb6b1c9 100644
--- a/app/views/play/level/tome/ProblemAlertView.coffee
+++ b/app/views/play/level/tome/ProblemAlertView.coffee
@@ -34,7 +34,7 @@ module.exports = class ProblemAlertView extends CocoView
   getRenderData: (context={}) ->
     context = super context
     if @problem?
-      format = (s) -> s?.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>')
+      format = (s) -> marked(s.replace(/</g, '&lt;').replace(/>/g, '&gt;')) if s?
       context.message = format @problem.aetherProblem.message
       context.hint = format @problem.aetherProblem.hint
     context