/* 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. 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, 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]); } 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; function vsSide(cls, f, who, q) { return `
${who}${esc(f.name)}${esc(f.tag)}

“${esc(q)}”

`; } 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 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}` : ''}
` + vsSide('l', L, 'PEMAIN 1', quote(L.id, 'intro')) + vsSide('r', R, two ? 'PEMAIN 2' : 'KOMPUTER', quote(R.id, 'intro')) + `
KLIK ATAU SPASI UNTUK LEWATI
`; document.body.append(el); const c = cur = { el, onDone, t0: performance.now(), done: false, timers: [] }; el.style.setProperty('--vs-run', (DURATION - FADE) + 'ms'); c.timers.push(setTimeout(() => finish('time'), DURATION - FADE), setTimeout(() => { try { window.__game?.menuSound?.('pick'); } catch (_) {} }, 520)); 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, fighter, duration: DURATION, 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 = {}) { // run.boss (RENCANA 3.6): 'win' = Bayangan dikalahkan, 'lose' = kalah dari Bayangan (Arcade tetap tamat, jadi pemain tetap tampil sebagai pemenang). // Bayangan (salinan gelap, id sama dengan pemain) selalu tampil di sisi yang kalah dengan nama dan ucapan khasnya (story.js). const run = r.run || null, two = !!r.p2, boss = run?.boss || null, playerWon = r.winner === 'player' || boss === 'lose'; const wid = playerWon ? r.player : r.enemy, lid = playerWon ? r.enemy : r.player, Wf = fighter(wid), Lf = boss && window.Story?.shadowFighter ? window.Story.shadowFighter(fighter(lid)) : fighter(lid); const tone = run ? (run.cleared ? 'win' : 'lose') : (two || playerWon ? 'win' : 'lose'); const q = r._q || (r._q = { win: quote(wid, 'win'), lose: boss ? (window.Story?.bossQuote?.(boss, r.player) || quote(lid, '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 ? (boss === 'win' ? 'BAYANGAN DIKALAHKAN' : boss === 'lose' ? 'TAMAT · BAYANGAN BELUM TERKALAHKAN' : '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

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)) + (boss ? stat('Bayangan', boss === 'win' ? 'Dikalahkan' : 'Bertahan') : 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' ? `` : '') + `
${esc(Wf.name)}
${boss === 'win' ? esc(window.Story?.title || 'PEMENANG') : 'PEMENANG'}${esc(Wf.name)}
` + `
${esc(kicker)}

${title}

${scoreHtml}
` + `

“${esc(q.win)}”

${esc(Wf.name)}
` + `
${coins}

STATISTIK

${stats}
` + `
` + `
${esc(Lf.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 }; })();