<!-- SAGA · part 67 -->
5x RUN speed */
    var runSp = 2.5;
    var fwdX = -Math.cos(camYaw), fwdZ = -Math.sin(camYaw);
    var rightX = Math.sin(camYaw), rightZ = -Math.cos(camYaw);
    var mx = rightX * dx * runSp + fwdX * (-dy) * runSp;
    var mz = rightZ * dx * runSp + fwdZ * (-dy) * runSp;
    lastDX = mx;
    lastDZ = mz;
    kingPos.x += mx;
    kingPos.z += mz;
    kingPos.y = groundY; /* Stay on ground when walking */
    if(typeof playerBody !== 'undefined' && playerBody){
      playerBody.rotation.x = 0;
    }
  }
});

/* RIGHT joystick — orbits the camera around the body */
makeJoystick('right:8%;bottom:100px;', function(dx, dy){
  if(window._freecam){
    /* Free camera — move position */
    var _fwd = new THREE.Vector3();
    camera.getWorldDirection(_fwd);
    camera.position.addScaledVector(_fwd, -dy * 2);
    var _right = new THREE.Vector3().crossVectors(_fwd, camera.up).normalize();
    camera.position.addScaledVector(_right, dx * 2);
  } else {
    camYaw -= dx * 0.08;
    camPitch = Math.max(0.1, Math.min(1.4, camPitch + dy * 0.05));
  }
});

/* ===== BUTTON WALL — 56 tappable buttons in 3D ===== */
var buttonWall = null;
var wallButtons = [];
function buildButtonWall(){
  if(buttonWall) scene.remove(buttonWall);
  wallButtons = [];
  buttonWall = new THREE.Group();
  /* Build 10 units ahead — you place it by where you stand */
  var wx = kingPos.x - Math.cos(camYaw) * 10;
  var wz = kingPos.z - Math.sin(camYaw) * 10;
  var wy = 25;
  say('wall placed where you stand');
  
  /* Wall: dark panel with gold trim, at Practice District */
  var wallGeo = new THREE.BoxGeometry(60, 40, 2);
  var wallMat = new THREE.MeshStandardMaterial({ color: 0x0b0906, roughness: 0.8 });
  var wall = new THREE.Mesh(wallGeo, wallMat);
  wall.position.set(wx, wy, wz);
  buttonWall.add(wall);
  
  /* Gold trim */
  var trimGeo = new THREE.BoxGeometry(62, 42, 1);
  var trimMat = new THREE.MeshStandardMaterial({ color: 0xd8a920, roughness: 0.3, metalness: 0.8 });
  var trim = new THREE.Mesh(trimGeo, trimMat);
  trim.position.set(wx, wy, wz + 0.5);
  buttonWall.add(trim);
  
  /* Title */
  var titleCanvas = document.createElement('canvas');
  titleCanvas.width = 1024; titleCanvas.height = 128;
  var tctx = titleCanvas.getContext('2d');
  tctx.fillStyle = '#0b0906'; tctx.fillRect(0,0,1024,128);
  tctx.fillStyle = '#ffd700'; tctx.font = '64px Georgia';
  tctx.textAlign = 'center'; tctx.fillText('C O D E R S 1 0 0', 512, 84);
  var titleTex = new THREE.CanvasTexture(titleCanvas);
  var titleMat = new THREE.MeshBasicMaterial({ map: titleTex, transparent: true });
  var titleMesh = new THREE.Mesh(new THREE.PlaneGeometry(40, 5), titleMat);
  titleMesh.position.set(wx, wy + 18, wz + 1.5);
  buttonWall.add(titleMesh);
  
  /* 56 buttons: 7 columns x 8 rows */
  var loader = new THREE.TextureLoader();
  var cols = 7, rows = 8;
  var btnSize = 6, gap = 1.5;
  var startX = wx - (cols * (btnSize + gap)) / 2 + btnSize/2;
  var startY = wy + 12;
  
  BTNS.forEach(function(b, i){
    var col = i % cols;
    var row = Math.floor(i / cols);
    var x = startX + col * (btnSize + gap);
    var y = startY - row * (btnSize + gap);
    
    var tex = loader.load('/coders100/buttons/' + b[0] + '.webp');
    tex.minFilter = THREE.LinearMipmapLinearFilter;
    tex.magFilter = THREE.LinearFilter;
    tex.anisotropy = renderer.capabilities.getMaxAnisotropy();
    var mat = new THREE.MeshBasicMaterial({ map: tex, transparent: true });
    var mesh = new THREE.Mesh(new THREE.PlaneGeometry(btnSize, btnSize), mat);
    mesh.position.set(x, y, wz + 1.5);
    mesh.userData.buttonIndex = i;
    mesh.userData.buttonFn = b[2];
    mesh.userData.buttonName = b[1];
    buttonWall.add(mesh);
    wallButtons.push(mesh);
  });
  
  /* Face the player */
  buttonWall.rotation.y = -camYaw + Math.PI;
  scene.add(buttonWall);
  say('button wall built — 56 buttons, tap to press');
}

/* Raycast tapping for wall buttons */
var raycaster = new THREE.Raycaster();
var tapPos = new THREE.Vector2();
function onWallTap(event){
  if(!buttonWall) return;
  var rect = renderer.domElement.getBoundingClientRect();
  tapPos.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
  tapPos.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
  raycaster.setFromCamera(tapPos, camera);
  var hits = raycaster.intersectObjects(wallButtons);
  if(hits.length > 0){
    var btn = hits[0].object;
    /* Press animation: scale down briefly */
    btn.scale.set(0.85, 0.85, 1);
    setTimeout(function(){ btn.scale.set(1, 1, 1); }, 150);
    /* Do the task */
    say('pressed: ' + btn.userData.buttonName);
    btn.userData.buttonFn();
  }
}
/* Attach tap listener */
if(typeof renderer !== 'undefined' && renderer.domElement){
  renderer.domElement.addEventListener('click', onWallTap);
}

/* ===== PRACTICE DISTRICT ===== */
registerDistrict('practice', 'Practice District', function(){
  var CX=600, CZ=600, PHI=1.6180339887;
  var entry=new THREE.Mesh(new THREE.CircleGeometry(20