diff --git a/app/lib/CampaignOptions.coffee b/app/lib/CampaignOptions.coffee
index 9762b4a9e..8fb64858a 100644
--- a/app/lib/CampaignOptions.coffee
+++ b/app/lib/CampaignOptions.coffee
@@ -6,8 +6,10 @@ CampaignList = require('views/play/WorldMapView').campaigns
 options =
   'default':
     autocompleteFontSizePx: 16
+    backspaceThrottle: false
   'dungeon':
     autocompleteFontSizePx: 20
+    backspaceThrottle: true
 
 module.exports = CampaignOptions =
   getCampaignForSlug: (slug) ->
diff --git a/app/lib/LevelOptions.coffee b/app/lib/LevelOptions.coffee
index c4e0d6778..78ca21820 100644
--- a/app/lib/LevelOptions.coffee
+++ b/app/lib/LevelOptions.coffee
@@ -91,7 +91,7 @@ module.exports = LevelOptions =
     hidesSay: true
     hidesCodeToolbar: true
     hidesRealTimePlayback: true
-    requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic'}
+    requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate'}
     restrictedGear: {feet: 'leather-boots'}
   'the-first-kithmaze':
     hidesRunShortcut: true
@@ -108,6 +108,7 @@ module.exports = LevelOptions =
     hidesSay: true
     hidesCodeToolbar: true
     hidesRealTimePlayback: true
+    moveRightLoopSnippet: true
     requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
     restrictedGear: {feet: 'leather-boots'}
     requiredCode: ['loop']
@@ -123,6 +124,7 @@ module.exports = LevelOptions =
     hidesSay: true
     hidesCodeToolbar: true
     hidesRealTimePlayback: true
+    moveRightLoopSnippet: true
     requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
     restrictedGear: {feet: 'leather-boots'}
   'dread-door':
@@ -137,14 +139,14 @@ module.exports = LevelOptions =
     hidesSay: true
     hidesCodeToolbar: true
     hidesRealTimePlayback: true
-    requiredGear: {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', torso: 'leather-tunic'}
+    requiredGear: {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', torso: 'tarnished-bronze-breastplate'}
     restrictedGear: {feet: 'leather-boots'}
   'master-of-names':
     hidesHUD: true
     hidesSay: true
     hidesCodeToolbar: true
     hidesRealTimePlayback: true
-    requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'leather-tunic'}
+    requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'tarnished-bronze-breastplate'}
     restrictedGear: {feet: 'leather-boots'}
     requiredCode: ['findNearestEnemy']
     suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
@@ -153,7 +155,7 @@ module.exports = LevelOptions =
     hidesSay: true
     hidesCodeToolbar: true
     hidesRealTimePlayback: true
-    requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'leather-tunic'}
+    requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'tarnished-bronze-breastplate'}
     restrictedGear: {feet: 'leather-boots'}
     requiredCode: ['findNearestEnemy']
     suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
@@ -162,7 +164,7 @@ module.exports = LevelOptions =
     hidesSay: true
     hidesCodeToolbar: true
     hidesRealTimePlayback: true
-    requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', eyes: 'crude-glasses'}
+    requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', eyes: 'crude-glasses'}
     restrictedGear: {feet: 'leather-boots'}
     suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
   'tactical-strike':
@@ -170,7 +172,7 @@ module.exports = LevelOptions =
     hidesSay: true
     hidesCodeToolbar: true
     hidesRealTimePlayback: true
-    requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', eyes: 'crude-glasses'}
+    requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', eyes: 'crude-glasses'}
     restrictedGear: {feet: 'leather-boots'}
     suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
   'the-final-kithmaze':
@@ -178,21 +180,21 @@ module.exports = LevelOptions =
     hidesSay: true
     hidesCodeToolbar: true
     hidesRealTimePlayback: true
-    requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
+    requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
     suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
   'the-gauntlet':
     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'}
+    requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', '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
     hidesRealTimePlayback: true
-    requiredGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', torso: 'leather-tunic'}
+    requiredGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', torso: 'tarnished-bronze-breastplate'}
     restrictedGear: {'right-hand': 'simple-sword'}
   'defense-of-plainswood':
     hidesRealTimePlayback: true
@@ -217,25 +219,51 @@ module.exports = LevelOptions =
     hidesCodeToolbar: true
     requiredGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
     restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword'}
+    requiredCode: ['topEnemy']
   'back-to-back':
     hidesCodeToolbar: true
-    requiredGear: {feet: 'leather-boots', torso: 'leather-tunic', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'}
+    requiredGear: {feet: 'leather-boots', torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'}
     restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer'}
   'ogre-encampment':
-    requiredGear: {torso: 'leather-tunic', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'}
+    requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'}
     restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer'}
   'woodland-cleaver':
-    requiredGear: {torso: 'leather-tunic', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'wooden-shield', wrists: 'sundial-wristwatch', feet: 'leather-boots'}
+    requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'wooden-shield', wrists: 'sundial-wristwatch', feet: 'leather-boots'}
     restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword'}
   'shield-rush':
-    requiredGear: {torso: 'leather-tunic', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
+    requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
     restrictedGear: {'left-hand': 'wooden-shield'}
+
+  # Warrior branch
   'peasant-protection':
-    requiredGear: {torso: 'leather-tunic', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
+    requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
     restrictedGear: {eyes: 'crude-glasses'}
   'munchkin-swarm':
-    requiredGear: {torso: 'leather-tunic', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
+    requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
     restrictedGear: {}
+
+  # Ranger branch
+  'munchkin-harvest':
+    requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
+    restrictedGear: {}
+  'swift-dagger':
+    requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'crude-dagger', wrists: 'sundial-wristwatch'}
+    restrictedGear: {eyes: 'crude-glasses'}
+  'shrapnel':
+    requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'weak-charge', wrists: 'sundial-wristwatch'}
+    restrictedGear: {eyes: 'crude-glasses', 'left-hand': 'crude-dagger'}
+
+  # Wizard branch
+  'arcane-ally':
+    requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
+    restrictedGear: {eyes: 'crude-glasses'}
+  'touch-of-death':
+    requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'unholy-tome-i', wrists: 'sundial-wristwatch'}
+    restrictedGear: {}
+  'bonemender':
+    requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'book-of-life-i', wrists: 'sundial-wristwatch'}
+    restrictedGear: {'left-hand': 'unholy-tome-i'}
+
   'coinucopia':
     requiredGear: {'programming-book': 'programmaticon-i', feet: 'leather-boots', 'programming-book': 'programmaticon-ii', flag: 'basic-flags'}
     restrictedGear: {}
@@ -249,8 +277,11 @@ module.exports = LevelOptions =
     requiredGear: {'programming-book': 'programmaticon-i', feet: 'leather-boots', 'programming-book': 'programmaticon-ii', flag: 'basic-flags', eyes: 'wooden-glasses', 'right-hand': 'crude-builders-hammer'}
     restrictedGear: {'right-hand': 'long-sword'}
   'rich-forager':
-    requiredGear: {'programming-book': 'programmaticon-i', feet: 'leather-boots', 'programming-book': 'programmaticon-ii', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'leather-tunic', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield'}
+    requiredGear: {'programming-book': 'programmaticon-i', feet: 'leather-boots', 'programming-book': 'programmaticon-ii', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield'}
     restrictedGear: {'right-hand': 'crude-builders-hammer'}
   'multiplayer-treasure-grove':
-    requiredGear: {'programming-book': 'programmaticon-i', feet: 'leather-boots', 'programming-book': 'programmaticon-ii', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'leather-tunic'}
+    requiredGear: {'programming-book': 'programmaticon-i', feet: 'leather-boots', 'programming-book': 'programmaticon-ii', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate'}
+    restrictedGear: {}
+  'siege-of-stonehold':
+    requiredGear: {}
     restrictedGear: {}
diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index 016fc81d6..195dab4b2 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -342,6 +342,7 @@ module.exports = Surface = class Surface extends CocoClass
       @ended = true
       @setPaused true
       Backbone.Mediator.publish 'surface:playback-ended', {}
+      @updatePaths()  # TODO: this is a hack to make sure paths are on the first time the level loads
     else if @currentFrame < @world.totalFrames and @ended
       @ended = false
       @setPaused false
@@ -586,7 +587,6 @@ module.exports = Surface = class Surface extends CocoClass
 
   updatePaths: ->
     return unless @options.paths and @heroLank
-    return unless me.isAdmin() # TODO: Fix world thang points, targets, then remove this
     @hidePaths()
     return if @world.showPaths is 'never'
     layerAdapter = @lankBoss.layerAdapters['Path']
diff --git a/app/locale/ru.coffee b/app/locale/ru.coffee
index a0c9f6b89..2535bc95c 100644
--- a/app/locale/ru.coffee
+++ b/app/locale/ru.coffee
@@ -64,7 +64,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
     next: "Выбрать" # Go from choose hero to choose inventory before playing a level
     change_hero: "Выбрать героя" # Go back from choose inventory to choose hero
     choose_inventory: "Выбрать предметы"
-#    buy_gems: "Buy Gems"
+    buy_gems: "Купить самоцветы"
     older_campaigns: "Старые кампании"
     anonymous: "Неизвестный игрок"
     level_difficulty: "Сложность: "
@@ -172,7 +172,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
     medium: "Нормально"
     hard: "Сложно"
     player: "Игрок"
-#    player_level: "Level" # Like player level 5, not like level: Dungeons of Kithgard
+    player_level: "Уровень" # Like player level 5, not like level: Dungeons of Kithgard
 
   units:
     second: "секунда"
@@ -315,17 +315,17 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
 #    equip: "Equip"
 #    unequip: "Unequip"
 
-#  buy_gems:
-#    few_gems: "A few gems"
-#    pile_gems: "Pile of gems"
-#    chest_gems: "Chest of gems"
+  buy_gems:
+    few_gems: "Немного самоцветов"
+    pile_gems: "Кучка самоцветов"
+    chest_gems: "Сундук с самоцветами"
 
   choose_hero:
     choose_hero: "Выберите героя"
     programming_language: "Язык программирования"
     programming_language_description: "Какой язык программирования вы хотите использовать?"
-#    default: "Default"
-#    experimental: "Experimental"
+    default: "По умолчанию"
+    experimental: "Экспериментальный"
     python_blurb: "Пусть простой, но мощный, Python - прекрасный язык программирования общего применения."
     javascript_blurb: "Язык для Сети."
     coffeescript_blurb: "Улучшенный синтаксис JavaScript."
@@ -647,9 +647,9 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
     diplomat_launch_url: "запуска в октябре"
     diplomat_introduction_suf: "было то, что есть значительная заинтересованность в CodeCombat в других странах! Мы создаём корпус переводчиков, стремящихся превратить один набор слов в другой набор слов для максимальной доступности CodeCombat по всему миру. Если вы любите видеть контент до официального выхода и получать эти уровни для ваших соотечественников как можно скорее, этот класс для вас."
     diplomat_attribute_1: "Свободное владение английским языком и языком, на который вы хотели бы переводить. При передаче сложных идей важно иметь сильную хватку в обоих!"
-#    diplomat_i18n_page_prefix: "You can start translating our levels by going to our"
-#    diplomat_i18n_page: "translations page"
-#    diplomat_i18n_page_suffix: ", or our interface and website on GitHub."
+    diplomat_i18n_page_prefix: "Вы можете начать переводить уровни, посетив нашу"
+    diplomat_i18n_page: "страницу переводчиков"
+    diplomat_i18n_page_suffix: ", или перевести наш интерфейс и сайт на GitHub."
     diplomat_join_pref_github: "Найдите файл локализации вашего языка "
     diplomat_github_url: "на GitHub"
     diplomat_join_suf_github: ", отредактируйте его онлайн и отправьте запрос на подтверждение изменений. Кроме того, установите флажок ниже, чтобы быть в курсе новых разработок интернационализации!"
diff --git a/app/schemas/subscriptions/auth.coffee b/app/schemas/subscriptions/auth.coffee
index fc396e4c4..3d1665ebe 100644
--- a/app/schemas/subscriptions/auth.coffee
+++ b/app/schemas/subscriptions/auth.coffee
@@ -8,6 +8,8 @@ module.exports =
 
   'auth:logging-in-with-facebook': c.object {}
   
+  'auth:signed-up': c.object {}
+
   'auth:logging-out': c.object {}
 
   'auth:logged-in-with-facebook': c.object {title: 'Facebook logged in', description: 'Published when you successfully logged in with Facebook', required: ['response']},
diff --git a/app/schemas/subscriptions/misc.coffee b/app/schemas/subscriptions/misc.coffee
index 5773dbbff..f85a036d7 100644
--- a/app/schemas/subscriptions/misc.coffee
+++ b/app/schemas/subscriptions/misc.coffee
@@ -29,6 +29,9 @@ module.exports =
 
   'modal:closed': c.object {}
 
+  'modal:open-modal-view': c.object {required: ['modalPath']},
+    modalPath: {type: 'string'}
+
   'router:navigate': c.object {required: ['route']},
     route: {type: 'string'}
     view: {type: 'object'}
@@ -49,11 +52,15 @@ module.exports =
     progress: {type: 'number', minimum: 0, maximum: 1}
 
   'buy-gems-modal:update-products': { }
-  
+
   'buy-gems-modal:purchase-initiated': c.object {required: ['productID']},
     productID: { type: 'string' }
 
   'stripe:received-token': c.object { required: ['token'] },
     token: { type: 'object', properties: {
       id: {type: 'string'}
-    }}
\ No newline at end of file
+    }}
+
+  'store:item-purchased': c.object {required: ['item', 'itemSlug']},
+    item: {type: 'object'}
+    itemSlug: {type: 'string'}
diff --git a/app/styles/play/level/level-dialogue-view.sass b/app/styles/play/level/level-dialogue-view.sass
index f29d65f8a..a1a01dbf8 100644
--- a/app/styles/play/level/level-dialogue-view.sass
+++ b/app/styles/play/level/level-dialogue-view.sass
@@ -54,9 +54,12 @@
       font-size: 18px
       line-height: 20px
 
-      strong
+      strong, a
         color: #FFCCAA
 
+      a
+        text-decoration: underline
+
       .hud-hint
         font-weight: normal
         color: #ddd
diff --git a/app/styles/play/level/tome/spell.sass b/app/styles/play/level/tome/spell.sass
index b3b0a82ec..39f65f6d4 100644
--- a/app/styles/play/level/tome/spell.sass
+++ b/app/styles/play/level/tome/spell.sass
@@ -66,7 +66,7 @@
     &.disabled
       @include opacity(0.8)
       .ace_cursor, .executing, .ace_active-line, .ace_gutter-active-line
-        @include opacity(0.2)
+        @include opacity(0.1)
 
     .ace_gutter
       background-color: transparent
diff --git a/app/styles/play/level/tome/spell_palette_entry.sass b/app/styles/play/level/tome/spell_palette_entry.sass
index 93640e9ef..fdda33886 100644
--- a/app/styles/play/level/tome/spell_palette_entry.sass
+++ b/app/styles/play/level/tome/spell_palette_entry.sass
@@ -45,11 +45,15 @@
   //  color: rgb(197, 6, 11)
   color: rgb(243, 169, 49)
 
+
+body:not(.dialogue-view-active)
+  .spell-palette-popover.popover
+    right: 45%
+    min-width: 350px
+
 .spell-palette-popover.popover
   // Only those popovers which are our direct children (spell documentation)
   max-width: 600px
-  right: 45%
-  min-width: 350px
 
   &.pinned
     left: auto !important
diff --git a/app/templates/play/modal/play-items-modal.jade b/app/templates/play/modal/play-items-modal.jade
index 3e81ebf53..08d0d52ba 100644
--- a/app/templates/play/modal/play-items-modal.jade
+++ b/app/templates/play/modal/play-items-modal.jade
@@ -49,11 +49,11 @@
                     span.cost
                       img(src="/images/common/gem.png", draggable="false")
                       span.big-font= item.get('gems')
-                    if item.equippable
+                    if item.unequippable
                       // Temp, while we only have Warriors: prevent them from buying non-Warrior stuff
-                      button.btn.unlock-button.big-font(data-i18n="play.unlock", disabled=!item.affordable, data-item-id=item.id)
-                    else
                       span.big-font.unequippable= item.get('heroClass')
+                    else
+                      button.btn.unlock-button.big-font(data-i18n="play.unlock", disabled=!item.affordable, data-item-id=item.id)
               .clearfix
               
     #item-details-view
diff --git a/app/templates/play/world-map-view.jade b/app/templates/play/world-map-view.jade
index 53134e321..10612fc5c 100644
--- a/app/templates/play/world-map-view.jade
+++ b/app/templates/play/world-map-view.jade
@@ -43,7 +43,7 @@
   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
+  if me.get('anonymous') === false || me.get('iosIdentifierForVendor') || isIPadApp
     button.btn.gems(data-toggle='coco-modal', data-target='play/modal/BuyGemsModal', data-i18n="[title]play.buy_gems")
   if me.isAdmin()
     button.btn.account(data-toggle='coco-modal', data-target='play/modal/PlayAccountModal', data-i18n="[title]play.account")
diff --git a/app/views/editor/level/treema_nodes.coffee b/app/views/editor/level/treema_nodes.coffee
index 989adfa22..e04092c8b 100644
--- a/app/views/editor/level/treema_nodes.coffee
+++ b/app/views/editor/level/treema_nodes.coffee
@@ -289,5 +289,4 @@ module.exports.ItemThangTypeNode = ItemThangTypeNode = class ItemThangTypeNode e
 
   processThangType: (thangType) ->
     return unless itemComponent = _.find thangType.get('components'), {original: LevelComponent.ItemID}
-    return unless itemComponent.config?.slots?.length
-    @constructor.thangTypes.push name: thangType.get('name'), original: thangType.get('original'), slots: itemComponent.config.slots
+    @constructor.thangTypes.push name: thangType.get('name'), original: thangType.get('original'), slots: itemComponent.config?.slots ? ['right-hand']
diff --git a/app/views/game-menu/InventoryModal.coffee b/app/views/game-menu/InventoryModal.coffee
index 618af9815..356adc24a 100644
--- a/app/views/game-menu/InventoryModal.coffee
+++ b/app/views/game-menu/InventoryModal.coffee
@@ -84,7 +84,7 @@ module.exports = class InventoryModal extends ModalView
 
     # sort into one of the four groups
     locked = not (item.get('original') in me.items())
-    locked = false if me.get('slug') is 'nick'
+    #locked = false if me.get('slug') is 'nick'
 
     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
@@ -247,6 +247,8 @@ module.exports = class InventoryModal extends ModalView
       @delegateEvents()
       @setUpDraggableEventsForAvailableEquipment()
       @itemDetailsView.setItem(item)
+
+      Backbone.Mediator.publish 'store:item-purchased', item: item, itemSlug: item.get('slug')
     else
       button.addClass('confirm').text($.i18n.t('play.confirm'))
       @$el.one 'click', (e) ->
@@ -378,15 +380,19 @@ module.exports = class InventoryModal extends ModalView
           unless itemModel and heroClass in itemModel.classes
             console.log 'Unequipping', itemModel.get('heroClass'), 'item', itemModel.get('name'), 'from slot due to class restrictions.'
             @unequipItemFromSlot @$el.find(".item-slot[data-slot='#{slot}']")
+            delete equipment[slot]
       for slot, item of restrictedGear
         equipped = equipment[slot]
         if equipped and equipped is gear[restrictedGear[slot]]
           console.log 'Unequipping restricted item', restrictedGear[slot], 'for', slot, 'before level', @options.levelID
           @unequipItemFromSlot @$el.find(".item-slot[data-slot='#{slot}']")
-      if heroClass is 'Warrior'
+          delete equipment[slot]
+      if (heroClass is 'Warrior' or
+          (heroClass is 'Ranger' and @options.levelID in ['swift-dagger', 'shrapnel']) or
+          (heroClass is 'Wizard' and @options.levelID in ['touch-of-death', 'bonemender']))
         # After they switch to a ranger or wizard, we stop being so finicky about gear.
         for slot, item of requiredGear
-          #continue if item is 'leather-tunic' and inWorldMap and @options.levelID is 'the-raised-sword'  # Don't tell them they need it until they need it in the level  # ... when we make it so that you can buy it
+          continue if item is 'tarnished-bronze-breastplate' and inWorldMap and @options.levelID is 'the-raised-sword'  # Don't tell them they need it until they need it in the level
           equipped = equipment[slot]
           continue if equipped and not (
             (item is 'crude-builders-hammer' and equipped in [gear['simple-sword'], gear['long-sword'], gear['sharpened-sword'], gear['roughedge']]) or
@@ -471,10 +477,11 @@ module.exports = class InventoryModal extends ModalView
 gear =
   'simple-boots': '53e237bf53457600003e3f05'
   'simple-sword': '53e218d853457600003e3ebe'
-  'leather-tunic': '53e22eac53457600003e3efc'
+  'tarnished-bronze-breastplate': '53e22eac53457600003e3efc'
   'leather-boots': '53e2384453457600003e3f07'
   'leather-belt': '5437002a7beba4a82024a97d'
   'programmaticon-i': '53e4108204c00d4607a89f78'
+  'programmaticon-ii': '546e25d99df4a17d0d449be1'
   'crude-glasses': '53e238df53457600003e3f0b'
   'crude-builders-hammer': '53f4e6e3d822c23505b74f42'
   'long-sword': '544d7d1f8494308424f564a3'
@@ -484,3 +491,9 @@ gear =
   'basic-flags': '545bacb41e649a4495f887da'
   'roughedge': '544d7d918494308424f564a7'
   'sharpened-sword': '544d7deb8494308424f564ab'
+  'crude-crossbow': '544d7ffd8494308424f564c3'
+  'crude-dagger': '544d952b8494308424f56517'
+  'weak-charge': '544d957d8494308424f5651f'
+  'enchanted-stick': '544d87188494308424f564f1'
+  'unholy-tome-i': '546374bc3839c6e02811d308'
+  'book-of-life-i': '546375653839c6e02811d30b'
diff --git a/app/views/kinds/RootView.coffee b/app/views/kinds/RootView.coffee
index 02c080596..48f24b938 100644
--- a/app/views/kinds/RootView.coffee
+++ b/app/views/kinds/RootView.coffee
@@ -32,6 +32,7 @@ module.exports = class RootView extends CocoView
 
   subscriptions:
     'achievements:new': 'handleNewAchievements'
+    'modal:open-modal-view': 'onOpenModalView'
 
   showNewAchievement: (achievement, earnedAchievement) ->
     return if achievement.get('collection') is 'level.sessions'
@@ -64,6 +65,10 @@ module.exports = class RootView extends CocoView
     window.tracker?.trackEvent 'Homepage', Action: anchorText, ['Google Analytics'] if @id is 'home-view' and anchorText
     @toggleModal e
 
+  onOpenModalView: (e) ->
+    return console.error "Couldn't find modalPath #{e.modalPath}" unless e.modalPath and ModalClass = require e.modalPath
+    @openModalView new ModalClass {}
+
   showLoading: ($el) ->
     $el ?= @$el.find('.main-content-area')
     super($el)
diff --git a/app/views/modal/AuthModal.coffee b/app/views/modal/AuthModal.coffee
index aa4c25cad..6027b8a86 100644
--- a/app/views/modal/AuthModal.coffee
+++ b/app/views/modal/AuthModal.coffee
@@ -90,6 +90,7 @@ module.exports = class AuthModal extends ModalView
     userObject.emails.generalNews.enabled = subscribe
     res = tv4.validateMultiple userObject, User.schema
     return forms.applyErrorsToForm(@$el, res.errors) unless res.valid
+    Backbone.Mediator.publish "auth:signed-up", {}
     window.tracker?.trackEvent 'Finished Signup'
     @enableModalInProgress(@$el)
     createUser userObject, null, window.nextLevelURL
@@ -125,7 +126,7 @@ module.exports = class AuthModal extends ModalView
   onClickGPlusLogin: ->
     step.done = false for step in @gplusAuthSteps
     handler = application.gplusHandler
-    
+
     @listenToOnce handler, 'logged-in', ->
       @gplusAuthSteps[0].done = true
       @renderGPlusAuthChecklist()
@@ -141,7 +142,7 @@ module.exports = class AuthModal extends ModalView
       @listenToOnce handler, 'logging-into-codecombat', ->
         @gplusAuthSteps[3].done = true
         @renderGPlusAuthChecklist()
-     
+
   renderGPlusAuthChecklist: ->
     template = require 'templates/modal/auth-modal-gplus-checklist'
     el = $(template({steps: @gplusAuthSteps}))
diff --git a/app/views/play/WorldMapView.coffee b/app/views/play/WorldMapView.coffee
index 0e0ed80b3..099fcfc19 100644
--- a/app/views/play/WorldMapView.coffee
+++ b/app/views/play/WorldMapView.coffee
@@ -40,7 +40,7 @@ module.exports = class WorldMapView extends RootView
     @levelStatusMap = {}
     @levelPlayCountMap = {}
     @sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', null, 0).model
-    
+
     # Temporary attempt to make sure all earned rewards are accounted for. Figure out a better solution...
     @earnedAchievements = new CocoCollection([], {url: '/db/earned_achievement', model:EarnedAchievement, project: ['earnedRewards']})
     @listenToOnce @earnedAchievements, 'sync', ->
@@ -56,7 +56,7 @@ module.exports = class WorldMapView extends RootView
               earned[group].push(reward)
               addedSomething = true
     @supermodel.loadCollection(@earnedAchievements, 'achievements')
-    
+
     @listenToOnce @sessions, 'sync', @onSessionsLoaded
     @getLevelPlayCounts()
     $(window).on 'resize', @onWindowResize
@@ -110,6 +110,7 @@ module.exports = class WorldMapView extends RootView
       window.levelUnlocksNotWorking = true if level.locked and level.id is @nextLevel  # Temporary
       level.locked = false if window.levelUnlocksNotWorking  # Temporary; also possible in HeroVictoryModal
       level.locked = false if @levelStatusMap[level.id] in ['started', 'complete']
+      level.locked = false if me.get('slug') is 'nick'
       level.disabled = false if @levelStatusMap[level.id] in ['started', 'complete']
       level.color = 'rgb(255, 80, 60)'
       if level.practice
@@ -136,7 +137,7 @@ module.exports = class WorldMapView extends RootView
       if levelID = @$el.find('.level.next').data('level-id')
         @$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show()
         pos = @$el.find('.level.next').offset()
-        @adjustLevelInfoPosition pageX: pos.left, pageY: pos.top + 250
+        @adjustLevelInfoPosition pageX: pos.left, pageY: pos.top
         @manuallyPositionedLevelInfoID = levelID
 
   afterInsert: ->
@@ -203,6 +204,7 @@ module.exports = class WorldMapView extends RootView
     @$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show()
     @adjustLevelInfoPosition e
     @endHighlight()
+    @manuallyPositionedLevelInfoID = false
 
   onMouseLeaveLevel: (e) ->
     return if application.isIPadApp
@@ -688,7 +690,9 @@ forest = [
       continue: 'peasant-protection'
     x: 58.54
     y: 66.73
-   }
+  }
+
+  # Warrior branch
   {
     name: 'Peasant Protection'
     type: 'hero'
@@ -709,6 +713,77 @@ forest = [
     x: 71.19
     y: 63.61
   }
+
+  # Ranger branch
+  {
+    name: 'Munchkin Harvest'
+    type: 'hero'
+    id: 'munchkin-harvest'
+    description: 'Join forces with a new hero: Amara Arrowhead.'
+    nextLevels:
+      continue: 'swift-dagger'
+    disabled: not me.isAdmin()
+    x: 64.37
+    y: 69.18
+  }
+  {
+    name: 'Swift Dagger'
+    type: 'hero'
+    id: 'swift-dagger'
+    description: 'Deal damage from a distance with your new hero.'
+    nextLevels:
+      continue: 'shrapnel'
+    disabled: not me.isAdmin()
+    x: 66
+    y: 75.61
+  }
+  {
+    name: 'Shrapnel'
+    type: 'hero'
+    id: 'shrapnel'
+    description: 'Explore the explosive arts.'
+    nextLevels:
+      continue: 'coinucopia'
+    disabled: not me.isAdmin()
+    x: 67
+    y: 81
+  }
+
+  # Wizard branch
+  {
+    name: 'Arcane Ally'
+    type: 'hero'
+    id: 'arcane-ally'
+    description: 'Stand your ground against large ogres with a new hero: Ms. Hushbaum.'
+    nextLevels:
+      continue: 'touch-of-death'
+    disabled: not me.isAdmin()
+    x: 64.37
+    y: 55.18
+  }
+  {
+    name: 'Touch of Death'
+    type: 'hero'
+    id: 'touch-of-death'
+    description: 'Learn your first spell to siphon life from your foes.'
+    nextLevels:
+      continue: 'bonemender'
+    disabled: not me.isAdmin()
+    x: 65
+    y: 48
+  }
+  {
+    name: 'Bonemender'
+    type: 'hero'
+    id: 'bonemender'
+    description: 'Cast regeneration on allied soldiers to withstand a siege.'
+    nextLevels:
+      continue: 'coinucopia'
+    disabled: not me.isAdmin()
+    x: 66
+    y: 40
+  }
+
   {
     name: 'Coinucopia'
     type: 'hero'
@@ -759,6 +834,17 @@ forest = [
     x: 77.54
     y: 25.94
   }
+  {
+    name: 'Siege of Stonehold'
+    type: 'hero'
+    id: 'siege-of-stonehold'
+    description: 'Unlock the desert world, if you are strong enough to win this epic battle!'
+    #nextLevels:
+    #  continue: ''
+    disabled: not me.isAdmin()
+    x: 77.54
+    y: 25.94
+  }
   {
     name: 'Multiplayer Treasure Grove'
     type: 'hero-ladder'
diff --git a/app/views/play/level/LevelDialogueView.coffee b/app/views/play/level/LevelDialogueView.coffee
index 3629ba687..f9cb4fa20 100644
--- a/app/views/play/level/LevelDialogueView.coffee
+++ b/app/views/play/level/LevelDialogueView.coffee
@@ -15,17 +15,22 @@ module.exports = class LevelDialogueView extends CocoView
 
   events:
     'click': 'onClick'
+    'click a': 'onClickLink'
 
   onClick: (e) ->
     Backbone.Mediator.publish 'tome:focus-editor', {}
 
-  onFrameChanged: (e) ->
-    @timeProgress = e.progress
-    @update()
+  onClickLink: (e) ->
+    route = $(e.target).attr('href')
+    if route and /item-store/.test route
+      PlayItemsModal = require 'views/play/modal/PlayItemsModal'
+      @openModalView new PlayItemsModal supermodel: @supermodal
+      e.stopPropagation()
 
   onSpriteDialogue: (e) ->
     return unless e.message
     @$el.addClass 'active speaking'
+    $('body').addClass('dialogue-view-active')
     @setMessage e.message, e.mood, e.responses
 
     window.tracker?.trackEvent 'Heard Sprite', {message: e.message, label: e.message}, ['Google Analytics']
@@ -35,6 +40,7 @@ module.exports = class LevelDialogueView extends CocoView
 
   onSpriteClearDialogue: ->
     @$el.removeClass 'active speaking'
+    $('body').removeClass('dialogue-view-active')
 
   setMessage: (message, mood, responses) ->
     message = marked message
diff --git a/app/views/play/level/LevelHUDView.coffee b/app/views/play/level/LevelHUDView.coffee
index 23a6c9617..e2f5fcb5c 100644
--- a/app/views/play/level/LevelHUDView.coffee
+++ b/app/views/play/level/LevelHUDView.coffee
@@ -56,6 +56,7 @@ module.exports = class LevelHUDView extends CocoView
   setThang: (thang, thangType) ->
     if not thang? and not @thang? then return
     if thang? and @thang? and thang.id is @thang.id then return
+    if thang? and @hidesHUD and thang.id isnt 'Hero Placeholder' then return  # Don't let them find the names of their opponents this way
     @thang = thang
     @thangType = thangType
     return unless @thang
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index 82ef103fa..3f0b759ae 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -78,6 +78,7 @@ module.exports = class PlayLevelView extends RootView
     'real-time-multiplayer:left-game': 'onRealTimeMultiplayerLeftGame'
     'real-time-multiplayer:manual-cast': 'onRealTimeMultiplayerCast'
     'ipad:memory-warning': 'onIPadMemoryWarning'
+    'store:item-purchased': 'onItemPurchased'
 
   events:
     'click #level-done-button': 'onDonePressed'
@@ -431,7 +432,6 @@ module.exports = class PlayLevelView extends RootView
       application.tracker?.trackEvent 'Saw Victory',
         level: @level.get('name')
         label: @level.get('name')
-        getDirectFirstGroup: me.getDirectFirstGroup()
       application.tracker?.trackTiming victoryTime, 'Level Victory Time', @levelID, @levelID, 100
 
   showVictory: ->
@@ -906,3 +906,13 @@ module.exports = class PlayLevelView extends RootView
 
   onIPadMemoryWarning: (e) ->
     @hasReceivedMemoryWarning = true
+
+  onItemPurchased: (e) ->
+    heroConfig = @session.get('heroConfig') ? {}
+    inventory = heroConfig.inventory ? {}
+    slot = e.item.getAllowedSlots()[0]
+    if slot and not inventory[slot]
+      # Open up the inventory modal so they can equip the new item
+      @setupManager?.destroy()
+      @setupManager = new LevelSetupManager({supermodel: @supermodel, levelID: @levelID, parent: @, session: @session, hadEverChosenHero: true})
+      @setupManager.open()
diff --git a/app/views/play/level/tome/CastButtonView.coffee b/app/views/play/level/tome/CastButtonView.coffee
index d425ad6db..5ed174743 100644
--- a/app/views/play/level/tome/CastButtonView.coffee
+++ b/app/views/play/level/tome/CastButtonView.coffee
@@ -148,6 +148,7 @@ module.exports = class CastButtonView extends CocoView
   onLeftRealTimeMultiplayerGame: (e) ->
     @inRealTimeMultiplayerSession = false
 
+  # https://mixpanel.com/report/227350/segmentation/#action:segment,arb_event:'Saw%20Victory',bool_op:or,chart_type:bar,from_date:-9,segfilter:!((filter:(operand:!('Ogre%20Encampment'),operator:%3D%3D),property:level,selected_property_type:string,type:string),(property:castButtonTextGroup,selected_property_type:number,type:number)),segment_type:number,to_date:0,type:unique,unit:day
   initButtonTextABTest: ->
     return if me.isAdmin()
     return unless $.i18n.lng() is 'en-US'
diff --git a/app/views/play/level/tome/SpellPaletteEntryView.coffee b/app/views/play/level/tome/SpellPaletteEntryView.coffee
index d6870656d..a66d70131 100644
--- a/app/views/play/level/tome/SpellPaletteEntryView.coffee
+++ b/app/views/play/level/tome/SpellPaletteEntryView.coffee
@@ -37,10 +37,11 @@ module.exports = class SpellPaletteEntryView extends CocoView
   afterRender: ->
     super()
     @$el.addClass(@doc.type)
+    placement = -> if $('body').hasClass('dialogue-view-active') then 'top' else 'left'
     @$el.popover(
       animation: false
       html: true
-      placement: 'left'
+      placement: placement
       trigger: 'manual'  # Hover, until they click, which will then pin it until unclick.
       content: @docFormatter.formatPopover()
       container: 'body'
diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee
index 16a2dd338..45fe0eb2e 100644
--- a/app/views/play/level/tome/SpellView.coffee
+++ b/app/views/play/level/tome/SpellView.coffee
@@ -131,13 +131,12 @@ module.exports = class SpellView extends CocoView
     addCommand
       name: 'toggle-playing'
       bindKey: {win: 'Ctrl-P', mac: 'Command-P|Ctrl-P'}
+      readOnly: true
       exec: -> Backbone.Mediator.publish 'level:toggle-playing', {}
     addCommand
       name: 'end-current-script'
       bindKey: {win: 'Shift-Space', mac: 'Shift-Space'}
-      # passEvent: true  # https://github.com/ajaxorg/ace/blob/master/lib/ace/keyboard/keybinding.js#L114
-      # No easy way to selectively cancel shift+space, since we don't get access to the event.
-      # Maybe we could temporarily set ourselves to read-only if we somehow know that a script is active?
+      readOnly: true
       exec: =>
         if @scriptRunning
           Backbone.Mediator.publish 'level:shift-space-pressed', {}
@@ -147,34 +146,44 @@ module.exports = class SpellView extends CocoView
     addCommand
       name: 'end-all-scripts'
       bindKey: {win: 'Escape', mac: 'Escape'}
-      exec: -> Backbone.Mediator.publish 'level:escape-pressed', {}
+      readOnly: true
+      exec: ->
+        console.log 'esc pressed'
+        Backbone.Mediator.publish 'level:escape-pressed', {}
     addCommand
       name: 'toggle-grid'
       bindKey: {win: 'Ctrl-G', mac: 'Command-G|Ctrl-G'}
+      readOnly: true
       exec: -> Backbone.Mediator.publish 'level:toggle-grid', {}
     addCommand
       name: 'toggle-debug'
       bindKey: {win: 'Ctrl-\\', mac: 'Command-\\|Ctrl-\\'}
+      readOnly: true
       exec: -> Backbone.Mediator.publish 'level:toggle-debug', {}
     addCommand
       name: 'toggle-pathfinding'
       bindKey: {win: 'Ctrl-O', mac: 'Command-O|Ctrl-O'}
+      readOnly: true
       exec: -> Backbone.Mediator.publish 'level:toggle-pathfinding', {}
     addCommand
       name: 'level-scrub-forward'
       bindKey: {win: 'Ctrl-]', mac: 'Command-]|Ctrl-]'}
+      readOnly: true
       exec: -> Backbone.Mediator.publish 'level:scrub-forward', {}
     addCommand
       name: 'level-scrub-back'
       bindKey: {win: 'Ctrl-[', mac: 'Command-[|Ctrl-]'}
+      readOnly: true
       exec: -> Backbone.Mediator.publish 'level:scrub-back', {}
     addCommand
       name: 'spell-step-forward'
       bindKey: {win: 'Ctrl-Alt-]', mac: 'Command-Alt-]|Ctrl-Alt-]'}
+      readOnly: true
       exec: -> Backbone.Mediator.publish 'tome:spell-step-forward', {}
     addCommand
       name: 'spell-step-backward'
       bindKey: {win: 'Ctrl-Alt-[', mac: 'Command-Alt-[|Ctrl-Alt-]'}
+      readOnly: true
       exec: -> Backbone.Mediator.publish 'tome:spell-step-backward', {}
     addCommand
       name: 'spell-beautify'
@@ -207,6 +216,37 @@ module.exports = class SpellView extends CocoView
       name: 'disable-spaces'
       bindKey: 'Space'
       exec: => @ace.execCommand 'insertstring', ' ' unless LevelOptions[@options.level.get('slug')]?.disableSpaces
+    addCommand
+      name: 'throttle-backspaces'
+      bindKey: 'Backspace'
+      exec: =>
+        # Throttle the backspace speed
+        # Slow to 500ms when whitespace at beginning of line is first encountered
+        # Slow to 100ms for remaining whitespace at beginning of line
+        # Rough testing showed backspaces happen at 150ms when tapping.
+        # Backspace speed varies by system when holding, 30ms on fastest Macbook setting.
+        unless CampaignOptions?.getOption?(@options?.level?.get?('slug'), 'backspaceThrottle')
+          @ace.remove "left"
+          return
+        
+        nowDate = Date.now()
+        if @aceSession.selection.isEmpty()
+          cursor = @ace.getCursorPosition()
+          line = @aceDoc.getLine(cursor.row)
+          if /^\s*$/.test line.substring(0, cursor.column)
+            @backspaceThrottleMs ?= 500
+            # console.log "SpellView @backspaceThrottleMs=#{@backspaceThrottleMs}"
+            # console.log 'SpellView lastBackspace diff', nowDate - @lastBackspace if @lastBackspace?
+            if not @lastBackspace? or nowDate - @lastBackspace > @backspaceThrottleMs
+              @backspaceThrottleMs = 100
+              @lastBackspace = nowDate
+              @ace.remove "left"
+            return
+        @backspaceThrottleMs = null
+        @lastBackspace = nowDate
+        @ace.remove "left"
+          
+
 
   fillACE: ->
     @ace.setValue @spell.source
@@ -253,8 +293,15 @@ module.exports = class SpellView extends CocoView
           return true if doc.owner is owner
           return (owner is 'this' or owner is 'more') and (not doc.owner? or doc.owner is 'this')
         if doc?.snippets?[e.language]
+          content = doc.snippets[e.language].code
+          if /loop/.test(content) and LevelOptions[@options.level.get('slug')]?.moveRightLoopSnippet
+            # Replace default loop snippet with an embedded moveRight()
+            content = switch e.language
+              when 'python' then 'loop:\n    self.moveRight()\n    ${1:}'
+              when 'javascript' then 'loop {\n    this.moveRight();\n    ${1:}\n}'
+              else content
           entry =
-            content: doc.snippets[e.language].code
+            content: content
             meta: 'press tab'
             name: doc.name
             tabTrigger: doc.snippets[e.language].tab
@@ -810,14 +857,16 @@ module.exports = class SpellView extends CocoView
   onDisableControls: (e) -> @toggleControls e, false
   onEnableControls: (e) -> @toggleControls e, @writable
   toggleControls: (e, enabled) ->
+    return if @destroyed
     return if e?.controls and not ('editor' in e.controls)
     return if enabled is @controlsEnabled
     @controlsEnabled = enabled and @writable
     disabled = not enabled
-    $('body').focus() if disabled and $(document.activeElement).is('.ace_text-input')
+    wasFocused = @ace.isFocused()
     @ace.setReadOnly disabled
     @ace[if disabled then 'setStyle' else 'unsetStyle'] 'disabled'
     @toggleBackground()
+    $('body').focus() if disabled and wasFocused
 
   toggleBackground: =>
     # TODO: make the background an actual background and do the CSS trick
diff --git a/app/views/play/modal/PlayItemsModal.coffee b/app/views/play/modal/PlayItemsModal.coffee
index d0e77f8d0..fd4b9c6a5 100644
--- a/app/views/play/modal/PlayItemsModal.coffee
+++ b/app/views/play/modal/PlayItemsModal.coffee
@@ -90,7 +90,7 @@ module.exports = class PlayItemsModal extends ModalView
       model.silhouetted = not model.owned and model.isSilhouettedItem()
       model.level = model.levelRequiredForItem() if model.get('tier')?
       model.unequippable = not ('Warrior' in model.getAllowedHeroClasses())  # Temp: while there are no wizards/rangers
-      model.comingSoon = not model.getFrontFacingStats().props.length and not _.size model.getFrontFacingStats().stats and not model.owned  # Temp: while there are placeholder items
+      model.comingSoon = not model.getFrontFacingStats().props.length and not _.size(model.getFrontFacingStats().stats) and not model.owned  # Temp: while there are placeholder items
       @idToItem[model.id] = model
 
     if needMore
@@ -112,6 +112,7 @@ module.exports = class PlayItemsModal extends ModalView
     @$el.find('.nano:visible').nanoScroller({alwaysVisible: true})
     @itemDetailsView = new ItemDetailsView()
     @insertSubView(@itemDetailsView)
+    @$el.find("a[href='#item-category-armor']").click()  # Start on armor tab, if it's there.
 
   onHidden: ->
     super()
@@ -156,6 +157,8 @@ module.exports = class PlayItemsModal extends ModalView
       #- ...then rerender key bits
       @renderSelectors(".item[data-item-id='#{item.id}']", "#gems-count")
       @itemDetailsView.render()
+
+      Backbone.Mediator.publish 'store:item-purchased', item: item, itemSlug: item.get('slug')
     else
       button.addClass('confirm').text($.i18n.t('play.confirm'))
       @$el.one 'click', (e) ->
diff --git a/server/achievements/EarnedAchievement.coffee b/server/achievements/EarnedAchievement.coffee
index bffe0be69..5ee686989 100644
--- a/server/achievements/EarnedAchievement.coffee
+++ b/server/achievements/EarnedAchievement.coffee
@@ -1,6 +1,7 @@
 mongoose = require 'mongoose'
 jsonschema = require '../../app/schemas/models/earned_achievement'
 util = require '../../app/lib/utils'
+log = require 'winston'
 
 EarnedAchievementSchema = new mongoose.Schema({
   notified:
@@ -68,6 +69,6 @@ EarnedAchievementSchema.statics.createForAchievement = (achievement, doc, origin
     (new EarnedAchievement(earned)).save (err, doc) ->
       return log.error err if err?
       earnedPoints = worth
-      wrapUp(doc)  
+      wrapUp(doc)
 
 module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema)
diff --git a/server/achievements/earned_achievement_handler.coffee b/server/achievements/earned_achievement_handler.coffee
index f49ff545a..cfb2a1f62 100644
--- a/server/achievements/earned_achievement_handler.coffee
+++ b/server/achievements/earned_achievement_handler.coffee
@@ -67,7 +67,7 @@ class EarnedAchievementHandler extends Handler
       if achievement.get('proportionalTo')
         return @sendBadInputError(res, 'Cannot currently do this to repeatable docs...')
       EarnedAchievement.createForAchievement(achievement, trigger, null, (earnedAchievementDoc) =>
-        @sendSuccess(res, earnedAchievementDoc.toObject())
+        @sendCreated(res, earnedAchievementDoc.toObject())
       )
         
     )
diff --git a/server/payments/payment_handler.coffee b/server/payments/payment_handler.coffee
index d4c60abeb..ae2393d67 100644
--- a/server/payments/payment_handler.coffee
+++ b/server/payments/payment_handler.coffee
@@ -88,10 +88,13 @@ PaymentHandler = class PaymentHandler extends Handler
         
       #- Check existence
       transactionID = transaction.transaction_id
-      criteria = { recipient: req.user._id, 'ios.transactionID': transactionID }
+      criteria = { 'ios.transactionID': transactionID }
       Payment.findOne(criteria).exec((err, payment) =>
         
         if payment
+          unless payment.get('recipient').equals(req.user._id)
+            return @sendForbiddenError(res)
+
           @recalculateGemsFor(req.user, (err) =>
             return @sendDatabaseError(res, err) if err
             @sendSuccess(res, @formatEntity(req, payment))
diff --git a/test/server/functional/achievement.spec.coffee b/test/server/functional/achievement.spec.coffee
index d1390a65c..1afac63ce 100644
--- a/test/server/functional/achievement.spec.coffee
+++ b/test/server/functional/achievement.spec.coffee
@@ -122,134 +122,133 @@ describe 'Achievement', ->
 
 # TODO: Took level achievements out of this auto achievement business, so fix these tests
       
-#describe 'Achieving Achievements', ->
-#  it 'wait for achievements to be loaded', (done) ->
-#    Achievement.loadAchievements (achievements) ->
-#      expect(Object.keys(achievements).length).toBe(1)
-#
-#      loadedAchievements = Achievement.getLoadedAchievements()
-#      expect(Object.keys(loadedAchievements).length).toBe(1)
-#      done()
-#
-#  it 'saving an object that should trigger an unlockable achievement', (done) ->
-#    unittest.getNormalJoe (joe) ->
-#      session = new LevelSession
-#        permissions: simplePermissions
-#        creator: joe._id
-#        level: original: 'dungeon-arena'
-#      session.save (err, doc) ->
-#        expect(err).toBeNull()
-#        expect(doc).toBeDefined()
-#        expect(doc.creator).toBe(session.creator)
-#        done()
-#
-#  it 'verify that an unlockable achievement has been earned', (done) ->
-#    unittest.getNormalJoe (joe) ->
-#      EarnedAchievement.find {}, (err, docs) ->
-#        expect(err).toBeNull()
-#        expect(docs.length).toBe(1)
-#        achievement = docs[0]
-#        expect(achievement).toBeDefined()
-#
-#        expect(achievement.get 'achievement').toBe unlockable._id
-#        expect(achievement.get 'user').toBe joe._id.toHexString()
-#        expect(achievement.get 'notified').toBeFalsy()
-#        expect(achievement.get 'earnedPoints').toBe unlockable.worth
-#        expect(achievement.get 'achievedAmount').toBeUndefined()
-#        expect(achievement.get 'previouslyAchievedAmount').toBeUndefined()
-#        done()
-#
-#  it 'saving an object that should trigger a repeatable achievement', (done) ->
-#    unittest.getNormalJoe (joe) ->
-#      expect(joe.get 'simulatedBy').toBeFalsy()
-#      joe.set('simulatedBy', 2)
-#      joe.save (err, doc) ->
-#        expect(err).toBeNull()
-#        done()
-#
-#  it 'verify that a repeatable achievement has been earned', (done) ->
-#    unittest.getNormalJoe (joe) ->
-#      EarnedAchievement.find {achievementName: repeatable.name}, (err, docs) ->
-#        expect(err).toBeNull()
-#        expect(docs.length).toBe(1)
-#        achievement = docs[0]
-#
-#        expect(achievement.get 'achievement').toBe repeatable._id
-#        expect(achievement.get 'user').toBe joe._id.toHexString()
-#        expect(achievement.get 'notified').toBeFalsy()
-#        expect(achievement.get 'earnedPoints').toBe 2 * repeatable.worth
-#        expect(achievement.get 'achievedAmount').toBe 2
-#        expect(achievement.get 'previouslyAchievedAmount').toBeFalsy()
-#        done()
-#
-#
-#  it 'verify that the repeatable achievement with complex exp has been earned', (done) ->
-#    unittest.getNormalJoe (joe) ->
-#      EarnedAchievement.find {achievementName: diminishing.name}, (err, docs) ->
-#        expect(err).toBeNull()
-#        expect(docs.length).toBe 1
-#        achievement = docs[0]
-#
-#        expect(achievement.get 'achievedAmount').toBe 2
-#        expect(achievement.get 'earnedPoints').toBe (Math.log(.5 * (2 + .5)) + 1) * diminishing.worth
-#
-#        done()
+describe 'Level Session Achievement', ->
+  it 'does not generate earned achievements automatically, they need to be created manually', (done) ->
+    unittest.getNormalJoe (joe) ->
+      session = new LevelSession
+        permissions: simplePermissions
+        creator: joe._id
+        level: original: 'dungeon-arena'
+      session.save (err, session) ->
+        expect(err).toBeNull()
+        expect(session).toBeDefined()
+        expect(session.creator).toBe(session.creator)
+          
+        EarnedAchievement.find {}, (err, earnedAchievements) ->
+          expect(err).toBeNull()
+          expect(earnedAchievements.length).toBe(0)
+          
+          json = {achievement: unlockable._id, triggeredBy: session._id, collection: 'level.sessions'}
+          request.post {uri: getURL('/db/earned_achievement'), json: json}, (err, res, body) ->
+            expect(res.statusCode).toBe(201)
+            expect(body.achievement).toBe unlockable._id+''
+            expect(body.user).toBe joe._id.toHexString()
+            expect(body.notified).toBeFalsy()
+            expect(body.earnedPoints).toBe unlockable.worth
+            expect(body.achievedAmount).toBeUndefined()
+            expect(body.previouslyAchievedAmount).toBeUndefined()
+            done()
 
-#describe 'Recalculate Achievements', ->
-#  EarnedAchievementHandler = require '../../../server/achievements/earned_achievement_handler'
-#
-#  it 'remove earned achievements', (done) ->
-#    clearModels [EarnedAchievement], (err) ->
-#      expect(err).toBeNull()
-#      EarnedAchievement.find {}, (err, earned) ->
-#        expect(earned.length).toBe 0
-#
-#        User.update {}, {$set: {points: 0}}, {multi:true}, (err) ->
-#          expect(err).toBeNull()
-#          done()
-#
-#  it 'can not be accessed by regular users', (done) ->
-#    loginJoe -> request.post {uri:getURL '/admin/earned_achievement/recalculate'}, (err, res, body) ->
-#      expect(res.statusCode).toBe 403
-#      done()
-#
-#  it 'can recalculate a selection of achievements', (done) ->
-#    loginAdmin ->
-#      EarnedAchievementHandler.constructor.recalculate ['dungeon-arena-started'], ->
-#        EarnedAchievement.find {}, (err, earnedAchievements) ->
-#          expect(earnedAchievements.length).toBe 1
-#
-#          # Recalculate again, doesn't change a thing
-#          EarnedAchievementHandler.constructor.recalculate ['dungeon-arena-started'], ->
-#            EarnedAchievement.find {}, (err, earnedAchievements) ->
-#              expect(earnedAchievements.length).toBe 1
-#
-#              unittest.getNormalJoe (joe) ->
-#                User.findById joe.get('id'), (err, guy) ->
-#                  expect(err).toBeNull()
-#                  expect(guy.get 'points').toBe unlockable.worth
-#                  done()
-#
-#  it 'can recalculate all achievements', (done) ->
-#    loginAdmin ->
-#      Achievement.count {}, (err, count) ->
-#        expect(count).toBe 3
-#        EarnedAchievementHandler.constructor.recalculate ->
-#          EarnedAchievement.find {}, (err, earnedAchievements) ->
-#            expect(earnedAchievements.length).toBe 3
-#            unittest.getNormalJoe (joe) ->
-#              User.findById joe.get('id'), (err, guy) ->
-#                expect(err).toBeNull()
-#                expect(guy.get 'points').toBe unlockable.worth + 2 * repeatable.worth + (Math.log(.5 * (2 + .5)) + 1) * diminishing.worth
-#                done()
-#
-#  it 'cleaning up test: deleting all Achievements and related', (done) ->
-#    clearModels [Achievement, EarnedAchievement, LevelSession], (err) ->
-#      expect(err).toBeNull()
-#
-#      # reset achievements in memory as well
-#      Achievement.resetAchievements()
-#      loadedAchievements = Achievement.getLoadedAchievements()
-#      expect(Object.keys(loadedAchievements).length).toBe(0)
-#
-#      done()
+            
+describe 'Achieving Achievements', ->
+  it 'wait for achievements to be loaded', (done) ->
+    Achievement.loadAchievements (achievements) ->
+      expect(Object.keys(achievements).length).toBe(1)
+
+      loadedAchievements = Achievement.getLoadedAchievements()
+      expect(Object.keys(loadedAchievements).length).toBe(1)
+      done()
+
+  it 'saving an object that should trigger a repeatable achievement', (done) ->
+    unittest.getNormalJoe (joe) ->
+      expect(joe.get 'simulatedBy').toBeFalsy()
+      joe.set('simulatedBy', 2)
+      joe.save (err, doc) ->
+        expect(err).toBeNull()
+        done()
+
+  it 'verify that a repeatable achievement has been earned', (done) ->
+    unittest.getNormalJoe (joe) ->
+      EarnedAchievement.find {achievementName: repeatable.name}, (err, docs) ->
+        expect(err).toBeNull()
+        expect(docs.length).toBe(1)
+        achievement = docs[0]
+
+        expect(achievement.get 'achievement').toBe repeatable._id
+        expect(achievement.get 'user').toBe joe._id.toHexString()
+        expect(achievement.get 'notified').toBeFalsy()
+        expect(achievement.get 'earnedPoints').toBe 2 * repeatable.worth
+        expect(achievement.get 'achievedAmount').toBe 2
+        expect(achievement.get 'previouslyAchievedAmount').toBeFalsy()
+        done()
+
+  it 'verify that the repeatable achievement with complex exp has been earned', (done) ->
+    unittest.getNormalJoe (joe) ->
+      EarnedAchievement.find {achievementName: diminishing.name}, (err, docs) ->
+        expect(err).toBeNull()
+        expect(docs.length).toBe 1
+        achievement = docs[0]
+
+        expect(achievement.get 'achievedAmount').toBe 2
+        expect(achievement.get 'earnedPoints').toBe (Math.log(.5 * (2 + .5)) + 1) * diminishing.worth
+
+        done()
+
+describe 'Recalculate Achievements', ->
+  EarnedAchievementHandler = require '../../../server/achievements/earned_achievement_handler'
+
+  it 'remove earned achievements', (done) ->
+    clearModels [EarnedAchievement], (err) ->
+      expect(err).toBeNull()
+      EarnedAchievement.find {}, (err, earned) ->
+        expect(earned.length).toBe 0
+
+        User.update {}, {$set: {points: 0}}, {multi:true}, (err) ->
+          expect(err).toBeNull()
+          done()
+
+  it 'can not be accessed by regular users', (done) ->
+    loginJoe -> request.post {uri:getURL '/admin/earned_achievement/recalculate'}, (err, res, body) ->
+      expect(res.statusCode).toBe 403
+      done()
+
+  it 'can recalculate a selection of achievements', (done) ->
+    loginAdmin ->
+      EarnedAchievementHandler.constructor.recalculate ['dungeon-arena-started'], ->
+        EarnedAchievement.find {}, (err, earnedAchievements) ->
+          expect(earnedAchievements.length).toBe 1
+
+          # Recalculate again, doesn't change a thing
+          EarnedAchievementHandler.constructor.recalculate ['dungeon-arena-started'], ->
+            EarnedAchievement.find {}, (err, earnedAchievements) ->
+              expect(earnedAchievements.length).toBe 1
+
+              unittest.getNormalJoe (joe) ->
+                User.findById joe.get('id'), (err, guy) ->
+                  expect(err).toBeNull()
+                  expect(guy.get 'points').toBe unlockable.worth
+                  done()
+
+  it 'can recalculate all achievements', (done) ->
+    loginAdmin ->
+      Achievement.count {}, (err, count) ->
+        expect(count).toBe 3
+        EarnedAchievementHandler.constructor.recalculate ->
+          EarnedAchievement.find {}, (err, earnedAchievements) ->
+            expect(earnedAchievements.length).toBe 3
+            unittest.getNormalJoe (joe) ->
+              User.findById joe.get('id'), (err, guy) ->
+                expect(err).toBeNull()
+                expect(guy.get 'points').toBe unlockable.worth + 2 * repeatable.worth + (Math.log(.5 * (2 + .5)) + 1) * diminishing.worth
+                done()
+
+  it 'cleaning up test: deleting all Achievements and related', (done) ->
+    clearModels [Achievement, EarnedAchievement, LevelSession], (err) ->
+      expect(err).toBeNull()
+
+      # reset achievements in memory as well
+      Achievement.resetAchievements()
+      loadedAchievements = Achievement.getLoadedAchievements()
+      expect(Object.keys(loadedAchievements).length).toBe(0)
+
+      done()
diff --git a/test/server/functional/payment.spec.coffee b/test/server/functional/payment.spec.coffee
index a779b8778..ec316a621 100644
--- a/test/server/functional/payment.spec.coffee
+++ b/test/server/functional/payment.spec.coffee
@@ -52,6 +52,12 @@ describe '/db/payment', ->
             done()
           )
 
+    it 'prevents other users from reusing payment receipts', (done) ->
+      loginSam ->
+        request.post {uri: paymentURL, json: firstApplePayment}, (err, res, body) ->
+          expect(res.statusCode).toBe 403
+          done()
+
     it 'processes only the transactionID that is given', (done) ->
       loginJoe ->
         request.post {uri: paymentURL, json: secondApplePayment}, (err, res, body) ->