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 248a9a5..d2ebb1e 100644 --- a/docs/ENGINE_NOTES.md +++ b/docs/ENGINE_NOTES.md @@ -19,6 +19,7 @@ Game fighting 2D canvas, tanpa dependensi dan tanpa langkah build. Semua teks un - `menu.js`: menu utama (home, pilih karakter, arena, hasil, about) di `#front-menu`; `window.FrontEnd`. - Slot misteri yang hidup (Tahap 3.7): lima petak `locked-N` di layar pilih karakter. Data `content/mystery.js` (`window.MYSTERY`, satu entri per petak menurut urutan; skema dan pedoman isi ada di komentar berkas itu), logika `mystery.js` (`window.Mystery`), gaya `mystery.css`; dimuat di `index.html` tepat sebelum `menu.js`. Tiap slot punya 3–4 petunjuk berurutan dengan syarat `need {type, n}` yang dibaca dari `Meta.save` (wins, matches, perfects, combo, survival, arcade, ach, eggs, pairs = `save.eggs['dialog:*']`, shop, coins; daftar `GOALS` di mystery.js; tahap `pairs` dilewati bila `window.DIALOGS.rahasia` tidak ada). Petak: siluet SVG sebaris (tubuh disalin dari `assets/menu/locked.svg`, benda khas muncul samar di tahap kedua terakhir dan jelas di tahap akhir) makin tajam lewat `--ms-p`, lencana `n/total`, tanda tanya berubah (`?` `??` `?!` `!?` `!`), titik BARU. Panel pratinjau (yang menampilkan sorotan: kiri saat memilih P1, kanan saat memilih lawan) memuat petunjuk terbuka, syarat petunjuk berikutnya dengan bilah progres, dan "Slot misteri n / total". Hook di menu.js: `Mystery.panel` (di `fighterPanel`), `decorate` (akhir `render()`), `preview` (di `updatePreview`: teks `#fighter-moves` dan `#roster-notice`), `notice` (di `confirm()` saat slot ditekan). Semua fungsi publik aman: bila gagal, tampilan lama slot terkunci dipakai. Petak tetap `data-fighter="locked-N"` karena easter egg `penasaran` (meta.js) membaca ketukan di atasnya. Semua petunjuk terbuka = pencapaian tersembunyi `penasaran_berat` (PENASARAN BERAT, +50 koin; dicek tiap layar pilih karakter dirender). Ingatan "sudah dilihat" untuk titik BARU dan toast ada di `localStorage['konoha.mystery.seen']` (bukan di `Meta.save`). Uji manual cepat: buka `?mystery=2` (atau `all`) untuk mempratinjau tahap 2 pada semua slot tanpa menyimpan atau membuka pencapaian; `Mystery.states()` di konsol meringkas kemajuan. Saat petarung sungguhan menggantikan satu slot, hapus entri slotnya dari `window.MYSTERY` supaya urutan tetap sama dengan petak `locked-N` yang tersisa. - `touch.js`: kontrol sentuh; tombol memanggil `__game.input(key, down)`. +- Bantuan dan aksesibilitas (Tahap 4.2 dan 4.3): `access.js` (`window.Access`) + `access.css`, dimuat tepat sesudah `feel.js`. Menyisipkan kotak lipat "Bantuan & aksesibilitas" ke `#settings-panel` (tombol biasa, bukan `
`, karena penangan tombol game mencegat Spasi pada ``), setiap pilihan punya tombol Atur ulang (muncul bila nilainya bukan bawaan) plus "Atur ulang semua". Simpanan `localStorage['konoha.access.v1']` hanya berisi nilai non-bawaan; `konoha.access.open` mengingat kotak terbuka. Id pilihan: `combo`, `block`, `nocd`, `regen` (true/false), `text` (0 Normal, 1 Besar, 2 Sangat besar), `cb` (`off`|`pro`|`deu`|`tri`), `calm` (`auto` mengikuti prefers-reduced-motion | `on`), `hc` (true/false). Penerapan lewat kelas pada ``: `acc-t` + `acc-text-1|2` (variabel `--acc-z` teks kecil dan `--acc-zm` teks sedang, dipakai lewat `zoom` pada elemen teks saja), `acc-cb` + `acc-cb-pro|deu|tri` (filter SVG `#acc-cb-*` pada `#game-shell`, dibuat access.js; bar HP lawan bergaris dan lapisan cadangan bertitik), `acc-hc`, `acc-calm` (cermin aturan prefers-reduced-motion yang sudah ada di berkas CSS lain). Mesin membaca `window.Access` secara malas dan tanpa modul ini semua kait menjadi no-op. Kait di game.js, semuanya hanya sisi pemain (`hero`): `startAttack` (Kombo otomatis: pukulan 1 dibuat dengan `queued: 2`), `heroGuard` + `step` (Blok otomatis: serangan dari depan saat di tanah, tidak beraksi, guard belum pecah, masuk jalur blok biasa lalu `hero.autoGuard` menahan sikap blok 0,3 dtk; guard tetap bisa pecah), `step` (`Access.capCooldowns`: skill 1 dan 2 selalu 0, ultimate paling lama 5 dtk), `ultMax` dan pie cooldown (`Access.cdMax` agar bilah ikut batas 5 dtk), `useShopMove` (`shopCd` 0), `passiveRegen` (+2 HP/dtk, aturan 3 dtk tanpa terkena tetap berlaku), `draw`/`drawFlash`/`updateHud` (`Access.motion`: 0 = tanpa getaran layar, sentakan, zoom kamera Bro Wo, kilat sprite, potongan ultimate meluncur). `feel.js` memakai pengali yang sama lewat `motion()`/`calm()` (zoom K.O. dimatikan, kilat K.O. dimatikan, vignette tetap, animasi peringkat kombo berhenti) dan `vs.js` menghentikan hitung naik koin. Saklar "Animasi latar menu" dipadamkan lewat saklar aslinya (menu.js tidak diubah). Hadiah koin dan pencapaian tidak dibedakan saat bantuan aktif. Uji manual cepat di konsol: `Access.set('nocd', true)`, `Access.set('cb', 'deu')`, `Access.set('text', 2)`, `Access.state()`, `Access.reset()`. `precache.js`/`files.json` perlu dibangun ulang oleh integrator agar `access.js` dan `access.css` ikut tersimpan offline. - `brand.js` (`window.Brand`) + `content/loading.js` (`window.LOADING_QUOTES`) + `content/changelog.js` (`window.CHANGELOG`): dimuat di `index.html` tepat setelah kartu `#boot`, sebelum `precache.js`. Kutipan acak (tidak pernah sama dengan yang terakhir, kunci `konoha.loadquote`; `?quote=N` memaksa) tampil di `#boot .boot-quote` dan `#load-state .load-quote`. "Yang baru": `Brand.decorate(root, screen)` dipanggil `menu.js` di akhir `render()`; layar `about` mendapat panel `.whatsnew-panel` dan tautan kredit, layar `home` memicu toast `.brand-toast` satu kali bila versi teratas CHANGELOG berbeda dari `localStorage['konoha.changelog.seen']` (pemain tanpa simpanan `konoha.save.v1` tidak diberi toast). Menambah pembaruan: sisipkan objek baru di AWAL `content/changelog.js` dengan version lebih besar. Berkas `content/*.js` ikut `tools/build_precache.py`. - Latar menu utama dan About: `menu.js` `collageHtml()` menyusun tujuh potret `assets/menu/-select.webp` (lima di layar sentuh) menjadi kolase CSS (`.hc-*` di bagian "Merek" paling bawah `menu.css`). Gerak berhenti lewat kelas `.bg-still` pada `#front-menu` (`FrontEnd.syncBackground()`: Pengaturan terbuka, tab tersembunyi, saklar "Animasi latar menu", `prefers-reduced-motion`). Video dan gambar bergaya game asal (`home-factions.webp`, `home-factions-loop.mp4`, `home-art.webp`) tidak dipakai lagi tetapi berkasnya masih ada (dan ikut daftar precache). - `vs.js` (`window.VS`, `window.ResultView`) + `vs.css` + `content/quotes.js` (`window.QUOTES`: intro/win/lose per karakter): layar VS dan layar hasil. `startMatch({..., vs: true | {label, p2}})` menampilkan layar VS 2,2 dtk (hanya mode versus; latihan tidak); selama itu `vsHold` di game.js menahan `advanceMatch` dan cue 'Round 1' keluar tepat sekali saat VS selesai atau dilewati (klik/Spasi/Enter/Esc setelah 0,38 dtk). `startMatch` tanpa `vs` berperilaku seperti dulu. `menu.js` screen `result` memakai `ResultView.view/mount` (layar lama jadi cadangan); `Meta.award` menyimpan rincian di `Meta.lastAward`; `Meta.nextFight`/`finishRun` memakai VS dan layar hasil yang sama. Pemilih kutipan `VS.quote(id, 'intro'|'win'|'lose')` memakai pengacak kosmetik terpisah. API uji: `__game.vsActive()`, `__game.vsSkip()`. Menu depan yang tersembunyi dijamin hilang oleh `#front-menu[hidden]{display:none!important}` (menu.css); aturan `display` per `data-screen` tidak boleh menimpanya. Layar hasil memfokuskan wadah `.rs` (`tabindex=-1`), bukan tombol, agar Spasi/Enter dari pertarungan tidak langsung memicu MAIN LAGI (Tab tetap menjangkau tombol). `Meta.toast` menahan toast selama `VS.active` dan memunculkannya sesudah VS selesai. diff --git a/feel.js b/feel.js index e7fd0e1..3f69ade 100644 --- a/feel.js +++ b/feel.js @@ -8,7 +8,11 @@ 'use strict'; const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); const smooth = u => u * u * (3 - 2 * u); - const calm = () => { try { return matchMedia('(prefers-reduced-motion: reduce)').matches; } catch (_) { return false; } }; + // Satu pengali gerak global: window.Access.motion (access.js, pilihan "Kurangi gerakan" yang otomatis mengikuti prefers-reduced-motion): 1 = penuh, 0 = dikurangi. + // Tanpa modul itu langsung ke prefers-reduced-motion. Getaran layar, sentakan, zoom kamera ultimate, dan kilat sprite memakai pengali yang sama di game.js (draw, drawFlash); + // di sini: zoom dan kilat K.O. serta animasi peringkat kombo (calm = gerak dikurangi). + const motion = () => { try { return window.Access ? window.Access.motion : matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 1; } catch (_) { return 1; } }; + const calm = () => motion() < 1; // ---- 1.7 Tabel penyetelan terpusat. Ubah angka di sini, bukan di game.js. ---- // stop = hit-stop (detik), trauma = getaran acak layar (0..1), kick = sentakan searah pukulan (px dunia), @@ -89,7 +93,7 @@ // ---- 1.8 K.O. slow-motion dan zoom kamera ---- const ko = { on: false, t: 0, cfg: FEEL.ko.round, who: 'dummy', final: false, calm: false }; function koStart(who, final) { - ko.cfg = final ? FEEL.ko.final : FEEL.ko.round; ko.who = who; ko.final = !!final; ko.t = 0; ko.on = true; ko.calm = calm(); // prefers-reduced-motion: zoom dan kilat diperkecil + ko.cfg = final ? FEEL.ko.final : FEEL.ko.round; ko.who = who; ko.final = !!final; ko.t = 0; ko.on = true; ko.calm = calm(); // gerak dikurangi: tanpa zoom dan tanpa kilat } function koClear() { ko.on = false; ko.t = 0; } function koScale() { @@ -99,7 +103,8 @@ if (ko.t <= rampAt) return c.scale; const u = (ko.t - rampAt) / FEEL.ko.ramp; return c.scale + (1 - c.scale) * u * u; } - const koZoomMax = () => ko.calm ? 1 + (ko.cfg.zoom - 1) * .4 : ko.cfg.zoom; + // Gerak dikurangi: zoom K.O. dimatikan. Nilainya sengaja 1,0005 (di bawah ambang kamera 1,002 di game.js) agar pembagi di koDrawOverlay tidak nol dan vignette tetap tampil. + const koZoomMax = () => ko.calm ? 1.0005 : ko.cfg.zoom; function koZoom() { if (!ko.on) return 1; const c = ko.cfg, z = koZoomMax() - 1; @@ -116,7 +121,7 @@ ctx.fillStyle = g; ctx.fillRect(0, 0, w, h); } const f = 1 - ko.t / FEEL.ko.flashTime; - if (f > 0) { ctx.fillStyle = `rgba(255,255,255,${(c.flash * f * f * (ko.calm ? .3 : 1)).toFixed(3)})`; ctx.fillRect(0, 0, w, h); } + if (f > 0) { ctx.fillStyle = `rgba(255,255,255,${(c.flash * f * f * (ko.calm ? 0 : 1)).toFixed(3)})`; ctx.fillRect(0, 0, w, h); } } // Maju dengan waktu NYATA (bukan waktu simulasi yang melambat). diff --git a/game.js b/game.js index f5184aa..fd72045 100644 --- a/game.js +++ b/game.js @@ -59,7 +59,7 @@ function passiveRegen(dt) { if (match && match.phase !== 'fight') return; for (const [side, actor] of [['player', hero], ['enemy', dummy]]) { - const r = passiveMod(actor, 'regen', 0); if (!(r > 0) || !bwAlive(actor) || time - (actor.lastHurt ?? -99) < 3) continue; + const r = passiveMod(actor, 'regen', 0) + (actor === hero ? window.Access?.regen || 0 : 0); if (!(r > 0) || !bwAlive(actor) || time - (actor.lastHurt ?? -99) < 3) continue; // + bantuan "Pemulihan darah pelan" (access.js), hanya sisi pemain if (actor === hero ? hero.hp >= BALANCE.heroMax : dummy.damage <= 0) { pregen[side] = 0; continue; } pregen[side] += r * dt; if (pregen[side] < 1) continue; const n = Math.floor(pregen[side]); pregen[side] -= n; if (actor === hero) hero.hp = Math.min(BALANCE.heroMax, hero.hp + n); else dummy.damage = Math.max(0, dummy.damage - n); @@ -346,7 +346,7 @@ if (hero.doublePose > 0) { hero.rollBuffer = 'attack'; return true; } if (hero.action) { if (hero.action.type === 'attack' && hero.action.index < 3) { hero.action.queued = Math.min(3 - hero.action.index, hero.action.queued + 1); hero.action.cancelInto = null; } return false; } const name = `attack${index}`, duration = selectedCharacter === 'fenr' ? F.move(hero,'attack',index).duration : KITS[selectedCharacter] ? KITS[selectedCharacter].move('attack',index).duration : animationDuration(name, [.39, .43, .57][index - 1]); - hero.action = { type: 'attack', index, name, t: 0, duration, hit: false, queued: 0, fired: false }; + hero.action = { type: 'attack', index, name, t: 0, duration, hit: false, queued: index === 1 && window.Access?.comboAuto ? 2 : 0, fired: false }; // kombo otomatis (access.js): pukulan 1 langsung mengantre dua pukulan berikutnya hero.runDirection = 0; state(name); return true; } // Skills cancel a basic attack at any point, so the kit never waits for the combo to finish. A skill that cannot start @@ -449,7 +449,7 @@ function useShopMove() { const id = window.Meta?.equippedMove?.(), m = SHOP_MOVES[id]; if (!m || !canAct() || hero.action || hero.doublePose > 0 || (hero.shopCd || 0) > 0) return false; - hero.shopCd = m.cd; announce(m.name, .7); sound('cast'); + hero.shopCd = window.Access?.noCooldown ? 0 : m.cd; announce(m.name, .7); sound('cast'); if (m.instant) { if (id === 'perisai') { hero.invuln = Math.max(hero.invuln, 1.6); hero.shield = 1.6; } if (id === 'pulih') { hero.hp = Math.min(BALANCE.heroMax, hero.hp + 25); damageNumbers.push({ x: hero.x, y: hero.y - 150, amount: '+25', life: .9, maxLife: .9, color: '#8dffa8' }); } @@ -526,12 +526,17 @@ function drawFlash(actor, img, r, dx, dy) { flashCv.width = r.w; flashCv.height = r.h; flashCx.drawImage(img, r.x, r.y, r.w, r.h, 0, 0, r.w, r.h); flashCx.globalCompositeOperation = 'source-atop'; flashCx.fillStyle = '#fff'; flashCx.fillRect(0, 0, r.w, r.h); flashCx.globalCompositeOperation = 'source-over'; - ctx.globalAlpha = Math.min(1, actor.flash / (actor.flashMax || actor.flash)) * .85; ctx.drawImage(flashCv, dx, dy, r.w, r.h); ctx.globalAlpha = 1; + ctx.globalAlpha = Math.min(1, actor.flash / (actor.flashMax || actor.flash)) * .85 * (window.Access?.motion ?? 1); ctx.drawImage(flashCv, dx, dy, r.w, r.h); ctx.globalAlpha = 1; // gerak dikurangi: tanpa kilat putih } // ---- Guard (hold L), parry (tap L just before a hit), throw (J), P2, CPU guard ---- + // Blok otomatis (access.js): serangan dari depan ditahan lewat jalur blok biasa bila pemain di tanah, tidak beraksi, dan guard belum pecah. + // Sesudahnya hero.autoGuard menahan sikap blok 0,3 dtk (lihat step) supaya bingkai blok terlihat dan pukulan berantai ikut ditahan. + const autoGuardReady = () => !!window.Access?.autoBlock && hero.grounded && hero.hurtTime <= 0 && (hero.guardBreak || 0) <= 0 && hero.doublePose <= 0 && (dummy.x - hero.x) * hero.facing > 0; function heroGuard(damage, options) { - if (!(hero.blocking || hero.parry > 0) || hero.action || options?.unblockable) return false; + const auto = !hero.blocking && !(hero.parry > 0) && autoGuardReady(); + if (!(hero.blocking || hero.parry > 0 || auto) || hero.action || options?.unblockable) return false; + if (auto || hero.autoGuard > 0) hero.autoGuard = .3; if (hero.parry > 0) { hero.parry = 0; hero.invuln = Math.max(hero.invuln, .35); announce('PARRY!', .6); sound('parry'); window.Meta?.shout?.('Parry!'); feelImpact('hero', 'parry', { dir: -hero.facing, x: hero.x + hero.facing * 40, y: hero.y - 100 }); damageNumbers.push({ x: hero.x, y: hero.y - 170, amount: 'PARRY', kind: 'parry', life: .8, maxLife: .8, color: Feel.table.colors.parry }); @@ -575,7 +580,7 @@ systemAnnouncer?.clear(); stopUltimateVoice(); Object.assign(hero, { x: 350, y: CONFIG.groundY, vx: 0, vy: 0, facing: 1, grounded: true, jumps: 0, state: 'idle', animTime: 0, walkPhase: 0, smokeDistance:0, action: null, runDirection: 0, hp: BALANCE.heroMax, koTime:0, invuln: 0, hurtTime: 0, doublePose: 0, rollDuration: CONFIG.rollDuration, rollBuffer: null, airTime: 0, cooldowns: { skill1: 0, skill2: 0, ultimate: 0 } }); - hero.spCd = hero.shopCd = hero.shield = 0; inputLog.length = 0; pcarry.player = pcarry.enemy = pregen.player = pregen.enemy = 0; hero.lastHurt = dummy.lastHurt = -99; + hero.spCd = hero.shopCd = hero.shield = hero.autoGuard = 0; inputLog.length = 0; pcarry.player = pcarry.enemy = pregen.player = pregen.enemy = 0; hero.lastHurt = dummy.lastHurt = -99; Object.assign(dummy, { x: 915, y: CONFIG.groundY, vx: 0, vy: 0, state: 'idle', stateTime: 0, damage: 0, ko:false, flash: 0, invuln: 0, angle: 0 }); if(F) {F.reset(hero);F.reset(dummy);Object.assign(dummy,{facing:-1,action:null,cooldowns:{skill1:0,skill2:0,ultimate:0},aiThink:.4,aiTime:0,aiChain:0,walkPhase:0,jumps:0});cpu={seen:new WeakMap(),decided:new WeakSet(),read:null,heroActionAt:0,heroAction:null,airborne:false,airAt:0,antiAirRead:false,plan:null,doubleAt:0};syncPlayerForm();} fenrCutin=null;hudIdentity=''; @@ -1817,11 +1822,11 @@ if(fenrCutin) {fenrCutin.t+=dt;if(fenrCutin.t>.78)fenrCutin=null;} if(F&&selectedCharacter==='fenr'){const changes=hero.formChanges;F.advance(hero,dt,hero.hp>0);if(changes!==hero.formChanges){syncPlayerForm();hero.doublePose=0;state(hero.grounded?'idle':'jump');}} hero.airTime = hero.grounded ? 0 : hero.airTime + dt; hero.spCd = Math.max(0, (hero.spCd || 0) - dt); hero.throwCd = Math.max(0, (hero.throwCd || 0) - dt); hero.shopCd = Math.max(0, (hero.shopCd || 0) - dt); hero.shield = Math.max(0, (hero.shield || 0) - dt); - for (const name of Object.keys(hero.cooldowns)) hero.cooldowns[name] = Math.max(0, hero.cooldowns[name] - dt * cdRate(hero, name)); passiveRegen(dt); + for (const name of Object.keys(hero.cooldowns)) hero.cooldowns[name] = Math.max(0, hero.cooldowns[name] - dt * cdRate(hero, name)); window.Access?.capCooldowns?.(hero.cooldowns); passiveRegen(dt); // "Tanpa cooldown" (access.js) if (hero.doublePose <= 0 && hero.rollBuffer && hero.hurtTime <= 0) { const buffered = hero.rollBuffer; hero.rollBuffer = null; if (buffered === 'attack') startAttack(); else cast(buffered); } comboTimer = Math.max(0, comboTimer - dt); if (!comboTimer) comboHits = comboDamage = 0; - hero.parry = Math.max(0, (hero.parry || 0) - dt); hero.guardBreak = Math.max(0, (hero.guardBreak || 0) - dt); hero.guard = Math.max(0, (hero.guard || 0) - dt * 22); - hero.blocking = keys.has('l') && hero.grounded && !hero.action && hero.hurtTime <= 0 && hero.hp > 0 && hero.guardBreak <= 0 && hero.doublePose <= 0; + hero.parry = Math.max(0, (hero.parry || 0) - dt); hero.guardBreak = Math.max(0, (hero.guardBreak || 0) - dt); hero.guard = Math.max(0, (hero.guard || 0) - dt * 22); hero.autoGuard = Math.max(0, (hero.autoGuard || 0) - dt); + hero.blocking = (keys.has('l') || hero.autoGuard > 0) && hero.grounded && !hero.action && hero.hurtTime <= 0 && hero.hp > 0 && hero.guardBreak <= 0 && hero.doublePose <= 0; const dir = (keys.has('d') ? 1 : 0) - (keys.has('a') ? 1 : 0), crouching = (keys.has('s') || hero.blocking) && hero.grounded && !hero.action; if (!hero.action && hero.hurtTime <= 0 && hero.hp > 0) { if (dir && hero.doublePose <= 0) hero.facing = dir; @@ -2744,11 +2749,12 @@ } function draw() { ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); ctx.imageSmoothingEnabled = false; ctx.fillStyle = '#9ed2dc'; ctx.fillRect(0, 0, viewW, viewH); - const kick = Feel.kickOffset(), shakeX = Math.sin(realTime * 93) * trauma * trauma * 13 + kick.x, shakeY = Math.cos(realTime * 111) * trauma * trauma * 9 + kick.y; + // Pengali gerak (access.js "Kurangi gerakan"): 1 = penuh, 0 = tanpa getaran layar (acak dan sentakan searah pukulan) dan tanpa zoom kamera ultimate. + const mo = window.Access?.motion ?? 1, kick = Feel.kickOffset(), shakeX = (Math.sin(realTime * 93) * trauma * trauma * 13 + kick.x) * mo, shakeY = (Math.cos(realTime * 111) * trauma * trauma * 9 + kick.y) * mo; // BRO WO's ultimate dives the camera onto him: the whole picture (stage and fighters) is zoomed around a point that eases // toward the screen centre, clamped so the zoomed stage never leaves an empty edge; the stage is then darkened. // K.O. (feel.js) memakai kamera yang sama: zoom ke yang kalah, dipusatkan penuh. - const koCam = koCamera(), camZoom = koCam ? koCam.z : bwx.zoom, camera = camZoom > 1.002 || bwx.dim > .004; + const koCam = koCamera(), camZoom = koCam ? koCam.z : 1 + (bwx.zoom - 1) * mo, camera = camZoom > 1.002 || bwx.dim > .004; if (camera) { const sx = offsetX + (koCam ? koCam.x : bwx.cx) * scale, sy = offsetY + (koCam ? koCam.y : bwx.cy) * scale, z = camZoom, w = koCam ? 1 : clamp((z - 1) / .75, 0, 1); const ax = clamp(sx + (viewW / 2 - sx) * w, viewW - z * (viewW - sx), z * sx), ay = clamp(sy + (viewH * .55 - sy) * w, viewH - z * (viewH - sy), z * sy); @@ -2806,7 +2812,7 @@ // SIAP berdenyut; selama mode bar menjadi waktu mode yang menyusut. DOM hanya ditulis saat tampilannya berubah. const ULT_COLOR = { arco: '#67e4dd', fenr: '#e0a060', browo: '#d9b44a' }, ultMeters = {}; function ultMax(side) { - if (side === 'player') return cooldownMax.ultimate; + if (side === 'player') return window.Access?.cdMax?.('ultimate', cooldownMax.ultimate) ?? cooldownMax.ultimate; // "Tanpa cooldown": bilah ikut batas 5 dtk const id = opponentCharacter; return (id === 'fenr' ? F?.balance?.cooldowns?.ultimate : KITS[id]?.balance?.cooldowns?.ultimate) || BALANCE.ultimate.cooldown; } @@ -2908,7 +2914,7 @@ card.classList.toggle('on-cooldown', cd > 0); card.setAttribute('aria-disabled', String(cd > 0 || (name==='ultimate'&&selectedCharacter==='fenr'&&(hero.form==='wolf'||hero.transformPending>0)))); card.classList.toggle('recharged',(rechargePulse[name]||0)>0); const wipe = card.querySelector('.cooldown-wipe'); - if (wipe) { const angle = cd > 0 ? (1 - cd / cooldownMax[name]) * 360 : 360; wipe.style.background = `conic-gradient(from 0deg, transparent 0deg ${angle.toFixed(2)}deg, rgba(0,0,0,.68) ${angle.toFixed(2)}deg 360deg)`; } + if (wipe) { const angle = cd > 0 ? (1 - cd / (name === 'ultimate' ? ultMax('player') : cooldownMax[name])) * 360 : 360; wipe.style.background = `conic-gradient(from 0deg, transparent 0deg ${angle.toFixed(2)}deg, rgba(0,0,0,.68) ${angle.toFixed(2)}deg 360deg)`; } } // One banner at a time: FENR transformation, then ARCO, then a MIRA/CORA/NAJA/HALDOR summon still inside its cut-in window. const cutin=$('#ultimate-cutin'),live=g=>g&&g.t{cutinArt.style.visibility='';};}cutin.querySelector('.cutin-kicker').textContent={fenr:'FENR / '+cutinOwner,mira:'MIRA / '+cutinOwner,cora:'CORA / '+cutinOwner,naja:'NAJA / '+cutinOwner,haldor:'HALDOR / '+cutinOwner,zanni:'ZANNI / '+cutinOwner,isolde:'ISOLDE / '+cutinOwner,rhea:'RHEA / '+cutinOwner,solan:'SOLAN / '+cutinOwner,browo:'BRO WO / '+cutinOwner,browofire:'BRO WO / '+cutinOwner,nib:'NIB / '+cutinOwner,edda:'EDDA / '+cutinOwner,arco:'ARCO / AETHER COMMAND'}[cutinKey];cutin.querySelector('strong').innerHTML={fenr:'FERAL
AWAKENING',mira:'ROCKET
PARADE',cora:'NIGHT
MURMURATION',naja:'DUNE
SERPENT',haldor:'FORGE
QUAKE',zanni:'GRAND
FINALE',isolde:'SKYFALL
LANCES',rhea:'GRAND
ORRERY',solan:'SUNMANE
ROAR',browo:'HUTAN
SAWIT',browofire:'TORNADO
API SAWIT',nib:'SPECIAL
DELIVERY',edda:'ELDER
TORTOISE',arco:'HELIOS
SQUADRON'}[cutinKey];cutin.querySelector('.cutin-detail').textContent={fenr:'THE BEAST WITHIN',mira:'TWELVE-ROCKET SALVO',cora:'THREE-PASS RAVEN STORM',naja:'THREE-STRIKE SAND COBRA',haldor:'THREE-SLAM MOLTEN SHOCKWAVE',zanni:'THREE-RING BLADE BOOMERANG',isolde:'THREE-LANCE ICE DIVE',rhea:'THREE-PLANET ORBIT',solan:'THREE-ROAR SHOCKWAVE',browo:'TEN-PALM RUN + GIANT SLAM',browofire:'FIRE TORNADO FINISHER',nib:'THREE-PARCEL HOMING RUN',edda:'THREE-STOMP SPIRIT WALK',arco:'ORBITAL LASER STRIKE'}[cutinKey];cutin.setAttribute('aria-label',{fenr:'FENR ultimate: Feral Awakening',mira:'MIRA ultimate: Rocket Parade',cora:'CORA ultimate: Night Murmuration',naja:'NAJA ultimate: Dune Serpent',haldor:'HALDOR ultimate: Forge Quake',zanni:'ZANNI ultimate: Grand Finale',isolde:'ISOLDE ultimate: Skyfall Lances',rhea:'RHEA ultimate: Grand Orrery',solan:'SOLAN ultimate: Sunmane Roar',browo:'BRO WO ultimate: Hutan Sawit',browofire:'BRO WO rage ultimate: Tornado Api Sawit',nib:'NIB ultimate: Special Delivery',edda:'EDDA ultimate: Elder Tortoise',arco:'ARCO ultimate: Helios Squadron'}[cutinKey]);cfCutin(cutinKey,cutinOwner);} const visible = ct < SQUAD.cutinDuration; cutin.classList.toggle('visible', visible); cutin.setAttribute('aria-hidden', String(!visible)); - const slide = !visible ? -110 : ct < .16 ? -110 * Math.pow(1 - ct / .16,3) : ct > .57 ? 110 * Math.pow((ct-.57)/.21,2) : 0; + const slide = window.Access?.reduced ? 0 : !visible ? -110 : ct < .16 ? -110 * Math.pow(1 - ct / .16,3) : ct > .57 ? 110 * Math.pow((ct-.57)/.21,2) : 0; cutin.style.transform = `translateX(${slide}%) rotate(-3deg)`; cutin.style.opacity = visible ? '1' : '0'; document.querySelectorAll('.combo-dots i').forEach((dot, i) => dot.classList.toggle('lit', hero.action?.type === 'attack' && hero.action.index > i)); diff --git a/index.html b/index.html index ad9e67f..cf7b201 100644 --- a/index.html +++ b/index.html @@ -1,7 +1,7 @@ Konoha Fighting - +
Menyiapkan…

@@ -24,6 +24,6 @@
✦KONOHA FIGHTING

Menyiapkan arena…
KONOHA FIGHTING / SETTINGS

Pengaturan arena

Atur audio, tampilan, dan kontrol permainan.

Kontrol keyboard

A / D · Gerak   2× A / D · Lari
W · Lompat (2× W = lompat ganda)   S · Menunduk
SPACE · Kombo 3 pukulan   ↓ ↓ + SPACE · Uppercut   ↓ → + SPACE · Tinju maju
I / O · Skill   P · Ultimate   K · Jurus toko
L · Blok (ketuk = parry)   J · Lempar
Esc · Jeda, berisi daftar gerakan karakter   R · Mulai ulang
Dua pemain (P2): ← → · Gerak   ↑ · Lompat   N · Serang   M / , / . · Skill 1 / 2 / Ultimate
Meter di bawah HP: bar ultimate (SIAP = bisa dipakai), saat mode aktif jadi bar waktu mode.

KONOHA FIGHTING / JEDA

JEDA

Pertarungan dijeda. Siap melanjutkan?

Daftar gerakan
-
+
diff --git a/vs.js b/vs.js index 72f0f85..dbb0166 100644 --- a/vs.js +++ b/vs.js @@ -139,7 +139,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;