Gabung wf/t4-assist: bantuan dan aksesibilitas
This commit is contained in:
commit
04b9acc689
|
|
@ -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); }
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
@ -22,6 +22,7 @@ Game fighting 2D canvas, tanpa dependensi dan tanpa langkah build. Semua teks un
|
||||||
- Gamepad dan ubah tombol (Tahap 4.5): `input.js` + `input.css` (`window.Input`), dimuat tepat setelah `touch.js`; satu-satunya kait ke game.js adalah `__game.input` (plus pembungkus `__game.startMatch/setP2` untuk mengetahui mode dua pemain; bila kelak ada `__game.p2Active()`, itu yang dipakai). **Ubah tombol** (Pengaturan > "Ubah tombol…", dialog `#keymap-dialog`): 11 aksi Pemain 1 disimpan di `localStorage['konoha.keymap.v1']` sebagai `{aksi: {c: e.code, l: label}}`. Hanya bila pemetaan berbeda dari bawaan, keydown/keyup ditangkap di fase capture pada `window`, diterjemahkan ke kunci kanonik (a d w s spasi i o p k l j) lewat `__game.input`, dan event aslinya dihentikan; tombol bawaan yang sudah dipindah ditelan (termasuk keyup-nya, supaya game.js tidak melepas kunci kanonik yang ditahan lewat tombol baru). Tidak aktif di menu depan (`body.in-menu`), dialog terbuka, layar VS/kartu cerita, atau kolom teks; Spasi/Enter pada `<button>` dibiarkan seperti di game.js. Tidak bisa dipetakan: panah, N, M, koma, titik (Pemain 2), Esc, R, Enter, Tab, Ctrl/Alt/Cmd, tombol F; bentrok antaraksi = kedua tombol ditukar. Bantuan kontrol di Pengaturan ditulis ulang dari teks aslinya (5 baris pertama), label `kbd` di HUD (kartu skill, meter ultimate; meter ditulis ulang game.js tiap keadaannya berubah jadi diamati) dan Daftar gerakan menu jeda (hanya bagian Kontrol dasar, Serangan dan skill, Gerakan perintah, Mode) mengikuti pemetaan lewat MutationObserver tanpa mengubah game.js. Daftar gerakan di menu Karakter (meta.js) dan teks "tombol K/I/O" di toko belum mengikuti. **Gamepad**: polling `requestAnimationFrame` hanya selama ada gamepad (tata letak standar; pad pertama = P1, pad kedua = P2 bila mode dua pemain). Di arena: stik/D-pad = a d w s, A/Cross = w, X/Square = spasi, Y/Triangle, B/Circle, R1 = i o p, L1/L2/R2 = k l j, Start = Esc; pad P2 memakai kunci P2 (panah, n m , .). Di menu depan: keydown sintetis ArrowKeys/Enter/Escape yang ditangani menu.js (bila tidak ditangani, fokus dipindah dan tombol diklik); di dialog (pengaturan, jeda, ubah tombol), hamparan Meta, layar VS dan kartu cerita: fokus dipindah langsung, A = klik/Enter, B/Start = tutup (Esc lewat `__game.input('escape')`). Kontrol yang sedang ditekan saat konteks berganti diabaikan sampai dilepas. Getaran lewat `vibrationActuator` saat `hero.hp` turun (dimatikan lewat Pengaturan, `localStorage['konoha.pad.v1']`). Gamepad API hanya jalan di `localhost`/HTTPS. Belum ada skenario otomatis (diuji manual).
|
- Gamepad dan ubah tombol (Tahap 4.5): `input.js` + `input.css` (`window.Input`), dimuat tepat setelah `touch.js`; satu-satunya kait ke game.js adalah `__game.input` (plus pembungkus `__game.startMatch/setP2` untuk mengetahui mode dua pemain; bila kelak ada `__game.p2Active()`, itu yang dipakai). **Ubah tombol** (Pengaturan > "Ubah tombol…", dialog `#keymap-dialog`): 11 aksi Pemain 1 disimpan di `localStorage['konoha.keymap.v1']` sebagai `{aksi: {c: e.code, l: label}}`. Hanya bila pemetaan berbeda dari bawaan, keydown/keyup ditangkap di fase capture pada `window`, diterjemahkan ke kunci kanonik (a d w s spasi i o p k l j) lewat `__game.input`, dan event aslinya dihentikan; tombol bawaan yang sudah dipindah ditelan (termasuk keyup-nya, supaya game.js tidak melepas kunci kanonik yang ditahan lewat tombol baru). Tidak aktif di menu depan (`body.in-menu`), dialog terbuka, layar VS/kartu cerita, atau kolom teks; Spasi/Enter pada `<button>` dibiarkan seperti di game.js. Tidak bisa dipetakan: panah, N, M, koma, titik (Pemain 2), Esc, R, Enter, Tab, Ctrl/Alt/Cmd, tombol F; bentrok antaraksi = kedua tombol ditukar. Bantuan kontrol di Pengaturan ditulis ulang dari teks aslinya (5 baris pertama), label `kbd` di HUD (kartu skill, meter ultimate; meter ditulis ulang game.js tiap keadaannya berubah jadi diamati) dan Daftar gerakan menu jeda (hanya bagian Kontrol dasar, Serangan dan skill, Gerakan perintah, Mode) mengikuti pemetaan lewat MutationObserver tanpa mengubah game.js. Daftar gerakan di menu Karakter (meta.js) dan teks "tombol K/I/O" di toko belum mengikuti. **Gamepad**: polling `requestAnimationFrame` hanya selama ada gamepad (tata letak standar; pad pertama = P1, pad kedua = P2 bila mode dua pemain). Di arena: stik/D-pad = a d w s, A/Cross = w, X/Square = spasi, Y/Triangle, B/Circle, R1 = i o p, L1/L2/R2 = k l j, Start = Esc; pad P2 memakai kunci P2 (panah, n m , .). Di menu depan: keydown sintetis ArrowKeys/Enter/Escape yang ditangani menu.js (bila tidak ditangani, fokus dipindah dan tombol diklik); di dialog (pengaturan, jeda, ubah tombol), hamparan Meta, layar VS dan kartu cerita: fokus dipindah langsung, A = klik/Enter, B/Start = tutup (Esc lewat `__game.input('escape')`). Kontrol yang sedang ditekan saat konteks berganti diabaikan sampai dilepas. Getaran lewat `vibrationActuator` saat `hero.hp` turun (dimatikan lewat Pengaturan, `localStorage['konoha.pad.v1']`). Gamepad API hanya jalan di `localhost`/HTTPS. Belum ada skenario otomatis (diuji manual).
|
||||||
- Tata letak sentuh yang bisa diatur (RENCANA 4.6): `touchlayout.js` + `touchlayout.css`, dimuat `defer` di `<head>` index.html (satu baris tag baru bersama manifest dan `pwa.js`); hanya aktif di `html.touch`, `touch.js` tidak diubah. Pengaturan mendapat baris `#tl-row` (disisipkan lewat JS sebelum `.dialog-buttons`) yang membuka editor `#tl-editor`: `<dialog>` modal layar penuh yang menumpuk di atas Pengaturan (Pengaturan disembunyikan lewat `html.tl-editing`, tetap terbuka sehingga `settingsOpen` menahan game; Esc ditangkap fase capture agar game tidak menutup Pengaturan). Di dalamnya ada salinan (`cloneNode`, tanpa pendengar dan `inert`) `.dpad`, `.action-pad`, `.extra-pad` plus kotak seret `.tl-box` dengan pegangan sudut; panah/+/- juga bekerja di papan ketik. Hasil ke kontrol asli hanya kelas `.tl-xf` + variabel `--tl-x/--tl-y/--tl-s` (`transform: translate() scale()`, titik tumpu sudut dekat tepi); elemen bawaan tidak disentuh sama sekali. Simpanan `localStorage['konoha.touchlayout.v1']` = `{v:1, g:{dpad|action|extra:{x,y,s}}}` (x,y = pecahan lebar/tinggi layar, s = skala 0,6-1,8). Penjepitan ke layar memakai `getBoundingClientRect` nyata (area = layar - tepi aman - 4 px - bilah editor 46 px di atas), dijalankan `ResizeObserver` pada `#touch-controls` saat kontrol tampil/ukuran berubah; nilai simpanan tidak ditimpa penjepitan. `window.TouchLayout` = `{open(), editing, layout}` (konsol: `TouchLayout.open()`). Belum ada skenario otomatis dan belum pernah dijalankan di peramban oleh penulisnya (hanya `node --check` dan smoke desktop): uji manual di 844x390 dengan `?touch=1`.
|
- Tata letak sentuh yang bisa diatur (RENCANA 4.6): `touchlayout.js` + `touchlayout.css`, dimuat `defer` di `<head>` index.html (satu baris tag baru bersama manifest dan `pwa.js`); hanya aktif di `html.touch`, `touch.js` tidak diubah. Pengaturan mendapat baris `#tl-row` (disisipkan lewat JS sebelum `.dialog-buttons`) yang membuka editor `#tl-editor`: `<dialog>` modal layar penuh yang menumpuk di atas Pengaturan (Pengaturan disembunyikan lewat `html.tl-editing`, tetap terbuka sehingga `settingsOpen` menahan game; Esc ditangkap fase capture agar game tidak menutup Pengaturan). Di dalamnya ada salinan (`cloneNode`, tanpa pendengar dan `inert`) `.dpad`, `.action-pad`, `.extra-pad` plus kotak seret `.tl-box` dengan pegangan sudut; panah/+/- juga bekerja di papan ketik. Hasil ke kontrol asli hanya kelas `.tl-xf` + variabel `--tl-x/--tl-y/--tl-s` (`transform: translate() scale()`, titik tumpu sudut dekat tepi); elemen bawaan tidak disentuh sama sekali. Simpanan `localStorage['konoha.touchlayout.v1']` = `{v:1, g:{dpad|action|extra:{x,y,s}}}` (x,y = pecahan lebar/tinggi layar, s = skala 0,6-1,8). Penjepitan ke layar memakai `getBoundingClientRect` nyata (area = layar - tepi aman - 4 px - bilah editor 46 px di atas), dijalankan `ResizeObserver` pada `#touch-controls` saat kontrol tampil/ukuran berubah; nilai simpanan tidak ditimpa penjepitan. `window.TouchLayout` = `{open(), editing, layout}` (konsol: `TouchLayout.open()`). Belum ada skenario otomatis dan belum pernah dijalankan di peramban oleh penulisnya (hanya `node --check` dan smoke desktop): uji manual di 844x390 dengan `?touch=1`.
|
||||||
- PWA (RENCANA 4.7): `manifest.webmanifest` (id `konoha-fighting`, `display: fullscreen` + `display_override [fullscreen, standalone]`, landscape, warna `#101923`, ikon PNG any dan maskable 192/512) ditautkan di index.html dan mobile.html bersama `rel=icon` dan `apple-touch-icon`. Ikon di `assets/pwa/` dibuat `tools/make_pwa_icons.py` (hanya Pillow) dari `assets/menu/browo-select.webp`; jalankan ulang bila ingin mengganti gambar. `pwa.js` (`window.KonohaPWA = {available(), install(), onChange(fn)}`) dimuat sinkron di `<head>` kedua halaman karena `beforeinstallprompt` datang ke dokumen paling atas: di HP itu `mobile.html`, dan iframe game memakai `window.parent.KonohaPWA` (panggilan langsung supaya gestur ketuk tetap berlaku untuk `prompt()`). Baris `#pwa-row` ("Pasang aplikasi") di Pengaturan hanya tampil bila event tersedia dan belum terpasang; iOS tidak punya event itu, jadi di sana tombol tidak tampil. `sw.js`: perilaku lama dipertahankan (gambar/font cache dulu, HTML/CSS/JS jaringan dulu); navigasi yang gagal dan tidak ada di cache mendapat `offline.html` dari cache kecil `konoha-offline-v1` (terpisah dari `aether-assets` karena `preload.js` menghapus entri yang tidak ada di `precache.js`). `tools/build_precache.py` kini memuat `manifest.webmanifest` dan `assets/pwa/*` (`offline.html`, `pwa.js`, `touchlayout.*` ikut otomatis); `precache.js`/`files.json` perlu dibangun ulang oleh integrator. mp3/mp4 tetap dilayani jaringan, jadi tanpa internet game jalan tanpa musik. Server perlu MIME `application/manifest+json` untuk `.webmanifest` (sudah di `.htaccess`).
|
- PWA (RENCANA 4.7): `manifest.webmanifest` (id `konoha-fighting`, `display: fullscreen` + `display_override [fullscreen, standalone]`, landscape, warna `#101923`, ikon PNG any dan maskable 192/512) ditautkan di index.html dan mobile.html bersama `rel=icon` dan `apple-touch-icon`. Ikon di `assets/pwa/` dibuat `tools/make_pwa_icons.py` (hanya Pillow) dari `assets/menu/browo-select.webp`; jalankan ulang bila ingin mengganti gambar. `pwa.js` (`window.KonohaPWA = {available(), install(), onChange(fn)}`) dimuat sinkron di `<head>` kedua halaman karena `beforeinstallprompt` datang ke dokumen paling atas: di HP itu `mobile.html`, dan iframe game memakai `window.parent.KonohaPWA` (panggilan langsung supaya gestur ketuk tetap berlaku untuk `prompt()`). Baris `#pwa-row` ("Pasang aplikasi") di Pengaturan hanya tampil bila event tersedia dan belum terpasang; iOS tidak punya event itu, jadi di sana tombol tidak tampil. `sw.js`: perilaku lama dipertahankan (gambar/font cache dulu, HTML/CSS/JS jaringan dulu); navigasi yang gagal dan tidak ada di cache mendapat `offline.html` dari cache kecil `konoha-offline-v1` (terpisah dari `aether-assets` karena `preload.js` menghapus entri yang tidak ada di `precache.js`). `tools/build_precache.py` kini memuat `manifest.webmanifest` dan `assets/pwa/*` (`offline.html`, `pwa.js`, `touchlayout.*` ikut otomatis); `precache.js`/`files.json` perlu dibangun ulang oleh integrator. mp3/mp4 tetap dilayani jaringan, jadi tanpa internet game jalan tanpa musik. Server perlu MIME `application/manifest+json` untuk `.webmanifest` (sudah di `.htaccess`).
|
||||||
|
- 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`.
|
- `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).
|
- 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.
|
- `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';
|
'use strict';
|
||||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||||
const smooth = u => u * u * (3 - 2 * u);
|
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. ----
|
// ---- 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),
|
// 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 ----
|
// ---- 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 };
|
const ko = { on: false, t: 0, cfg: FEEL.ko.round, who: 'dummy', final: false, calm: false };
|
||||||
function koStart(who, final) {
|
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 koClear() { ko.on = false; ko.t = 0; }
|
||||||
function koScale() {
|
function koScale() {
|
||||||
|
|
@ -99,7 +103,8 @@
|
||||||
if (ko.t <= rampAt) return c.scale;
|
if (ko.t <= rampAt) return c.scale;
|
||||||
const u = (ko.t - rampAt) / FEEL.ko.ramp; return c.scale + (1 - c.scale) * u * u;
|
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() {
|
function koZoom() {
|
||||||
if (!ko.on) return 1;
|
if (!ko.on) return 1;
|
||||||
const c = ko.cfg, z = koZoomMax() - 1;
|
const c = ko.cfg, z = koZoomMax() - 1;
|
||||||
|
|
@ -116,7 +121,7 @@
|
||||||
ctx.fillStyle = g; ctx.fillRect(0, 0, w, h);
|
ctx.fillStyle = g; ctx.fillRect(0, 0, w, h);
|
||||||
}
|
}
|
||||||
const f = 1 - ko.t / FEEL.ko.flashTime;
|
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).
|
// Maju dengan waktu NYATA (bukan waktu simulasi yang melambat).
|
||||||
|
|
|
||||||
34
game.js
34
game.js
|
|
@ -61,7 +61,7 @@
|
||||||
function passiveRegen(dt) {
|
function passiveRegen(dt) {
|
||||||
if (match && match.phase !== 'fight') return;
|
if (match && match.phase !== 'fight') return;
|
||||||
for (const [side, actor] of [['player', hero], ['enemy', dummy]]) {
|
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; }
|
if (actor === hero ? hero.hp >= BALANCE.heroMax : dummy.damage <= 0) { pregen[side] = 0; continue; }
|
||||||
pregen[side] += r * dt; if (pregen[side] < 1) 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);
|
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);
|
||||||
|
|
@ -351,7 +351,7 @@
|
||||||
if (hero.doublePose > 0) { hero.rollBuffer = 'attack'; return true; }
|
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; }
|
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]);
|
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
|
||||||
window.MatchStats?.cast('attack', hero.action);
|
window.MatchStats?.cast('attack', hero.action);
|
||||||
hero.runDirection = 0; state(name); return true;
|
hero.runDirection = 0; state(name); return true;
|
||||||
}
|
}
|
||||||
|
|
@ -456,7 +456,7 @@
|
||||||
function useShopMove() {
|
function useShopMove() {
|
||||||
const id = window.Meta?.equippedMove?.(), m = SHOP_MOVES[id];
|
const id = window.Meta?.equippedMove?.(), m = SHOP_MOVES[id];
|
||||||
if (!m || !canAct() || hero.action || hero.doublePose > 0 || (hero.shopCd || 0) > 0) return false;
|
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 (m.instant) {
|
||||||
if (id === 'perisai') { hero.invuln = Math.max(hero.invuln, 1.6); hero.shield = 1.6; }
|
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' }); }
|
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' }); }
|
||||||
|
|
@ -533,12 +533,17 @@
|
||||||
function drawFlash(actor, img, r, dx, dy) {
|
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);
|
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';
|
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 ----
|
// ---- 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) {
|
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) {
|
if (hero.parry > 0) {
|
||||||
hero.parry = 0; hero.invuln = Math.max(hero.invuln, .35); announce('PARRY!', .6); sound('parry'); window.Meta?.shout?.('Parry!');
|
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 });
|
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 });
|
||||||
|
|
@ -584,7 +589,7 @@
|
||||||
systemAnnouncer?.clear();
|
systemAnnouncer?.clear();
|
||||||
stopUltimateVoice();
|
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 } });
|
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 });
|
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();}
|
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='';
|
fenrCutin=null;hudIdentity='';
|
||||||
|
|
@ -1830,11 +1835,11 @@
|
||||||
if(fenrCutin) {fenrCutin.t+=dt;if(fenrCutin.t>.78)fenrCutin=null;}
|
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');}}
|
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);
|
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); }
|
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;
|
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.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.grounded && !hero.action && hero.hurtTime <= 0 && hero.hp > 0 && hero.guardBreak <= 0 && hero.doublePose <= 0;
|
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;
|
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 (!hero.action && hero.hurtTime <= 0 && hero.hp > 0) {
|
||||||
if (dir && hero.doublePose <= 0) hero.facing = dir;
|
if (dir && hero.doublePose <= 0) hero.facing = dir;
|
||||||
|
|
@ -2757,11 +2762,12 @@
|
||||||
}
|
}
|
||||||
function draw() {
|
function draw() {
|
||||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); ctx.imageSmoothingEnabled = false; ctx.fillStyle = '#9ed2dc'; ctx.fillRect(0, 0, viewW, viewH);
|
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
|
// 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.
|
// 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.
|
// 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) {
|
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 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);
|
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);
|
||||||
|
|
@ -2819,7 +2825,7 @@
|
||||||
// SIAP berdenyut; selama mode bar menjadi waktu mode yang menyusut. DOM hanya ditulis saat tampilannya berubah.
|
// 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 = {};
|
const ULT_COLOR = { arco: '#67e4dd', fenr: '#e0a060', browo: '#d9b44a' }, ultMeters = {};
|
||||||
function ultMax(side) {
|
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;
|
const id = opponentCharacter;
|
||||||
return (id === 'fenr' ? F?.balance?.cooldowns?.ultimate : KITS[id]?.balance?.cooldowns?.ultimate) || BALANCE.ultimate.cooldown;
|
return (id === 'fenr' ? F?.balance?.cooldowns?.ultimate : KITS[id]?.balance?.cooldowns?.ultimate) || BALANCE.ultimate.cooldown;
|
||||||
}
|
}
|
||||||
|
|
@ -2921,7 +2927,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('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);
|
card.classList.toggle('recharged',(rechargePulse[name]||0)>0);
|
||||||
const wipe = card.querySelector('.cooldown-wipe');
|
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.
|
// 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;
|
const cutin=$('#ultimate-cutin'),live=g=>g&&g.t<SQUAD.cutinDuration?g:null;
|
||||||
|
|
@ -2933,7 +2939,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);}
|
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;
|
const visible = ct < SQUAD.cutinDuration;
|
||||||
cutin.classList.toggle('visible', visible); cutin.setAttribute('aria-hidden', String(!visible));
|
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.transform = `translateX(${slide}%) rotate(-3deg)`;
|
||||||
cutin.style.opacity = visible ? '1' : '0';
|
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));
|
document.querySelectorAll('.combo-dots i').forEach((dot, i) => dot.classList.toggle('lit', hero.action?.type === 'attack' && hero.action.index > i));
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<html lang="id">
|
<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">
|
<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="manifest" href="manifest.webmanifest"><link rel="icon" type="image/png" sizes="192x192" href="assets/pwa/icon-192.png"><link rel="apple-touch-icon" href="assets/pwa/apple-touch-icon.png"><link rel="stylesheet" href="touchlayout.css"><script src="pwa.js"></script><script src="touchlayout.js" defer></script>
|
<link rel="manifest" href="manifest.webmanifest"><link rel="icon" type="image/png" sizes="192x192" href="assets/pwa/icon-192.png"><link rel="apple-touch-icon" href="assets/pwa/apple-touch-icon.png"><link rel="stylesheet" href="touchlayout.css"><script src="pwa.js"></script><script src="touchlayout.js" defer></script>
|
||||||
<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="input.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="input.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">
|
<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>
|
<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">
|
<header class="combat-header">
|
||||||
|
|
@ -27,6 +27,6 @@
|
||||||
<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>
|
<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>
|
||||||
<!-- Statistik pertandingan (RENCANA 5.5): gaya dan logika di berkas sendiri; MatchStats dipanggil lewat window.MatchStats?. dari game.js, meta.js, vs.js -->
|
<!-- Statistik pertandingan (RENCANA 5.5): gaya dan logika di berkas sendiri; MatchStats dipanggil lewat window.MatchStats?. dari game.js, meta.js, vs.js -->
|
||||||
<link rel="stylesheet" href="stats.css"><script src="stats.js"></script>
|
<link rel="stylesheet" href="stats.css"><script src="stats.js"></script>
|
||||||
</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="input.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="input.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
|
|
@ -141,7 +141,7 @@
|
||||||
// Setelah html dipasang: hitung naik angka koin dan fokuskan wadah layar hasil (kecuali layar sentuh).
|
// Setelah html dipasang: hitung naik angka koin dan fokuskan wadah layar hasil (kecuali layar sentuh).
|
||||||
function mount(root) {
|
function mount(root) {
|
||||||
const rs = root.querySelector('.rs'); if (!rs) return;
|
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]');
|
const n = rs.querySelector('[data-count]');
|
||||||
if (n && !calm) {
|
if (n && !calm) {
|
||||||
const to = Number(n.dataset.count) || 0, t0 = performance.now() + 700, dur = 900;
|
const to = Number(n.dataset.count) || 0, t0 = performance.now() + 700, dur = 900;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue