<!-- RUBY-BACKUP · part 235 -->
art);
  var body = new THREE.Mesh(new THREE.CylinderGeometry(0.16, 0.2, 0.8, 12), cloth);
  body.position.y = 0.9; g.add(body);
  [-1,1].forEach(function(s){
    var leg = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.05, 0.5, 8), cloth);
    leg.position.set(0, 0.25, s*0.09); g.add(leg);
  });
  g.position.set(x, y, z);
  /* walk to the soul, give mission, walk back */
  var state = 'approach';
  var t = 0;
  var mission = MISSIONS[Math.floor(Math.random()*MISSIONS.length)];
  /* mission bubble */
  var mCv = document.createElement('canvas');
  mCv.width = 512; mCv.height = 96;
  var mCtx = mCv.getContext('2d');
  mCtx.fillStyle = 'rgba(255,255,240,0.95)';
  mCtx.fillRect(0, 0, 512, 96);
  mCtx.strokeStyle = '#ffcc00'; mCtx.lineWidth = 4;
  mCtx.strokeRect(4, 4, 504, 88);
  mCtx.fillStyle = '#333333';
  mCtx.font = '20px Arial'; mCtx.textAlign = 'center';
  mCtx.fillText('MISSION:', 256, 28);
  mCtx.font = 'bold 18px Arial';
  /* wrap text */
  var words = mission.text.split(' ');
  var line = '', lines = [];
  words.forEach(function(w){
    if((line+' '+w).length > 30){ lines.push(line); line = w; }
    else line += (line ? ' ' : '')+w;
  });
  lines.push(line);
  lines.slice(0,2).forEach(function(l, i){
    mCtx.fillText(l, 256, 52+i*20);
  });
  var mTex = new THREE.CanvasTexture(mCv);
  var bubble = new THREE.Mesh(new THREE.PlaneGeometry(2.5, 0.47),
    new THREE.MeshBasicMaterial({map: mTex, transparent: true}));
  bubble.position.set(0, 2.2, 0);
  bubble.visible = false;
  g.add(bubble);
  var anim = setInterval(function(){
    t += 0.03;
    if(state==='approach'){
      /* move toward target */
      var dx = targetX - g.position.x, dz = targetZ - g.position.z;
      var dist = Math.sqrt(dx*dx+dz*dz);
      if(dist > 1.5){
        g.position.x += (dx/dist)*0.03;
        g.position.z += (dz/dist)*0.03;
        g.rotation.y = Math.atan2(dx, dz);
        /* walking bob */
        g.position.y = y + Math.abs(Math.sin(t*8))*0.05;
      }else{
        state = 'give';
        bubble.visible = true;
        setTimeout(function(){ bubble.visible = false; state = 'leave'; }, 5000);
      }
    }else if(state==='leave'){
      /* walk back */
      var bx = x - g.position.x, bz = z - g.position.z;
      var bdist = Math.sqrt(bx*bx+bz*bz);
      if(bdist > 0.5){
        g.position.x += (bx/bdist)*0.03;
        g.position.z += (bz/bdist)*0.03;
        g.rotation.y = Math.atan2(bx, bz);
        g.position.y = y + Math.abs(Math.sin(t*8))*0.05;
      }else{
        state = 'rest';
        setTimeout(function(){ state = 'approach'; }, 10000);
      }
    }else if(state==='rest'){
      /* wait, then go again */
      g.rotation.y += 0.01;
    }
    /* heart glows */
    heart.material.emissiveIntensity = 1.2 + Math.sin(t*3)*0.4;
  }, 50);
  g.userData.anim = anim;
  g.userData.mission = mission;
  return g;
}
function placeMissionGivers(){
  if(typeof scene==='undefined' || typeof mePos==='undefined') return;
  MISSION_GIVERS.forEach(function(m){
    try{ scene.remove(m); }catch(e){}
    if(m.userData.anim) clearInterval(m.userData.anim);
  });
  MISSION_GIVERS = [];
  var px = mePos.x, pz = mePos.z;
  var givers = [
    {name: 'Kai', color: 0x0099cc, sx: px-170, sz: pz+120, tx: (px-175)*0.13, tz: (pz+125)*0.13},
    {name: 'Leilani', color: 0xff99cc, sx: px-170, sz: pz-120, tx: (px-175)*0.13, tz: (pz-115)*0.13},
    {name: 'Makoa', color: 0xcc6633, sx: px+170, sz: pz+120, tx: (px+175)*0.13, tz: (pz+125)*0.13},
    {name: 'Nalani', color: 0x66ccff, sx: px+170, sz: pz-120, tx: (px+180)*0.13, tz: (pz-110)*0.13}
  ];
  givers.forEach(function(gv){
    var giver = buildMissionGiver(gv.name, gv.color, gv.sx*0.13, 0, gv.sz*0.13, gv.tx, gv.tz);
    scene.add(giver);
    MISSION_GIVERS.push(giver);
  });
  if(!DARK_PLACES) DARK_PLACES = [];
  MISSION_GIVERS.forEach(function(m){ DARK_PLACES.push(m); });
  codeStatus('good souls — Kai, Leilani, Makoa, Nalani. They bring missions.');
}
function buildDarkPlaces(){
  if(typeof scene==='undefined' || typeof mePos==='undefined') return;
  /* bypassed for kids */
  if(typeof KIDMODE!=='undefined' && KIDMODE){
    codeStatus('dark places are bypassed for kids');
    return;
  }
  if(DARK_PLACES){
    DARK_PLACES.forEach(function(d){
      try{ scene.remove(d); }catch(e){}
      d.traverse(function(o){
        if(o.userData.anim) clearInterval(o.userData.anim);
        if(o.userData.boltAnim) clearInterval(o.userData.boltAnim);
      });
    });
  }
  DARK_PLACES = [];
  var px = mePos.x, pz = mePos.z;
  var beach = buildStormBeach(px-180, pz-120);
  beach.scale.set(0.3, 0.3, 0.3); /* very small */
  scene.add(beach); DARK_PLACES.push(beach);
  var peak = buildLostSoulsPeak(px+180, pz-120);
  peak.scale.set(0.3, 0.3, 0.3);
  scene.add(peak); DARK_PLACES.push(peak);
  var under = buildUnderworldFallacy(px-180, pz+120);
  under.scale.set(0.3, 0.3, 0.3);
  scene.add(under); DARK_PLACES.push(under);
  var worm = buildParadoxWormholes(px+180, pz+120)