diff --git a/app/lib/God.coffee b/app/lib/God.coffee
index 4d3a195d8..730c034a0 100644
--- a/app/lib/God.coffee
+++ b/app/lib/God.coffee
@@ -210,8 +210,11 @@ class Angel
     @purgatoryTimer = null
     if @worker
       worker = @worker
-      _.defer -> worker.terminate()
-      @worker.removeEventListener 'message', @onWorkerMessage
+      onWorkerMessage = @onWorkerMessage
+      _.delay ->
+        worker.terminate()
+        worker.removeEventListener 'message', onWorkerMessage
+      , 1000
       @worker = null
     @
 
diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee
index dfae34788..aba6fae47 100644
--- a/app/lib/LevelLoader.coffee
+++ b/app/lib/LevelLoader.coffee
@@ -98,7 +98,7 @@ module.exports = class LevelLoader extends CocoClass
   onSupermodelError: ->
     msg = $.i18n.t('play_level.level_load_error',
       defaultValue: "Level could not be loaded.")
-    @$el.html('<div class="alert">' + msg + '</div>')
+    $('body').append('<div class="alert">' + msg + '</div>')
 
   onSupermodelLoadedOne: (e) ->
     @update()
diff --git a/app/lib/surface/CocoSprite.coffee b/app/lib/surface/CocoSprite.coffee
index c843d3f00..6246619db 100644
--- a/app/lib/surface/CocoSprite.coffee
+++ b/app/lib/surface/CocoSprite.coffee
@@ -140,8 +140,8 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass
     @show()
     return @updateActionDirection() unless action.animation or action.container
     m = if action.container then "gotoAndStop" else "gotoAndPlay"
-    @imageObject[m] action.name
     @imageObject.framerate = action.framerate or 20
+    @imageObject[m] action.name
     reg = @getOffset 'registration'
     @imageObject.regX = -reg.x
     @imageObject.regY = -reg.y
diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index e503250d3..b0b1ee423 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -36,6 +36,7 @@ module.exports = Surface = class Surface extends CocoClass
   worldLoaded: false
   scrubbing: false
   debug: false
+  frameRate: 60
 
   defaults:
     wizards: true
@@ -190,7 +191,7 @@ module.exports = Surface = class Surface extends CocoClass
       createjs.Tween.removeTweens(@)
       @currentFrame = @scrubbingTo
 
-    @scrubbingTo = parseInt(progress * @world.totalFrames)
+    @scrubbingTo = Math.floor(progress * @world.totalFrames)
     @scrubbingPlaybackSpeed = Math.sqrt(Math.abs(@scrubbingTo - @currentFrame) * @world.dt / (scrubDuration or 0.5))
     if scrubDuration
       t = createjs.Tween
@@ -227,7 +228,7 @@ module.exports = Surface = class Surface extends CocoClass
     @onFrameChanged()
 
   getCurrentFrame: ->
-    return Math.max(0, Math.min(parseInt(@currentFrame), @world.totalFrames - 1))
+    return Math.max(0, Math.min(Math.floor(@currentFrame), @world.totalFrames - 1))
 
   getProgress: -> @currentFrame / @world.totalFrames
 
@@ -344,8 +345,7 @@ module.exports = Surface = class Surface extends CocoClass
     @stage.addEventListener 'stagemousedown', @onMouseDown
     @canvas.on 'mousewheel', @onMouseWheel
     @hookUpChooseControls() if @options.choosing
-    console.log "Setting fps", @world.frameRate unless @world.frameRate is 30
-    createjs.Ticker.setFPS @world.frameRate
+    createjs.Ticker.setFPS @frameRate
 
   showLevel: ->
     return if @dead
@@ -467,16 +467,16 @@ module.exports = Surface = class Surface extends CocoClass
       @trailmaster.tick() if @trailmaster
       # Skip some frame updates unless we're playing and not at end (or we haven't drawn much yet)
       frameAdvanced = (@playing and @currentFrame < @world.totalFrames) or @totalFramesDrawn < 2
-      ++@currentFrame if frameAdvanced
+      @currentFrame += @world.frameRate / @frameRate if frameAdvanced
       @updateSpriteSounds() if frameAdvanced
       break unless Dropper.drop()
 
     # these are skipped for dropped frames
     @updateState @currentFrame isnt oldFrame
-    @drawCurrentFrame()
+    @drawCurrentFrame e
     @onFrameChanged()
-    @updatePaths() if (@totalFramesDrawn % 2) is 0 or createjs.Ticker.getMeasuredFPS() > createjs.Ticker.getFPS() - 5
-    Backbone.Mediator.publish('surface:ticked', {dt: @world.dt})
+    @updatePaths() if (@totalFramesDrawn % 4) is 0 or createjs.Ticker.getMeasuredFPS() > createjs.Ticker.getFPS() - 5
+    Backbone.Mediator.publish('surface:ticked', {dt: 1 / @frameRate})
     mib = @stage.mouseInBounds
     if @mouseInBounds isnt mib
       Backbone.Mediator.publish('surface:mouse-' + (if mib then "over" else "out"), {})
@@ -484,6 +484,11 @@ module.exports = Surface = class Surface extends CocoClass
 
   updateSpriteSounds: ->
     @world.getFrame(@getCurrentFrame()).restoreState()
+    current = Math.max(0, Math.min(@currentFrame, @world.totalFrames - 1))
+    if current - Math.floor(current) > 0.01
+      next = Math.ceil current
+      ratio = current % 1
+      @world.frames[next].restorePartialState ratio if next > 1
     @spriteBoss.updateSounds()
 
   updateState: (frameChanged) ->
@@ -492,9 +497,9 @@ module.exports = Surface = class Surface extends CocoClass
     @spriteBoss.update frameChanged
     @dimmer?.setSprites @spriteBoss.sprites
 
-  drawCurrentFrame: ->
+  drawCurrentFrame: (e) ->
     ++@totalFramesDrawn
-    @stage.update()
+    @stage.update e
 
   # paths - TODO: move to SpriteBoss? but only update on frame drawing instead of on every frame update?
 
diff --git a/app/lib/world/thang_state.coffee b/app/lib/world/thang_state.coffee
index a6ca7546c..964f25eda 100644
--- a/app/lib/world/thang_state.coffee
+++ b/app/lib/world/thang_state.coffee
@@ -67,7 +67,7 @@ module.exports = class ThangState
 
   restore: ->
     # Restore trackedProperties' values to @thang, retrieving them from @trackedPropertyValues if needed. Optimize it.
-    return @ if @thang._state is @
+    return @ if @thang._state is @ and not @thang.partialState
     unless @hasRestored  # Restoring in a deserialized World for first time
       props = []
       for prop, propIndex in @trackedPropertyKeys
@@ -81,6 +81,26 @@ module.exports = class ThangState
     else  # Restoring later times
       for prop, propIndex in @trackedPropertyKeys
         @thang[prop] = @props[propIndex]
+    @thang.partialState = false
+    @
+
+  restorePartial: (ratio) ->
+    inverse = 1 - ratio
+    for prop, propIndex in @trackedPropertyKeys when prop is "pos" or prop is "rotation"
+      if @hasRestored
+        value = @props[propIndex]
+      else
+        type = @trackedPropertyTypes[propIndex]
+        storage = @trackedPropertyValues[propIndex]
+        value = @getStoredProp propIndex, type, storage
+      if prop is "pos"
+        @thang.pos = @thang.pos.copy()
+        @thang.pos.x = inverse * @thang.pos.x + ratio * value.x
+        @thang.pos.y = inverse * @thang.pos.y + ratio * value.y
+        @thang.pos.z = inverse * @thang.pos.z + ratio * value.z
+      else if prop is "rotation"
+        @thang.rotation = inverse * @thang.rotation + ratio * value
+      @thang.partialState = true
     @
 
   serialize: (frameIndex, trackedPropertyIndices, trackedPropertyTypes, trackedPropertyValues, specialValuesToKeys, specialKeysToValues) ->
diff --git a/app/lib/world/world_frame.coffee b/app/lib/world/world_frame.coffee
index 997cf548f..fcac050a8 100644
--- a/app/lib/world/world_frame.coffee
+++ b/app/lib/world/world_frame.coffee
@@ -25,6 +25,9 @@ module.exports = class WorldFrame
         #console.log "Frame", @time, "restoring state for", thang.id, "and saying it don't exist"
         thang.exists = false
 
+  restorePartialState: (ratio) ->
+    thangState.restorePartial ratio for thangID, thangState of @thangStateMap
+
   restoreStateForThang: (thang) ->
     thangState = @thangStateMap[thang.id]
     if not thangState