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){