diff --git a/package.json b/package.json
index 8e9686b49f005683ee8839013b58ad4c8095e166..304d7bff27963b890eb12e5c698ac619041e84f5 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
   "dependencies": {
     "aframe": "https://github.com/robertlong/aframe#fix-increasing-offset",
     "aframe-extras": "^3.12.4",
-    "aframe-input-mapping-component": "https://github.com/fernandojsg/aframe-input-mapping-component#6ebc38f",
+    "aframe-input-mapping-component": "https://github.com/johnshaughnessy/aframe-input-mapping-component#23e2855",
     "aframe-teleport-controls": "https://github.com/netpro2k/aframe-teleport-controls#feature/teleport-origin",
     "extract-text-webpack-plugin": "^3.0.2",
     "material-design-lite": "^1.3.0",
diff --git a/src/activators/pressedmove.js b/src/activators/pressedmove.js
new file mode 100644
index 0000000000000000000000000000000000000000..c035383186e0997908be2eae5868a1a97e546dd4
--- /dev/null
+++ b/src/activators/pressedmove.js
@@ -0,0 +1,35 @@
+function PressedMove(el, button, onActivate) {
+  this.down = button + "down";
+  this.up = button + "up";
+  this.pressed = false;
+  this.onActivate = onActivate;
+  this.el = el;
+  this.onButtonDown = this.onButtonDown.bind(this);
+  this.onButtonUp = this.onButtonUp.bind(this);
+  this.onAxisMove = this.onAxisMove.bind(this);
+  el.addEventListener(this.down, this.onButtonDown);
+  el.addEventListener(this.up, this.onButtonUp);
+  el.addEventListener("axismove", this.onAxisMove);
+}
+
+PressedMove.prototype = {
+  onAxisMove: function(event) {
+    if (this.pressed) {
+      this.onActivate(event);
+    }
+  },
+  onButtonDown: function(event) {
+    this.pressed = true;
+  },
+  onButtonUp: function(event) {
+    this.pressed = false;
+  },
+
+  removeListeners: function() {
+    this.el.addEventListener(this.down, this.onButtonDown);
+    this.el.addEventListener(this.up, this.onButtonUp);
+    this.el.addEventListener("axismove", this.onAxisMove);
+  }
+};
+
+export { PressedMove };
diff --git a/src/activators/reversey.js b/src/activators/reversey.js
new file mode 100644
index 0000000000000000000000000000000000000000..d3f3967f2da72d954b9a47d81bfaa0f1782f95f2
--- /dev/null
+++ b/src/activators/reversey.js
@@ -0,0 +1,20 @@
+function ReverseY(el, button, onActivate) {
+  this.el = el;
+  this.emitReverseY = this.emitReverseY.bind(this);
+  this.onActivate = onActivate;
+  this.button = button;
+  this.removeListeners = this.removeListeners.bind(this);
+  el.addEventListener(button, this.emitReverseY);
+}
+
+ReverseY.prototype = {
+  emitReverseY: function(event) {
+    event.detail.axis[1] *= -1;
+    this.onActivate(event);
+  },
+  removeListeners: function() {
+    this.el.removeEventListener(this.button, this.emitReverseY);
+  }
+};
+
+export { ReverseY };
diff --git a/src/activators/shortpress.js b/src/activators/shortpress.js
new file mode 100644
index 0000000000000000000000000000000000000000..494de3f7ab9e7ebb11c04a227740fe9fcd7e6a42
--- /dev/null
+++ b/src/activators/shortpress.js
@@ -0,0 +1,39 @@
+function ShortPress(el, button, onActivate) {
+  this.lastTime = 0;
+  this.timeOut = 500;
+  this.eventNameDown = button + "down";
+  this.eventNameUp = button + "up";
+
+  this.el = el;
+  this.onActivate = onActivate;
+
+  this.onButtonDown = this.onButtonDown.bind(this);
+  this.onButtonUp = this.onButtonUp.bind(this);
+
+  el.addEventListener(this.eventNameDown, this.onButtonDown);
+  el.addEventListener(this.eventNameUp, this.onButtonUp);
+}
+
+ShortPress.prototype = {
+  onButtonDown(event) {
+    console.log("down");
+    var self = this;
+    this.pressTimer = window.setTimeout(function() {
+      console.log("activate");
+      self.onActivate(event);
+    }, this.timeOut);
+  },
+
+  onButtonUp(event) {
+    console.log("up");
+    clearTimeout(this.pressTimer);
+  },
+
+  removeListeners() {
+    this.el.removeEventListener(this.eventNameDown, this.onButtonDown);
+    this.el.removeEventListener(this.eventNameUp, this.onButtonUp);
+  }
+};
+
+console.log("foo");
+AFRAME.registerInputActivator("shortpress", ShortPress);
diff --git a/src/behaviours/oculus-touch-joystick-dpad4.js b/src/behaviours/oculus-touch-joystick-dpad4.js
new file mode 100644
index 0000000000000000000000000000000000000000..ed85bca9555cf127d6fd90e196753cfcd47e9f97
--- /dev/null
+++ b/src/behaviours/oculus-touch-joystick-dpad4.js
@@ -0,0 +1,31 @@
+import { angleTo4Direction, angleTo8Direction } from "../utils";
+
+// @TODO specify 4 or 8 direction
+function oculus_touch_joystick_dpad4(el, outputPrefix) {
+  this.angleToDirection = angleTo4Direction;
+  this.outputPrefix = outputPrefix;
+  this.centerRadius = 0.6;
+  this.previous = "none";
+  this.hapticIntensity = "low";
+  this.emitDPad4 = this.emitDPad4.bind(this);
+  el.addEventListener("axismove", this.emitDPad4);
+}
+
+oculus_touch_joystick_dpad4.prototype = {
+  emitDPad4: function(event) {
+    const x = event.detail.axis[0];
+    const y = event.detail.axis[1];
+    const inCenter =
+      Math.abs(x) < this.centerRadius && Math.abs(y) < this.centerRadius;
+    const current = inCenter
+      ? "center"
+      : this.angleToDirection(Math.atan2(x, -y));
+    if (current !== this.previous) {
+      this.previous = current;
+      event.target.emit(`${this.outputPrefix}_dpad4_${current}`);
+      event.target.emit("haptic_pulse", { intensity: this.hapticIntensity });
+    }
+  }
+};
+
+export { oculus_touch_joystick_dpad4 };
diff --git a/src/behaviours/vive-trackpad-dpad4.js b/src/behaviours/vive-trackpad-dpad4.js
new file mode 100644
index 0000000000000000000000000000000000000000..4ef7364ab7d8ee4c22780c68b1183b4eba0db82a
--- /dev/null
+++ b/src/behaviours/vive-trackpad-dpad4.js
@@ -0,0 +1,57 @@
+import { angleTo4Direction, angleTo8Direction } from "../utils";
+
+function vive_trackpad_dpad4(el, outputPrefix) {
+  this.outputPrefix = outputPrefix;
+  this.lastDirection = "";
+  this.previous = "";
+  this.pressed = false;
+  this.emitDPad4 = this.emitDPad4.bind(this);
+  this.press = this.press.bind(this);
+  this.unpress = this.unpress.bind(this);
+  this.hapticIntensity = "low";
+  this.centerRadius = 0.6;
+  el.addEventListener("axismove", this.emitDPad4);
+  el.addEventListener("trackpaddown", this.press);
+  el.addEventListener("trackpadup", this.unpress);
+}
+
+vive_trackpad_dpad4.prototype = {
+  press: function(_) {
+    this.pressed = true;
+  },
+  unpress: function(_) {
+    this.pressed = false;
+  },
+  emitDPad4: function(event) {
+    const x = event.detail.axis[0];
+    const y = event.detail.axis[1];
+    const inCenter =
+      Math.abs(x) < this.centerRadius && Math.abs(y) < this.centerRadius;
+    const direction = inCenter
+      ? "center"
+      : angleTo4Direction(Math.atan2(x, -y));
+    const pressed = this.pressed ? "pressed_" : "";
+    const current = `${pressed + direction}`; // e.g. "pressed_north"
+
+    // Real axismove events are not perfectly [0,0]...
+    // This is a touchend event.
+    if (x === 0 && y === 0) {
+      event.target.emit(`${this.outputPrefix}_dpad4_${this.previous}_up`);
+      this.previous = ""; // Clear this because the user has lifted their finger.
+      return;
+    }
+
+    if (current === this.previous) {
+      return;
+    }
+
+    if (this.previous !== "") {
+      event.target.emit(`${this.outputPrefix}_dpad4_${this.previous}_up`);
+    }
+
+    event.target.emit(`${this.outputPrefix}_dpad4_${current}_down`);
+    this.previous = current;
+  }
+};
+
+export { vive_trackpad_dpad4 };
diff --git a/src/components/axis-dpad.js b/src/components/axis-dpad.js
deleted file mode 100644
index 90fb29176013384f42d3c2da02631a1f0a88c634..0000000000000000000000000000000000000000
--- a/src/components/axis-dpad.js
+++ /dev/null
@@ -1,81 +0,0 @@
-/**
- * @fileOverview
- * Treats a pair of axes and a button as a dpad
- * This is useful for Vive trackpad and Oculus Touch thumbstick
- *
- * @name axis-dpad.js
- * @TODO allow use of thumbstick without press
- * @TODO make axes configurable
- */
-
-const angleToDirection = function(angle) {
-  angle = (angle * THREE.Math.RAD2DEG + 180 + 45) % 360;
-  if (angle > 0 && angle < 90) {
-    return "down";
-  } else if (angle >= 90 && angle < 180) {
-    return "left";
-  } else if (angle >= 180 && angle < 270) {
-    return "up";
-  } else {
-    return "right";
-  }
-};
-
-AFRAME.registerComponent("axis-dpad", {
-  schema: {
-    centerZone: { default: 0.5 },
-    moveEvents: { default: ["axismove"] },
-    downEvents: { default: ["trackpaddown", "thumbstickdown"] },
-    upEvents: { default: ["trackpadup", "thumbstickup"] }
-  },
-
-  init: function() {
-    this.onAxisMove = this.onAxisMove.bind(this);
-    this.onButtonPressed = this.onButtonPressed.bind(this);
-    this.lastPos = [0, 0];
-  },
-
-  play: function() {
-    const { moveEvents, downEvents, upEvents } = this.data;
-    moveEvents.forEach(moveEvent => {
-      this.el.addEventListener(moveEvent, this.onAxisMove);
-    });
-    downEvents.concat(upEvents).forEach(eventName => {
-      this.el.addEventListener(eventName, this.onButtonPressed);
-    });
-  },
-
-  pause: function() {
-    const { moveEvents, downEvents, upEvents } = this.data;
-    moveEvents.forEach(moveEvent => {
-      this.el.removeEventListener(moveEvent, this.onAxisMove);
-    });
-    downEvents.concat(upEvents).forEach(eventName => {
-      this.el.removeEventListener(eventName, this.onButtonPressed);
-    });
-  },
-
-  onAxisMove: function(e) {
-    this.lastPos = e.detail.axis;
-  },
-
-  onButtonPressed: function(e) {
-    const [x, y] = this.lastPos;
-    const { upEvents, centerZone } = this.data;
-    const state = upEvents.includes(e.type) ? "up" : "down";
-    const direction =
-      state === "up" && this.lastDirection // Always trigger the up event for the last down event
-        ? this.lastDirection
-        : x * x + y * y < centerZone * centerZone // If within center zone angle does not matter
-          ? "center"
-          : angleToDirection(Math.atan2(x, y));
-
-    this.el.emit(`dpad${direction}${state}`);
-
-    if (state === "down") {
-      this.lastDirection = direction;
-    } else {
-      delete this.lastDirection;
-    }
-  }
-});
diff --git a/src/components/character-controller.js b/src/components/character-controller.js
index f20c758d04fec67ca3b8a20070965b4c8714073b..bd0a6617abaaaed5fdda9fc2e0c0811d97021a4c 100644
--- a/src/components/character-controller.js
+++ b/src/components/character-controller.js
@@ -4,196 +4,66 @@ const MAX_DELTA = 0.2;
 // Does not have any type of collisions yet.
 AFRAME.registerComponent("character-controller", {
   schema: {
-    groundAcc: { default: 10 },
-    easing: { default: 8 },
+    groundAcc: { default: 7 },
+    easing: { default: 10 },
     pivot: { type: "selector" },
-    snapRotationRadian: { default: THREE.Math.DEG2RAD * 45 },
-    wasdSpeed: { default: 0.8 },
+    snapRotationDegrees: { default: THREE.Math.DEG2RAD * 45 },
     rotationSpeed: { default: -3 }
   },
 
   init: function() {
     this.velocity = new THREE.Vector3(0, 0, 0);
     this.accelerationInput = new THREE.Vector3(0, 0, 0);
-    this.onStopMoving = this.onStopMoving.bind(this);
-    this.onTranslateX = this.onTranslateX.bind(this);
-    this.onTranslateY = this.onTranslateY.bind(this);
-    this.onTranslateZ = this.onTranslateZ.bind(this);
-    this.onMoveForward = this.onMoveForward.bind(this);
-    this.onDontMoveForward = this.onDontMoveForward.bind(this);
-    this.onMoveBackward = this.onMoveBackward.bind(this);
-    this.onDontMoveBackward = this.onDontMoveBackward.bind(this);
-    this.onMoveLeft = this.onMoveLeft.bind(this);
-    this.onDontMoveLeft = this.onDontMoveLeft.bind(this);
-    this.onMoveRight = this.onMoveRight.bind(this);
-    this.onDontMoveRight = this.onDontMoveRight.bind(this);
-    this.boost = 1.0;
-    this.onBoost = this.onBoost.bind(this);
-
     this.pendingSnapRotationMatrix = new THREE.Matrix4();
-    this.onSnapRotateLeft = this.onSnapRotateLeft.bind(this);
-    this.onSnapRotateRight = this.onSnapRotateRight.bind(this);
     this.angularVelocity = 0; // Scalar value because we only allow rotation around Y
-    this.onRotateY = this.onRotateY.bind(this);
+    this.setAccelerationInput = this.setAccelerationInput.bind(this);
+    this.snapRotateLeft = this.snapRotateLeft.bind(this);
+    this.snapRotateRight = this.snapRotateRight.bind(this);
+    this.setAngularVelocity = this.setAngularVelocity.bind(this);
   },
 
   update: function() {
     this.leftRotationMatrix = new THREE.Matrix4().makeRotationY(
-      this.data.snapRotationRadian
+      this.data.snapRotationDegrees
     );
     this.rightRotationMatrix = new THREE.Matrix4().makeRotationY(
-      -this.data.snapRotationRadian
+      -this.data.snapRotationDegrees
     );
   },
 
   play: function() {
     const eventSrc = this.el.sceneEl;
-    eventSrc.addEventListener("stop_moving", this.onStopMoving);
-    eventSrc.addEventListener("translateX", this.onTranslateX);
-    eventSrc.addEventListener("translateY", this.onTranslateY);
-    eventSrc.addEventListener("translateZ", this.onTranslateZ);
-    eventSrc.addEventListener("action_move_forward", this.onMoveForward);
-    eventSrc.addEventListener(
-      "action_dont_move_forward",
-      this.onDontMoveForward
-    );
-    eventSrc.addEventListener("action_move_backward", this.onMoveBackward);
-    eventSrc.addEventListener(
-      "action_dont_move_backward",
-      this.onDontMoveBackward
-    );
-    eventSrc.addEventListener("action_move_left", this.onMoveLeft);
-    eventSrc.addEventListener("action_dont_move_left", this.onDontMoveLeft);
-    eventSrc.addEventListener("action_move_right", this.onMoveRight);
-    eventSrc.addEventListener("action_dont_move_right", this.onDontMoveRight);
-
-    eventSrc.addEventListener("rotateY", this.onRotateY);
-    eventSrc.addEventListener("action_snap_rotate_left", this.onSnapRotateLeft);
-    eventSrc.addEventListener(
-      "action_snap_rotate_right",
-      this.onSnapRotateRight
-    );
-    eventSrc.addEventListener("boost", this.onBoost);
+    eventSrc.addEventListener("move", this.setAccelerationInput);
+    eventSrc.addEventListener("rotateY", this.setAngularVelocity);
+    eventSrc.addEventListener("snap_rotate_left", this.snapRotateLeft);
+    eventSrc.addEventListener("snap_rotate_right", this.snapRotateRight);
   },
 
   pause: function() {
     const eventSrc = this.el.sceneEl;
-    eventSrc.removeEventListener("stop_moving", this.onStopMoving);
-    eventSrc.removeEventListener("translateX", this.onTranslateX);
-    eventSrc.removeEventListener("translateY", this.onTranslateY);
-    eventSrc.removeEventListener("translateZ", this.onTranslateZ);
-    eventSrc.removeEventListener("action_move_forward", this.onMoveForward);
-    eventSrc.removeEventListener(
-      "action_dont_move_forward",
-      this.onDontMoveForward
-    );
-    eventSrc.removeEventListener("action_move_backward", this.onMoveBackward);
-    eventSrc.removeEventListener(
-      "action_dont_move_backward",
-      this.onDontMoveBackward
-    );
-    eventSrc.removeEventListener("action_move_left", this.onMoveLeft);
-    eventSrc.removeEventListener("action_dont_move_left", this.onDontMoveLeft);
-    eventSrc.removeEventListener("action_move_right", this.onMoveRight);
-    eventSrc.removeEventListener(
-      "action_dont_move_right",
-      this.onDontMoveRight
-    );
-    eventSrc.removeEventListener("rotateY", this.onRotateY);
-    eventSrc.removeEventListener(
-      "action_snap_rotate_left",
-      this.onSnapRotateLeft
-    );
-    eventSrc.removeEventListener(
-      "action_snap_rotate_right",
-      this.onSnapRotateRight
-    );
+    eventSrc.removeEventListener("move", this.setAccelerationInput);
+    eventSrc.removeEventListener("rotateY", this.setAngularVelocity);
+    eventSrc.removeEventListener("snap_rotate_left", this.snapRotateLeft);
+    eventSrc.removeEventListener("snap_rotate_right", this.snapRotateRight);
   },
 
-  onStopMoving: function(event) {
-    this.accelerationInput.set(0, 0, 0);
+  setAccelerationInput: function(event) {
+    const axes = event.detail.axis;
+    this.accelerationInput.set(axes[0], 0, axes[1]);
   },
 
-  onTranslateX: function(event) {
-    // TODO: event.detail should't default to an object.
-    if (typeof event.detail !== "object") {
-      this.accelerationInput.setX(event.detail);
-    } else {
-      this.accelerationInput.setX(0);
-    }
-  },
-
-  onTranslateY: function(event) {
-    // TODO: event.detail should't default to an object.
-    if (typeof event.detail !== "object") {
-      this.accelerationInput.setY(event.detail);
-    } else {
-      this.accelerationInput.setY(0);
-    }
-  },
-
-  onTranslateZ: function(event) {
-    // TODO: event.detail should't default to an object.
-    if (typeof event.detail !== "object") {
-      this.accelerationInput.setZ(event.detail);
-    } else {
-      this.accelerationInput.setZ(0);
-    }
+  setAngularVelocity: function(event) {
+    this.angularVelocity = event.detail.value;
   },
 
-  onMoveForward: function(event) {
-    this.accelerationInput.z = this.data.wasdSpeed;
-  },
-
-  onDontMoveForward: function(event) {
-    this.accelerationInput.z = 0;
-  },
-
-  onMoveBackward: function(event) {
-    this.accelerationInput.z = -this.data.wasdSpeed;
-  },
-
-  onDontMoveBackward: function(event) {
-    this.accelerationInput.z = 0;
-  },
-
-  onMoveLeft: function(event) {
-    this.accelerationInput.x = -this.data.wasdSpeed;
-  },
-
-  onDontMoveLeft: function(event) {
-    this.accelerationInput.x = 0;
-  },
-
-  onMoveRight: function(event) {
-    this.accelerationInput.x = this.data.wasdSpeed;
-  },
-
-  onDontMoveRight: function(event) {
-    this.accelerationInput.x = 0;
-  },
-
-  onRotateY: function(event) {
-    // TODO: event.detail should't default to an object.
-    if (typeof event.detail !== "object") {
-      this.angularVelocity = event.detail;
-    } else {
-      this.angularVelocity = 0;
-    }
-  },
-
-  onSnapRotateLeft: function(event) {
+  snapRotateLeft: function(event) {
     this.pendingSnapRotationMatrix.copy(this.leftRotationMatrix);
   },
 
-  onSnapRotateRight: function(event) {
+  snapRotateRight: function(event) {
     this.pendingSnapRotationMatrix.copy(this.rightRotationMatrix);
   },
 
-  onBoost: function(event) {
-    this.boost = event.detail;
-  },
-
   tick: (function() {
     const move = new THREE.Matrix4();
     const trans = new THREE.Matrix4();
@@ -298,9 +168,13 @@ AFRAME.registerComponent("character-controller", {
       velocity.z = 0;
     }
 
-    const dvx = data.groundAcc * dt * this.accelerationInput.x * this.boost;
-    const dvz = data.groundAcc * dt * -this.accelerationInput.z * this.boost;
+    const dvx = data.groundAcc * dt * this.accelerationInput.x;
+    const dvz = data.groundAcc * dt * -this.accelerationInput.z;
     velocity.x += dvx;
     velocity.z += dvz;
+
+    const decay = 0.7;
+    this.accelerationInput.x = this.accelerationInput.x * decay;
+    this.accelerationInput.z = this.accelerationInput.z * decay;
   }
 });
diff --git a/src/components/haptic-feedback.js b/src/components/haptic-feedback.js
new file mode 100644
index 0000000000000000000000000000000000000000..1ef413d38939a183c25438694cebaec934d9cf23
--- /dev/null
+++ b/src/components/haptic-feedback.js
@@ -0,0 +1,57 @@
+const strengthForIntensity = {
+  low: 0.07,
+  medium: 0.2,
+  high: 1
+};
+
+AFRAME.registerComponent("haptic-feedback", {
+  schema: {
+    hapticEventName: { default: "haptic_pulse" }
+  },
+
+  init: function() {
+    this.pulse = this.pulse.bind(this);
+    this.getActuator = this.getActuator.bind(this);
+    this.getActuator().then(actuator => {
+      this.actuator = actuator;
+    });
+  },
+
+  getActuator() {
+    return new Promise((resolve, reject) => {
+      const tryGetActivator = () => {
+        var trackedControls = this.el.components["tracked-controls"];
+        if (
+          trackedControls &&
+          trackedControls.controller &&
+          trackedControls.controller.hapticActuators &&
+          trackedControls.controller.hapticActuators.length
+        ) {
+          resolve(trackedControls.controller.hapticActuators[0]);
+        } else {
+          setTimeout(tryGetActivator, 1000);
+        }
+      };
+      setTimeout(tryGetActivator, 1000);
+    });
+  },
+
+  play: function() {
+    this.el.addEventListener(this.data.hapticEventName, this.pulse);
+  },
+  pause: function() {
+    this.el.removeEventListener(this.data.hapticEventName, this.pulse);
+  },
+
+  pulse: function(event) {
+    let { intensity } = event.detail;
+    if (!strengthForIntensity[intensity]) {
+      console.warn(`Invalid intensity : ${intensity}`);
+      return;
+    }
+
+    if (this.actuator) {
+      this.actuator.pulse(strengthForIntensity[intensity], 15);
+    }
+  }
+});
diff --git a/src/components/split-axis-events.js b/src/components/split-axis-events.js
deleted file mode 100644
index 291b91ff6ebcf4add84004bc49002e936423b2df..0000000000000000000000000000000000000000
--- a/src/components/split-axis-events.js
+++ /dev/null
@@ -1,30 +0,0 @@
-AFRAME.registerComponent("split-axis-events", {
-  init: function() {
-    this.pressed = false;
-    this.onAxisMove = this.onAxisMove.bind(this);
-    this.onButtonChanged = this.onButtonChanged.bind(this);
-  },
-
-  play: function() {
-    this.el.addEventListener("axismove", this.onAxisMove);
-    this.el.addEventListener("buttonchanged", this.onButtonChanged);
-  },
-
-  pause: function() {
-    this.el.removeEventListener("axismove", this.onAxisMove);
-    this.el.removeEventListener("buttonchanged", this.onButtonChanged);
-  },
-
-  onAxisMove: function(event) {
-    var name = "touchpad" + (this.pressed ? "pressed" : "") + "axismove";
-    this.el.emit(name + "x", event.detail.axis[0]);
-    this.el.emit(name + "y", event.detail.axis[1]);
-  },
-
-  onButtonChanged: function(event) {
-    if (this.pressed && !event.detail.state.pressed) {
-      this.el.emit("touchpadbuttonup");
-    }
-    this.pressed = event.detail.state.pressed;
-  }
-});
diff --git a/src/components/virtual-gamepad-controls.js b/src/components/virtual-gamepad-controls.js
index 21ecac8e22386e2a0bf6543a18903c959fb89891..d399e38b29765280a677bb23289cdc3e4c2aff6b 100644
--- a/src/components/virtual-gamepad-controls.js
+++ b/src/components/virtual-gamepad-controls.js
@@ -57,11 +57,9 @@ AFRAME.registerComponent("virtual-gamepad-controls", {
         const force = joystick.force < 1 ? joystick.force : 1;
         const x = Math.cos(angle) * force;
         const z = Math.sin(angle) * force;
-        this.el.sceneEl.emit("translateX", x);
-        this.el.sceneEl.emit("translateZ", z);
+        this.el.sceneEl.emit("move", { axis: [x, z] });
       } else {
-        this.el.sceneEl.emit("translateX", 0);
-        this.el.sceneEl.emit("translateZ", 0);
+        this.el.sceneEl.emit("move", { axis: [0, 0] });
       }
     } else {
       if (event.type === "move") {
@@ -69,10 +67,10 @@ AFRAME.registerComponent("virtual-gamepad-controls", {
         const angle = joystick.angle.radian;
         const force = joystick.force < 1 ? joystick.force : 1;
         this.yaw = Math.cos(angle) * force;
-        this.el.sceneEl.emit("rotateY", this.yaw);
+        this.el.sceneEl.emit("rotateY", { value: this.yaw });
       } else {
         this.yaw = 0;
-        this.el.sceneEl.emit("rotateY", this.yaw);
+        this.el.sceneEl.emit("rotateY", { value: this.yaw });
       }
     }
   },
diff --git a/src/components/wasd-to-analog2d.js b/src/components/wasd-to-analog2d.js
new file mode 100644
index 0000000000000000000000000000000000000000..1b83f8f63a2ef969300ef1c91a0814f04a1f0ed8
--- /dev/null
+++ b/src/components/wasd-to-analog2d.js
@@ -0,0 +1,100 @@
+AFRAME.registerComponent("wasd-to-analog2d", {
+  schema: {
+    analog2dOutputAction: { default: "wasd_analog2d" }
+  },
+
+  init: function() {
+    this.output = [0, 0];
+    this.vectors = {
+      w: [0, 1],
+      a: [-1, 0],
+      s: [0, -1],
+      d: [1, 0]
+    };
+    this.onWasd = this.onWasd.bind(this);
+    this.keys = {};
+    this.move = this.move.bind(this);
+  },
+
+  play: function() {
+    const eventNames = [
+      "w_down",
+      "w_up",
+      "a_down",
+      "a_up",
+      "s_down",
+      "s_up",
+      "d_down",
+      "d_up"
+    ];
+    for (var name of eventNames) {
+      this.el.sceneEl.addEventListener(name, this.onWasd);
+    }
+    // I listen to events that this component generates instead of emitting "move"
+    // directly because ideally this would live as an input mapping, but the events
+    // generated by this component won't actually get mapped.
+    this.el.sceneEl.addEventListener(this.data.analog2dOutputAction, this.move);
+  },
+
+  move: function(event) {
+    this.el.emit("move", { axis: event.detail.axis });
+  },
+
+  pause: function() {
+    this.el.sceneEl.removeEventListener("wasd", this.onWasd);
+    this.el.sceneEl.removeEventListener(
+      this.data.analog2dOutputAction,
+      this.move
+    );
+  },
+
+  onWasd: function(event) {
+    const keyEvent = event.type;
+    const down = keyEvent.indexOf("down") !== -1;
+    const key = keyEvent[0].toLowerCase();
+    this.keys[key] = down;
+  },
+
+  tick: function(t, dt) {
+    this.target = [0, 0];
+
+    for (var key in this.keys) {
+      if (this.keys[key] && this.vectors[key]) {
+        this.target = [
+          this.target[0] + this.vectors[key][0],
+          this.target[1] + this.vectors[key][1]
+        ];
+      }
+    }
+
+    const targetMagnitude = Math.sqrt(
+      this.target[0] * this.target[0] + this.target[1] * this.target[1]
+    );
+    if (targetMagnitude !== 0) {
+      this.target[0] = this.target[0] / targetMagnitude;
+      this.target[1] = this.target[1] / targetMagnitude;
+    }
+
+    const epsilon = 0.01;
+    if (
+      Math.abs(this.output[0]) < epsilon &&
+      Math.abs(this.output[1]) < epsilon &&
+      this.target[0] === 0 &&
+      this.target[1] === 0
+    ) {
+      return; // Staying at [0,0] doesn't require new events.
+    }
+
+    const easeInSpeed = 0.25;
+    this.output = [
+      this.output[0] + easeInSpeed * (this.target[0] - this.output[0]),
+      this.output[1] + easeInSpeed * (this.target[1] - this.output[1])
+    ];
+
+    if (this.output !== [0, 0]) {
+      this.el.emit(this.data.analog2dOutputAction, {
+        axis: this.output
+      });
+    }
+  }
+});
diff --git a/src/input-mappings.js b/src/input-mappings.js
index d57ff6496b1d35bf0a435b06cfdb74dd807008e6..272e3889ada96d6638d27544d3ea27ec379e5395 100644
--- a/src/input-mappings.js
+++ b/src/input-mappings.js
@@ -1,47 +1,94 @@
-export default function registerInputMappings() {
-  AFRAME.registerInputMappings({
-    mappings: {
-      default: {
-        common: {
-          // @TODO these dpad events are emmited by an axis-dpad component. This should probalby move into either tracked-controller or input-mapping
-          dpadleftdown: "action_snap_rotate_left",
-          dpadrightdown: "action_snap_rotate_right",
-          dpadcenterdown: "action_teleport_down", // @TODO once once #30 lands in aframe-teleport controls this just maps to "action_teleport_aim"
-          dpadcenterup: "action_teleport_up", // @TODO once once #30 lands in aframe-teleport controls this just maps to "action_teleport_teleport"
-          touchpadpressedaxismovex: "translateX",
-          touchpadpressedaxismovey: "translateZ",
-          touchpadbuttonup: "stop_moving"
-        },
-        "vive-controls": {
-          menudown: "action_mute"
-        },
-        "oculus-touch-controls": {
-          xbuttondown: "action_mute",
-          gripdown: "middle_ring_pinky_down",
-          gripup: "middle_ring_pinky_up",
-          thumbsticktouchstart: "thumb_down",
-          thumbsticktouchend: "thumb_up",
-          triggerdown: "index_down",
-          triggerup: "index_up"
+const inGameActions = {
+  // Define action sets here.
+  // An action set separates "driving" controls from "menu" controls.
+  // Only one action set is active at a time.
+  default: {
+    move: { label: "Move" },
+    snap_rotate_left: { label: "Snap Rotate Left" },
+    snap_rotate_right: { label: "Snap Rotate Right" },
+    action_mute: { label: "Mute" },
+    action_teleport_down: { label: "Teleport Aim" },
+    action_teleport_up: { label: "Teleport" },
+    action_share_screen: { label: "Share Screen" }
+  }
+};
+
+const config = {
+  behaviours: {
+    default: {
+      "oculus-touch-controls": {
+        joystick: "oculus_touch_joystick_dpad4"
+      },
+      "vive-controls": {
+        trackpad: "vive_trackpad_dpad4"
+      }
+    }
+  },
+  mappings: {
+    default: {
+      "vive-controls": {
+        menudown: "action_mute",
+        "trackpad.pressedmove": { left: "move" },
+        trackpad_dpad4_pressed_west_down: { right: "snap_rotate_left" },
+        trackpad_dpad4_pressed_east_down: { right: "snap_rotate_right" },
+        trackpad_dpad4_pressed_center_down: { right: "action_teleport_down" },
+        trackpadup: { right: "action_teleport_up" }
+      },
+      "oculus-touch-controls": {
+        joystick_dpad4_west: {
+          right: "snap_rotate_left"
         },
-        daydream: {
-          menudown: "action_mute"
+        joystick_dpad4_east: {
+          right: "snap_rotate_right"
         },
-        keyboard: {
-          m_press: "action_mute",
-          q_press: "action_snap_rotate_left",
-          e_press: "action_snap_rotate_right",
-          v_press: "action_share_screen",
-          w_down: "action_move_forward",
-          w_up: "action_dont_move_forward",
-          a_down: "action_move_left",
-          a_up: "action_dont_move_left",
-          s_down: "action_move_backward",
-          s_up: "action_dont_move_backward",
-          d_down: "action_move_right",
-          d_up: "action_dont_move_right"
-        }
+        xbuttondown: "action_mute",
+        gripdown: "middle_ring_pinky_down",
+        gripup: "middle_ring_pinky_up",
+        thumbsticktouchstart: "thumb_down",
+        thumbsticktouchend: "thumb_up",
+        // @TODO: How do I map more than one action to triggerdown?
+        //        triggerdown: "index_down",
+        //        triggerup: "index_up",
+        triggerdown: "action_teleport_down",
+        triggerup: "action_teleport_up",
+        "axismove.reverseY": { left: "move" },
+        right_dpad_east: "snap_rotate_right",
+        right_dpad_west: "snap_rotate_left",
+        abuttondown: "action_teleport_down",
+        abuttonup: "action_teleport_up"
+      },
+      "daydream-controls": {
+        menudown: "action_mute",
+        trackpaddown: "action_teleport_down",
+        trackpadup: "action_teleport_up"
+      },
+      keyboard: {
+        m_press: "action_mute",
+        q_press: "snap_rotate_left",
+        e_press: "snap_rotate_right",
+        v_press: "action_share_screen",
+
+        // We can't create a keyboard behaviour with AFIM yet,
+        // so these will get captured by wasd-to-analog2d
+        w_down: "w_down",
+        w_up: "w_up",
+        a_down: "a_down",
+        a_up: "a_up",
+        s_down: "s_down",
+        s_up: "s_up",
+        d_down: "d_down",
+        d_up: "d_up",
+        W_down: "w_down",
+        W_up: "w_up",
+        A_down: "a_down",
+        A_up: "a_up",
+        S_down: "s_down",
+        S_up: "s_up",
+        D_down: "d_down",
+        D_up: "d_up"
       }
     }
-  });
-}
+  }
+};
+
+export { inGameActions, config };
diff --git a/src/room.js b/src/room.js
index b79902452663c26a2c8b16e611f697ee67269107..4124d5fd7f4719ccbe8fc99c0817621d2a879aa9 100644
--- a/src/room.js
+++ b/src/room.js
@@ -10,7 +10,13 @@ import "aframe-input-mapping-component";
 import animationMixer from "aframe-extras/src/loaders/animation-mixer";
 AFRAME.registerComponent("animation-mixer", animationMixer);
 
-import "./components/axis-dpad";
+import { vive_trackpad_dpad4 } from "./behaviours/vive-trackpad-dpad4";
+import { oculus_touch_joystick_dpad4 } from "./behaviours/oculus-touch-joystick-dpad4";
+import { PressedMove } from "./activators/pressedmove";
+import { ReverseY } from "./activators/reversey";
+import "./activators/shortpress";
+import "./components/wasd-to-analog2d"; //Might be a behaviour or activator in the future
+
 import "./components/mute-mic";
 import "./components/audio-feedback";
 import "./components/nametag-transform";
@@ -19,7 +25,7 @@ import "./components/virtual-gamepad-controls";
 import "./components/body-controller";
 import "./components/hand-controls2";
 import "./components/character-controller";
-import "./components/split-axis-events";
+import "./components/haptic-feedback";
 import "./components/networked-video-player";
 import "./components/offset-relative-to";
 import "./components/cached-gltf-model";
@@ -29,12 +35,21 @@ import "./components/layers";
 import "./components/spawn-controller";
 import "./systems/personal-space-bubble";
 
-import registerNetworkScheams from "./network-schemas";
-import registerInputMappings from "./input-mappings";
 import { promptForName, getCookie, parseJwt } from "./utils";
-
-registerNetworkScheams();
-registerInputMappings();
+import registerNetworkSchemas from "./network-schemas";
+import { inGameActions, config } from "./input-mappings";
+
+AFRAME.registerInputBehaviour("vive_trackpad_dpad4", vive_trackpad_dpad4);
+AFRAME.registerInputBehaviour(
+  "oculus_touch_joystick_dpad4",
+  oculus_touch_joystick_dpad4
+);
+AFRAME.registerInputActivator("pressedmove", PressedMove);
+AFRAME.registerInputActivator("reverseY", ReverseY);
+AFRAME.registerInputActions(inGameActions, "default");
+AFRAME.registerInputMappings(config);
+
+registerNetworkSchemas();
 
 function shareScreen() {
   const track = NAF.connection.adapter.localMediaStream.getVideoTracks()[0];
diff --git a/src/utils.js b/src/utils.js
index b37764120a388120c89392fb6b9b97f31dd59578..346f0994cf16943b7461a34e778cca16c87eebfc 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -167,9 +167,8 @@ export function generateName() {
 }
 
 export function promptForName(username) {
-  if (!username)
-    username = generateName();
-  
+  if (!username) username = generateName();
+
   do {
     username = prompt("Choose a username", username);
   } while (!(username && username.length));
@@ -179,11 +178,46 @@ export function promptForName(username) {
 export function getCookie(name) {
   var value = "; " + document.cookie;
   var parts = value.split("; " + name + "=");
-  if (parts.length == 2) return parts.pop().split(";").shift();
+  if (parts.length == 2)
+    return parts
+      .pop()
+      .split(";")
+      .shift();
 }
 
-export function parseJwt (token) {
-  var base64Url = token.split('.')[1];
-  var base64 = base64Url.replace('-', '+').replace('_', '/');
+export function parseJwt(token) {
+  var base64Url = token.split(".")[1];
+  var base64 = base64Url.replace("-", "+").replace("_", "/");
   return JSON.parse(window.atob(base64));
 }
+
+export function angleTo4Direction(angle) {
+  angle = (angle * THREE.Math.RAD2DEG + 180 + 45) % 360;
+  if (angle > 0 && angle < 90) {
+    return "north";
+  } else if (angle >= 90 && angle < 180) {
+    return "west";
+  } else if (angle >= 180 && angle < 270) {
+    return "south";
+  } else {
+    return "east";
+  }
+}
+
+export function angleTo8Direction(angle) {
+  angle = (angle * THREE.Math.RAD2DEG + 180 + 45) % 360;
+  var direction = "";
+  if ((angle >= 0 && angle < 120) || angle >= 330) {
+    direction += "north";
+  }
+  if (angle >= 150 && angle < 300) {
+    direction += "south";
+  }
+  if (angle >= 60 && angle < 210) {
+    direction += "west";
+  }
+  if ((angle >= 240 && angle < 360) || angle < 30) {
+    direction += "east";
+  }
+  return direction;
+}
diff --git a/templates/room.hbs b/templates/room.hbs
index 0e90903456c0dda97f644339b5cc3eea899c90c1..5a86966219e6648d0076302ae17d5ed28538bfca 100644
--- a/templates/room.hbs
+++ b/templates/room.hbs
@@ -14,7 +14,7 @@
     <script src="{{asset "manifest.js" }}"></script>
     <script src="{{asset "room-vendor.js" }}"></script>
     <script src="{{asset "room.js" }}"></script>
-    
+    <meta charset="UTF-8">
     <style>
         .a-enter-vr {
             top: 90px;
@@ -123,6 +123,7 @@
             id="player-rig"
             networked
             spawn-controller="radius: 4;"
+            wasd-to-analog2d
             character-controller="pivot: #head"
         >
             <a-entity
@@ -130,7 +131,7 @@
                 camera="userHeight: 1.6"
                 personal-space-bubble
                 look-controls
-                networked="template: #head-template; showLocalTemplate: false;" 
+                networked="template: #head-template; showLocalTemplate: false;"
             ></a-entity>
 
             <a-entity
@@ -138,7 +139,6 @@
                 body-controller="eyeNeckOffset: 0 -0.11 0.09; neckHeight: 0.05"
                 networked="template: #body-template;"
             ></a-entity>
-             
             <a-entity
                 id="nametag"
                 networked="template: #nametag-template; showLocalTemplate: false;"
@@ -146,9 +146,9 @@
 
             <a-entity
                 id="left-hand"
-                split-axis-events
                 hand-controls2="left"
-                axis-dpad="centerZone: 1"
+                tracked-controls
+                haptic-feedback
                 teleport-controls="cameraRig: #player-rig; teleportOrigin: #head; button: action_teleport_"
                 networked="template: #left-hand-template;"
             >
@@ -165,7 +165,7 @@
             <a-entity
                 id="right-hand"
                 hand-controls2="right"
-                axis-dpad
+                haptic-feedback
                 teleport-controls="cameraRig: #player-rig;
                                     teleportOrigin: #head;
                                     hitEntity: #telepor-indicator;
diff --git a/yarn.lock b/yarn.lock
index b7dcd3fb103518f93c53d7dec1ac1fa0781aa98b..f80cf7ea4a2386dfa33d9e906fd203d28d923787 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -49,15 +49,15 @@ acorn@^5.0.0, acorn@^5.2.1:
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7"
 
 aframe-extras@^3.12.4:
-  version "3.12.4"
-  resolved "https://registry.yarnpkg.com/aframe-extras/-/aframe-extras-3.12.4.tgz#9276bde8b51a07a9822bbce1fc55f2eb8e6810dc"
+  version "3.13.1"
+  resolved "https://registry.yarnpkg.com/aframe-extras/-/aframe-extras-3.13.1.tgz#f8b6ef18c29e92538d05d94913640942a307c46c"
   dependencies:
     aframe-physics-system "^1.4.3"
     three-pathfinding "^0.2.2"
 
-"aframe-input-mapping-component@https://github.com/fernandojsg/aframe-input-mapping-component#6ebc38f":
-  version "0.1.1"
-  resolved "https://github.com/fernandojsg/aframe-input-mapping-component#6ebc38f0e871e8ab66673aef5cd11f6ce052076c"
+"aframe-input-mapping-component@https://github.com/johnshaughnessy/aframe-input-mapping-component#23e2855":
+  version "0.1.2"
+  resolved "https://github.com/johnshaughnessy/aframe-input-mapping-component#23e28559046a56e9606fd6cdb64cf8c2dfbd12ec"
 
 aframe-lerp-component@^1.1.0:
   version "1.1.0"
@@ -117,8 +117,8 @@ ajv@^4.9.1:
     json-stable-stringify "^1.0.1"
 
 ajv@^5.0.0, ajv@^5.1.5, ajv@^5.2.3, ajv@^5.3.0:
-  version "5.3.0"
-  resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.3.0.tgz#4414ff74a50879c208ee5fdc826e32c303549eda"
+  version "5.5.1"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.1.tgz#b38bb8876d9e86bee994956a04e721e88b248eb2"
   dependencies:
     co "^4.6.0"
     fast-deep-equal "^1.0.0"
@@ -934,8 +934,8 @@ big.js@^3.1.3:
   resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
 
 binary-extensions@^1.0.0:
-  version "1.10.0"
-  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0"
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
 
 blob@0.0.4:
   version "0.0.4"
@@ -1077,11 +1077,11 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
     electron-to-chromium "^1.2.7"
 
 browserslist@^2.1.2:
-  version "2.9.0"
-  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.9.0.tgz#706aca15c53be15610f466e348cbfa0c00a6a379"
+  version "2.10.0"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.10.0.tgz#bac5ee1cc69ca9d96403ffb8a3abdc5b6aed6346"
   dependencies:
-    caniuse-lite "^1.0.30000760"
-    electron-to-chromium "^1.3.27"
+    caniuse-lite "^1.0.30000780"
+    electron-to-chromium "^1.3.28"
 
 buffer-equal@0.0.1:
   version "0.0.1"
@@ -1186,12 +1186,12 @@ caniuse-api@^1.5.2:
     lodash.uniq "^4.5.0"
 
 caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
-  version "1.0.30000766"
-  resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000766.tgz#4c911aa3747f01388452fa4b927b78fcf1430680"
+  version "1.0.30000780"
+  resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000780.tgz#8d1977561d00ff0f0ed2b6b66140328ab4504c0a"
 
-caniuse-lite@^1.0.30000760:
-  version "1.0.30000766"
-  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000766.tgz#8a095cc5eb9923c27008ce4d0db23e65a3e28843"
+caniuse-lite@^1.0.30000780:
+  version "1.0.30000780"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000780.tgz#1f9095f2efd4940e0ba6c5992ab7a9b64cc35ba4"
 
 "cannon@github:donmccurdy/cannon.js#v0.6.2-dev1":
   version "0.6.2"
@@ -1227,8 +1227,8 @@ chalk@^2.0.0, chalk@^2.1.0, chalk@^2.3.0:
     supports-color "^4.0.0"
 
 chardet@^0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.0.tgz#0bbe1355ac44d7a3ed4a925707c4ef70f8190f6c"
+  version "0.4.2"
+  resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
 
 chokidar@^1.6.0, chokidar@^1.7.0:
   version "1.7.0"
@@ -1362,11 +1362,7 @@ commander@2.2.x:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.2.0.tgz#175ad4b9317f3ff615f201c1e57224f55a3e91df"
 
-commander@^2.5.0:
-  version "2.11.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563"
-
-commander@~2.12.1:
+commander@^2.5.0, commander@~2.12.1:
   version "2.12.2"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555"
 
@@ -1469,8 +1465,8 @@ content-type@~1.0.4:
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
 
 convert-source-map@^1.5.0:
-  version "1.5.0"
-  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5"
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
 
 cookie-signature@1.0.6:
   version "1.0.6"
@@ -1835,18 +1831,18 @@ detect-indent@^4.0.0:
     repeating "^2.0.0"
 
 detect-libc@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.2.tgz#71ad5d204bf17a6a6ca8f450c61454066ef461e1"
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
 
 detect-node@^2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127"
 
 detective@^4.3.1:
-  version "4.5.0"
-  resolved "https://registry.yarnpkg.com/detective/-/detective-4.5.0.tgz#6e5a8c6b26e6c7a254b1c6b6d7490d98ec91edd1"
+  version "4.7.0"
+  resolved "https://registry.yarnpkg.com/detective/-/detective-4.7.0.tgz#6276e150f9e50829ad1f90ace4d9a2304188afcf"
   dependencies:
-    acorn "^4.0.3"
+    acorn "^5.2.1"
     defined "^1.0.0"
 
 diffie-hellman@^5.0.0:
@@ -1874,12 +1870,11 @@ dns-txt@^2.0.2:
   dependencies:
     buffer-indexof "^1.0.0"
 
-doctrine@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63"
+doctrine@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.2.tgz#68f96ce8efc56cc42651f1faadb4f175273b0075"
   dependencies:
     esutils "^2.0.2"
-    isarray "^1.0.0"
 
 document-register-element@dmarcos/document-register-element#8ccc532b7:
   version "0.5.4"
@@ -1932,9 +1927,9 @@ ee-first@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
 
-electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.27:
-  version "1.3.27"
-  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.27.tgz#78ecb8a399066187bb374eede35d9c70565a803d"
+electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.28:
+  version "1.3.28"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.28.tgz#8dd4e6458086644e9f9f0a1cf32e2a1f9dffd9ee"
 
 elliptic@^6.0.0:
   version "6.4.0"
@@ -2036,8 +2031,8 @@ error-ex@^1.2.0:
     is-arrayish "^0.2.1"
 
 es-abstract@^1.7.0:
-  version "1.9.0"
-  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.9.0.tgz#690829a07cae36b222e7fd9b75c0d0573eb25227"
+  version "1.10.0"
+  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864"
   dependencies:
     es-to-primitive "^1.1.1"
     function-bind "^1.1.1"
@@ -2054,8 +2049,8 @@ es-to-primitive@^1.1.1:
     is-symbol "^1.0.1"
 
 es5-ext@^0.10.12, es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14:
-  version "0.10.35"
-  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.35.tgz#18ee858ce6a3c45c7d79e91c15fcca9ec568494f"
+  version "0.10.37"
+  resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.37.tgz#0ee741d148b80069ba27d020393756af257defc3"
   dependencies:
     es6-iterator "~2.0.1"
     es6-symbol "~3.1.1"
@@ -2130,8 +2125,8 @@ escope@^3.6.0:
     estraverse "^4.1.1"
 
 eslint-config-prettier@^2.6.0:
-  version "2.7.0"
-  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-2.7.0.tgz#7bbfef66ad783277836f4ea556e68b9bcc9da4d0"
+  version "2.9.0"
+  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-2.9.0.tgz#5ecd65174d486c22dff389fe036febf502d468a3"
   dependencies:
     get-stdin "^5.0.1"
 
@@ -2150,8 +2145,8 @@ eslint-scope@^3.7.1:
     estraverse "^4.1.1"
 
 eslint@^4.10.0:
-  version "4.11.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.11.0.tgz#39a8c82bc0a3783adf5a39fa27fdd9d36fac9a34"
+  version "4.12.1"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.12.1.tgz#5ec1973822b4a066b353770c3c6d69a2a188e880"
   dependencies:
     ajv "^5.3.0"
     babel-code-frame "^6.22.0"
@@ -2159,7 +2154,7 @@ eslint@^4.10.0:
     concat-stream "^1.6.0"
     cross-spawn "^5.1.0"
     debug "^3.0.1"
-    doctrine "^2.0.0"
+    doctrine "^2.0.2"
     eslint-scope "^3.7.1"
     espree "^3.5.2"
     esquery "^1.0.0"
@@ -2168,7 +2163,7 @@ eslint@^4.10.0:
     file-entry-cache "^2.0.0"
     functional-red-black-tree "^1.0.1"
     glob "^7.1.2"
-    globals "^9.17.0"
+    globals "^11.0.1"
     ignore "^3.3.3"
     imurmurhash "^0.1.4"
     inquirer "^3.0.6"
@@ -2304,7 +2299,7 @@ expand-tilde@^1.2.2:
   dependencies:
     os-homedir "^1.0.1"
 
-express@^4.10.7, express@^4.13.3:
+express@^4.10.7, express@^4.16.2:
   version "4.16.2"
   resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c"
   dependencies:
@@ -2366,10 +2361,14 @@ extract-text-webpack-plugin@^3.0.2:
     schema-utils "^0.3.0"
     webpack-sources "^1.0.1"
 
-extsprintf@1.3.0, extsprintf@^1.2.0:
+extsprintf@1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
 
+extsprintf@^1.2.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+
 fast-deep-equal@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
@@ -2594,15 +2593,7 @@ fs-exists-sync@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add"
 
-fs-extra@^4.0.2:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.2.tgz#f91704c53d1b461f893452b0c307d9997647ab6b"
-  dependencies:
-    graceful-fs "^4.1.2"
-    jsonfile "^4.0.0"
-    universalify "^0.1.0"
-
-fs-extra@^4.0.3:
+fs-extra@^4.0.2, fs-extra@^4.0.3:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94"
   dependencies:
@@ -2770,7 +2761,11 @@ global@~4.3.0:
     min-document "^2.19.0"
     process "~0.5.1"
 
-globals@^9.17.0, globals@^9.18.0:
+globals@^11.0.1:
+  version "11.0.1"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-11.0.1.tgz#12a87bb010e5154396acc535e1e43fc753b0e5e8"
+
+globals@^9.18.0:
   version "9.18.0"
   resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
 
@@ -3045,8 +3040,8 @@ inherits@2.0.1:
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
 
 ini@^1.3.4, ini@~1.3.0:
-  version "1.3.4"
-  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e"
+  version "1.3.5"
+  resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
 
 inquirer@^3.0.6:
   version "3.3.0"
@@ -3074,8 +3069,8 @@ internal-ip@1.2.0:
     meow "^3.3.0"
 
 interpret@^1.0.0:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.4.tgz#820cdd588b868ffb191a809506d6c9c8f212b1b0"
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
 
 invariant@^2.2.2:
   version "2.2.2"
@@ -3208,8 +3203,8 @@ is-path-in-cwd@^1.0.0:
     is-path-inside "^1.0.0"
 
 is-path-inside@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f"
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
   dependencies:
     path-is-inside "^1.0.1"
 
@@ -3309,8 +3304,8 @@ jest-docblock@^21.0.0:
   resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-21.2.0.tgz#51529c3b30d5fd159da60c27ceedc195faf8d414"
 
 js-base64@^2.1.9:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf"
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.0.tgz#9e566fee624751a1d720c966cd6226d29d4025aa"
 
 js-tokens@^3.0.0, js-tokens@^3.0.2:
   version "3.0.2"
@@ -3660,8 +3655,8 @@ miller-rabin@^4.0.0:
     brorand "^1.0.1"
 
 "mime-db@>= 1.30.0 < 2":
-  version "1.31.0"
-  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.31.0.tgz#a49cd8f3ebf3ed1a482b60561d9105ad40ca74cb"
+  version "1.32.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.32.0.tgz#485b3848b01a3cda5f968b4882c0771e58e09414"
 
 mime-db@~1.30.0:
   version "1.30.0"
@@ -3673,10 +3668,14 @@ mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.16,
   dependencies:
     mime-db "~1.30.0"
 
-mime@1.4.1, mime@^1.3.4:
+mime@1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
 
+mime@^1.3.4, mime@^1.5.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+
 mimic-fn@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
@@ -3773,8 +3772,8 @@ multicast-dns-service-types@^1.1.0:
   resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901"
 
 multicast-dns@^6.0.1:
-  version "6.1.1"
-  resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.1.1.tgz#6e7de86a570872ab17058adea7160bbeca814dde"
+  version "6.2.1"
+  resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.1.tgz#c5035defa9219d30640558a49298067352098060"
   dependencies:
     dns-packet "^1.0.1"
     thunky "^0.1.0"
@@ -4565,8 +4564,8 @@ preserve@^0.2.0:
   resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
 
 prettier@^1.7.0:
-  version "1.8.2"
-  resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.8.2.tgz#bff83e7fd573933c607875e5ba3abbdffb96aeb8"
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.9.1.tgz#41638a0d47c1efbd1b7d5a742aaa5548eab86d70"
 
 private@^0.1.6, private@^0.1.7, private@~0.1.5:
   version "0.1.8"
@@ -4758,8 +4757,8 @@ rc@^1.1.7:
     strip-json-comments "~2.0.1"
 
 react-dom@^16.1.1:
-  version "16.1.1"
-  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.1.1.tgz#b2e331b6d752faf1a2d31399969399a41d8d45f8"
+  version "16.2.0"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.2.0.tgz#69003178601c0ca19b709b33a83369fe6124c044"
   dependencies:
     fbjs "^0.8.16"
     loose-envify "^1.1.0"
@@ -4767,8 +4766,8 @@ react-dom@^16.1.1:
     prop-types "^15.6.0"
 
 react@^16.1.1:
-  version "16.1.1"
-  resolved "https://registry.yarnpkg.com/react/-/react-16.1.1.tgz#d5c4ef795507e3012282dd51261ff9c0e824fe1f"
+  version "16.2.0"
+  resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"
   dependencies:
     fbjs "^0.8.16"
     loose-envify "^1.1.0"
@@ -4879,8 +4878,8 @@ regenerate@^1.2.1:
   resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f"
 
 regenerator-runtime@^0.11.0:
-  version "0.11.0"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1"
+  version "0.11.1"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
 
 regenerator-transform@^0.10.0:
   version "0.10.1"
@@ -5911,18 +5910,18 @@ wbuf@^1.1.0, wbuf@^1.7.2:
     minimalistic-assert "^1.0.0"
 
 webpack-dev-middleware@^1.11.0:
-  version "1.12.0"
-  resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.0.tgz#d34efefb2edda7e1d3b5dbe07289513219651709"
+  version "1.12.2"
+  resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz#f8fc1120ce3b4fc5680ceecb43d777966b21105e"
   dependencies:
     memory-fs "~0.4.1"
-    mime "^1.3.4"
+    mime "^1.5.0"
     path-is-absolute "^1.0.0"
     range-parser "^1.0.3"
     time-stamp "^2.0.0"
 
 webpack-dev-server@^2.9.3:
-  version "2.9.4"
-  resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.9.4.tgz#7883e61759c6a4b33e9b19ec4037bd4ab61428d1"
+  version "2.9.7"
+  resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.9.7.tgz#100ad6a14775478924d417ca6dcfb9d52a98faed"
   dependencies:
     ansi-html "0.0.7"
     array-includes "^3.0.3"
@@ -5932,7 +5931,7 @@ webpack-dev-server@^2.9.3:
     connect-history-api-fallback "^1.3.0"
     debug "^3.1.0"
     del "^3.0.0"
-    express "^4.13.3"
+    express "^4.16.2"
     html-entities "^1.2.0"
     http-proxy-middleware "~0.17.4"
     import-local "^0.1.1"
@@ -5966,8 +5965,8 @@ webpack-sources@1.0.1, webpack-sources@^1.0.1:
     source-map "~0.5.3"
 
 webpack@^3.6.0:
-  version "3.8.1"
-  resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.8.1.tgz#b16968a81100abe61608b0153c9159ef8bb2bd83"
+  version "3.10.0"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.10.0.tgz#5291b875078cf2abf42bdd23afe3f8f96c17d725"
   dependencies:
     acorn "^5.0.0"
     acorn-dynamic-import "^2.0.0"
@@ -5993,8 +5992,8 @@ webpack@^3.6.0:
     yargs "^8.0.2"
 
 webrtc-adapter@^6.0.2:
-  version "6.0.2"
-  resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-6.0.2.tgz#765f99c163e46046a758fec457f7859c90a19487"
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-6.0.3.tgz#850ab1649099922086c2c038c87c10c46d0c42cc"
   dependencies:
     rtcpeerconnection-shim "^1.1.13"
     sdp "^2.3.0"