diff --git a/package.json b/package.json
index 69b31a5a3854a7672fed750b1b0ecf0cbf6fa2d0..b3d4b301e5e9732b26a197efee373e863e722a7c 100644
--- a/package.json
+++ b/package.json
@@ -4,22 +4,19 @@
   "main": "src/index.js",
   "license": "MPL-2.0",
   "scripts": {
-    "dev": "webpack-dev-server --https --open --config webpack.dev.js",
+    "dev": "webpack-dev-server --https --host 0.0.0.0 --useLocalIp --open --config webpack.dev.js",
     "build": "webpack --config webpack.prod.js",
-    "prettier": "prettier --write src/**/*.js",
-    "deploy": "npm run build && now deploy --static ./public/"
+    "prettier": "prettier --write src/**/*.js"
   },
-  "pre-commit": [
-    "prettier"
-  ],
   "dependencies": {
+    "aframe-extras": "^3.12.4",
     "aframe": "0.7.0",
-    "aframe-input-mapping-component": "https://github.com/fernandojsg/aframe-input-mapping-component",
+    "aframe-input-mapping-component": "https://github.com/fernandojsg/aframe-input-mapping-component#6ebc38f",
     "aframe-teleport-controls": "https://github.com/netpro2k/aframe-teleport-controls#feature/teleport-origin",
-    "naf-janus-adapter": "^0.1.4",
+    "minijanus": "^0.1.6",
+    "naf-janus-adapter": "^0.1.5",
     "networked-aframe": "https://github.com/netpro2k/networked-aframe#bugfix/chrome/audio",
     "nipplejs": "^0.6.7",
-    "pleasejs": "^0.4.2",
     "query-string": "^5.0.1",
     "react": "^16.1.1",
     "react-dom": "^16.1.1"
@@ -34,8 +31,6 @@
     "eslint": "^4.10.0",
     "eslint-config-prettier": "^2.6.0",
     "eslint-plugin-prettier": "^2.3.1",
-    "now": "^8.3.11",
-    "pre-commit": "^1.2.2",
     "prettier": "^1.7.0",
     "style-loader": "^0.19.0",
     "webpack": "^3.6.0",
diff --git a/public/assets/avatars/Bot_Body_Mesh.glb b/public/assets/avatars/Bot_Body_Mesh.glb
new file mode 100644
index 0000000000000000000000000000000000000000..5b6ec544498ba30094dfb229de1fd4e93fff3da0
Binary files /dev/null and b/public/assets/avatars/Bot_Body_Mesh.glb differ
diff --git a/public/assets/avatars/Bot_Head_Mesh.glb b/public/assets/avatars/Bot_Head_Mesh.glb
new file mode 100644
index 0000000000000000000000000000000000000000..9d4ace94f63794cb1b54119c86d1632d139f2d9f
Binary files /dev/null and b/public/assets/avatars/Bot_Head_Mesh.glb differ
diff --git a/public/assets/avatars/Bot_LeftHand_Mesh.glb b/public/assets/avatars/Bot_LeftHand_Mesh.glb
new file mode 100644
index 0000000000000000000000000000000000000000..1ba4463d4aba8054a20fee420d82962daf071178
Binary files /dev/null and b/public/assets/avatars/Bot_LeftHand_Mesh.glb differ
diff --git a/public/assets/avatars/Bot_RightHand_Mesh.glb b/public/assets/avatars/Bot_RightHand_Mesh.glb
new file mode 100644
index 0000000000000000000000000000000000000000..bb73909d7ed484a241737c116126e278cb58274c
Binary files /dev/null and b/public/assets/avatars/Bot_RightHand_Mesh.glb differ
diff --git a/public/room.html b/public/room.html
index 2edb82aeb09039d5dc62f0001ee246001ecd336a..0b96981873dda6a51ecfb559275a31adbbc4a051 100644
--- a/public/room.html
+++ b/public/room.html
@@ -4,18 +4,18 @@
     <title>Mozilla Mixed Reality Social Client</title>
     <script src="./app.bundle.js"></script>
     <style>
-     .a-enter-vr {
-         top: 90px;
-         bottom: auto;
-     }
-     #loader {
-        position: fixed;
-        width: 100vw;
-        height: 100vh;
-        z-index: 10001;
-        background: #eaeaea no-repeat url(assets/loading.gif) center center;
-        opacity: 0.9;
-     }
+        .a-enter-vr {
+            top: 90px;
+            bottom: auto;
+        }
+        #loader {
+            position: fixed;
+            width: 100vw;
+            height: 100vh;
+            z-index: 10001;
+            background: #eaeaea no-repeat url(assets/loading.gif) center center;
+            opacity: 0.9;
+        }
     </style>
 </head>
 
@@ -33,7 +33,10 @@
             <img id="grid" src="assets/grid.png" crossorigin="anonymous" />
             <img id="sky" src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/sechelt.jpg" crossorigin="anonymous" />
 
-            <a-asset-item id="dodec-avatar-head" src="assets/avatars/dodec/DodecAvatarGLTF/DodecAvatar_Head.gltf"></a-asset-item>
+            <a-asset-item id="bot-head-mesh" src="assets/avatars/Bot_Head_Mesh.glb"></a-asset-item>
+            <a-asset-item id="bot-body-mesh" src="assets/avatars/Bot_Body_Mesh.glb"></a-asset-item>
+            <a-asset-item id="bot-left-hand-mesh" src="assets/avatars/Bot_LeftHand_Mesh.glb"></a-asset-item>
+            <a-asset-item id="bot-right-hand-mesh" src="assets/avatars/Bot_RightHand_Mesh.glb"></a-asset-item>
 
             <a-asset-item id="watch-model" src="assets/hud/watch.gltf"></a-asset-item>
 
@@ -43,18 +46,46 @@
             <script id="head-template" type="text/html">
                 <a-entity
                     class="head"
-                    gltf-model="#dodec-avatar-head"
+                    gltf-model="#bot-head-mesh"
                     networked-audio-source
                     networked-audio-analyser
-                    matcolor-audio-feedback="objectName: DodecAvatar_Head_0"
+                    matcolor-audio-feedback="objectName: Head_Mesh"
                     scale-audio-feedback
-                    avatar-customization
                     personal-space-invader
+                    rotation="0 180 0"
+                    animation-mixer
                 ></a-entity>
             </script>
 
-            <script id="hand-template" type="text/html">
-                <a-box class="hand" personal-space-invader scale="0.2 0.1 0.3"></a-box>
+            <script id="body-template" type="text/html">
+                <a-entity
+                    class="body"
+                    gltf-model="#bot-body-mesh"
+                    personal-space-invader
+                    rotation="0 180 0"
+                    position="0 -0.05 0"
+                ></a-entity>
+            </script>
+
+            <script id="left-hand-template" type="text/html">
+                <a-entity
+                    class="hand"
+                    gltf-model="#bot-left-hand-mesh"
+                    animation-mixer
+                    personal-space-invader
+                    rotation="-90 90 0"
+                    position="0 0 0.075"
+                ></a-entity>
+            </script>
+
+            <script id="right-hand-template" type="text/html">
+                <a-entity
+                    class="hand"
+                    gltf-model="#bot-right-hand-mesh"
+                    personal-space-invader
+                    rotation="-90 -90 0"
+                    position="0 0 0.075"
+                ></a-entity>
             </script>
 
             <script id="video-template" type="text/html">
@@ -65,33 +96,86 @@
                 <a-entity
                     class="nametag"
                     nametag-transform="follow: .head"
-                    text="side:double;align:center;color:#555"
+                    text="side: double; align: center; color: #555"
                     position="0 2.5 0"
-                    scale="6 6 6"></a-entity>
+                    scale="6 6 6"
+                ></a-entity>
             </script>
         </a-assets>
-        <a-entity id="player-rig" networked character-controller="pivot: #head">
+
+        <!-- Player Rig -->
+        <a-entity
+            id="player-rig"
+            networked
+            character-controller="pivot: #head"
+        >
             <a-sphere scale="0.1 0.1 0.1"></a-sphere>
-            <a-entity id="head" camera="userHeight: 1.6" personal-space-bubble look-controls networked="template:#head-template;showLocalTemplate:false;"></a-entity>
-            <a-entity id="nametag" networked="template:#nametag-template;showLocalTemplate:false;"></a-entity>
-
-            <a-entity id="left-hand" split-axis-events hand-controls="left" hand-controls-visibility axis-dpad="centerZone: 1" networked="template:#hand-template;showLocalTemplate:false;">
-                <a-entity id="watch" gltf-model="assets/hud/watch.gltf" position="0 0.0015 0.147" rotation="3.5 0 0">
-                    <a-circle mute-state-indicator scale-audio-feedback="analyserSrc: #head; minScale: 0.035; maxScale: 0.08;" position="0 0.023 0"
-                              rotation="-90 0 0" scale="0.04 0.04 0.04" material="color:#d8eece;shader:flat">
-                    </a-circle>
+
+            <a-entity
+                id="head"
+                camera="userHeight: 1.6"
+                personal-space-bubble
+                look-controls
+                networked="template: #head-template; showLocalTemplate: false;" 
+            ></a-entity>
+
+            <a-entity
+                id="body"
+                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;"
+            ></a-entity>
+
+            <a-entity
+                id="left-hand"
+                split-axis-events
+                hand-controls2="left"
+                axis-dpad="centerZone: 1"
+                teleport-controls="cameraRig: #player-rig; teleportOrigin: #head; button: action_teleport_"
+                networked="template: #left-hand-template;"
+            >
+                <a-entity
+                    id="watch"
+                    gltf-model="assets/hud/watch.gltf"
+                    position="0 0.0015 0.147"
+                    rotation="3.5 0 0"
+                >
+                    <a-circle
+                        mute-state-indicator
+                        scale-audio-feedback="analyserSrc: #head; minScale: 0.035; maxScale: 0.08;"
+                        position="0 0.023 0"
+                        rotation="-90 0 0"
+                        scale="0.04 0.04 0.04"
+                        material="color: #d8eece; shader: flat;"
+                    ></a-circle>
                 </a-entity>
             </a-entity>
 
-            <a-entity id="right-hand" hand-controls="right" hand-controls-child-visibility axis-dpad teleport-controls="cameraRig: #player-rig; teleportOrigin: #head; hitEntity: #telepor-indicator; button: action_teleport_;"
-                      networked="template:#hand-template;showLocalTemplate:false;">
-            </a-entity>
+            <a-entity
+                id="right-hand"
+                hand-controls2="right"
+                axis-dpad
+                teleport-controls="cameraRig: #player-rig;
+                                    teleportOrigin: #head;
+                                    hitEntity: #telepor-indicator;
+                                    button: action_teleport_;"
+                networked="template: #right-hand-template;"
+            ></a-entity>
         </a-entity>
 
-        <a-entity class="head" gltf-model="#rock-island" position="0 0 0">
+        <!-- Environment -->
+        <a-entity
+            gltf-model="#rock-island"
+            position="0 0 0"
+        >
             <a-sky color="#DDFFD9"></a-sky>
         </a-entity>
     </a-scene>
+
     <script>
         document.querySelector('a-scene').addEventListener('loaded', App.onSceneLoad)
     </script>
diff --git a/src/components/avatar-customization.js b/src/components/avatar-customization.js
deleted file mode 100644
index 8346b8e821ac904d8711dffe6563643f7628ba09..0000000000000000000000000000000000000000
--- a/src/components/avatar-customization.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import Please from "pleasejs";
-
-// @TODO this whole thing is a bit of a hack. This should probably just be setting some uniforms on a sharder.
-// @TODO the palate should be indexed by alpha channel instead of the red channel.
-// @TODO color should be multiplied with the texture value to allow for texture to provide surface detail.
-// @TODO texture should be regenerated whenever headColor/LidColor values change to allow for networking, though arguably these will eventually be fetched from a users "profile" anywya, so might not be worth trying to network through NAF.
-AFRAME.registerComponent("avatar-customization", {
-  schema: {
-    headColor: { type: "color", default: null },
-    lidColor: { type: "color", default: null }
-  },
-
-  init: function() {
-    const colors = Please.make_color({
-      format: "rgb-string",
-      colors_returned: 2
-    });
-
-    this.colorMap = new Map();
-    this.colorMap.set(128, this.data.headColor || new THREE.Color(colors[0]));
-    this.colorMap.set(115, this.data.lidColor || new THREE.Color(colors[1]));
-
-    this.el.addEventListener("model-loaded", () => {
-      const map = this.el.object3D.getObjectByName("DodecAvatar_Head_0")
-        .material.map;
-      const img = map.image;
-
-      const canvas = document.createElement("canvas");
-      canvas.width = img.width;
-      canvas.height = img.height;
-      const ctx = canvas.getContext("2d");
-      ctx.drawImage(img, 0, 0, img.width, img.height);
-      const imageData = ctx.getImageData(0, 0, img.width, img.height);
-      const pixelData = imageData.data;
-
-      for (let i = 0; i < pixelData.length; i += 4) {
-        // @TODO check alpha channel and multiply colors to preserver surface detail
-        if (this.colorMap.has(pixelData[i])) {
-          const color = this.colorMap.get(pixelData[i]);
-          pixelData[i] = color.r * 255;
-          pixelData[i + 1] = color.g * 255;
-          pixelData[i + 2] = color.b * 255;
-        }
-      }
-      ctx.putImageData(imageData, 0, 0);
-      map.image = canvas;
-    });
-  }
-});
diff --git a/src/components/body-controller.js b/src/components/body-controller.js
new file mode 100644
index 0000000000000000000000000000000000000000..05afec55951aff8ba8108bd6ee95e8212d9c8b95
--- /dev/null
+++ b/src/components/body-controller.js
@@ -0,0 +1,49 @@
+AFRAME.registerComponent("body-controller", {
+  schema: {
+    camera: { type: "selector", default: "[camera]" },
+    rotationSpeed: { default: 0.07 },
+    eyeNeckOffset: { type: "vec3" },
+    neckHeight: { type: "number" }
+  },
+
+  init() {
+    this.targetAngleEuler = new THREE.Euler();
+    this.targetAngle = new THREE.Quaternion();
+    this.cameraPositionMatrix = new THREE.Matrix4();
+    const offset = this.data.eyeNeckOffset;
+    this.eyeNeckTransformMatrix = new THREE.Matrix4().makeTranslation(
+      offset.x,
+      offset.y,
+      offset.z
+    );
+    this.neckTransformMatrix = new THREE.Matrix4().makeTranslation(
+      0,
+      this.data.neckHeight,
+      0
+    );
+    this.bodyPositionMatrix = new THREE.Matrix4();
+    this.bodyPositionVector = new THREE.Vector3();
+  },
+
+  tick(time, dt) {
+    const object3D = this.el.object3D;
+    const cameraObject3D = this.data.camera.object3D;
+
+    // Set Rotation
+    this.targetAngleEuler.set(0, cameraObject3D.rotation.y, 0);
+    this.targetAngle.setFromEuler(this.targetAngleEuler);
+    object3D.quaternion.slerp(this.targetAngle, this.data.rotationSpeed);
+    const object3DRotation = object3D.rotation;
+    this.el.setAttribute("rotation", {
+      x: object3DRotation.x * THREE.Math.RAD2DEG,
+      y: object3DRotation.y * THREE.Math.RAD2DEG,
+      z: object3DRotation.z * THREE.Math.RAD2DEG
+    });
+
+    //Set Position
+    this.bodyPositionMatrix.copy(cameraObject3D.matrix);
+    this.bodyPositionMatrix.multiply(this.eyeNeckTransformMatrix);
+    this.bodyPositionVector.setFromMatrixPosition(this.bodyPositionMatrix);
+    this.el.setAttribute("position", this.bodyPositionVector);
+  }
+});
diff --git a/src/components/hand-controls-visibility.js b/src/components/hand-controls-visibility.js
deleted file mode 100644
index 50d7514897b3974e3963c3fd636bea1a9e8d3aeb..0000000000000000000000000000000000000000
--- a/src/components/hand-controls-visibility.js
+++ /dev/null
@@ -1,21 +0,0 @@
-AFRAME.registerComponent("hand-controls-visibility", {
-  init() {
-    this.onControllerConnected = this.onControllerConnected.bind(this);
-    this.onControllerDisconnected = this.onControllerDisconnected.bind(this);
-    this.el.addEventListener("controllerconnected", this.onControllerConnected);
-    this.el.addEventListener(
-      "controllerdisconnected",
-      this.onControllerDisconnected
-    );
-
-    this.el.setAttribute("visible", false);
-  },
-
-  onControllerConnected() {
-    this.el.setAttribute("visible", true);
-  },
-
-  onControllerDisconnected() {
-    this.el.setAttribute("visible", false);
-  }
-});
diff --git a/src/components/hand-controls2.js b/src/components/hand-controls2.js
new file mode 100644
index 0000000000000000000000000000000000000000..80625759a8cba93b5d8e4f0da71037735823d971
--- /dev/null
+++ b/src/components/hand-controls2.js
@@ -0,0 +1,167 @@
+const GESTURES = {
+  open: "open",
+  // point: grip active, trackpad surface active, trigger inactive.
+  point: "point",
+  // pointThumb: grip active, trigger inactive, trackpad surface inactive.
+  pointThumb: "pointThumb",
+  // fist: grip active, trigger active, trackpad surface active.
+  fist: "fist",
+  // hold: trigger active, grip inactive.
+  hold: "hold",
+  // thumbUp: grip active, trigger active, trackpad surface inactive.
+  thumbUp: "thumbUp"
+};
+
+AFRAME.registerComponent("hand-controls2", {
+  schema: { default: "left" },
+
+  init() {
+    const el = this.el;
+
+    this.gesture = GESTURES.open;
+
+    this.fingersDown = {
+      thumb: false,
+      index: false,
+      middle: false,
+      ring: false,
+      pinky: false
+    };
+
+    this.onMiddleRingPinkyDown = this.updateGesture.bind(this, {
+      middle: true,
+      ring: true,
+      pinky: true
+    });
+
+    this.onMiddleRingPinkyUp = this.updateGesture.bind(this, {
+      middle: false,
+      ring: false,
+      pinky: false
+    });
+
+    this.onIndexDown = this.updateGesture.bind(this, {
+      index: true
+    });
+
+    this.onIndexUp = this.updateGesture.bind(this, {
+      index: false
+    });
+
+    this.onThumbDown = this.updateGesture.bind(this, {
+      thumb: true
+    });
+
+    this.onThumbUp = this.updateGesture.bind(this, {
+      thumb: false
+    });
+
+    this.onControllerConnected = this.onControllerConnected.bind(this);
+    this.onControllerDisconnected = this.onControllerDisconnected.bind(this);
+
+    el.addEventListener("controllerconnected", this.onControllerConnected);
+    el.addEventListener(
+      "controllerdisconnected",
+      this.onControllerDisconnected
+    );
+
+    el.setAttribute("visible", false);
+  },
+
+  play() {
+    const el = this.el;
+    el.addEventListener("middle_ring_pinky_down", this.onMiddleRingPinkyDown);
+    el.addEventListener("middle_ring_pinky_up", this.onMiddleRingPinkyUp);
+    el.addEventListener("thumb_down", this.onThumbDown);
+    el.addEventListener("thumb_up", this.onThumbUp);
+    el.addEventListener("index_down", this.onIndexDown);
+    el.addEventListener("index_up", this.onIndexUp);
+  },
+
+  pause() {
+    const el = this.el;
+    el.removeEventListener(
+      "middle_ring_pinky_down",
+      this.onMiddleRingPinkyDown
+    );
+    el.removeEventListener("middle_ring_pinky_up", this.onMiddleRingPinkyUp);
+    el.removeEventListener("thumb_down", this.onThumbDown);
+    el.removeEventListener("thumb_up", this.onThumbUp);
+    el.removeEventListener("index_down", this.onIndexDown);
+    el.removeEventListener("index_up", this.onIndexUp);
+  },
+
+  // Attach the platform specific tracked controllers.
+  update(prevData) {
+    const el = this.el;
+    const hand = this.data;
+
+    const controlConfiguration = {
+      hand: hand,
+      model: false,
+      rotationOffset: 0
+    };
+
+    if (hand !== prevData) {
+      el.setAttribute("vive-controls", controlConfiguration);
+      el.setAttribute("oculus-touch-controls", controlConfiguration);
+      el.setAttribute("windows-motion-controls", controlConfiguration);
+    }
+  },
+
+  remove() {
+    const el = this.el;
+    el.removeEventListener("controllerconnected", this.onControllerConnected);
+    el.removeEventListener(
+      "controllerdisconnected",
+      this.onControllerDisconnected
+    );
+  },
+
+  updateGesture(nextFingersDown) {
+    Object.assign(this.fingersDown, nextFingersDown);
+    const gesture = this.determineGesture();
+
+    if (gesture !== this.gesture) {
+      this.gesture = gesture;
+      this.el.emit(this.last + "end");
+      this.el.emit(this.gesture + "start");
+    }
+  },
+
+  determineGesture() {
+    const { thumb, index, middle, ring, pinky } = this.fingersDown;
+
+    if (!thumb && !index && !middle && !ring && !pinky) {
+      return GESTURES.open;
+    } else if (thumb && index && middle && ring && pinky) {
+      return GESTURES.fist;
+    } else if (!thumb && index && middle && ring && pinky) {
+      return GESTURES.thumbUp;
+    } else if (!thumb && !index && middle && ring && pinky) {
+      return GESTURES.pointThumb;
+    } else if (!thumb && index && !middle && !ring && !pinky) {
+      return GESTURES.hold;
+    } else if (thumb && !index && !middle && !ring && !pinky) {
+      return GESTURES.hold;
+    } else if (thumb && index && !middle && !ring && !pinky) {
+      return GESTURES.hold;
+    } else if (thumb && !index && middle && ring && pinky) {
+      return GESTURES.point;
+    }
+
+    console.warn("Did not find matching gesture for ", this.fingersDown);
+
+    return GESTURES.open;
+  },
+
+  // Show controller when connected
+  onControllerConnected() {
+    this.el.setAttribute("visible", true);
+  },
+
+  // Hide controller on disconnect
+  onControllerDisconnected() {
+    this.el.setAttribute("visible", false);
+  }
+});
diff --git a/src/components/rig-selector.js b/src/components/rig-selector.js
deleted file mode 100644
index f27a7bba4654c51d6b3b6b5a6c6c9c679068d2ba..0000000000000000000000000000000000000000
--- a/src/components/rig-selector.js
+++ /dev/null
@@ -1,59 +0,0 @@
-AFRAME.registerComponent("rig-selector", {
-  schema: {
-    vive: { default: "" },
-    oculus: { default: "" },
-    daydream: { default: "" },
-    gearvr: { default: "" },
-    mobile: { default: "" },
-    desktop: { default: "" }
-  },
-  init: function() {
-    var vrDevice = this.el.sceneEl.effect.getVRDisplay();
-    var rigEl = document.createElement("a-entity");
-
-    if (vrDevice !== undefined) {
-      var displayName = vrDevice.displayName;
-
-      if (displayName.indexOf("Oculus") !== -1) {
-        rigEl.setAttribute(
-          "template",
-          "src:" + this.data.oculus || this.data.desktop
-        );
-      } else if (displayName.indexOf("OpenVR") !== -1) {
-        rigEl.setAttribute(
-          "template",
-          "src:" + this.data.vive || this.data.desktop
-        );
-      } else if (displayName.indexOf("Daydream") !== -1) {
-        rigEl.setAttribute(
-          "template",
-          "src:" + this.data.daydream || this.data.mobile
-        );
-      } else {
-        rigEl.setAttribute(
-          "template",
-          "src:" + this.data.desktop || this.data.mobile
-        );
-      }
-    } else {
-      if (AFRAME.utils.device.isGearVR()) {
-        rigEl.setAttribute(
-          "template",
-          "src:" + this.data.gearvr || this.data.mobile
-        );
-      } else if (AFRAME.utils.device.isMobile()) {
-        rigEl.setAttribute(
-          "template",
-          "src:" + this.data.mobile || this.data.desktop
-        );
-      } else {
-        rigEl.setAttribute(
-          "template",
-          "src:" + this.data.desktop || this.data.mobile
-        );
-      }
-    }
-
-    this.el.appendChild(rigEl);
-  }
-});
diff --git a/src/config.js b/src/config.js
index 4eefb9bc77ce9a3f4796f933178f6b2f2bcb74b9..eed5c042fc564ebe1fdd872632de7ad835517e66 100644
--- a/src/config.js
+++ b/src/config.js
@@ -1,5 +1,4 @@
 export default {
-  // janus_server_url: "wss://dev-janus.reticulum.io",
   janus_server_url: "wss://quander.me:8989",
   public_rooms: [1, 2, 3, 4, 5],
   default_room: 1
diff --git a/src/index.js b/src/index.js
index 58b492b63ef9cba4bb8cede767bf5e64ddaeb6fa..c1b6368b509e5d66d3dc68698a3adb9225196f78 100644
--- a/src/index.js
+++ b/src/index.js
@@ -6,16 +6,19 @@ import "naf-janus-adapter";
 import "aframe-teleport-controls";
 import "aframe-input-mapping-component";
 
+import animationMixer from "aframe-extras/src/loaders/animation-mixer";
+AFRAME.registerComponent("animation-mixer", animationMixer);
+
 import "./components/axis-dpad";
 import "./components/mute-mic";
 import "./components/audio-feedback";
 import "./components/nametag-transform";
-import "./components/avatar-customization";
 import "./components/mute-state-indicator";
-import "./components/hand-controls-visibility";
+import "./components/virtual-gamepad-controls";
+import "./components/body-controller";
+import "./components/hand-controls2";
 import "./components/character-controller";
 import "./components/split-axis-events";
-import "./components/virtual-gamepad-controls";
 import "./systems/personal-space-bubble";
 
 import registerNetworkScheams from "./network-schemas";
diff --git a/src/input-mappings.js b/src/input-mappings.js
index 9035563b1ace79fea88b588f9863988d7404c8d6..d57ff6496b1d35bf0a435b06cfdb74dd807008e6 100644
--- a/src/input-mappings.js
+++ b/src/input-mappings.js
@@ -1,38 +1,46 @@
 export default function registerInputMappings() {
   AFRAME.registerInputMappings({
-    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"
-      },
-      daydream: {
-        menudown: "action_mute"
-      },
-      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"
+    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"
+        },
+        daydream: {
+          menudown: "action_mute"
+        },
+        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"
+        }
       }
     }
   });
diff --git a/src/lobby.js b/src/lobby.js
index 4ae9eeef7b5de88548969c1a16d6ddb6125366ae..61b3e35e814aa31763335a3a75076b5c534a1e4e 100644
--- a/src/lobby.js
+++ b/src/lobby.js
@@ -46,20 +46,12 @@ class Lobby extends React.Component {
   }
 
   fetchRooms() {
-    return Promise.all(
-      Config.public_rooms.map(room_id => {
-        return this.handle
-          .sendMessage({
-            kind: "listusers",
-            room_id
-          })
-          .then(signal => ({
-            id: room_id,
-            limit: 12,
-            users: signal.plugindata.data.response.user_ids
-          }));
-      })
-    );
+    return this.handle
+      .sendMessage({ kind: "listusers" })
+      .then(signal => {
+        const usersByRoom = signal.plugindata.data.response.users;
+        return Config.public_rooms.map(id => ({id, limit: 12, users: usersByRoom[id] || []}));
+      });
   }
 
   onWebsocketMessage(event) {
diff --git a/src/network-schemas.js b/src/network-schemas.js
index 0f00706410b075fa3f4f7689ee8c22b6caa4e84d..d93c552cb6b90a68ee158c66cd93475338c60408 100644
--- a/src/network-schemas.js
+++ b/src/network-schemas.js
@@ -11,7 +11,12 @@ function registerNetworkSchemas() {
   });
 
   NAF.schemas.add({
-    template: "#hand-template",
+    template: "#right-hand-template",
+    components: ["position", "rotation", "visible"]
+  });
+
+  NAF.schemas.add({
+    template: "#left-hand-template",
     components: ["position", "rotation", "visible"]
   });
 
diff --git a/yarn.lock b/yarn.lock
index c60d2aea81572d59f6443c01eb9332a9f8670173..ef0c717dacd6ff8e82bed56f238917614cee86fd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -52,9 +52,16 @@ acorn@^5.1.1:
   version "5.2.1"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7"
 
-"aframe-input-mapping-component@https://github.com/fernandojsg/aframe-input-mapping-component":
-  version "1.0.0"
-  resolved "https://github.com/fernandojsg/aframe-input-mapping-component#577cadad5a42e7216843dfeca9770fa8be465e09"
+aframe-extras@^3.12.4:
+  version "3.12.4"
+  resolved "https://registry.yarnpkg.com/aframe-extras/-/aframe-extras-3.12.4.tgz#9276bde8b51a07a9822bbce1fc55f2eb8e6810dc"
+  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-lerp-component@^1.1.0:
   version "1.1.0"
@@ -62,6 +69,13 @@ aframe-lerp-component@^1.1.0:
   dependencies:
     almost-equal "^1.1.0"
 
+aframe-physics-system@^1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/aframe-physics-system/-/aframe-physics-system-1.4.3.tgz#c6927e847081bfe546658314aa4c04958ef27934"
+  dependencies:
+    cannon "github:donmccurdy/cannon.js#v0.6.2-dev1"
+    three-to-cannon "^1.1.1"
+
 "aframe-teleport-controls@https://github.com/netpro2k/aframe-teleport-controls#feature/teleport-origin":
   version "0.3.0"
   resolved "https://github.com/netpro2k/aframe-teleport-controls#41fe311d3123503ba44761acce69d0f0634139cc"
@@ -1339,6 +1353,10 @@ caniuse-lite@^1.0.30000744:
   version "1.0.30000751"
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000751.tgz#298ad34182ca4359757b4a93afc681b7b917e358"
 
+"cannon@github:donmccurdy/cannon.js#v0.6.2-dev1":
+  version "0.6.2"
+  resolved "https://codeload.github.com/donmccurdy/cannon.js/tar.gz/022e8ba53fa83abf0ad8a0e4fd08623123838a17"
+
 caseless@~0.12.0:
   version "0.12.0"
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
@@ -1564,7 +1582,7 @@ concat-stream@1.4.x:
     readable-stream "~1.1.9"
     typedarray "~0.0.5"
 
-concat-stream@^1.4.7, concat-stream@^1.6.0:
+concat-stream@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
   dependencies:
@@ -3642,9 +3660,9 @@ min-document@^2.19.0:
   dependencies:
     dom-walk "^0.1.0"
 
-minijanus@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/minijanus/-/minijanus-0.1.4.tgz#1a8294fa0735ce6a6c58a10dcff9a9ab0cfe8132"
+minijanus@^0.1.6:
+  version "0.1.6"
+  resolved "https://registry.yarnpkg.com/minijanus/-/minijanus-0.1.6.tgz#8aa08d5797239b3c54a998efa0ee25fa4a700689"
 
 minimalistic-assert@^1.0.0:
   version "1.0.0"
@@ -3704,11 +3722,11 @@ mute-stream@0.0.7:
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
 
-naf-janus-adapter@^0.1.4:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.1.4.tgz#3e6e2022e8f6d5b7f6221fcec8b36a1e1e13be44"
+naf-janus-adapter@^0.1.5:
+  version "0.1.5"
+  resolved "https://registry.yarnpkg.com/naf-janus-adapter/-/naf-janus-adapter-0.1.5.tgz#f10c8cf390e226ddd62a0058c19f072547bd783a"
   dependencies:
-    minijanus "^0.1.4"
+    minijanus "^0.1.6"
 
 nan@^2.3.0:
   version "2.7.0"
@@ -3840,10 +3858,6 @@ normalize-url@^1.4.0:
     query-string "^4.1.0"
     sort-keys "^1.0.0"
 
-now@^8.3.11:
-  version "8.3.11"
-  resolved "https://registry.yarnpkg.com/now/-/now-8.3.11.tgz#bca2043bc4485b1b24ea656d652e448ae27fe0bb"
-
 npm-run-path@^2.0.0:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@@ -3987,10 +4001,6 @@ os-locale@^2.0.0:
     lcid "^1.0.0"
     mem "^1.1.0"
 
-os-shim@^0.1.2:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/os-shim/-/os-shim-0.1.3.tgz#6b62c3791cf7909ea35ed46e17658bb417cb3917"
-
 os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
@@ -4179,10 +4189,6 @@ pkg-dir@^2.0.0:
   dependencies:
     find-up "^2.1.0"
 
-pleasejs@^0.4.2:
-  version "0.4.2"
-  resolved "https://registry.yarnpkg.com/pleasejs/-/pleasejs-0.4.2.tgz#aaaa1a5fa6902518de7e51e3c63b5f537f823164"
-
 pluralize@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
@@ -4441,14 +4447,6 @@ postcss@^6.0.1:
     source-map "^0.6.1"
     supports-color "^4.4.0"
 
-pre-commit@^1.2.2:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/pre-commit/-/pre-commit-1.2.2.tgz#dbcee0ee9de7235e57f79c56d7ce94641a69eec6"
-  dependencies:
-    cross-spawn "^5.0.1"
-    spawn-sync "^1.0.15"
-    which "1.2.x"
-
 prelude-ls@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -5164,13 +5162,6 @@ source-map@~0.1.31:
   dependencies:
     amdefine ">=0.0.4"
 
-spawn-sync@^1.0.15:
-  version "1.0.15"
-  resolved "https://registry.yarnpkg.com/spawn-sync/-/spawn-sync-1.0.15.tgz#b00799557eb7fb0c8376c29d44e8a1ea67e57476"
-  dependencies:
-    concat-stream "^1.4.7"
-    os-shim "^0.1.2"
-
 spdx-correct@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
@@ -5431,6 +5422,14 @@ three-buffer-vertex-data@^1.0.0:
   dependencies:
     flatten-vertex-data "^1.0.0"
 
+three-pathfinding@^0.2.2:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/three-pathfinding/-/three-pathfinding-0.2.3.tgz#469bb26fb6b331f536c9ec88fde78e9c9219f637"
+
+three-to-cannon@^1.1.1:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/three-to-cannon/-/three-to-cannon-1.1.2.tgz#b0040b893b2fa5f0e8a0aedf58bc90fc07f137aa"
+
 three@^0.87.0:
   version "0.87.1"
   resolved "https://registry.yarnpkg.com/three/-/three-0.87.1.tgz#466a34edc4543459ced9b9d7d276b65216fe2ba8"
@@ -5796,12 +5795,6 @@ which-module@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
 
-which@1.2.x:
-  version "1.2.14"
-  resolved "https://registry.yarnpkg.com/which/-/which-1.2.14.tgz#9a87c4378f03e827cecaf1acdf56c736c01c14e5"
-  dependencies:
-    isexe "^2.0.0"
-
 which@^1.2.12, which@^1.2.9:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"