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,