/* Bantuan dan aksesibilitas (RENCANA 4.2 dan 4.3): modul mandiri, gaya di access.css. Menyisipkan satu kotak lipat "Bantuan & aksesibilitas" ke #settings-panel (seperti mountCheatRow di meta.js), menyimpan pilihan di localStorage['konoha.access.v1'] (hanya nilai yang berbeda dari bawaan) dan menerapkannya lewat kelas pada serta variabel CSS. Mesin membaca window.Access secara malas (tiap dipakai), jadi urutan muat tidak penting dan tanpa modul ini game berjalan seperti biasa. Bantuan hanya berlaku untuk sisi pemain (hero); CPU dan pemain 2 tidak terpengaruh. Kait di game.js: comboAuto -> startAttack (rantai pukulan 1 langsung mengantre dua pukulan berikutnya) autoBlock -> heroGuard + step (sikap blok singkat sesudah serangan depan ditahan; jalur damage sama dengan blok biasa) noCooldown -> step (capCooldowns), ultMax/bilah cooldown (cdMax), useShopMove regen -> passiveRegen (jumlahkan dengan pasif regen; aturan 3 detik tanpa terkena tetap berlaku) motion/reduced -> draw (getaran dan zoom kamera Bro Wo), drawFlash, updateHud (potongan ultimate), feel.js calm() (zoom dan kilat K.O.), vs.js (hitung naik koin) */ (() => { 'use strict'; const KEY = 'konoha.access.v1', OPEN_KEY = 'konoha.access.open'; const ULT_CD = 5; // Tanpa cooldown: ultimate tetap siap paling lambat 5 detik sesudah dipakai const REGEN_HPS = 2; // Pemulihan darah pelan: HP per detik const root = document.documentElement, NS = 'http://www.w3.org/2000/svg'; // Semua teks untuk pemain ada di sini (siap diterjemahkan nanti, RENCANA 4.4). const TXT = { box: 'Bantuan & aksesibilitas', assist: 'Bantuan permainan', access: 'Aksesibilitas', reset: 'Atur ulang', resetAll: 'Atur ulang semua', done: 'Semua pilihan kembali ke bawaan.', active: 'aktif', sysOn: 'sekarang: aktif', sysOff: 'sekarang: biasa' }; // type 'check' = kotak centang (true/false); 'select' = daftar pilihan (vals dan names sejajar). def = nilai bawaan. const OPTS = [ { id: 'combo', group: 'assist', type: 'check', def: false, label: 'Kombo otomatis', hint: 'Satu ketukan Spasi menjalankan rantai 3 pukulan.' }, { id: 'block', group: 'assist', type: 'check', def: false, label: 'Blok otomatis', hint: 'Memblok sendiri serangan dari depan saat kamu tidak sedang beraksi. Damage seperti blok biasa.' }, { id: 'nocd', group: 'assist', type: 'check', def: false, label: 'Tanpa cooldown', hint: 'Mode lucu: skill dan jurus toko tanpa cooldown. Ultimate tetap siap tiap 5 detik.' }, { id: 'regen', group: 'assist', type: 'check', def: false, label: 'Pemulihan darah pelan', hint: 'HP pulih pelan (2 per detik) bila kamu tidak terkena serangan selama 3 detik.' }, { id: 'text', group: 'access', type: 'select', def: 0, vals: [0, 1, 2], names: ['Normal', 'Besar', 'Sangat besar'], label: 'Ukuran teks', hint: 'Memperbesar teks kecil di HUD, menu, serta layar VS, hasil, dan cerita.' }, { id: 'cb', group: 'access', type: 'select', def: 'off', vals: ['off', 'pro', 'deu', 'tri'], names: ['Mati', 'Protanopia (merah)', 'Deuteranopia (hijau)', 'Tritanopia (biru-kuning)'], label: 'Mode buta warna', hint: 'Menggeser warna arena dan HUD. Bar HP lawan diberi garis miring dan lapisan cadangan bertitik.' }, { id: 'calm', group: 'access', type: 'select', def: 'auto', vals: ['auto', 'on'], names: ['Ikuti sistem', 'Selalu kurangi'], label: 'Kurangi gerakan', hint: 'Mematikan getaran layar, zoom K.O., kilat, dan animasi latar menu.' }, { id: 'hc', group: 'access', type: 'check', def: false, label: 'Kontras tinggi', hint: 'Teks dan bingkai HUD lebih tegas.' } ]; const DEF = Object.fromEntries(OPTS.map(o => [o.id, o.def])); let S = { ...DEF }, mq = null, badge = null, live = null; const refs = {}, subs = []; const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; }; const valid = (o, v) => o.type === 'check' ? typeof v === 'boolean' : o.vals.includes(v); const osReduce = () => !!mq && mq.matches; // dibaca langsung tiap dipakai, jadi selalu terkini const reduced = () => S.calm === 'on' || (S.calm === 'auto' && osReduce()); // ---- Simpanan: hanya nilai non-bawaan, jadi "Atur ulang" cukup menghapus kunci ---- function load() { let raw = null; try { raw = JSON.parse(localStorage.getItem(KEY)); } catch (_) {} S = { ...DEF }; if (raw && typeof raw === 'object') for (const o of OPTS) if (valid(o, raw[o.id])) S[o.id] = raw[o.id]; } function persist() { try { const diff = {}; for (const o of OPTS) if (S[o.id] !== o.def) diff[o.id] = S[o.id]; if (Object.keys(diff).length) localStorage.setItem(KEY, JSON.stringify(diff)); else localStorage.removeItem(KEY); } catch (_) {} } // ---- Buta warna: tiga filter SVG (satu feColorMatrix tiap filter) dipasang pada #game-shell (arena + HUD) lewat CSS ---- // Simulasi Machado 2009 (keparahan penuh, ruang warna linear) lalu "daltonisasi": C = I + M (I - S) menggeser informasi yang hilang ke kanal yang masih terlihat. // Bukan alat medis dan belum dinilai pengguna buta warna sungguhan; hanya penolong membedakan warna yang berdekatan. const I3 = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]; const SIM = { pro: [[.152286, 1.052583, -.204868], [.114503, .786281, .099216], [-.003882, -.048116, 1.051998]], deu: [[.367322, .860646, -.227968], [.280085, .672501, .047413], [-.01182, .04294, .968881]], tri: [[1.255528, -.076749, -.178779], [-.078411, .930809, .147602], [.004733, .691367, .3039]] }; const SHIFT = { pro: [[0, 0, 0], [.7, 1, 0], [.7, 0, 1]], deu: [[0, 0, 0], [.7, 1, 0], [.7, 0, 1]], tri: [[1, 0, .7], [0, 1, .7], [0, 0, 0]] }; const mul = (A, B) => A.map(r => B[0].map((_, j) => r.reduce((s, v, k) => s + v * B[k][j], 0))); function cbValues(k) { const E = I3.map((r, i) => r.map((v, j) => v - SIM[k][i][j])), M = mul(SHIFT[k], E), C = I3.map((r, i) => r.map((v, j) => v + M[i][j])); return [...C[0], 0, 0, ...C[1], 0, 0, ...C[2], 0, 0, 0, 0, 0, 1, 0].map(v => +v.toFixed(5)).join(' '); } function buildFilters() { if (document.getElementById('acc-filters')) return; const svg = document.createElementNS(NS, 'svg'), defs = document.createElementNS(NS, 'defs'); svg.id = 'acc-filters'; svg.setAttribute('aria-hidden', 'true'); svg.setAttribute('focusable', 'false'); svg.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden;pointer-events:none'; for (const k of ['pro', 'deu', 'tri']) { const f = document.createElementNS(NS, 'filter'), m = document.createElementNS(NS, 'feColorMatrix'); f.id = 'acc-cb-' + k; f.setAttribute('x', '0'); f.setAttribute('y', '0'); f.setAttribute('width', '1'); f.setAttribute('height', '1'); f.setAttribute('color-interpolation-filters', 'linearRGB'); m.setAttribute('type', 'matrix'); m.setAttribute('values', cbValues(k)); f.append(m); defs.append(f); } svg.append(defs); (document.body || root).append(svg); } // ---- Penerapan: kelas pada (access.css membaca kelas ini) ---- function apply() { const c = root.classList; c.toggle('acc-t', S.text > 0); c.toggle('acc-text-1', S.text === 1); c.toggle('acc-text-2', S.text === 2); c.toggle('acc-cb', S.cb !== 'off'); for (const k of ['pro', 'deu', 'tri']) c.toggle('acc-cb-' + k, S.cb === k); c.toggle('acc-hc', S.hc); c.toggle('acc-calm', reduced()); if (reduced()) quietMenu(); } // Gerak dikurangi memadamkan saklar "Animasi latar menu" lewat saklar aslinya (menu.js tidak diubah); CSS sudah menghentikan animasinya. function quietMenu() { const t = document.querySelector('#menu-video-toggle'); if (t && t.checked) { t.checked = false; t.dispatchEvent(new Event('change')); } } function emit(id) { for (const fn of subs) try { fn({ ...S }, id); } catch (e) { console.error(e); } } function set(id, v) { const o = OPTS.find(x => x.id === id); if (!o || !valid(o, v)) return false; if (live) live.textContent = ''; if (S[id] === v) return true; S[id] = v; persist(); apply(); sync(); emit(id); return true; } function reset(id) { if (id) return set(id, DEF[id]); S = { ...DEF }; persist(); apply(); sync(); emit(null); return true; } // ---- Kotak di panel Pengaturan ---- function row(o) { const r = el('div', 'setting-row acc-row'), txt = el('div', 'acc-txt'), lab = el('label'), ctl = el('span', 'acc-ctl'), rst = el('button', 'acc-reset', TXT.reset), inputId = 'acc-' + o.id; let input; if (o.type === 'check') { input = el('input'); input.type = 'checkbox'; } else { input = el('select'); o.vals.forEach((v, i) => { const op = el('option', '', o.names[i]); op.value = String(v); input.append(op); }); } input.id = inputId; lab.htmlFor = inputId; lab.append(el('span', '', o.label), el('small', '', o.hint)); rst.type = 'button'; rst.hidden = true; rst.setAttribute('aria-label', TXT.reset + ': ' + o.label); input.addEventListener('change', () => set(o.id, o.type === 'check' ? input.checked : o.vals.find(v => String(v) === input.value))); rst.addEventListener('click', () => { reset(o.id); input.focus(); }); txt.append(lab, rst); ctl.append(input); r.append(txt, ctl); refs[o.id] = { input, rst }; return r; } function sync() { for (const o of OPTS) { const r = refs[o.id]; if (!r) continue; const v = S[o.id]; if (o.type === 'check') r.input.checked = v; else r.input.value = String(v); r.rst.hidden = v === o.def; } const calm = refs.calm?.input.options[0]; if (calm) calm.textContent = OPTS.find(o => o.id === 'calm').names[0] + ' (' + (osReduce() ? TXT.sysOn : TXT.sysOff) + ')'; if (badge) { const n = OPTS.filter(o => S[o.id] !== o.def).length; badge.hidden = !n; badge.textContent = n + ' ' + TXT.active; } } function mount() { const panel = document.querySelector('#settings-panel'); if (!panel || panel.querySelector('#acc-box')) return; const box = el('div', 'acc-box'), head = el('button', 'acc-head'), chev = el('b', 'acc-chev', '▾'), body = el('div', 'acc-body'), foot = el('div', 'acc-foot'), all = el('button', '', TXT.resetAll); box.id = 'acc-box'; body.id = 'acc-body'; head.type = 'button'; head.setAttribute('aria-controls', 'acc-body'); chev.setAttribute('aria-hidden', 'true'); badge = el('em', 'acc-badge'); badge.hidden = true; head.append(el('span', 'acc-title', TXT.box), badge, chev); for (const g of ['assist', 'access']) { body.append(el('h3', 'acc-h', TXT[g])); OPTS.filter(o => o.group === g).forEach(o => body.append(row(o))); } all.type = 'button'; foot.append(all); live = el('p', 'acc-live'); live.setAttribute('role', 'status'); live.setAttribute('aria-live', 'polite'); body.append(foot, live); box.append(head, body); // Tombol biasa (bukan
): Spasi dan Enter sampai ke tombol tanpa dicegat penangan tombol game. let open = false; try { open = localStorage.getItem(OPEN_KEY) === '1'; } catch (_) {} const show = v => { open = v; head.setAttribute('aria-expanded', String(v)); body.hidden = !v; }; show(open); // Saat dibuka, kotak digulirkan ke atas panel supaya pilihan langsung terlihat walau jendela hanya setinggi 520 px. head.addEventListener('click', () => { show(!open); if (open) box.scrollIntoView({ block: 'start' }); try { localStorage.setItem(OPEN_KEY, open ? '1' : '0'); } catch (_) {} }); all.addEventListener('click', () => { reset(); live.textContent = TXT.done; }); const anchor = panel.querySelector('.dialog-buttons'); (anchor || panel.lastElementChild).before(box); sync(); } // ---- Awal ---- load(); try { mq = matchMedia('(prefers-reduced-motion: reduce)'); const onMq = () => { apply(); sync(); emit(null); }; if (mq.addEventListener) mq.addEventListener('change', onMq); else if (mq.addListener) mq.addListener(onMq); } catch (_) {} buildFilters(); apply(); mount(); // menu.js memasang saklar latar sesudah modul ini; ulangi sekali setelah semua skrip jalan. if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => { if (reduced()) quietMenu(); }, { once: true }); window.addEventListener('storage', e => { if (e.key === KEY || e.key === null) { load(); apply(); sync(); emit(null); } }); window.Access = { // Dibaca mesin (lihat daftar kait di atas). get comboAuto() { return S.combo; }, get autoBlock() { return S.block; }, get noCooldown() { return S.nocd; }, get regen() { return S.regen ? REGEN_HPS : 0; }, get reduced() { return reduced(); }, get motion() { return reduced() ? 0 : 1; }, // pengali gerak global: 1 = penuh, 0 = tanpa getaran, zoom K.O., dan kilat get ultCd() { return ULT_CD; }, // Tanpa cooldown: skill selalu siap, ultimate tidak pernah menunggu lebih dari ULT_CD detik (dipanggil tiap langkah dengan hero.cooldowns). capCooldowns(cd) { if (!S.nocd || !cd) return; cd.skill1 = 0; cd.skill2 = 0; if (cd.ultimate > ULT_CD) cd.ultimate = ULT_CD; }, // Penyebut bilah/pie cooldown agar tampilan ikut batas 5 detik. cdMax(name, max) { return S.nocd && name === 'ultimate' ? Math.min(max, ULT_CD) : max; }, get: id => S[id], set, reset, state: () => ({ ...S }), onChange(fn) { if (typeof fn === 'function') subs.push(fn); } }; })();