=12) out[id+'('+a[0].字+') 最大'+Math.round(mx)+'px']=a.map(x=>x.上); }
/* 盤面のずらしが動いていないか(跳びの本命)。値が変わったコマだけ並べる */
const cam=[]; let pv=null;
for(const f of L){
const k=f.盤面の位置+'|'+f.親たち+'/'+f.送り+'/'+f.見える窓のずれ+'/'+f.ずらし+'/'+f.倍率+'/'+f.拡大+'/'+f.縦ずれ+'/'+f.箱の高さ;
if(k!==pv){ cam.push({t:f.t, 盤面の位置:f.盤面の位置, 親たち:f.親たち, 送り:f.送り,
見える窓のずれ:f.見える窓のずれ, ずらし:f.ずらし, 倍率:f.倍率,
拡大:f.拡大, 縦ずれ:f.縦ずれ, 箱の高さ:f.箱の高さ}); pv=k; }
}
if(cam.length>1) out['★土台が動いた']=cam.slice(0,24);
else if(cam.length===1) out['★土台は動いていない']=cam[0];
return Object.keys(out).length? out : {跳んだ物:'なし(直近3秒)'};
}catch(e){ return null; } })(),
/* コマ仕事の内訳(2026-08-21): 襲撃ループ/教室ループの自己時間。19ms級の犯人をここで切り分ける */
perf:(()=>{ try{ const q=a=>{ if(!a||!a.length) return null; const b=a.slice().sort((x,y)=>x-y);
return {中央:+b[Math.floor(b.length/2)].toFixed(1), 遅い方1割:+b[Math.floor(b.length*0.9)].toFixed(1), 最悪:+b[b.length-1].toFixed(1)}; };
const P=window.__hzgPerf||{}; return {襲撃ループ:q(P.raid), 教室ループ:q(P.cls)}; }catch(e){ return null; } })(),
/* 切り替え工程の時刻印(全撤去TODO段0・2026-08-23): 実機のカクつきがどの工程間で起きたかを読む */
switchLog:(()=>{ try{ return ((window.__hzgPerf||{}).sw||[]).slice(-24); }catch(e){ return null; } })(),
/* 発熱計(2026-08-23 K1報告): 描画回数/秒と隠れている間の活動。隠れて動いた秒数>0なら異常 */
heat:(()=>{ try{ const R=window.__heatRing||[]; if(!R.length) return null;
const last=R.slice(-60);
const sum=(a,k)=>a.reduce((x,y)=>x+y[k],0);
const hid=R.filter(x=>x.h);
return { 直近60秒:{ 教室描画_平均毎秒:+(sum(last,'p')/last.length).toFixed(1),
教室描画_最大毎秒:Math.max.apply(null,last.map(x=>x.p)),
戦闘描画_平均毎秒:+(sum(last,'d')/last.length).toFixed(1) },
隠れていた秒数:hid.length,
隠れて動いた秒数:hid.filter(x=>x.p>0||x.d>0).length };
}catch(e){ return null; } })(),
events:BB_EV.slice(-100) };
try{
const js=document.querySelector('script[src*="raid_battle.js"]');
if(js) rep.jsver=String(js.getAttribute('src')||'').split('?')[1]||'';
}catch(e){}
try{ if(typeof game!=='undefined' && game){
rep.screen={ phase:game.phase, day:game.day, slot:game.slot,
alive:(game.students||[]).filter(function(s){ return ['healthy','injured','suspect'].indexOf(s.state)>=0; }).length };
rep.daily=slimDaily(game);
} }catch(e){ rep.screenErr=String(e).slice(0,120); }
try{ if(typeof window.HZG_SNAPSHOT==='function') rep.raid=window.HZG_SNAPSHOT(); }catch(e){ rep.raidErr=String(e).slice(0,200); }
try{ rep.dom=domCount(); }catch(e){}
/* BGMの内部状態(2026-08-21 K1報告「小刻みにリピート」の原因特定用の覗き窓)。
曲名・どちら側が鳴っているか・再生位置・終端との距離・停止/終了/エラーの旗を写す */
try{
const out={wanted:(typeof bgmWanted!=='undefined')?bgmWanted:undefined,
playing:(typeof bgmSrc!=='undefined')?!!bgmSrc:undefined,
ctx:(typeof actx!=='undefined'&&actx)?actx.state:null, tracks:{}};
if(typeof bgmEls!=='undefined') for(const k in bgmEls){
const e2=bgmEls[k]; if(!e2) continue;
const side=s2=>({t:+(s2.el.currentTime||0).toFixed(2), dur:+(s2.el.duration||0).toFixed(2),
paused:s2.el.paused, ended:s2.el.ended, ready:s2.el.readyState,
gain:+(s2.g&&s2.g.gain?s2.g.gain.value:0).toFixed(3),
err:s2.el.error?String(s2.el.error.code):null});
out.tracks[k]={cur:e2.cur?(e2.cur===e2.a?'a':'b'):null, timer:!!e2.timer, a:side(e2.a), b:side(e2.b)};
}
const rb=window.__HZG_BGM_EL||{};
for(const k in rb){ const e3=rb[k]; if(e3&&e3.el) out.tracks[k]={t:+(e3.el.currentTime||0).toFixed(2),
paused:e3.el.paused, ended:e3.el.ended, gain:+(e3.g&&e3.g.gain?e3.g.gain.value:0).toFixed(3)}; }
rep.bgm=out;
}catch(e){ rep.bgmErr=String(e).slice(0,120); }
/* IndexedDBはDOM要素や循環参照を保存できないので、ここで安全な形へ落としてから返す */
try{ return JSON.parse(safeJson(rep)); }
catch(e){ return {id:rep.id, at:rep.at, memo:'', err:'保存できない値が混じっていた: '+String(e).slice(0,120)}; }
}
/* --- 常設ボタンと件数バッジ --- */
const BB_MARU='①②③④⑤⑥⑦⑧⑨⑩';
function bbBuildBtn(){
if(bbBtn) return;
bbBtn=document.createElement('button');
bbBtn.id='hzgBugBtn';
bbBtn.type='button';
/* 置き場所(2026-08-25 K1指示): 画面右の縦置きをやめ、下段ラベルの工作(右端)の真上へ横書きで置く。
左右は工作ボタンの中心(5個並びなので幅の90%)に合わせ、下は下段ラベルのすぐ上(2026-08-25 K1指示でさらに10px下げた。--tabHは index.html の :root) */
bbBtn.style.cssText='position:fixed; left:90%; bottom:calc(var(--tabH, 62px) - 4px); transform:translateX(-50%); z-index:2147483000;'
+'font:bold 11px/1.15 -apple-system,sans-serif; color:#fff2e0; background:rgba(150,40,30,.8);'
+'border:1px solid rgba(255,150,120,.65); border-radius:9px; padding:5px 8px; letter-spacing:.06em;'
+'white-space:nowrap; box-shadow:0 2px 8px rgba(0,0,0,.5);'
+'-webkit-user-select:none; user-select:none; -webkit-tap-highlight-color:transparent;';
/* 押し方で分ける(2026-08-28 K1指示。テスターに使いやすく)。
・軽く押す = 「何が起きた?」だけ書いて送る小さな窓(テスター用)
・長押し = 今までの報告箱(K1用。一覧・選択・書き出し)
どちらの場合も、窓を出す前に必ず状態を確定する——画面を触ると状態が変わるため */
var bbHold=null, bbLong=false;
function bbStart(){ bbLong=false; clearTimeout(bbHold); bbTimeHold(true);
bbHold=setTimeout(function(){ bbLong=true;
const rep=bbCapture();
bbPut(rep).then(function(){ bbRefresh(); bbOpenWin(rep.id); })
.catch(function(e){ toast('報告の保存に失敗: '+e); });
}, 600); }
function bbEnd(ev){ clearTimeout(bbHold);
if(bbLong){ bbLong=false; if(ev){ ev.preventDefault(); ev.stopPropagation(); } return; }
if(ev){ ev.preventDefault(); ev.stopPropagation(); }
bbQuickOpen();
}
bbBtn.addEventListener('pointerdown', function(ev){ ev.stopPropagation(); bbStart(); });
bbBtn.addEventListener('pointerup', bbEnd);
bbBtn.addEventListener('pointercancel', function(){ clearTimeout(bbHold); bbLong=false; });
bbBtn.addEventListener('pointerleave', function(){ clearTimeout(bbHold); });
bbBtn.addEventListener('click', function(ev){ ev.preventDefault(); ev.stopPropagation(); });
document.body.appendChild(bbBtn);
bbRefresh();
}
function bbRefresh(){
bbAll().then(function(list){
bbCount=list.length;
if(bbBtn) bbBtn.textContent='報告'+(bbCount?BB_MARU[Math.min(9,bbCount-1)]:'');
}).catch(function(){});
}
/* --- テスター用の小さな窓(2026-08-28 K1指示) ---
軽く押した時に出る。書くのは「何が起きたか」だけで、裏では今までどおり
押した瞬間の状態をJSONで確定して端末へ保存し、そのままサーバーへ送る。
送った直後だけ「間違えて送った」を出す——消しはせず、印だけ付ける
(K1が一覧で「これは間違いだな」と目で分かればいい、という指示) */
var bbQuickWin=null, bbLastSent=null;
function bbQuickClose(){ if(bbQuickWin){ bbQuickWin.remove(); bbQuickWin=null; } bbHoldSync(); }
function bbQuickOpen(){
if(bbQuickWin) return;
const rep=bbCapture(); /* ★窓を出す前に確定する */
const w=document.createElement('div');
bbQuickWin=w;
w.style.cssText='position:fixed; inset:0; z-index:2147483002; display:flex; align-items:center;'
+'justify-content:center; padding:18px; background:rgba(6,10,8,.72);'
+'font:13px/1.6 -apple-system,"Hiragino Kaku Gothic ProN",sans-serif;';
const box=document.createElement('div');
box.style.cssText='width:100%; max-width:330px; background:#f7f2e2; color:#2e3a2e; border-radius:14px;'
+'padding:16px 15px 14px; box-shadow:0 10px 30px rgba(0,0,0,.6);';
box.innerHTML='なにが起こったか教えてください
'
+''
+'簡単な説明で大丈夫です。画面の様子は自動で一緒に送られます。
';
const ta=document.createElement('textarea');
ta.placeholder='例: 扉を押したら動かなくなった';
ta.style.cssText='width:100%; height:92px; margin-top:10px; padding:9px; border-radius:9px;'
+'border:1px solid #b3a888; background:#fffdf5; color:#2e3a2e;'
+'font:14px/1.6 inherit; resize:none; -webkit-appearance:none;';
box.appendChild(ta);
/* 画面の絵(2026-09-02 K1指示)。文字だけでも送れるし、絵に印を付けても送れる */
const shotRow=document.createElement('div');
shotRow.style.cssText='display:flex; gap:8px; align-items:center; margin-top:10px;';
const shotBtn=document.createElement('button');
shotBtn.type='button'; shotBtn.textContent='画面の絵を付ける';
shotBtn.style.cssText='flex:1; padding:11px 6px; border:1px dashed #a8997a; border-radius:10px;'
+'font-weight:900; font-size:13px; background:#efe8d4; color:#4a4436;';
const shotThumb=document.createElement('img');
shotThumb.style.cssText='display:none; width:46px; height:46px; object-fit:cover;'
+'border-radius:8px; border:1px solid #b3a888;';
const shotDel=document.createElement('button');
shotDel.type='button'; shotDel.textContent='外す';
shotDel.style.cssText='display:none; padding:11px 10px; border:0; border-radius:10px;'
+'font-weight:900; font-size:12px; background:#ddd5c0; color:#4a4436;';
shotRow.appendChild(shotBtn); shotRow.appendChild(shotThumb); shotRow.appendChild(shotDel);
box.appendChild(shotRow);
const shotHint=document.createElement('div');
shotHint.style.cssText='margin-top:5px; font-size:10.5px; color:#8a8069; line-height:1.7;';
shotHint.textContent='押すと今の画面がそのまま出ます。気になる所を指でなぞってください。';
box.appendChild(shotHint);
function shotShow(){
const has=!!rep.shot;
shotThumb.style.display=has?'block':'none';
shotDel.style.display=has?'block':'none';
if(has) shotThumb.src=rep.shot;
shotBtn.textContent=has?'描き直す':'画面の絵を付ける';
shotHint.textContent=has
? ('絵を付けました('+Math.round(rep.shot.length/1024)+'KB)。押すと描き直せます。')
: '押すと今の画面がそのまま出ます。気になる所を指でなぞってください。';
}
shotBtn.onclick=function(ev){ ev.stopPropagation();
shotBtn.disabled=true; shotBtn.textContent='画面を写しています…';
/* 報告の窓が写り込まないよう、写している間だけ隠す */
var w0=bbQuickWin; if(w0) w0.style.visibility='hidden';
bbShotNow().then(function(url){
if(w0) w0.style.visibility='';
shotBtn.disabled=false; shotShow();
if(url) bbShotEdit(url, function(u){ rep.shot=u; shotShow(); });
else toast('画面を写せませんでした');
}).catch(function(e){
if(w0) w0.style.visibility='';
shotBtn.disabled=false; shotShow(); toast('画面を写せなかった: '+e.message);
});
};
shotDel.onclick=function(ev){ ev.stopPropagation(); rep.shot=null; shotShow(); };
const row=document.createElement('div');
row.style.cssText='display:flex; gap:8px; margin-top:12px;';
const mk=function(label,bg,fg){
const b=document.createElement('button'); b.type='button'; b.textContent=label;
b.style.cssText='flex:1; padding:13px 6px; border:0; border-radius:10px; font-weight:900;'
+'font-size:15px; background:'+bg+'; color:'+fg+';';
row.appendChild(b); return b; };
const cancel=mk('キャンセル','#ddd5c0','#4a4436');
const send=mk('報告する','#a94037','#fff4ea');
box.appendChild(row);
const note=document.createElement('div');
note.style.cssText='margin-top:10px; text-align:center; min-height:18px; font-size:11.5px; color:#6b6250;';
box.appendChild(note);
w.appendChild(box); document.body.appendChild(w);
setTimeout(function(){ try{ ta.focus(); }catch(e){} }, 60);
cancel.onclick=function(ev){ ev.stopPropagation(); bbQuickClose(); };
w.addEventListener('click', function(ev){ if(ev.target===w) bbQuickClose(); });
send.onclick=function(ev){
ev.stopPropagation();
send.disabled=true; cancel.disabled=true; send.textContent='送っています…';
rep.memo=ta.value.slice(0,400);
bbPut(rep).then(function(){ bbRefresh(); }).catch(function(){});
const body=JSON.stringify(Object.assign({}, rep, {
note:rep.memo, ua:navigator.userAgent,
who:(function(){ try{ return localStorage.getItem('hzg_tester')||''; }catch(e){ return ''; } })()
}));
fetch('/api/bug', {method:'POST', headers:{'content-type':'application/json'}, body:body})
.then(function(r){ return r.ok ? r.json() : Promise.reject(r.status); })
.then(function(d){ bbLastSent=d.id; bbQuickDone(box, note, true); })
.catch(function(){ bbQuickDone(box, note, false); });
};
}
/* --- 画面の絵を付ける(2026-09-02 K1指示) ---
K1「スクショしてそこに印つけて送れる機能を作れ。従来の文字だけで送るか、
その画面のスクショにまるとかをペンで書いて送るかできる感じに」
絵はゲームの中で写さず、**端末で撮ったスクショを選んでもらう**。
理由: GPU描画のcanvasはtoDataURLで空の絵になることがある(描いた直後でないと中身が残らない)。
撮った本人の画面をそのまま持ってくる方が確実で、ゲームの外の崩れも写る。
選んだ絵は900pxまで縮めてJPEGにする——1件512KBの上限に収めるため */
var BB_SHOT_MAX=1200, BB_SHOT_BYTES=260*1024; // 900→1200(2026-09-04): 900だと3倍の画面から落としすぎて別物に見えていた
/* 今の画面をそのまま1枚の絵にする(2026-09-02 K1指示)。
報告を押した瞬間ゲームの時間は止まっているので、次に描かれるコマは今見えている画そのもの。
GPUの盤面は「描き終えた直後」でないと中身が残らないので、絵の受け取りはgl_sprites側の
コマの終わりに仕込んである(window.__HZG_GRAB)。
盤面が動いていない画面(タイトル・名簿など)では取れないので、その時は写真を選ぶ方へ回す */
function bbGrabScreen(){
return new Promise(function(res){
var done=false;
try{
window.__HZG_GRAB=function(url){ if(done) return; done=true; res(url||null); };
}catch(e){ res(null); return; }
setTimeout(function(){ if(done) return; done=true;
try{ window.__HZG_GRAB=null; }catch(e){}
res(null); }, 700); // 描かれないまま待たされない
});
}
/* 画面をその場で1枚の絵にする(2026-09-02 K1指示の作り直し)。
K1「なんでiPhoneから写真を添付する画面出るんだよ。押したらその画面のスクショを
そのままペイントして報告と一緒に送れるようにしろ」——写真選びへ逃げるのをやめる。
・盤面(教室・戦闘)はGPUが描いているので、コマの終わりに取る(__HZG_GRAB)
・それ以外の画面(購買部・名簿・タイトル…)はHTMLなので、html2canvasで写す
・両方ある時は、HTMLの絵の上へ盤面の絵を同じ位置で重ねる
html2canvasは同梱(lib_html2canvas.js)。押した時に初めて読み込む */
function bbH2C(){
if(window.html2canvas) return Promise.resolve(window.html2canvas);
if(bbH2C._p) return bbH2C._p;
return bbH2C._p=new Promise(function(res,rej){
var sc=document.createElement('script');
sc.src='lib_html2canvas.js?v=20260902zp';
sc.onload=function(){ res(window.html2canvas); };
sc.onerror=function(){ rej(new Error('画面を写す道具が読めなかった')); };
document.head.appendChild(sc);
});
}
/* 報告の窓など「写したくない物」を外す */
function bbShotSkip(el){
try{
if(!el || !el.classList) return false;
if(el===bbQuickWin || el===bbWin || el===bbBtn) return true;
if(el.id==='hzgUiedCross') return true;
return false;
}catch(e){ return false; }
}
/* 報告に付ける絵(2026-09-04 K1報告「iPhone13でそのまま表示されてるスクショになってねえよ」の直し)。
直したのは2つ。
①**粗さ**: HTMLの写しを等倍(390x797)で作っていた。iPhone 13の実際の画面は3倍の細かさなので、
字も線も潰れて別物に見えていた。端末の細かさぶん(上限2倍)で写す。
②**盤面の貼り位置**: GPUの板を重ねる時、置き場所を「文書で最初に見つかったcanvas」から
取っていた。別のcanvasが先にあると見当違いの所へ貼られる。板そのものを名指しする。
なお、iOSのSafariにはJavaScriptから本物のスクショを撮る方法が無い。
HTMLの部分は html2canvas が描き直した近似で、盤面だけが本物の画素。 */
function bbGlCanvas(){
try{ if(window.__HZG_GLCANVAS && window.__HZG_GLCANVAS.getBoundingClientRect) return window.__HZG_GLCANVAS; }catch(e){}
/* 名指しが無い時は、画面に出ている一番大きいcanvasを選ぶ(最初の1個ではなく) */
try{
var best=null, area=0;
var all=document.querySelectorAll('canvas');
for(var i=0;iarea){ area=r.width*r.height; best=all[i]; }
}
return best;
}catch(e){ return null; }
}
function bbShotNow(){
var K=Math.max(1, Math.min(2, window.devicePixelRatio||1)); // 端末の細かさぶん(上限2倍)
return bbH2C().then(function(h2c){
return h2c(document.body, {
backgroundColor:'#000', scale:K, logging:false, useCORS:true, allowTaint:true,
width:window.innerWidth, height:window.innerHeight,
windowWidth:window.innerWidth, windowHeight:window.innerHeight,
ignoreElements:bbShotSkip
});
}).then(function(page){
/* 盤面(GPU)が動いていれば、その絵を同じ場所へ重ねる */
return bbGrabScreen().then(function(gl){
if(!gl) return page.toDataURL('image/jpeg', .9);
return new Promise(function(res){
var im=new Image();
im.onload=function(){
try{
var el=bbGlCanvas();
var g=page.getContext('2d');
if(el){
var r=el.getBoundingClientRect();
/* html2canvasの絵はK倍で作ってあるので、貼る枠も同じ倍率に合わせる */
g.drawImage(im, r.left*K, r.top*K, r.width*K, r.height*K);
}
res(page.toDataURL('image/jpeg', .9));
}catch(e){ res(page.toDataURL('image/jpeg', .9)); }
};
im.onerror=function(){ res(page.toDataURL('image/jpeg', .9)); };
im.src=gl;
});
});
});
}
function bbShotPick(cb){
var inp=document.createElement('input');
inp.type='file'; inp.accept='image/*';
inp.style.cssText='position:fixed;left:-9999px;top:0;';
document.body.appendChild(inp);
inp.onchange=function(){
var f=inp.files && inp.files[0];
inp.remove();
if(!f) return;
var fr=new FileReader();
fr.onload=function(){ bbShotEdit(String(fr.result), cb); };
fr.readAsDataURL(f);
};
inp.click();
}
/* 絵の上に指で描く窓。丸でも矢印でも、指で好きに */
function bbShotEdit(src, cb){
var im=new Image();
im.onload=function(){
var k=Math.min(1, BB_SHOT_MAX/Math.max(im.width, im.height));
var W=Math.round(im.width*k), H=Math.round(im.height*k);
var base=document.createElement('canvas'); base.width=W; base.height=H;
base.getContext('2d').drawImage(im,0,0,W,H);
var w=document.createElement('div');
w.style.cssText='position:fixed; inset:0; z-index:2147483003; background:#0b0e0c;'
+'display:flex; flex-direction:column; touch-action:none;'
+'font:13px/1.6 -apple-system,"Hiragino Kaku Gothic ProN",sans-serif; color:#f7f0dd;';
var head=document.createElement('div');
head.style.cssText='flex:0 0 auto; padding:calc(8px + env(safe-area-inset-top)) 10px 8px;'
+'display:flex; gap:6px; align-items:center; background:#1a241e; border-bottom:1px solid #3d5c4c;';
/* 確定と取り消しは上に小さく置く。下に6個並べると折り返して潰れ、
どれで確定するのか分からなくなっていた(2026-09-03 K1指摘) */
var bCancel=document.createElement('button'); bCancel.type='button'; bCancel.textContent='やめる';
bCancel.style.cssText='flex:0 0 auto;padding:6px 11px;border:0;border-radius:8px;'
+'font-weight:900;font-size:12px;background:#3d5c4c;color:#cfe0d5;';
var ttl=document.createElement('span');
ttl.style.cssText='flex:1 1 auto;text-align:center;font-weight:900;font-size:12.5px;';
ttl.textContent='気になる所に印を付ける';
var bOk=document.createElement('button'); bOk.type='button'; bOk.textContent='確定';
bOk.style.cssText='flex:0 0 auto;padding:6px 15px;border:0;border-radius:8px;'
+'font-weight:900;font-size:12.5px;background:#e0b048;color:#1a241e;';
head.appendChild(bCancel); head.appendChild(ttl); head.appendChild(bOk);
var stage=document.createElement('div');
stage.style.cssText='flex:1 1 auto; min-height:0; display:flex; align-items:center;'
+'justify-content:center; padding:6px; position:relative;';
var wrap=document.createElement('div');
wrap.style.cssText='position:relative; max-width:100%; max-height:100%;';
base.style.cssText='display:block; max-width:100%; max-height:100%; width:auto; height:auto;';
var pen=document.createElement('canvas'); pen.width=W; pen.height=H;
pen.style.cssText='position:absolute; left:0; top:0; width:100%; height:100%; touch-action:none;';
wrap.appendChild(base); wrap.appendChild(pen); stage.appendChild(wrap);
var foot=document.createElement('div');
foot.style.cssText='flex:0 0 auto; padding:8px 10px calc(8px + env(safe-area-inset-bottom));'
+'background:#1a241e; border-top:1px solid #3d5c4c; display:flex; gap:6px;';
function btn(label,bg,fg,grow){
var b=document.createElement('button'); b.type='button'; b.textContent=label;
b.style.cssText='flex:'+(grow||1)+' 1 0; padding:11px 4px; border:0; border-radius:10px;'
+'font-weight:900; font-size:13px; background:'+bg+'; color:'+fg+';';
foot.appendChild(b); return b;
}
var col='#ff3b2f';
var bRed=btn('赤','#ff3b2f','#fff'), bYel=btn('黄','#ffd23b','#3a2c00');
var bUndo=btn('1つ戻す','#3d5c4c','#e8f0ea'), bClr=btn('全部消す','#3d5c4c','#e8f0ea');
function mark(){ bRed.style.outline=(col==='#ff3b2f')?'3px solid #fff':'none';
bYel.style.outline=(col==='#ffd23b')?'3px solid #fff':'none'; }
bRed.onclick=function(){ col='#ff3b2f'; mark(); };
bYel.onclick=function(){ col='#ffd23b'; mark(); };
mark();
w.appendChild(head); w.appendChild(stage); w.appendChild(foot);
document.body.appendChild(w);
var g=pen.getContext('2d');
g.lineCap='round'; g.lineJoin='round';
var strokes=[], cur=null, id=null;
function redraw(){
g.clearRect(0,0,W,H);
for(var i=0;iBB_SHOT_BYTES && q>0.35){ q-=0.12; url=out.toDataURL('image/jpeg', q); }
w.remove();
cb(url);
};
};
im.onerror=function(){ toast('その絵は読めませんでした'); };
im.src=src;
}
/* 送った後の姿。ここで初めて「間違えて送った」を出す */
function bbQuickDone(box, note, ok){
box.innerHTML='';
const h=document.createElement('div');
h.style.cssText='font-size:15px;font-weight:900;letter-spacing:.04em;text-align:center;';
h.textContent = ok ? 'ありがとうございます。送りました。' : '送れませんでした';
box.appendChild(h);
if(!ok){ // 失敗した時だけ、どうすればいいかを1行だけ出す
const p=document.createElement('div');
p.style.cssText='margin-top:7px;font-size:11.5px;color:#6b6250;line-height:1.7;text-align:center;';
p.textContent='電波の届く所でもう一度お願いします。';
box.appendChild(p);
}
const row=document.createElement('div');
row.style.cssText='display:flex; gap:8px; margin-top:13px;';
const close=document.createElement('button');
close.type='button'; close.textContent='閉じる';
close.style.cssText='flex:1; padding:13px 6px; border:0; border-radius:10px; font-weight:900;'
+'font-size:15px; background:#28533f; color:#f7f0dd;';
close.onclick=function(ev){ ev.stopPropagation(); bbQuickClose(); };
row.appendChild(close);
box.appendChild(row);
if(ok && bbLastSent){
const oops=document.createElement('button');
oops.type='button'; oops.textContent='間違えて送ってしまった';
oops.style.cssText='width:100%; margin-top:9px; padding:11px 6px; border:0; border-radius:10px;'
+'font-weight:700; font-size:13px; background:#e6dcc4; color:#7a3b32;';
oops.onclick=function(ev){
ev.stopPropagation();
oops.disabled=true; oops.textContent='印を付けています…';
fetch('/api/bug?oops=1&id='+encodeURIComponent(bbLastSent), {cache:'no-store'})
.then(function(){ oops.textContent='「間違い」と印を付けました'; })
.catch(function(){ oops.textContent='印を付けられませんでした'; });
};
box.appendChild(oops);
}
}
/* --- 一覧の画面 --- */
function bbOpenWin(focusId){
if(bbWin){ bbWin.remove(); bbWin=null; }
bbWin=document.createElement('div');
bbWin.style.cssText='position:fixed; inset:4% 3%; z-index:2147483001; background:rgba(10,16,13,.97);'
+'border:1px solid #4d7a5f; border-radius:12px; padding:9px; display:flex; flex-direction:column; gap:7px;'
+'font:12px/1.5 -apple-system,sans-serif; color:#e6f2ea;';
const head=document.createElement('div');
head.style.cssText='display:flex; align-items:center; gap:6px;';
head.innerHTML='バグ報告箱'
+'押した瞬間の状態を保存しました';
const close=document.createElement('button');
close.textContent='閉じる';
close.style.cssText='margin-left:auto; padding:6px 10px; border:0; border-radius:8px; background:#28533f; color:#f7f0dd; font-weight:bold;';
close.onclick=function(){ bbWin.remove(); bbWin=null; bbHoldSync(); };
head.appendChild(close);
const listEl=document.createElement('div');
listEl.style.cssText='flex:1; overflow:auto; display:flex; flex-direction:column; gap:6px; -webkit-overflow-scrolling:touch;';
/* 計器の入切(2026-09-06 K1指示「規定は全部オフ」「完全にオフのモードも作れ」)。
追う時だけ点けて、終わったら消す。切ってある計器は記録を作る所へ入らないので、
点けていない間は本当に何もしない */
const met=document.createElement('div');
met.style.cssText='display:flex; gap:4px; flex-wrap:wrap; align-items:center;'
+'padding:6px 0 2px; border-top:1px solid #3d5c4c; margin-top:4px;';
const NAMES=['効果音','床の物','必殺技','絵の大きさ','起動','音の口','曲','画面','コマ仕事','物理','スポット'];
function metDraw(){
met.innerHTML='計器'
+(HZG_METERS.全部切る?'(全部切っています)':'')+'';
const all=document.createElement('button');
all.textContent=HZG_METERS.全部切る?'全部入れる':'全部切る';
all.style.cssText='font-size:10.5px; font-weight:bold; padding:6px 9px; border-radius:8px;'
+'border:1px solid #b0523f; background:'+(HZG_METERS.全部切る?'#c2503a':'#2a1a16')+';'
+'color:#f7f0dd;';
all.onclick=function(){ HZG_METERS.set('全部切る', !HZG_METERS.全部切る); metDraw(); };
met.appendChild(all);
NAMES.forEach(function(nm){
const b=document.createElement('button');
const on=HZG_METERS.on(nm);
b.textContent=nm;
b.style.cssText='font-size:10.5px; font-weight:bold; padding:6px 9px; border-radius:8px;'
+'border:1px solid #4d7a5f; background:'+(on?'#d98d2b':'#22322a')+';'
+'color:'+(on?'#2b1a06':'#7f8f99')+';'
+(HZG_METERS.全部切る?'opacity:.45;':'');
b.onclick=function(){ HZG_METERS.set(nm, !HZG_METERS[nm]); metDraw(); };
met.appendChild(b);
});
const note=document.createElement('div');
note.style.cssText='flex:0 0 100%; font-size:9.5px; color:#8fa2ab; line-height:1.6; margin-top:3px;';
note.textContent='橙=点いている。既定は「効果音・床の物・必殺技・絵の大きさ」が消えています'
+'(毎回DOMを触ったり入れ物を作るので重い)。追う時だけ点けてください。';
met.appendChild(note);
}
metDraw();
const foot=document.createElement('div');
foot.style.cssText='display:flex; gap:5px; flex-wrap:wrap;';
bbWin.appendChild(head); bbWin.appendChild(listEl); bbWin.appendChild(met); bbWin.appendChild(foot);
document.body.appendChild(bbWin);
const sel={};
function draw(){
return bbAll().then(function(list){
list.sort(function(a,b){ return a.at