Tambah mode bantuan dan aksesibilitas (RENCANA 4.2 dan 4.3): modul access.js + access.css
Kotak lipat "Bantuan & aksesibilitas" disisipkan ke Pengaturan lewat JS (pilihan tersimpan di localStorage, tiap pilihan punya Atur ulang, plus Atur ulang semua). Bantuan (hanya sisi pemain): Kombo otomatis (satu ketukan Spasi = rantai 3 pukulan), Blok otomatis (serangan dari depan ditahan lewat jalur blok biasa), Tanpa cooldown (skill dan jurus toko tanpa cooldown, ultimate siap paling lama 5 dtk), Pemulihan darah pelan (+2 HP/dtk). Aksesibilitas: ukuran teks Normal/Besar/Sangat besar (variabel CSS), mode buta warna protanopia/deuteranopia/tritanopia (filter SVG pada arena dan HUD, bar HP lawan bergaris dan lapisan cadangan bertitik), Kurangi gerakan (otomatis mengikuti prefers-reduced-motion atau dipaksa; pengali Access.motion untuk getaran layar, zoom K.O., kilat, dan animasi latar), Kontras tinggi untuk HUD. Kait kecil di game.js, feel.js, dan vs.js memakai window.Access secara malas (tanpa modul semuanya no-op); catatan mesin diperbarui. Mode ringan: diuji node --check dan smoke (tanpa galat halaman); keadaan pilihan aktif dan tampilan belum diuji, pengguna menguji manual.
This commit is contained in:
parent
b4a32e5c4a
commit
6244437fbb
|
|
@ -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 <html> (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}
|
||||
|
|
@ -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 <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); }
|
||||
};
|
||||
})();
|
||||
|
|
@ -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 `<details>`, karena penangan tombol game mencegat Spasi pada `<summary>`), 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 `<html>`: `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/<id>-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.
|
||||
|
|
|
|||
13
feel.js
13
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).
|
||||
|
|
|
|||
34
game.js
34
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<SQUAD.cutinDuration?g:null;
|
||||
|
|
@ -2920,7 +2926,7 @@
|
|||
if(cutin.dataset.identity!==cutinKey+cutinOwner){cutin.dataset.identity=cutinKey+cutinOwner;cutinArt.src=CUTIN_ART[cutinKey];if(cutinArt.complete)cutinArt.style.visibility='';else{cutinArt.style.visibility='hidden';cutinArt.onload=()=>{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<br><em>AWAKENING</em>',mira:'ROCKET<br><em>PARADE</em>',cora:'NIGHT<br><em>MURMURATION</em>',naja:'DUNE<br><em>SERPENT</em>',haldor:'FORGE<br><em>QUAKE</em>',zanni:'GRAND<br><em>FINALE</em>',isolde:'SKYFALL<br><em>LANCES</em>',rhea:'GRAND<br><em>ORRERY</em>',solan:'SUNMANE<br><em>ROAR</em>',browo:'HUTAN<br><em>SAWIT</em>',browofire:'TORNADO<br><em>API SAWIT</em>',nib:'SPECIAL<br><em>DELIVERY</em>',edda:'ELDER<br><em>TORTOISE</em>',arco:'HELIOS<br><em>SQUADRON</em>'}[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));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<html lang="id">
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#101923"><script>/* Phones/tablets: mark .touch and open the landscape mobile shell (mobile.html). ?touch=1 forces it on desktop, ?touch=0 turns it off. */(()=>{const q=new URLSearchParams(location.search),f=q.get('touch'),t=f?f==='1':matchMedia('(pointer: coarse)').matches||(navigator.maxTouchPoints>0&&/Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent));if(!t)return;document.documentElement.classList.add('touch');if(window.top===window&&!q.has('embed'))location.replace('mobile.html'+location.search+location.hash);})();</script><title>Konoha Fighting</title><link rel="preload" href="assets/fonts/Rajdhani-Bold.ttf" as="font" type="font/ttf" crossorigin><link rel="stylesheet" href="style.css"><link rel="stylesheet" href="menu.css"><link rel="stylesheet" href="meta.css">
|
||||
<link rel="stylesheet" href="feel.css"><link rel="stylesheet" href="vs.css"><link rel="stylesheet" href="story.css"><link rel="stylesheet" href="mystery.css"></head>
|
||||
<link rel="stylesheet" href="feel.css"><link rel="stylesheet" href="vs.css"><link rel="stylesheet" href="story.css"><link rel="stylesheet" href="mystery.css"><link rel="stylesheet" href="access.css"></head>
|
||||
<body><div id="boot" role="status" aria-live="polite"><div class="boot-card"><strong class="boot-logo">KONOHA<br><em>FIGHTING</em></strong><div class="boot-bar"><i></i></div><span class="boot-status">Menyiapkan…</span><p class="boot-quote"></p></div></div><script src="content/loading.js"></script><script src="content/changelog.js"></script><script src="brand.js"></script><script src="content/story.js"></script><script src="story.js"></script><script src="precache.js"></script><script src="preload.js"></script><main id="game-shell" aria-label="Konoha Fighting, game fighting parodi">
|
||||
<canvas id="arena" tabindex="0" aria-label="Arena pertarungan. WASD bergerak, Spasi menyerang, I O P untuk skill."></canvas><div class="vignette" aria-hidden="true"></div>
|
||||
<header class="combat-header">
|
||||
|
|
@ -24,6 +24,6 @@
|
|||
<div id="load-state" class="load-state"><span class="loading-glyph">✦</span><strong>KONOHA FIGHTING</strong><p class="load-quote"></p><span id="load-message">Menyiapkan arena…</span></div>
|
||||
<dialog id="settings-panel"><div class="dialog-kicker">KONOHA FIGHTING / SETTINGS</div><h2>Pengaturan arena</h2><p>Atur audio, tampilan, dan kontrol permainan.</p><label class="setting-row training-only"><span>Karakter pemain<small>Mengganti karakter memulai ulang latihan</small></span><select id="character-select"><option value="browo">BRO WO · Human</option><option value="gufron">GUFRON · Human</option><option value="soloman">SOLO MAN · Human</option><option value="anice">A NICE · Human</option><option value="ethanolman">ETHANOL MAN · Human</option><option value="sonofsolo">SON OF SOLO MAN · Human</option><option value="megachan">MEGA CHAN · Human</option></select></label><label class="setting-row training-only"><span>Lawan aktif<small>Aktifkan serangan CPU saat latihan</small></span><input type="checkbox" id="ai-toggle" checked></label><label class="setting-row"><span>Visualisasi hitbox<small>Area tubuh dan jangkauan serangan</small></span><input type="checkbox" id="hitbox-toggle"></label><label class="setting-row"><span>Audio aktif<small>Suara karakter, benturan, dan skill</small></span><input type="checkbox" id="sound-toggle" checked></label><label class="setting-row"><span>Musik latar<small>Midday Showdown, diputar berulang di menu dan arena</small></span><input type="checkbox" id="music-toggle" checked></label><label class="volume-row"><span>Musik</span><input type="range" id="music-slider" min="0" max="100" value="5" aria-label="Volume musik"><output id="music-value">5%</output></label><label class="setting-row"><span>Animasi latar menu<small>Gerak pelan petarung di latar main menu</small></span><input type="checkbox" id="menu-video-toggle" checked></label><label class="volume-row"><span>Suara</span><input type="range" id="volume-slider" min="0" max="100" value="45" aria-label="Volume audio"><output id="volume-value">45%</output></label><div class="dialog-buttons"><button id="sound-test">Tes audio</button><button id="fullscreen-btn">Layar penuh</button></div><details class="controls-help"><summary>Kontrol keyboard</summary><p>A / D · Gerak 2× A / D · Lari<br>W · Lompat (2× W = lompat ganda) S · Menunduk<br>SPACE · Kombo 3 pukulan ↓ ↓ + SPACE · Uppercut ↓ → + SPACE · Tinju maju<br>I / O · Skill P · Ultimate K · Jurus toko<br>L · Blok (ketuk = parry) J · Lempar<br>Esc · Jeda, berisi daftar gerakan karakter R · Mulai ulang<br>Dua pemain (P2): ← → · Gerak ↑ · Lompat N · Serang M / , / . · Skill 1 / 2 / Ultimate<br>Meter di bawah HP: bar ultimate (SIAP = bisa dipakai), saat mode aktif jadi bar waktu mode.</p></details><button class="primary-button" id="close-settings">Kembali ke arena <span>↗</span></button></dialog>
|
||||
<dialog id="pause-panel" aria-labelledby="pause-title"><div class="pause-main"><div class="dialog-kicker">KONOHA FIGHTING / JEDA</div><h2 id="pause-title">JEDA</h2><p>Pertarungan dijeda. Siap melanjutkan?</p><button class="primary-button" id="resume-btn">Lanjut bertarung <span>↗</span></button><button class="text-button" id="restart-btn">Mulai ulang</button><button class="text-button" id="back-menu">Kembali ke main menu</button></div><details class="move-list" id="move-list"><summary>Daftar gerakan</summary><div class="move-body" id="move-list-body"></div></details></dialog>
|
||||
</main><section id="front-menu" aria-label="Main menu Konoha Fighting"></section><script src="assets/mecha/manifest.js"></script><script src="assets/stage-config.js"></script><script src="assets/ui/drone-config.js"></script><script src="assets/fenr/human/manifest.js"></script><script src="assets/fenr/wolf/manifest.js"></script><script src="fenr.js"></script><script src="assets/browo/manifest.js"></script><script src="browo.js"></script><script src="assets/megachan/manifest.js"></script><script src="assets/sonofsolo/manifest.js"></script><script src="assets/ethanolman/manifest.js"></script><script src="assets/anice/manifest.js"></script><script src="assets/soloman/manifest.js"></script><script src="assets/gufron/manifest.js"></script><script src="assets/audio/announcer/manifest.js"></script><script src="announcer.js"></script><script src="match.js"></script><script src="feel.js"></script><script src="content/passives.js"></script><script src="passives.js"></script><script src="content/quotes.js"></script><script src="content/dialogs.js"></script><script src="vs.js"></script><script>window.FRONTEND_ENABLED=true;</script><script src="game.js"></script><script src="touch.js"></script><script src="content/mystery.js"></script><script src="mystery.js"></script><script src="menu.js"></script><script src="meta.js"></script></body></html>
|
||||
</main><section id="front-menu" aria-label="Main menu Konoha Fighting"></section><script src="assets/mecha/manifest.js"></script><script src="assets/stage-config.js"></script><script src="assets/ui/drone-config.js"></script><script src="assets/fenr/human/manifest.js"></script><script src="assets/fenr/wolf/manifest.js"></script><script src="fenr.js"></script><script src="assets/browo/manifest.js"></script><script src="browo.js"></script><script src="assets/megachan/manifest.js"></script><script src="assets/sonofsolo/manifest.js"></script><script src="assets/ethanolman/manifest.js"></script><script src="assets/anice/manifest.js"></script><script src="assets/soloman/manifest.js"></script><script src="assets/gufron/manifest.js"></script><script src="assets/audio/announcer/manifest.js"></script><script src="announcer.js"></script><script src="match.js"></script><script src="feel.js"></script><script src="access.js"></script><script src="content/passives.js"></script><script src="passives.js"></script><script src="content/quotes.js"></script><script src="content/dialogs.js"></script><script src="vs.js"></script><script>window.FRONTEND_ENABLED=true;</script><script src="game.js"></script><script src="touch.js"></script><script src="content/mystery.js"></script><script src="mystery.js"></script><script src="menu.js"></script><script src="meta.js"></script></body></html>
|
||||
|
||||
|
||||
|
|
|
|||
2
vs.js
2
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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue