diff --git a/app/assets/javascripts/workers/aether_worker.js b/app/assets/javascripts/workers/aether_worker.js
index 810537599..4d2db81d4 100644
--- a/app/assets/javascripts/workers/aether_worker.js
+++ b/app/assets/javascripts/workers/aether_worker.js
@@ -1,7 +1,18 @@
 var window = self;
 var Global = self;
 
-importScripts("/javascripts/lodash.js", "/javascripts/aether.js", "/javascripts/esper.js");
+importScripts("/javascripts/lodash.js", "/javascripts/aether.js");
+
+try {
+  //Detect very modern javascript support.
+  (0,eval("'use strict'; let test = (class Test { *gen(a=7) { yield yield * () => WeakMap; } });"));
+  console.log("Modern javascript detected, aw yeah!");
+  self.importScripts('/javascripts/esper.modern.js');  
+} catch (e) {
+  console.log("Legacy javascript detected, falling back...", e.message);
+  self.importScripts('/javascripts/esper.js');  
+}
+
 //console.log("Aether Tome worker has finished importing scripts.");
 var aethers = {};
 var languagesImported = {};
diff --git a/app/assets/javascripts/workers/worker_world.js b/app/assets/javascripts/workers/worker_world.js
index 26217cb16..dcfc5b13b 100644
--- a/app/assets/javascripts/workers/worker_world.js
+++ b/app/assets/javascripts/workers/worker_world.js
@@ -63,7 +63,18 @@ var console = {
 console.error = console.warn = console.info = console.debug = console.log;
 self.console = console;
 
-self.importScripts('/javascripts/lodash.js', '/javascripts/world.js', '/javascripts/aether.js', '/javascripts/esper.js');
+self.importScripts('/javascripts/lodash.js', '/javascripts/world.js', '/javascripts/aether.js');
+try {
+  //Detect very modern javascript support.
+  (0,eval("'use strict'; let test = (class Test { *gen(a=7) { yield yield * () => WeakMap; } });"));
+  console.log("Modern javascript detected, aw yeah!");
+  self.importScripts('/javascripts/esper.modern.js');  
+} catch (e) {
+  console.log("Legacy javascript detected, falling back...", e.message);
+  self.importScripts('/javascripts/esper.js');  
+}
+
+
 var myImportScripts = importScripts;
 
 var languagesImported = {};
@@ -402,6 +413,17 @@ self.serializeFramesSoFar = function serializeFramesSoFar() {
   self.world.framesSerializedSoFar = self.world.frames.length;
 };
 
+function trySerialize() {
+  try {
+    var serialized = self.world.serialize();
+  }
+  catch(error) {
+    console.log("World serialization error:", error.toString() + "\n" + error.stack || error.stackTrace);
+    return false;
+  }
+  return serialized;
+}
+
 self.onWorldLoaded = function onWorldLoaded() {
   if(self.world.framesSerializedSoFar == self.world.frames.length) return;
   if(self.world.ended)
@@ -419,12 +441,9 @@ self.onWorldLoaded = function onWorldLoaded() {
     return console.log('Headless simulation completed in ' + diff + 'ms.');
 
   var worldEnded = self.world.ended;
+  var serialized;
   var transferableSupported = self.transferableSupported();
-  try {
-    var serialized = self.world.serialize();
-  }
-  catch(error) {
-    console.log("World serialization error:", error.toString() + "\n" + error.stack || error.stackTrace);
+  if ( !( serialized = trySerialize()) ) {
     self.destroyWorld();
     return;
   }
diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee
index af7e2b65b..266f4bc1e 100644
--- a/app/lib/world/world.coffee
+++ b/app/lib/world/world.coffee
@@ -162,11 +162,25 @@ module.exports = class World
 
   shouldContinueLoading: (t1, loadProgressCallback, skipDeferredLoading, continueLaterFn) ->
     t2 = now()
+    chunkSize = @frames.length - @framesSerializedSoFar
+    simedTime = @frames.length / @frameRate
+
+    chunkTime = switch
+      when simedTime > 15 then 7
+      when simedTime > 10 then 5
+      when simedTime > 5 then 3
+      when simedTime > 2 then 1
+      else 0.5
+
+    bailoutTime = Math.max(2000*chunkTime, 10000)
+
+    dt = t2 - t1
+
     if @realTime
       shouldUpdateProgress = @shouldUpdateRealTimePlayback t2
       shouldDelayRealTimeSimulation = not shouldUpdateProgress and @shouldDelayRealTimeSimulation t2
     else
-      shouldUpdateProgress = t2 - t1 > PROGRESS_UPDATE_INTERVAL# and (@frames.length - @framesSerializedSoFar >= @frameRate or t2 - t1 > 1000)
+      shouldUpdateProgress = (dt > PROGRESS_UPDATE_INTERVAL and (chunkSize / @frameRate >= chunkTime) or dt > bailoutTime)
       shouldDelayRealTimeSimulation = false
     return true unless shouldUpdateProgress or shouldDelayRealTimeSimulation
     # Stop loading frames for now; continue in a moment.
diff --git a/app/templates/courses/teacher-courses-view.jade b/app/templates/courses/teacher-courses-view.jade
index 9707e0170..42634b962 100644
--- a/app/templates/courses/teacher-courses-view.jade
+++ b/app/templates/courses/teacher-courses-view.jade
@@ -57,6 +57,8 @@ block content
                   select.level-select.form-control
                     if view.campaigns.loaded
                       each level, levelIndex in view.campaigns.get(course.get('campaignID')).getLevels().models
+                        if level.get('type') === 'hero-practice'
+                          - continue;
                         option(value=level.get('slug'))
                           span
                             = levelIndex + 1
diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee
index 4cff9c845..ed78d3b8f 100644
--- a/app/views/editor/campaign/CampaignEditorView.coffee
+++ b/app/views/editor/campaign/CampaignEditorView.coffee
@@ -239,6 +239,14 @@ module.exports = class CampaignEditorView extends RootView
     @campaign.set key, value for key, value of @treema.data
     @campaignView.setCampaign(@campaign)
 
+  onTreemaSelectionChanged: (e, node) =>
+    return unless node[0]?.data?.original?
+    elem = @$("div[data-level-original='#{node[0].data.original}']")
+    elem.toggle('pulsate')
+    setTimeout ()->
+      elem.toggle('pulsate')
+    , 1000
+
   onTreemaDoubleClicked: (e, node) =>
     path = node.getPath()
     return unless _.string.startsWith path, '/levels/'
diff --git a/app/views/editor/level/settings/SettingsTabView.coffee b/app/views/editor/level/settings/SettingsTabView.coffee
index 842978d87..f042b5ab1 100644
--- a/app/views/editor/level/settings/SettingsTabView.coffee
+++ b/app/views/editor/level/settings/SettingsTabView.coffee
@@ -15,7 +15,7 @@ module.exports = class SettingsTabView extends CocoView
   editableSettings: [
     'name', 'description', 'documentation', 'nextLevel', 'background', 'victory', 'i18n', 'icon', 'goals',
     'type', 'terrain', 'showsGuide', 'banner', 'employerDescription', 'loadingTip', 'requiresSubscription',
-    'helpVideos', 'replayable', 'scoreTypes', 'concepts', 'picoCTFProblem'
+    'helpVideos', 'replayable', 'scoreTypes', 'concepts', 'picoCTFProblem', 'practiceThresholdMinutes'
   ]
 
   subscriptions:
diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee
index 9cddc2178..cd7cc1c18 100644
--- a/app/views/play/level/tome/SpellView.coffee
+++ b/app/views/play/level/tome/SpellView.coffee
@@ -316,7 +316,7 @@ module.exports = class SpellView extends CocoView
 
             xstart = startOfRow(row)
             if language is 'python'
-              requiredIndent = new RegExp '^' + new Array(xstart / 4 + 1).join('(    |\t)') + '(    |\t)+(\\S|\\s*$)'
+              requiredIndent = new RegExp '^' + new Array(Math.floor(xstart / 4 + 1)).join('(    |\t)') + '(    |\t)+(\\S|\\s*$)'
               for crow in [docRange.start.row+1..docRange.end.row]
                 unless requiredIndent.test lines[crow]
                   docRange.end.row = crow - 1
diff --git a/app/views/teachers/TeachersContactModal.coffee b/app/views/teachers/TeachersContactModal.coffee
index ab8fbf06b..52d4488c7 100644
--- a/app/views/teachers/TeachersContactModal.coffee
+++ b/app/views/teachers/TeachersContactModal.coffee
@@ -62,7 +62,7 @@ module.exports = class TeachersContactModal extends ModalView
     return unless _.isEmpty(formErrors)
 
     @state.set('sendingState', 'sending')
-    data = _.extend({ country: me.get('country'), recipientID: 'schools@codecombat.com' }, formValues)
+    data = _.extend({ country: me.get('country') }, formValues)
     contact.send({
       data
       context: @
diff --git a/config.coffee b/config.coffee
index 6690d8e2e..75e614786 100644
--- a/config.coffee
+++ b/config.coffee
@@ -207,6 +207,7 @@ exports.config =
       copyTo:
         'lib/ace': ['node_modules/ace-builds/src-min-noconflict/*']
         'fonts': ['bower_components/openSansCondensed/*', 'bower_components/openSans/*']
+        'javascripts': ['bower_components/esper.js/esper.modern.js']
     autoReload:
       delay: 1000
 
diff --git a/server/handlers/level_handler.coffee b/server/handlers/level_handler.coffee
index 5fd573dd1..a44a4e34b 100644
--- a/server/handlers/level_handler.coffee
+++ b/server/handlers/level_handler.coffee
@@ -67,6 +67,7 @@ LevelHandler = class LevelHandler extends Handler
     'scoreTypes'
     'concepts'
     'picoCTFProblem'
+    'practiceThresholdMinutes'
   ]
 
   postEditableProperties: ['name']
diff --git a/server/lib/closeIO.coffee b/server/lib/closeIO.coffee
index c72c4b8c4..8de1f78fa 100644
--- a/server/lib/closeIO.coffee
+++ b/server/lib/closeIO.coffee
@@ -66,33 +66,81 @@ module.exports =
           return done("Unexpected activities format: " + body) unless activities.data?
           for activity in activities.data when activity._type is 'Email'
             if /@codecombat\.(?:com)|(?:nl)/ig.test(activity.sender) and not activity.sender?.indexOf(config.mail.username) >= 0
-              return done(null, activity.sender, lead.id)
+              return done(null, activity.sender, activity.user_id, lead.id)
           return done(null, config.mail.supportSchools, lead.id)
     catch error
       log.error("closeIO.getSalesContactEmail Error for #{email}: #{JSON.stringify(error)}")
       return done(error)
 
-  sendMail: (fromAddress, subject, content, done) ->
+  sendMail: (fromAddress, subject, content, salesContactEmail, leadID, done) ->
     # log.info("DEBUG: closeIO.sendMail #{fromAddress} #{subject} #{content}")
-    @getSalesContactEmail fromAddress, (err, salesContactEmail, leadID) ->
-      return done("Error getting sales contact for #{fromAddress}: #{err}") if err
-      matches = salesContactEmail.match(/^[a-zA-Z_]+ <(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3})>$|(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3})/i)
-      salesContactEmail = matches?[1] ? matches?[2] ? config.mail.supportSchools
-      salesContactEmail = config.mail.supportSchools if salesContactEmail?.indexOf('brian@codecombat.com') >= 0
+    matches = salesContactEmail.match(/^[a-zA-Z_]+ <(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3})>$|(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3})/i)
+    salesContactEmail = matches?[1] ? matches?[2] ? config.mail.supportSchools
+    salesContactEmail = config.mail.supportSchools if salesContactEmail?.indexOf('brian@codecombat.com') >= 0
+
+    postData =
+      to: [salesContactEmail]
+      sender: config.mail.username
+      subject: subject
+      body_text: content
+      lead_id: leadID
+      status: 'outbox'
+    options =
+      uri: "https://#{apiKey}:X@app.close.io/api/v1/activity/email/"
+      body: JSON.stringify(postData)
+    request.post options, (error, response, body) =>
+      return done(error) if error
+      result = JSON.parse(body)
+      if result.errors or result['field-errors']
+        errorMessage = "Close.io Send email POST error for #{fromAddress} #{JSON.stringify(result.errors)} #{JSON.stringify(result['field-errors'])}"
+        return done(errorMessage)
+      return done()
+
+  processLicenseRequest: (teacherEmail, userID, leadID, licensesRequested, amount, done) ->
+    # Update lead with licenses requested
+    putData = 'custom.licensesRequested': licensesRequested
+    options =
+      uri: "https://#{apiKey}:X@app.close.io/api/v1/lead/#{leadID}/"
+      body: JSON.stringify(putData)
+    request.put options, (error, response, body) =>
+      return done(error) if error 
+      result = JSON.parse(body)
+      if result.errors or result['field-errors']
+        errorMessage = "Update Close.io lead PUT error for #{teacherEmail} #{leadID}"
+        return done(errorMessage)
+
+      # Create call task
       postData =
-        to: [salesContactEmail]
-        sender: config.mail.username
-        subject: subject
-        body_text: content
+        _type: "lead"
         lead_id: leadID
-        status: 'outbox'
+        assigned_to: userID
+        text: "Call #{teacherEmail}"
+        is_complete: false
       options =
-        uri: "https://#{apiKey}:X@app.close.io/api/v1/activity/email/"
+        uri: "https://#{apiKey}:X@app.close.io/api/v1/task/"
         body: JSON.stringify(postData)
       request.post options, (error, response, body) =>
-        return done(error) if error
-        result = JSON.parse(body);
+        return done(error) if error 
+        result = JSON.parse(body)
         if result.errors or result['field-errors']
-          errorMessage = "Close.io Send email POST error for #{fromAddress} #{JSON.stringify(result.errors)} #{JSON.stringify(result['field-errors'])}";
+          errorMessage = "Create Close.io call task POST error for #{teacherEmail} #{leadID}"
           return done(errorMessage)
-        return done()
+
+        # Create opportunity
+        postData =
+          note: "#{licensesRequested} licenses requested"
+          confidence: 5
+          lead_id: leadID
+          status: 'Active'
+          value: parseInt(licensesRequested) * amount
+          value_period: "annual"
+        options =
+          uri: "https://#{apiKey}:X@app.close.io/api/v1/opportunity/"
+          body: JSON.stringify(postData)
+        request.post options, (error, response, body) =>
+          return done(error) if error 
+          result = JSON.parse(body)
+          if result.errors or result['field-errors']
+            errorMessage = "Create Close.io opportunity POST error for #{teacherEmail} #{leadID}"
+            return done(errorMessage)
+          return done()
diff --git a/server/routes/contact.coffee b/server/routes/contact.coffee
index f391a9050..8cbf51dbe 100644
--- a/server/routes/contact.coffee
+++ b/server/routes/contact.coffee
@@ -3,8 +3,9 @@ log = require 'winston'
 User = require '../models/User'
 sendwithus = require '../sendwithus'
 async = require 'async'
-LevelSession = require '../models/LevelSession'
 moment = require 'moment'
+LevelSession = require '../models/LevelSession'
+Product = require '../models/Product'
 closeIO = require '../lib/closeIO'
 
 module.exports.setup = (app) ->
@@ -13,10 +14,19 @@ module.exports.setup = (app) ->
     # log.info "Sending mail from #{req.body.email} saying #{req.body.message}"
     fromAddress = req.body.sender or req.body.email or req.user.get('email')
     createMailContent req, fromAddress, (subject, content) ->
-      if req.body.recipientID is 'schools@codecombat.com' or req.user.isTeacher()
-        req.user.update({$set: { enrollmentRequestSent: true }}).exec(_.noop) if req.body.recipientID is 'schools@codecombat.com'
-        closeIO.sendMail fromAddress, subject, content, (err) ->
-          log.error "Error sending contact form email via Close.io: #{err.message or err}" if err
+      if req.body.licensesNeeded or req.user.isTeacher()
+        closeIO.getSalesContactEmail fromAddress, (err, salesContactEmail, userID, leadID) ->
+          return log.error("Error getting sales contact for #{fromAddress}: #{err.message or err}") if err
+          closeIO.sendMail fromAddress, subject, content, salesContactEmail, leadID, (err) ->
+            return log.error("Error sending contact form email via Close.io: #{err.message or err}") if err
+            if licensesNeeded = req.body.licensesNeeded
+              Product.findOne({name: 'course'}).exec (err, product) =>
+                return log.error(err) if err
+                return log.error('course product not found') if not product
+                amount = product.get('amount')
+                closeIO.processLicenseRequest fromAddress, userID, leadID, licensesNeeded, amount, (err) ->
+                  return log.error("Error processing license request via Close.io: #{err.message or err}") if err
+                  req.user.update({$set: { enrollmentRequestSent: true }}).exec(_.noop)
       else 
         createSendWithUsContext req, fromAddress, subject, content, (context) ->
           sendwithus.api.send context, (err, result) ->
@@ -53,7 +63,7 @@ createSendWithUsContext = (req, fromAddress, subject, content, done) ->
   premium = user?.isPremium()
   teacher = user?.isTeacher()
 
-  if recipientID is 'schools@codecombat.com' or teacher
+  if teacher or req.body.licensesNeeded
     return done("Tried to send a teacher contact us email via sendwithus #{fromAddress} #{subject}")
 
   toAddress = switch