<!-- RUBY-BACKUP · part 438 -->
   var t = Math.random()*10;
        var anim = setInterval(function(){
          t += 0.03;
          if(sh.userData.state==='landed'){
            sh.position.y = Math.sin(t*2)*0.05;
          }else if(sh.userData.state==='takingoff'){
            sh.position.y += 0.05;
            if(sh.position.y > 8) sh.userData.state = 'hover';
          }else if(sh.userData.state==='hover'){
            sh.position.y = 8 + Math.sin(t)*0.3;
            sh.rotation.y += 0.01;
          }
        }, 50);
        SPACESHIPS.push({mesh: sh, anim: anim, pad: pd});
      })(ship, pad);
    }
  }
  codeStatus('spaceport — 3 landing pads, 2 ships. The future lands here.');
}
function launchShip(idx){
  idx = idx || 0;
  if(SPACESHIPS[idx]){
    SPACESHIPS[idx].mesh.userData.state = 'takingoff';
    codeStatus('ship launching');
  }else{
    codeStatus('no ship at index '+idx);
  }
}
function landShip(idx){
  idx = idx || 0;
  if(SPACESHIPS[idx]){
    SPACESHIPS[idx].mesh.userData.state = 'landed';
    SPACESHIPS[idx].mesh.position.y = 0;
    codeStatus('ship landed');
  }
}
function clearSpaceport(){
  SPACESHIPS.forEach(function(s){ try{ scene.remove(s.mesh); }catch(e){} if(s.anim) clearInterval(s.anim); });
  LANDING_PADS.forEach(function(p){ try{ scene.remove(p); }catch(e){} });
  SPACESHIPS = []; LANDING_PADS = [];
  codeStatus('spaceport cleared');
}

/* THE OFFICER AND THE FIREFIGHTER */
var OFFICER_GROUP = null;
function buildPoliceOfficer(x, y, z){
  var g = new THREE.Group();
  var skin = new THREE.MeshStandardMaterial({color: 0xc89868, roughness: 0.7});
  var uniform = new THREE.MeshStandardMaterial({color: 0x1a2a5a, roughness: 0.7});
  /* head */
  var head = new THREE.Mesh(new THREE.SphereGeometry(0.15, 18, 18), skin);
  head.position.y = 1.55; g.add(head);
  /* police cap */
  var cap = new THREE.Mesh(new THREE.CylinderGeometry(0.16, 0.17, 0.1, 12), uniform);
  cap.position.y = 1.68; g.add(cap);
  var badge = new THREE.Mesh(new THREE.CircleGeometry(0.04, 8),
    new THREE.MeshStandardMaterial({color: 0xffcc00, metalness: 0.8, roughness: 0.3}));
  badge.position.set(0, 1.68, 0.16); g.add(badge);
  /* concerned eyes — looking for the pad */
  [-1,1].forEach(function(s){
    var eye = new THREE.Mesh(new THREE.SphereGeometry(0.025, 10, 10),
      new THREE.MeshStandardMaterial({color: 0x222222}));
    eye.position.set(0.08, 1.57, s*0.06); g.add(eye);
  });
  /* uniform body */
  var body = new THREE.Mesh(new THREE.CylinderGeometry(0.18, 0.22, 0.9, 12), uniform);
  body.position.y = 1.0; g.add(body);
  /* badge on chest */
  var chestBadge = new THREE.Mesh(new THREE.CircleGeometry(0.05, 8),
    new THREE.MeshStandardMaterial({color: 0xffcc00, metalness: 0.8, roughness: 0.3}));
  chestBadge.position.set(0, 1.2, 0.2); g.add(chestBadge);
  [-1,1].forEach(function(s){
    var leg = new THREE.Mesh(new THREE.CylinderGeometry(0.07, 0.06, 0.55, 8), uniform);
    leg.position.set(0, 0.28, s*0.1); g.add(leg);
  });
  /* empty hands — patting pockets, pad is missing */
  [-1,1].forEach(function(s){
    var arm = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.045, 0.4, 8), uniform);
    arm.position.set(-0.1, 1.1, s*0.25); arm.rotation.z = 0.5;
    g.add(arm);
  });
  /* speech — "Have you seen my pad?" */
  var cv = document.createElement('canvas');
  cv.width = 512; cv.height = 80;
  var ctx = cv.getContext('2d');
  ctx.fillStyle = 'rgba(240,240,255,0.95)';
  ctx.fillRect(0, 0, 512, 80);
  ctx.strokeStyle = '#1a2a5a'; ctx.lineWidth = 4;
  ctx.strokeRect(4, 4, 504, 72);
  ctx.fillStyle = '#1a2a5a'; ctx.font = 'bold 24px Arial'; ctx.textAlign = 'center';
  ctx.fillText('Have you seen my pad?', 256, 48);
  var tex = new THREE.CanvasTexture(cv);
  var bubble = new THREE.Mesh(new THREE.PlaneGeometry(2.2, 0.34),
    new THREE.MeshBasicMaterial({map: tex, transparent: true}));
  bubble.position.set(0, 2.1, 0);
  g.add(bubble);
  g.position.set(x, y, z);
  var t = 0;
  var anim = setInterval(function(){
    t += 0.04;
    /* looking around, patting pockets */
    g.rotation.y = Math.sin(t*0.8)*0.6;
    bubble.rotation.y = -g.rotation.y; /* face camera */
  }, 50);
  g.userData.anim = anim;
  return g;
}
function buildFirefighter(x, y, z){
  var g = new THREE.Group();
  var skin = new THREE.MeshStandardMaterial({color: 0xd4a574, roughness: 0.7});
  var gear = new THREE.MeshStandardMaterial({color: 0xcc3300, roughness: 0.6});
  var yellow = new THREE.MeshStandardMaterial({color: 0xffcc00, emissive: 0xffcc00, emissiveIntensity: 0.4});
  /* head */
  var head = new THREE.Mesh(new THREE.SphereGeometry(0.15, 18, 18), skin);
  head.position.y = 1.55; g.add(head);
  /* firefighter helmet */
  var helmet = new THREE.Mesh(new THREE.SphereGeometry(0.18, 14, 14, 0, Math.PI*2, 0, Math.PI*0.6), gear);
  helmet.position.y = 1.58; g.add(helmet);
  var brim = new THREE.Mesh(new THREE.CylinderGeometry(0.22, 0.22, 0.03, 14), gear);
  brim.position.y = 1.58; g.add(brim);
  /* reflective stripes */
  [-1,1].forEach(func