284 lines
22 KiB
JavaScript
284 lines
22 KiB
JavaScript
/* Latihan lanjutan (RENCANA 6.5, training.js + training.css): panel "DUMMY" di arena latihan (bukan di tutorial). Tombol DUMMY di bilah kecil bawah jam (di samping Pengaturan
|
|
dan Jeda) membuka panel kecil yang bisa ditutup, dipindah ke sisi lain, dan ingatannya (buka/tutup, sisi) tersimpan di localStorage['konoha.training.v1'].
|
|
Isi panel:
|
|
Perilaku dummy: Diam (bawaan), Blok terus, Lompat terus, Serang acak, Ikuti pemain, CPU penuh (= saklar "Lawan aktif" di Pengaturan, tetap sinkron), dan Reset posisi.
|
|
Alat bantu: Tampilkan hitbox (setHitboxes yang sudah ada), Tampilkan frame data (startup, aktif, recovery, total dalam ms, jangkauan dalam px, damage dasar, dari data
|
|
jurus serangan terakhir pemain), Darah dummy tak terbatas (dummy tidak pernah K.O.; bar terisi lagi 1 dtk setelah kombo berhenti), dan penghitung kombo/damage.
|
|
Rekam dan putar ulang: Rekam menyimpan tombol yang kamu tekan dan dilepas beserta waktu simulasinya, paling lama 10 dtk, mulai pada tombol pertama (arena direset lebih
|
|
dulu dan dummy menunggu); Putar mereset arena yang sama lalu menekan ulang urutan itu lewat __game.press/release/input terjadwal oleh waktu simulasi. Benih random() mesin
|
|
disetel sama saat rekam dan putar dimulai supaya dummy "Serang acak" bertindak sama. Hasil putar dibandingkan dengan hasil rekaman (damage dan kombo terbaik) dan ditulis
|
|
SAMA atau BERBEDA; tidak dijamin sama persis (mis. tombol yang ditekan lewat klik kartu skill di HUD tidak terekam).
|
|
Kait di game.js (semuanya `window.Training?.` satu baris): init (startMatch), pre (awal tiap langkah simulasi: jadwal putar, darah tak terbatas, aksi terakhir), drive (cabang
|
|
dummy tanpa AI di updateFenrAI), blocks (hitDummy), rec (press, release, forceCommand, clearInput), reset (resetCombat). Akses sempit ke mesin lewat __game.dummyCtl,
|
|
__game.kitMove, __game.rng. Semua tidak aktif di versus, saat Tutorial.active, dan di menu. Bila ada galat, fitur berhenti sendiri dan game berjalan seperti biasa. */
|
|
(() => {
|
|
'use strict';
|
|
const MAXREC = 10; // detik rekaman paling lama
|
|
const SEED = 20261005; // benih random() mesin saat rekam dan putar dimulai
|
|
const KEY = 'konoha.training.v1';
|
|
const $ = s => document.querySelector(s);
|
|
const G = () => window.__game;
|
|
const esc = t => String(t).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
const num = (n, d = 1) => Number(n).toFixed(d).replace('.', ',');
|
|
const ms = s => Math.round(s * 1000) + ' ms';
|
|
const BEH = [['diam', 'Diam'], ['blok', 'Blok terus'], ['lompat', 'Lompat terus'], ['serang', 'Serang acak'], ['ikuti', 'Ikuti pemain'], ['cpu', 'CPU penuh']];
|
|
// Gerakan khusus pemain: nama, tombol, dan jangkauan (disalin dari specialHit di game.js; hanya untuk tampilan).
|
|
const SPECIAL = {
|
|
uppercut: { name: 'UPPERCUT', sub: 'bawah, bawah + SPASI', range: '150 px di depan, tinggi ±150 px' },
|
|
lunge: { name: 'TINJU MAJU', sub: 'bawah, maju + SPASI', range: '175 px di depan, sambil meluncur' },
|
|
throw: { name: 'LEMPAR', sub: 'tombol J', range: '125 px di sekitar, menembus blok' },
|
|
seruduk: { name: 'SERUDUK KILAT', sub: 'jurus toko (K)', range: '190 px di depan, sambil meluncur' },
|
|
hentak: { name: 'HENTAKAN BUMI', sub: 'jurus toko (K)', range: '230 px di sekitar' }
|
|
};
|
|
const S = { on: false, fail: false, beh: 'diam', inf: false, fd: false, open: false, side: 'right', next: 0, dj: false, time: 0, last: null, fdv: null, fdn: 0, cmb: { live: false, hits: 0, dmg: 0, last: null }, busy: false, ui: null, key: '' };
|
|
const R = { state: 'idle', ev: [], down: new Set(), t0: 0, p0: null, i: 0, dur: 0, beh: 'diam', inf: false, ref: null, res: null, msg: '', tone: '', keep: null };
|
|
try { const v = JSON.parse(localStorage.getItem(KEY) || 'null'); if (v) { S.open = !!v.open; S.side = v.side === 'left' ? 'left' : 'right'; } } catch (_) { /* tanpa simpanan */ }
|
|
const save = () => { try { localStorage.setItem(KEY, JSON.stringify({ open: S.open, side: S.side })); } catch (_) { /* abaikan */ } };
|
|
const usable = () => S.on && !S.fail && !window.Tutorial?.active && $('#arena')?.dataset.mode === 'training' && !document.body.classList.contains('in-menu');
|
|
const guard = fn => (...a) => { try { return fn(...a); } catch (e) { console.error(e); S.fail = true; return false; } };
|
|
|
|
// ---- perilaku dummy: dipanggil updateFenrAI hanya bila AI mati (Diam = tidak ada yang dilakukan) ----
|
|
function loco(d, dt) {
|
|
const v = Math.abs(d.vx), next = v > 5 ? (v > 300 ? 'run' : 'walk') : 'idle';
|
|
if (next !== d.state) { d.state = next; d.stateTime = 0; }
|
|
d.walkPhase = (d.walkPhase || 0) + v * dt / 150;
|
|
}
|
|
function driveAttack(g, d, h, c, t, dt) {
|
|
if (!S.next) S.next = t + .8 + c.rand() * 1.2;
|
|
if (t < S.next) return false;
|
|
const dist = Math.abs(h.x - d.x), pool = [];
|
|
for (const n of ['attack', 'skill2', 'skill1']) {
|
|
if (n !== 'attack' && d.cooldowns[n] > 0) continue;
|
|
const m = c.move(n); if (m.guard) continue;
|
|
if (m.projectile ? dist >= 220 : dist <= c.reach(n) - 8) pool.push([n, n === 'attack' ? 3 : 1]);
|
|
}
|
|
if (!pool.length) { d.vx = Math.sign(h.x - d.x) * 240; loco(d, dt); return true; } // terlalu jauh dan tak ada tembakan siap: terus mendekat (S.next tetap, dinilai ulang tiap langkah)
|
|
let r = c.rand() * pool.reduce((a, p) => a + p[1], 0), pick = pool[0][0];
|
|
for (const [n, w] of pool) { if ((r -= w) < 0) { pick = n; break; } }
|
|
S.next = t + (c.act(pick, 1, true) ? .9 + c.rand() * 1.6 : .25); // serangan tunggal: tanpa rangkaian kombo otomatis
|
|
return true;
|
|
}
|
|
const drive = guard(dt => {
|
|
if (!S.on || S.beh === 'diam' || S.beh === 'cpu' || R.state === 'armed' || !usable()) return false;
|
|
const g = G(), d = g.dummy, h = g.hero, c = g.dummyCtl, t = S.time; if (!c) return false;
|
|
const grounded = d.y >= g.config.groundY - 1 && d.vy >= 0;
|
|
if (!grounded) { // di udara: lompat ganda untuk "Lompat terus" bergantian, selain itu biarkan fisika berjalan
|
|
if (S.beh === 'lompat' && S.dj && d.vy > 0 && d.jumps < 2) { d.jumps = 2; d.vy = -g.config.doubleJumpSpeed; }
|
|
return true;
|
|
}
|
|
d.facing = h.x >= d.x ? 1 : -1;
|
|
if (S.beh === 'blok') { d.vx = 0; if (d.state !== 'crouch') { d.state = 'crouch'; d.stateTime = 0; } return true; }
|
|
if (S.beh === 'lompat') {
|
|
d.vx = 0; if (!S.next) S.next = t + .4;
|
|
if (t < S.next) return false;
|
|
c.jump(); S.dj = !S.dj; S.next = t + .75; return true;
|
|
}
|
|
if (S.beh === 'ikuti') {
|
|
const dist = Math.abs(h.x - d.x), dir = Math.sign(h.x - d.x), want = 200; // menjaga jarak sekitar 200 px: mendekat bila jauh, mundur bila terlalu dekat
|
|
d.vx = dist > want + 40 ? dir * (dist > 360 ? 420 : 240) : dist < want - 50 ? -dir * 240 : 0;
|
|
loco(d, dt); return true;
|
|
}
|
|
if (S.beh === 'serang') return driveAttack(g, d, h, c, t, dt);
|
|
return false;
|
|
});
|
|
const blocks = () => S.on && !S.fail && S.beh === 'blok' && !window.Tutorial?.active;
|
|
|
|
// ---- data frame serangan terakhir pemain ----
|
|
const cardName = slot => ($(`.skill-card[data-action="${slot}"] .skill-name`)?.textContent || '').trim();
|
|
function rangeText(a, m) {
|
|
if (a.type === 'ultimate') return 'sinematik, tanpa jangkauan tetap';
|
|
if (a.special) return SPECIAL[a.special]?.range || '—';
|
|
if (!m) return '—';
|
|
if (m.tree && window.Browo?.tree) return `pohon ${window.Browo.tree.ahead} px di depan, radius ${window.Browo.tree.radius} px`;
|
|
if (m.dance && window.Browo?.dance) return `lingkaran radius ${window.Browo.dance.radius} px di sekitar`;
|
|
if (m.area) return `lingkaran radius ${m.radius} px, pusat ${m.ahead || 0} px di depan, tinggi ±${m.vy || 150} px`;
|
|
if (m.kind === 'lob') return 'lemparan melengkung, kolam di titik jatuh';
|
|
if (m.projectile) return `terbang sampai ${m.range} px`;
|
|
if (m.dash) return `${Math.round((m.reach || 120) + 27)} px di depan, meluncur ±${Math.round(m.dash * m.duration * .4)} px`;
|
|
if (a.type === 'attack') return `${Math.round(m.reach + 27)} px di depan`;
|
|
return '—';
|
|
}
|
|
// Rumus waktu kena sama dengan step() di game.js: duration x (hitAt, atau .53 untuk skill 2 dan .5 selain itu).
|
|
function describe(a) {
|
|
const g = G(), type = a.type, idx = a.index || 1;
|
|
const m = type === 'attack' && !a.special ? (g.kitMove?.('attack', idx) || a) : a;
|
|
const dur = a.duration || m.duration || 0, su = dur * (a.hitAt || (type === 'skill2' ? .53 : .5));
|
|
let name, sub;
|
|
if (a.special) ({ name, sub } = SPECIAL[a.special] || { name: String(a.special).toUpperCase(), sub: '' });
|
|
else if (type === 'attack') { name = cardName('attack') || 'SERANGAN DASAR'; sub = `pukulan ke-${idx}, tombol SPASI`; }
|
|
else { name = cardName(type) || type.toUpperCase(); sub = type === 'skill1' ? 'skill 1, tombol I' : type === 'skill2' ? 'skill 2, tombol O' : 'ultimate, tombol P'; }
|
|
const rows = [];
|
|
if (type === 'ultimate') rows.push(['Pembuka', ms(dur)], ['Sesudahnya', 'rangkaian sinematik; tak ada data frame tetap']);
|
|
else {
|
|
rows.push(['Startup', ms(su)]);
|
|
if (a.dance && window.Browo?.dance) { const D = window.Browo.dance; rows.push(['Aktif', `${D.pulses} denyut, tiap ${ms(D.gap)}`]); }
|
|
else rows.push(['Aktif', 'sesaat (±8 ms)']);
|
|
rows.push(['Recovery', ms(Math.max(0, dur - su))], ['Total', ms(dur)]);
|
|
if (m.projectile && m.range && m.speed) rows.push(['Terbang', `±${ms(m.range / m.speed)} sampai ujung`]);
|
|
}
|
|
rows.push(['Jangkauan', rangeText(a, m)]);
|
|
if (m.damage) rows.push(['Damage dasar', String(m.damage)]);
|
|
return { name, sub, rows };
|
|
}
|
|
const fdHtml = f => (f
|
|
? `<h3>SERANGAN TERAKHIR</h3><p class="dm-mv"><b>${esc(f.name)}</b><small>${esc(f.sub)}</small></p><dl>${f.rows.map(([k, v]) => `<dt>${esc(k)}</dt><dd>${esc(v)}</dd>`).join('')}</dl>`
|
|
: '<h3>FRAME DATA</h3><p class="dm-note">Belum ada serangan. Pukul atau pakai skill dulu.</p>');
|
|
|
|
// ---- rekam dan putar ulang ----
|
|
const stats = () => { const s = G().stats?.() || {}; return { dmg: s.totalDamage || 0, best: s.bestCombo || 0 }; };
|
|
const say = (msg, tone = '') => { R.msg = msg; R.tone = tone; };
|
|
function internalReset() { S.busy = true; try { G().reset(); } finally { S.busy = false; } S.next = 0; S.dj = false; }
|
|
function startRec() {
|
|
if (!usable()) return;
|
|
stopAll(); internalReset();
|
|
R.keep = { ev: R.ev, dur: R.dur, ref: R.ref, beh: R.beh, inf: R.inf, res: R.res }; // rekaman lama tetap ada bila Rekam dibatalkan sebelum tombol pertama
|
|
R.state = 'armed'; R.ev = []; R.down.clear(); R.res = null; R.ref = null; R.dur = 0; R.beh = S.beh; R.inf = S.inf;
|
|
say('Siap. Tekan tombol apa saja untuk mulai merekam (maksimal 10 dtk).', 'hint');
|
|
}
|
|
function stopRec(why) {
|
|
if (R.state === 'armed') { R.state = 'idle'; if (R.keep) Object.assign(R, R.keep); R.keep = null; say(R.ev.length ? 'Perekaman dibatalkan; rekaman sebelumnya masih ada.' : 'Perekaman dibatalkan.', ''); return; }
|
|
if (R.state !== 'rec') return;
|
|
const rel = S.time - R.t0; for (const k of R.down) R.ev.push({ t: rel, k, d: 0 }); R.down.clear();
|
|
R.dur = rel; R.ref = stats(); R.state = 'idle';
|
|
say(R.ev.length ? (why === 'batas' ? 'Batas 10 dtk tercapai. ' : '') + 'Rekaman siap. Tekan PUTAR.' : 'Tidak ada input terekam.', R.ev.length ? 'good' : 'bad');
|
|
}
|
|
function startPlay() {
|
|
if (!usable() || !R.ev.length) return;
|
|
stopAll(); S.beh = R.beh; S.inf = R.inf; syncAi();
|
|
internalReset(); G().rng?.set(SEED);
|
|
R.state = 'play'; R.p0 = null; R.i = 0; R.res = null; say('Memutar ulang... jangan menekan tombol.', 'hint');
|
|
}
|
|
function stopAll() {
|
|
if (R.state === 'rec' || R.state === 'armed') stopRec(); else if (R.state === 'play') { R.state = 'idle'; say('Putar ulang dihentikan.', ''); }
|
|
}
|
|
const send = e => { const g = G(); if (e.k.startsWith('cmd:')) g.input?.(e.k, true); else if (e.d) g.press(e.k); else g.release(e.k); };
|
|
function playStep(time) {
|
|
if (R.p0 === null) R.p0 = time;
|
|
const rel = time - R.p0 + 1e-6;
|
|
while (R.i < R.ev.length && R.ev[R.i].t <= rel) send(R.ev[R.i++]);
|
|
if (rel < R.dur) return;
|
|
const now = stats(), ref = R.ref || now, same = now.dmg === ref.dmg && now.best === ref.best;
|
|
R.state = 'idle'; R.res = { same, now, ref };
|
|
say(same ? `Putar selesai: SAMA seperti rekaman (damage ${now.dmg}, kombo terbaik ${now.best}).`
|
|
: `Putar selesai: BERBEDA dari rekaman. Damage ${ref.dmg} jadi ${now.dmg}, kombo terbaik ${ref.best} jadi ${now.best}. Waktu atau jarak sedikit bergeser; coba rekam ulang.`, same ? 'good' : 'bad');
|
|
}
|
|
// Kait press/release/forceCommand/clearInput: key = tombol (huruf kecil), 'cmd:<id>' untuk gerakan perintah sentuh, atau null = semua tombol dilepas.
|
|
const rec = guard((key, down, time) => {
|
|
if (!S.on || (R.state !== 'armed' && R.state !== 'rec')) return;
|
|
if (R.state === 'armed') {
|
|
if (key === null || !down) return;
|
|
R.state = 'rec'; R.keep = null; R.t0 = time; R.ev = []; R.down.clear(); R.beh = S.beh; R.inf = S.inf; S.next = 0; S.dj = false; G().rng?.set(SEED); say('', '');
|
|
}
|
|
const rel = time - R.t0;
|
|
if (key === null) { for (const k of R.down) R.ev.push({ t: rel, k, d: 0 }); R.down.clear(); return; }
|
|
if (key.startsWith('cmd:')) { if (down) R.ev.push({ t: rel, k: key, d: 1 }); return; }
|
|
if (down) { R.down.add(key); R.ev.push({ t: rel, k: key, d: 1 }); } else if (R.down.delete(key)) R.ev.push({ t: rel, k: key, d: 0 });
|
|
});
|
|
|
|
// ---- kait mesin ----
|
|
const init = isTraining => {
|
|
S.on = !!isTraining; S.fail = false; S.beh = 'diam'; S.inf = false; S.fd = false; S.next = 0; S.dj = false; S.last = null; S.fdv = null; S.cmb = { live: false, hits: 0, dmg: 0, last: null };
|
|
R.state = 'idle'; R.keep = null; R.ev = []; R.down.clear(); R.res = null; R.ref = null; R.dur = 0; say('', ''); S.key = '';
|
|
};
|
|
const reset = () => {
|
|
if (S.busy || !S.on) return;
|
|
if (R.state === 'rec' || R.state === 'armed') stopRec(); else if (R.state === 'play') { R.state = 'idle'; say('Putar ulang dihentikan (arena direset).', ''); }
|
|
S.next = 0; S.last = null;
|
|
};
|
|
const pre = guard(time => {
|
|
if (!S.on || S.fail) return;
|
|
S.time = time;
|
|
if (window.Tutorial?.active) return;
|
|
const g = G(), h = g.hero, d = g.dummy;
|
|
if (h.action !== S.last) { S.last = h.action; if (h.action) { try { S.fdv = describe(h.action); S.fdn++; } catch (e) { S.fdv = null; console.error(e); } } }
|
|
if (S.inf && !d.ko) { const max = g.balance.dummyMax; if (d.damage > max * .8) d.damage = max * .8; if (d.damage > 0 && time - (d.lastHurt ?? -99) > 1) d.damage = 0; }
|
|
if (R.state === 'rec') { if (time - R.t0 >= MAXREC) stopRec('batas'); } else if (R.state === 'play') playStep(time);
|
|
});
|
|
|
|
// ---- tampilan ----
|
|
const aiOn = () => !!$('#ai-toggle')?.checked;
|
|
const hbOn = () => !!$('#hitbox-toggle')?.checked;
|
|
function syncAi() { // 'CPU penuh' memakai saklar Lawan aktif yang sudah ada, supaya Pengaturan dan panel tidak saling bertentangan
|
|
const t = $('#ai-toggle'), want = S.beh === 'cpu';
|
|
if (t && t.checked !== want) { t.checked = want; t.dispatchEvent(new Event('change')); }
|
|
}
|
|
function setBeh(b) { if (R.state !== 'idle') return; S.beh = b; S.next = 0; S.dj = false; syncAi(); }
|
|
function resetPos() {
|
|
if (R.state !== 'idle') return;
|
|
const g = G(), h = g.hero, d = g.dummy; h.x = 350; h.vx = 0; h.facing = 1; d.x = 915; d.vx = 0; d.facing = -1; S.next = 0;
|
|
}
|
|
const arena = () => $('#arena')?.focus({ preventScroll: true });
|
|
function act(a, v) {
|
|
const g = G(); if (!g) return;
|
|
if (a === 'toggle') { S.open = !S.open; save(); }
|
|
else if (a === 'close') { S.open = false; save(); }
|
|
else if (a === 'side') { S.side = S.side === 'left' ? 'right' : 'left'; save(); }
|
|
else if (a === 'beh') setBeh(v);
|
|
else if (a === 'pos') resetPos();
|
|
else if (a === 'hb') g.setHitboxes?.(!hbOn());
|
|
else if (a === 'fd') S.fd = !S.fd;
|
|
else if (a === 'inf') { if (R.state === 'idle') S.inf = !S.inf; }
|
|
else if (a === 'rec') { if (R.state === 'armed' || R.state === 'rec') stopRec(); else startRec(); }
|
|
else if (a === 'play') { if (R.state === 'play') stopAll(); else startPlay(); }
|
|
else if (a === 'stop') stopAll();
|
|
S.key = ''; refresh(); arena();
|
|
}
|
|
function build() {
|
|
if (S.ui) return;
|
|
const nav = $('.match-actions'), shell = $('#game-shell'); if (!nav || !shell) return;
|
|
const btn = document.createElement('button'); btn.id = 'dm-btn'; btn.type = 'button'; btn.className = 'training-only'; btn.textContent = 'DUMMY'; btn.title = 'Panel dummy latihan';
|
|
btn.setAttribute('aria-controls', 'dm-panel'); btn.setAttribute('aria-expanded', 'false'); btn.dataset.a = 'toggle'; btn.hidden = true;
|
|
const p = document.createElement('section'); p.id = 'dm-panel'; p.className = 'training-only'; p.hidden = true; p.dataset.side = S.side; p.setAttribute('role', 'region'); p.setAttribute('aria-label', 'Panel dummy latihan');
|
|
const sw = (a, t) => `<button type="button" class="dm-sw" role="switch" aria-checked="false" data-a="${a}"><i aria-hidden="true"></i><span>${t}</span></button>`;
|
|
p.innerHTML = `<header class="dm-head"><b>DUMMY LATIHAN</b><span><button type="button" class="dm-mini" data-a="side" title="Pindahkan panel ke sisi lain" aria-label="Pindahkan panel ke sisi lain">⇄</button><button type="button" class="dm-mini" data-a="close" title="Tutup" aria-label="Tutup panel dummy">✕</button></span></header>`
|
|
+ `<div class="dm-sec"><h3>PERILAKU</h3><div class="dm-grid" role="radiogroup" aria-label="Perilaku dummy">${BEH.map(([id, t]) => `<button type="button" class="dm-b" role="radio" aria-checked="false" data-a="beh" data-v="${id}">${t}</button>`).join('')}</div><button type="button" class="dm-b dm-wide" data-a="pos">Reset posisi</button></div>`
|
|
+ `<div class="dm-sec"><h3>ALAT BANTU</h3>${sw('hb', 'Tampilkan hitbox')}${sw('fd', 'Tampilkan frame data')}${sw('inf', 'Darah dummy tak terbatas')}<p class="dm-count" id="dm-count"></p></div>`
|
|
+ `<div class="dm-sec dm-fd" id="dm-fd" hidden></div>`
|
|
+ `<div class="dm-sec"><h3>REKAM & PUTAR ULANG</h3><div class="dm-rec"><button type="button" class="dm-b" data-a="rec">● Rekam</button><button type="button" class="dm-b" data-a="play">▶ Putar</button><button type="button" class="dm-b" data-a="stop">■ Henti</button></div><p class="dm-note" id="dm-note" aria-live="polite"></p></div>`;
|
|
// Tombol tidak boleh merebut fokus dari arena: tombol yang fokus menelan SPASI (serangan) sebagai klik.
|
|
for (const el of [btn, p]) { el.addEventListener('mousedown', e => { if (e.target.closest('button')) e.preventDefault(); }); el.addEventListener('click', e => { const b = e.target.closest('[data-a]'); if (b && !b.disabled) act(b.dataset.a, b.dataset.v); }); }
|
|
nav.append(btn); shell.append(p);
|
|
S.ui = { btn, p, fd: p.querySelector('#dm-fd'), note: p.querySelector('#dm-note'), count: p.querySelector('#dm-count') };
|
|
}
|
|
function refresh() {
|
|
if (!S.ui) return;
|
|
const U = S.ui, show = usable();
|
|
if (!show) { if (!U.btn.hidden) { U.btn.hidden = true; U.p.hidden = true; } S.key = ''; return; }
|
|
const g = G(), ai = aiOn();
|
|
if (ai && S.beh !== 'cpu') S.beh = 'cpu'; else if (!ai && S.beh === 'cpu') S.beh = 'diam'; // Pengaturan mengubah Lawan aktif
|
|
const busy = R.state !== 'idle', st = g.stats?.() || { comboHits: 0, bestCombo: 0, totalDamage: 0, comboDamage: 0 };
|
|
if (st.comboHits > 0) { S.cmb.live = true; S.cmb.hits = st.comboHits; S.cmb.dmg = st.comboDamage || 0; } else if (S.cmb.live) { S.cmb.live = false; S.cmb.last = { hits: S.cmb.hits, dmg: S.cmb.dmg }; }
|
|
const relNow = R.state === 'rec' ? S.time - R.t0 : R.state === 'play' && R.p0 !== null ? S.time - R.p0 : 0;
|
|
const hb = hbOn();
|
|
const key = JSON.stringify([S.open, S.side, S.beh, S.inf, S.fd, hb, R.state, R.ev.length, Math.floor(relNow * 5), R.dur, R.msg, S.cmb.live, S.cmb.hits, S.cmb.dmg, S.cmb.last, st.bestCombo, st.totalDamage, S.fd ? S.fdn : 0]);
|
|
if (key === S.key) return; S.key = key;
|
|
U.btn.hidden = false; U.btn.setAttribute('aria-expanded', String(S.open)); U.btn.classList.toggle('on', S.open);
|
|
U.p.hidden = !S.open; U.p.dataset.side = S.side; if (!S.open) return;
|
|
for (const b of U.p.querySelectorAll('[data-a="beh"]')) { const on = b.dataset.v === S.beh; b.setAttribute('aria-checked', String(on)); b.disabled = busy; }
|
|
U.p.querySelector('[data-a="pos"]').disabled = busy;
|
|
const sw = { hb, fd: S.fd, inf: S.inf };
|
|
for (const b of U.p.querySelectorAll('.dm-sw')) { b.setAttribute('aria-checked', String(!!sw[b.dataset.a])); b.disabled = b.dataset.a === 'inf' && busy; }
|
|
const cb = S.cmb.live ? `<b>${S.cmb.hits}</b> hit · <b>${S.cmb.dmg}</b> dmg` : S.cmb.last ? `terakhir <b>${S.cmb.last.hits}</b> hit · <b>${S.cmb.last.dmg}</b> dmg` : 'belum ada';
|
|
U.count.innerHTML = `<span>KOMBO ${cb}</span><span>terbaik <b>${st.bestCombo}</b> · total <b>${st.totalDamage}</b> dmg</span>`;
|
|
U.fd.hidden = !S.fd; if (S.fd) U.fd.innerHTML = fdHtml(S.fdv);
|
|
const bs = { rec: U.p.querySelector('[data-a="rec"]'), play: U.p.querySelector('[data-a="play"]'), stop: U.p.querySelector('[data-a="stop"]') };
|
|
bs.rec.innerHTML = R.state === 'armed' || R.state === 'rec' ? '■ Selesai' : '● Rekam'; bs.rec.disabled = R.state === 'play'; bs.rec.classList.toggle('on', R.state === 'armed' || R.state === 'rec');
|
|
bs.play.innerHTML = R.state === 'play' ? '▶ Memutar...' : '▶ Putar'; bs.play.disabled = R.state === 'rec' || R.state === 'armed' || !R.ev.length; bs.play.classList.toggle('on', R.state === 'play');
|
|
bs.stop.disabled = R.state === 'idle';
|
|
let msg = R.msg, tone = R.tone;
|
|
if (R.state === 'rec') { msg = `Merekam ${num(relNow)} / ${MAXREC} dtk · ${R.ev.length} input`; tone = 'hint'; }
|
|
else if (R.state === 'play') { msg = `Memutar ${num(Math.min(relNow, R.dur))} / ${num(R.dur)} dtk. Jangan menekan tombol.`; tone = 'hint'; }
|
|
else if (R.state === 'idle' && !msg) msg = R.ev.length ? `Rekaman: ${num(R.dur)} dtk · ${R.ev.length} input. Tekan PUTAR.` : 'Belum ada rekaman. Tekan REKAM, lalu mainkan urutanmu.';
|
|
U.note.className = 'dm-note' + (tone ? ' ' + tone : ''); U.note.textContent = msg;
|
|
}
|
|
|
|
function ready() { build(); refresh(); setInterval(refresh, 120); document.getElementById('hitbox-toggle')?.addEventListener('change', () => { S.key = ''; }); }
|
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', ready); else ready();
|
|
|
|
window.Training = {
|
|
init, pre, drive, blocks, rec, reset,
|
|
open: () => { S.open = true; save(); S.key = ''; refresh(); }, close: () => { S.open = false; save(); S.key = ''; refresh(); },
|
|
state: () => ({ on: S.on, fail: S.fail, behavior: S.beh, infinite: S.inf, frameData: S.fd, open: S.open, side: S.side, rec: { state: R.state, events: R.ev.length, duration: +R.dur.toFixed(2), reference: R.ref, result: R.res }, lastMove: S.fdv })
|
|
};
|
|
})();
|