diff --git a/access.css b/access.css new file mode 100644 index 0000000..949d639 --- /dev/null +++ b/access.css @@ -0,0 +1,97 @@ +/* Bantuan dan aksesibilitas (access.js, RENCANA 4.2 dan 4.3). + Semua aturan di luar kotak Pengaturan hanya berlaku bila access.js memberi kelas pada (acc-t, acc-cb, acc-hc, acc-calm), + jadi tanpa opsi aktif tampilan sama persis dengan sebelumnya. Tinggi minimum yang didukung tetap 520 px. */ + +/* ---- Kotak lipat di panel Pengaturan ---- */ +#settings-panel{overscroll-behavior:contain} +.acc-box{margin:14px 0 4px;border:1px solid #98abb73a;background:#0e1d27} +.acc-head{display:flex;align-items:center;gap:10px;width:100%;padding:10px 14px;border:0;background:#1a3441;color:#f9f0dc;font:700 17px Rajdhani,Arial,sans-serif;letter-spacing:.4px;text-align:left} +.acc-head:hover{background:#244858} +.acc-title{flex:1} +.acc-badge{padding:1px 8px;border-radius:9px;background:var(--gold);color:#132b37;font:700 12px Rajdhani,Arial,sans-serif;font-style:normal;letter-spacing:.5px} +.acc-badge[hidden],.acc-body[hidden],.acc-reset[hidden]{display:none} +.acc-chev{font-size:14px;transition:transform .15s} +.acc-head[aria-expanded=true] .acc-chev{transform:rotate(180deg)} +.acc-body{padding:0 14px 12px} +.acc-h{margin:14px 0 0;font-size:12px;font-weight:700;letter-spacing:2px;color:var(--gold);text-transform:uppercase} +.acc-row{gap:12px} +.acc-txt{flex:1;min-width:0} +.acc-txt label{display:block;cursor:pointer} +.acc-ctl{flex:none;display:flex;align-items:center} +.acc-ctl select{max-width:170px} +.acc-reset{display:block;margin-top:4px;padding:1px 0;border:0;background:none;color:#8fe3d0;font:700 13px Rajdhani,Arial,sans-serif;text-decoration:underline;cursor:pointer} +.acc-foot{display:flex;margin-top:12px} +.acc-foot button{flex:1;border:1px solid #8da8b153;background:#274352;padding:8px;font-size:15px;font-weight:700} +#settings-panel .acc-live{margin:6px 0 0;min-height:1.3em;font-size:13px;line-height:1.3;color:#8fe3d0} +@media(max-height:760px){.acc-box{margin-top:8px}.acc-h{margin-top:8px}.acc-foot{margin-top:8px}} + +/* ---- Ukuran teks (Normal/Besar/Sangat besar): variabel CSS dipakai lewat zoom pada elemen teks saja ---- + --acc-zh = teks kecil HUD (faktor lebih kecil supaya header dan dek jurus tidak menyempitkan arena di tinggi 520 px), + --acc-z = teks kecil menu, layar VS, hasil, dan cerita, --acc-zm = teks sedang (panel, kutipan, daftar). + zoom ikut mengubah ukuran kotak, jadi aturan media query lama (ukuran per tinggi/lebar jendela) tetap berlaku lalu dikalikan. */ +:root{--acc-z:1;--acc-zh:1;--acc-zm:1} +html.acc-text-1{--acc-z:1.25;--acc-zh:1.15;--acc-zm:1.15} +html.acc-text-2{--acc-z:1.5;--acc-zh:1.3;--acc-zm:1.3} +html.acc-t :is( + .class-tag,.bar-caption,.fighter-mark,.ult-meter,.mode-kicker,.match-rule,.deck-heading,.skill-name,.training-data>div>span,.training-data strong small,.reset-hint,.stage-sign small,.form-timer,#round-call small +){zoom:var(--acc-zh)} +/* Tombol jeda/pengaturan diposisikan mutlak tepat di bawah kolom tengah header; geser sebesar pertumbuhan baris kicker, jam, dan skor. */ +html.acc-t .match-actions{transform:translate(-50%,calc(31px*(var(--acc-zh) - 1)))} +html.acc-t :is( + .eyebrow,.build-tag,.home-title>span,.home-nav small,.home-nav .nav-index,.home-caption small,.home-footer,.menu-back span,.selection-steps,.side-tag,.fighter-copy>span,.fighter-copy p,.fighter-copy small,.roster-caption strong,.roster-caption>span,.roster-tile>span,.fighter-moves>span,.fighter-moves b,.roster-notice,.menu-footer,.arena-window small,.arena-number,.arena-day,.arena-tile span,.match-rules,.about-fighters p,.about-stats,.about-panel>small,.result-copy>small, + .vs-top,.vs-who,.vs-tag,.vs-skip,.vs-perk,.vs-secret,.rs-kicker,.rs-plate small,.rs-quote cite,.rs-cols h3,.rs-total,.rs-stats dt,.st-kicker,.st-gelar small,.st-skip,.st-face figcaption +){zoom:var(--acc-z)} +html.acc-t :is( + #settings-panel .setting-row,#settings-panel .volume-row,#settings-panel .dialog-buttons,#settings-panel .controls-help,#settings-panel>p,#settings-panel .acc-head,#settings-panel .acc-h,#settings-panel .acc-foot,#settings-panel .acc-live, + #pause-panel .pause-main>p,#pause-panel .move-body,.meta-panel,#shop-chip,.meta-toast,.brand-toast, + .home-title p,.about-panel>p,.vs-quote,.rs-quote p,.rs-coins li,.st-line +){zoom:var(--acc-zm)} +/* Nama jurus boleh turun baris dan tetap di tengah kartu supaya tidak menabrak jurus di sebelahnya. */ +html.acc-t .skill-name{white-space:normal;text-align:center;line-height:1.1} + +/* ---- Mode buta warna: filter SVG (access.js) pada arena + HUD; indikator non-warna: bar HP lawan bergaris, lapisan cadangan bertitik ---- */ +html.acc-cb-pro #game-shell{filter:url(#acc-cb-pro)} +html.acc-cb-deu #game-shell{filter:url(#acc-cb-deu)} +html.acc-cb-tri #game-shell{filter:url(#acc-cb-tri)} +html.acc-cb .life-fill::after{content:'';position:absolute;inset:0;pointer-events:none} +html.acc-cb .fighter-two .life-fill::after{background:repeating-linear-gradient(135deg,#0000003d 0 3px,transparent 3px 7px)} +html.acc-cb .life-fill.reserve-fill::after{background:radial-gradient(circle,#00000055 1.2px,transparent 1.8px) 0 0/6px 6px} +html.acc-cb .fighter-two .life-fill.reserve-fill::after{background:radial-gradient(circle,#00000055 1.2px,transparent 1.8px) 0 0/6px 6px,repeating-linear-gradient(135deg,#0000003d 0 3px,transparent 3px 7px)} +/* Meter ultimate: tanda bentuk di depan nama menurut keadaan (siap, mode aktif), selain teks SIAP/waktu. */ +html.acc-cb .ult-meter[data-state=ready] .ult-name::before{content:'\2605\00a0'} +html.acc-cb .ult-meter[data-state=mode] .ult-name::before{content:'\25B6\00a0'} + +/* ---- Kontras tinggi untuk HUD: teks putih berbingkai hitam, bingkai bar dan kartu putih, latar gelap di belakang teks kecil ---- */ +html.acc-hc .vignette{background:linear-gradient(180deg,#000000d6,#00000070 22%,transparent 34%,transparent 62%,#000000c4)} +html.acc-hc.touch .vignette{background:linear-gradient(180deg,#000000d6,#00000070 22%,transparent 34%)} +html.acc-hc :is(.class-tag,.bar-caption,.fighter-mark,.mode-kicker,.match-rule,.deck-heading,.skill-name,.training-data>div>span,.training-data strong small,.reset-hint,.stage-sign>span,.stage-sign small,.ult-meter,.form-timer){color:#fff;text-shadow:0 0 3px #000,0 0 3px #000,1px 1px 0 #000,-1px -1px 0 #000} +html.acc-hc :is(.bar-caption span,.fighter-mark span,.class-tag,.mode-kicker,.match-rule span,.deck-heading span,.deck-heading b,.skill-name,.ult-name,.ult-val){background:#000000b8;padding:0 4px} +html.acc-hc :is(.name-row h1,.name-row h2){text-shadow:3px 3px 0 #000,-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000} +html.acc-hc :is(.match-clock,.combo-counter,.announcement,#round-call strong,#round-call small){text-shadow:-2px -2px 0 #000,2px -2px 0 #000,-2px 2px 0 #000,2px 2px 0 #000,0 0 12px #000} +html.acc-hc .life-frame{border:2px solid #fff;background:#000} +html.acc-hc .life-track{background:#262626} +html.acc-hc .ult-track{border-color:#fff;background:#000} +html.acc-hc .portrait-frame{border-color:#fff} +html.acc-hc .skill-art{border:2px solid #fff} +html.acc-hc .skill-card.on-cooldown .skill-art{border-color:#9aa6ab} +html.acc-hc .skill-card kbd{background:#fff;color:#000} +html.acc-hc .combo-rank b{-webkit-text-stroke:2px #000} +html.acc-hc dialog{background:#0a141c;border-color:#fff} +html.acc-hc :is(.setting-row small,.mv-row small,.mv-keys,.mv-sec h3 small,.mv-note,.controls-help,.dialog-kicker){color:#e3edf0} + +/* ---- Kurangi gerakan: cermin aturan prefers-reduced-motion yang sudah ada di berkas lain, supaya pilihan "Selalu kurangi" berlaku sama ---- */ +html.acc-calm .acc-chev{transition:none} +html.acc-calm #front-menu *,html.acc-calm #round-call strong{animation:none!important;transition:none!important} +html.acc-calm .menu-fullscreen.hinted{animation:none} +html.acc-calm .roster-tile.picked,html.acc-calm #player-preview.picked .selection-art,html.acc-calm #enemy-preview.picked .selection-art{animation:none;filter:brightness(1.35)} +html.acc-calm .hc-stripes,html.acc-calm .hc-slot,html.acc-calm .brand-toast{animation:none!important} +html.acc-calm .brand-toast{opacity:1} +html.acc-calm #vs-screen,html.acc-calm #vs-screen *{animation:none!important} +html.acc-calm #vs-screen:after{display:none} +html.acc-calm .vs-timer i{transform:none} +html.acc-calm .rs *{animation:none!important} +html.acc-calm .rs-confetti,html.acc-calm .st-confetti{display:none} +html.acc-calm .combo-rank{transition:none} +html.acc-calm .ult-meter .ult-track,html.acc-calm .ult-meter .ult-fill,html.acc-calm .ult-meter .ult-fill:after,html.acc-calm .ult-meter .ult-val{animation:none!important} +html.acc-calm #story-screen,html.acc-calm #story-screen *{animation:none!important} +html.acc-calm .st-timer i{transform:none} diff --git a/access.js b/access.js new file mode 100644 index 0000000..2372466 --- /dev/null +++ b/access.js @@ -0,0 +1,159 @@ +/* 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); } + }; +})(); diff --git a/docs/ENGINE_NOTES.md b/docs/ENGINE_NOTES.md index c359e2f..733d53c 100644 --- a/docs/ENGINE_NOTES.md +++ b/docs/ENGINE_NOTES.md @@ -22,6 +22,7 @@ Game fighting 2D canvas, tanpa dependensi dan tanpa langkah build. Semua teks un - Gamepad dan ubah tombol (Tahap 4.5): `input.js` + `input.css` (`window.Input`), dimuat tepat setelah `touch.js`; satu-satunya kait ke game.js adalah `__game.input` (plus pembungkus `__game.startMatch/setP2` untuk mengetahui mode dua pemain; bila kelak ada `__game.p2Active()`, itu yang dipakai). **Ubah tombol** (Pengaturan > "Ubah tombol…", dialog `#keymap-dialog`): 11 aksi Pemain 1 disimpan di `localStorage['konoha.keymap.v1']` sebagai `{aksi: {c: e.code, l: label}}`. Hanya bila pemetaan berbeda dari bawaan, keydown/keyup ditangkap di fase capture pada `window`, diterjemahkan ke kunci kanonik (a d w s spasi i o p k l j) lewat `__game.input`, dan event aslinya dihentikan; tombol bawaan yang sudah dipindah ditelan (termasuk keyup-nya, supaya game.js tidak melepas kunci kanonik yang ditahan lewat tombol baru). Tidak aktif di menu depan (`body.in-menu`), dialog terbuka, layar VS/kartu cerita, atau kolom teks; Spasi/Enter pada `
Daftar gerakan
-
+
diff --git a/vs.js b/vs.js index 54c4376..02ab2ab 100644 --- a/vs.js +++ b/vs.js @@ -141,7 +141,7 @@ // Setelah html dipasang: hitung naik angka koin dan fokuskan wadah layar hasil (kecuali layar sentuh). function mount(root) { const rs = root.querySelector('.rs'); if (!rs) return; - const calm = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; + const calm = window.Access ? window.Access.reduced : window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; // access.js: "Kurangi gerakan" (otomatis mengikuti sistem) const n = rs.querySelector('[data-count]'); if (n && !calm) { const to = Number(n.dataset.count) || 0, t0 = performance.now() + 700, dur = 900;