diff --git a/app/assets/javascripts/workers/worker_debug.js b/app/assets/javascripts/workers/worker_debug.js
new file mode 100644
index 000000000..8b6c6cf58
--- /dev/null
+++ b/app/assets/javascripts/workers/worker_debug.js
@@ -0,0 +1,210 @@
+// There's no reason that this file is in JavaScript instead of CoffeeScript.
+// We should convert it and update the brunch config.
+
+// If we wanted to be more robust, we could use this: https://github.com/padolsey/operative/blob/master/src/operative.js
+if(typeof window !== 'undefined' || !self.importScripts)
+    throw "Attempt to load worker_world into main window instead of web worker.";
+
+// Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
+// This is here for running simuations in enviroments lacking function.bind (PhantomJS mostly)
+if (!Function.prototype.bind) {
+    Function.prototype.bind = function (oThis) {
+        if (typeof this !== "function") {
+            // closest thing possible to the ECMAScript 5 internal IsCallable function
+            throw new TypeError("Function.prototype.bind (Shim) - target is not callable");
+        }
+
+        var aArgs = Array.prototype.slice.call(arguments, 1),
+            fToBind = this,
+            fNOP = function () {},
+            fBound = function () {
+                return fToBind.apply(this instanceof fNOP && oThis
+                        ? this
+                        : oThis,
+                    aArgs.concat(Array.prototype.slice.call(arguments)));
+            };
+
+        fNOP.prototype = this.prototype;
+        fBound.prototype = new fNOP();
+
+        return fBound;
+    };
+}
+
+// assign global window so that Brunch's require (in world.js) can go into it
+self.window = self;
+self.workerID = "Worker";
+
+self.logLimit = 200;
+self.logsLogged = 0;
+var console = {
+    log: function() {
+        if(self.logsLogged++ == self.logLimit)
+            self.postMessage({type: 'console-log', args: ["Log limit " + self.logLimit + " reached; shutting up."], id: self.workerID});
+        else if(self.logsLogged < self.logLimit) {
+            args = [].slice.call(arguments);
+            for(var i = 0; i < args.length; ++i) {
+                if(args[i] && args[i].constructor) {
+                    if(args[i].constructor.className === "Thang" || args[i].isComponent)
+                        args[i] = args[i].toString();
+                }
+            }
+            try {
+                self.postMessage({type: 'console-log', args: args, id: self.workerID});
+            }
+            catch(error) {
+                self.postMessage({type: 'console-log', args: ["Could not post log: " + args, error.toString(), error.stack, error.stackTrace], id: self.workerID});
+            }
+        }
+    }};  // so that we don't crash when debugging statements happen
+console.error = console.info = console.log;
+self.console = console;
+
+importScripts('/javascripts/world.js');
+
+// We could do way more from this: http://stackoverflow.com/questions/10653809/making-webworkers-a-safe-environment
+Object.defineProperty(self, "XMLHttpRequest", {
+    get: function() { throw new Error("Access to XMLHttpRequest is forbidden."); },
+    configurable: false
+});
+
+self.transferableSupported = function transferableSupported() {
+    // Not in IE, even in IE 11
+    try {
+        var ab = new ArrayBuffer(1);
+        worker.postMessage(ab, [ab]);
+        return ab.byteLength == 0;
+    } catch(error) {
+        return false;
+    }
+    return false;
+}
+
+var World = self.require('lib/world/world');
+var GoalManager = self.require('lib/world/GoalManager');
+
+self.getCurrentFrame = function getCurrentFrame(args) { return self.world.frames.length; };
+
+self.runWorld = function runWorld(args) {
+    self.postedErrors = {};
+    self.t0 = new Date();
+    self.firstWorld = args.firstWorld;
+    self.postedErrors = false;
+    self.logsLogged = 0;
+
+    try {
+        self.world = new World(args.worldName, args.userCodeMap);
+        if(args.level)
+            self.world.loadFromLevel(args.level, true);
+        self.goalManager = new GoalManager(self.world);
+        self.goalManager.setGoals(args.goals);
+        self.goalManager.setCode(args.userCodeMap);
+        self.goalManager.worldGenerationWillBegin();
+        self.world.setGoalManager(self.goalManager);
+    }
+    catch (error) {
+        self.onWorldError(error);
+        return;
+    }
+    Math.random = self.world.rand.randf;  // so user code is predictable
+    self.world.loadFrames(self.onWorldLoaded, self.onWorldError, self.onWorldLoadProgress);
+};
+
+self.runWorldUntilFrame = function runWorldUntilFrame(args) {
+    self.postedErrors = {};
+    self.t0 = new Date();
+    self.firstWorld = args.firstWorld;
+    self.postedErrors = false;
+    self.logsLogged = 0;
+    if (!self.world)
+    {
+        try {
+            self.world = new World(args.worldName, args.userCodeMap);
+            if(args.level)
+                self.world.loadFromLevel(args.level, true);
+            self.goalManager = new GoalManager(self.world);
+            self.goalManager.setGoals(args.goals);
+            self.goalManager.setCode(args.userCodeMap);
+            self.goalManager.worldGenerationWillBegin();
+            self.world.setGoalManager(self.goalManager);
+        }
+        catch (error) {
+            self.onWorldError(error);
+            return;
+        }
+        Math.random = self.world.rand.randf;  // so user code is predictable
+    }
+    
+    self.world.totalFrames = args.frame; //hack to work around error checking
+    
+    self.world.loadFramesUntilFrame(args.frame, self.onWorldLoaded, self.onWorldError, self.onWorldLoadProgress);
+};
+
+self.onWorldLoaded = function onWorldLoaded() {
+    var t1 = new Date();
+    var diff = t1 - self.t0;
+    var transferableSupported = self.transferableSupported();
+    try {
+        var serialized = self.world.serialize();
+    }
+    catch(error) {
+        console.log("World serialization error:", error.toString() + "\n" + error.stack || error.stackTrace);
+    }
+    var t2 = new Date();
+    //console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects);
+    try {
+        if(transferableSupported)
+            self.postMessage({type: 'new-debug-world', serialized: serialized.serializedWorld, goalStates: self.goalManager.getGoalStates()}, serialized.transferableObjects);
+        else
+            self.postMessage({type: 'new-debug-world', serialized: serialized.serializedWorld, goalStates: self.goalManager.getGoalStates()});
+    }
+    catch(error) {
+        console.log("World delivery error:", error.toString() + "\n" + error.stack || error.stackTrace);
+    }
+    var t3 = new Date();
+    console.log("And it was so: (" + (diff / self.world.totalFrames).toFixed(3) + "ms per frame,", self.world.totalFrames, "frames)\nSimulation   :", diff + "ms \nSerialization:", (t2 - t1) + "ms\nDelivery     :", (t3 - t2) + "ms");
+};
+
+self.onWorldError = function onWorldError(error) {
+    if(error instanceof Aether.problems.UserCodeProblem) {
+        if(!self.postedErrors[error.key]) {
+            var problem = error.serialize();
+            self.postMessage({type: 'user-code-problem', problem: problem});
+            self.postedErrors[error.key] = problem;
+        }
+    }
+    else {
+        console.log("Non-UserCodeError:", error.toString() + "\n" + error.stack || error.stackTrace);
+    }
+    /*  We don't actually have the recoverable property any more; hmm
+     if(!self.firstWorld && !error.recoverable) {
+     self.abort();
+     return false;
+     }
+     */
+    return true;
+};
+
+self.onWorldLoadProgress = function onWorldLoadProgress(progress) {
+    self.postMessage({type: 'world-load-progress-changed', progress: progress});
+};
+
+self.abort = function abort() {
+    if(self.world && self.world.name) {
+        console.log("About to abort:", self.world.name, typeof self.world.abort);
+        if(typeof self.world !== "undefined")
+            self.world.abort();
+        self.world = null;
+    }
+    self.postMessage({type: 'abort'});
+};
+
+self.reportIn = function reportIn() {
+    self.postMessage({type: 'reportIn'});
+}
+
+self.addEventListener('message', function(event) {
+    self[event.data.func](event.data.args);
+});
+
+self.postMessage({type: 'worker-initialized'});
diff --git a/app/lib/God.coffee b/app/lib/God.coffee
index 6fe758391..983b264c6 100644
--- a/app/lib/God.coffee
+++ b/app/lib/God.coffee
@@ -23,6 +23,8 @@ module.exports = class God
     Backbone.Mediator.subscribe 'tome:cast-spells', @onTomeCast, @
     @fillWorkerPool = _.throttle @fillWorkerPool, 3000, leading: false
     @fillWorkerPool()
+    #TODO: have this as a constructor option
+    @debugWorker = @createDebugWorker()
 
   onTomeCast: (e) ->
     return if @dead
@@ -48,14 +50,31 @@ module.exports = class God
     worker.creationTime = new Date()
     worker.addEventListener 'message', @onWorkerMessage
     worker
-
+    
+  createDebugWorker: ->
+    worker = new Worker '/javascripts/workers/worker_debug.js'
+    worker.creationTime = new Date()
+    worker.addEventListener 'message', @onDebugWorkerMessage
+    console.log "GOD: Created debug worker"
+    worker
+    
   onWorkerMessage: (event) =>
     worker = event.target
     if event.data.type is 'worker-initialized'
       #console.log @id, "worker initialized after", ((new Date()) - worker.creationTime), "ms (before it was needed)"
       worker.initialized = true
       worker.removeEventListener 'message', @onWorkerMessage
-
+      
+  onDebugWorkerMessage: (event) =>
+    worker = event.target
+    switch event.data.type
+      when "worker-initialized"
+        worker.initialized = true
+      when 'new-debug-world'
+        console.log "Created new debug world!"
+      when 'console-log'
+        console.log "|" + @id + "'s " + @id + "|", event.data.args...
+  
   getAngel: ->
     freeAngel = null
     for angel in @angels
@@ -106,7 +125,22 @@ module.exports = class God
       firstWorld: @firstWorld
       goals: @goalManager?.getGoals()
     }}
-
+    
+  createDebugWorldUntilFrame: (frame) ->
+    @debugWorker.postMessage 
+      func : 'runWorldUntilFrame'
+      args:
+        worldName: @level.name
+        userCodeMap: @getUserCodeMap()
+        level: @level
+        firstWorld: @firstWorld
+        goals: @goalManager?.getGoals()
+        frame: frame
+        
+  getDebugWorldCurrentFrame: ->
+    @debugWorker.postMessage
+      func: 'getCurrentFrame'
+  
   beholdWorld: (angel, serialized, goalStates) ->
     worldCreation = angel.started
     angel.free()
diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee
index f209f82d3..ed694cd9a 100644
--- a/app/lib/world/world.coffee
+++ b/app/lib/world/world.coffee
@@ -103,6 +103,36 @@ module.exports = class World
     loadProgressCallback? 1
     loadedCallback()
 
+  loadFramesUntilFrame: (frameToLoadUntil, loadedCallback, errorCallback, loadProgressCallback) ->
+    return if @aborted
+    unless @thangs.length
+      console.log "Warning: loadFrames called on empty World"
+    t1 = now()
+    @t0 ?= t1
+    i = @frames.length
+    while i < frameToLoadUntil
+      try
+        @getFrame(i)
+        ++i  # increment this after we have succeeded in getting the frame, otherwise we'll have to do that frame again
+      catch error
+      # Not an Aether.errors.UserCodeError; maybe we can't recover
+        @addError error
+      for error in (@unhandledRuntimeErrors ? [])
+        return unless errorCallback error  # errorCallback tells us whether the error is recoverable
+      @unhandledRuntimeErrors = []
+      t2 = now()
+      if t2 - t1 > PROGRESS_UPDATE_INTERVAL
+        loadProgressCallback? i / @totalFrames
+        t1 = t2
+        if t2 - @t0 > 1000
+          console.log('  Loaded', i, 'of', frameToLoadUntil, "(+" + (t2 - @t0).toFixed(0) + "ms)")
+          @t0 = t2
+        setTimeout((=> @loadFrames(loadedCallback, errorCallback, loadProgressCallback)), 0)
+        return
+    @ended = true
+    loadProgressCallback? 1
+    loadedCallback()
+
   abort: ->
     @aborted = true
 
@@ -221,7 +251,7 @@ module.exports = class World
       @scriptNotes.push scriptNote
     return unless @goalManager
     @goalManager.submitWorldGenerationEvent(channel, event, @frames.length)
-    
+
   setGoalState: (goalID, status) ->
     @goalManager.setGoalState(goalID, status)