<!-- SOLID CITY · part 1498 -->
); });
$('sp-wform').addEventListener('submit', function (e) { e.preventDefault(); wallPost($('sp-wmsg').value); });
$('sp-close').addEventListener('click', function () { clearTimeout(wall.timer); });
(function () { // push-to-talk via the browser's speech recognition
  var SR = window.SpeechRecognition || window.webkitSpeechRecognition, rec = null, btn = $('t-ptt'), heard = '';
  if (!SR) { btn.addEventListener('click', function () { logLine('sys', 'Voice input isn\u2019t supported in this browser — type instead.'); }); return; }
  function start(e) { e.preventDefault(); if (rec) return; heard = ''; rec = new SR(); rec.lang = 'en-US'; rec.interimResults = true; rec.continuous = true;
    rec.onresult = function (ev) { var s = ''; for (var i = 0; i < ev.results.length; i++) s += ev.results[i][0].transcript; heard = s; $('t-msg').value = s; };
    rec.onerror = function (ev) { logLine('sys', 'Mic: ' + ev.error); };
    rec.onend = function () { btn.classList.remove('is-on'); var h = heard; rec = null; if (h.trim()) sendTalk(h); };
    try { speechSynthesis.cancel(); } catch (x) {} rec.start(); btn.classList.add('is-on'); }
  function stop() { if (rec) try { rec.stop(); } catch (x) {} }
  btn.addEventListener('click', function (e) { e.preventDefault(); if (typeof micLockStart !== 'function') return; if (micLock.on) micLockStop(); else micLockStart(); });
})();
/* Mic lock — tap the avatar locks the mic on; 20s of quiet releases it. Plain text only. */
var micLock = { on: false, rec: null, lastVoice: 0, timer: 0 };
function micLockStart() {
  var SR = window.SpeechRecognition || window.webkitSpeechRecognition;
  if (!SR) { logLine('sys', 'Voice input isn\u2019t supported in this browser.'); return; }
  if (micLock.on) return;
  micLock.on = true; micLock.lastVoice = Date.now(); window.__sableSpeaking = false;
  try { var CPR = window.CityPlayer; micLock.radioWas = !!(CPR && CPR.state && CPR.state().playing); if (micLock.radioWas && CPR.toggle) CPR.toggle(); } catch (e6) {}
  $('t-form').style.display = 'none';
  $('sc-talk').classList.add('behind');
  logLine('sys', 'Mic locked on — speak. Quiet 20s releases it.');
  micRecStart();
  micLock.timer = setInterval(function () {
    if (!micLock.on || document.hidden) return;
    if (!window.__sableSpeaking && Date.now() - micLock.lastVoice > 20000) micLockStop();
  }, 1000);
}
function micLockStop(quiet) {
  if (!micLock.on) return;
  micLock.on = false; micLock.aborts = 0;
  try { var CPR2 = window.CityPlayer; if (micLock.radioWas && CPR2 && CPR2.toggle && CPR2.state && !CPR2.state().playing) CPR2.toggle(); } catch (e7) {}
  micLock.radioWas = false;
  try { clearInterval(micLock.timer); } catch (e) {}
  try { if (micLock.rec) { micLock.rec.onend = null; micLock.rec.onerror = null; micLock.rec.stop(); } } catch (e2) {}
  micLock.rec = null;
  $('t-form').style.display = '';
  $('sc-talk').classList.remove('behind');
  if (!quiet) logLine('sys', 'Mic off — quiet 20s.');
}
function micRecStart() {
  var SR = window.SpeechRecognition || window.webkitSpeechRecognition;
  if (!micLock.on || micLock.rec || !SR) return;
  var rec = null;
  try { rec = new SR(); } catch (e) { return; }
  micLock.rec = rec;
  rec.lang = 'en-US'; rec.interimResults = true; rec.continuous = true;
  var finalSent = '';
  rec.onresult = function (ev) {
    micLock.aborts = 0;
    var s = '', fin = false, i;
    for (i = 0; i < ev.results.length; i++) { s += ev.results[i][0].transcript; if (ev.results[i].isFinal) fin = true; }
    if (window.__sableSpeaking) { if (s.trim()) micLock.lastVoice = Date.now(); return; }
    if (s.trim()) micLock.lastVoice = Date.now();
    if (fin && s.trim() && s.trim() !== finalSent) { finalSent = s.trim(); sendTalk(s.trim()); }
  };
  rec.onerror = function (ev) {
    if (ev.error === 'not-allowed' || ev.error === 'service-not-allowed') { logLine('sys', 'Mic blocked.'); micLockStop(true); return; }
    if (ev.error === 'aborted') { micLock.aborts = (micLock.aborts || 0) + 1; if (micLock.aborts >= 3) { logLine('sys', 'Mic keeps aborting — tap the mic button to retry.'); micLockStop(true); } return; }
    logLine('sys', 'Mic: ' + ev.error);
  };
  rec.onend = function () {
    micLock.rec = null;
    if (!micLock.on || (micLock.aborts || 0) >= 3) return;
    setTimeout(micRecStart, 400);
  };
  try { rec.start(); } catch (e) { micLock.rec = null; }
}

/* ---------------- main loop ---------------- */
var workTimes = [], last = performance.now(), fpsAcc = 0, fpsN = 0, fpsVal = 60, running = false, frameTimes = [];
var aiF = 0, aiLX = 1e9, aiLZ = 1e9, aiLYaw = 1e9;   // the AI mode's render-rest tally: last drawn position and heading
function frame(now) {
  if (!running) return;
  requestAnimationFrame(frame);
  var now0 = performance.now();
  var dt = Math.min(0.1, (now - last) / 1000); last = now; S.t += dt;
  frameTimes.push(dt * 1000); if (frameTimes.length > 600) frameTimes.shift();
  fpsAcc += dt; fpsN++; if (fpsAcc > 0.5) { f