diff --git a/src/assets/avatars/BotDom_Avatar.glb b/src/assets/avatars/BotDom_Avatar.glb
new file mode 100644
index 0000000000000000000000000000000000000000..9c72e3d2fc170d6b0782d741eca441a62780daad
Binary files /dev/null and b/src/assets/avatars/BotDom_Avatar.glb differ
diff --git a/src/assets/stylesheets/room.scss b/src/assets/stylesheets/room.scss
index 8f3cfb7b5ebf1553653649575d03c5e0a215956c..d042519b31409aa888dde3bbe83e69758fb1cb54 100644
--- a/src/assets/stylesheets/room.scss
+++ b/src/assets/stylesheets/room.scss
@@ -17,3 +17,10 @@
   bottom: 20px;
 }
 
+.a-canvas.a-grab-cursor:hover {
+	cursor: none;
+}
+
+.a-canvas.a-grab-cursor:active {
+	cursor: none;
+}
diff --git a/src/components/debug.js b/src/components/debug.js
new file mode 100644
index 0000000000000000000000000000000000000000..77202f03295be677b9156e1ff16f11100ab19351
--- /dev/null
+++ b/src/components/debug.js
@@ -0,0 +1,25 @@
+AFRAME.registerComponent("lifecycle-checker", {
+  schema: {
+    tick: { default: false }
+  },
+  init: function() {
+    console.log("init", this.el);
+  },
+  update: function() {
+    console.log("update", this.el);
+  },
+  tick: function() {
+    if (this.data.tick) {
+      console.log("tick", this.el);
+    }
+  },
+  remove: function() {
+    console.log("remove", this.el);
+  },
+  pause: function() {
+    console.log("pause", this.el);
+  },
+  play: function() {
+    console.log("play", this.el);
+  }
+});
diff --git a/src/components/ik-controller.js b/src/components/ik-controller.js
index 3b4f323df72d83cfbe0d03ab00094318485eed22..0a1990bd8ede46f4e5bbdde5cfced182ed4f5a3d 100644
--- a/src/components/ik-controller.js
+++ b/src/components/ik-controller.js
@@ -7,29 +7,27 @@ AFRAME.registerComponent("ik-root", {
     rightController: { type: "string", default: ".right-controller" }
   },
   update(oldData) {
-    let updated = false;
-
     if (this.data.camera !== oldData.camera) {
       this.camera = this.el.querySelector(this.data.camera);
-      updated = true;
     }
 
     if (this.data.leftController !== oldData.leftController) {
       this.leftController = this.el.querySelector(this.data.leftController);
-      updated = true;
     }
 
     if (this.data.rightController !== oldData.rightController) {
       this.rightController = this.el.querySelector(this.data.rightController);
-      updated = true;
-    }
-
-    if (updated) {
-      this.el.querySelector("[ik-controller]").components["ik-controller"].updateIkRoot(this);
     }
   }
 });
 
+function findIKRoot(entity) {
+  while (entity && !(entity.components && entity.components["ik-root"])) {
+    entity = entity.parentNode;
+  }
+  return entity && entity.components["ik-root"];
+}
+
 AFRAME.registerComponent("ik-controller", {
   schema: {
     leftEye: { type: "string", default: ".LeftEye" },
@@ -65,6 +63,8 @@ AFRAME.registerComponent("ik-controller", {
     this.rootToChest = new Matrix4();
     this.invRootToChest = new Matrix4();
 
+    this.ikRoot = findIKRoot(this.el);
+
     this.hands = {
       left: {
         lastVisible: true,
@@ -124,10 +124,6 @@ AFRAME.registerComponent("ik-controller", {
       .negate();
   },
 
-  updateIkRoot(ikRoot) {
-    this.ikRoot = ikRoot;
-  },
-
   tick(time, dt) {
     if (!this.ikRoot) {
       return;
diff --git a/src/components/player-info.js b/src/components/player-info.js
new file mode 100644
index 0000000000000000000000000000000000000000..8cb265a67b9c67ac26510d6b8a03efb59703c931
--- /dev/null
+++ b/src/components/player-info.js
@@ -0,0 +1,31 @@
+AFRAME.registerComponent("player-info", {
+  schema: {
+    displayName: { type: "string" },
+    avatar: { type: "string" }
+  },
+  init() {
+    this.applyProperties = this.applyProperties.bind(this);
+  },
+  play() {
+    this.el.addEventListener("model-loaded", this.applyProperties);
+  },
+  pause() {
+    this.el.removeEventListener("model-loaded", this.applyProperties);
+  },
+  update(oldProps) {
+    this.applyProperties();
+  },
+  applyProperties() {
+    const nametagEl = this.el.querySelector(".nametag");
+    if (this.data.displayName && nametagEl) {
+      nametagEl.setAttribute("text", {
+        value: this.data.displayName
+      });
+    }
+
+    const modelEl = this.el.querySelector(".model");
+    if (this.data.avatar && modelEl) {
+      modelEl.setAttribute("src", this.data.avatar);
+    }
+  }
+});
diff --git a/src/components/super-cursor.js b/src/components/super-cursor.js
index d1b28b0f1566d43fe68e40d1fc0d9e0ecc13be65..e498a1acc2ad9b99c9131134bd17560bee1af531 100644
--- a/src/components/super-cursor.js
+++ b/src/components/super-cursor.js
@@ -2,42 +2,55 @@ AFRAME.registerComponent("super-cursor", {
   dependencies: ["raycaster"],
   schema: {
     cursor: { type: "selector" },
-    maxDistance: { type: "number", default: 3 },
-    minDistance: { type: "number", default: 0.5 }
+    camera: { type: "selector" },
+    maxDistance: { default: 3 },
+    minDistance: { default: 0.5 },
+    cursorColorHovered: { default: "#FF0000" },
+    cursorColorUnhovered: { efault: "#FFFFFF" }
   },
 
   init: function() {
     this.isGrabbing = false;
+    this.isInteractable = false;
     this.wasIntersecting = false;
     this.currentDistance = this.data.maxDistance;
     this.currentDistanceMod = 0;
     this.enabled = true;
-    this.isGrabbing = false;
     this.origin = new THREE.Vector3();
     this.direction = new THREE.Vector3();
     this.point = new THREE.Vector3();
-  },
+    this.mousePos = new THREE.Vector2();
+
+    this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });
 
-  play: function() {
     this.mouseDownListener = this._handleMouseDown.bind(this);
+    this.mouseMoveListener = this._handleMouseMove.bind(this);
     this.mouseUpListener = this._handleMouseUp.bind(this);
     this.wheelListener = this._handleWheel.bind(this);
     this.enterVRListener = this._handleEnterVR.bind(this);
-    this.exitVRListener = this._handleExitVR.bind(this);   
+    this.exitVRListener = this._handleExitVR.bind(this);
+  },
 
+  play: function() {
     document.addEventListener("mousedown", this.mouseDownListener);
+    document.addEventListener("mousemove", this.mouseMoveListener);
     document.addEventListener("mouseup", this.mouseUpListener);
     document.addEventListener("wheel", this.wheelListener);
     window.addEventListener("enter-vr", this.enterVRListener);
     window.addEventListener("exit-vr", this.exitVRListener);
+
+    this._enable();
   },
 
   pause: function() {
     document.removeEventListener("mousedown", this.mouseDownListener);
+    document.removeEventListener("mousemove", this.mouseMoveListener);
     document.removeEventListener("mouseup", this.mouseUpListener);
     document.removeEventListener("wheel", this.wheelListener);
     window.removeEventListener("enter-vr", this.enterVRListener);
     window.removeEventListener("exit-vr", this.exitVRListener);
+
+    this._disable();
   },
 
   tick: function() {
@@ -46,14 +59,21 @@ AFRAME.registerComponent("super-cursor", {
     }
 
     this.isGrabbing = this.data.cursor.components["super-hands"].state.has("grab-start");
-    let isIntersecting = false;
+
+    const camera = this.data.camera.components.camera.camera;
+    const raycaster = this.el.components.raycaster.raycaster;
+    raycaster.setFromCamera(this.mousePos, camera);
+    this.origin = raycaster.ray.origin;
+    this.direction = raycaster.ray.direction;
+    this.el.setAttribute("raycaster", { origin: this.origin, direction: this.direction });
+
+    let intersection = null;
 
     if (!this.isGrabbing) {
       const intersections = this.el.components.raycaster.intersections;
       if (intersections.length > 0 && intersections[0].distance <= this.data.maxDistance) {
-        isIntersecting = true;
-        this.point = intersections[0].point;
-        this.data.cursor.object3D.position.copy(this.point);
+        intersection = intersections[0];
+        this.data.cursor.object3D.position.copy(intersection.point);
         this.currentDistance = intersections[0].distance;
         this.currentDistanceMod = 0;
       } else {
@@ -61,50 +81,69 @@ AFRAME.registerComponent("super-cursor", {
       }
     }
 
-    if (this.isGrabbing || !isIntersecting) {
-      const head = this.el.object3D;
-      head.getWorldPosition(this.origin);
-      head.getWorldDirection(this.direction);
+    if (this.isGrabbing || !intersection) {
       const distance = Math.min(
         Math.max(this.data.minDistance, this.currentDistance - this.currentDistanceMod),
         this.data.maxDistance
       );
       this.currentDistanceMod = this.currentDistance - distance;
-      this.direction.multiplyScalar(-distance);
+      this.direction.multiplyScalar(distance);
       this.point.addVectors(this.origin, this.direction);
       this.data.cursor.object3D.position.copy(this.point);
     }
 
-    if ((this.isGrabbing || isIntersecting) && !this.wasIntersecting) {
+    this.isInteractable = intersection && intersection.object.el.className === "interactable";
+
+    if ((this.isGrabbing || this.isInteractable) && !this.wasIntersecting) {
       this.wasIntersecting = true;
-      this.data.cursor.setAttribute("material", {color: "#00FF00"});
-    } else if (!this.isGrabbing && !isIntersecting && this.wasIntersecting) {
+      this.data.cursor.setAttribute("material", { color: this.data.cursorColorHovered });
+    } else if (!this.isGrabbing && !this.isInteractable && this.wasIntersecting) {
       this.wasIntersecting = false;
-      this.data.cursor.setAttribute("material", {color: "#00EFFF"});
+      this.data.cursor.setAttribute("material", { color: this.data.cursorColorUnhovered });
     }
   },
 
   _handleMouseDown: function(e) {
+    if (this.isInteractable) {
+      const lookControls = this.data.camera.components["look-controls"];
+      lookControls.pause();
+    }
     this.data.cursor.emit("action_grab", {});
   },
 
+  _handleMouseMove: function(e) {
+    this.mousePos.set(e.clientX / window.innerWidth * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
+  },
+
   _handleMouseUp: function(e) {
+    const lookControls = this.data.camera.components["look-controls"];
+    lookControls.play();
     this.data.cursor.emit("action_release", {});
   },
 
   _handleWheel: function(e) {
     if (this.isGrabbing) this.currentDistanceMod += e.deltaY / 10;
-  }, 
+  },
 
   _handleEnterVR: function(e) {
     if (AFRAME.utils.device.checkHeadsetConnected() || AFRAME.utils.device.isMobile()) {
-      this.enabled = false;
-      this.data.cursor.setAttribute("visible", false);
+      this._disable();
     }
   },
 
   _handleExitVR: function(e) {
+    this._enable();
+  },
+
+  _enable: function() {
     this.enabled = true;
     this.data.cursor.setAttribute("visible", true);
+    this.el.setAttribute("raycaster", { enabled: true });
   },
+
+  _disable: function() {
+    this.enabled = false;
+    this.data.cursor.setAttribute("visible", false);
+    this.el.setAttribute("raycaster", { enabled: false });
+  }
 });
diff --git a/src/components/super-spawner.js b/src/components/super-spawner.js
index 42d7c9b48f3067e87f0242aebc15b7ff07332eca..72d24eff785b9eebed42c60e2e4feb21e659468a 100644
--- a/src/components/super-spawner.js
+++ b/src/components/super-spawner.js
@@ -21,7 +21,7 @@ AFRAME.registerComponent("super-spawner", {
   remove: function() {
     for (let entity of this.entities.keys()) {
       const data = this.entities.get(entity);
-      entity.removeEventListener("componentinitialized", data.componentinItializedListener);
+      entity.removeEventListener("componentinitialized", data.componentinInitializedListener);
       entity.removeEventListener("bodyloaded", data.bodyLoadedListener);
     }
 
@@ -34,18 +34,18 @@ AFRAME.registerComponent("super-spawner", {
 
     entity.setAttribute("networked", "template:" + this.data.template);
 
-    const componentinItializedListener = this._handleComponentInitialzed.bind(this, entity);
+    const componentinInitializedListener = this._handleComponentInitialzed.bind(this, entity);
     const bodyLoadedListener = this._handleBodyLoaded.bind(this, entity);
 
     this.entities.set(entity, {
       hand: hand,
       componentInitialized: false, 
       bodyLoaded: false, 
-      componentinItializedListener: componentinItializedListener, 
+      componentinInitializedListener: componentinInitializedListener, 
       bodyLoadedListener: bodyLoadedListener
     });
     
-    entity.addEventListener("componentinitialized", componentinItializedListener);
+    entity.addEventListener("componentinitialized", componentinInitializedListener);
     entity.addEventListener("body-loaded", bodyLoadedListener);
 
     const pos = this.data.useCustomSpawnPosition ? this.data.spawnPosition : this.el.getAttribute("position");
@@ -71,7 +71,7 @@ AFRAME.registerComponent("super-spawner", {
       data.hand.emit("action_grab", { targetEntity: entity });
       entity.emit("grab-start", { hand: data.hand });
 
-      entity.removeEventListener("componentinitialized", data.componentinItializedListener);
+      entity.removeEventListener("componentinitialized", data.componentinInitializedListener);
       entity.removeEventListener("body-loaded", data.bodyLoadedListener);
 
       this.entities.delete(entity);
diff --git a/src/elements/a-gltf-entity.js b/src/elements/a-gltf-entity.js
index 3c9c07031eecbf21069569b0f2738fe48afcea25..e88f7a76bf6ee4218f37997d9497e576b3bed55a 100644
--- a/src/elements/a-gltf-entity.js
+++ b/src/elements/a-gltf-entity.js
@@ -22,19 +22,21 @@ AFRAME.AGLTFEntity = {
 // From https://gist.github.com/cdata/f2d7a6ccdec071839bc1954c32595e87
 // Tracking glTF cloning here: https://github.com/mrdoob/three.js/issues/11573
 function cloneGltf(gltf) {
-  const clone = {
-    animations: gltf.animations,
-    scene: gltf.scene.clone(true)
-  };
-
   const skinnedMeshes = {};
-
   gltf.scene.traverse(node => {
+    if (!node.name) {
+      node.name = node.uuid;
+    }
     if (node.isSkinnedMesh) {
       skinnedMeshes[node.name] = node;
     }
   });
 
+  const clone = {
+    animations: gltf.animations,
+    scene: gltf.scene.clone(true)
+  };
+
   const cloneBones = {};
   const cloneSkinnedMeshes = {};
 
@@ -68,18 +70,24 @@ function cloneGltf(gltf) {
   return clone;
 }
 
-const inflateEntities = function(classPrefix, parentEl, node) {
+const inflateEntities = function(parentEl, node) {
   // setObject3D mutates the node's parent, so we have to copy
   const children = node.children.slice(0);
 
   const el = document.createElement("a-entity");
 
   // Remove invalid CSS class name characters.
-  const className = node.name.replace(/[^\w-]/g, "");
-  el.classList.add(classPrefix + className);
+  const className = (node.name || node.uuid).replace(/[^\w-]/g, "");
+  el.classList.add(className);
   parentEl.appendChild(el);
 
-  // Copy over transform to the THREE.Group and reset the actual transform of the Object3D
+  // AFRAME rotation component expects rotations in YXZ, convert it
+  if (node.rotation.order !== "YXZ") {
+    node.rotation.setFromQuaternion(node.quaternion, "YXZ");
+  }
+
+  // Copy over the object's transform to the THREE.Group and reset the actual transform of the Object3D
+  // all updates to the object should be done through the THREE.Group wrapper
   el.setAttribute("position", {
     x: node.position.x,
     y: node.position.y,
@@ -95,10 +103,8 @@ const inflateEntities = function(classPrefix, parentEl, node) {
     y: node.scale.y,
     z: node.scale.z
   });
-
-  node.position.set(0, 0, 0);
-  node.rotation.set(0, 0, 0);
-  node.scale.set(1, 1, 1);
+  node.matrixAutoUpdate = false;
+  node.matrix.identity();
 
   el.setObject3D(node.type.toLowerCase(), node);
 
@@ -116,7 +122,6 @@ const inflateEntities = function(classPrefix, parentEl, node) {
   }
 
   const entityComponents = node.userData.components;
-
   if (entityComponents) {
     for (const prop in entityComponents) {
       if (entityComponents.hasOwnProperty(prop)) {
@@ -130,116 +135,184 @@ const inflateEntities = function(classPrefix, parentEl, node) {
   }
 
   children.forEach(childNode => {
-    inflateEntities(classPrefix, el, childNode);
+    inflateEntities(el, childNode);
   });
-};
 
-function attachTemplate(templateEl) {
-  const selector = templateEl.getAttribute("data-selector");
-  const targetEls = templateEl.parentNode.querySelectorAll(selector);
-  const clone = document.importNode(templateEl.content, true);
-  const templateRoot = clone.firstElementChild;
+  return el;
+};
 
+function attachTemplate(root, { selector, templateRoot }) {
+  const targetEls = root.querySelectorAll(selector);
   for (const el of targetEls) {
+    const root = templateRoot.cloneNode(true);
     // Merge root element attributes with the target element
-    for (const { name, value } of templateRoot.attributes) {
+    for (const { name, value } of root.attributes) {
       el.setAttribute(name, value);
     }
 
     // Append all child elements
-    for (const child of templateRoot.children) {
-      el.appendChild(document.importNode(child, true));
+    for (const child of root.children) {
+      el.appendChild(child);
     }
   }
 }
 
+function nextTick() {
+  return new Promise(resolve => {
+    setTimeout(resolve, 0);
+  });
+}
+
+function cachedLoadGLTF(src, onProgress) {
+  return new Promise((resolve, reject) => {
+    // Load the gltf model from the cache if it exists.
+    if (GLTFCache[src]) {
+      // Use a cloned copy of the cached model.
+      resolve(cloneGltf(GLTFCache[src]));
+    } else {
+      // Otherwise load the new gltf model.
+      new THREE.GLTFLoader().load(
+        src,
+        model => {
+          if (!GLTFCache[src]) {
+            // Store a cloned copy of the gltf model.
+            GLTFCache[src] = cloneGltf(model);
+          }
+          resolve(model);
+        },
+        onProgress,
+        reject
+      );
+    }
+  });
+}
+
 AFRAME.registerElement("a-gltf-entity", {
   prototype: Object.create(AFRAME.AEntity.prototype, {
     load: {
-      value() {
+      async value() {
         if (this.hasLoaded || !this.parentEl) {
           return;
         }
 
-        // Get the src url.
-        let src = this.getAttribute("src");
-
-        // If the src attribute is a selector, get the url from the asset item.
-        if (src.charAt(0) === "#") {
-          const assetEl = document.getElementById(src.substring(1));
-
-          const fallbackSrc = assetEl.getAttribute("src");
-          const highSrc = assetEl.getAttribute("high-src");
-          const lowSrc = assetEl.getAttribute("low-src");
+        // The code above and below this are from AEntity.prototype.load, we need to monkeypatch in gltf loading mid function
+        this.loadTemplates();
+        await this.applySrc(this.getAttribute("src"));
+        //
 
-          if (highSrc && window.APP.quality === "high") {
-            src = highSrc;
-          } else if (lowSrc && window.APP.quality === "low") {
-            src = lowSrc;
-          } else {
-            src = fallbackSrc;
+        AFRAME.ANode.prototype.load.call(this, () => {
+          // Check if entity was detached while it was waiting to load.
+          if (!this.parentEl) {
+            return;
           }
-        }
 
-        const onLoad = gltfModel => {
-          if (!GLTFCache[src]) {
-            // Store a cloned copy of the gltf model.
-            GLTFCache[src] = cloneGltf(gltfModel);
+          this.updateComponents();
+          if (this.isScene || this.parentEl.isPlaying) {
+            this.play();
           }
+        });
+      }
+    },
 
-          this.model = gltfModel.scene || gltfModel.scenes[0];
-          this.model.animations = gltfModel.animations;
+    loadTemplates: {
+      value() {
+        this.templates = [];
+        this.querySelectorAll(":scope > template").forEach(templateEl =>
+          this.templates.push({
+            selector: templateEl.getAttribute("data-selector"),
+            templateRoot: document.importNode(templateEl.content.firstElementChild, true)
+          })
+        );
+      }
+    },
+
+    applySrc: {
+      async value(src) {
+        try {
+          // If the src attribute is a selector, get the url from the asset item.
+          if (src && src.charAt(0) === "#") {
+            const assetEl = document.getElementById(src.substring(1));
+
+            const fallbackSrc = assetEl.getAttribute("src");
+            const highSrc = assetEl.getAttribute("high-src");
+            const lowSrc = assetEl.getAttribute("low-src");
+
+            if (highSrc && window.APP.quality === "high") {
+              src = highSrc;
+            } else if (lowSrc && window.APP.quality === "low") {
+              src = lowSrc;
+            } else {
+              src = fallbackSrc;
+            }
+          }
 
-          this.setObject3D("mesh", this.model);
-          this.emit("model-loaded", { format: "gltf", model: this.model });
+          if (src === this.lastSrc) return;
+          this.lastSrc = src;
 
-          if (this.getAttribute("inflate")) {
-            inflate(this.model, finalizeLoad);
-          } else {
-            finalizeLoad();
+          if (!src) {
+            if (this.inflatedEl) {
+              console.warn("gltf-entity set to an empty source, unloading inflated model.");
+              this.removeInflatedEl();
+            }
+            return;
           }
-        };
 
-        const inflate = (model, callback) => {
-          inflateEntities("", this, model);
-          this.querySelectorAll(":scope > template").forEach(attachTemplate);
+          const model = await cachedLoadGLTF(src);
 
-          // Wait one tick for the appended custom elements to be connected before calling finalizeLoad
-          setTimeout(callback, 0);
-        };
+          // If we started loading something else already
+          // TODO: there should be a way to cancel loading instead
+          if (src != this.lastSrc) return;
 
-        const finalizeLoad = () => {
-          AFRAME.ANode.prototype.load.call(this, () => {
-            // Check if entity was detached while it was waiting to load.
-            if (!this.parentEl) {
-              return;
-            }
+          // If we had inflated something already before, clean that up
+          this.removeInflatedEl();
 
-            this.updateComponents();
-            if (this.isScene || this.parentEl.isPlaying) {
-              this.play();
-            }
-          });
-        };
+          this.model = model.scene || model.scenes[0];
+          this.model.animations = model.animations;
+
+          this.setObject3D("mesh", this.model);
 
-        // Load the gltf model from the cache if it exists.
-        const gltf = GLTFCache[src];
+          if (this.getAttribute("inflate")) {
+            this.inflatedEl = inflateEntities(this, this.model);
+            // TODO: Still don't fully understand the lifecycle here and how it differs between browsers, we should dig in more
+            // Wait one tick for the appended custom elements to be connected before attaching templates
+            await nextTick();
+            if (src != this.lastSrc) return; // TODO: there must be a nicer pattern for this
+            this.templates.forEach(attachTemplate.bind(null, this));
+          }
 
-        if (gltf) {
-          // Use a cloned copy of the cached model.
-          const clonedGltf = cloneGltf(gltf);
-          onLoad(clonedGltf);
-          return;
+          this.emit("model-loaded", { format: "gltf", model: this.model });
+        } catch (e) {
+          const message = (e && e.message) || "Failed to load glTF model";
+          console.error(message);
+          this.emit("model-error", { format: "gltf", src });
+        }
+      }
+    },
+
+    removeInflatedEl: {
+      value() {
+        if (this.inflatedEl) {
+          this.inflatedEl.parentNode.removeChild(this.inflatedEl);
+          delete this.inflatedEl;
         }
+      }
+    },
 
-        // Otherwise load the new gltf model.
-        new THREE.GLTFLoader().load(src, onLoad, undefined /* onProgress */, error => {
-          // On glTF load error
+    attributeChangedCallback: {
+      value(attr, oldVal, newVal) {
+        if (attr === "src") {
+          this.applySrc(newVal);
+        }
+        AFRAME.AEntity.prototype.attributeChangedCallback.call(this, attr, oldVal, newVal);
+      }
+    },
 
-          const message = error && error.message ? error.message : "Failed to load glTF model";
-          console.warn(message);
-          this.emit("model-error", { format: "gltf", src });
-        });
+    setAttribute: {
+      value(attr, arg1, arg2) {
+        if (attr === "src") {
+          this.applySrc(arg1);
+        }
+        AFRAME.AEntity.prototype.setAttribute.call(this, attr, arg1, arg2);
       }
     }
   })
diff --git a/src/network-schemas.js b/src/network-schemas.js
index 4632a5933807b1e97132c726fcc335afa6b68964..54cc0b63ba40efed17baf43c0d3554b1ea2fa4bc 100644
--- a/src/network-schemas.js
+++ b/src/network-schemas.js
@@ -5,6 +5,7 @@ function registerNetworkSchemas() {
       "position",
       "rotation",
       "scale",
+      "player-info",
       {
         selector: ".camera",
         component: "position"
@@ -36,11 +37,6 @@ function registerNetworkSchemas() {
       {
         selector: ".right-controller",
         component: "visible"
-      },
-      {
-        selector: ".nametag",
-        component: "text",
-        property: "value"
       }
     ]
   });
diff --git a/src/room.html b/src/room.html
index 339b2ec76d4d14327619bd6d31c32d37a82ae517..4f1e132b9eb3a2ce94afdfd757fb87f2cde5ddb6 100644
--- a/src/room.html
+++ b/src/room.html
@@ -30,7 +30,7 @@
                 high-src="./assets/avatars/BotDefault_Avatar.glb"
                 low-src="./assets/avatars/BotDefault_Avatar_Unlit.glb"
             ></a-progressive-asset>
-            
+            <a-asset-item id="bot-dom-mesh" response-type="arraybuffer" src="./assets/avatars/BotDom_Avatar.glb"></a-asset-item>
             <a-asset-item id="watch-model" response-type="arraybuffer" src="./assets/hud/watch.glb"></a-asset-item>
 
             <a-asset-item id="meeting-space1-mesh" response-type="arraybuffer" src="./assets/environments/MeetingSpace1_mesh.glb"></a-asset-item>
@@ -48,16 +48,16 @@
             </template>
 
             <template id="remote-avatar-template">
-                <a-entity ik-root>
+                <a-entity ik-root player-info>
                     <a-entity class="camera"></a-entity>
 
                     <a-entity class="left-controller"></a-entity>
 
                     <a-entity class="right-controller"></a-entity>
 
-                    <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller >
+                    <a-gltf-entity class="model" inflate="true">
                         <template data-selector=".RootScene">
-                            <a-entity animation-mixer ></a-entity>
+                            <a-entity ik-controller animation-mixer></a-entity>
                         </template>
 
                         <template data-selector=".Neck">
@@ -81,7 +81,7 @@
                             </a-entity>
                         </template>
 
-                        <template selector=".LeftHand">
+                        <template data-selector=".LeftHand">
                             <a-entity personal-space-invader ></a-entity>
                         </template>
 
@@ -96,13 +96,12 @@
                 <a-entity
                     gltf-model="#interactable-duck"
                     scale="2 2 2"
-                    class="collidable" 
+                    class="interactable" 
                     super-networked-interactable="counter: #counter; mass: 5;"
                     body="type: dynamic; mass: 5; shape: box;"
                     grabbable
                     stretchable="useWorldPosition: true;"
-                >
-                </a-entity>
+                ></a-entity>
             </template>
 
             <a-mixin id="super-hands"
@@ -123,21 +122,24 @@
         <a-entity 
             gltf-model="#interactable-duck"
             scale="2 2 2"
-            class="collidable" 
-            class="collidable" 
+            class="interactable" 
             super-spawner="template: #interactable-template;" 
             position="2.5 1.2 0" 
             body="mass: 0; type: static; shape: box;"
         ></a-entity>
 
-        <a-sphere
-            id="3d-cursor"
-            material="color: #00EFFF"
-            radius=0.02
-            static-body="shape: sphere;"
-            mixin="super-hands"
-        ></a-sphere>  
-
+        <a-entity
+            id="super-cursor"
+            super-cursor="cursor: #3d-cursor; camera: #player-camera;"
+            raycaster="objects: .collidable, .interactable; far: 10;"
+        >
+            <a-sphere
+                id="3d-cursor"
+                radius=0.02
+                static-body="shape: sphere;"
+                mixin="super-hands"
+            ></a-sphere>  
+        </a-entity>
 
         <!-- Player Rig -->
         <a-entity
@@ -147,6 +149,7 @@
             wasd-to-analog2d
             character-controller="pivot: #player-camera"
             ik-root
+            player-info
         >
             <a-entity
                 id="player-camera"
@@ -154,14 +157,8 @@
                 camera
                 position="0 1.6 0"
                 personal-space-bubble
-            >
-                <a-entity
-                    id="super-cursor"
-                    super-cursor="cursor: #3d-cursor"
-                    position="0 0 0"
-                    raycaster="objects: .collidable; direction: 0 0 -1;"
-                ></a-entity>
-            </a-entity>
+                look-controls
+            ></a-entity>
 
             <a-entity
                 id="player-left-controller"
@@ -181,9 +178,13 @@
                 haptic-feedback
             ></a-entity>
 
-            <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller >
+            <a-gltf-entity class="model" inflate="true">
                 <template data-selector=".RootScene">
-                    <a-entity animation-mixer animated-robot-hands ></a-entity>
+                    <a-entity
+                        ik-controller 
+                        animated-robot-hands
+                        animation-mixer
+                    ></a-entity>
                 </template>
 
                 <template data-selector=".Neck">
@@ -298,8 +299,8 @@
             height="35" 
             width="35" 
             static-body 
-            class="collidable">
-        </a-plane> 
+            class="collidable"
+        ></a-plane> 
     </a-scene>
 
     <div id="ui-root" class="ui"></div>
diff --git a/src/room.js b/src/room.js
index be559c209526c4255b12dffe8165ec333ab9249c..ab05f114958dc0a0885681ab790d870aab263d50 100644
--- a/src/room.js
+++ b/src/room.js
@@ -37,6 +37,8 @@ import "./components/layers";
 import "./components/spawn-controller";
 import "./components/animated-robot-hands";
 import "./components/hide-when-quality";
+import "./components/player-info";
+import "./components/debug";
 import "./components/animation-mixer";
 import "./components/loop-animation";
 
@@ -100,7 +102,7 @@ const concurrentLoadDetector = new ConcurrentLoadDetector();
 concurrentLoadDetector.start();
 
 // Always layer in any new default profile bits
-store.update({ profile:  { ...generateDefaultProfile(), ...(store.state.profile || {}) }})
+store.update({ profile: { ...generateDefaultProfile(), ...(store.state.profile || {}) } });
 
 async function exitScene() {
   const scene = document.querySelector("a-scene");
@@ -108,15 +110,21 @@ async function exitScene() {
   document.body.removeChild(scene);
 }
 
-function setNameTagFromStore() {
-  const myNametag = document.querySelector("#player-rig .nametag");
-  myNametag.setAttribute("text", "value", store.state.profile.display_name);
+function updatePlayerInfoFromStore() {
+  const qs = queryString.parse(location.search);
+  const playerRig = document.querySelector("#player-rig");
+  playerRig.setAttribute("player-info", {
+    displayName: store.state.profile.display_name,
+    avatar: qs.avatar || "#bot-skinned-mesh"
+  });
 }
 
 async function enterScene(mediaStream, enterInVR) {
   const scene = document.querySelector("a-scene");
-  document.querySelector("a-scene canvas").classList.remove("blurred")
-  scene.setAttribute("networked-scene", "adapter: janus; audio: true; debug: true; connectOnLoad: false;");
+  const playerRig = document.querySelector("#player-rig");
+  const qs = queryString.parse(location.search);
+
+  document.querySelector("a-scene canvas").classList.remove("blurred");
   registerNetworkSchemas();
 
   if (enterInVR) {
@@ -125,9 +133,11 @@ async function enterScene(mediaStream, enterInVR) {
 
   AFRAME.registerInputActions(inGameActions, "default");
 
-  document.querySelector("#player-camera").setAttribute("look-controls", "pointerLockEnabled: true;");
-
   scene.setAttribute("networked-scene", {
+    adapter: "janus",
+    audio: true,
+    debug: true,
+    connectOnLoad: false,
     room: qs.room && !isNaN(parseInt(qs.room, 10)) ? parseInt(qs.room, 10) : 1,
     serverURL: process.env.JANUS_SERVER
   });
@@ -136,13 +146,12 @@ async function enterScene(mediaStream, enterInVR) {
     scene.setAttribute("stats", true);
   }
 
-  if (isMobile || qsTruthy("mobile")) {
-    const playerRig = document.querySelector("#player-rig");
+  if (isMobile || qsTruthy(qs.mobile)) {
     playerRig.setAttribute("virtual-gamepad-controls", {});
   }
 
-  setNameTagFromStore();
-  store.addEventListener('statechanged', setNameTagFromStore);
+  updatePlayerInfoFromStore();
+  store.addEventListener("statechanged", updatePlayerInfoFromStore);
 
   const avatarScale = parseInt(qs.avatar_scale, 10);
 
@@ -200,8 +209,7 @@ async function enterScene(mediaStream, enterInVR) {
   }
 }
 
-function onConnect() {
-}
+function onConnect() {}
 
 function mountUI(scene) {
   const qs = queryString.parse(location.search);
@@ -209,16 +217,21 @@ function mountUI(scene) {
   const forcedVREntryType = qs.vr_entry_type || null;
   const enableScreenSharing = qsTruthy("enable_screen_sharing");
 
-  const uiRoot = ReactDOM.render(<UIRoot {...{
-    scene,
-    enterScene,
-    exitScene,
-    concurrentLoadDetector,
-    disableAutoExitOnConcurrentLoad,
-    forcedVREntryType,
-    enableScreenSharing,
-    store
-  }} />, document.getElementById("ui-root"));
+  const uiRoot = ReactDOM.render(
+    <UIRoot
+      {...{
+        scene,
+        enterScene,
+        exitScene,
+        concurrentLoadDetector,
+        disableAutoExitOnConcurrentLoad,
+        forcedVREntryType,
+        enableScreenSharing,
+        store
+      }}
+    />,
+    document.getElementById("ui-root")
+  );
 
   getAvailableVREntryTypes().then(availableVREntryTypes => {
     uiRoot.setState({ availableVREntryTypes });