/* 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.'] ]; 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.'] }; const blank = () => ({ v: 1, coins: 0, earned: 0, owned: { skin: ['standar'], move: [] }, skin: {}, move: null, 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 (_) {} }; const $ = s => document.querySelector(s); const esc = t => String(t).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); // ---- toasts ---- let toastBox; function toast(msg, kind = '') { 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 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); } function award(r) { const won = r.winner === 'player', parts = []; let gain = won ? 120 : 30; parts.push(won ? 'Menang +120' : 'Hadiah main +30'); if (won) { if (r.level && r.level !== 'easy') { const b = { medium: 40, hard: 80, excellent: 120 }[r.level] || 40; gain += b; parts.push('Tingkat +' + b); } if (r.perfect) { gain += 60; parts.push('Bersih +60'); } } const cb = Math.min(60, (r.bestCombo || 0) * 4); if (cb) { gain += cb; parts.push('Kombo +' + cb); } 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'); addCoins(gain); toast('KOIN +' + gain + ' (' + parts.join(', ') + ')', 'coin'); } // ---- 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 = `
◆ ${save.coins}
${notice ? `

${esc(notice)}

` : ''}
${body}
`; } function renderShop() { const skins = Object.entries(SKINS).filter(([k]) => k !== 'standar').map(([k, s]) => { const own = save.owned.skin.includes(k); return `

${s.name}

Warna baru untuk semua karakter.

`; }).join(''); const moves = Object.entries(MOVES).map(([k, m]) => { const own = save.owned.move.includes(k); return `

${m.name}

${m.desc}

Cooldown ${m.cd} detik · tombol K
`; }).join(''); return `

Jurus toko

${moves}

Skin warna

${skins}

Koin didapat dari menang pertandingan, kombo, dan pencapaian. Pasang barang di menu KARAKTER.

`; } const portrait = id => `assets/${id}/ui/portrait.webp`; function renderLocker() { const ids = roster(), sk = save.skin[who] || 'standar'; const pick = ids.map(id => ``).join(''); const skins = save.owned.skin.map(k => ``).join(''); const moves = [``].concat(save.owned.move.map(k => ``)).join(''); const n = fnames(who); return `
${pick}

${fname(who)}

Skin
${skins}
Jurus toko (tombol K)
${moves}

${save.owned.move.length ? '' : 'Belum punya jurus toko. Beli di tab TOKO.'}

Daftar gerakan
`; } function renderBook() { const got = Object.keys(ACH).filter(k => save.ach[k]).length, st = save.stats; const list = Object.entries(ACH).map(([k, [n, d]]) => { const ok = !!save.ach[k], hidden = !ok && ['konami', 'syududu', 'mbg', 'esemka', 'penasaran', 'logo'].includes(k); return `
  • ${ok ? '★' : '☆'} ${hidden ? '???' : n}${hidden ? 'Pencapaian rahasia.' : d}
  • `; }).join(''); return `

    Statistik

    Menang ${st.wins} · Kalah ${st.losses} · Bersih ${st.perfects} · Kombo terbaik ${st.bestCombo} · Total koin ${save.earned}

    Pencapaian ${got}/${Object.keys(ACH).length}

    Simpanan

    `; } 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) { who = b.dataset.who; 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, id] = b.dataset.buy.split(':'), item = (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; notice = item.name + ' dibeli.'; persist(); unlock('shopper'); if (save.owned.skin.length >= 4) unlock('fashion'); 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); window.Meta = { skinFilter, equippedMove, hud, award, open, close, toast, addCoins, unlock, get coins() { return save.coins; }, get save() { return save; } }; })();