Slot misteri yang hidup (RENCANA 3.7): lima petak terkunci jadi sistem petunjuk bertahap tanpa gambar baru
- content/mystery.js: 5 slot dengan 3-4 petunjuk tiap slot (19 total); syarat dibaca dari Meta.save (menang, Survival, Arcade tamat, kombo, menang bersih, pencapaian, kejutan, dialog rahasia, barang toko). Teks samar dan lucu tanpa nama tokoh nyata. - mystery.js dan mystery.css: siluet SVG sebaris dari bentuk locked.svg yang makin tajam tiap petunjuk terbuka, benda khas muncul di dua tahap terakhir, tanda tanya berubah, lencana n/total, titik BARU dan toast. Panel pratinjau memuat petunjuk terbuka, syarat petunjuk berikutnya dengan bilah progres, dan nomor slot. - menu.js: empat pengait kecil (panel, dekorasi petak, pratinjau teks bawah roster, pesan saat ditekan). Slot tetap tidak bisa dipilih dan tetap data-fighter locked-N sehingga easter egg penasaran tetap jalan. - meta.js: pencapaian tersembunyi PENASARAN BERAT bila semua petunjuk terbuka. - ?mystery=N atau all di URL mempratinjau tahap tanpa menyimpan apa pun. Catatan mesin dan README diperbarui. Mode ringan: tanpa berkas uji baru, diuji manual oleh pemilik.
This commit is contained in:
parent
4ce380873d
commit
eed969afe8
|
|
@ -56,13 +56,15 @@ Di menu: W A S D atau tombol panah untuk memilih, Enter untuk konfirmasi, Esc un
|
||||||
├── menu.css # menu utama, pilih karakter dan arena, hasil, latar kolase, About
|
├── menu.css # menu utama, pilih karakter dan arena, hasil, latar kolase, About
|
||||||
├── feel.css # peringkat kombo
|
├── feel.css # peringkat kombo
|
||||||
├── meta.css # toko, karakter, koleksi, toast
|
├── meta.css # toko, karakter, koleksi, toast
|
||||||
|
├── mystery.css # gaya slot misteri di layar pilih karakter
|
||||||
├── game.js # mesin: loop, fisika, pertarungan, otak CPU, rendering, HUD
|
├── game.js # mesin: loop, fisika, pertarungan, otak CPU, rendering, HUD
|
||||||
├── menu.js # menu: alur layar, roster, arena, tingkat kesulitan, kolase latar
|
├── menu.js # menu: alur layar, roster, arena, tingkat kesulitan, kolase latar
|
||||||
├── match.js # aturan ronde, timer, arena, penyetelan tingkat CPU
|
├── match.js # aturan ronde, timer, arena, penyetelan tingkat CPU
|
||||||
├── feel.js # rasa kena: hit-stop, getaran, K.O. gerak lambat
|
├── feel.js # rasa kena: hit-stop, getaran, K.O. gerak lambat
|
||||||
├── meta.js # koin, toko, skin, pencapaian, Arcade/Survival
|
├── meta.js # koin, toko, skin, pencapaian, Arcade/Survival
|
||||||
|
├── mystery.js # slot misteri yang hidup: siluet dan petunjuk bertahap dari kemajuan pemain
|
||||||
├── brand.js # kutipan layar loading dan daftar "Yang baru"
|
├── brand.js # kutipan layar loading dan daftar "Yang baru"
|
||||||
├── content/ # data teks: loading.js (kutipan), changelog.js (Yang baru)
|
├── content/ # data teks: loading.js (kutipan), changelog.js (Yang baru), mystery.js (petunjuk slot misteri)
|
||||||
├── announcer.js # antrean suara pengumuman
|
├── announcer.js # antrean suara pengumuman
|
||||||
├── touch.js # kontrol sentuh di layar
|
├── touch.js # kontrol sentuh di layar
|
||||||
├── browo.js, fenr.js # kit tulisan tangan (karakter lain berbasis data di assets/<id>/manifest.js)
|
├── browo.js, fenr.js # kit tulisan tangan (karakter lain berbasis data di assets/<id>/manifest.js)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
/* Slot misteri yang hidup (RENCANA 3.7): window.MYSTERY = [slot, ...], satu entri per petak terkunci 'locked-N' di layar pilih
|
||||||
|
karakter. Urutan data = urutan petak (MYSTERY[0] untuk locked-0, dan seterusnya). Dibaca mystery.js; gaya di mystery.css.
|
||||||
|
|
||||||
|
Bentuk satu slot:
|
||||||
|
id kunci tetap (dipakai untuk mengingat petunjuk yang sudah dilihat di localStorage['konoha.mystery.seen']); jangan diganti.
|
||||||
|
color warna aksen '#rrggbb' untuk siluet, lencana, dan panel.
|
||||||
|
shape benda pada siluet: palu | gunting | pengeras | helm | ponsel (digambar di mystery.js, tanpa gambar baru).
|
||||||
|
stages 3 sampai 4 petunjuk berurutan. Tahap terbuka bila syaratnya terpenuhi DAN tahap sebelumnya sudah terbuka.
|
||||||
|
need {type, n}: type dibaca dari Meta.save oleh mystery.js. Yang dikenal: wins (menang), matches (jumlah main),
|
||||||
|
perfects (menang tanpa kehilangan ronde), combo (kombo terbaik), survival (rekor beruntun Survival),
|
||||||
|
arcade (Arcade tamat, n = 1), ach (jumlah pencapaian), eggs (kejutan tersembunyi), pairs (dialog rahasia
|
||||||
|
berpasangan; tahap ini dilewati bila fitur dialog tidak ada), shop (barang toko yang dimiliki), coins (total koin).
|
||||||
|
tag label singkat huruf besar untuk petunjuk (CIRI, JURUS, JULUKAN, PESAN).
|
||||||
|
text teka-teki pendek, usahakan di bawah 70 huruf supaya muat dua baris di panel.
|
||||||
|
|
||||||
|
Pedoman isi: petunjuk samar dan lucu tentang ciri, julukan, atau jurus fiktif. Jangan menyebut nama tokoh nyata, jangan
|
||||||
|
menuduh fakta, tanpa unsur agama, tanpa merendahkan siapa pun. Teks di bawah memakai sosok umum (juru palu, gunting pita,
|
||||||
|
pengeras suara tangan, helm proyek, siaran langsung) sebagai pengisi; ganti bila karakter sebenarnya sudah ditentukan.
|
||||||
|
|
||||||
|
Saat petarung sungguhan dirilis menggantikan satu slot, hapus entri slotnya dari daftar ini supaya urutan data tetap sama
|
||||||
|
dengan petak 'locked-N' yang tersisa. Semua petunjuk terbuka (seluruh slot) = pencapaian PENASARAN BERAT di meta.js. */
|
||||||
|
window.MYSTERY = [
|
||||||
|
{
|
||||||
|
id: 'ketok', color: '#7fe0ff', shape: 'palu',
|
||||||
|
stages: [
|
||||||
|
{ need: { type: 'wins', n: 1 }, tag: 'CIRI', text: 'Tak butuh pengeras suara. Cukup sepotong kayu dan satu meja.' },
|
||||||
|
{ need: { type: 'wins', n: 5 }, tag: 'JURUS', text: 'Jurusnya berbunyi tok-tok-tok. Yang ketiga paling menentukan.' },
|
||||||
|
{ need: { type: 'wins', n: 12 }, tag: 'JULUKAN', text: 'Dijuluki Si Ketok Tiga. Rapat selesai kalau dia bilang selesai.' },
|
||||||
|
{ need: { type: 'wins', n: 25 }, tag: 'PESAN', text: 'Sidang diskors dulu. Dia sedang memanaskan palunya.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pita', color: '#efce91', shape: 'gunting',
|
||||||
|
stages: [
|
||||||
|
{ need: { type: 'survival', n: 2 }, tag: 'CIRI', text: 'Tiap dia datang, selalu ada pita yang sudah terbentang.' },
|
||||||
|
{ need: { type: 'survival', n: 5 }, tag: 'JURUS', text: 'Jurusnya ‘Peresmian Mendadak’: gunting dulu, kenalan belakangan.' },
|
||||||
|
{ need: { type: 'arcade', n: 1 }, tag: 'JULUKAN', text: 'Dijuluki Si Gunting Pita. Aba-abanya selalu tepuk tangan.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'volume', color: '#f4a869', shape: 'pengeras',
|
||||||
|
stages: [
|
||||||
|
{ need: { type: 'combo', n: 5 }, tag: 'CIRI', text: 'Suaranya sampai ke baris paling belakang, bahkan saat berbisik.' },
|
||||||
|
{ need: { type: 'perfects', n: 1 }, tag: 'JURUS', text: 'Jurusnya ‘Orasi Tiga Babak’: pembuka, isi, lalu pukulan penutup.' },
|
||||||
|
{ need: { type: 'combo', n: 10 }, tag: 'JULUKAN', text: 'Dijuluki Si Volume Penuh. Tombol pelannya entah ke mana.' },
|
||||||
|
{ need: { type: 'perfects', n: 3 }, tag: 'PESAN', text: 'Katanya dia sedang latihan vokal. Siapkan penyumbat telinga.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'helm', color: '#8be0a8', shape: 'helm',
|
||||||
|
stages: [
|
||||||
|
{ need: { type: 'shop', n: 1 }, tag: 'CIRI', text: 'Di tangannya gulungan denah, di kepalanya selalu ada helm.' },
|
||||||
|
{ need: { type: 'ach', n: 5 }, tag: 'JURUS', text: 'Jurusnya ‘Tinjau Lokasi’: datang, menunjuk, semua langsung mengangguk.' },
|
||||||
|
{ need: { type: 'shop', n: 4 }, tag: 'JULUKAN', text: 'Dijuluki Si Helm Kuning. Aman, kecuali untuk lawannya.' },
|
||||||
|
{ need: { type: 'ach', n: 12 }, tag: 'PESAN', text: 'Dia sedang mengukur arena. Pertanda baik untuk yang menunggu.' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'siaran', color: '#f08fb8', shape: 'ponsel',
|
||||||
|
stages: [
|
||||||
|
{ need: { type: 'eggs', n: 1 }, tag: 'CIRI', text: 'Selalu mencari sudut terbaik, termasuk saat sedang dipukul.' },
|
||||||
|
{ need: { type: 'eggs', n: 3 }, tag: 'JURUS', text: 'Jurusnya ‘Siaran Langsung’: pukulan sempat tertunda karena sinyal.' },
|
||||||
|
{ need: { type: 'pairs', n: 2 }, tag: 'JULUKAN', text: 'Dijuluki Si Sinyal Penuh. Selalu ada penonton, walau cuma satu.' },
|
||||||
|
{ need: { type: 'eggs', n: 4 }, tag: 'PESAN', text: 'Kolom komentar ramai menebak siapa dia. Kamu salah satunya.' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
@ -15,6 +15,7 @@ Game fighting 2D canvas, tanpa dependensi dan tanpa langkah build. Semua teks un
|
||||||
- Aturan ronde: `match.js` (`MatchRules`), dipakai `advanceMatch` di game.js.
|
- Aturan ronde: `match.js` (`MatchRules`), dipakai `advanceMatch` di game.js.
|
||||||
- `meta.js`: `window.Meta` — koin, toko, skin (filter warna), jurus toko (tombol K), varian skill, menu Karakter/Koleksi, pencapaian, easter egg, Arcade/Survival (`startRun`), kolom kode cheat di Pengaturan. Simpanan: `localStorage['konoha.save.v1']`.
|
- `meta.js`: `window.Meta` — koin, toko, skin (filter warna), jurus toko (tombol K), varian skill, menu Karakter/Koleksi, pencapaian, easter egg, Arcade/Survival (`startRun`), kolom kode cheat di Pengaturan. Simpanan: `localStorage['konoha.save.v1']`.
|
||||||
- `menu.js`: menu utama (home, pilih karakter, arena, hasil, about) di `#front-menu`; `window.FrontEnd`.
|
- `menu.js`: menu utama (home, pilih karakter, arena, hasil, about) di `#front-menu`; `window.FrontEnd`.
|
||||||
|
- Slot misteri yang hidup (Tahap 3.7): lima petak `locked-N` di layar pilih karakter. Data `content/mystery.js` (`window.MYSTERY`, satu entri per petak menurut urutan; skema dan pedoman isi ada di komentar berkas itu), logika `mystery.js` (`window.Mystery`), gaya `mystery.css`; dimuat di `index.html` tepat sebelum `menu.js`. Tiap slot punya 3–4 petunjuk berurutan dengan syarat `need {type, n}` yang dibaca dari `Meta.save` (wins, matches, perfects, combo, survival, arcade, ach, eggs, pairs = `save.eggs['dialog:*']`, shop, coins; daftar `GOALS` di mystery.js; tahap `pairs` dilewati bila `window.DIALOGS.rahasia` tidak ada). Petak: siluet SVG sebaris (tubuh disalin dari `assets/menu/locked.svg`, benda khas muncul samar di tahap kedua terakhir dan jelas di tahap akhir) makin tajam lewat `--ms-p`, lencana `n/total`, tanda tanya berubah (`?` `??` `?!` `!?` `!`), titik BARU. Panel pratinjau (yang menampilkan sorotan: kiri saat memilih P1, kanan saat memilih lawan) memuat petunjuk terbuka, syarat petunjuk berikutnya dengan bilah progres, dan "Slot misteri n / total". Hook di menu.js: `Mystery.panel` (di `fighterPanel`), `decorate` (akhir `render()`), `preview` (di `updatePreview`: teks `#fighter-moves` dan `#roster-notice`), `notice` (di `confirm()` saat slot ditekan). Semua fungsi publik aman: bila gagal, tampilan lama slot terkunci dipakai. Petak tetap `data-fighter="locked-N"` karena easter egg `penasaran` (meta.js) membaca ketukan di atasnya. Semua petunjuk terbuka = pencapaian tersembunyi `penasaran_berat` (PENASARAN BERAT, +50 koin; dicek tiap layar pilih karakter dirender). Ingatan "sudah dilihat" untuk titik BARU dan toast ada di `localStorage['konoha.mystery.seen']` (bukan di `Meta.save`). Uji manual cepat: buka `?mystery=2` (atau `all`) untuk mempratinjau tahap 2 pada semua slot tanpa menyimpan atau membuka pencapaian; `Mystery.states()` di konsol meringkas kemajuan. Saat petarung sungguhan menggantikan satu slot, hapus entri slotnya dari `window.MYSTERY` supaya urutan tetap sama dengan petak `locked-N` yang tersisa.
|
||||||
- `touch.js`: kontrol sentuh; tombol memanggil `__game.input(key, down)`.
|
- `touch.js`: kontrol sentuh; tombol memanggil `__game.input(key, down)`.
|
||||||
- `brand.js` (`window.Brand`) + `content/loading.js` (`window.LOADING_QUOTES`) + `content/changelog.js` (`window.CHANGELOG`): dimuat di `index.html` tepat setelah kartu `#boot`, sebelum `precache.js`. Kutipan acak (tidak pernah sama dengan yang terakhir, kunci `konoha.loadquote`; `?quote=N` memaksa) tampil di `#boot .boot-quote` dan `#load-state .load-quote`. "Yang baru": `Brand.decorate(root, screen)` dipanggil `menu.js` di akhir `render()`; layar `about` mendapat panel `.whatsnew-panel` dan tautan kredit, layar `home` memicu toast `.brand-toast` satu kali bila versi teratas CHANGELOG berbeda dari `localStorage['konoha.changelog.seen']` (pemain tanpa simpanan `konoha.save.v1` tidak diberi toast). Menambah pembaruan: sisipkan objek baru di AWAL `content/changelog.js` dengan version lebih besar. Berkas `content/*.js` ikut `tools/build_precache.py`.
|
- `brand.js` (`window.Brand`) + `content/loading.js` (`window.LOADING_QUOTES`) + `content/changelog.js` (`window.CHANGELOG`): dimuat di `index.html` tepat setelah kartu `#boot`, sebelum `precache.js`. Kutipan acak (tidak pernah sama dengan yang terakhir, kunci `konoha.loadquote`; `?quote=N` memaksa) tampil di `#boot .boot-quote` dan `#load-state .load-quote`. "Yang baru": `Brand.decorate(root, screen)` dipanggil `menu.js` di akhir `render()`; layar `about` mendapat panel `.whatsnew-panel` dan tautan kredit, layar `home` memicu toast `.brand-toast` satu kali bila versi teratas CHANGELOG berbeda dari `localStorage['konoha.changelog.seen']` (pemain tanpa simpanan `konoha.save.v1` tidak diberi toast). Menambah pembaruan: sisipkan objek baru di AWAL `content/changelog.js` dengan version lebih besar. Berkas `content/*.js` ikut `tools/build_precache.py`.
|
||||||
- Latar menu utama dan About: `menu.js` `collageHtml()` menyusun tujuh potret `assets/menu/<id>-select.webp` (lima di layar sentuh) menjadi kolase CSS (`.hc-*` di bagian "Merek" paling bawah `menu.css`). Gerak berhenti lewat kelas `.bg-still` pada `#front-menu` (`FrontEnd.syncBackground()`: Pengaturan terbuka, tab tersembunyi, saklar "Animasi latar menu", `prefers-reduced-motion`). Video dan gambar bergaya game asal (`home-factions.webp`, `home-factions-loop.mp4`, `home-art.webp`) tidak dipakai lagi tetapi berkasnya masih ada (dan ikut daftar precache).
|
- Latar menu utama dan About: `menu.js` `collageHtml()` menyusun tujuh potret `assets/menu/<id>-select.webp` (lima di layar sentuh) menjadi kolase CSS (`.hc-*` di bagian "Merek" paling bawah `menu.css`). Gerak berhenti lewat kelas `.bg-still` pada `#front-menu` (`FrontEnd.syncBackground()`: Pengaturan terbuka, tab tersembunyi, saklar "Animasi latar menu", `prefers-reduced-motion`). Video dan gambar bergaya game asal (`home-factions.webp`, `home-factions-loop.mp4`, `home-art.webp`) tidak dipakai lagi tetapi berkasnya masih ada (dan ikut daftar precache).
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="id">
|
<html lang="id">
|
||||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#101923"><script>/* Phones/tablets: mark .touch and open the landscape mobile shell (mobile.html). ?touch=1 forces it on desktop, ?touch=0 turns it off. */(()=>{const q=new URLSearchParams(location.search),f=q.get('touch'),t=f?f==='1':matchMedia('(pointer: coarse)').matches||(navigator.maxTouchPoints>0&&/Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent));if(!t)return;document.documentElement.classList.add('touch');if(window.top===window&&!q.has('embed'))location.replace('mobile.html'+location.search+location.hash);})();</script><title>Konoha Fighting</title><link rel="preload" href="assets/fonts/Rajdhani-Bold.ttf" as="font" type="font/ttf" crossorigin><link rel="stylesheet" href="style.css"><link rel="stylesheet" href="menu.css"><link rel="stylesheet" href="meta.css">
|
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#101923"><script>/* Phones/tablets: mark .touch and open the landscape mobile shell (mobile.html). ?touch=1 forces it on desktop, ?touch=0 turns it off. */(()=>{const q=new URLSearchParams(location.search),f=q.get('touch'),t=f?f==='1':matchMedia('(pointer: coarse)').matches||(navigator.maxTouchPoints>0&&/Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent));if(!t)return;document.documentElement.classList.add('touch');if(window.top===window&&!q.has('embed'))location.replace('mobile.html'+location.search+location.hash);})();</script><title>Konoha Fighting</title><link rel="preload" href="assets/fonts/Rajdhani-Bold.ttf" as="font" type="font/ttf" crossorigin><link rel="stylesheet" href="style.css"><link rel="stylesheet" href="menu.css"><link rel="stylesheet" href="meta.css">
|
||||||
<link rel="stylesheet" href="feel.css"><link rel="stylesheet" href="vs.css"></head>
|
<link rel="stylesheet" href="feel.css"><link rel="stylesheet" href="vs.css"><link rel="stylesheet" href="mystery.css"></head>
|
||||||
<body><div id="boot" role="status" aria-live="polite"><div class="boot-card"><strong class="boot-logo">KONOHA<br><em>FIGHTING</em></strong><div class="boot-bar"><i></i></div><span class="boot-status">Menyiapkan…</span><p class="boot-quote"></p></div></div><script src="content/loading.js"></script><script src="content/changelog.js"></script><script src="brand.js"></script><script src="precache.js"></script><script src="preload.js"></script><main id="game-shell" aria-label="Konoha Fighting, game fighting parodi">
|
<body><div id="boot" role="status" aria-live="polite"><div class="boot-card"><strong class="boot-logo">KONOHA<br><em>FIGHTING</em></strong><div class="boot-bar"><i></i></div><span class="boot-status">Menyiapkan…</span><p class="boot-quote"></p></div></div><script src="content/loading.js"></script><script src="content/changelog.js"></script><script src="brand.js"></script><script src="precache.js"></script><script src="preload.js"></script><main id="game-shell" aria-label="Konoha Fighting, game fighting parodi">
|
||||||
<canvas id="arena" tabindex="0" aria-label="Arena pertarungan. WASD bergerak, Spasi menyerang, I O P untuk skill."></canvas><div class="vignette" aria-hidden="true"></div>
|
<canvas id="arena" tabindex="0" aria-label="Arena pertarungan. WASD bergerak, Spasi menyerang, I O P untuk skill."></canvas><div class="vignette" aria-hidden="true"></div>
|
||||||
<header class="combat-header">
|
<header class="combat-header">
|
||||||
|
|
@ -24,6 +24,6 @@
|
||||||
<div id="load-state" class="load-state"><span class="loading-glyph">✦</span><strong>KONOHA FIGHTING</strong><p class="load-quote"></p><span id="load-message">Menyiapkan arena…</span></div>
|
<div id="load-state" class="load-state"><span class="loading-glyph">✦</span><strong>KONOHA FIGHTING</strong><p class="load-quote"></p><span id="load-message">Menyiapkan arena…</span></div>
|
||||||
<dialog id="settings-panel"><div class="dialog-kicker">KONOHA FIGHTING / SETTINGS</div><h2>Pengaturan arena</h2><p>Atur audio, tampilan, dan kontrol permainan.</p><label class="setting-row training-only"><span>Karakter pemain<small>Mengganti karakter memulai ulang latihan</small></span><select id="character-select"><option value="browo">BRO WO · Human</option><option value="gufron">GUFRON · Human</option><option value="soloman">SOLO MAN · Human</option><option value="anice">A NICE · Human</option><option value="ethanolman">ETHANOL MAN · Human</option><option value="sonofsolo">SON OF SOLO MAN · Human</option><option value="megachan">MEGA CHAN · Human</option></select></label><label class="setting-row training-only"><span>Lawan aktif<small>Aktifkan serangan CPU saat latihan</small></span><input type="checkbox" id="ai-toggle" checked></label><label class="setting-row"><span>Visualisasi hitbox<small>Area tubuh dan jangkauan serangan</small></span><input type="checkbox" id="hitbox-toggle"></label><label class="setting-row"><span>Audio aktif<small>Suara karakter, benturan, dan skill</small></span><input type="checkbox" id="sound-toggle" checked></label><label class="setting-row"><span>Musik latar<small>Midday Showdown, diputar berulang di menu dan arena</small></span><input type="checkbox" id="music-toggle" checked></label><label class="volume-row"><span>Musik</span><input type="range" id="music-slider" min="0" max="100" value="5" aria-label="Volume musik"><output id="music-value">5%</output></label><label class="setting-row"><span>Animasi latar menu<small>Gerak pelan petarung di latar main menu</small></span><input type="checkbox" id="menu-video-toggle" checked></label><label class="volume-row"><span>Suara</span><input type="range" id="volume-slider" min="0" max="100" value="45" aria-label="Volume audio"><output id="volume-value">45%</output></label><div class="dialog-buttons"><button id="sound-test">Tes audio</button><button id="fullscreen-btn">Layar penuh</button></div><details class="controls-help"><summary>Kontrol keyboard</summary><p>A / D · Gerak 2× A / D · Lari<br>W · Lompat (2× W = lompat ganda) S · Menunduk<br>SPACE · Kombo 3 pukulan ↓ ↓ + SPACE · Uppercut ↓ → + SPACE · Tinju maju<br>I / O · Skill P · Ultimate K · Jurus toko<br>L · Blok (ketuk = parry) J · Lempar<br>Esc · Jeda, berisi daftar gerakan karakter R · Mulai ulang<br>Dua pemain (P2): ← → · Gerak ↑ · Lompat N · Serang M / , / . · Skill 1 / 2 / Ultimate<br>Meter di bawah HP: bar ultimate (SIAP = bisa dipakai), saat mode aktif jadi bar waktu mode.</p></details><button class="primary-button" id="close-settings">Kembali ke arena <span>↗</span></button></dialog>
|
<dialog id="settings-panel"><div class="dialog-kicker">KONOHA FIGHTING / SETTINGS</div><h2>Pengaturan arena</h2><p>Atur audio, tampilan, dan kontrol permainan.</p><label class="setting-row training-only"><span>Karakter pemain<small>Mengganti karakter memulai ulang latihan</small></span><select id="character-select"><option value="browo">BRO WO · Human</option><option value="gufron">GUFRON · Human</option><option value="soloman">SOLO MAN · Human</option><option value="anice">A NICE · Human</option><option value="ethanolman">ETHANOL MAN · Human</option><option value="sonofsolo">SON OF SOLO MAN · Human</option><option value="megachan">MEGA CHAN · Human</option></select></label><label class="setting-row training-only"><span>Lawan aktif<small>Aktifkan serangan CPU saat latihan</small></span><input type="checkbox" id="ai-toggle" checked></label><label class="setting-row"><span>Visualisasi hitbox<small>Area tubuh dan jangkauan serangan</small></span><input type="checkbox" id="hitbox-toggle"></label><label class="setting-row"><span>Audio aktif<small>Suara karakter, benturan, dan skill</small></span><input type="checkbox" id="sound-toggle" checked></label><label class="setting-row"><span>Musik latar<small>Midday Showdown, diputar berulang di menu dan arena</small></span><input type="checkbox" id="music-toggle" checked></label><label class="volume-row"><span>Musik</span><input type="range" id="music-slider" min="0" max="100" value="5" aria-label="Volume musik"><output id="music-value">5%</output></label><label class="setting-row"><span>Animasi latar menu<small>Gerak pelan petarung di latar main menu</small></span><input type="checkbox" id="menu-video-toggle" checked></label><label class="volume-row"><span>Suara</span><input type="range" id="volume-slider" min="0" max="100" value="45" aria-label="Volume audio"><output id="volume-value">45%</output></label><div class="dialog-buttons"><button id="sound-test">Tes audio</button><button id="fullscreen-btn">Layar penuh</button></div><details class="controls-help"><summary>Kontrol keyboard</summary><p>A / D · Gerak 2× A / D · Lari<br>W · Lompat (2× W = lompat ganda) S · Menunduk<br>SPACE · Kombo 3 pukulan ↓ ↓ + SPACE · Uppercut ↓ → + SPACE · Tinju maju<br>I / O · Skill P · Ultimate K · Jurus toko<br>L · Blok (ketuk = parry) J · Lempar<br>Esc · Jeda, berisi daftar gerakan karakter R · Mulai ulang<br>Dua pemain (P2): ← → · Gerak ↑ · Lompat N · Serang M / , / . · Skill 1 / 2 / Ultimate<br>Meter di bawah HP: bar ultimate (SIAP = bisa dipakai), saat mode aktif jadi bar waktu mode.</p></details><button class="primary-button" id="close-settings">Kembali ke arena <span>↗</span></button></dialog>
|
||||||
<dialog id="pause-panel" aria-labelledby="pause-title"><div class="pause-main"><div class="dialog-kicker">KONOHA FIGHTING / JEDA</div><h2 id="pause-title">JEDA</h2><p>Pertarungan dijeda. Siap melanjutkan?</p><button class="primary-button" id="resume-btn">Lanjut bertarung <span>↗</span></button><button class="text-button" id="restart-btn">Mulai ulang</button><button class="text-button" id="back-menu">Kembali ke main menu</button></div><details class="move-list" id="move-list"><summary>Daftar gerakan</summary><div class="move-body" id="move-list-body"></div></details></dialog>
|
<dialog id="pause-panel" aria-labelledby="pause-title"><div class="pause-main"><div class="dialog-kicker">KONOHA FIGHTING / JEDA</div><h2 id="pause-title">JEDA</h2><p>Pertarungan dijeda. Siap melanjutkan?</p><button class="primary-button" id="resume-btn">Lanjut bertarung <span>↗</span></button><button class="text-button" id="restart-btn">Mulai ulang</button><button class="text-button" id="back-menu">Kembali ke main menu</button></div><details class="move-list" id="move-list"><summary>Daftar gerakan</summary><div class="move-body" id="move-list-body"></div></details></dialog>
|
||||||
</main><section id="front-menu" aria-label="Main menu Konoha Fighting"></section><script src="assets/mecha/manifest.js"></script><script src="assets/stage-config.js"></script><script src="assets/ui/drone-config.js"></script><script src="assets/fenr/human/manifest.js"></script><script src="assets/fenr/wolf/manifest.js"></script><script src="fenr.js"></script><script src="assets/browo/manifest.js"></script><script src="browo.js"></script><script src="assets/megachan/manifest.js"></script><script src="assets/sonofsolo/manifest.js"></script><script src="assets/ethanolman/manifest.js"></script><script src="assets/anice/manifest.js"></script><script src="assets/soloman/manifest.js"></script><script src="assets/gufron/manifest.js"></script><script src="assets/audio/announcer/manifest.js"></script><script src="announcer.js"></script><script src="match.js"></script><script src="feel.js"></script><script src="content/quotes.js"></script><script src="vs.js"></script><script>window.FRONTEND_ENABLED=true;</script><script src="game.js"></script><script src="touch.js"></script><script src="menu.js"></script><script src="meta.js"></script></body></html>
|
</main><section id="front-menu" aria-label="Main menu Konoha Fighting"></section><script src="assets/mecha/manifest.js"></script><script src="assets/stage-config.js"></script><script src="assets/ui/drone-config.js"></script><script src="assets/fenr/human/manifest.js"></script><script src="assets/fenr/wolf/manifest.js"></script><script src="fenr.js"></script><script src="assets/browo/manifest.js"></script><script src="browo.js"></script><script src="assets/megachan/manifest.js"></script><script src="assets/sonofsolo/manifest.js"></script><script src="assets/ethanolman/manifest.js"></script><script src="assets/anice/manifest.js"></script><script src="assets/soloman/manifest.js"></script><script src="assets/gufron/manifest.js"></script><script src="assets/audio/announcer/manifest.js"></script><script src="announcer.js"></script><script src="match.js"></script><script src="feel.js"></script><script src="content/quotes.js"></script><script src="vs.js"></script><script>window.FRONTEND_ENABLED=true;</script><script src="game.js"></script><script src="touch.js"></script><script src="content/mystery.js"></script><script src="mystery.js"></script><script src="menu.js"></script><script src="meta.js"></script></body></html>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
10
menu.js
10
menu.js
|
|
@ -25,6 +25,8 @@
|
||||||
// shown and then come from the browser cache; holding all of them decoded would waste a phone's image memory.
|
// shown and then come from the browser cache; holding all of them decoded would waste a phone's image memory.
|
||||||
const warm=opened.map(id=>info(id).portrait).filter(Boolean).map(src=>{const im=new Image();im.decoding='async';im.src=src;return im;});
|
const warm=opened.map(id=>info(id).portrait).filter(Boolean).map(src=>{const im=new Image();im.decoding='async';im.src=src;return im;});
|
||||||
const roster=[...opened,...Array.from({length:Math.max(0,12-opened.length)},(_,i)=>'locked-'+i)];
|
const roster=[...opened,...Array.from({length:Math.max(0,12-opened.length)},(_,i)=>'locked-'+i)];
|
||||||
|
// Petak terkunci jadi slot misteri yang hidup (mystery.js, data di content/mystery.js): petunjuk bertahap, slot tetap tidak bisa dipilih.
|
||||||
|
const lockedSlots=roster.length-opened.length;
|
||||||
// lockIn: the pending step change while a confirmed fighter flashes 3 times (PICK_FLASH s each); input waits for it.
|
// lockIn: the pending step change while a confirmed fighter flashes 3 times (PICK_FLASH s each); input waits for it.
|
||||||
const PICK_FLASH=.3;let lockIn=null,lockTicks=[];
|
const PICK_FLASH=.3;let lockIn=null,lockTicks=[];
|
||||||
const ids=Object.keys(fighters);
|
const ids=Object.keys(fighters);
|
||||||
|
|
@ -53,7 +55,7 @@
|
||||||
const brand='<span class="wordmark">KONOHA<span>FIGHTING</span><i>✦</i></span>',versionText=()=>window.Brand?.version?'VERSI '+window.Brand.version:'';
|
const brand='<span class="wordmark">KONOHA<span>FIGHTING</span><i>✦</i></span>',versionText=()=>window.Brand?.version?'VERSI '+window.Brand.version:'';
|
||||||
const footer=()=>'<footer class="menu-footer"><span><kbd>W A S D</kbd> / <kbd>↑ ↓ ← →</kbd> SELECT <kbd>ENTER</kbd> CONFIRM <kbd>ESC</kbd> BACK</span><span>LOCAL PLAY <i></i> '+(versionText()||'BUILD 02')+'</span></footer>';
|
const footer=()=>'<footer class="menu-footer"><span><kbd>W A S D</kbd> / <kbd>↑ ↓ ← →</kbd> SELECT <kbd>ENTER</kbd> CONFIRM <kbd>ESC</kbd> BACK</span><span>LOCAL PLAY <i></i> '+(versionText()||'BUILD 02')+'</span></footer>';
|
||||||
function navHeader(title,subtitle){return `<header class="select-header"><button class="menu-back" data-cmd="back" aria-label="Kembali">← <span>BACK</span></button><div><span class="eyebrow">${subtitle}</span><h1>${title}</h1></div>${brand}</header>`;}
|
function navHeader(title,subtitle){return `<header class="select-header"><button class="menu-back" data-cmd="back" aria-label="Kembali">← <span>BACK</span></button><div><span class="eyebrow">${subtitle}</span><h1>${title}</h1></div>${brand}</header>`;}
|
||||||
function fighterPanel(id,side){const f=info(id);return `<div class="fighter-plinth ${side} ${f?'':'unknown'} ${soon(id)?'soon':''}"><div class="plinth-light"></div><span class="side-tag">${side==='player'?'P1 / YOUR FIGHTER':'CPU / YOUR RIVAL'}</span><img class="selection-art" src="${f?.art||'assets/menu/locked.svg'}" alt="${f?.name||'Karakter belum dipilih'}"><div class="fighter-copy"><span>${f?.tag||'AWAITING CHALLENGER'}</span><h2>${f?.name||'???'}</h2><p>${f?.race||'ROSTER EXPANDING'}</p><small>${f?.detail||'A new challenger is on the way.'}</small></div></div>`;}
|
function fighterPanel(id,side){const ms=window.Mystery?.panel(id,side,lockedSlots);if(ms)return ms;const f=info(id);return `<div class="fighter-plinth ${side} ${f?'':'unknown'} ${soon(id)?'soon':''}"><div class="plinth-light"></div><span class="side-tag">${side==='player'?'P1 / YOUR FIGHTER':'CPU / YOUR RIVAL'}</span><img class="selection-art" src="${f?.art||'assets/menu/locked.svg'}" alt="${f?.name||'Karakter belum dipilih'}"><div class="fighter-copy"><span>${f?.tag||'AWAITING CHALLENGER'}</span><h2>${f?.name||'???'}</h2><p>${f?.race||'ROSTER EXPANDING'}</p><small>${f?.detail||'A new challenger is on the way.'}</small></div></div>`;}
|
||||||
function render(){
|
function render(){
|
||||||
if(lockIn){clearTimeout(lockIn);lockIn=null;}for(const t of lockTicks)clearTimeout(t);lockTicks=[];
|
if(lockIn){clearTimeout(lockIn);lockIn=null;}for(const t of lockTicks)clearTimeout(t);lockTicks=[];
|
||||||
root.dataset.screen=screen;root.dataset.step=String(step);root.dataset.mode=mode;
|
root.dataset.screen=screen;root.dataset.step=String(step);root.dataset.mode=mode;
|
||||||
|
|
@ -72,16 +74,16 @@
|
||||||
} else if(screen==='result'){
|
} else if(screen==='result'){
|
||||||
const won=result.winner==='player',winner=won?result.player:result.enemy;root.innerHTML=`<div class="result-bg"></div>${navHeader('MATCH COMPLETE',MatchRules.stages[result.stage].name.toUpperCase())}<div class="result-layout"><img class="result-fighter" src="${fighters[winner].art}" alt="${fighters[winner].name}"><div class="result-copy"><span class="eyebrow">${won?'PLAYER 1':'COMPUTER'} TAKES THE MATCH</span><h1>${won?'VICTORY':'DEFEAT'}</h1><p>${fighters[winner].name} WINS</p><div class="final-score"><span>${result.playerWins}</span><i>—</i><span>${result.enemyWins}</span></div><small>${fighters[result.player].name} vs ${fighters[result.enemy].name} · ${MatchRules.difficulties[result.level].label}</small><button class="menu-primary" data-cmd="rematch">REMATCH <b>↗</b></button><button class="result-secondary" data-cmd="reselect">CHARACTER SELECT</button><button class="result-secondary" data-cmd="home">MAIN MENU</button></div></div>`;
|
const won=result.winner==='player',winner=won?result.player:result.enemy;root.innerHTML=`<div class="result-bg"></div>${navHeader('MATCH COMPLETE',MatchRules.stages[result.stage].name.toUpperCase())}<div class="result-layout"><img class="result-fighter" src="${fighters[winner].art}" alt="${fighters[winner].name}"><div class="result-copy"><span class="eyebrow">${won?'PLAYER 1':'COMPUTER'} TAKES THE MATCH</span><h1>${won?'VICTORY':'DEFEAT'}</h1><p>${fighters[winner].name} WINS</p><div class="final-score"><span>${result.playerWins}</span><i>—</i><span>${result.enemyWins}</span></div><small>${fighters[result.player].name} vs ${fighters[result.enemy].name} · ${MatchRules.difficulties[result.level].label}</small><button class="menu-primary" data-cmd="rematch">REMATCH <b>↗</b></button><button class="result-secondary" data-cmd="reselect">CHARACTER SELECT</button><button class="result-secondary" data-cmd="home">MAIN MENU</button></div></div>`;
|
||||||
}
|
}
|
||||||
bind();attachBackground();window.Brand?.decorate(root,screen);
|
bind();attachBackground();window.Brand?.decorate(root,screen);window.Mystery?.decorate(root,screen,hover,lockedSlots);
|
||||||
}
|
}
|
||||||
function updatePreview(play=true){
|
function updatePreview(play=true){
|
||||||
root.querySelectorAll('[data-fighter]').forEach(b=>b.classList.toggle('highlight',b.dataset.fighter===hover));
|
root.querySelectorAll('[data-fighter]').forEach(b=>b.classList.toggle('highlight',b.dataset.fighter===hover));
|
||||||
const panel=root.querySelector(step===0?'#player-preview':'#enemy-preview');if(panel)panel.innerHTML=fighterPanel(hover,step===0?'player':'enemy');
|
const panel=root.querySelector(step===0?'#player-preview':'#enemy-preview');if(panel)panel.innerHTML=fighterPanel(hover,step===0?'player':'enemy');
|
||||||
const f=info(hover),moves=root.querySelector('#fighter-moves');if(moves)moves.innerHTML=f?`<span>${f.style}</span>${soon(hover)?'':`<b>${f.ultimate}</b>`}`:'<span>NEW CHALLENGER</span><b>COMING LATER</b>';
|
const f=info(hover),moves=root.querySelector('#fighter-moves');if(moves)moves.innerHTML=f?`<span>${f.style}</span>${soon(hover)?'':`<b>${f.ultimate}</b>`}`:'<span>NEW CHALLENGER</span><b>COMING LATER</b>';
|
||||||
const confirm=root.querySelector('.confirm-fighter');if(confirm)confirm.setAttribute('aria-disabled',String(!fighters[hover]));if(play)sfx('move');
|
const confirm=root.querySelector('.confirm-fighter');if(confirm)confirm.setAttribute('aria-disabled',String(!fighters[hover]));window.Mystery?.preview(root,hover,lockedSlots);if(play)sfx('move');
|
||||||
}
|
}
|
||||||
function focusFighter(id){if(hover===id||lockIn)return;hover=id;updatePreview();}
|
function focusFighter(id){if(hover===id||lockIn)return;hover=id;updatePreview();}
|
||||||
function confirm(){if(lockIn)return;if(!fighters[hover]){sfx('locked',true);root.querySelector('#roster-notice').textContent=soon(hover)?showcase[hover].name+' belum bisa dimainkan.':'Karakter ini belum terbuka.';return;}
|
function confirm(){if(lockIn)return;if(!fighters[hover]){sfx('locked',true);root.querySelector('#roster-notice').textContent=soon(hover)?showcase[hover].name+' belum bisa dimainkan.':(window.Mystery?.notice(hover,lockedSlots)||'Karakter ini belum terbuka.');return;}
|
||||||
// The pick flashes 3 times (tile and big art) with a lock-in chime before the next step, so the screen does not jump.
|
// The pick flashes 3 times (tile and big art) with a lock-in chime before the next step, so the screen does not jump.
|
||||||
sfx('pick',true);game.announceSelection(hover);root.dataset.picking='true';
|
sfx('pick',true);game.announceSelection(hover);root.dataset.picking='true';
|
||||||
root.querySelector(`[data-fighter="${hover}"]`)?.classList.add('picked');root.querySelector(step===0?'#player-preview':'#enemy-preview')?.classList.add('picked');
|
root.querySelector(`[data-fighter="${hover}"]`)?.classList.add('picked');root.querySelector(step===0?'#player-preview':'#enemy-preview')?.classList.add('picked');
|
||||||
|
|
|
||||||
3
meta.js
3
meta.js
|
|
@ -53,6 +53,7 @@
|
||||||
mbg: ['TRUK LEWAT', 'Panggil truk katering.'],
|
mbg: ['TRUK LEWAT', 'Panggil truk katering.'],
|
||||||
esemka: ['KONVOI', 'Panggil konvoi.'],
|
esemka: ['KONVOI', 'Panggil konvoi.'],
|
||||||
penasaran: ['PENASARAN', 'Ketuk slot tanda tanya berulang kali.'],
|
penasaran: ['PENASARAN', 'Ketuk slot tanda tanya berulang kali.'],
|
||||||
|
penasaran_berat: ['PENASARAN BERAT', 'Buka semua petunjuk slot misteri.'],
|
||||||
logo: ['TELITI', 'Temukan rahasia di judul.'],
|
logo: ['TELITI', 'Temukan rahasia di judul.'],
|
||||||
parry: ['TEPAT WAKTU', 'Lakukan parry yang berhasil.'],
|
parry: ['TEPAT WAKTU', 'Lakukan parry yang berhasil.'],
|
||||||
arcade_clear: ['TAMAT', 'Selesaikan mode Arcade.'],
|
arcade_clear: ['TAMAT', 'Selesaikan mode Arcade.'],
|
||||||
|
|
@ -219,7 +220,7 @@
|
||||||
}
|
}
|
||||||
function renderBook() {
|
function renderBook() {
|
||||||
const got = Object.keys(ACH).filter(k => save.ach[k]).length, st = save.stats;
|
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 `<li class="${ok ? 'ok' : ''}"><b>${ok ? '★' : '☆'} ${hidden ? '???' : n}</b><span>${hidden ? 'Pencapaian rahasia.' : d}</span></li>`; }).join('');
|
const list = Object.entries(ACH).map(([k, [n, d]]) => { const ok = !!save.ach[k], hidden = !ok && ['konami', 'syududu', 'mbg', 'esemka', 'penasaran', 'penasaran_berat', 'logo'].includes(k); return `<li class="${ok ? 'ok' : ''}"><b>${ok ? '★' : '☆'} ${hidden ? '???' : n}</b><span>${hidden ? 'Pencapaian rahasia.' : d}</span></li>`; }).join('');
|
||||||
return `<h3>Statistik</h3><p class="stats">Menang <b>${st.wins}</b> · Kalah <b>${st.losses}</b> · Bersih <b>${st.perfects}</b> · Kombo terbaik <b>${st.bestCombo}</b> · Total koin <b>${save.earned}</b></p><h3>Pencapaian ${got}/${Object.keys(ACH).length}</h3><ul class="ach">${list}</ul><h3>Suara</h3><p><button class="pill ${save.voice ? 'on' : ''}" data-voice>TERIAKAN JURUS: ${save.voice ? 'AKTIF' : 'MATI'}</button> <small class="meta-hint">Memakai suara bawaan browser.</small></p><h3>Simpanan</h3><p><button class="pill" data-export>SALIN KODE SIMPANAN</button> <button class="pill" data-import>TEMPEL KODE</button> <button class="pill danger" data-reset>HAPUS SEMUA</button></p>`;
|
return `<h3>Statistik</h3><p class="stats">Menang <b>${st.wins}</b> · Kalah <b>${st.losses}</b> · Bersih <b>${st.perfects}</b> · Kombo terbaik <b>${st.bestCombo}</b> · Total koin <b>${save.earned}</b></p><h3>Pencapaian ${got}/${Object.keys(ACH).length}</h3><ul class="ach">${list}</ul><h3>Suara</h3><p><button class="pill ${save.voice ? 'on' : ''}" data-voice>TERIAKAN JURUS: ${save.voice ? 'AKTIF' : 'MATI'}</button> <small class="meta-hint">Memakai suara bawaan browser.</small></p><h3>Simpanan</h3><p><button class="pill" data-export>SALIN KODE SIMPANAN</button> <button class="pill" data-import>TEMPEL KODE</button> <button class="pill danger" data-reset>HAPUS SEMUA</button></p>`;
|
||||||
}
|
}
|
||||||
function onClick(e) {
|
function onClick(e) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
/* Slot misteri yang hidup (RENCANA 3.7): petak roster 'locked-N' dan panel petunjuk di layar pilih karakter.
|
||||||
|
Dihias oleh mystery.js. Tanpa gambar baru: siluet adalah SVG sebaris; blur, garis tepi, dan tanda tanya mengikuti --ms-p
|
||||||
|
(0 sampai 1 = tahap terbuka / jumlah tahap), warna aksen --ms-c per slot. Ukuran petak roster tidak berubah (img tetap di
|
||||||
|
alur, hanya disembunyikan), semua hiasan memakai posisi absolut. Aturan dibatasi ke #front-menu agar menang atas menu.css. */
|
||||||
|
|
||||||
|
/* ---- petak roster ---- */
|
||||||
|
#front-menu .roster-tile.ms-tile{--ms-c:#7fe0ff;--ms-a:#7fe0ff44;--ms-b:#7fe0ff00;--ms-p:0;--ms-blur:calc((1 - var(--ms-p)) * 3.4px + .15px)}
|
||||||
|
#front-menu .roster-tile.ms-tile>b{background:#0b1d27d9;color:var(--ms-c);border:1px solid var(--ms-c);padding:1px 4px;letter-spacing:.5px}
|
||||||
|
#front-menu .roster-tile.ms-tile:not(.ms-on)>b{opacity:.7}
|
||||||
|
#front-menu .roster-tile.ms-done>b{background:var(--ms-c);color:#09202b}
|
||||||
|
#front-menu .roster-tile.ms-on{filter:none;background:radial-gradient(ellipse at 50% 44%,#2e5361 0,#13262f 64%,#0b1820 100%)}
|
||||||
|
#front-menu .roster-tile.ms-on:not(.highlight){border-color:#4d7280}
|
||||||
|
#front-menu .roster-tile.ms-on>img{visibility:hidden}
|
||||||
|
#front-menu .roster-tile.ms-on>span{color:var(--ms-c);letter-spacing:1.5px}
|
||||||
|
#front-menu .roster-tile .ms-sil,#front-menu .roster-tile .ms-q{display:none}
|
||||||
|
#front-menu .roster-tile.ms-on .ms-sil{display:block;position:absolute;inset:0;pointer-events:none;transform:skewX(4deg)}
|
||||||
|
#front-menu .roster-tile .ms-svg{display:block;width:100%;height:100%;overflow:visible;filter:blur(var(--ms-blur)) drop-shadow(0 0 4px var(--ms-c))}
|
||||||
|
#front-menu .roster-tile.ms-on .ms-q{display:block;position:absolute;top:19%;right:4px;font:700 italic 24px/1 Rajdhani,Arial,sans-serif;color:var(--ms-c);text-shadow:0 0 8px var(--ms-c),0 2px 0 #06111a;opacity:calc(1 - var(--ms-p) * .5);pointer-events:none;transform:skewX(4deg)}
|
||||||
|
#front-menu .roster-tile .ms-new{position:absolute;top:4px;right:5px;width:9px;height:9px;border-radius:50%;background:#ffd66b;box-shadow:0 0 0 0 #ffd66bb3;animation:ms-ping 1.5s ease-out infinite;pointer-events:none}
|
||||||
|
#front-menu .roster-tile.ms-on.highlight .ms-svg{animation:ms-bob 1.8s ease-in-out infinite alternate}
|
||||||
|
@keyframes ms-ping{to{box-shadow:0 0 0 7px #ffd66b00}}
|
||||||
|
@keyframes ms-bob{from{transform:translateY(0)}to{transform:translateY(-3%)}}
|
||||||
|
|
||||||
|
/* ---- siluet (dipakai petak dan panel) ---- */
|
||||||
|
#front-menu .ms-body{fill:#050d13;stroke:var(--ms-c);stroke-width:2.2;stroke-linejoin:round;stroke-opacity:calc(.2 + var(--ms-p) * .6)}
|
||||||
|
#front-menu .ms-acc{color:var(--ms-c);fill:currentColor;opacity:0}
|
||||||
|
#front-menu .ms-g .ms-acc{opacity:.4}
|
||||||
|
#front-menu .ms-f .ms-acc{opacity:.95}
|
||||||
|
|
||||||
|
/* ---- panel pratinjau slot yang disorot: siluet di atas, kartu petunjuk di bawah (flex kolom agar tidak saling menimpa) ---- */
|
||||||
|
#front-menu .fighter-plinth.mystery{--ms-c:#7fe0ff;--ms-a:#7fe0ff44;--ms-b:#7fe0ff00;--ms-p:0;--ms-blur:calc((1 - var(--ms-p)) * 9px + .2px);display:flex;flex-direction:column;padding-top:calc(7vh + 6px)}
|
||||||
|
#front-menu .mystery .ms-art{position:relative;flex:1 1 0;min-height:56px;margin:0 8%;animation:ms-in .25s ease-out both}
|
||||||
|
#front-menu .mystery .ms-art:before{content:'';position:absolute;left:0;right:0;bottom:0;top:6%;background:radial-gradient(ellipse at 50% 74%,var(--ms-a),var(--ms-b) 68%)}
|
||||||
|
#front-menu .mystery .ms-art>svg,#front-menu .mystery .ms-art>img{position:absolute;left:0;top:0;width:100%;height:100%}
|
||||||
|
#front-menu .mystery .ms-art>img{object-fit:contain;object-position:50% 100%;opacity:.5}
|
||||||
|
#front-menu .mystery .ms-art>svg{overflow:visible;filter:blur(var(--ms-blur)) drop-shadow(0 0 10px var(--ms-c))}
|
||||||
|
#front-menu .mystery .ms-glyph{position:absolute;right:2%;top:0;font:700 italic clamp(34px,7vh,64px)/1 Rajdhani,Arial,sans-serif;color:var(--ms-c);text-shadow:0 0 14px var(--ms-c),0 3px 0 #06111a;opacity:calc(1 - var(--ms-p) * .45);animation:ms-pulse 1.4s ease-in-out infinite alternate}
|
||||||
|
@keyframes ms-in{from{opacity:.25}}
|
||||||
|
@keyframes ms-pulse{to{transform:translateY(-4px) scale(1.06)}}
|
||||||
|
#front-menu .mystery .ms-card{position:relative;flex:0 0 auto;max-height:66%;overflow:auto;margin:6px 5% 9px 11%;padding:7px 8px 7px 11px;border-left:3px solid var(--ms-c);background:linear-gradient(90deg,#08151ecc,#08151e66);text-align:left;scrollbar-width:thin}
|
||||||
|
#front-menu .enemy.mystery .ms-card{margin:6px 11% 9px 5%}
|
||||||
|
#front-menu .mystery .ms-head{display:flex;justify-content:space-between;align-items:center;gap:8px;font-size:clamp(9px,1.5vh,11px);font-weight:700;letter-spacing:2px;color:var(--ms-c)}
|
||||||
|
#front-menu .mystery .ms-dots{display:flex;gap:5px;flex:none}
|
||||||
|
#front-menu .mystery .ms-dots i{width:8px;height:8px;border:1px solid var(--ms-c);transform:rotate(45deg)}
|
||||||
|
#front-menu .mystery .ms-dots i.on{background:var(--ms-c)}
|
||||||
|
#front-menu .mystery .ms-title{margin:2px 0 4px;font-size:clamp(24px,4.4vh,46px);font-style:italic;line-height:.9;letter-spacing:2px}
|
||||||
|
#front-menu .mystery .ms-clues{list-style:none;margin:0;padding:0}
|
||||||
|
#front-menu .mystery .ms-clues li{margin:0 0 4px;font-size:clamp(10px,1.75vh,13.5px);line-height:1.25;color:#d6e2de}
|
||||||
|
#front-menu .mystery .ms-clues li b{display:inline-block;margin-right:6px;font-size:.72em;letter-spacing:1.5px;color:var(--ms-c)}
|
||||||
|
#front-menu .mystery .ms-clues .ms-empty{color:#8fa9ad;font-style:italic}
|
||||||
|
#front-menu .mystery .ms-next{margin-top:6px;padding-top:6px;border-top:1px solid #ffffff1f}
|
||||||
|
#front-menu .mystery .ms-next small{display:block;font-size:clamp(8px,1.3vh,10px);letter-spacing:2px;color:#9fb8bc}
|
||||||
|
#front-menu .mystery .ms-next strong{display:block;font-size:clamp(11px,1.9vh,15px);letter-spacing:.5px;color:#f1dfae}
|
||||||
|
#front-menu .mystery .ms-next em{display:block;font-style:normal;font-size:clamp(9px,1.5vh,11.5px);color:#8fa9ad}
|
||||||
|
#front-menu .mystery .ms-prog{display:flex;align-items:center;gap:8px;margin-top:4px}
|
||||||
|
#front-menu .mystery .ms-prog i{display:block;flex:1;height:5px;background:#ffffff1f;border:1px solid #ffffff24}
|
||||||
|
#front-menu .mystery .ms-prog u{display:block;height:100%;background:var(--ms-c);text-decoration:none}
|
||||||
|
#front-menu .mystery .ms-prog b{font-size:11px;letter-spacing:1px;color:var(--ms-c)}
|
||||||
|
|
||||||
|
/* ---- teks bawah roster untuk slot misteri: satu baris, tidak boleh membungkus (tinggi #fighter-moves tetap) ---- */
|
||||||
|
#front-menu .fighter-moves.ms-moves>span,#front-menu .fighter-moves.ms-moves b{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
#front-menu .fighter-moves.ms-moves b{font-size:clamp(11px,1.9vh,14px);letter-spacing:1px}
|
||||||
|
#front-menu .roster-notice{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
|
||||||
|
/* ---- layar pendek (HP landscape) dan sempit: panel dipadatkan, kartu boleh digulir di dalam panel ---- */
|
||||||
|
@media(max-height:420px){
|
||||||
|
#front-menu .fighter-plinth.mystery{padding-top:16px}
|
||||||
|
#front-menu .mystery .ms-art{min-height:40px;margin:0 4%}
|
||||||
|
#front-menu .mystery .ms-title,#front-menu .mystery .ms-next em{display:none}
|
||||||
|
#front-menu .mystery .ms-card{margin:3px 3% 2px 5%;padding:4px 6px 4px 8px;max-height:72%}
|
||||||
|
#front-menu .enemy.mystery .ms-card{margin:3px 5% 2px 3%}
|
||||||
|
#front-menu .mystery .ms-clues li{margin-bottom:2px;line-height:1.18}
|
||||||
|
#front-menu .mystery .ms-next{margin-top:3px;padding-top:3px}
|
||||||
|
#front-menu .mystery .ms-prog{margin-top:2px}
|
||||||
|
#front-menu .roster-tile.ms-on .ms-q{font-size:19px}
|
||||||
|
}
|
||||||
|
@media(max-width:550px){
|
||||||
|
#front-menu .fighter-plinth.mystery{padding-top:20px}
|
||||||
|
#front-menu .mystery .ms-art,#front-menu .mystery .ms-title,#front-menu .mystery .ms-next em{display:none}
|
||||||
|
#front-menu .mystery .ms-card,#front-menu .enemy.mystery .ms-card{flex:1 1 0;min-height:0;max-height:none;margin:4px 0 0}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,179 @@
|
||||||
|
/* Slot misteri yang hidup (RENCANA 3.7). Lima petak 'locked-N' di layar pilih karakter (menu.js) menjadi sistem petunjuk bertahap.
|
||||||
|
Data: content/mystery.js (window.MYSTERY). Kemajuan dibaca langsung dari Meta.save. Gaya: mystery.css. Tanpa gambar baru:
|
||||||
|
siluet digambar sebagai SVG sebaris (bentuk tubuh disalin dari assets/menu/locked.svg), makin tajam tiap petunjuk terbuka,
|
||||||
|
dan sebuah benda khas muncul samar lalu jelas di dua tahap terakhir.
|
||||||
|
menu.js memanggil: Mystery.panel (panel pratinjau slot yang disorot), decorate (petak roster, tiap selesai render), preview
|
||||||
|
(teks di bawah roster saat sorotan berpindah) dan notice (pesan saat slot terkunci ditekan). Slot tetap tidak bisa dipilih.
|
||||||
|
Fungsi publik tidak pernah melempar galat; bila gagal, layar kembali ke tampilan slot terkunci yang lama.
|
||||||
|
Semua petunjuk terbuka = pencapaian PENASARAN BERAT (meta.js). ?mystery=N (atau all) di URL hanya mempratinjau tahap N pada
|
||||||
|
semua slot, tanpa menyimpan apa pun dan tanpa membuka pencapaian. Petunjuk yang sudah dilihat diingat di
|
||||||
|
localStorage['konoha.mystery.seen'] (hanya untuk titik BARU dan toast; bukan bagian Meta.save). */
|
||||||
|
(() => {
|
||||||
|
'use strict';
|
||||||
|
const SEEN_KEY = 'konoha.mystery.seen', ACH_ALL = 'penasaran_berat', FALLBACK_COLOR = '#7fe0ff';
|
||||||
|
const PREVIEW = (() => { try { const v = new URLSearchParams(location.search).get('mystery'); if (v === 'all') return 99; const n = v === null || v === '' ? -1 : parseInt(v, 10); return n >= 0 ? n : null; } catch (_) { return null; } })();
|
||||||
|
const num = v => (typeof v === 'number' && isFinite(v) ? v : 0);
|
||||||
|
const len = a => (Array.isArray(a) ? a.length : 0);
|
||||||
|
const count = (o, ok) => (o && typeof o === 'object' ? Object.keys(o).filter(k => o[k] && ok(k)).length : 0);
|
||||||
|
const esc = t => String(t).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||||
|
const save = () => window.Meta?.save || {};
|
||||||
|
const enabled = () => Array.isArray(window.MYSTERY) && window.MYSTERY.length > 0;
|
||||||
|
const slotIndex = id => { const m = /^locked-(\d+)$/.exec(id || ''); return m ? +m[1] : -1; };
|
||||||
|
|
||||||
|
// Syarat yang dikenali data (need.type). read = nilai sekarang dari Meta.save; brief = tujuan satu kalimat (l = sisa, n = target);
|
||||||
|
// hint = cara mencapainya (opsional); avail = false bila fitur pendukungnya tidak ada (tahap itu dilewati).
|
||||||
|
const GOALS = {
|
||||||
|
wins: { read: s => num(s.stats?.wins), brief: l => `Menang ${l} kali lagi` },
|
||||||
|
matches: { read: s => num(s.stats?.wins) + num(s.stats?.losses), brief: l => `Main ${l} pertandingan lagi` },
|
||||||
|
perfects: { read: s => num(s.stats?.perfects), brief: l => `Menang bersih ${l} kali lagi`, hint: 'Menang tanpa kehilangan satu ronde pun.' },
|
||||||
|
combo: { read: s => num(s.stats?.bestCombo), brief: (l, n) => `Capai kombo ${n} pukulan` },
|
||||||
|
survival: { read: s => num(s.best?.survival), brief: (l, n) => `Menang ${n}x beruntun (Survival)`, hint: 'Yang dihitung rekor beruntun terbaikmu di Survival.' },
|
||||||
|
arcade: { read: s => (s.ach?.arcade_clear ? 1 : 0), brief: () => 'Tamatkan mode Arcade' },
|
||||||
|
ach: { read: s => count(s.ach, k => k !== ACH_ALL), brief: l => `Buka ${l} pencapaian lagi` },
|
||||||
|
eggs: { read: s => count(s.eggs, k => !/^(code|dialog):/.test(k)), brief: l => `Temukan ${l} kejutan lagi`, hint: 'Coba ketik, ketuk, atau klik sesuatu yang tidak biasa.' },
|
||||||
|
pairs: { read: s => count(s.eggs, k => k.startsWith('dialog:')), brief: l => `Temukan ${l} dialog rahasia lagi`, hint: 'Pertemukan pasangan lawan tertentu di mode Versus.', avail: () => Object.keys(window.DIALOGS?.rahasia || {}).length > 0 },
|
||||||
|
shop: { read: s => Math.max(0, len(s.owned?.skin) - 1) + len(s.owned?.move) + len(s.owned?.alt), brief: l => `Beli ${l} barang toko lagi`, hint: 'Skin, jurus toko, atau varian skill.' },
|
||||||
|
coins: { read: s => num(s.earned), brief: l => `Kumpulkan ${l} koin lagi` }
|
||||||
|
};
|
||||||
|
const need = st => Math.max(1, Math.floor(num(st.need.n)));
|
||||||
|
const hex = c => (/^#[0-9a-f]{6}$/i.test(c || '') ? c : FALLBACK_COLOR);
|
||||||
|
|
||||||
|
// Keadaan satu slot: stage = jumlah petunjuk terbuka (berurutan), next = syarat petunjuk berikutnya (null bila semua terbuka).
|
||||||
|
function compute(i, s) {
|
||||||
|
const d = window.MYSTERY[i];
|
||||||
|
if (!d || !Array.isArray(d.stages)) return null;
|
||||||
|
const stages = d.stages.filter(st => st && st.need && GOALS[st.need.type] && (!GOALS[st.need.type].avail || GOALS[st.need.type].avail()));
|
||||||
|
if (!stages.length) return null;
|
||||||
|
let open = 0;
|
||||||
|
while (open < stages.length && GOALS[stages[open].need.type].read(s) >= need(stages[open])) open++;
|
||||||
|
if (PREVIEW !== null) open = Math.min(PREVIEW, stages.length);
|
||||||
|
const nx = stages[open], g = nx && GOALS[nx.need.type], n = nx ? need(nx) : 0, cur = nx ? Math.min(n, g.read(s)) : 0;
|
||||||
|
return { i, id: String(d.id || 'm' + i), color: hex(d.color), shape: d.shape, max: stages.length, stage: open, clues: stages.slice(0, open),
|
||||||
|
next: nx ? { n, cur, brief: g.brief(n - cur, n, cur), hint: g.hint || '', pct: Math.round(cur / n * 100) } : null };
|
||||||
|
}
|
||||||
|
// Petak yang punya data saja; locked = jumlah petak terkunci di roster (menu.js) supaya "N dari M" sesuai yang tampil.
|
||||||
|
function stateOf(id, locked) {
|
||||||
|
const i = slotIndex(id), cap = Math.min(locked == null ? Infinity : locked, enabled() ? window.MYSTERY.length : 0);
|
||||||
|
if (i < 0 || i >= cap) return null;
|
||||||
|
const st = compute(i, save());
|
||||||
|
if (st) { st.n = i + 1; st.total = cap; }
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
function states(locked) {
|
||||||
|
const cap = Math.min(locked == null ? Infinity : locked, enabled() ? window.MYSTERY.length : 0), s = save();
|
||||||
|
return Array.from({ length: cap }, (_, i) => { const st = compute(i, s); if (st) { st.n = i + 1; st.total = cap; } return st; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- siluet: tubuh dari locked.svg + satu benda per slot (muncul samar di tahap kedua terakhir, jelas di tahap akhir) ----
|
||||||
|
const BODY = 'm50 58 9-27 18-12 20 8 17 24-6 7 2 24-12 15-1 13 28 12 12 13 13 45H10l13-45 12-13 28-12-1-13-12-15-2-24Z';
|
||||||
|
const RING = 'fill="none" stroke="currentColor" stroke-width="3.2"';
|
||||||
|
const SHAPES = {
|
||||||
|
palu: '<path d="M114.6 107.5 109.4 104.5 135.4 58.5 140.6 61.5Z"/><path d="M154 58 146 72 122 58 130 44Z"/><circle cx="111" cy="107" r="7"/>',
|
||||||
|
gunting: `<path d="M126.8 105 121.2 103 147 33.7 149 34.3Z"/><path d="M145.2 105 150.8 103 125 33.7 123 34.3Z"/><circle cx="120" cy="111" r="6.5" ${RING}/><circle cx="152" cy="111" r="6.5" ${RING}/>`,
|
||||||
|
pengeras: '<path d="M112 68 154 44 154 100 112 80Z"/><path d="M104 69 112 68 112 80 104 79Z"/><path d="M122 86 132 90 128 106 119 103Z"/>',
|
||||||
|
helm: '<path d="M42 48A39 33 0 0 1 120 48Z"/><path d="M34 48H128L126 56H36Z"/><path d="M77 14H85V30H77Z"/>',
|
||||||
|
ponsel: '<rect x="118" y="46" width="30" height="54" rx="5"/><path d="M124 38Q133 30 142 38M118 32Q133 20 148 32" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>'
|
||||||
|
};
|
||||||
|
const VARIANTS = [[1, 1], [-1, .95], [1, 1], [-1, 1.04], [1, .93]]; // [arah, lebar] supaya tiap slot punya potongan tubuh berbeda
|
||||||
|
const GLYPHS = ['?', '??', '?!', '!?', '!'];
|
||||||
|
const glyph = st => GLYPHS[st.stage >= st.max ? 4 : Math.min(3, Math.round(st.stage / st.max * 4))];
|
||||||
|
function sil(st) {
|
||||||
|
const [dir, w] = VARIANTS[st.i % VARIANTS.length], cls = st.stage >= st.max ? 'ms-f' : st.stage === st.max - 1 ? 'ms-g' : '';
|
||||||
|
return `<svg class="ms-svg ${cls}" viewBox="0 0 160 180" preserveAspectRatio="xMidYMax meet" aria-hidden="true" focusable="false"><g transform="translate(80 0) scale(${(dir * w).toFixed(2)} 1) translate(-80 0)"><path class="ms-body" d="${BODY}"/><g class="ms-acc">${SHAPES[st.shape] || ''}</g></g></svg>`;
|
||||||
|
}
|
||||||
|
// --ms-p (0..1) = tahap/jumlah tahap: CSS memakainya untuk blur, kekuatan garis tepi, dan kecerahan tanda tanya.
|
||||||
|
const varMap = st => ({ '--ms-c': st.color, '--ms-a': st.color + '44', '--ms-b': st.color + '00', '--ms-p': (st.max ? st.stage / st.max : 0).toFixed(3) });
|
||||||
|
const vars = st => Object.entries(varMap(st)).map(([k, v]) => k + ':' + v).join(';');
|
||||||
|
|
||||||
|
// ---- petunjuk yang sudah dilihat (titik BARU dan toast) ----
|
||||||
|
let seenCache = null;
|
||||||
|
function seenMap() { if (!seenCache) { try { const v = JSON.parse(localStorage.getItem(SEEN_KEY) || '{}'); seenCache = v && typeof v === 'object' ? v : {}; } catch (_) { seenCache = {}; } } return seenCache; }
|
||||||
|
function seenSave() { try { localStorage.setItem(SEEN_KEY, JSON.stringify(seenCache || {})); } catch (_) {} }
|
||||||
|
// s = tahap yang sudah dilihat di panel; t = tahap yang sudah diberitahukan lewat toast. Keduanya turun ikut bila simpanan di-reset.
|
||||||
|
function see(st) { if (PREVIEW !== null) return; const m = seenMap(), e = m[st.id]; if (e && e.s === st.stage && e.t === st.stage) return; m[st.id] = { s: st.stage, t: st.stage }; seenSave(); }
|
||||||
|
function announce(list) {
|
||||||
|
if (PREVIEW !== null) return;
|
||||||
|
const m = seenMap(); let fresh = 0, changed = false;
|
||||||
|
for (const st of list) {
|
||||||
|
if (!st) continue;
|
||||||
|
const e = m[st.id] || (m[st.id] = { s: 0, t: 0 });
|
||||||
|
if (st.stage > e.t) fresh += st.stage - e.t;
|
||||||
|
if (e.t !== st.stage) { e.t = st.stage; changed = true; }
|
||||||
|
}
|
||||||
|
if (changed) seenSave();
|
||||||
|
if (fresh) window.Meta?.toast?.(fresh === 1 ? 'Petunjuk baru terbuka di slot misteri!' : fresh + ' petunjuk baru terbuka di slot misteri!');
|
||||||
|
}
|
||||||
|
function evaluate(list) {
|
||||||
|
if (PREVIEW !== null) return;
|
||||||
|
const live = list.filter(Boolean);
|
||||||
|
if (live.length && live.every(st => st.stage >= st.max)) window.Meta?.unlock?.(ACH_ALL);
|
||||||
|
}
|
||||||
|
function overview(locked) {
|
||||||
|
const live = states(locked).filter(Boolean);
|
||||||
|
if (!live.length) return '';
|
||||||
|
return `${live.length} slot misteri · ${live.reduce((a, st) => a + st.stage, 0)} dari ${live.reduce((a, st) => a + st.max, 0)} petunjuk terbuka`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- tampilan ----
|
||||||
|
// Petak roster: img tetap di alur (hanya disembunyikan lewat CSS) sehingga ukuran grid tidak berubah.
|
||||||
|
function decorateTile(tile, st, fresh) {
|
||||||
|
const lv = st.stage, done = lv >= st.max;
|
||||||
|
tile.classList.add('ms-tile'); tile.classList.toggle('ms-on', lv > 0); tile.classList.toggle('ms-done', done);
|
||||||
|
for (const [k, v] of Object.entries(varMap(st))) tile.style.setProperty(k, v);
|
||||||
|
tile.setAttribute('aria-label', `Slot misteri ${st.n} dari ${st.total}, petunjuk terbuka ${lv} dari ${st.max}, terkunci`);
|
||||||
|
const badge = tile.querySelector(':scope > b'), label = tile.querySelector(':scope > span');
|
||||||
|
if (badge) badge.textContent = lv + '/' + st.max;
|
||||||
|
if (label) label.textContent = done ? 'LENGKAP' : lv ? 'MISTERI' : 'LOCKED';
|
||||||
|
tile.querySelectorAll('.ms-sil,.ms-q,.ms-new').forEach(e => e.remove());
|
||||||
|
if (lv) tile.querySelector('img')?.insertAdjacentHTML('afterend', `<i class="ms-sil" aria-hidden="true">${sil(st)}</i><i class="ms-q" aria-hidden="true">${glyph(st)}</i>`);
|
||||||
|
if (fresh) tile.insertAdjacentHTML('beforeend', '<i class="ms-new" aria-hidden="true"></i>');
|
||||||
|
}
|
||||||
|
// Panel pratinjau slot yang disorot (menggantikan fighterPanel untuk petak terkunci); '' bila id bukan slot misteri.
|
||||||
|
function panel(id, side, locked) {
|
||||||
|
try {
|
||||||
|
const st = stateOf(id, locked); if (!st) return '';
|
||||||
|
const art = st.stage ? `${sil(st)}<b class="ms-glyph" aria-hidden="true">${glyph(st)}</b>` : '<img src="assets/menu/locked.svg" alt="">';
|
||||||
|
const dots = Array.from({ length: st.max }, (_, k) => `<i${k < st.stage ? ' class="on"' : ''}></i>`).join('');
|
||||||
|
const clues = st.clues.length ? st.clues.map(c => `<li><b>${esc(c.tag || 'PETUNJUK')}</b>${esc(c.text)}</li>`).join('') : '<li class="ms-empty">Belum ada petunjuk. Siapa dia?</li>';
|
||||||
|
const nx = st.next;
|
||||||
|
const next = nx ? `<small>PETUNJUK BERIKUTNYA</small><strong>${esc(nx.brief)}</strong>${nx.hint ? `<em>${esc(nx.hint)}</em>` : ''}<span class="ms-prog"><i><u style="width:${nx.pct}%"></u></i><b>${nx.cur}/${nx.n}</b></span>` : '<small>SEMUA PETUNJUK TERBUKA</small><strong>Tinggal menunggu giliran tampil.</strong>';
|
||||||
|
return `<div class="fighter-plinth ${side} mystery" data-ms-lv="${st.stage}" style="${vars(st)}"><div class="plinth-light"></div><span class="side-tag">${side === 'player' ? 'P1 / YOUR FIGHTER' : 'CPU / YOUR RIVAL'}</span><div class="ms-art">${art}</div><div class="ms-card"><div class="ms-head"><span>SLOT MISTERI ${st.n} / ${st.total}</span><span class="ms-dots" role="img" aria-label="Petunjuk terbuka ${st.stage} dari ${st.max}">${dots}</span></div><h2 class="ms-title">???</h2><ol class="ms-clues">${clues}</ol><div class="ms-next">${next}</div></div></div>`;
|
||||||
|
} catch (e) { console.warn('[mystery]', e); return ''; }
|
||||||
|
}
|
||||||
|
// Dipanggil menu.js tiap selesai render layar pilih karakter.
|
||||||
|
function decorate(root, screen, hover, locked) {
|
||||||
|
if (screen !== 'select' || !enabled()) return;
|
||||||
|
try {
|
||||||
|
const list = states(locked), seen = seenMap();
|
||||||
|
root.querySelectorAll('.roster-tile.locked[data-fighter^="locked-"]').forEach(tile => {
|
||||||
|
const st = list[slotIndex(tile.dataset.fighter)];
|
||||||
|
if (st) decorateTile(tile, st, PREVIEW === null && st.stage > (seen[st.id]?.s || 0));
|
||||||
|
});
|
||||||
|
announce(list); evaluate(list); preview(root, hover, locked);
|
||||||
|
} catch (e) { console.warn('[mystery]', e); }
|
||||||
|
}
|
||||||
|
// Dipanggil menu.js saat sorotan berpindah: teks #fighter-moves dan #roster-notice; slot yang disorot dianggap sudah dilihat.
|
||||||
|
function preview(root, hover, locked) {
|
||||||
|
if (!enabled()) return;
|
||||||
|
try {
|
||||||
|
const st = stateOf(hover, locked), moves = root.querySelector('#fighter-moves'), note = root.querySelector('#roster-notice');
|
||||||
|
if (moves) {
|
||||||
|
moves.classList.toggle('ms-moves', !!st);
|
||||||
|
if (st) moves.innerHTML = `<span>Slot misteri ${st.n} / ${st.total} · petunjuk ${st.stage} / ${st.max}</span><b>${esc(st.next ? st.next.brief : 'Semua petunjuk terbuka')}</b>`;
|
||||||
|
}
|
||||||
|
if (st) { see(st); root.querySelector(`[data-fighter="${hover}"] .ms-new`)?.remove(); }
|
||||||
|
if (note) note.textContent = st ? `Slot misteri ${st.n}/${st.total} · ${st.next ? st.next.brief : 'petunjuk lengkap'}` : overview(locked);
|
||||||
|
} catch (e) { console.warn('[mystery]', e); }
|
||||||
|
}
|
||||||
|
// Pesan saat slot terkunci ditekan/dikonfirmasi; '' bila bukan slot misteri (menu.js memakai pesan lamanya).
|
||||||
|
function notice(id, locked) {
|
||||||
|
try { const st = stateOf(id, locked); return st ? `Slot misteri ${st.n}/${st.total} · ${st.next ? st.next.brief : 'petunjuk lengkap'}` : ''; } catch (_) { return ''; }
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Mystery = {
|
||||||
|
panel, decorate, preview, notice,
|
||||||
|
overview: locked => { try { return enabled() ? overview(locked) : ''; } catch (_) { return ''; } },
|
||||||
|
// Ringkasan untuk uji manual di konsol: Mystery.states()
|
||||||
|
states: locked => { try { return states(locked).filter(Boolean).map(st => ({ slot: st.n, id: st.id, stage: st.stage, max: st.max, next: st.next && st.next.brief })); } catch (_) { return []; } }
|
||||||
|
};
|
||||||
|
})();
|
||||||
Loading…
Reference in New Issue