<!-- SAGA2 · part 79 -->
 hdraw(pts, hDim);
    }
  });
  [-1,1].forEach(function(sd){
    var lx = sd*3;
    for(var l=0; l<18; l++){
      var pts=[], ly = -3-l*1.5, lr = 2.8-l*0.06;
      for(var j=0;j<=14;j++){ var an=j/14*Math.PI*2;
        pts.push(new THREE.Vector3(lx+Math.cos(an)*lr, ly, Math.sin(an)*lr*0.85));
      }
      hdraw(pts, l%6===0?hWarm:hSoft);
    }
  });
}

document.getElementById('cambtn').addEventListener('click', function(){
  camMode = (camMode+1)%camAngles.length;
  var a = camAngles[camMode];
  camera.position.set(a[0][0], a[0][1], a[0][2]);
  camera.lookAt(a[1][0], a[1][1], a[1][2]);
});
window.addEventListener('resize',function(){
  camera.aspect=window.innerWidth/window.innerHeight;
  camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth,window.innerHeight);
});
console.log("Solid's practice live");
// ===== WEAPONS: twin laser cannons, sticks become crosshairs =====
function buildCannons(){
  buildAvatar();
  cannonL = {virtual:true, side:-1};
  cannonR = {virtual:true, side:1};
  // tactical: brighten visor + sights in combat
  if(avatarVisor) avatarVisor.material.color.setHex(0x88eeff);
}
function setWeaponMode(on){
  var was = weaponMode;
  weaponMode = on;
  if(on && !cannonL) buildCannons();
  if(on && !was && navigator.vibrate){ try{ navigator.vibrate([40,30,40]); }catch(e){} }
  // diamonds are permanent — nothing to change
}
function fireLaser(){
  if(!weaponMode || !cannonL) return;
  // HAPTIC: feel the cannons in your hands
  if(navigator.vibrate){ try{ navigator.vibrate(25); }catch(e){} }
  [cannonL, cannonR].forEach(function(cn, i){
    setTimeout(function(){
      var dir = new THREE.Vector3(0,0,-1).applyQuaternion(camera.quaternion).normalize();
      // start slightly below camera, offset left/right
      var start = new THREE.Vector3();
  var ac = (i===0) ? avatarCannonL : avatarCannonR;
  var tipObj = ac ? (ac.userData.tipL || ac.userData.tip || ac.userData.tipPos) : null;
  if(tipObj){ if(tipObj.getWorldPosition) tipObj.getWorldPosition(start); else start.copy(tipObj).applyMatrix4(avatarGrp.matrixWorld); }
  else { start = camera.position.clone().add(new THREE.Vector3(cn.side*8, -6, 0).applyQuaternion(camera.quaternion)); }
  // muzzle flash on avatar cannon
  if(tipObj){
    tipObj.material.color.setHex(0xffffff);
    (function(tp){ setTimeout(function(){ tp.material.color.setHex(0x44ccff); }, 90); })(tipObj);
  }
      // raycast for impact
      var ray = new THREE.Raycaster(start, dir, 0, 1200);
      var hits = ray.intersectObjects(scene.children, true);
      var endDist = 900;
      var hitPoint = null, hitObj = null;
      for(var h=0; h<hits.length; h++){
        var o = hits[h].object;
        // skip lasers, sprites, glows
        if(o.isLine || o.isSprite || o.isPointLight) continue;
        if(lasers.some(function(l){ return l.mesh===o; })) continue;
        endDist = hits[h].distance;
        hitPoint = hits[h].point;
        hitObj = o;
        break;
      }
      var end = start.clone().add(dir.clone().multiplyScalar(endDist));
      // SPARKLING BEAM: core + glow shell
      var beamGrp = new THREE.Group();
      var coreGeo = new THREE.CylinderGeometry(0.8, 0.8, endDist, 6);
      coreGeo.translate(0, endDist/2, 0);
      var core = new THREE.Mesh(coreGeo, new THREE.MeshBasicMaterial({
        color: 0xffffff, transparent:true, opacity:0.95 }));
      var shellGeo = new THREE.CylinderGeometry(2.5, 2.5, endDist, 8);
      shellGeo.translate(0, endDist/2, 0);
      var shell = new THREE.Mesh(shellGeo, new THREE.MeshBasicMaterial({
        color: i?0xffd766:0x44ccff, transparent:true, opacity:0.45,
        blending:THREE.AdditiveBlending, depthWrite:false }));
      beamGrp.add(core); beamGrp.add(shell);
      beamGrp.position.copy(start);
      beamGrp.lookAt(end);
      beamGrp.rotateX(Math.PI/2);
      scene.add(beamGrp);
      lasers.push({mesh:beamGrp, life:0.18});
      // SPARKLES along beam
      for(var sp=0; sp<8; sp++){
        var t = Math.random();
        var pos = start.clone().lerp(end, t);
        var spark = new THREE.Mesh(new THREE.OctahedronGeometry(1.5+Math.random()*2),
          new THREE.MeshBasicMaterial({color: i?0xffeebb:0xbbffff, transparent:true, opacity:1}));
        spark.position.copy(pos);
        spark.userData = {vx:(Math.random()-0.5)*30, vy:(Math.random()-0.5)*30, vz:(Math.random()-0.5)*30, life:0.5+Math.random()*0.4};
        scene.add(spark);
        sparkles.push(spark);
      }
      // BLACK SCORCH MARK at impact
      if(hitPoint && hitObj){
        var scorch = new THREE.Mesh(new THREE.CircleGeometry(6+Math.random()*4, 12),
          new THREE.MeshBasicMaterial({color:0x000000, transparent:true, opacity:0.55, depthWrite:false}));
        scorch.position.copy(hitPoint);
        // orient to surface
        var n = hits[0].face ? hits[0].face.normal.clone() : dir.clone().negate();
        if(hitObj.matrixWorld) n.transformDirection(hitObj.matrixWorld);
        scorch.lookAt(hitPoint.