183 lines
12 KiB
JavaScript
183 lines
12 KiB
JavaScript
/* Tata letak tombol sentuh yang bisa diatur (RENCANA 4.6). Hanya aktif di html.touch.
|
|
Pengaturan > "Tata letak tombol" > ATUR membuka editor layar penuh: sebuah <dialog> modal di atas Pengaturan (jadi game tetap beku, sama seperti saat
|
|
Pengaturan terbuka). Salinan pad arah (.dpad), tombol aksi (.action-pad), dan panel tambahan (.extra-pad) dibungkus kotak yang bisa diseret (pindah)
|
|
atau ditarik pegangan sudutnya (ukuran). Salinan itu tidak punya pendengar apa pun dan dokumen lain tidak aktif selama editor terbuka, jadi tidak ada
|
|
input yang terkirim ke game. touch.js tidak diubah: hasilnya hanya variabel CSS --tl-x/--tl-y/--tl-s dan kelas .tl-xf pada elemen asli (transformasi
|
|
ada di touchlayout.css; titik tumpunya sudut yang dekat tepi layar, jadi pembesaran selalu menjauhi tepi).
|
|
Simpanan: localStorage['konoha.touchlayout.v1'] = {v:1, g:{dpad|action|extra:{x,y,s}}}; x,y = geseran sebagai pecahan lebar/tinggi layar, s = skala.
|
|
Posisi dijepit ke dalam layar memakai kotak nyata hasil ukur (juga saat ukuran layar berubah); jepitan itu tidak menimpa nilai simpanan. */
|
|
(() => {
|
|
'use strict';
|
|
const html = document.documentElement;
|
|
if (!html.classList.contains('touch')) return;
|
|
const root = document.getElementById('touch-controls'), panel = document.getElementById('settings-panel');
|
|
if (!root || !panel) return;
|
|
const KEY = 'konoha.touchlayout.v1', S_MIN = .6, S_MAX = 1.8, GAP = 4, BAR = 46; // BAR = tinggi bilah editor; area tombol dimulai di bawahnya
|
|
const GROUPS = [
|
|
{ id: 'dpad', sel: '.dpad', name: 'ARAH', label: 'pad arah', grip: 'tr' },
|
|
{ id: 'action', sel: '.action-pad', name: 'AKSI', label: 'tombol aksi', grip: 'tl' },
|
|
{ id: 'extra', sel: '.extra-pad', name: 'TAMBAHAN', label: 'tombol tambahan', grip: 'tl' },
|
|
].map(g => ({ ...g, node: root.querySelector(g.sel) })).filter(g => g.node);
|
|
if (!GROUPS.length) return;
|
|
|
|
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v)), r4 = v => Math.round(v * 1e4) / 1e4, num = v => typeof v === 'number' && isFinite(v);
|
|
const blank = () => ({ x: 0, y: 0, s: 1 });
|
|
const isBlank = st => GROUPS.every(g => !st[g.id].x && !st[g.id].y && st[g.id].s === 1);
|
|
function read() {
|
|
let raw = null;
|
|
try { raw = JSON.parse(localStorage.getItem(KEY)); } catch (_) { /* simpanan rusak atau diblokir: pakai tata letak bawaan */ }
|
|
const st = {};
|
|
for (const g of GROUPS) {
|
|
const v = raw && raw.v === 1 && raw.g && raw.g[g.id], o = blank();
|
|
if (v && num(v.x) && num(v.y) && num(v.s)) { o.x = clamp(v.x, -1, 1); o.y = clamp(v.y, -1, 1); o.s = clamp(v.s, S_MIN, S_MAX); }
|
|
st[g.id] = o;
|
|
}
|
|
return st;
|
|
}
|
|
function write(st) { try { if (isBlank(st)) localStorage.removeItem(KEY); else localStorage.setItem(KEY, JSON.stringify({ v: 1, g: st })); } catch (_) { /* tidak tersimpan; tata letak berlaku sampai halaman ditutup */ } }
|
|
let state = read();
|
|
let ed = null; // editor yang sedang terbuka: { dlg, snap, parts: {id: {g, clone, frame, shown}}, z }
|
|
const cur = id => ({ dx: state[id].x * innerWidth, dy: state[id].y * innerHeight, s: state[id].s });
|
|
|
|
// Batas area tombol: layar dikurangi tepi aman (takik), jarak kecil ke tepi, dan bilah editor di atas.
|
|
let area = null;
|
|
addEventListener('resize', () => { area = null; });
|
|
function bounds() {
|
|
if (area) return area;
|
|
const p = document.createElement('div');
|
|
p.style.cssText = 'position:fixed;left:0;top:0;width:0;height:0;visibility:hidden;pointer-events:none;padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)';
|
|
document.body.append(p);
|
|
const c = getComputedStyle(p), n = k => parseFloat(c[k]) || 0;
|
|
area = { l: n('paddingLeft') + GAP, t: n('paddingTop') + BAR, r: innerWidth - n('paddingRight') - GAP, b: innerHeight - n('paddingBottom') - GAP };
|
|
p.remove();
|
|
return area;
|
|
}
|
|
// Menerapkan {dx, dy, s} pada node, mengukur kotak nyatanya, lalu menjepit ukuran dan posisi ke dalam batas. Mengembalikan nilai akhir.
|
|
// Node tersembunyi (mis. kontrol asli saat di menu) tidak bisa diukur: nilai dipasang apa adanya dan dijepit nanti saat tampil (lihat ResizeObserver).
|
|
function fit(node, v, b) {
|
|
const o = { dx: v.dx, dy: v.dy, s: clamp(v.s, S_MIN, S_MAX) };
|
|
const put = () => { node.style.setProperty('--tl-x', o.dx + 'px'); node.style.setProperty('--tl-y', o.dy + 'px'); node.style.setProperty('--tl-s', String(o.s)); };
|
|
put();
|
|
let r = node.getBoundingClientRect();
|
|
if (!r.width || !r.height) return o;
|
|
const f = Math.min(1, (b.r - b.l) / r.width, (b.b - b.t) / r.height);
|
|
if (f < 1) { o.s = Math.max(S_MIN, o.s * f); put(); r = node.getBoundingClientRect(); }
|
|
o.dx += r.left < b.l ? b.l - r.left : r.right > b.r ? b.r - r.right : 0;
|
|
o.dy += r.top < b.t ? b.t - r.top : r.bottom > b.b ? b.b - r.bottom : 0;
|
|
put();
|
|
return o;
|
|
}
|
|
|
|
// Elemen asli: pasang tata letak simpanan; kelas dan variabel dilepas seluruhnya bila nilainya bawaan (tampilan sama persis seperti sebelum fitur ini).
|
|
function applyReal() {
|
|
const b = bounds();
|
|
for (const g of GROUPS) {
|
|
const st = state[g.id];
|
|
if (!st.x && !st.y && st.s === 1) { g.node.classList.remove('tl-xf'); for (const k of ['--tl-x', '--tl-y', '--tl-s']) g.node.style.removeProperty(k); continue; }
|
|
g.node.classList.add('tl-xf');
|
|
fit(g.node, cur(g.id), b);
|
|
}
|
|
}
|
|
applyReal();
|
|
// Kontrol asli tersembunyi di menu dan baru bisa diukur saat tampil di arena: ResizeObserver menjepit ulang pada saat itu (sebelum digambar) dan saat layar berubah.
|
|
if (typeof ResizeObserver === 'function') new ResizeObserver(() => applyReal()).observe(root); else addEventListener('resize', applyReal);
|
|
// Sabuk pengaman: selama editor terbuka, sentuhan tidak boleh sampai ke tombol asli (dokumen sudah tidak aktif karena dialog modal).
|
|
root.addEventListener('pointerdown', e => { if (ed) { e.stopPropagation(); e.preventDefault(); } }, true);
|
|
|
|
// ---- editor ----
|
|
const store = (p, o) => { state[p.g.id] = { x: r4(o.dx / innerWidth), y: r4(o.dy / innerHeight), s: r4(o.s) }; };
|
|
function paint(p, v) {
|
|
const o = fit(p.clone, v, bounds());
|
|
p.shown = o;
|
|
const r = p.clone.getBoundingClientRect();
|
|
Object.assign(p.frame.style, { left: r.left + 'px', top: r.top + 'px', width: r.width + 'px', height: r.height + 'px' });
|
|
p.frame.querySelector('.tl-name').textContent = p.g.name + (Math.abs(o.s - 1) > .005 ? ' ' + Math.round(o.s * 100) + '%' : '');
|
|
return o;
|
|
}
|
|
function repaint() { area = null; if (ed) for (const id in ed.parts) paint(ed.parts[id], cur(id)); }
|
|
function reset() { for (const g of GROUPS) { state[g.id] = blank(); paint(ed.parts[g.id], cur(g.id)); } }
|
|
function onKey(e) { if (e.key === 'Escape') { e.preventDefault(); e.stopImmediatePropagation(); finish(false); } } // Esc milik editor, bukan milik game (yang akan menutup Pengaturan)
|
|
|
|
// Satu bagian editor: salinan visual tombol (tanpa pendengar, tanpa input) + kotak seret di atasnya.
|
|
function part(g, stage) {
|
|
const clone = g.node.cloneNode(true);
|
|
clone.classList.add('tl-xf', 'tl-clone'); clone.inert = true;
|
|
for (const n of clone.querySelectorAll('.cooldown-wipe,.cooldown-num')) n.remove();
|
|
for (const n of [clone, ...clone.querySelectorAll('*')]) { n.classList.remove('pressed', 'active', 'recharged', 'on-cooldown'); for (const a of ['data-key', 'data-slot', 'tabindex', 'aria-label', 'aria-hidden']) n.removeAttribute(a); }
|
|
const frame = document.createElement('div');
|
|
frame.className = 'tl-box'; frame.tabIndex = 0; frame.setAttribute('role', 'group');
|
|
frame.setAttribute('aria-label', g.label + ': panah menggeser, plus dan minus mengubah ukuran');
|
|
frame.innerHTML = '<span class="tl-name"></span><i class="tl-grip ' + g.grip + '"></i>';
|
|
stage.append(clone, frame);
|
|
const p = { g, clone, frame, shown: cur(g.id) };
|
|
let d = null; // seretan yang sedang berjalan pada kotak ini (tiap kotak punya jarinya sendiri)
|
|
frame.addEventListener('pointerdown', e => {
|
|
if (d || (e.pointerType === 'mouse' && e.button !== 0)) return;
|
|
e.preventDefault();
|
|
const r = clone.getBoundingClientRect(), ax = g.grip === 'tl' ? r.right : r.left, ay = r.bottom; // titik tumpu = sudut yang tidak ikut bergerak saat ukuran berubah
|
|
d = { id: e.pointerId, grip: !!e.target.closest('.tl-grip'), x: e.clientX, y: e.clientY, v: { ...p.shown }, ax, ay, len: Math.max(24, Math.hypot(e.clientX - ax, e.clientY - ay)) };
|
|
try { frame.setPointerCapture(e.pointerId); } catch (_) { /* tanpa capture seretan tetap jalan selama jari di atas kotak */ }
|
|
frame.classList.add('on'); frame.style.zIndex = String(++ed.z);
|
|
});
|
|
frame.addEventListener('pointermove', e => {
|
|
if (!d || e.pointerId !== d.id) return;
|
|
const v = d.grip ? { dx: d.v.dx, dy: d.v.dy, s: d.v.s * Math.hypot(e.clientX - d.ax, e.clientY - d.ay) / d.len } : { dx: d.v.dx + e.clientX - d.x, dy: d.v.dy + e.clientY - d.y, s: d.v.s };
|
|
store(p, paint(p, v));
|
|
});
|
|
const end = e => { if (d && e.pointerId === d.id) { d = null; frame.classList.remove('on'); } };
|
|
for (const t of ['pointerup', 'pointercancel', 'lostpointercapture']) frame.addEventListener(t, end);
|
|
frame.addEventListener('keydown', e => { // papan ketik: panah menggeser (Shift = lebih jauh), + dan - mengubah ukuran
|
|
const step = e.shiftKey ? 24 : 8, move = { ArrowLeft: [-step, 0], ArrowRight: [step, 0], ArrowUp: [0, -step], ArrowDown: [0, step] }[e.key];
|
|
if (!move && !['+', '=', '-', '_'].includes(e.key)) return;
|
|
e.preventDefault(); e.stopPropagation();
|
|
const v = { ...p.shown };
|
|
if (move) { v.dx += move[0]; v.dy += move[1]; } else v.s *= ['+', '='].includes(e.key) ? 1.06 : 1 / 1.06;
|
|
store(p, paint(p, v));
|
|
});
|
|
return p;
|
|
}
|
|
|
|
function open() {
|
|
if (ed) return;
|
|
const dlg = document.createElement('dialog');
|
|
dlg.id = 'tl-editor'; dlg.className = document.body.classList.contains('in-menu') ? 'tl-over-menu' : '';
|
|
dlg.setAttribute('aria-label', 'Atur tata letak tombol sentuh');
|
|
dlg.innerHTML = '<div class="tl-bar"><strong>ATUR TOMBOL</strong><button type="button" data-tl="reset">ATUR ULANG</button><button type="button" data-tl="cancel">BATAL</button><button type="button" data-tl="done" class="tl-done">SELESAI</button></div><div class="tl-stage"><p class="tl-hint">Seret kotak untuk memindahkan.<br>Tarik lingkaran di sudutnya untuk mengubah ukuran.</p></div>';
|
|
ed = { dlg, snap: JSON.parse(JSON.stringify(state)), parts: {}, z: 2 };
|
|
try {
|
|
const stage = dlg.querySelector('.tl-stage');
|
|
for (const g of GROUPS) ed.parts[g.id] = part(g, stage);
|
|
dlg.addEventListener('click', e => { const act = e.target.closest('[data-tl]')?.dataset.tl; if (act === 'reset') reset(); else if (act === 'cancel') finish(false); else if (act === 'done') finish(true); });
|
|
dlg.addEventListener('close', () => finish(false)); // ditutup peramban (mis. tombol Kembali di Android): perubahan dibuang
|
|
document.body.append(dlg);
|
|
html.classList.add('tl-editing'); // menyembunyikan Pengaturan yang masih terbuka di bawah editor
|
|
addEventListener('keydown', onKey, true); addEventListener('resize', repaint);
|
|
dlg.showModal();
|
|
repaint();
|
|
dlg.querySelector('.tl-done').focus();
|
|
} catch (err) { console.error(err); finish(false); }
|
|
}
|
|
function finish(save) {
|
|
if (!ed) return;
|
|
const { dlg, snap } = ed; ed = null;
|
|
removeEventListener('keydown', onKey, true); removeEventListener('resize', repaint);
|
|
if (save) write(state); else state = snap;
|
|
html.classList.remove('tl-editing'); // sebelum dlg.close(): fokus harus kembali ke tombol ATUR yang harus sudah terlihat
|
|
if (dlg.open) dlg.close();
|
|
dlg.remove();
|
|
applyReal(); status();
|
|
}
|
|
|
|
// Baris di Pengaturan, di atas tombol Tes audio / Layar penuh (setelah baris kode cheat dari meta.js).
|
|
function status() { const m = panel.querySelector('#tl-msg'); if (m) m.textContent = isBlank(state) ? 'Geser dan ubah ukuran pad arah dan tombol aksi' : 'Tata letak kustom aktif'; }
|
|
if (!panel.querySelector('#tl-row')) {
|
|
const row = document.createElement('div');
|
|
row.className = 'setting-row'; row.id = 'tl-row';
|
|
row.innerHTML = '<span>Tata letak tombol<small id="tl-msg"></small></span><button type="button" class="kf-set-btn" id="tl-open">ATUR</button>';
|
|
(panel.querySelector('.dialog-buttons') || panel.lastElementChild).before(row);
|
|
row.querySelector('#tl-open').onclick = open;
|
|
status();
|
|
}
|
|
window.TouchLayout = { open, get editing() { return !!ed; }, get layout() { return JSON.parse(JSON.stringify(state)); } };
|
|
})();
|