<!-- SAGA3 · part 52 -->
0.35; camDist=12; }},
  'vright': {cat:'Camera', desc:'Right view', fn:function(){ camYaw=-Math.PI/2; camPitch=0.35; camDist=12; }},
  'vtop':   {cat:'Camera', desc:'Top down', fn:function(){ camPitch=1.45; camDist=30; }},
  'vbelow': {cat:'Camera', desc:'From below', fn:function(){ camPitch=-0.5; camDist=20; }},
  'vclose': {cat:'Camera', desc:'Close up', fn:function(){ camDist=6; }},
  'vfar':   {cat:'Camera', desc:'Far away', fn:function(){ camDist=80; camPitch=0.7; }},
  'vzoomout':{cat:'Camera', desc:'Zoom out', fn:function(){ camDist=Math.min(200, camDist*1.5); }},
  'vreset': {cat:'Camera', desc:'Reset camera', fn:function(){ camYaw=0; camPitch=0.5; camDist=20; }},
  'look':   {cat:'Camera', desc:'Look at x z — look 600 600', fn:function(a){
    if(a.length>=2){ kingPos.x=parseFloat(a[0])||0; kingPos.z=parseFloat(a[1])||0; }
  }},
  'skyview':{cat:'Camera', desc:'Sky view', fn:function(){
    camera.position.set(0,900,900); camera.lookAt(0,0,0);
  }},
  'vtoggle':{cat:'Camera', desc:'Toggle view mode', fn:function(){
    camDist = camDist > 30 ? 12 : 80;
  }},
  'angle':  {cat:'Camera', desc:'Set angle — angle 45', fn:function(a){
    if(a[0]) camYaw = (parseFloat(a[0])||0) * Math.PI/180;
  }},
  'glide':  {cat:'Camera', desc:'Smooth glide', fn:function(){ say('gliding'); }},
};

/* Code runner */
var codeInput = document.getElementById('codeinput');
function runCode(text){
  text = (text||'').trim().toLowerCase();
  if(!text) return;
  var parts = text.split(/\s+/);
  var cmd = parts[0], args = parts.slice(1);
  var c = CODES[cmd];
  if(c){ try{ c.fn(args); }catch(e){ say('stumbled'); } }
  else say('unknown — '+cmd);
}
document.getElementById('codesend').onclick = function(){
  runCode(codeInput.value); codeInput.value=''; codeInput.blur();
};
codeInput.addEventListener('keydown', function(e){
  if(e.key==='Enter'){ runCode(codeInput.value); codeInput.value=''; codeInput.blur(); }
});
/* Os never blocks keyboard — close Os when typing */
codeInput.addEventListener('focus', function(){
  osOpen = false;
  osEl.classList.remove('open');;
});

/* ===== CAMERA TOGGLE — cycle through modes ===== */
var camModes = [
  {n:'Orbit', yaw:0, pitch:0.5, dist:20},
  {n:'Top', yaw:0, pitch:1.45, dist:30},
  {n:'Front', yaw:0, pitch:0.35, dist:12},
  {n:'Back', yaw:Math.PI, pitch:0.35, dist:12},
  {n:'Left', yaw:Math.PI/2, pitch:0.35, dist:12},
  {n:'Right', yaw:-Math.PI/2, pitch:0.35, dist:12},
  {n:'Far', yaw:0, pitch:0.7, dist:80},
  {n:'Close', yaw:0, pitch:0.4, dist:6}
];
var camModeIdx = 0;
function toggleCamera(){
  camModeIdx = (camModeIdx + 1) % camModes.length;
  var m = camModes[camModeIdx];
  camYaw = m.yaw; camPitch = m.pitch; camDist = m.dist;
  say('camera: ' + m.n);
}

/* ===== BODY MOVEMENT TOGGLE — walk / fly / still ===== */
var moveModes = ['still', 'walk', 'fly', 'swim'];
var moveModeIdx = 0;
var moveMode = 'still';
function toggleMove(){
  moveModeIdx = (moveModeIdx + 1) % moveModes.length;
  moveMode = moveModes[moveModeIdx];
  var joy = document.getElementById('joy');
  if(moveMode === 'still'){
    if(joy) joy.style.display = 'none';
    say('still');
  } else if(moveMode === 'walk'){
    if(joy) joy.style.display = 'block';
    say('walk — joystick to move');
  } else if(moveMode === 'fly'){
    if(joy) joy.style.display = 'block';
    say('fly — joystick to soar');
  }
}

/* Joystick movement */
var joyActive = false, joyDX = 0, joyDY = 0;
var joyEl = document.getElementById('joy');
var knobEl = document.getElementById('joyknob');
if(joyEl){
  joyEl.addEventListener('touchstart', function(e){
    joyActive = true;
    e.preventDefault();
  }, {passive:false});
  joyEl.addEventListener('touchmove', function(e){
    if(!joyActive) return;
    var r = joyEl.getBoundingClientRect();
    var cx = r.left + r.width/2, cy = r.top + r.height/2;
    var dx = e.touches[0].clientX - cx, dy = e.touches[0].clientY - cy;
    var max = r.width/2 - 18;
    var len = Math.sqrt(dx*dx + dy*dy);
    if(len > max){ dx = dx/len*max; dy = dy/len*max; }
    joyDX = dx/max; joyDY = dy/max;
    knobEl.style.transform = 'translate('+dx+'px,'+dy+'px)';
    e.preventDefault();
  }, {passive:false});
  joyEl.addEventListener('touchend', function(){
    joyActive = false; joyDX = 0; joyDY = 0;
    knobEl.style.transform = 'translate(0,0)';
  }, {passive:true});
}

/* Movement applied in animate loop below */

/* ===== PRECISE MAP — canvas, exact positions ===== */
var mapCanvas = null, mapOpen = false;
function toggleMap(){
  if(!mapCanvas){
    mapCanvas = document.createElement('canvas');
    mapCanvas.width = 300; mapCanvas.height = 300;
    mapCanvas.style.cssText = 'position:fixed;top:70px;right:12px;z-index:90;border:2px solid #8a6a1c;border-radius:24px;background:rgba(10,8,4,0.95);display:none;';
    document.body.appendChild(mapCanvas);
  }
  mapOpen = !mapOpen;
  mapCanvas.style.display = mapOpen ? 'block' : 'none';
  if(mapOpen) drawMap();
}
var cityMapImg = new Image();
cityMapIm