public/dev_tools.js
分割 3 / 10 · 元ファイル 371〜577行付近
if(raw.__ver===uiedVer()){ uiedOff=raw.off||{}; }
else { console.log('[UI編集] 版が変わったので端末の調整値を捨てた:', raw.__ver, '→', uiedVer());
localStorage.removeItem(UIED_KEY); }
} else if(raw){
/* 版の印が無い古い形は、いつのコードに対する差か分からないので捨てる */
console.log('[UI編集] 版の印が無い古い調整値を捨てた');
localStorage.removeItem(UIED_KEY);
}
}catch(e){}
/* 保存はいつも版の印つき。読む時に版が違えば捨てられる */
function uiedSave(){
try{ localStorage.setItem(UIED_KEY, JSON.stringify({__ver:uiedVer(), off:uiedOff})); }catch(e){}
}
/* 焼き込み済みの部品は、起動のたびに端末の調整値を必ず捨てる(2026-08-26 K1報告の根治)。
「印が変わった時だけ捨てる」方式は実機で効かなかった——報告のJSONで、焼き込みの上に
端末の値(dy26・1.06倍・歯車+143,+41)が乗ったままなのが数字で確定した
(下段ラベルの実測 高さ98=93×1.06 / 上端715 / 歯車の右端512=343+143+26)。
条件付きだと、印を書いた後に編集し直された値が次の起動で復活して二度と消えない。
ここに載っている部品は「コードが正本」。編集は開いている間だけ効き、読み込み直すと
必ずコードの値へ戻る——二重掛けが構造的に起きなくなる。
K1から新しい数値をもらったらコードへ焼く、という運用はそのまま */
const UIED_BAKED_IDS=['tabBar','setBtn','moneyHud','lfChip']; // 所持金と中継の小窓も焼いた(2026-09-01)
try{
let drop=false;
for(const id of UIED_BAKED_IDS) if(uiedOff[id]){ delete uiedOff[id]; drop=true; }
if(drop){
console.log('[UI編集] 焼き込み済みの部品なので端末の調整値を捨てた:', UIED_BAKED_IDS.join(','));
if(Object.keys(uiedOff).length) uiedSave();
else localStorage.removeItem(UIED_KEY);
}
localStorage.setItem(UIED_BAKED_KEY, UIED_BAKED);
}catch(e){}
/* Shadow DOMの中も総当たりで探す(2026-08-07 K1指摘: 肝心のSPバーが掴めない)。
本編では襲撃エンジンのホストが#mountではないので、全要素のshadowRootを潜る */
function uiedFind(id){
const direct=document.getElementById(id);
if(direct) return direct;
const stack=[document];
while(stack.length){
const root=stack.pop();
const hit=root.getElementById ? root.getElementById(id) : root.querySelector('#'+id);
if(hit) return hit;
for(const el of root.querySelectorAll('*')) if(el.shadowRoot) stack.push(el.shadowRoot);
}
return null;
}
const uiedCache={}; // 深い探索は重いので見つけた要素は覚えておく
function uiedEl(id){
const c=uiedCache[id];
if(c && c.isConnected) return c;
const el=uiedFind(id);
if(el) uiedCache[id]=el;
return el;
}
function uiedApply(){
for(const id in uiedOff){
const el=uiedEl(id); if(!el) continue;
const o=uiedOff[id];
if(id==='armoryRow') continue; // 中のアイテム欄は下でまとめて合成する
el.style.translate=(o.dx||0)+'px '+(o.dy||0)+'px';
el.style.scale=(o.s!=null && o.s!==1) ? String(o.s) : '';
}
}
/* 画面の中央に十字のガイドを出し、近づいたら吸い付く(2026-08-07 K1指示。
まっすぐ縦・横に動かしたい時に手が震えても中心を外さないため) */
const UIED_SNAP=10; // この距離まで近づいたら吸着(px)
let uiedCross=null;
function uiedCrossShow(){
if(uiedCross) return;
uiedCross=document.createElement('div');
uiedCross.id='hzgUiedCross';
uiedCross.style.cssText='position:fixed; inset:0; z-index:999998; pointer-events:none;';
uiedCross.innerHTML='<i class="v"></i><i class="h"></i>';
const st=document.createElement('style');
st.textContent='#hzgUiedCross i{position:absolute;background:rgba(120,200,255,.5)}'
+'#hzgUiedCross .v{left:50%;top:0;bottom:0;width:1px;margin-left:-.5px}'
+'#hzgUiedCross .h{top:50%;left:0;right:0;height:1px;margin-top:-.5px}'
+'#hzgUiedCross i.hot{background:rgba(255,214,106,.95);box-shadow:0 0 6px rgba(255,214,106,.8)}';
uiedCross.appendChild(st);
document.body.appendChild(uiedCross);
}
function uiedCrossHide(){ if(uiedCross){ uiedCross.remove(); uiedCross=null; } }
/* 動かしている部品の中心が画面の中心線に近ければ、その軸を中心へ吸わせる */
function uiedSnap(el, dx, dy){
if(!uiedCross) return {dx, dy};
const r=el.getBoundingClientRect();
const cx=r.left+r.width/2, cy=r.top+r.height/2;
const tx=window.innerWidth/2, ty=window.innerHeight/2;
let sx=false, sy=false;
if(Math.abs(cx-tx)<UIED_SNAP){ dx+=tx-cx; sx=true; }
if(Math.abs(cy-ty)<UIED_SNAP){ dy+=ty-cy; sy=true; }
uiedCross.querySelector('.v').classList.toggle('hot', sx);
uiedCross.querySelector('.h').classList.toggle('hot', sy);
return {dx, dy};
}
/* 保存済みの差分は起動後もずっと当て続ける(襲撃の部品は後から生まれるため) */
if(Object.keys(uiedOff).length) setInterval(uiedApply, 1200);
function uiedMark(on){
for(const id of UIED_IDS){
const el=uiedEl(id); if(!el) continue;
el.style.outline=on?'2px dashed rgba(120,200,255,.9)':'';
el.style.outlineOffset=on?'2px':'';
/* 指を通す(2026-09-01 K1報告「所持金が動かせない。青い枠には囲まれてるのに」)。
#moneyHud のように pointer-events:none の飾りは、枠は描けても**指が当たらない**ので
掴めなかった。編集中だけ受け取る側へ回し、終わったら元の指定へ戻す
(空文字にすればCSSの指定が戻る)。他の部品にも同じ物が増えても勝手に効く */
el.style.pointerEvents=on?'auto':'';
}
}
function uiedStart(){
uiedOn=true;
uiedApply(); uiedMark(true); uiedCrossShow();
uiedBar=document.createElement('div');
uiedBar.id='hzgUiedBar';
uiedBar.style.cssText='position:fixed; left:50%; top:calc(6px + env(safe-area-inset-top)); transform:translateX(-50%);'
+'z-index:999999; display:flex; gap:6px; align-items:center; padding:6px 8px;'
+'background:rgba(12,20,16,.95); border:1px solid #4d7a5f; border-radius:10px;'
+'-webkit-user-select:none; user-select:none;';
const tag=document.createElement('span');
tag.textContent='🧲UI編集中';
tag.style.cssText='font:bold 11px/1 -apple-system,sans-serif; color:#ffe184; padding:0 2px;';
uiedBar.appendChild(tag);
const mkB=(label,fn)=>{
const b=document.createElement('button');
b.textContent=label;
b.style.cssText='padding:8px 10px; border:0; border-radius:8px; background:#28533f; color:#f7f0dd; font:bold 11px/1 -apple-system,sans-serif;';
b.addEventListener('click', ev=>{ ev.stopPropagation(); fn(); });
uiedBar.appendChild(b);
};
// 選んだ部品の大きさ(2026-08-07 K1指示)。最後に触った部品に効く
const sz=document.createElement('input');
sz.type='range'; sz.min='50'; sz.max='200'; sz.step='2'; sz.value='100';
sz.style.cssText='width:88px;';
const szl=document.createElement('span');
szl.textContent='100%';
szl.style.cssText='font:bold 10px/1 -apple-system,sans-serif; color:#ffd76a; min-width:32px; text-align:right;';
sz.addEventListener('input', ()=>{
const id=uiedLast; szl.textContent=sz.value+'%';
if(!id) return;
const o=uiedOff[id]||(uiedOff[id]=uiedInit(uiedEl(id)));
o.s=+sz.value/100;
const el=uiedEl(id);
if(id==='armoryRow' || id==='armory') uiedRowSync();
if(id!=='armoryRow' && el) el.style.scale=String(o.s);
uiedSave();
});
uiedSzInput=sz; uiedSzLabel=szl;
uiedBar.appendChild(sz); uiedBar.appendChild(szl);
mkB('差分コピー', ()=>{
const out={};
for(const id in uiedOff){
const o=uiedOff[id]; if(!o) continue;
/* 実際に要素へ当たっている値を出す。入れ子(armoryRowはarmoryの中)の割り算も
済んだ後の値なので、**そのままCSSへ書ける**。こちらで計算し直さない */
const el0=uiedEl(id);
if(el0){
const t0=el0.style.translate, s0=el0.style.scale;
const e0={};
if(t0){ const p=t0.split(/\s+/); const a0=parseFloat(p[0])||0, b0=parseFloat(p[1]||'0')||0;
if(a0||b0){ e0.translate=Math.round(a0)+'px '+Math.round(b0)+'px'; } }
if(s0 && parseFloat(s0)!==1) e0.scale=Math.round(parseFloat(s0)*1000)/1000;
/* K1指示(2026-09-02)「10px上に、ではなく絶対値でないと怖い」。
画面上の最終的な位置と大きさもそのまま出す。
焼き込んだ後、こちらで同じ数字になるか測って確かめられる */
try{
const r0=el0.getBoundingClientRect();
e0.実際の位置=Math.round(r0.left)+','+Math.round(r0.top);
e0.実際の大きさ=Math.round(r0.width)+'x'+Math.round(r0.height);
}catch(e2){}
if(Object.keys(e0).length) out[id]=e0;
}
const e={};
if(o.dx) e.dx=Math.round(o.dx);
if(o.dy) e.dy=Math.round(o.dy);
if(o.s!=null && o.s!==1) e.scale=Math.round(o.s*100)/100;
if(Object.keys(e).length) out[id]=e;
}
const txt=JSON.stringify(out);
if(navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(txt).then(()=>toast('差分をコピーした '+txt.slice(0,60))).catch(()=>manualCopy(txt));
} else manualCopy(txt);
});
mkB('リセット', ()=>{
for(const id in uiedOff){ const el=uiedEl(id); if(el){ el.style.translate=''; el.style.scale=''; } }
uiedLast=null; if(uiedSzInput){ uiedSzInput.value='100'; uiedSzLabel.textContent='100%'; }
uiedOff={}; localStorage.removeItem(UIED_KEY);
toast('UI差分をリセットした');
});
mkB('終了', uiedStop);
uiedBar.title='部品をドラッグで移動 / つまみで大きさ / 中央の十字に吸着';
uiedBar.addEventListener('pointerdown', ev=>ev.stopPropagation());
document.body.appendChild(uiedBar);
toast('点線の部品をドラッグで動かせる');
}
function uiedStop(){
uiedOn=false; uiedMark(false); uiedCrossHide();
if(uiedBar){ uiedBar.remove(); uiedBar=null; }
}
/* 編集中はdocumentのキャプチャで先取りして、ゲーム側のタップ処理へ渡さない */
document.addEventListener('pointerdown', ev=>{
if(!uiedOn) return;
const t=(ev.composedPath && ev.composedPath()[0]) || ev.target;
if(uiedBar && (t===uiedBar || uiedBar.contains(t))) return;
for(const id of UIED_IDS){
const el=uiedEl(id);
if(el && (t===el || el.contains(t))){
ev.stopPropagation(); ev.preventDefault();