${s.name}
Warna baru untuk semua karakter.
/* Meta layer: coins, shop, character locker (skin + shop move), achievements and easter eggs. Everything is stored in this browser only (localStorage). The game engine reads it through window.Meta. */ (() => { 'use strict'; const KEY = 'konoha.save.v1'; const SKINS = { standar: { name: 'STANDAR', price: 0, filter: '' }, senja: { name: 'SENJA', price: 150, filter: 'hue-rotate(-35deg) saturate(1.25)' }, rimba: { name: 'HIJAU RIMBA', price: 150, filter: 'hue-rotate(75deg) saturate(1.2)' }, laut: { name: 'BIRU LAUT', price: 150, filter: 'hue-rotate(160deg) saturate(1.2)' }, neon: { name: 'NEON', price: 300, filter: 'hue-rotate(270deg) saturate(1.9) brightness(1.08)' }, emas: { name: 'EMAS', price: 450, filter: 'sepia(1) saturate(3.2) hue-rotate(-12deg) brightness(1.1)' }, mono: { name: 'MONOKROM', price: 250, filter: 'grayscale(1) contrast(1.25)' } }; const MOVES = { seruduk: { name: 'SERUDUK KILAT', price: 350, cd: 7, desc: 'Menerjang maju dan menghantam lawan di depan. Kuat untuk membuka jarak.' }, hentak: { name: 'HENTAKAN BUMI', price: 500, cd: 10, desc: 'Menghentak tanah ke dua sisi; lawan di dekatmu terjatuh.' }, perisai: { name: 'PERISAI', price: 450, cd: 16, desc: 'Kebal serangan selama 1,6 detik. Pakai untuk lolos dari ultimate.' }, pulih: { name: 'AURA PEMULIH', price: 600, cd: 30, desc: 'Memulihkan 25 HP seketika.' }, isiulang: { name: 'ISI ULANG', price: 700, cd: 35, desc: 'Menyegarkan cooldown skill 1 dan 2 seketika.' } }; const COMMANDS = [ ['↓ ↓ + SPASI', 'UPPERCUT', 'Meluncurkan lawan ke atas dan menjatuhkannya.'], ['↓ → + SPASI', 'TINJU MAJU', 'Maju sambil memukul, jangkauan panjang.'], ['SPASI x3', 'KOMBO', 'Rantai tiga pukulan; pukulan ketiga paling kuat.'], ['W W', 'LOMPAT GANDA', 'Lompat kedua di udara (berputar).'], ['K', 'JURUS TOKO', 'Memakai jurus toko yang dipasang di menu Karakter.'], ['TAHAN L', 'BLOK', 'Mengurangi damage 80%. Terlalu lama diserang = guard pecah.'], ['KETUK L', 'PARRY', 'Ketuk tepat sebelum kena: tanpa damage dan lawan terhenti.'], ['J', 'LEMPAR', 'Menembus blok. Harus dekat dan di tanah.'] ]; const ALT_PRICE = 450; function altOptions(id) { const f = window.FIGHTERS?.[id]; if (!f) return []; return ['skill1', 'skill2'].flatMap((slot, i) => { const kind = f.skills?.[slot]?.kind, proj = ['projectile', 'wave', 'homing'].includes(kind), nm = f.names[i + 1]; return [{ key: `${id}:${slot}:ganda`, slot, v: 'ganda', name: nm + (proj ? ' GANDA' : ' CEPAT'), desc: proj ? 'Menembak dua kali lipat dengan damage lebih kecil per peluru.' : 'Lebih cepat dan cooldown pendek, damage lebih kecil.' }, { key: `${id}:${slot}:berat`, slot, v: 'berat', name: nm + ' BERAT', desc: 'Lebih besar dan keras, tetapi lebih lambat dan cooldown panjang.' }]; }); } const ACH = { first_win: ['PERTAMA KALI', 'Menangkan satu pertandingan.'], perfect: ['BERSIH', 'Menang tanpa kehilangan satu ronde pun.'], combo5: ['KOMBO 5', 'Capai kombo 5 pukulan.'], combo10: ['KOMBO 10', 'Capai kombo 10 pukulan.'], hard_win: ['SANG PEMBURU', 'Menang di tingkat selain Easy.'], wins10: ['SEPULUH KALI', 'Menangkan 10 pertandingan.'], shopper: ['PEMBELI', 'Beli barang pertamamu di toko.'], fashion: ['MODIS', 'Miliki 3 skin.'], rich: ['KAYA', 'Kumpulkan total 2000 koin.'], konami: ['KODE LAMA', 'Temukan kode rahasia lama.'], syududu: ['SYUDUDU', 'Panggil semut.'], mbg: ['TRUK LEWAT', 'Panggil truk katering.'], esemka: ['KONVOI', 'Panggil konvoi.'], penasaran: ['PENASARAN', 'Ketuk slot tanda tanya berulang kali.'], logo: ['TELITI', 'Temukan rahasia di judul.'], parry: ['TEPAT WAKTU', 'Lakukan parry yang berhasil.'], arcade_clear: ['TAMAT', 'Selesaikan mode Arcade.'], survival5: ['BERTAHAN', 'Menang 5 kali berturut-turut di Survival.'], twoplayer: ['TEMAN BERMAIN', 'Mainkan satu pertandingan dua pemain.'], special: ['SPESIALIS', 'Beli satu varian skill karakter.'] }; const blank = () => ({ v: 1, coins: 0, earned: 0, voice: true, owned: { skin: ['standar'], move: [], alt: [] }, skin: {}, move: null, alt: {}, best: { survival: 0 }, stats: { wins: 0, losses: 0, bestCombo: 0, perfects: 0 }, ach: {}, eggs: {} }); let save = blank(); try { const raw = JSON.parse(localStorage.getItem(KEY) || 'null'); if (raw && raw.v === 1) save = { ...blank(), ...raw, owned: { ...blank().owned, ...raw.owned }, stats: { ...blank().stats, ...raw.stats } }; } catch (_) {} const persist = () => { try { localStorage.setItem(KEY, JSON.stringify(save)); } catch (_) {} }; // The styles are attached here too, so the overlay is never unstyled (and hidden under the menu) when index.html is stale. if (!document.querySelector('link[href^="meta.css"]')) { const l = document.createElement('link'); l.rel = 'stylesheet'; l.href = 'meta.css?' + Date.now(); document.head.append(l); } const $ = s => document.querySelector(s); const esc = t => String(t).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); // ---- toasts ---- let toastBox, held = []; // Selama layar VS tampil toast ditahan (menutupi bilah atas VS), lalu dimunculkan penuh setelah layar selesai atau dibatalkan. function holdToasts() { setTimeout(() => { if (window.VS?.active) return holdToasts(); const q = held; held = []; for (const [m, k] of q) toast(m, k); }, 150); } function toast(msg, kind = '') { if (window.VS?.active) { held.push([msg, kind]); if (held.length === 1) holdToasts(); return; } if (!toastBox) { toastBox = document.createElement('div'); toastBox.id = 'meta-toasts'; document.body.append(toastBox); } const el = document.createElement('div'); el.className = 'meta-toast ' + kind; el.textContent = msg; toastBox.append(el); setTimeout(() => el.classList.add('out'), 3200); setTimeout(() => el.remove(), 3800); } function addCoins(n) { save.coins += n; if (n > 0) save.earned += n; persist(); checkRich(); refreshCoins(); } function checkRich() { if (save.earned >= 2000) unlock('rich'); } function unlock(id) { if (save.ach[id] || !ACH[id]) return false; save.ach[id] = Date.now(); save.coins += 50; save.earned += 50; persist(); toast('PENCAPAIAN: ' + ACH[id][0] + ' +50 koin', 'ach'); refreshCoins(); return true; } // ---- engine API ---- function skinFilter(id) { const s = SKINS[save.skin[id] || 'standar']; return s && save.owned.skin.includes(save.skin[id] || 'standar') ? s.filter : ''; } function altSkill(id, slot) { const v = save.alt?.[id]?.[slot]; return v && save.owned.alt.includes(`${id}:${slot}:${v}`) ? v : null; } // Shouts use the browser's own text-to-speech (generic Indonesian voice), never an imitation of anyone's voice. let lastShout = 0; function shout(text) { if (!save.voice || !text || !window.speechSynthesis || Date.now() - lastShout < 900) return; lastShout = Date.now(); try { speechSynthesis.cancel(); const u = new SpeechSynthesisUtterance(String(text).toLowerCase()); u.lang = 'id-ID'; u.rate = 1.15; u.pitch = 1.25; u.volume = .9; speechSynthesis.speak(u); } catch (_) {} } function event(name) { if (name === 'parry') unlock('parry'); if (name === 'twoplayer') unlock('twoplayer'); } function equippedMove() { return save.move && save.owned.move.includes(save.move) ? save.move : null; } let chip; function hud(info) { if (!chip) { chip = document.createElement('div'); chip.id = 'shop-chip'; document.body.append(chip); } const m = equippedMove() ? info.move : null; const key = m ? m.name + '|' + Math.ceil(info.cd) : ''; if (chip.dataset.k === key) return; chip.dataset.k = key; chip.hidden = !m; chip.innerHTML = m ? `K${esc(m.name)}${info.cd > 0 ? Math.ceil(info.cd) + 's' : 'SIAP'}` : ''; chip.classList.toggle('cool', info.cd > 0); } // Rincian koin per baris disimpan di lastAward (dibaca layar hasil); jumlah koin tidak berubah. let lastAward = null, lastMatch = null; function award(r) { const won = r.winner === 'player', parts = [], lines = []; const add = (key, label, amount, partLabel, note) => { lines.push({ key, label, amount, ...(note ? { note } : {}) }); parts.push(partLabel + ' +' + amount); }; let gain = won ? 120 : 30; add(won ? 'win' : 'play', won ? 'Menang' : 'Hadiah main', gain, won ? 'Menang' : 'Hadiah main'); if (won) { if (r.level && r.level !== 'easy') { const b = { medium: 40, hard: 80, excellent: 120 }[r.level] || 40; gain += b; add('level', 'Tingkat kesulitan', b, 'Tingkat', ({ medium: 'Sedang', hard: 'Sulit', excellent: 'Sangat sulit' })[r.level]); } if (r.perfect) { gain += 60; add('perfect', 'Bersih', 60, 'Bersih', 'Tanpa kehilangan ronde'); } } const cb = Math.min(60, (r.bestCombo || 0) * 4); if (cb) { gain += cb; add('combo', 'Bonus kombo', cb, 'Kombo', r.bestCombo + ' pukulan, ronde terakhir'); } if (won) { save.stats.wins++; unlock('first_win'); if (save.stats.wins >= 10) unlock('wins10'); if (r.perfect) { save.stats.perfects++; unlock('perfect'); } if (r.level && r.level !== 'easy') unlock('hard_win'); } else save.stats.losses++; save.stats.bestCombo = Math.max(save.stats.bestCombo, r.bestCombo || 0); if ((r.bestCombo || 0) >= 5) unlock('combo5'); if ((r.bestCombo || 0) >= 10) unlock('combo10'); const prev = run ? run.earned || 0 : 0; if (run) run.earned = prev + gain; lastAward = { won, gain, lines, level: r.level || null, prev }; lastMatch = { ...r }; addCoins(gain); const handled = run ? advanceRun(won) : false; // Layar hasil (pertandingan biasa atau akhir Arcade/Survival) sudah memuat rincian yang sama; toast hanya untuk lawan di tengah rangkaian. if (run || !(window.ResultView && window.FrontEnd?.showResult)) toast('KOIN +' + gain + ' (' + parts.join(', ') + ')', 'coin'); return handled; } // ---- Arcade (fight everyone once, one life) and Survival (endless, harder each win) ---- let run = null; const shuffle = a => { for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; }; function startRun(kind, player) { run = { kind, player, wins: 0, queue: kind === 'arcade' ? shuffle(roster().filter(x => x !== player)) : [] }; nextFight(); } function nextFight() { const g = window.__game, stages = Object.keys(window.MatchRules?.stages || { bellora: 1 }); const enemy = run.kind === 'arcade' ? run.queue[run.wins] : shuffle(roster().filter(x => x !== run.player))[0]; const level = run.kind === 'arcade' ? 'medium' : run.wins < 3 ? 'easy' : run.wins < 6 ? 'medium' : run.wins < 9 ? 'hard' : 'excellent'; let ok = false; for (const st of shuffle(stages.slice()).concat('bellora')) { ok = g.startMatch({ player: run.player, enemy, stage: st, level, mode: 'versus', vs: { label: (run.kind === 'arcade' ? 'ARCADE · LAWAN ' + (run.wins + 1) + ' / ' + run.queue.length : 'SURVIVAL · LAWAN ' + (run.wins + 1)) } }); if (ok) break; } if (!ok) { toast('Aset belum siap, coba lagi.'); run = null; window.FrontEnd?.home(); } } function advanceRun(won) { if (won) { run.wins++; if (run.kind === 'arcade' && run.wins >= run.queue.length) { finishRun(true); return true; } if (run.kind === 'survival') { save.best.survival = Math.max(save.best.survival, run.wins); if (run.wins >= 5) unlock('survival5'); persist(); } setTimeout(() => { if (run) nextFight(); }, 2600); return true; } finishRun(false); return true; } function finishRun(cleared) { const r = run; run = null; let bonus = 0; if (r.kind === 'arcade' && cleared) { bonus = 500; unlock('arcade_clear'); } else if (r.kind === 'survival') bonus = r.wins * 30; if (bonus) addCoins(bonus); // Layar akhir sama gayanya dengan layar hasil pertandingan (menu.js 'result' lewat ResultView); layar lama tetap jadi cadangan. if (lastMatch && window.ResultView && window.FrontEnd?.showResult) { // Satu baris per kelompok supaya muat: lawan sebelumnya, pertandingan terakhir, bonus rangkaian. const base = lastAward || { gain: 0, lines: [], prev: 0 }, before = cleared ? r.wins - 1 : r.wins, lines = []; if (base.prev > 0) lines.push({ key: 'prev', label: 'Lawan sebelumnya', amount: base.prev, note: before + ' pertandingan' }); lines.push({ key: 'last', label: 'Pertandingan terakhir', amount: base.gain, note: cleared ? 'Menang' : 'Kalah' }); if (bonus) lines.push({ key: 'run', label: r.kind === 'arcade' ? 'Bonus Arcade tamat' : 'Bonus rangkaian menang', amount: bonus, note: r.kind === 'arcade' ? '' : r.wins + ' x 30' }); window.FrontEnd.showResult({ ...lastMatch, mode: 'versus', award: { ...base, lines, gain: base.prev + base.gain + bonus }, run: { kind: r.kind, wins: r.wins, total: r.queue.length, cleared, best: save.best.survival, bonus } }); return; } if (!root) build(); root.hidden = false; const title = r.kind === 'arcade' ? (cleared ? 'ARCADE TAMAT!' : 'ARCADE BERAKHIR') : 'SURVIVAL BERAKHIR'; const sub = r.kind === 'arcade' ? (cleared ? 'Semua lawan dikalahkan.' : 'Kalah di lawan ke-' + (r.wins + 1) + ' dari ' + r.queue.length + '.') : 'Kemenangan beruntun: ' + r.wins + ' · rekor ' + save.best.survival; root.innerHTML = `
`; } // ---- UI ---- let root, tab = 'shop', who = null, notice = ''; const roster = () => Object.keys(window.FIGHTERS || {}).concat(window.Browo && window.BROWO_MANIFEST ? ['browo'] : []); const fname = id => window.FIGHTERS?.[id]?.name || (id === 'browo' ? 'BRO WO' : id.toUpperCase()); const fnames = id => window.FIGHTERS?.[id]?.names || window.Browo?.names || []; function refreshCoins() { const c = $('#meta-coins'); if (c) c.textContent = save.coins; } function build() { root = document.createElement('div'); root.id = 'meta-overlay'; root.hidden = true; document.body.append(root); root.addEventListener('click', onClick); } function open(t = 'shop') { if (!root) build(); tab = t; if (!who) who = roster()[0]; root.hidden = false; render(); document.addEventListener('keydown', escClose, true); } function close() { root.hidden = true; document.removeEventListener('keydown', escClose, true); } function escClose(e) { if (e.key === 'Escape') { e.stopPropagation(); close(); } } function tabs() { return ['shop:TOKO', 'locker:KARAKTER', 'book:KOLEKSI'].map(x => { const [k, l] = x.split(':'); return ``; }).join(''); } function render() { let body = ''; if (tab === 'shop') body = renderShop(); else if (tab === 'locker') body = renderLocker(); else body = renderBook(); root.innerHTML = ``; } function renderShop() { const skins = Object.entries(SKINS).filter(([k]) => k !== 'standar').map(([k, s]) => { const own = save.owned.skin.includes(k); return `Warna baru untuk semua karakter.
${m.desc}
Cooldown ${m.cd} detik · tombol K${o.desc}
${fname(who)} · ${o.slot === 'skill1' ? 'tombol I' : 'tombol O'}Menang ${st.wins} · Kalah ${st.losses} · Bersih ${st.perfects} · Kombo terbaik ${st.bestCombo} · Total koin ${save.earned}
Memakai suara bawaan browser.
`; } function onClick(e) { const b = e.target.closest('button'); if (!b) return; if (b.hasAttribute('data-close')) return close(); if (b.dataset.tab) { tab = b.dataset.tab; notice = ''; return render(); } if (b.dataset.who || b.dataset.shopwho) { who = b.dataset.who || b.dataset.shopwho; return render(); } if (b.hasAttribute('data-runmenu')) { close(); return window.FrontEnd?.home(); } if (b.dataset.alt !== undefined && b.hasAttribute('data-alt')) { const [slot, v] = b.dataset.alt.split(':'); (save.alt[who] = save.alt[who] || {})[slot] = v || null; persist(); return render(); } if (b.dataset.skin) { save.skin[who] = b.dataset.skin; persist(); return render(); } if (b.dataset.move !== undefined && b.hasAttribute('data-move')) { save.move = b.dataset.move || null; persist(); return render(); } if (b.dataset.buy) { const kind = b.dataset.buy.split(':')[0], id = b.dataset.buy.slice(kind.length + 1), item = kind === 'alt' ? { name: altOptions(who).find(o => o.key === id)?.name || id, price: ALT_PRICE } : (kind === 'skin' ? SKINS : MOVES)[id]; if (save.owned[kind].includes(id)) return; if (save.coins < item.price) { notice = 'Koin belum cukup untuk ' + item.name + '.'; return render(); } save.coins -= item.price; save.owned[kind].push(id); if (kind === 'move' && !save.move) save.move = id; if (kind === 'alt') { const [c, sl, v] = id.split(':'); (save.alt[c] = save.alt[c] || {})[sl] = v; unlock('special'); } notice = item.name + ' dibeli.'; persist(); unlock('shopper'); if (save.owned.skin.length >= 4) unlock('fashion'); return render(); } if (b.hasAttribute('data-voice')) { save.voice = !save.voice; persist(); return render(); } if (b.hasAttribute('data-export')) { const code = btoa(unescape(encodeURIComponent(JSON.stringify(save)))); navigator.clipboard?.writeText(code).then(() => { notice = 'Kode disalin ke papan klip.'; render(); }, () => { notice = 'Tidak bisa menyalin.'; render(); }); return; } if (b.hasAttribute('data-import')) { const code = prompt('Tempel kode simpanan:'); if (!code) return; try { const d = JSON.parse(decodeURIComponent(escape(atob(code.trim())))); if (d.v !== 1) throw 0; save = { ...blank(), ...d }; persist(); notice = 'Simpanan dimuat.'; } catch (_) { notice = 'Kode tidak valid.'; } return render(); } if (b.hasAttribute('data-reset')) { if (confirm('Hapus semua koin, skin, jurus, dan pencapaian?')) { save = blank(); persist(); notice = 'Simpanan dihapus.'; render(); } } } // ---- easter eggs ---- const KONAMI = ['arrowup', 'arrowup', 'arrowdown', 'arrowdown', 'arrowleft', 'arrowright', 'arrowleft', 'arrowright', 'b', 'a']; let kbuf = [], tbuf = ''; function swarm(emoji, n, cls) { const box = document.createElement('div'); box.className = 'egg-layer ' + cls; document.body.append(box); for (let i = 0; i < n; i++) { const s = document.createElement('span'); s.textContent = emoji; s.style.setProperty('--y', (8 + Math.random() * 84) + 'vh'); s.style.setProperty('--d', (Math.random() * 1.6) + 's'); s.style.setProperty('--t', (3 + Math.random() * 2) + 's'); s.style.setProperty('--s', (22 + Math.random() * 30) + 'px'); box.append(s); } setTimeout(() => box.remove(), 6500); } function egg(id, label, fn) { fn(); if (!save.eggs[id]) { save.eggs[id] = 1; addCoins(100); toast(label + ' +100 koin', 'ach'); } if (ACH[id]) unlock(id); } const CODES = { syududu: () => egg('syududu', 'SYUDUDU!', () => swarm('🐜', 40, 'ants')), mbg: () => egg('mbg', 'TRUK KATERING LEWAT', () => swarm('🚚', 1, 'truck')), esemka: () => egg('esemka', 'KONVOI ESEMKA', () => swarm('🚗', 6, 'truck')) }; document.addEventListener('keydown', e => { if (e.target?.matches?.('input,textarea')) return; const k = e.key.length === 1 ? e.key.toLowerCase() : e.key.toLowerCase(); kbuf.push(k); if (kbuf.length > KONAMI.length) kbuf.shift(); if (kbuf.join() === KONAMI.join()) { kbuf = []; egg('konami', 'KODE RAHASIA TERBUKA', () => swarm('✦', 24, 'ants')); } if (k.length === 1) { tbuf = (tbuf + k).slice(-10); for (const c in CODES) if (tbuf.endsWith(c)) { tbuf = ''; CODES[c](); } } }); let logoClicks = 0, lockClicks = 0, lt = 0; document.addEventListener('click', e => { if (e.target.closest?.('.home-title h1')) { if (++logoClicks >= 7) { logoClicks = 0; egg('logo', 'KAMU TELITI', () => swarm('✦', 20, 'ants')); } } const lock = e.target.closest?.('[data-fighter^="locked"]'); if (lock) { const now = Date.now(); if (now - lt > 4000) lockClicks = 0; lt = now; lockClicks++; if (lockClicks === 3) toast('Siapa yang akan datang berikutnya…?'); if (lockClicks >= 6) { lockClicks = 0; egg('penasaran', 'RASA INGIN TAHUMU DIHARGAI', () => {}); } } }, true); // ---- cheat / redeem codes (Settings) ---- const REDEEM = { rizkyganteng: { coins: 10000, label: 'KODE DITERIMA: +10.000 koin' } }; function redeem(raw) { const c = String(raw || '').trim().toLowerCase(), r = REDEEM[c]; if (!c) return 'Ketik kodenya dulu.'; if (!r) return 'Kode tidak dikenal.'; if (save.eggs['code:' + c]) return 'Kode ini sudah dipakai.'; save.eggs['code:' + c] = 1; addCoins(r.coins); toast(r.label, 'coin'); return r.label; } function mountCheatRow() { const panel = document.querySelector('#settings-panel'); if (!panel || panel.querySelector('#cheat-row')) return; const row = document.createElement('div'); row.className = 'setting-row'; row.id = 'cheat-row'; row.innerHTML = 'Kode cheatMasukkan kode untuk hadiah koin'; const anchor = panel.querySelector('.dialog-buttons'); (anchor || panel.lastElementChild).before(row); const go = () => { const input = row.querySelector('#cheat-input'); row.querySelector('#cheat-msg').textContent = redeem(input.value); input.value = ''; }; row.querySelector('#cheat-go').onclick = go; row.querySelector('#cheat-input').addEventListener('keydown', e => { e.stopPropagation(); if (e.key === 'Enter') { e.preventDefault(); go(); } }); row.querySelector('#cheat-input').addEventListener('keyup', e => e.stopPropagation()); } mountCheatRow(); window.Meta = { get lastAward() { return lastAward; }, redeem, shout, cancelRun() { run = null; }, altSkill, event, startRun, skinFilter, equippedMove, hud, award, open, close, toast, addCoins, unlock, get coins() { return save.coins; }, get save() { return save; } }; })();