public/dev_tools.js
分割 8 / 10 · 元ファイル 1272〜1466行付近
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;i<all.length;i++){
var r=all[i].getBoundingClientRect();
if(r.width<40 || r.height<40) continue;
if(r.width*r.height>area){ 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(){