public/dev_tools.js

分割 5 / 10 · 元ファイル 771〜963行付近

原文の連続部分です。長い圧縮行は行の途中で分割する場合があります。全分割を順に連結するとファイル全文になります。

  try{ if(typeof battleHandle!=='undefined' && battleHandle && battleHandle.reportHold) battleHandle.reportHold(!!on); }catch(e){}
}
/* 報告の窓が2つとも閉じたら時間を戻す。どちらか一方が開いている間は止めたまま */
function bbHoldSync(){ bbTimeHold(!!(bbWin || bbQuickWin)); }

/* --- 直近の出来事(軽い輪) --- */
const BB_EV=[], BB_EV_MAX=120;
window.HZG_EV=(tag, data)=>{
  try{
    BB_EV.push({t:Math.round(performance.now()), tag:String(tag).slice(0,40),
      d:(data==null?null:String(typeof data==='object'?JSON.stringify(data):data).slice(0,120))});
    if(BB_EV.length>BB_EV_MAX) BB_EV.shift();
  }catch(e){}
};
/* 本編のログ関数が生えたら、そこも出来事として拾う(二重に包まない) */
{
  const hook=()=>{
    for(const n of ['logF','logS','logA','logHead']){
      const f=window[n];
      if(typeof f==='function' && !f.__bb){
        const w=function(){ try{ window.HZG_EV(n, arguments[0]); }catch(e){} return f.apply(this, arguments); };
        w.__bb=true; window[n]=w;
      }
    }
  };
  hook();
  const iv=setInterval(hook, 2000);
  setTimeout(function(){ clearInterval(iv); }, 60000);
}
/* --- 落ちた時の記録 --- */
const BB_ERR=[];
function bbErr(o){ BB_ERR.push(o); if(BB_ERR.length>20) BB_ERR.shift(); }
window.addEventListener('error', function(e){
  bbErr({t:new Date().toISOString(), kind:'error', msg:String(e.message||'').slice(0,200),
    at:String(e.filename||'').split('/').pop()+':'+e.lineno,
    stack:String((e.error&&e.error.stack)||'').slice(0,700)});
});
window.addEventListener('unhandledrejection', function(e){
  const r=e.reason;
  bbErr({t:new Date().toISOString(), kind:'promise', msg:String((r&&r.message)||r||'').slice(0,200),
    stack:String((r&&r.stack)||'').slice(0,700)});
});
/* --- 絵や音の読み込み失敗 --- */
const BB_IMGERR=[];
window.addEventListener('error', function(e){
  const t=e.target;
  if(t && (t.tagName==='IMG'||t.tagName==='AUDIO') && t.src){
    const s=String(t.src).split('/').pop();
    if(BB_IMGERR.indexOf(s)<0 && BB_IMGERR.length<20) BB_IMGERR.push(s);
  }
}, true);

/* --- コマ落ちの記録(直近10秒) --- */
const BB_FPS=[];
/* JSの忙しさも録る(2026-08-19 K1の切り分け用):
   「コマが遅い」だけでは、JSが忙しいのか・描画が重いのか・端末側の事情かが分からない。
   長いタスク(50ms超)の合計時間を録り、直近10秒の「主スレッドが塞がっていた割合」を出す */
let BB_BUSY=[];
try{
  new PerformanceObserver(function(list){
    for(const e of list.getEntries()) BB_BUSY.push([performance.now(), e.duration]);
  }).observe({entryTypes:['longtask']});
}catch(e){}
/* 1コマの実際の仕事時間(rAFの中身のms)。「長いタスク0」は50ms超が無いだけで、
   毎コマ15ms級の仕事は写らない——60fpsに必要なのは毎コマ16ms以内なのでこちらが本命(2026-08-19) */
let BB_WORK=[];
(function(){
  const orig=window.requestAnimationFrame.bind(window);
  window.requestAnimationFrame=function(cb){
    return orig(function(t){
      const a2=performance.now();
      cb(t);
      const ms=performance.now()-a2;
      if(ms>0.2) BB_WORK.push([a2, ms]);
      while(BB_WORK.length && a2-BB_WORK[0][0]>10000) BB_WORK.shift();
    });
  };
})();
(function bbFrame(){
  const t=performance.now();
  if(bbFrame._l){ const g=t-bbFrame._l; if(g<2000) BB_FPS.push([t,g]); }
  bbFrame._l=t;
  while(BB_FPS.length && t-BB_FPS[0][0]>10000) BB_FPS.shift();
  while(BB_BUSY.length && t-BB_BUSY[0][0]>10000) BB_BUSY.shift();
  requestAnimationFrame(bbFrame);
})();
function bbFps(){
  if(!BB_FPS.length) return null;
  const g=BB_FPS.map(function(x){ return x[1]; }).sort(function(a,b){ return a-b; });
  const q=function(p){ return Math.round(g[Math.min(g.length-1, Math.floor(g.length*p))]*10)/10; };
  const avg=g.reduce(function(a,b){ return a+b; },0)/g.length;
  const busy=BB_BUSY.reduce(function(a,x){ return a+x[1]; },0);
  const span=BB_FPS.length?(BB_FPS[BB_FPS.length-1][0]-BB_FPS[0][0]):1;
  const wk=BB_WORK.map(function(x){ return x[1]; }).sort(function(a,b){ return a-b; });
  const wq=function(p2){ return wk.length?Math.round(wk[Math.min(wk.length-1,Math.floor(wk.length*p2))]*10)/10:0; };
  const wsum=wk.reduce(function(a,b){ return a+b; },0);
  return { コマ数:g.length, fps:Math.round(1000/avg), 中央:q(.5), 遅い方1割:q(.9), 最悪:q(.999),
    詰まり:g.filter(function(x){ return x>50; }).length,
    JSが塞いだ割合:Math.round(busy/Math.max(1,span)*100)+'%', 長いタスク:BB_BUSY.length,
    コマ仕事:{中央:wq(.5), 遅い方1割:wq(.9), 最悪:wq(.999), 割合:Math.round(wsum/Math.max(1,span)*100)+'%'} };
}

/* --- 入れ物(IndexedDB。セーブとは別) --- */
function bbOpen(){
  return new Promise(function(res,rej){
    const q=indexedDB.open(BB_DB, 1);
    q.onupgradeneeded=function(){ const db=q.result;
      if(!db.objectStoreNames.contains(BB_STORE)) db.createObjectStore(BB_STORE, {keyPath:'id'}); };
    q.onsuccess=function(){ res(q.result); };
    q.onerror=function(){ rej(q.error); };
  });
}
function bbAll(){
  return bbOpen().then(function(db){ return new Promise(function(res,rej){
    const st=db.transaction(BB_STORE,'readonly').objectStore(BB_STORE);
    const r=st.getAll(); r.onsuccess=function(){ res(r.result||[]); }; r.onerror=function(){ rej(r.error); };
  }); });
}
function bbPut(rep){
  return bbOpen().then(function(db){ return new Promise(function(res,rej){
    const tx=db.transaction(BB_STORE,'readwrite');
    tx.objectStore(BB_STORE).put(rep);
    tx.oncomplete=function(){ res(true); }; tx.onerror=function(){ rej(tx.error); };
  }); });
}
function bbDel(id){
  return bbOpen().then(function(db){ return new Promise(function(res,rej){
    const tx=db.transaction(BB_STORE,'readwrite');
    tx.objectStore(BB_STORE).delete(id);
    tx.oncomplete=function(){ res(true); }; tx.onerror=function(){ rej(tx.error); };
  }); });
}

/* --- 押した瞬間の状態を確定する(UIを開く前に必ずこれを呼ぶ) --- */
function bbCapture(){
  /* 画面の実寸(2026-08-26 K1報告「下段ラベルが埋まる」の計測)。
     手元のヘッドレスでは再現しないので、実機の生の数字をここで拾う。
     レイアウト用の高さ・実際に見えている高さ・拡大率・横のはみ出し・板の位置 */
  const gaMen=(()=>{ try{
    const vv=window.visualViewport, el=document.getElementById('tabBar');
    const q=el?el.getBoundingClientRect():null;
    const probe=document.createElement('div');
    probe.style.cssText='position:fixed;left:0;top:0;width:1px;height:100dvh;visibility:hidden';
    document.body.appendChild(probe);
    const dvh=Math.round(probe.getBoundingClientRect().height);
    probe.style.height='100vh'; const vh=Math.round(probe.getBoundingClientRect().height);
    probe.remove();
    let widest=null, wmax=0;
    document.querySelectorAll('*').forEach(e=>{ const r=e.getBoundingClientRect();
      if(r.width&&r.right>wmax){ wmax=r.right; widest=e.tagName+(e.id?'#'+e.id:''); } });
    return { 見えている高さ:vv?Math.round(vv.height):null, 見えている幅:vv?Math.round(vv.width):null,
      拡大率:vv?+vv.scale.toFixed(3):null, ずれ上:vv?Math.round(vv.offsetTop):null,
      innerHeight:window.innerHeight, innerWidth:window.innerWidth,
      dvh:dvh, vh:vh, 文書の幅:document.documentElement.scrollWidth,
      vvh変数:getComputedStyle(document.documentElement).getPropertyValue('--vvh').trim()||'(なし)',
      下段ラベル:q?[Math.round(q.top),Math.round(q.bottom),Math.round(q.height)]:null,
      一番右に出ている物:widest+' '+Math.round(wmax) };
  }catch(e){ return {読めず:String(e)}; } })();
  const rep={ 画面:gaMen, id:'R'+Date.now().toString(36)+Math.random().toString(36).slice(2,5),
    at:new Date().toISOString(), memo:'',
    build:window.HZG_BUILD||'?',
    url:location.pathname+location.search,
    ua:String(navigator.userAgent).slice(0,180),
    view:{w:innerWidth, h:innerHeight, dpr:window.devicePixelRatio||1},
    /* SETTINGSはトップレベルconst(windowに載らない)——window.SETTINGSは常にundefinedで
       GPU設定中でも'dom'と記録されていた(2026-08-20 Codex指摘)。裸の識別子で読む */
    draw:(typeof SETTINGS!=='undefined' && SETTINGS.gl) ? 'webgl' : 'dom',
    fps:bbFps(), errors:BB_ERR.slice(-8), imgErrors:BB_IMGERR.slice(0,10),
    /* メモリの内訳(2026-08-26 K1報告「画面が一切描き直されない・音とタップ音だけ生きている・
       アプリを立ち上げ直すと直る」の原因特定用)。これまで画面のメーターにしか出ておらず、
       報告のJSONには入っていなかった。iOSはメモリ逼迫でタブの合成だけ止めることがあるので、
       止まった時の実数を残す */
    mem:(()=>{ try{
      const gl=(window.HZG_GLSHARE&&HZG_GLSHARE.stats)?HZG_GLSHARE.stats():null;
      return {
        音MB:+audioBufMB().toFixed(1),
        GPU板MB: gl? +(((gl.幅||2048)*(gl.上限||0)*4/1048576).toFixed(0)) : null,
        GPU板使用率: gl? (gl.使用率||null) : null,
        焼いた枚数: gl? (gl.枚数||null) : null,
        画像枚数: (()=>{ try{ return performance.getEntriesByType('resource').filter(r=>r.initiatorType==='img').length; }catch(e){ return null; } })(),
        JSヒープMB: (performance.memory&&performance.memory.usedJSHeapSize)? +(performance.memory.usedJSHeapSize/1048576).toFixed(1) : null,
        画面の状態: document.visibilityState,
      };
    }catch(e){ return {読めず:String(e)}; } })(),
    /* 教室の描き手の健康状態(2026-08-26 K1報告「教室の絵だけ止まった・音楽は正常」の原因特定用)。
       これまで報告に載っていたのは襲撃側のglAbnだけで、教室が描けているかは記録が無かった。
       板を持っているか/最後に描けてから何秒経ったか/画面に板が載っているか、で止まった枝が分かる */
    clsDraw:(()=>{ try{
      const C=window.ClassGL||null;
      const sc=document.getElementById('scene');
      const cv=sc?sc.querySelector('canvas'):null;
      return {
        板を持っている: C?!!C.owning:null,