public/dev_tools.js

分割 2 / 10 · 元ファイル 184〜370行付近

原文の連続部分です。長い圧縮行は行の途中で分割する場合があります。全分割を順に連結するとファイル全文になります。

    navigator.clipboard.writeText(txt).then(()=>done(true)).catch(()=>{ done(false); manualCopy(txt); });
  } else { done(false); manualCopy(txt); }
}
function manualCopy(txt){
  const w=document.createElement('div');
  w.style.cssText='position:fixed; inset:8% 5%; z-index:999999; background:rgba(12,20,16,.97);'
    +'border:1px solid #4d7a5f; border-radius:12px; padding:10px; display:flex; flex-direction:column; gap:8px;';
  const ta=document.createElement('textarea');
  ta.value=txt; ta.readOnly=true;
  ta.style.cssText='flex:1; background:#0b130f; color:#cfe0d5; font:10px/1.4 monospace; border:0; border-radius:8px; padding:8px;';
  const bt=document.createElement('button');
  bt.textContent='閉じる';
  bt.style.cssText='padding:10px; border:0; border-radius:9px; background:#28533f; color:#f7f0dd; font-weight:bold;';
  bt.addEventListener('click',()=>w.remove());
  w.appendChild(ta); w.appendChild(bt);
  document.body.appendChild(w);
  ta.focus(); ta.select();
}
function toast(msg){
  const t=document.createElement('div');
  t.textContent=msg;
  t.style.cssText='position:fixed; left:50%; top:14%; transform:translateX(-50%); z-index:999999;'
    +'background:rgba(12,20,16,.92); color:#f7f0dd; font:bold 12px/1.4 -apple-system,sans-serif;'
    +'padding:9px 14px; border-radius:9px; border:1px solid #4d7a5f; pointer-events:none; white-space:nowrap;';
  document.body.appendChild(t);
  const a=t.animate([{opacity:0},{opacity:1,offset:.1},{opacity:1,offset:.8},{opacity:0}],{duration:1900});
  a.finished.then(()=>t.remove()).catch(()=>t.remove());
}

/* ---------- メモ帳(2026-08-07 K1指示) ----------
   開いたら毎回さっきの続き(書きかけは自動保存)。保存=1行目を見出しに一覧へ入れて新規に。
   削除=書きかけを捨てて新規に。本文コピーあり。保存済みは見出しタップで続きを書ける */
const MEMO_DRAFT='hzg_memo_draft_v1', MEMO_LIST='hzg_memo_list_v1';
let memoWin=null;
function memoList(){ try{ return JSON.parse(localStorage.getItem(MEMO_LIST)||'[]')||[]; }catch(e){ return []; } }
function openMemo(){
  if(memoWin){ memoWin.remove(); memoWin=null; }
  const w=document.createElement('div');
  memoWin=w;
  w.style.cssText='position:fixed; inset:6% 4% 8%; z-index:999999; display:flex; flex-direction:column; gap:7px;'
    +'background:rgba(12,20,16,.97); border:1px solid #4d7a5f; border-radius:13px; padding:10px;';
  const ta=document.createElement('textarea');
  ta.placeholder='メモ(書きかけは勝手に残る)';
  ta.value=localStorage.getItem(MEMO_DRAFT)||'';
  ta.style.cssText='flex:1; min-height:0; background:#0b130f; color:#eee6d4; font:13px/1.6 -apple-system,sans-serif;'
    +'border:1px solid #2c4638; border-radius:9px; padding:9px; resize:none; -webkit-user-select:text; user-select:text;';
  ta.addEventListener('input', ()=>localStorage.setItem(MEMO_DRAFT, ta.value));
  const row=document.createElement('div');
  row.style.cssText='display:flex; gap:6px;';
  const mkB=(label,bg,fn)=>{
    const b=document.createElement('button');
    b.textContent=label;
    b.style.cssText='flex:1; padding:11px 4px; border:0; border-radius:9px; background:'+bg+';'
      +'color:#f7f0dd; font:bold 12px/1 -apple-system,sans-serif;';
    b.addEventListener('click', ev=>{ ev.stopPropagation(); fn(); });
    row.appendChild(b);
  };
  const list=document.createElement('div');
  list.style.cssText='flex:0 0 auto; max-height:32%; overflow-y:auto; display:flex; flex-direction:column; gap:4px;';
  const renderList=()=>{
    list.innerHTML='';
    for(const [i,m] of memoList().entries()){
      const r=document.createElement('div');
      r.style.cssText='display:flex; align-items:center; gap:6px; background:#1a2a21; border:1px solid #2c4638;'
        +'border-radius:8px; padding:8px 9px;';
      const t=document.createElement('span');
      t.textContent=m.t||'(無題)';
      t.style.cssText='flex:1; font:bold 11.5px/1.3 -apple-system,sans-serif; color:#cfe0d5;'
        +'white-space:nowrap; overflow:hidden; text-overflow:ellipsis;';
      t.addEventListener('click', ()=>{   // 見出しタップ=そのメモの続きを書く(一覧からは外す)
        const arr=memoList(); const [mm]=arr.splice(i,1);
        localStorage.setItem(MEMO_LIST, JSON.stringify(arr));
        ta.value=mm.body; localStorage.setItem(MEMO_DRAFT, mm.body);
        renderList();
      });
      const x=document.createElement('button');
      x.textContent='✕';
      x.style.cssText='flex:0 0 auto; width:30px; height:30px; border:0; border-radius:7px;'
        +'background:#7a4040; color:#f7f0dd; font:bold 12px/1 sans-serif;';
      x.addEventListener('click', ()=>{
        const arr=memoList(); arr.splice(i,1);
        localStorage.setItem(MEMO_LIST, JSON.stringify(arr));
        renderList();
      });
      r.appendChild(t); r.appendChild(x); list.appendChild(r);
    }
  };
  mkB('保存','#28533f',()=>{
    const v=ta.value.trim();
    if(!v){ toast('空のメモは保存しない'); return; }
    const arr=memoList();
    arr.unshift({ t:v.split('\n')[0].slice(0,40), body:ta.value, at:Date.now() });
    localStorage.setItem(MEMO_LIST, JSON.stringify(arr));
    ta.value=''; localStorage.setItem(MEMO_DRAFT,'');
    renderList(); toast('保存した(新規メモに)');
  });
  mkB('本文コピー','#3f6f56',()=>{
    const v=ta.value;
    if(navigator.clipboard && navigator.clipboard.writeText){
      navigator.clipboard.writeText(v).then(()=>toast('本文をコピーした')).catch(()=>manualCopy(v));
    } else manualCopy(v);
  });
  mkB('削除','#7a4040',()=>{ ta.value=''; localStorage.setItem(MEMO_DRAFT,''); toast('消して新規メモに'); });
  mkB('閉じる','#5d5040',()=>{ w.remove(); memoWin=null; });
  w.appendChild(ta); w.appendChild(row); w.appendChild(list);
  w.addEventListener('pointerdown', ev=>ev.stopPropagation());
  document.body.appendChild(w);
  renderList();
}

/* ---------- ④ UIレイアウト編集モード(2026-08-07 K1指示) ----------
   対象のUI部品をドラッグで動かし、動かした差分だけをJSONで吐く。
   差分はlocalStorage(hzg_uiedit_v1)にも残して次回起動時も適用——
   K1がJSONをくれたらコードへ焼き込み、リセットで消す運用 */
const UIED_KEY='hzg_uiedit_v1';
/* 【2026-09-02 K1指摘の根治】K1「デプロイごとのUIの差分をJSONで吐かないといけないところを、
   UIレイアウト編集を開くごとの差分にしてないか」——そのとおりだった。
   端末に残った調整値はコードの値の上に重ねて掛かるので、こちらが数値を焼き込むと
   「焼いた値+端末の値」の二重になり、次に出す差分は**焼き込み後の位置からの差**になっていた。
   これでは何度やっても合わない。
   直し方: 調整値に**その時の版番号**を付けて持ち、版が変わったら丸ごと捨てる。
   つまり調整値は必ず「今デプロイされているコードの値」からの差になる。
   部品ごとに手で UIED_BAKED_IDS へ足していく運用も要らなくなる */
function uiedVer(){
  try{
    const js=document.querySelector('script[src*="raid_battle.js"]');
    return String((js && js.getAttribute('src')) || '').split('?')[1] || '?';
  }catch(e){ return '?'; }
}
/* 焼き込みの印(2026-08-26 K1報告の根治): K1が調整した値をコードへ焼いた時は、この文字列を変える。
   端末に残っている調整値は、焼いた値と二重に掛かって「調整が無かったことにされた」ように見える——
   実際に下段ラベルで発生した(焼き込みで11px下げ+端末の11pxが1.2秒後の再適用で乗り、22px下がった)。
   印が変わっていたら、起動時に端末の調整値を自動で捨てる(=リセットを押したのと同じ)。
   K1に「リセットを押してください」と頼まなくて済むようにするための仕掛け */
const UIED_BAKED='20260901v';
const UIED_BAKED_KEY='hzg_uiedit_baked';
/* armoryRowは armory の中にあるので、**必ずarmoryより先**に並べる——
   先に書いた方が掴める(2026-09-02) */
const UIED_IDS=['armoryRow','spPanel','wazaBtn','ctrlFab','pauseTag','dmgLbl','itemBtn','raidTestClose','armory','readyBtn','sleepBtn','goPop','hdr',
  'rationBar','amTidy','setBtn',
  'partyAsk',   // 「誰が行きますか」バー(2026-08-21 K1指示)
  /* 下段ラベル(2026-08-26 K1指示「素材に圧迫感がある」)。板と中身を別々に動かせる:
     tabBar=ドックの板そのもの / tabRow=アイコンと文字の5個ぶんまとめて。
     大きさのつまみ(%)で縮めれば圧迫感を弱められる */
  'tabBar','tabRow',
  /* 所持金と探索ライブ中継の小窓(2026-09-01 K1指示)。
     小窓は**探索に出している間しか出ない**ので、動かす時は隊を出してから編集モードに入る */
  'moneyHud','lfChip',
  /* インベントリ(2026-09-02 K1指示)。**中身と縁を別々に動かす**:
     ・armoryRow = 中のアイテム欄(66pxのマスの格子)そのもの
     ・armory    = 外の縁とタブボタンが合体した板ぜんぶ
     armoryRowをarmoryより先に並べる——重なっているので、先に書いた方が掴める。
     縁(armory)を大きくしても中のマスは大きくならない(下のuiedRowSyncが打ち消す)ので、
     「縁だけ広げて、中身は中身で動かす」ができる */
  ];
/* 【2026-09-02 K1指摘の根治その2】つまみは開くと必ず100%から始まり、
   触った瞬間にCSSへ焼いてある値(例 #armory の scale:.86)を**上書き**していた。
   つまり編集を始めた瞬間に部品が跳び、K1は「今どこにいるか」を知らないまま調整していた。
   位置も同じで、CSSに translate が焼いてある部品は掴んだ瞬間0へ戻っていた。
   直し方: 初めて掴んだ時に**今そう見えている値**(computed)を読んで、そこから始める。
   これでつまみの数字は常に「今の姿」を指し、出てくる差分は
   **そのままCSSへ書ける最終の値**になる(こちらで掛け算しない=ずれる余地が消える) */
function uiedInit(el){
  const o={dx:0, dy:0};
  try{
    const cs=getComputedStyle(el);
    const t=cs.translate;
    if(t && t!=='none'){ const p=t.split(/\s+/); o.dx=parseFloat(p[0])||0; o.dy=parseFloat(p[1]||'0')||0; }
    const sc=cs.scale;
    if(sc && sc!=='none'){ const v=parseFloat(sc); if(v>0) o.s=v; }
  }catch(e){}
  return o;
}
/* 縁(armory)の拡大を中のアイテム欄に効かせない。
   armoryRow自身のずらし・大きさは、その上に足す */
function uiedRowSync(){
  const el=uiedEl('armoryRow'); if(!el) return;
  const S=(uiedOff.armory && uiedOff.armory.s) || 1;
  const r=uiedOff.armoryRow || {};
  const rs=(r.s!=null? r.s : 1);
  el.style.scale=String(rs/S);
  el.style.translate=((r.dx||0)/S).toFixed(1)+'px '+((r.dy||0)/S).toFixed(1)+'px';
}
let uiedOn=false, uiedOff={}, uiedBar=null, uiedDrag=null, uiedLast=null, uiedSzInput=null, uiedSzLabel=null;
try{
  const raw=JSON.parse(localStorage.getItem(UIED_KEY)||'null');
  if(raw && raw.__ver!==undefined){