fighting-konoha/access.js

160 lines
13 KiB
JavaScript

/* 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 <html> 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 <html> (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 <details>): 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); }
};
})();