/* Layar VS (RENCANA 2.2), tampilan layar hasil (RENCANA 2.3) dan pemilih kutipan (RENCANA 2.6). Gaya: vs.css, data kutipan: content/quotes.js.
- window.VS.show(info, onDone): overlay sekitar 2,2 detik sebelum ronde 1. onDone dipanggil tepat satu kali (waktu habis atau dilewati
dengan klik/Spasi/Enter/Esc); setelah itu game memulai urutan cue ronde seperti biasa (game.js startMatch -> emitRoundCue).
- window.ResultView.view(hasil, ctx) -> {title, eyebrow, html}; ResultView.mount(root) menghidupkan hitungan koin dan memberi fokus ke wadah layar.
- Dialog pasangan (RENCANA 3.1, content/dialogs.js): bila pasangan punya dialog, kutipan pembuka diganti dua balon ucapan (kiri = pemain, kanan = lawan)
dan layar tampil lebih lama (DURATION + DIALOG_EXTRA) supaya terbaca. Pasangan tersembunyi (DIALOGS.rahasia, RENCANA 3.4) menampilkan dialog rahasia
berlabel DIALOG RAHASIA dan memanggil Meta.pairEgg (koin dan toast sekali per pasangan, pencapaian REUNI).
Pengacak di sini kosmetik saja (bukan random() mesin), jadi tidak memengaruhi hasil pertandingan. */
(() => {
'use strict';
const esc = t => String(t ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
let seed = (Date.now() ^ Math.floor(performance.now() * 1000)) >>> 0;
const rnd = () => { seed = (seed + 0x6D2B79F5) >>> 0; let t = seed; t = Math.imul(t ^ t >>> 15, t | 1); t ^= t + Math.imul(t ^ t >>> 7, t | 61); return ((t ^ t >>> 14) >>> 0) / 4294967296; };
const LEVELS = { easy: 'MUDAH', medium: 'SEDANG', hard: 'SULIT', excellent: 'SANGAT SULIT' };
const DURATION = 2200, DIALOG_EXTRA = 1100, FADE = 260, MIN_SKIP = 380;
// ---- kutipan ----
const GENERIC = { intro: ['Siap bertarung!', 'Ayo mulai.', 'Tidak ada yang mundur hari ini.'], win: ['Pertandingan yang bagus!', 'Terima kasih sudah bertanding.'], lose: ['Kali ini belum beruntung.', 'Aku akan kembali.'] };
const lastQuote = {};
function quote(id, kind) {
const own = window.QUOTES?.[id]?.[kind], pool = own && own.length ? own : GENERIC[kind] || GENERIC.intro, key = id + ':' + kind;
let i = Math.floor(rnd() * pool.length);
if (pool.length > 1 && pool[i] === lastQuote[key]) i = (i + 1 + Math.floor(rnd() * (pool.length - 1))) % pool.length;
return (lastQuote[key] = pool[i]);
}
// Dialog pasangan: kunci berurutan alfabet, a = ucapan karakter pertama (pembuka), b = balasan. Hasil: {key, secret, l, r, reply} (l = sisi kiri/pemain, r = sisi kanan/lawan,
// reply = sisi yang membalas, muncul belakangan) atau null
// bila pasangan tidak punya dialog (mis. karakter yang sama) sehingga kutipan umum jadi cadangan. Dialog rahasia menggantikan dialog biasa.
const pairKey = (x, y) => (String(x) < String(y) ? x + '|' + y : y + '|' + x);
function dialog(idL, idR) {
if (!idL || !idR || idL === idR) return null;
const key = pairKey(idL, idR), all = window.DIALOGS, secret = all?.rahasia?.[key], d = secret || all?.[key];
if (!d || typeof d.a !== 'string' || typeof d.b !== 'string') return null;
const first = idL < idR;
return { key, secret: !!secret, l: first ? d.a : d.b, r: first ? d.b : d.a, reply: first ? 'r' : 'l' };
}
function fighter(id) {
const f = window.FIGHTERS?.[id], portrait = `assets/${id}/ui/portrait.webp`;
if (f) return { id, name: f.name, tag: f.tag || '', art: f.select || `assets/menu/${id}-select.webp`, portrait, color: f.color || '#e0bf85' };
if (id === 'browo') return { id, name: 'BRO WO', tag: 'THE PALM RANGER', art: 'assets/menu/browo-select.webp', portrait, color: '#c9b87a' };
return { id, name: String(id || '?').toUpperCase(), tag: '', art: '', portrait: id === 'arco' ? 'assets/ui/arco-avatar.webp' : portrait, color: '#e0bf85' };
}
const artOf = f => f.art || f.portrait;
const stageName = id => window.MatchRules?.stages?.[id]?.name || '';
// ---- layar VS ----
let cur = null;
// text = kutipan umum (diberi tanda kutip) atau, bila dlg ada, ucapan dialog pasangan dalam balon (ekor menunjuk ke papan nama).
function vsSide(cls, f, who, text, dlg) {
const talk = dlg ? `
${esc(text)}
` : `“${esc(text)}”
`;
return `
${who}${esc(f.name)}${esc(f.tag)}${talk}`;
}
function show(info = {}, onDone) {
cancel();
if (!document.body) return false;
const L = fighter(info.player), R = fighter(info.enemy), two = !!info.p2, st = stageName(info.stage), lv = LEVELS[info.level] || '';
const dlg = dialog(L.id, R.id), dur = DURATION + (dlg ? DIALOG_EXTRA : 0);
const kicker = info.label || (two ? 'DUA PEMAIN' : 'VERSUS KOMPUTER');
const el = document.createElement('div');
el.id = 'vs-screen'; el.setAttribute('role', 'dialog'); el.setAttribute('aria-label', `${L.name} melawan ${R.name}. Klik atau tekan Spasi untuk melewati.`);
const bg = window.MatchRules?.stages?.[info.stage]?.image;
el.innerHTML = ``
+ `${esc(kicker)}${st ? `${esc(st.toUpperCase())}` : ''}${lv && !two ? `${lv}` : ''}
`
+ (dlg?.secret ? `DIALOG RAHASIA
` : '')
+ vsSide('l', L, 'PEMAIN 1', dlg ? dlg.l : quote(L.id, 'intro'), dlg) + vsSide('r', R, two ? 'PEMAIN 2' : 'KOMPUTER', dlg ? dlg.r : quote(R.id, 'intro'), dlg)
+ `VS
KLIK ATAU SPASI UNTUK LEWATI
`;
document.body.append(el);
const c = cur = { el, onDone, t0: performance.now(), done: false, timers: [], dialog: dlg };
el.style.setProperty('--vs-run', (dur - FADE) + 'ms');
c.timers.push(setTimeout(() => finish('time'), dur - FADE), setTimeout(() => { try { window.__game?.menuSound?.('pick'); } catch (_) {} }, 520));
// Dialog rahasia ditemukan: koin dan toast sekali per pasangan (toast ditahan Meta sampai layar VS selesai).
if (dlg?.secret) { try { window.Meta?.pairEgg?.(dlg.key); } catch (e) { console.error(e); } }
el.addEventListener('pointerdown', e => { e.preventDefault(); skip(); });
window.addEventListener('keydown', onKey, true);
return true;
}
// Selama layar VS aktif semua tombol ditahan agar tidak bocor ke game (Esc tidak boleh membuka jeda di bawahnya).
function onKey(e) {
if (!cur || cur.done) return;
e.stopImmediatePropagation(); e.preventDefault();
if (e.repeat) return;
if (e.key === ' ' || e.key === 'Enter' || e.key === 'Escape') skip();
}
function release() { const c = cur; if (!c) return null; window.removeEventListener('keydown', onKey, true); for (const t of c.timers) clearTimeout(t); c.timers = []; return c; }
function finish(reason) {
const c = cur; if (!c || c.done) return false;
c.done = true; release(); c.el.classList.add('out'); c.reason = reason;
setTimeout(() => c.el.remove(), reason === 'time' ? FADE + 40 : 200);
try { c.onDone?.(reason); } catch (e) { console.error(e); }
return true;
}
function skip() { if (!cur || cur.done || performance.now() - cur.t0 < MIN_SKIP) return false; return finish('skip'); }
// Dipanggil saat pertandingan dibatalkan (kembali ke menu / pertandingan baru): tanpa onDone.
function cancel() { const c = release(); if (!c) return; c.done = true; c.el.remove(); cur = null; }
window.VS = { show, skip, cancel, quote, dialog, pairKey, fighter, duration: DURATION, durationDialog: DURATION + DIALOG_EXTRA, get active() { return !!cur && !cur.done; }, seed(n) { seed = n >>> 0; }, force() { if (cur) { cur.t0 = -1e9; return skip(); } return false; } };
// ---- layar hasil ----
const CONFETTI = ['#ffe08a', '#ffffff', '#8bd4c3', '#ff9a7a'];
function confetti(color, n) {
return Array.from({ length: n }, (_, i) => ``).join('');
}
function view(r, ctx = {}) {
const run = r.run || null, two = !!r.p2, playerWon = r.winner === 'player';
const wid = playerWon ? r.player : r.enemy, lid = playerWon ? r.enemy : r.player, Wf = fighter(wid), Lf = fighter(lid);
const tone = run ? (run.cleared ? 'win' : 'lose') : (two || playerWon ? 'win' : 'lose');
const q = r._q || (r._q = { win: quote(wid, 'win'), lose: quote(lid, 'lose') });
const lv = LEVELS[r.level] || '', st = window.MatchRules?.stages?.[r.stage], stName = st ? st.name : '';
let title, kicker, scoreHtml;
if (run) {
title = run.cleared ? 'TAMAT!' : 'BERAKHIR';
kicker = run.kind === 'arcade' ? (run.cleared ? 'SEMUA LAWAN DIKALAHKAN' : `KALAH DI LAWAN KE-${run.wins + 1} DARI ${run.total}`) : `SURVIVAL · REKOR ${run.best}`;
scoreHtml = `${run.wins}${run.kind === 'arcade' ? `DARI ${run.total} LAWAN` : 'MENANG BERUNTUN'}
`;
} else {
title = two ? 'MENANG' : playerWon ? 'MENANG' : 'KALAH';
kicker = two ? (playerWon ? 'PEMAIN 1' : 'PEMAIN 2') + ' MENANG' : playerWon ? 'PEMAIN 1 MENANG' : 'KOMPUTER MENANG';
scoreHtml = `${r.playerWins}—${r.enemyWins}
`;
}
const aw = r.award, lines = aw?.lines || [];
const coins = aw ? `KOIN
${lines.map((l, i) => `- ${esc(l.label)}${l.note ? `${esc(l.note)}` : ''}+${l.amount}
`).join('')}
TOTAL◆ +${aw.gain}
` : '';
const stat = (k, v) => `${k}${esc(v)}`;
const combo = r.matchBest ?? r.bestCombo ?? 0;
const stats = run
? stat(run.kind === 'arcade' ? 'Lawan' : 'Menang beruntun', run.kind === 'arcade' ? `${run.wins} / ${run.total}` : String(run.wins)) + stat(run.kind === 'arcade' ? 'Tingkat' : 'Rekor', run.kind === 'arcade' ? lv : String(run.best)) + stat('Kombo terbaik', combo + ' pukulan') + stat('Ronde terakhir', `${r.playerWins} – ${r.enemyWins}`)
: stat('Kombo terbaik', combo + ' pukulan') + stat('Ronde', `${r.playerWins} – ${r.enemyWins}`) + stat('Tingkat', two ? '—' : lv) + stat('Arena', st ? (st.short || stName) : '—');
const html = ``
+ (tone === 'win' ? `
${confetti(Wf.color, 26)}
` : '')
+ `
)})
PEMENANG${esc(Wf.name)}
`
+ `
${esc(kicker)}
${title}
${scoreHtml}`
+ `
“${esc(q.win)}”
${esc(Wf.name)}
`
+ `
`
+ `
`
+ `
)})
${esc(Lf.name)}“${esc(q.lose)}”
`;
return { title: run ? (run.kind === 'arcade' ? 'MODE ARCADE' : 'MODE SURVIVAL') : 'PERTANDINGAN SELESAI', eyebrow: stName ? stName.toUpperCase() : 'KONOHA FIGHTING', html };
}
// Setelah html dipasang: hitung naik angka koin dan fokuskan wadah layar hasil (kecuali layar sentuh).
function mount(root) {
const rs = root.querySelector('.rs'); if (!rs) return;
const calm = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
const n = rs.querySelector('[data-count]');
if (n && !calm) {
const to = Number(n.dataset.count) || 0, t0 = performance.now() + 700, dur = 900;
const tick = now => { if (!n.isConnected) return; const k = Math.min(1, Math.max(0, (now - t0) / dur)); n.textContent = '◆ +' + Math.round(to * (1 - Math.pow(1 - k, 3))); if (k < 1) requestAnimationFrame(tick); };
n.textContent = '◆ +0'; requestAnimationFrame(tick);
setTimeout(() => { if (n.isConnected) n.textContent = '◆ +' + to; }, 700 + dur + 400);
}
// Fokus ke wadah (bukan tombol): Spasi/Enter yang masih ditekan dari pertarungan tidak boleh langsung mengaktifkan MAIN LAGI; Tab tetap menjangkau tombol.
if (!document.documentElement.classList.contains('touch')) { rs.tabIndex = -1; rs.focus({ preventScroll: true }); }
}
window.ResultView = { view, mount };
})();