public/dev_tools.js
分割 1 / 10 · 元ファイル 1〜183行付近
/* 開発者向け隠しツール(2026-08-07 K1指示)。
★リリース時はこの1行をfalseにするだけで全部止まる(K1指示: 隠し機能を消せるように)★
呼び出し: 画面をトリプルタップ(3連打)して3回目を長押し→ツールメニュー
・px物差し: ドラッグで移動、右端の○で回転(左端が軸)。半透明・300px・10px刻み
・タップ座標: ONの間、タップした場所の教室内座標(390×632系)を表示
・スナップショット: いまの戦況/ゲーム状態をJSONでクリップボードへ(バグ報告用) */
window.HZG_DEV_TOOLS = true;
/* 計器の入切(2026-09-06 K1指示「規定は全部オフ」「計器を完全にオフのモードも作れ」)。
毎コマ・毎回、入れ物を作ったりDOMを触る計器は**既定オフ**。
出来事が起きた時だけ動く軽い物(起動・音の口・曲・画面が隠れた・コマ仕事・物理の重さ)は既定オン。
HZG_METERS.全部切る=true にすると、軽い物も含めて**1つ残らず止まる**。
ここは端末に保存する(🐛の「計器」から切り替え)。バグを追う時だけ点けて、終わったら消す。 */
window.HZG_METERS = (function(){
var D = { 全部切る:false, 効果音:false, 床の物:false, 必殺技:false, 絵の大きさ:false,
起動:true, 音の口:true, 曲:true, 画面:true, コマ仕事:true, 物理:true,
/* ★スポット計器(そのバグ専用・直したら消す)。追っている間だけ点ける物なので既定オン */
スポット:true };
var v = {};
for(var k in D) v[k]=D[k];
try{ var j=JSON.parse(localStorage.getItem('hzg_meters_v1')||'null');
if(j) for(var k2 in D) if(typeof j[k2]==='boolean') v[k2]=j[k2]; }catch(e){}
v.既定 = D;
v.on = function(name){ return !v.全部切る && !!v[name]; };
v.set = function(name, b){ v[name]=!!b;
try{ var o={}; for(var k3 in D) o[k3]=v[k3]; localStorage.setItem('hzg_meters_v1', JSON.stringify(o)); }catch(e){} };
return v;
})();
(function(){
'use strict';
if(!window.HZG_DEV_TOOLS) return;
/* ---------- 共通: 教室シーンの座標系(本編・襲撃どちらも390×632の#scene) ---------- */
function findScene(){
const m=document.getElementById('mount');
if(m && m.shadowRoot){ const sc=m.shadowRoot.getElementById('scene'); if(sc) return sc; }
return document.getElementById('scene');
}
function toSceneXY(cx,cy){
const sc=findScene(); if(!sc) return null;
const r=sc.getBoundingClientRect(); if(!r.width) return null;
const k=r.width/390;
return { x:Math.round((cx-r.left)/k), y:Math.round((cy-r.top)/k) };
}
/* ---------- ① px物差し ---------- */
let ruler=null, angle=0, px=60, py=200;
function buildRuler(){
const r=document.createElement('div');
r.id='hzgRuler';
r.style.cssText='position:fixed; left:0; top:0; width:300px; height:44px; z-index:999999;'
+'touch-action:none; -webkit-user-select:none; user-select:none; transform-origin:0 50%;'
+'background:rgba(255,244,200,.42); border:1px solid rgba(120,90,20,.6); border-radius:4px;'
+'box-shadow:0 2px 8px rgba(0,0,0,.25);';
const ticks=document.createElement('div');
ticks.style.cssText='position:absolute; inset:0; pointer-events:none;'
+'background:'
+'repeating-linear-gradient(90deg, rgba(60,40,10,.75) 0 1px, transparent 1px 10px),'
+'repeating-linear-gradient(90deg, rgba(60,40,10,.9) 0 1px, transparent 1px 50px);'
+'background-size:100% 8px, 100% 15px; background-repeat:no-repeat;';
r.appendChild(ticks);
for(let v=0; v<=300; v+=50){
const lb=document.createElement('span');
lb.textContent=v;
lb.style.cssText='position:absolute; top:17px; left:'+(v-11)+'px; width:22px; text-align:center;'
+'font:9px/1 -apple-system,sans-serif; color:rgba(60,40,10,.85); pointer-events:none;';
r.appendChild(lb);
}
const deg=document.createElement('span');
deg.id='hzgRulerDeg';
deg.style.cssText='position:absolute; left:50%; bottom:2px; transform:translateX(-50%);'
+'font:9px/1 -apple-system,sans-serif; color:rgba(60,40,10,.7); pointer-events:none;';
r.appendChild(deg);
const hd=document.createElement('div');
hd.id='hzgRulerHandle';
hd.textContent='↻';
hd.style.cssText='position:absolute; right:-15px; top:50%; margin-top:-15px; width:30px; height:30px;'
+'border-radius:50%; background:rgba(40,83,63,.85); color:#f7f0dd; font:16px/30px sans-serif;'
+'text-align:center; border:1px solid rgba(255,255,255,.4); touch-action:none;';
r.appendChild(hd);
let drag=null;
r.addEventListener('pointerdown', ev=>{
if(ev.target===hd) return;
ev.stopPropagation(); ev.preventDefault();
drag={x0:ev.clientX, y0:ev.clientY, px0:px, py0:py};
try{ r.setPointerCapture(ev.pointerId); }catch(e){}
});
r.addEventListener('pointermove', ev=>{
if(!drag) return;
px=drag.px0+(ev.clientX-drag.x0); py=drag.py0+(ev.clientY-drag.y0);
placeRuler();
});
const dEnd=()=>{ drag=null; };
r.addEventListener('pointerup', dEnd);
r.addEventListener('pointercancel', dEnd);
hd.addEventListener('pointerdown', ev=>{
ev.stopPropagation(); ev.preventDefault();
try{ hd.setPointerCapture(ev.pointerId); }catch(e){}
const mv=e2=>{ angle=Math.atan2(e2.clientY-py, e2.clientX-px)*180/Math.PI; placeRuler(); };
const up=()=>{ hd.removeEventListener('pointermove',mv); hd.removeEventListener('pointerup',up); hd.removeEventListener('pointercancel',up); };
hd.addEventListener('pointermove',mv);
hd.addEventListener('pointerup',up);
hd.addEventListener('pointercancel',up);
});
document.body.appendChild(r);
return r;
}
function placeRuler(){
ruler.style.transform='translate('+px+'px,'+(py-22)+'px) rotate('+angle+'deg)';
const d=document.getElementById('hzgRulerDeg');
if(d){ const a=((angle%360)+360)%360; d.textContent=Math.round(a)+'°'; }
}
function toggleRuler(x,y){
if(ruler){ ruler.remove(); ruler=null; return; }
px=Math.min(Math.max(10,x), innerWidth-60); py=y; angle=0;
ruler=buildRuler(); placeRuler();
}
/* ---------- ② タップ座標インスペクタ ---------- */
let inspectOn=false;
document.addEventListener('pointerdown', ev=>{
if(!inspectOn || !ev.isPrimary) return;
if(ev.target && ev.target.closest && ev.target.closest('#hzgDevMenu,#hzgRuler')) return;
const p=toSceneXY(ev.clientX, ev.clientY);
const lb=document.createElement('div');
lb.textContent=p ? (p.x+', '+p.y) : (Math.round(ev.clientX)+', '+Math.round(ev.clientY)+' (画面px)');
lb.style.cssText='position:fixed; z-index:999998; pointer-events:none;'
+'left:'+(ev.clientX+10)+'px; top:'+(ev.clientY-30)+'px;'
+'background:rgba(12,20,16,.9); color:#ffe184; font:bold 12px/1 -apple-system,sans-serif;'
+'padding:5px 8px; border-radius:7px; border:1px solid rgba(255,214,106,.5); white-space:nowrap;';
document.body.appendChild(lb);
const a=lb.animate([{opacity:1},{opacity:1,offset:.7},{opacity:0}],{duration:1400});
a.finished.then(()=>lb.remove()).catch(()=>lb.remove());
}, true);
/* ---------- ③ 不具合スナップショット ---------- */
/* DOM・関数・循環参照を安全に落とすstringify(フィールドを列挙しないので実装が変わっても壊れない) */
function safeJson(obj){
/* 「同じ物を2度見たら循環」だと、共有しているだけの値(席の座標など)まで潰れる。
今いる枝をたどって、本当に自分へ戻ってきた時だけ循環とする(2026-08-18) */
const stack=[];
return JSON.stringify(obj, function(k,v){
if(typeof v==='function') return undefined;
if(v && typeof v==='object'){
if(typeof Element!=='undefined' && v instanceof Element) return undefined;
if(typeof Animation!=='undefined' && v instanceof Animation) return undefined;
while(stack.length && stack[stack.length-1]!==this) stack.pop();
if(stack.indexOf(v)>=0) return '[循環]';
stack.push(v);
}
if(typeof v==='number') return Math.round(v*100)/100;
return v;
});
}
/* gameの丸ごと書き出しをやめ、開始時から積み上がる巨大データは要約に置き換える(2026-08-10 K1指示)。
mobPool=140人の名簿+フレーバー全文/blood=血痕1滴ごとの座標/corpses=死体のフレーバー/
既読セリフ6種の番号の束——調査で使わないのに全体の8割を占めていた */
function slimDaily(g){
const d={};
for(const k in g){
if(k==='mobPool'){ const p=g[k]||{}; d.mobPool='残り 2年'+((p.y2||[]).length)+'人・他学年'+((p.other||[]).length)+'人'; continue; }
if(k==='blood'){ let n=(g[k]||[]).length;
try{ if(typeof battleHandle!=='undefined' && battleHandle && battleHandle.bloodRefs) n=battleHandle.bloodRefs().length; }catch(e){}
d.blood='血痕'+n+'滴'; continue; } // 段2: 正本はエンジン帳面
if(k==='corpses'){ d.corpses=(g[k]||[]).map(c=>({x:c.x, y:c.y, label:c.label, fall:c.fall})); continue; }
if(/Seen$/.test(k)){ const v=g[k]; d[k]='既読'+((v&&v.size!=null)?v.size:(v?Object.keys(v).length:0))+'件'; continue; }
d[k]=g[k];
}
return d;
}
function takeSnapshot(){
const out={ at:new Date().toISOString(), ver:window.HZG_BUILD||'?', url:location.pathname+location.search };
// 職員室凍結の遠隔調査(2026-08-13): 部屋の組み立て結果と端末に残る保存キーも吐く
try{ if(typeof placeHandle!=='undefined' && placeHandle && placeHandle.roomInfo) out.roomInfo=placeHandle.roomInfo(); }catch(e){ out.roomInfo='err:'+e.message; }
try{ out.lsKeys=Object.keys(localStorage).map(k=>k+':'+(localStorage.getItem(k)||'').length); }catch(e){}
try{ if(typeof window.HZG_SNAPSHOT==='function') out.raid=window.HZG_SNAPSHOT(); }catch(e){ out.raidErr=String(e); }
try{ if(typeof game!=='undefined') out.daily=slimDaily(game); }catch(e){}
let txt;
try{ txt=safeJson(out); }catch(e){ txt='{"err":"'+String(e)+'"}'; }
const kb=Math.round(txt.length/102.4)/10;
const done=okFlag=>toast(okFlag ? 'スナップショットをコピーした ('+kb+'KB)' : 'コピー失敗→手動コピー画面を出す');
if(navigator.clipboard && navigator.clipboard.writeText){